@abloatai/transaction 0.44.0 → 0.45.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai-sdk/modelTools.d.ts +2 -4
- package/dist/ai-sdk/modelTools.d.ts.map +1 -1
- package/dist/ai-sdk/modelTools.js.map +1 -1
- package/dist/ai-sdk/updateTool.d.ts +2 -4
- package/dist/ai-sdk/updateTool.d.ts.map +1 -1
- package/dist/ai-sdk/updateTool.js.map +1 -1
- package/dist/branches.d.ts +4 -4
- package/dist/coordination/awaitClaimGrant.d.ts +10 -0
- package/dist/coordination/awaitClaimGrant.d.ts.map +1 -1
- package/dist/coordination/awaitClaimGrant.js +58 -45
- package/dist/coordination/awaitClaimGrant.js.map +1 -1
- package/dist/resources/httpResources.d.ts +4 -4
- package/dist/resources/httpResources.d.ts.map +1 -1
- package/dist/resources/modelOperations.d.ts +113 -21
- package/dist/resources/modelOperations.d.ts.map +1 -1
- package/dist/resources/modelOperations.js +43 -1
- package/dist/resources/modelOperations.js.map +1 -1
- package/dist/transport/httpTransport.d.ts.map +1 -1
- package/dist/transport/httpTransport.js +49 -15
- package/dist/transport/httpTransport.js.map +1 -1
- package/package.json +1 -1
- package/src/ai-sdk/modelTools.ts +2 -1
- package/src/ai-sdk/updateTool.ts +2 -1
- package/src/coordination/awaitClaimGrant.ts +86 -74
- package/src/resources/httpResources.ts +5 -1
- package/src/resources/modelOperations.ts +169 -19
- package/src/transport/httpTransport.ts +73 -19
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
import type { ModelScope } from '../types/index.js';
|
|
14
14
|
import type { ResolveClaimMeta } from '../types/global.js';
|
|
15
|
-
import type {
|
|
15
|
+
import type { AbloError } from '../errors.js';
|
|
16
16
|
import type { StaleNotification, TrackDependency } from '../coordination/schema.js';
|
|
17
17
|
import type { FieldRef, FieldSelector } from '../schema/fieldRef.js';
|
|
18
18
|
import type { BaseModelFields } from '../schema/schema.js';
|
|
@@ -29,6 +29,112 @@ import type {
|
|
|
29
29
|
import type { MutationOptions } from './mutationOptions.js';
|
|
30
30
|
import type { LoadWhere } from './where.js';
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* One authoritative status transition for a claim attempt. This is scoped
|
|
34
|
+
* to the request that supplied the callback, unlike `claim.state` / `queue`,
|
|
35
|
+
* which are reactive snapshots of everyone on the row.
|
|
36
|
+
*/
|
|
37
|
+
export type ClaimAttemptEvent =
|
|
38
|
+
| {
|
|
39
|
+
readonly type: 'queued';
|
|
40
|
+
readonly claimId: string;
|
|
41
|
+
/** Zero-based place in line (`0` means next behind the holder). */
|
|
42
|
+
readonly position: number;
|
|
43
|
+
/** Human-readable count of waiters ahead, including the current holder. */
|
|
44
|
+
readonly ahead: number;
|
|
45
|
+
}
|
|
46
|
+
| {
|
|
47
|
+
readonly type: 'granted';
|
|
48
|
+
readonly claimId: string;
|
|
49
|
+
/** True when this request waited in line before it was granted. */
|
|
50
|
+
readonly waited: boolean;
|
|
51
|
+
}
|
|
52
|
+
| {
|
|
53
|
+
readonly type: 'skipped';
|
|
54
|
+
/** The typed contention error behind the expected `null` result. */
|
|
55
|
+
readonly error: AbloError;
|
|
56
|
+
}
|
|
57
|
+
| {
|
|
58
|
+
readonly type: 'failed';
|
|
59
|
+
/** The same typed Ablo error the claim promise rejects with. */
|
|
60
|
+
readonly error: AbloError;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export interface ClaimContentionOptions {
|
|
64
|
+
/**
|
|
65
|
+
* `wait` (default) joins the FIFO line. `skip` resolves the model try-claim
|
|
66
|
+
* as `null` when another participant holds the target.
|
|
67
|
+
*/
|
|
68
|
+
readonly mode?: 'wait' | 'skip';
|
|
69
|
+
/** Fail instead of joining at or beyond this zero-based queue depth. */
|
|
70
|
+
readonly maxDepth?: number;
|
|
71
|
+
/** Maximum queue wait in milliseconds. */
|
|
72
|
+
readonly timeoutMs?: number;
|
|
73
|
+
/** Abort the pending wait. Ignored after the grant. */
|
|
74
|
+
readonly signal?: AbortSignal;
|
|
75
|
+
/**
|
|
76
|
+
* Request-scoped attempt statuses. The callback is observational:
|
|
77
|
+
* throwing from it never changes whether the claim is granted or skipped.
|
|
78
|
+
*/
|
|
79
|
+
readonly onStatus?: (event: ClaimAttemptEvent) => void;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface ResolvedClaimContentionOptions {
|
|
83
|
+
readonly wait: boolean;
|
|
84
|
+
readonly maxDepth?: number;
|
|
85
|
+
readonly timeoutMs?: number;
|
|
86
|
+
readonly signal?: AbortSignal;
|
|
87
|
+
readonly onStatus?: (event: ClaimAttemptEvent) => void;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** One compatibility boundary for structured contention and legacy queue options. */
|
|
91
|
+
export function resolveClaimContentionOptions(options: {
|
|
92
|
+
readonly queue?: boolean;
|
|
93
|
+
readonly contention?: ClaimContentionOptions;
|
|
94
|
+
readonly maxQueueDepth?: number;
|
|
95
|
+
readonly waitTimeoutMs?: number;
|
|
96
|
+
readonly signal?: AbortSignal;
|
|
97
|
+
}): ResolvedClaimContentionOptions {
|
|
98
|
+
const structured = options.contention;
|
|
99
|
+
const maxDepth = structured?.maxDepth ?? options.maxQueueDepth;
|
|
100
|
+
const timeoutMs = structured?.timeoutMs ?? options.waitTimeoutMs;
|
|
101
|
+
const signal = structured?.signal ?? options.signal;
|
|
102
|
+
return {
|
|
103
|
+
wait: structured
|
|
104
|
+
? structured.mode !== 'skip'
|
|
105
|
+
: options.queue !== false,
|
|
106
|
+
...(maxDepth !== undefined ? { maxDepth } : {}),
|
|
107
|
+
...(timeoutMs !== undefined ? { timeoutMs } : {}),
|
|
108
|
+
...(signal !== undefined ? { signal } : {}),
|
|
109
|
+
...(structured?.onStatus !== undefined
|
|
110
|
+
? { onStatus: structured.onStatus }
|
|
111
|
+
: {}),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Emit a status without letting an observer alter the claim attempt. */
|
|
116
|
+
export function emitClaimStatus(
|
|
117
|
+
listener: ((event: ClaimAttemptEvent) => void) | undefined,
|
|
118
|
+
event: ClaimAttemptEvent,
|
|
119
|
+
): void {
|
|
120
|
+
try {
|
|
121
|
+
listener?.(event);
|
|
122
|
+
} catch {
|
|
123
|
+
// The claim attempt is authoritative; telemetry/UI callbacks are not.
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Separate expected skipped work from an actual failed claim attempt. */
|
|
128
|
+
export function claimAttemptFailure(
|
|
129
|
+
wait: boolean,
|
|
130
|
+
error: AbloError,
|
|
131
|
+
): ClaimAttemptEvent {
|
|
132
|
+
const code = error.code;
|
|
133
|
+
return !wait && (code === 'claim_conflict' || code === 'entity_claimed')
|
|
134
|
+
? { type: 'skipped', error }
|
|
135
|
+
: { type: 'failed', error };
|
|
136
|
+
}
|
|
137
|
+
|
|
32
138
|
/**
|
|
33
139
|
* A lifecycle filter, accepted either as the enum or as its bare string. The
|
|
34
140
|
* string arm is a template projection of the enum rather than a second list, so
|
|
@@ -142,8 +248,7 @@ export type ClaimField<T> = FieldRef<
|
|
|
142
248
|
* - **what you claim** — `fields`, selected from the model's Zod shape;
|
|
143
249
|
* narrowed below the row;
|
|
144
250
|
* - **what others see** — `description` / `meta`, the presence half;
|
|
145
|
-
* - **how you wait** — `queue
|
|
146
|
-
* `signal`, admission to the line;
|
|
251
|
+
* - **how you wait** — structured `queue`, admission to the line;
|
|
147
252
|
* - **how long you hold** — `ttl` / `heartbeat`, the lease.
|
|
148
253
|
*/
|
|
149
254
|
export interface ClaimTargetOptions<T = Record<string, unknown>> {
|
|
@@ -175,18 +280,20 @@ export interface ClaimTargetOptions<T = Record<string, unknown>> {
|
|
|
175
280
|
*/
|
|
176
281
|
meta?: ResolveClaimMeta;
|
|
177
282
|
|
|
178
|
-
// ── How you
|
|
283
|
+
// ── How you handle contention ──────────────────────────────────────────
|
|
179
284
|
|
|
180
285
|
/**
|
|
181
|
-
* Behavior under contention.
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
* {
|
|
185
|
-
* distributed work ("if someone else has this job, skip it"), where waiting
|
|
186
|
-
* would mean double-processing.
|
|
286
|
+
* Behavior under contention. Prefer the structured form so the decision,
|
|
287
|
+
* bounds, cancellation, and request-scoped notifications stay together:
|
|
288
|
+
* `contention: { mode: 'wait', maxDepth, timeoutMs, signal, onStatus }`.
|
|
289
|
+
* `{ mode: 'skip' }` is claim-or-skip dedup.
|
|
187
290
|
*/
|
|
291
|
+
contention?: ClaimContentionOptions;
|
|
292
|
+
/** Concise compatibility shorthand: `true` waits and `false` skips. */
|
|
188
293
|
queue?: boolean;
|
|
189
294
|
/**
|
|
295
|
+
* @deprecated Prefer `contention: { maxDepth }`.
|
|
296
|
+
*
|
|
190
297
|
* Backpressure: queue, but not behind too many others. If the server reports a
|
|
191
298
|
* position at or beyond `maxQueueDepth` when the client joins the line, it
|
|
192
299
|
* rejects with {@link AbloClaimedError} (`queue_too_deep`) instead of waiting.
|
|
@@ -194,6 +301,8 @@ export interface ClaimTargetOptions<T = Record<string, unknown>> {
|
|
|
194
301
|
*/
|
|
195
302
|
maxQueueDepth?: number;
|
|
196
303
|
/**
|
|
304
|
+
* @deprecated Prefer `contention: { timeoutMs }`.
|
|
305
|
+
*
|
|
197
306
|
* Cap on how long a queued claim waits for its grant before rejecting with
|
|
198
307
|
* {@link AbloClaimedError} (`grant_timeout`). Omit to wait as long as the
|
|
199
308
|
* line takes. Same meaning on both transports; on the stateless HTTP client
|
|
@@ -201,6 +310,8 @@ export interface ClaimTargetOptions<T = Record<string, unknown>> {
|
|
|
201
310
|
*/
|
|
202
311
|
waitTimeoutMs?: number;
|
|
203
312
|
/**
|
|
313
|
+
* @deprecated Prefer `contention: { signal }`.
|
|
314
|
+
*
|
|
204
315
|
* Abort a pending wait from outside — the same signal that cancels
|
|
205
316
|
* everything else in the program, so a cancelled agent task or an unmounted
|
|
206
317
|
* component takes its queued claim with it. Rejects with
|
|
@@ -234,6 +345,14 @@ export interface ClaimParams<T = Record<string, unknown>>
|
|
|
234
345
|
readonly id: string;
|
|
235
346
|
}
|
|
236
347
|
|
|
348
|
+
/** The two fail-fast spellings, kept as one overload discriminator. */
|
|
349
|
+
export type ClaimSkipParams<T = Record<string, unknown>> =
|
|
350
|
+
ClaimParams<T> & {
|
|
351
|
+
readonly queue: false;
|
|
352
|
+
} | ClaimParams<T> & {
|
|
353
|
+
readonly contention: ClaimContentionOptions & { readonly mode: 'skip' };
|
|
354
|
+
};
|
|
355
|
+
|
|
237
356
|
export interface ClaimLookupParams<T = Record<string, unknown>> {
|
|
238
357
|
readonly id: string;
|
|
239
358
|
}
|
|
@@ -243,6 +362,32 @@ export interface ClaimReorderParams<T = Record<string, unknown>>
|
|
|
243
362
|
readonly order: readonly Claim[];
|
|
244
363
|
}
|
|
245
364
|
|
|
365
|
+
/**
|
|
366
|
+
* One wait-line snapshot. `data` preserves the standard list-envelope shape;
|
|
367
|
+
* the named aliases make coordination code read without unpacking conventions.
|
|
368
|
+
*/
|
|
369
|
+
export interface ClaimQueueView<M = ResolveClaimMeta> {
|
|
370
|
+
readonly object: 'list';
|
|
371
|
+
readonly data: readonly Claim<Record<string, unknown>, M>[];
|
|
372
|
+
/** The same ordered array as `data`, named for what it contains. */
|
|
373
|
+
readonly waiting: readonly Claim<Record<string, unknown>, M>[];
|
|
374
|
+
readonly size: number;
|
|
375
|
+
/** The next participant to receive the lease, or `null` when the line is empty. */
|
|
376
|
+
readonly next: Claim<Record<string, unknown>, M> | null;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
export function claimQueueView<M = ResolveClaimMeta>(
|
|
380
|
+
waiting: readonly Claim<Record<string, unknown>, M>[],
|
|
381
|
+
): ClaimQueueView<M> {
|
|
382
|
+
return {
|
|
383
|
+
object: 'list',
|
|
384
|
+
data: waiting,
|
|
385
|
+
waiting,
|
|
386
|
+
size: waiting.length,
|
|
387
|
+
next: waiting[0] ?? null,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
246
391
|
/**
|
|
247
392
|
* A claim handle: the held entity data plus an explicit release hook.
|
|
248
393
|
*
|
|
@@ -272,6 +417,13 @@ export type { Claim, ClaimHeartbeat, ClaimHeartbeatOptions, HeldClaim, HeldLease
|
|
|
272
417
|
|
|
273
418
|
export type ClaimOptions<T = Record<string, unknown>> = ClaimTargetOptions<T>;
|
|
274
419
|
|
|
420
|
+
export type ClaimSkipOptions<T = Record<string, unknown>> =
|
|
421
|
+
ClaimOptions<T> & {
|
|
422
|
+
readonly queue: false;
|
|
423
|
+
} | ClaimOptions<T> & {
|
|
424
|
+
readonly contention: ClaimContentionOptions & { readonly mode: 'skip' };
|
|
425
|
+
};
|
|
426
|
+
|
|
275
427
|
/**
|
|
276
428
|
* The coordination surface for a model, exposed as a callable namespace.
|
|
277
429
|
*
|
|
@@ -346,10 +498,7 @@ export interface ClaimReadApi<T = Record<string, unknown>> {
|
|
|
346
498
|
*/
|
|
347
499
|
queue<M = ResolveClaimMeta>(
|
|
348
500
|
params: ClaimLookupParams<T>,
|
|
349
|
-
):
|
|
350
|
-
readonly object: 'list';
|
|
351
|
-
readonly data: readonly Claim<Record<string, unknown>, M>[];
|
|
352
|
-
};
|
|
501
|
+
): ClaimQueueView<M>;
|
|
353
502
|
|
|
354
503
|
/**
|
|
355
504
|
* Re-rank the wait line. Advanced and permission-gated.
|
|
@@ -376,14 +525,15 @@ export interface ClaimApi<
|
|
|
376
525
|
Fields = T,
|
|
377
526
|
> extends ClaimReadApi<T> {
|
|
378
527
|
/**
|
|
379
|
-
* The try-claim: `
|
|
380
|
-
* not an error — it resolves
|
|
528
|
+
* The try-claim: `contention: { mode: 'skip' }` (or `queue: false`)
|
|
529
|
+
* treats a held target as an expected outcome, not an error — it resolves
|
|
530
|
+
* `null`, so claim-or-skip dedup reads
|
|
381
531
|
* `if (!claim) return` with no try/catch. Who holds it, and why, stays
|
|
382
532
|
* readable through `claim.state({ id })`. (A write to a row someone else
|
|
383
533
|
* holds still rejects with `entity_claimed` — a failed write is an error;
|
|
384
|
-
* a
|
|
534
|
+
* a skipped try is not.)
|
|
385
535
|
*/
|
|
386
|
-
(params:
|
|
536
|
+
(params: ClaimSkipParams<Fields>): Promise<HeldClaim<T> | null>;
|
|
387
537
|
/**
|
|
388
538
|
* Takes a claim and returns an explicit held-work handle — a {@link HeldClaim}.
|
|
389
539
|
* `data`, `release`, `revoke`, and the async disposer are always present (this
|
|
@@ -392,7 +542,7 @@ export interface ClaimApi<
|
|
|
392
542
|
*/
|
|
393
543
|
(params: ClaimParams<Fields>): Promise<HeldClaim<T>>;
|
|
394
544
|
/** The row-free try-claim — `null` when the key is already held. */
|
|
395
|
-
(id: string, opts:
|
|
545
|
+
(id: string, opts: ClaimSkipOptions<Fields>): Promise<HeldLease | null>;
|
|
396
546
|
/**
|
|
397
547
|
* Takes a claim by id alone, for a row that lives only in the customer's own
|
|
398
548
|
* database — Ablo has never seen it, so there is nothing to re-read. Returns a
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import {
|
|
10
10
|
AbloClaimedError,
|
|
11
|
+
AbloError,
|
|
11
12
|
AbloAuthenticationError,
|
|
12
13
|
AbloConnectionError,
|
|
13
14
|
AbloIdempotencyError,
|
|
@@ -113,10 +114,19 @@ import type {
|
|
|
113
114
|
ClaimLookupParams,
|
|
114
115
|
ClaimOptions,
|
|
115
116
|
ClaimParams,
|
|
117
|
+
ClaimSkipParams,
|
|
116
118
|
ClaimReorderParams,
|
|
117
119
|
ModelTrackParams,
|
|
118
120
|
ModelTrackResult,
|
|
119
121
|
ServerReadOptions,
|
|
122
|
+
ResolvedClaimContentionOptions,
|
|
123
|
+
ClaimQueueView,
|
|
124
|
+
} from '../resources/modelOperations.js';
|
|
125
|
+
import {
|
|
126
|
+
claimAttemptFailure,
|
|
127
|
+
claimQueueView,
|
|
128
|
+
emitClaimStatus,
|
|
129
|
+
resolveClaimContentionOptions,
|
|
120
130
|
} from '../resources/modelOperations.js';
|
|
121
131
|
import type { Duration } from '../utils/duration.js';
|
|
122
132
|
import type { TrackDependency } from '../coordination/schema.js';
|
|
@@ -1148,7 +1158,7 @@ export function createHttpTransport(options: HttpTransportOptions): HttpTranspor
|
|
|
1148
1158
|
async function awaitGrantOverHttp(
|
|
1149
1159
|
targetLabel: string,
|
|
1150
1160
|
queued: ClaimQueuedResponse,
|
|
1151
|
-
options:
|
|
1161
|
+
options: ResolvedClaimContentionOptions,
|
|
1152
1162
|
): Promise<{ id: string; fenceToken?: number }> {
|
|
1153
1163
|
// The queued reply is a claim resource in its waiting state, so the
|
|
1154
1164
|
// handle is its `id` — same rule as the 201 and the poll.
|
|
@@ -1161,17 +1171,24 @@ export function createHttpTransport(options: HttpTransportOptions): HttpTranspor
|
|
|
1161
1171
|
throw error;
|
|
1162
1172
|
};
|
|
1163
1173
|
|
|
1164
|
-
|
|
1174
|
+
emitClaimStatus(options.onStatus, {
|
|
1175
|
+
type: 'queued',
|
|
1176
|
+
claimId,
|
|
1177
|
+
position: queued.position,
|
|
1178
|
+
ahead: queued.position + 1,
|
|
1179
|
+
});
|
|
1180
|
+
|
|
1181
|
+
if (options.maxDepth !== undefined && queued.position >= options.maxDepth) {
|
|
1165
1182
|
return rejectAndLeave(
|
|
1166
1183
|
new AbloClaimedError(
|
|
1167
|
-
`Claim queue for ${targetLabel} is ${queued.position} deep (max ${options.
|
|
1184
|
+
`Claim queue for ${targetLabel} is ${queued.position} deep (max ${options.maxDepth}).`,
|
|
1168
1185
|
{ code: 'queue_too_deep' }
|
|
1169
1186
|
)
|
|
1170
1187
|
);
|
|
1171
1188
|
}
|
|
1172
1189
|
|
|
1173
1190
|
const deadline =
|
|
1174
|
-
options.
|
|
1191
|
+
options.timeoutMs !== undefined ? Date.now() + options.timeoutMs : undefined;
|
|
1175
1192
|
let delay = GRANT_POLL_FIRST_MS;
|
|
1176
1193
|
for (;;) {
|
|
1177
1194
|
if (signal?.aborted) {
|
|
@@ -1185,7 +1202,7 @@ export function createHttpTransport(options: HttpTransportOptions): HttpTranspor
|
|
|
1185
1202
|
if (deadline !== undefined && Date.now() >= deadline) {
|
|
1186
1203
|
return rejectAndLeave(
|
|
1187
1204
|
new AbloClaimedError(
|
|
1188
|
-
`Timed out after ${options.
|
|
1205
|
+
`Timed out after ${options.timeoutMs}ms waiting for the queue grant on ${targetLabel}.`,
|
|
1189
1206
|
{ code: 'grant_timeout' }
|
|
1190
1207
|
)
|
|
1191
1208
|
);
|
|
@@ -1218,6 +1235,11 @@ export function createHttpTransport(options: HttpTransportOptions): HttpTranspor
|
|
|
1218
1235
|
})
|
|
1219
1236
|
);
|
|
1220
1237
|
}
|
|
1238
|
+
emitClaimStatus(options.onStatus, {
|
|
1239
|
+
type: 'granted',
|
|
1240
|
+
claimId,
|
|
1241
|
+
waited: true,
|
|
1242
|
+
});
|
|
1221
1243
|
return state.fenceToken !== undefined
|
|
1222
1244
|
? { id: claimId, fenceToken: state.fenceToken }
|
|
1223
1245
|
: { id: claimId };
|
|
@@ -1492,6 +1514,7 @@ export function createHttpTransport(options: HttpTransportOptions): HttpTranspor
|
|
|
1492
1514
|
const acquireClaim = async (
|
|
1493
1515
|
params: ClaimParams<Fields>
|
|
1494
1516
|
): Promise<{ id: string; fenceToken?: number }> => {
|
|
1517
|
+
const contention = resolveClaimContentionOptions(params);
|
|
1495
1518
|
// The row is named by the URL, so `target` carries only the narrowing a
|
|
1496
1519
|
// claim adds below it. Sending it is what makes a field-scoped claim
|
|
1497
1520
|
// actually field-scoped: the server's conflict rule reads `path`,
|
|
@@ -1512,13 +1535,25 @@ export function createHttpTransport(options: HttpTransportOptions): HttpTranspor
|
|
|
1512
1535
|
...(Object.keys(narrowing).length > 0 ? { target: narrowing } : {}),
|
|
1513
1536
|
// `queue` (default true) → queue behind the holder; false → fail-fast
|
|
1514
1537
|
// with AbloClaimedError (work-distribution dedup).
|
|
1515
|
-
queue:
|
|
1538
|
+
queue: contention.wait,
|
|
1516
1539
|
};
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1540
|
+
let body: z.infer<typeof claimAcquireResponseSchema>;
|
|
1541
|
+
try {
|
|
1542
|
+
body = await requestJson(
|
|
1543
|
+
claimPath(params.id),
|
|
1544
|
+
{ method: 'POST', body: JSON.stringify(request) },
|
|
1545
|
+
claimAcquireResponseSchema
|
|
1546
|
+
);
|
|
1547
|
+
} catch (error) {
|
|
1548
|
+
const normalized = error instanceof AbloError
|
|
1549
|
+
? error
|
|
1550
|
+
: new AbloConnectionError(String(error));
|
|
1551
|
+
emitClaimStatus(
|
|
1552
|
+
contention.onStatus,
|
|
1553
|
+
claimAttemptFailure(contention.wait, normalized),
|
|
1554
|
+
);
|
|
1555
|
+
throw error;
|
|
1556
|
+
}
|
|
1522
1557
|
// One resource, two states, discriminated by `status`. The queued arm
|
|
1523
1558
|
// WAITS, exactly as the socket client does: `claim({ id })` means
|
|
1524
1559
|
// "serialize me behind the holder" on every transport, and the grant
|
|
@@ -1527,8 +1562,28 @@ export function createHttpTransport(options: HttpTransportOptions): HttpTranspor
|
|
|
1527
1562
|
// why it no longer surfaces as one here. The `claims` namespace remains
|
|
1528
1563
|
// the manual ticket surface.)
|
|
1529
1564
|
if (body.status === 'queued') {
|
|
1530
|
-
|
|
1565
|
+
try {
|
|
1566
|
+
return await awaitGrantOverHttp(
|
|
1567
|
+
`${name}/${params.id}`,
|
|
1568
|
+
body,
|
|
1569
|
+
contention,
|
|
1570
|
+
);
|
|
1571
|
+
} catch (error) {
|
|
1572
|
+
const normalized = error instanceof AbloError
|
|
1573
|
+
? error
|
|
1574
|
+
: new AbloConnectionError(String(error));
|
|
1575
|
+
emitClaimStatus(
|
|
1576
|
+
contention.onStatus,
|
|
1577
|
+
claimAttemptFailure(contention.wait, normalized),
|
|
1578
|
+
);
|
|
1579
|
+
throw error;
|
|
1580
|
+
}
|
|
1531
1581
|
}
|
|
1582
|
+
emitClaimStatus(contention.onStatus, {
|
|
1583
|
+
type: 'granted',
|
|
1584
|
+
claimId: body.id,
|
|
1585
|
+
waited: false,
|
|
1586
|
+
});
|
|
1532
1587
|
// The lease's own fields are mirrored at the top level, the same place
|
|
1533
1588
|
// the poll puts them — one reader for both answers.
|
|
1534
1589
|
return body.fenceToken !== undefined
|
|
@@ -1566,7 +1621,7 @@ export function createHttpTransport(options: HttpTransportOptions): HttpTranspor
|
|
|
1566
1621
|
};
|
|
1567
1622
|
|
|
1568
1623
|
function claimImpl(
|
|
1569
|
-
params:
|
|
1624
|
+
params: ClaimSkipParams<Fields>
|
|
1570
1625
|
): Promise<HeldClaim<T> | null>;
|
|
1571
1626
|
function claimImpl(params: ClaimParams<Fields>): Promise<HeldClaim<T>>;
|
|
1572
1627
|
async function claimImpl(
|
|
@@ -1582,7 +1637,7 @@ export function createHttpTransport(options: HttpTransportOptions): HttpTranspor
|
|
|
1582
1637
|
// and the write-site claim path calls `acquireClaim` directly, so a
|
|
1583
1638
|
// write that could not claim still fails loudly.
|
|
1584
1639
|
if (
|
|
1585
|
-
params.
|
|
1640
|
+
!resolveClaimContentionOptions(params).wait &&
|
|
1586
1641
|
error instanceof AbloClaimedError &&
|
|
1587
1642
|
(error.code === 'entity_claimed' || error.code === 'claim_conflict')
|
|
1588
1643
|
) {
|
|
@@ -1687,14 +1742,13 @@ export function createHttpTransport(options: HttpTransportOptions): HttpTranspor
|
|
|
1687
1742
|
},
|
|
1688
1743
|
queue: async (
|
|
1689
1744
|
params: ClaimLookupParams<T>
|
|
1690
|
-
): Promise<
|
|
1745
|
+
): Promise<ClaimQueueView> => {
|
|
1691
1746
|
const res = await claimsForEntity(params);
|
|
1692
|
-
return
|
|
1693
|
-
|
|
1694
|
-
data: res.data
|
|
1747
|
+
return claimQueueView(
|
|
1748
|
+
res.data
|
|
1695
1749
|
.filter((row) => row.status === 'queued')
|
|
1696
1750
|
.map(claimFromModelClaim),
|
|
1697
|
-
|
|
1751
|
+
);
|
|
1698
1752
|
},
|
|
1699
1753
|
reorder: async (params: ClaimReorderParams<T>): Promise<void> => {
|
|
1700
1754
|
await requestRaw(`${claimPath(params.id)}/reorder`, {
|