@abloatai/transaction 0.44.0 → 0.46.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.
Files changed (32) hide show
  1. package/dist/ai-sdk/modelTools.d.ts +2 -4
  2. package/dist/ai-sdk/modelTools.d.ts.map +1 -1
  3. package/dist/ai-sdk/modelTools.js.map +1 -1
  4. package/dist/ai-sdk/updateTool.d.ts +2 -4
  5. package/dist/ai-sdk/updateTool.d.ts.map +1 -1
  6. package/dist/ai-sdk/updateTool.js.map +1 -1
  7. package/dist/branches.d.ts +4 -4
  8. package/dist/coordination/awaitClaimGrant.d.ts +10 -0
  9. package/dist/coordination/awaitClaimGrant.d.ts.map +1 -1
  10. package/dist/coordination/awaitClaimGrant.js +58 -45
  11. package/dist/coordination/awaitClaimGrant.js.map +1 -1
  12. package/dist/resources/httpResources.d.ts +4 -4
  13. package/dist/resources/httpResources.d.ts.map +1 -1
  14. package/dist/resources/modelOperations.d.ts +113 -21
  15. package/dist/resources/modelOperations.d.ts.map +1 -1
  16. package/dist/resources/modelOperations.js +43 -1
  17. package/dist/resources/modelOperations.js.map +1 -1
  18. package/dist/transport/httpTransport.d.ts.map +1 -1
  19. package/dist/transport/httpTransport.js +49 -15
  20. package/dist/transport/httpTransport.js.map +1 -1
  21. package/dist/wire/dataSourceResponses.d.ts +8 -0
  22. package/dist/wire/dataSourceResponses.d.ts.map +1 -1
  23. package/dist/wire/dataSourceResponses.js +11 -0
  24. package/dist/wire/dataSourceResponses.js.map +1 -1
  25. package/package.json +1 -1
  26. package/src/ai-sdk/modelTools.ts +2 -1
  27. package/src/ai-sdk/updateTool.ts +2 -1
  28. package/src/coordination/awaitClaimGrant.ts +86 -74
  29. package/src/resources/httpResources.ts +5 -1
  30. package/src/resources/modelOperations.ts +169 -19
  31. package/src/transport/httpTransport.ts +73 -19
  32. package/src/wire/dataSourceResponses.ts +11 -0
@@ -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 { AbloClaimedError } from '../errors.js';
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` / `maxQueueDepth` / `waitTimeoutMs` /
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 wait admission to the line ───────────────────────────────
283
+ // ── How you handle contention ──────────────────────────────────────────
179
284
 
180
285
  /**
181
- * Behavior under contention. `true` (the default) queues behind the current
182
- * holder and resolves once the row is yours. `false` is fail-fast: if another
183
- * participant already holds the row, it rejects immediately with
184
- * {@link AbloClaimedError} instead of waiting. Use `false` to deduplicate
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: `queue: false` treats a held target as an expected outcome,
380
- * not an error — it resolves `null`, so claim-or-skip dedup reads
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 declined try is not.)
534
+ * a skipped try is not.)
385
535
  */
386
- (params: ClaimParams<Fields> & { queue: false }): Promise<HeldClaim<T> | null>;
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: ClaimOptions<Fields> & { queue: false }): Promise<HeldLease | null>;
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: { maxQueueDepth?: number; waitTimeoutMs?: number; signal?: AbortSignal }
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
- if (options.maxQueueDepth !== undefined && queued.position >= options.maxQueueDepth) {
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.maxQueueDepth}).`,
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.waitTimeoutMs !== undefined ? Date.now() + options.waitTimeoutMs : undefined;
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.waitTimeoutMs}ms waiting for the queue grant on ${targetLabel}.`,
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: params.queue ?? true,
1538
+ queue: contention.wait,
1516
1539
  };
1517
- const body = await requestJson(
1518
- claimPath(params.id),
1519
- { method: 'POST', body: JSON.stringify(request) },
1520
- claimAcquireResponseSchema
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
- return awaitGrantOverHttp(`${name}/${params.id}`, body, params);
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: ClaimParams<Fields> & { queue: false }
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.queue === false &&
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<{ readonly object: 'list'; readonly data: readonly Claim[] }> => {
1745
+ ): Promise<ClaimQueueView> => {
1691
1746
  const res = await claimsForEntity(params);
1692
- return {
1693
- object: 'list',
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`, {
@@ -136,6 +136,17 @@ export const datasourceValidationResponseSchema = z.object({
136
136
  connection: z.enum(['direct', 'endpoint']).optional().catch(undefined),
137
137
  reachable: z.boolean(),
138
138
  ready: z.boolean(),
139
+ /**
140
+ * A direct connection is not fully readable until Ablo has copied the rows
141
+ * that predate its replication slot into the sync log. Optional so an older
142
+ * server remains readable by a newer CLI.
143
+ */
144
+ initial_snapshot: z
145
+ .object({
146
+ status: z.enum(['loading', 'retrying', 'complete']),
147
+ detail: z.string().optional(),
148
+ })
149
+ .optional(),
139
150
  reason: z.string().optional(),
140
151
  failures: z.array(readinessFailureSchema).readonly(),
141
152
  advisories: z.array(readinessAdvisorySchema).readonly().optional(),