@abloatai/ablo 0.10.0 → 0.11.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 (94) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +2 -1
  3. package/dist/BaseSyncedStore.d.ts +75 -0
  4. package/dist/BaseSyncedStore.js +193 -8
  5. package/dist/Database.d.ts +10 -2
  6. package/dist/Database.js +15 -1
  7. package/dist/SyncClient.d.ts +12 -1
  8. package/dist/SyncClient.js +110 -26
  9. package/dist/agent/Agent.d.ts +9 -9
  10. package/dist/agent/Agent.js +16 -16
  11. package/dist/agent/index.d.ts +1 -1
  12. package/dist/agent/index.js +2 -2
  13. package/dist/agent/types.d.ts +1 -1
  14. package/dist/agent/types.js +1 -1
  15. package/dist/ai-sdk/{intent-broadcast.d.ts → claim-broadcast.d.ts} +10 -10
  16. package/dist/ai-sdk/{intent-broadcast.js → claim-broadcast.js} +6 -6
  17. package/dist/ai-sdk/coordination-context.d.ts +9 -9
  18. package/dist/ai-sdk/coordination-context.js +8 -8
  19. package/dist/ai-sdk/index.d.ts +1 -1
  20. package/dist/ai-sdk/index.js +1 -1
  21. package/dist/ai-sdk/wrap.d.ts +4 -4
  22. package/dist/ai-sdk/wrap.js +4 -4
  23. package/dist/api/index.d.ts +2 -2
  24. package/dist/cli.cjs +254 -48
  25. package/dist/client/Ablo.d.ts +30 -63
  26. package/dist/client/Ablo.js +108 -102
  27. package/dist/client/ApiClient.d.ts +6 -5
  28. package/dist/client/ApiClient.js +83 -62
  29. package/dist/client/createModelProxy.d.ts +16 -54
  30. package/dist/client/createModelProxy.js +44 -16
  31. package/dist/client/httpClient.d.ts +2 -0
  32. package/dist/client/httpClient.js +1 -1
  33. package/dist/client/index.d.ts +3 -3
  34. package/dist/client/writeOptionsSchema.d.ts +4 -4
  35. package/dist/client/writeOptionsSchema.js +4 -4
  36. package/dist/coordination/schema.d.ts +249 -38
  37. package/dist/coordination/schema.js +172 -39
  38. package/dist/core/index.d.ts +2 -2
  39. package/dist/core/index.js +4 -4
  40. package/dist/errorCodes.d.ts +9 -9
  41. package/dist/errorCodes.js +15 -15
  42. package/dist/errors.d.ts +51 -2
  43. package/dist/errors.js +94 -5
  44. package/dist/interfaces/index.d.ts +8 -4
  45. package/dist/policy/index.d.ts +1 -1
  46. package/dist/policy/types.d.ts +13 -13
  47. package/dist/policy/types.js +8 -8
  48. package/dist/react/AbloProvider.d.ts +51 -4
  49. package/dist/react/AbloProvider.js +95 -11
  50. package/dist/react/context.d.ts +26 -9
  51. package/dist/react/context.js +2 -2
  52. package/dist/react/index.d.ts +4 -4
  53. package/dist/react/index.js +4 -4
  54. package/dist/react/useAblo.js +5 -5
  55. package/dist/react/{useIntent.d.ts → useClaim.d.ts} +9 -9
  56. package/dist/react/useClaim.js +42 -0
  57. package/dist/schema/index.js +1 -1
  58. package/dist/schema/sugar.d.ts +3 -3
  59. package/dist/schema/sugar.js +3 -3
  60. package/dist/schema/sync-delta-wire.d.ts +8 -8
  61. package/dist/server/commit.d.ts +2 -2
  62. package/dist/sync/AreaOfInterestManager.d.ts +162 -0
  63. package/dist/sync/AreaOfInterestManager.js +233 -0
  64. package/dist/sync/BootstrapHelper.d.ts +9 -1
  65. package/dist/sync/BootstrapHelper.js +15 -5
  66. package/dist/sync/NetworkProbe.d.ts +1 -1
  67. package/dist/sync/NetworkProbe.js +1 -1
  68. package/dist/sync/SyncWebSocket.d.ts +59 -25
  69. package/dist/sync/SyncWebSocket.js +123 -26
  70. package/dist/sync/awaitClaimGrant.d.ts +40 -0
  71. package/dist/sync/awaitClaimGrant.js +86 -0
  72. package/dist/sync/createClaimStream.d.ts +34 -0
  73. package/dist/sync/{createIntentStream.js → createClaimStream.js} +92 -81
  74. package/dist/sync/createPresenceStream.js +3 -2
  75. package/dist/sync/participants.d.ts +10 -10
  76. package/dist/sync/participants.js +17 -10
  77. package/dist/sync/schemas.d.ts +8 -8
  78. package/dist/transactions/TransactionQueue.d.ts +12 -0
  79. package/dist/transactions/TransactionQueue.js +126 -8
  80. package/dist/types/global.d.ts +10 -10
  81. package/dist/types/global.js +3 -3
  82. package/dist/types/index.d.ts +9 -7
  83. package/dist/types/index.js +2 -2
  84. package/dist/types/streams.d.ts +114 -98
  85. package/dist/types/streams.js +1 -1
  86. package/dist/utils/asyncIterator.d.ts +1 -1
  87. package/dist/utils/asyncIterator.js +1 -1
  88. package/dist/wire/frames.d.ts +2 -2
  89. package/docs/migration.md +52 -0
  90. package/package.json +3 -2
  91. package/dist/react/useIntent.js +0 -42
  92. package/dist/sync/awaitIntentGrant.d.ts +0 -40
  93. package/dist/sync/awaitIntentGrant.js +0 -62
  94. package/dist/sync/createIntentStream.d.ts +0 -34
@@ -5,16 +5,17 @@
5
5
  * IndexedDB, no WebSocket. It maps the public Model / Claim / Commit
6
6
  * nouns directly to HTTP routes on sync-server.
7
7
  */
8
- import type { AbloOptions, CommitResource, IntentCreateOptions, IntentHandle, IntentWaitOptions, ModelClient, ModelClaim, ModelTarget } from './Ablo.js';
8
+ import type { AbloOptions, CommitResource, ClaimCreateOptions, ClaimWaitOptions, ModelClient, ModelClaim, ModelTarget } from './Ablo.js';
9
+ import type { ClaimHandle } from './createModelProxy.js';
9
10
  import type { Duration } from '../utils/duration.js';
10
11
  export type AbloApiClientOptions = Omit<AbloOptions, 'schema'> & {
11
12
  readonly schema?: null | undefined;
12
13
  readonly bootstrapBaseUrl?: string | undefined;
13
14
  };
14
- export interface AbloApiIntents {
15
- create(options: IntentCreateOptions): Promise<IntentHandle>;
15
+ export interface AbloApiClaims {
16
+ create(options: ClaimCreateOptions): Promise<ClaimHandle>;
16
17
  list(target?: Partial<ModelTarget>): Promise<readonly ModelClaim[]>;
17
- waitFor(target: Partial<ModelTarget>, options?: IntentWaitOptions): Promise<void>;
18
+ waitFor(target: Partial<ModelTarget>, options?: ClaimWaitOptions): Promise<void>;
18
19
  }
19
20
  export type CapabilityParticipantKind = 'agent' | 'system';
20
21
  export interface CapabilityCreateBaseOptions {
@@ -121,7 +122,7 @@ export interface AbloApi {
121
122
  dispose(): Promise<void>;
122
123
  purge(): Promise<void>;
123
124
  readonly capabilities: CapabilityResource;
124
- readonly intents: AbloApiIntents;
125
+ readonly claims: AbloApiClaims;
125
126
  readonly commits: CommitResource;
126
127
  model<T = Record<string, unknown>>(name: string): ModelClient<T>;
127
128
  /**
@@ -5,8 +5,9 @@
5
5
  * IndexedDB, no WebSocket. It maps the public Model / Claim / Commit
6
6
  * nouns directly to HTTP routes on sync-server.
7
7
  */
8
- import { AbloClaimedError, AbloAuthenticationError, AbloConnectionError, AbloValidationError, translateHttpError, } from '../errors.js';
9
- import { assertBrowserSafety, readProcessEnv, resolveApiKey, resolveApiKeyValue, resolveAuthToken, resolveBaseURL, resolveBootstrapBaseUrl, } from './auth.js';
8
+ import { AbloClaimedError, AbloAuthenticationError, AbloConnectionError, AbloValidationError, claimedError, translateHttpError, } from '../errors.js';
9
+ import { assertBrowserSafety, readProcessEnv, resolveApiKey, resolveApiKeyValue, resolveAuthToken, resolveBaseURL, resolveBootstrapBaseUrl, resolveDatabaseUrl, } from './auth.js';
10
+ import { registerDataSource } from './registerDataSource.js';
10
11
  import { toSeconds } from '../utils/duration.js';
11
12
  import { assertWriteOptions } from './writeOptionsSchema.js';
12
13
  const DEFAULT_AGENT_LEASE = '10m';
@@ -15,8 +16,10 @@ export function createProtocolClient(options) {
15
16
  const authInput = { options, env };
16
17
  const configuredApiKey = resolveApiKey(authInput);
17
18
  const configuredAuthToken = resolveAuthToken(authInput);
19
+ const configuredDatabaseUrl = resolveDatabaseUrl(authInput);
18
20
  assertBrowserSafety({
19
21
  apiKey: configuredApiKey,
22
+ databaseUrl: configuredDatabaseUrl,
20
23
  dangerouslyAllowBrowser: options.dangerouslyAllowBrowser,
21
24
  });
22
25
  const fetchImpl = options.fetch ?? globalThis.fetch;
@@ -28,6 +31,28 @@ export function createProtocolClient(options) {
28
31
  url,
29
32
  bootstrapBaseUrl: options.bootstrapBaseUrl,
30
33
  }).replace(/\/+$/, '');
34
+ let readyPromise = null;
35
+ async function ready() {
36
+ if (readyPromise)
37
+ return readyPromise;
38
+ readyPromise = (async () => {
39
+ if (!configuredDatabaseUrl)
40
+ return;
41
+ await registerDataSource({
42
+ baseUrl: apiBaseUrl,
43
+ apiKey: await resolveApiKeyValue(configuredApiKey),
44
+ databaseUrl: configuredDatabaseUrl,
45
+ ...(options.fetch ? { fetchImpl: options.fetch } : {}),
46
+ });
47
+ })();
48
+ try {
49
+ await readyPromise;
50
+ }
51
+ catch (error) {
52
+ readyPromise = null;
53
+ throw error;
54
+ }
55
+ }
31
56
  async function authHeaders() {
32
57
  const apiKey = await resolveApiKeyValue(configuredApiKey);
33
58
  const token = apiKey ?? configuredAuthToken;
@@ -57,6 +82,7 @@ export function createProtocolClient(options) {
57
82
  return target.toString();
58
83
  }
59
84
  async function requestJson(path, init) {
85
+ await ready();
60
86
  const { idempotencyKey, ...requestInit } = init;
61
87
  const headers = await authHeaders();
62
88
  if (idempotencyKey)
@@ -82,7 +108,7 @@ export function createProtocolClient(options) {
82
108
  ? crypto.randomUUID()
83
109
  : `tx_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
84
110
  }
85
- function createIntentId() {
111
+ function createClaimId() {
86
112
  return typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'
87
113
  ? `int_${crypto.randomUUID()}`
88
114
  : `int_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
@@ -128,7 +154,7 @@ export function createProtocolClient(options) {
128
154
  }
129
155
  return inputOperations.map((op) => normalizeCommitOperation(op, commitOptions));
130
156
  }
131
- async function listIntents(target) {
157
+ async function listClaims(target) {
132
158
  const state = await listClaimState(target);
133
159
  return state.active;
134
160
  }
@@ -141,24 +167,16 @@ export function createProtocolClient(options) {
141
167
  if (target?.field)
142
168
  params.set('field', target.field);
143
169
  const suffix = params.toString();
144
- const body = await requestJson(`/v1/intents${suffix ? `?${suffix}` : ''}`, { method: 'GET' });
170
+ const body = await requestJson(`/v1/claims${suffix ? `?${suffix}` : ''}`, { method: 'GET' });
145
171
  return {
146
- active: body.intents ?? [],
172
+ active: body.claims ?? [],
147
173
  queue: body.queue ?? [],
148
174
  };
149
175
  }
150
- function claimedError(target, claims, code) {
151
- const label = [target.model, target.id, target.field].filter(Boolean).join('/');
152
- const holder = claims[0];
153
- const suffix = holder
154
- ? ` held by ${holder.actor} (${holder.action})`
155
- : ' held by another participant';
156
- return new AbloClaimedError(`Model row is claimed: ${label || 'target'}${suffix}.`, { code, claims });
157
- }
158
176
  function delay(ms, signal) {
159
177
  if (signal?.aborted) {
160
- return Promise.reject(new AbloConnectionError('Intent wait aborted.', {
161
- code: 'intent_wait_aborted',
178
+ return Promise.reject(new AbloConnectionError('Claim wait aborted.', {
179
+ code: 'claim_wait_aborted',
162
180
  cause: signal.reason,
163
181
  }));
164
182
  }
@@ -174,28 +192,28 @@ export function createProtocolClient(options) {
174
192
  }
175
193
  function onAbort() {
176
194
  cleanup();
177
- reject(new AbloConnectionError('Intent wait aborted.', {
178
- code: 'intent_wait_aborted',
195
+ reject(new AbloConnectionError('Claim wait aborted.', {
196
+ code: 'claim_wait_aborted',
179
197
  cause: signal?.reason,
180
198
  }));
181
199
  }
182
200
  signal?.addEventListener('abort', onAbort, { once: true });
183
201
  });
184
202
  }
185
- async function waitForNoIntents(target, options) {
203
+ async function waitForNoClaims(target, options) {
186
204
  const startedAt = Date.now();
187
205
  const pollInterval = options?.pollInterval;
188
206
  for (;;) {
189
- const intents = await listIntents(target);
190
- if (intents.length === 0)
207
+ const claims = await listClaims(target);
208
+ if (claims.length === 0)
191
209
  return;
192
210
  if (pollInterval == null) {
193
211
  throw new AbloValidationError('Cannot wait for claims over the HTTP client without `pollInterval`. ' +
194
212
  'Use the schema client for event-driven claim waits, pass `ifClaimed: "return"`, ' +
195
- 'or provide an explicit poll interval for this runtime.', { code: 'intent_wait_poll_interval_required' });
213
+ 'or provide an explicit poll interval for this runtime.', { code: 'claim_wait_poll_interval_required' });
196
214
  }
197
215
  if (options?.timeout != null && Date.now() - startedAt >= options.timeout) {
198
- throw claimedError(target, intents, 'model_claimed_timeout');
216
+ throw claimedError(target, claims, 'model_claimed_timeout');
199
217
  }
200
218
  const remaining = options?.timeout == null
201
219
  ? pollInterval
@@ -217,7 +235,7 @@ export function createProtocolClient(options) {
217
235
  state.queue.length >= options.maxQueueDepth) {
218
236
  throw claimedError(target, state.active, 'queue_too_deep');
219
237
  }
220
- await waitForNoIntents(target, {
238
+ await waitForNoClaims(target, {
221
239
  timeout: options?.claimedTimeout,
222
240
  pollInterval: options?.claimedPollInterval,
223
241
  });
@@ -230,7 +248,7 @@ export function createProtocolClient(options) {
230
248
  readAt: commitOptions.readAt,
231
249
  onStale: commitOptions.onStale,
232
250
  wait: commitOptions.wait,
233
- intent: commitOptions.intent,
251
+ claim: commitOptions.claim,
234
252
  }, 'commits.create');
235
253
  const clientTxId = createClientTxId(commitOptions.idempotencyKey);
236
254
  // Same claim vocabulary as the WS client's `commits.create`: a handle
@@ -247,7 +265,7 @@ export function createProtocolClient(options) {
247
265
  body: JSON.stringify({
248
266
  clientTxId,
249
267
  idempotencyKey: clientTxId,
250
- intent: normalizeIntentId(commitOptions.intent) ?? claim?.claimId,
268
+ claim: normalizeClaimId(commitOptions.claimRef) ?? claim?.claimId,
251
269
  operations,
252
270
  }),
253
271
  });
@@ -353,39 +371,42 @@ export function createProtocolClient(options) {
353
371
  return capabilities.create(options);
354
372
  },
355
373
  };
356
- const intents = {
357
- async create(intentOptions) {
358
- const intentId = createIntentId();
359
- const body = await requestJson('/v1/intents', {
374
+ const claims = {
375
+ async create(claimOptions) {
376
+ const claimId = createClaimId();
377
+ const body = await requestJson('/v1/claims', {
360
378
  method: 'POST',
361
379
  body: JSON.stringify({
362
- intentId,
363
- target: intentOptions.target,
364
- action: intentOptions.action,
365
- ttl: intentOptions.ttl,
366
- queue: intentOptions.queue,
380
+ claimId,
381
+ target: claimOptions.target,
382
+ action: claimOptions.action,
383
+ ttl: claimOptions.ttl,
384
+ queue: claimOptions.queue,
367
385
  }),
368
386
  });
369
- // The fair-queue grant is PUSHED over a WebSocket (`intent_granted`),
387
+ // The fair-queue grant is PUSHED over a WebSocket (`claim_granted`),
370
388
  // which this stateless HTTP client doesn't hold. Returning a handle here
371
389
  // would be a phantom holder — a lease we can't confirm is ours. So a
372
390
  // queued response is surfaced as a typed claimed signal; callers that need
373
391
  // to *wait* in line use the realtime (WS-backed) `ablo.<model>.claim`.
374
392
  if (body.status === 'queued') {
375
- throw new AbloClaimedError(`Target ${intentOptions.target.model}/${intentOptions.target.id} is held; ` +
393
+ throw new AbloClaimedError(`Target ${claimOptions.target.model}/${claimOptions.target.id} is held; ` +
376
394
  `queued at position ${body.position ?? 0}. The HTTP client can't await ` +
377
- `the grant (no socket) — use the realtime client to wait in line.`, { code: 'intent_queued' });
395
+ `the grant (no socket) — use the realtime client to wait in line.`, { code: 'claim_queued' });
378
396
  }
379
- const id = body.intent?.id ?? intentId;
397
+ const id = body.claim?.id ?? claimId;
380
398
  let released = false;
381
399
  const release = async () => {
382
400
  if (released)
383
401
  return;
384
402
  released = true;
385
- await requestJson(`/v1/intents/${encodeURIComponent(id)}`, { method: 'DELETE' });
403
+ await requestJson(`/v1/claims/${encodeURIComponent(id)}`, { method: 'DELETE' });
386
404
  };
387
405
  return {
388
- id,
406
+ object: 'claim',
407
+ claimId: id,
408
+ action: claimOptions.action,
409
+ target: claimOptions.target,
389
410
  release,
390
411
  revoke: () => {
391
412
  void release().catch(() => { });
@@ -393,9 +414,9 @@ export function createProtocolClient(options) {
393
414
  [Symbol.asyncDispose]: release,
394
415
  };
395
416
  },
396
- list: listIntents,
417
+ list: listClaims,
397
418
  waitFor(target, options) {
398
- return waitForNoIntents(target, options);
419
+ return waitForNoClaims(target, options);
399
420
  },
400
421
  };
401
422
  async function listModel(modelName, options) {
@@ -456,7 +477,7 @@ export function createProtocolClient(options) {
456
477
  readAt: options.readAt,
457
478
  onStale: options.onStale,
458
479
  wait: options.wait,
459
- intent: options.intent,
480
+ claim: options.claim,
460
481
  }, `${modelName} ${action}`);
461
482
  const clientTxId = createClientTxId(options?.idempotencyKey);
462
483
  const encModel = encodeURIComponent(modelName);
@@ -475,7 +496,7 @@ export function createProtocolClient(options) {
475
496
  const readAt = options?.readAt ?? claimHandle?.readAt;
476
497
  const requestBody = {
477
498
  idempotencyKey: clientTxId,
478
- intent: normalizeIntentId(options?.intent) ?? claimHandle?.claimId,
499
+ claim: normalizeClaimId(options?.claimRef) ?? claimHandle?.claimId,
479
500
  onStale: options?.onStale ?? (claimHandle?.readAt !== undefined ? 'reject' : undefined),
480
501
  readAt,
481
502
  };
@@ -528,9 +549,9 @@ export function createProtocolClient(options) {
528
549
  });
529
550
  if (body.status === 'queued') {
530
551
  throw new AbloClaimedError(`Target ${name}/${params.id} is held; queued at position ${body.position ?? 0}. ` +
531
- `The HTTP client cannot await the grant without a WebSocket.`, { code: 'intent_queued' });
552
+ `The HTTP client cannot await the grant without a WebSocket.`, { code: 'claim_queued' });
532
553
  }
533
- return body.intent?.id ?? body.id ?? body.intentId ?? createIntentId();
554
+ return body.claim?.id ?? body.id ?? body.claimId ?? createClaimId();
534
555
  };
535
556
  const releaseClaim = (params) => requestJson(claimPath(isClaimHandle(params) ? params.target.id : params.id), { method: 'DELETE' }).then(() => undefined);
536
557
  async function claimImpl(params) {
@@ -559,23 +580,23 @@ export function createProtocolClient(options) {
559
580
  [Symbol.asyncDispose]: release,
560
581
  };
561
582
  }
562
- const intentsForEntity = async (params) => requestJson(`/v1/intents?model=${encodeURIComponent(name)}&id=${encodeURIComponent(params.id)}${params.field ? `&field=${encodeURIComponent(params.field)}` : ''}`, { method: 'GET' });
583
+ const claimsForEntity = async (params) => requestJson(`/v1/claims?model=${encodeURIComponent(name)}&id=${encodeURIComponent(params.id)}${params.field ? `&field=${encodeURIComponent(params.field)}` : ''}`, { method: 'GET' });
563
584
  const claim = Object.assign(claimImpl, {
564
585
  release: releaseClaim,
565
586
  state: async (params) => {
566
- const res = await intentsForEntity(params);
567
- return res.intents?.[0] ?? null;
587
+ const res = await claimsForEntity(params);
588
+ return res.claims?.[0] ?? null;
568
589
  },
569
590
  queue: async (params) => {
570
- const res = await intentsForEntity(params);
591
+ const res = await claimsForEntity(params);
571
592
  return { object: 'list', data: res.queue ?? [] };
572
593
  },
573
594
  reorder: async (params) => {
574
595
  await requestJson(`${claimPath(params.id)}/reorder`, {
575
596
  method: 'POST',
576
- // The reorder route's payload is `{ heldBy, intentId }[]` — Intent's id
577
- // IS the intentId.
578
- body: JSON.stringify({ order: params.order.map((i) => ({ heldBy: i.heldBy, intentId: i.id })) }),
597
+ // The reorder route's payload is `{ heldBy, claimId }[]` — Claim's id
598
+ // IS the claimId.
599
+ body: JSON.stringify({ order: params.order.map((i) => ({ heldBy: i.heldBy, claimId: i.id })) }),
579
600
  });
580
601
  },
581
602
  });
@@ -584,11 +605,11 @@ export function createProtocolClient(options) {
584
605
  if (!claimInput)
585
606
  return run(input);
586
607
  if (isClaimHandle(claimInput)) {
587
- return run({ ...input, intent: { id: claimInput.claimId }, claim: undefined });
608
+ return run({ ...input, claimRef: { id: claimInput.claimId }, claim: undefined });
588
609
  }
589
610
  const claimId = await acquireClaim({ id, ...claimInput });
590
611
  try {
591
- return await run({ ...input, intent: { id: claimId }, claim: undefined });
612
+ return await run({ ...input, claimRef: { id: claimId }, claim: undefined });
592
613
  }
593
614
  finally {
594
615
  await releaseClaim({ id }).catch(() => { });
@@ -624,12 +645,12 @@ export function createProtocolClient(options) {
624
645
  };
625
646
  }
626
647
  return {
627
- async ready() { },
648
+ ready,
628
649
  async waitForFlush() { },
629
650
  async dispose() { },
630
651
  async purge() { },
631
652
  capabilities,
632
- intents,
653
+ claims,
633
654
  commits,
634
655
  model,
635
656
  async getAuthToken() {
@@ -639,10 +660,10 @@ export function createProtocolClient(options) {
639
660
  },
640
661
  };
641
662
  }
642
- function normalizeIntentId(intent) {
643
- if (typeof intent === 'string')
644
- return intent;
645
- return intent?.id;
663
+ function normalizeClaimId(claim) {
664
+ if (typeof claim === 'string')
665
+ return claim;
666
+ return claim?.id;
646
667
  }
647
668
  function parseBody(bodyText) {
648
669
  if (bodyText.length === 0)
@@ -21,7 +21,7 @@ import type { SyncClient } from '../SyncClient.js';
21
21
  import type { HydrationCoordinator } from '../sync/HydrationCoordinator.js';
22
22
  import type { LoadWhere } from '../query/types.js';
23
23
  import { ModelScope } from '../types/index.js';
24
- import type { Duration, Intent, IntentWaitOptions, Snapshot, TargetRange } from '../types/streams.js';
24
+ import type { Duration, Claim, ClaimHandle, ClaimWaitOptions, Snapshot, TargetRange } from '../types/streams.js';
25
25
  export interface ModelClientMeta {
26
26
  readonly key: string;
27
27
  readonly typename: string;
@@ -73,21 +73,8 @@ export interface ModelLoadOptions<T> {
73
73
  /** Options for the single-row async server read `retrieve({ id })`. A subset of
74
74
  * {@link ModelLoadOptions} — `where`/`limit`/`orderBy` are fixed by the id. */
75
75
  export type ModelRetrieveOptions = Pick<ModelLoadOptions<unknown>, 'type' | 'expand'>;
76
- export interface IntentLeaseHandle {
77
- readonly id: string;
78
- /**
79
- * True when the grant came AFTER waiting in the server's FIFO line
80
- * (`intent_granted`) — the authoritative "the row may have changed
81
- * underneath us" signal. The local `observe()` snapshot can't stand in
82
- * for this: intent fan-out is entity-scoped, so org-wide subscriptions
83
- * (the default hosted client) never see peers' claims at all.
84
- */
85
- readonly waited?: boolean;
86
- release(): Promise<void>;
87
- revoke(): void;
88
- }
89
76
  export interface ModelCollaboration<T> {
90
- createIntent(options: {
77
+ createClaim(options: {
91
78
  target: {
92
79
  model: string;
93
80
  id: string;
@@ -106,11 +93,11 @@ export interface ModelCollaboration<T> {
106
93
  queue?: boolean;
107
94
  /** Reject (don't wait) if the queue is already this deep when we join. */
108
95
  maxQueueDepth?: number;
109
- }): Promise<IntentLeaseHandle>;
96
+ }): Promise<ClaimHandle>;
110
97
  createSnapshot(modelKey: string, id: string): Snapshot;
111
98
  /**
112
99
  * Current coordination state on a target — who (if anyone) holds it.
113
- * Synchronous reactive snapshot read off the presence/intent stream;
100
+ * Synchronous reactive snapshot read off the presence/claim stream;
114
101
  * `null` when the target is free. The wiring site computes it because
115
102
  * only it knows the local participant id (needed to distinguish "I
116
103
  * hold it" from "someone else holds it").
@@ -118,15 +105,15 @@ export interface ModelCollaboration<T> {
118
105
  observe(target: {
119
106
  model: string;
120
107
  id: string;
121
- }): Intent | null;
108
+ }): Claim | null;
122
109
  /**
123
- * The reactive wait queue on a target — the FIFO line of queued intents
124
- * behind the holder. Synchronous snapshot off the synced intent stream.
110
+ * The reactive wait queue on a target — the FIFO line of queued claims
111
+ * behind the holder. Synchronous snapshot off the synced claim stream.
125
112
  */
126
113
  queue(target: {
127
114
  model: string;
128
115
  id: string;
129
- }): readonly Intent[];
116
+ }): readonly Claim[];
130
117
  /**
131
118
  * Re-rank the wait queue on a target (privileged — server-gated). `order` is
132
119
  * the desired front-of-line ordering, taken from `queue(target)`.
@@ -134,16 +121,16 @@ export interface ModelCollaboration<T> {
134
121
  reorder(target: {
135
122
  model: string;
136
123
  id: string;
137
- }, order: readonly Intent[]): void;
124
+ }, order: readonly Claim[]): void;
138
125
  /**
139
- * Resolve once no participant holds an active intent on the target.
140
- * The contender's "wait until it's free" — delegates to the intent
126
+ * Resolve once no participant holds an active claim on the target.
127
+ * The contender's "wait until it's free" — delegates to the claim
141
128
  * stream's `waitFor`.
142
129
  */
143
130
  waitFor(target: {
144
131
  model: string;
145
132
  id: string;
146
- }, options?: IntentWaitOptions): Promise<void>;
133
+ }, options?: ClaimWaitOptions): Promise<void>;
147
134
  /**
148
135
  * The local participant's id. Used to distinguish "I already hold this"
149
136
  * from "someone else holds it" in `claimOrWait`.
@@ -191,7 +178,7 @@ export interface ClaimLookupParams<T = Record<string, unknown>> {
191
178
  readonly field?: string;
192
179
  }
193
180
  export interface ClaimReorderParams<T = Record<string, unknown>> extends ClaimLookupParams<T> {
194
- readonly order: readonly Intent[];
181
+ readonly order: readonly Claim[];
195
182
  }
196
183
  /**
197
184
  * A claim handle: the held entity data plus an explicit release hook, so
@@ -217,32 +204,7 @@ export interface ClaimReorderParams<T = Record<string, unknown>> extends ClaimLo
217
204
  * `ablo.<model>.update({ id, data, claim })` verb — the handle carries the
218
205
  * lease id and snapshot watermark for attribution + stale protection.
219
206
  */
220
- export interface ClaimHandle<T = Record<string, unknown>> extends AsyncDisposable {
221
- readonly object: 'claim';
222
- readonly claimId: string;
223
- /**
224
- * Sync watermark of the held snapshot (`data` was read at this stamp).
225
- * Writes that carry the handle — `update({ id, data, claim })` or
226
- * `commits.create({ claim, ... })` — use it as the `readAt` stale guard,
227
- * so a concurrent commit between snapshot and write is rejected instead
228
- * of clobbered. Optional for wire/duck-type compat with externally
229
- * constructed handles.
230
- */
231
- readonly readAt?: number;
232
- readonly target: {
233
- readonly model: string;
234
- readonly id: string;
235
- readonly field?: string;
236
- readonly path?: string;
237
- readonly range?: TargetRange;
238
- readonly meta?: Record<string, unknown>;
239
- };
240
- readonly action: string;
241
- readonly description?: string;
242
- readonly data: T;
243
- release(): Promise<void>;
244
- revoke(): void;
245
- }
207
+ export type { ClaimHandle };
246
208
  export type ClaimOptions<T = Record<string, unknown>> = ClaimTargetOptions<T>;
247
209
  /**
248
210
  * The coordination surface for a model, exposed as a callable namespace.
@@ -273,14 +235,14 @@ export interface ClaimApi<T> {
273
235
  * Current holder for a row, or `null` when free. Use this for UI badges and
274
236
  * preflight checks, not for the normal write path.
275
237
  */
276
- state(params: ClaimLookupParams<T>): Intent | null;
238
+ state(params: ClaimLookupParams<T>): Claim | null;
277
239
  /**
278
240
  * FIFO wait line behind the current holder. Advanced: useful for operator
279
241
  * UIs and schedulers.
280
242
  */
281
243
  queue(params: ClaimLookupParams<T>): {
282
244
  readonly object: 'list';
283
- readonly data: readonly Intent[];
245
+ readonly data: readonly Claim[];
284
246
  };
285
247
  /**
286
248
  * Re-rank the wait line. Advanced and permission-gated.