@abloatai/ablo 0.10.1 → 0.11.1

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 (105) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +63 -23
  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 +369 -67
  25. package/dist/client/Ablo.d.ts +30 -63
  26. package/dist/client/Ablo.js +124 -103
  27. package/dist/client/ApiClient.d.ts +6 -5
  28. package/dist/client/ApiClient.js +86 -62
  29. package/dist/client/auth.d.ts +9 -4
  30. package/dist/client/auth.js +40 -5
  31. package/dist/client/createModelProxy.d.ts +41 -54
  32. package/dist/client/createModelProxy.js +123 -20
  33. package/dist/client/httpClient.d.ts +2 -0
  34. package/dist/client/httpClient.js +1 -1
  35. package/dist/client/index.d.ts +3 -3
  36. package/dist/client/writeOptionsSchema.d.ts +4 -4
  37. package/dist/client/writeOptionsSchema.js +4 -4
  38. package/dist/coordination/schema.d.ts +249 -38
  39. package/dist/coordination/schema.js +172 -39
  40. package/dist/core/index.d.ts +2 -2
  41. package/dist/core/index.js +4 -4
  42. package/dist/errorCodes.d.ts +9 -9
  43. package/dist/errorCodes.js +16 -16
  44. package/dist/errors.d.ts +51 -2
  45. package/dist/errors.js +94 -5
  46. package/dist/interfaces/index.d.ts +8 -4
  47. package/dist/policy/index.d.ts +1 -1
  48. package/dist/policy/types.d.ts +13 -13
  49. package/dist/policy/types.js +8 -8
  50. package/dist/react/AbloProvider.d.ts +51 -4
  51. package/dist/react/AbloProvider.js +95 -11
  52. package/dist/react/context.d.ts +26 -9
  53. package/dist/react/context.js +2 -2
  54. package/dist/react/index.d.ts +4 -4
  55. package/dist/react/index.js +4 -4
  56. package/dist/react/useAblo.js +5 -5
  57. package/dist/react/{useIntent.d.ts → useClaim.d.ts} +9 -9
  58. package/dist/react/useClaim.js +42 -0
  59. package/dist/schema/index.js +1 -1
  60. package/dist/schema/schema.d.ts +3 -3
  61. package/dist/schema/sugar.d.ts +3 -3
  62. package/dist/schema/sugar.js +3 -3
  63. package/dist/schema/sync-delta-wire.d.ts +8 -8
  64. package/dist/server/commit.d.ts +2 -2
  65. package/dist/sync/AreaOfInterestManager.d.ts +162 -0
  66. package/dist/sync/AreaOfInterestManager.js +233 -0
  67. package/dist/sync/BootstrapHelper.d.ts +9 -1
  68. package/dist/sync/BootstrapHelper.js +15 -5
  69. package/dist/sync/NetworkProbe.d.ts +1 -1
  70. package/dist/sync/NetworkProbe.js +1 -1
  71. package/dist/sync/SyncWebSocket.d.ts +59 -25
  72. package/dist/sync/SyncWebSocket.js +123 -26
  73. package/dist/sync/awaitClaimGrant.d.ts +40 -0
  74. package/dist/sync/awaitClaimGrant.js +86 -0
  75. package/dist/sync/createClaimStream.d.ts +34 -0
  76. package/dist/sync/{createIntentStream.js → createClaimStream.js} +92 -81
  77. package/dist/sync/createPresenceStream.js +3 -2
  78. package/dist/sync/participants.d.ts +10 -10
  79. package/dist/sync/participants.js +17 -10
  80. package/dist/sync/schemas.d.ts +8 -8
  81. package/dist/transactions/TransactionQueue.d.ts +23 -0
  82. package/dist/transactions/TransactionQueue.js +186 -12
  83. package/dist/types/global.d.ts +18 -13
  84. package/dist/types/global.js +11 -6
  85. package/dist/types/index.d.ts +9 -7
  86. package/dist/types/index.js +2 -2
  87. package/dist/types/streams.d.ts +114 -98
  88. package/dist/types/streams.js +1 -1
  89. package/dist/utils/asyncIterator.d.ts +1 -1
  90. package/dist/utils/asyncIterator.js +1 -1
  91. package/dist/wire/frames.d.ts +2 -2
  92. package/docs/api.md +3 -3
  93. package/docs/client-behavior.md +6 -3
  94. package/docs/coordination.md +13 -3
  95. package/docs/data-sources.md +29 -9
  96. package/docs/migration.md +40 -0
  97. package/docs/quickstart.md +61 -33
  98. package/docs/react.md +46 -0
  99. package/llms-full.txt +25 -8
  100. package/llms.txt +11 -9
  101. package/package.json +3 -2
  102. package/dist/react/useIntent.js +0 -42
  103. package/dist/sync/awaitIntentGrant.d.ts +0 -40
  104. package/dist/sync/awaitIntentGrant.js +0 -62
  105. package/dist/sync/createIntentStream.d.ts +0 -34
@@ -11,6 +11,7 @@ import { EventEmitter } from 'events';
11
11
  import { getContext } from '../context.js';
12
12
  import { flushOfflineQueueOnce } from './OfflineFlush.js';
13
13
  import { AbloConnectionError, AbloError, CapabilityError, SyncSessionError, errorFromWire, toAbloError, } from '../errors.js';
14
+ import { subscriptionAckPayloadSchema } from '../coordination/schema.js';
14
15
  import { WS_BEARER_SUBPROTOCOL_PREFIX, WS_SYNC_SUBPROTOCOL, } from '../auth/credentialSource.js';
15
16
  // ---------------------------------------------------------------------------
16
17
  // Ablo-specific collaboration events moved to apps/web/src/lib/sync/collaboration-events.ts
@@ -105,6 +106,15 @@ export class SyncWebSocket extends EventEmitter {
105
106
  * over a multiplexed connection.
106
107
  */
107
108
  pendingClaims = new Map();
109
+ /**
110
+ * In-flight `update_subscription` frames awaiting `subscription_ack`.
111
+ * A FIFO queue rather than a keyed Map because the wire ack carries no
112
+ * correlation id — the server applies subscription updates in receive
113
+ * order and acks in the same order, so `shift()` on ack matches the
114
+ * oldest pending request. (Read-interest changes are infrequent and
115
+ * usually settle before the next one, so depth is ~1 in practice.)
116
+ */
117
+ pendingSubscriptions = [];
108
118
  constructor(options) {
109
119
  super();
110
120
  // Construct WebSocket URL from base Go server URL
@@ -430,6 +440,37 @@ export class SyncWebSocket extends EventEmitter {
430
440
  }
431
441
  break;
432
442
  }
443
+ case 'subscription_ack': {
444
+ // Ack for a prior `update_subscription`. The wire carries no
445
+ // correlation id, so FIFO-match against the oldest pending
446
+ // request — the server applies and acks subscription updates
447
+ // in receive order. Validated through the canonical zod schema
448
+ // (mirrors how the Hub validates inbound frames).
449
+ const pending = this.pendingSubscriptions.shift();
450
+ if (!pending)
451
+ break;
452
+ clearTimeout(pending.timeout);
453
+ const parsed = subscriptionAckPayloadSchema.safeParse(message.payload);
454
+ if (!parsed.success) {
455
+ // Unreadable ack — resolve the pending request as a failure
456
+ // rather than hang it until timeout.
457
+ pending.reject(errorFromWire('malformed subscription_ack from server', {
458
+ code: 'malformed_subscription',
459
+ }));
460
+ break;
461
+ }
462
+ const ack = parsed.data;
463
+ if (ack.success) {
464
+ // Keep the reconnect URL aligned with current interest: a
465
+ // reconnect re-subscribes from `this.options.syncGroups`.
466
+ this.options.syncGroups = ack.syncGroups;
467
+ pending.resolve({ syncGroups: ack.syncGroups });
468
+ }
469
+ else {
470
+ pending.reject(errorFromWire(ack.error?.message ?? 'update_subscription rejected by server', { code: ack.error?.code ?? 'malformed_subscription' }));
471
+ }
472
+ break;
473
+ }
433
474
  case 'claim_expired': {
434
475
  // Server-initiated expiry notification. Emit as a typed
435
476
  // event so consumers can react (re-claim with a fresh
@@ -441,39 +482,39 @@ export class SyncWebSocket extends EventEmitter {
441
482
  }
442
483
  break;
443
484
  }
444
- case 'intent_rejected': {
445
- // Server denied an `intent_begin` because the target is
485
+ case 'claim_rejected': {
486
+ // Server denied an `claim_begin` because the target is
446
487
  // already claimed by another participant. Forward the
447
- // payload as-is — the IntentStream consumer interprets
488
+ // payload as-is — the ClaimStream consumer interprets
448
489
  // the conflict shape (peerId, target, etc.).
449
- this.emit('intent_rejected', message.payload ?? {});
490
+ this.emit('claim_rejected', message.payload ?? {});
450
491
  break;
451
492
  }
452
- case 'intent_acquired': {
493
+ case 'claim_acquired': {
453
494
  // Opt-in fair queue: the target was free, so the lease is ours
454
- // immediately (no waiting). Payload carries { intentId, target }.
455
- this.emit('intent_acquired', message.payload ?? {});
495
+ // immediately (no waiting). Payload carries { claimId, target }.
496
+ this.emit('claim_acquired', message.payload ?? {});
456
497
  break;
457
498
  }
458
- case 'intent_queue': {
499
+ case 'claim_queue': {
459
500
  // Per-entity wait-queue snapshot for reactive `queue(id)`.
460
- this.emit('intent_queue', message.payload ?? {});
501
+ this.emit('claim_queue', message.payload ?? {});
461
502
  break;
462
503
  }
463
- case 'intent_queued': {
504
+ case 'claim_queued': {
464
505
  // Opt-in fair queue: our claim is waiting in line. Payload
465
- // carries { intentId, target, position }.
466
- this.emit('intent_queued', message.payload ?? {});
506
+ // carries { claimId, target, position }.
507
+ this.emit('claim_queued', message.payload ?? {});
467
508
  break;
468
509
  }
469
- case 'intent_granted': {
510
+ case 'claim_granted': {
470
511
  // Our queued claim reached the head — the lease is now ours.
471
- this.emit('intent_granted', message.payload ?? {});
512
+ this.emit('claim_granted', message.payload ?? {});
472
513
  break;
473
514
  }
474
- case 'intent_lost': {
515
+ case 'claim_lost': {
475
516
  // A held/granted claim was taken from us (TTL lapse, revoke).
476
- this.emit('intent_lost', message.payload ?? {});
517
+ this.emit('claim_lost', message.payload ?? {});
477
518
  break;
478
519
  }
479
520
  case 'delta': {
@@ -585,6 +626,17 @@ export class SyncWebSocket extends EventEmitter {
585
626
  }
586
627
  this.pendingClaims.clear();
587
628
  }
629
+ // Cancel in-flight subscription updates — the reconnect handshake
630
+ // re-sends `options.syncGroups` (the last acked interest) in the
631
+ // upgrade URL, so a pending change that never acked is simply
632
+ // retried by the caller against the fresh connection.
633
+ if (this.pendingSubscriptions.length > 0) {
634
+ for (const pending of this.pendingSubscriptions) {
635
+ clearTimeout(pending.timeout);
636
+ pending.reject(new AbloConnectionError(`WebSocket closed while update_subscription was in flight (code=${event.code})`));
637
+ }
638
+ this.pendingSubscriptions = [];
639
+ }
588
640
  // Check for session-related close codes
589
641
  // 1008 = Policy Violation (often auth)
590
642
  // 4001 = Unauthorized (custom)
@@ -702,7 +754,6 @@ export class SyncWebSocket extends EventEmitter {
702
754
  type: 'ack',
703
755
  payload: {
704
756
  lastSyncId: syncId,
705
- versions: this.versionVector,
706
757
  },
707
758
  });
708
759
  }
@@ -887,13 +938,13 @@ export class SyncWebSocket extends EventEmitter {
887
938
  sendRelease(claimId) {
888
939
  // Cancel any in-flight claim that hadn't acked yet — the user
889
940
  // changed their mind. Without this the timer would eventually
890
- // reject; doing it now matches the user's intent immediately.
941
+ // reject; doing it now matches the user's claim immediately.
891
942
  const pending = this.pendingClaims.get(claimId);
892
943
  if (pending) {
893
944
  clearTimeout(pending.timeout);
894
945
  this.pendingClaims.delete(claimId);
895
946
  pending.reject(new AbloError(`claim ${claimId} released before ack`, {
896
- code: 'intent_wait_aborted',
947
+ code: 'claim_wait_aborted',
897
948
  httpStatus: 409,
898
949
  }));
899
950
  }
@@ -906,6 +957,56 @@ export class SyncWebSocket extends EventEmitter {
906
957
  // Idempotent contract — silent failure is acceptable here.
907
958
  }
908
959
  }
960
+ /**
961
+ * Move this connection's READ interest — replace the connection-level
962
+ * sync groups mid-session as the user opens/closes entities. This is the
963
+ * area-of-interest (AOI) navigation primitive: the server fans out
964
+ * deltas only for groups currently in view, instead of the frozen set
965
+ * chosen at connect.
966
+ *
967
+ * Full-set replace semantics — pass the complete new group list, not a
968
+ * delta. Resolves with the server's effective set once `subscription_ack`
969
+ * arrives; rejects (typed) on a scope denial (a restricted `rk_` key
970
+ * requesting a group outside its allowlist), timeout, or disconnect. On
971
+ * success the new set is recorded as `options.syncGroups` so a later
972
+ * reconnect re-subscribes to current interest, not the connect-time set.
973
+ *
974
+ * Distinct from {@link sendClaim} (write-claim, per-op, TTL'd) — this is
975
+ * the read side and carries no capability token of its own; it's bounded
976
+ * by the connection credential's grant.
977
+ */
978
+ updateSubscription(syncGroups, options) {
979
+ if (this.ws?.readyState !== WebSocket.OPEN) {
980
+ return Promise.reject(this.notConnectedError('update_subscription'));
981
+ }
982
+ const timeoutMs = options?.timeoutMs ?? 15_000;
983
+ return new Promise((resolve, reject) => {
984
+ const entry = {
985
+ resolve,
986
+ reject,
987
+ timeout: setTimeout(() => {
988
+ const idx = this.pendingSubscriptions.indexOf(entry);
989
+ if (idx !== -1)
990
+ this.pendingSubscriptions.splice(idx, 1);
991
+ reject(new AbloConnectionError(`update_subscription timed out after ${timeoutMs}ms`, { code: 'wait_for_timeout' }));
992
+ }, timeoutMs),
993
+ };
994
+ this.pendingSubscriptions.push(entry);
995
+ try {
996
+ this.ws.send(JSON.stringify({
997
+ type: 'update_subscription',
998
+ payload: { syncGroups: [...syncGroups] },
999
+ }));
1000
+ }
1001
+ catch (error) {
1002
+ clearTimeout(entry.timeout);
1003
+ const idx = this.pendingSubscriptions.indexOf(entry);
1004
+ if (idx !== -1)
1005
+ this.pendingSubscriptions.splice(idx, 1);
1006
+ reject(toAbloError(error));
1007
+ }
1008
+ });
1009
+ }
909
1010
  /**
910
1011
  * Compatibility setter for direct SyncWebSocket users. The SDK-owned
911
1012
  * `Ablo()` path passes `getAuthToken`, so reconnect URL auth reads the
@@ -1287,9 +1388,7 @@ export class SyncWebSocket extends EventEmitter {
1287
1388
  this.send({
1288
1389
  type: 'sync_request',
1289
1390
  payload: {
1290
- cursor: this.syncCursor,
1291
- versions: this.versionVector,
1292
- // Always send lastSyncId to ensure server uses client's current position
1391
+ cursor: this.syncCursor, // Always send lastSyncId to ensure server uses client's current position
1293
1392
  lastSyncId: this.lastSyncId,
1294
1393
  capabilities: capsArr,
1295
1394
  },
@@ -1309,9 +1408,7 @@ export class SyncWebSocket extends EventEmitter {
1309
1408
  this.send({
1310
1409
  type: 'bootstrap_request',
1311
1410
  payload: {
1312
- entities: entities || [],
1313
- versions: this.versionVector,
1314
- capabilities: this.options.capabilities,
1411
+ entities: entities || [], capabilities: this.options.capabilities,
1315
1412
  },
1316
1413
  });
1317
1414
  }
@@ -1416,7 +1513,7 @@ export class SyncWebSocket extends EventEmitter {
1416
1513
  *
1417
1514
  * Wire frame (apps/sync-server/src/hub/types.ts PresenceUpdateMessage):
1418
1515
  * { type: 'presence_update', payload: { kind, userId, status,
1419
- * syncGroups, activity, isAgent, timestamp, activeIntents } }
1516
+ * syncGroups, activity, isAgent, timestamp, activeClaims } }
1420
1517
  */
1421
1518
  handlePresenceUpdate(message) {
1422
1519
  const event =
@@ -0,0 +1,40 @@
1
+ /**
2
+ * `awaitClaimGrant` — the client side of the fair-queue handover.
3
+ *
4
+ * When a `claim` is contended, the server enqueues it and replies `queued`
5
+ * (HTTP 202 on `/v1/claims`, or `claim_queued` over WS). The grant is then
6
+ * PUSHED later over the WS as `claim_granted` when the claim reaches the head.
7
+ * This resolves once that frame arrives for our `claimId` — so the caller's
8
+ * `claim` promise stays pending (event-driven; no poll, no race) until it's
9
+ * actually our turn. Rejects on `claim_lost` (surfaced as `claim_lost`: the claim was taken away — TTL
10
+ * lapse on disconnect, revoke) or an optional timeout.
11
+ *
12
+ * Takes only a minimal `{ subscribe }` transport so it unit-tests against a
13
+ * fake; `SyncWebSocket` satisfies it structurally.
14
+ */
15
+ export interface GrantTransport {
16
+ subscribe(event: 'claim_acquired' | 'claim_granted' | 'claim_lost' | 'claim_queued' | 'claim_rejected', handler: (payload: Record<string, unknown>) => void): () => void;
17
+ }
18
+ export interface ClaimGrantInfo {
19
+ /**
20
+ * True when the grant arrived as `claim_granted` — i.e. the target was
21
+ * HELD when we asked and we waited in the FIFO line behind the holder.
22
+ * False for the immediate `claim_acquired` (target was free).
23
+ *
24
+ * Callers use this to know the row may have changed while we queued:
25
+ * claim VISIBILITY is entity-scoped (org-wide subscriptions receive no
26
+ * presence/claim fan-out — see Hub.broadcastPresenceChange), so the
27
+ * local coordination snapshot cannot be trusted to detect "we waited".
28
+ * The grant frame itself is the authoritative signal.
29
+ */
30
+ readonly waited: boolean;
31
+ }
32
+ export declare function awaitClaimGrant(transport: GrantTransport, claimId: string, options?: {
33
+ timeoutMs?: number;
34
+ /**
35
+ * Backpressure: reject instead of waiting if, when we join the line, the
36
+ * server reports `position >= maxQueueDepth` (i.e. that many claims are
37
+ * already ahead of us). Omit to wait however deep the queue is.
38
+ */
39
+ maxQueueDepth?: number;
40
+ }): Promise<ClaimGrantInfo>;
@@ -0,0 +1,86 @@
1
+ /**
2
+ * `awaitClaimGrant` — the client side of the fair-queue handover.
3
+ *
4
+ * When a `claim` is contended, the server enqueues it and replies `queued`
5
+ * (HTTP 202 on `/v1/claims`, or `claim_queued` over WS). The grant is then
6
+ * PUSHED later over the WS as `claim_granted` when the claim reaches the head.
7
+ * This resolves once that frame arrives for our `claimId` — so the caller's
8
+ * `claim` promise stays pending (event-driven; no poll, no race) until it's
9
+ * actually our turn. Rejects on `claim_lost` (surfaced as `claim_lost`: the claim was taken away — TTL
10
+ * lapse on disconnect, revoke) or an optional timeout.
11
+ *
12
+ * Takes only a minimal `{ subscribe }` transport so it unit-tests against a
13
+ * fake; `SyncWebSocket` satisfies it structurally.
14
+ */
15
+ import { AbloClaimedError, formatClaimedErrorMessage, claimTargetLabel, } from '../errors.js';
16
+ export function awaitClaimGrant(transport, claimId, options) {
17
+ return new Promise((resolve, reject) => {
18
+ const unsubs = [];
19
+ let timer;
20
+ const settle = (fn) => {
21
+ if (timer)
22
+ clearTimeout(timer);
23
+ for (const u of unsubs)
24
+ u();
25
+ fn();
26
+ };
27
+ // The target was free → `claim_acquired` (immediate); it was contended,
28
+ // we waited in line, and reached the head → `claim_granted`. Either frame
29
+ // means the lease is now ours; `waited` records which path it was.
30
+ unsubs.push(transport.subscribe('claim_acquired', (p) => {
31
+ if (p?.claimId === claimId)
32
+ settle(() => resolve({ waited: false }));
33
+ }));
34
+ unsubs.push(transport.subscribe('claim_granted', (p) => {
35
+ if (p?.claimId === claimId)
36
+ settle(() => resolve({ waited: true }));
37
+ }));
38
+ if (options?.maxQueueDepth !== undefined) {
39
+ const max = options.maxQueueDepth;
40
+ unsubs.push(transport.subscribe('claim_queued', (p) => {
41
+ if (p?.claimId !== claimId)
42
+ return;
43
+ const position = typeof p.position === 'number' ? p.position : 0;
44
+ if (position >= max) {
45
+ settle(() => reject(new AbloClaimedError(`Claim queue for ${claimId} is ${position} deep (max ${max}).`, { code: 'queue_too_deep' })));
46
+ }
47
+ }));
48
+ }
49
+ unsubs.push(transport.subscribe('claim_rejected', (p) => {
50
+ const rejection = p;
51
+ if (rejection.claimId !== claimId)
52
+ return;
53
+ const target = rejection.target
54
+ ? claimTargetLabel({
55
+ model: rejection.target.entityType,
56
+ id: rejection.target.entityId,
57
+ field: rejection.target.field,
58
+ })
59
+ : claimId;
60
+ settle(() => reject(new AbloClaimedError(formatClaimedErrorMessage({
61
+ targetLabel: target,
62
+ heldBy: rejection.heldBy,
63
+ claim: rejection.heldByClaim,
64
+ policyReason: rejection.policyReason,
65
+ fallback: `Claim rejected for ${target}.`,
66
+ }), {
67
+ code: rejection.reason === 'conflict'
68
+ ? 'claim_conflict'
69
+ : 'claim_lease_unavailable',
70
+ claims: rejection.heldByClaim ? [rejection.heldByClaim] : undefined,
71
+ })));
72
+ }));
73
+ unsubs.push(transport.subscribe('claim_lost', (p) => {
74
+ if (p?.claimId === claimId) {
75
+ settle(() => reject(new AbloClaimedError(`Claim lost while queued for ${claimId}.`, {
76
+ code: 'claim_lost',
77
+ })));
78
+ }
79
+ }));
80
+ if (options?.timeoutMs && options.timeoutMs > 0) {
81
+ timer = setTimeout(() => {
82
+ settle(() => reject(new AbloClaimedError(`Timed out waiting for the queue grant on claim ${claimId}.`, { code: 'grant_timeout' })));
83
+ }, options.timeoutMs);
84
+ }
85
+ });
86
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Transport-driven ClaimStream factory.
3
+ *
4
+ * Mirrors `createPresenceStream` — built directly on `SyncWebSocket`,
5
+ * no SyncAgent wrapper. Claims derive their `others` view from the
6
+ * same `presence_update` frames the presence stream consumes (the
7
+ * Hub piggybacks `activeClaims` on every presence frame). Outbound
8
+ * announce/revoke ride the same socket via `claim_begin` /
9
+ * `claim_abandon` frames.
10
+ *
11
+ * Wire contract (apps/sync-server/src/hub/types.ts):
12
+ * • Outbound: `{ type: 'claim_begin', payload: { claimId,
13
+ * entityType, entityId, action, field?, estimatedMs? } }`
14
+ * • Outbound: `{ type: 'claim_abandon', payload: { claimId,
15
+ * entityType?, entityId? } }`
16
+ * • Inbound (via presence): `event.activeClaims: Claim[]`
17
+ * stamped with `declaredAt`, `expiresAt`.
18
+ * • Inbound: `claim_rejected` event with conflict metadata.
19
+ *
20
+ * After the dual-engine collapse (step #36), this is the only
21
+ * ClaimStream factory in the SDK; the older compatibility path
22
+ * deletes.
23
+ */
24
+ import type { SyncWebSocket } from './SyncWebSocket.js';
25
+ import type { ClaimStream } from '../types/streams.js';
26
+ export interface ClaimStreamConfig {
27
+ /** Identity used to filter our own active claims out of `others`. */
28
+ participantId: string;
29
+ }
30
+ export interface AttachableClaimStream extends ClaimStream {
31
+ attach(transport: SyncWebSocket): void;
32
+ dispose(): void;
33
+ }
34
+ export declare function createClaimStream(config: ClaimStreamConfig, transport?: SyncWebSocket | null): AttachableClaimStream;