@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
@@ -1,37 +1,38 @@
1
1
  /**
2
- * Transport-driven IntentStream factory.
2
+ * Transport-driven ClaimStream factory.
3
3
  *
4
4
  * Mirrors `createPresenceStream` — built directly on `SyncWebSocket`,
5
- * no SyncAgent wrapper. Intents derive their `others` view from the
5
+ * no SyncAgent wrapper. Claims derive their `others` view from the
6
6
  * same `presence_update` frames the presence stream consumes (the
7
- * Hub piggybacks `activeIntents` on every presence frame). Outbound
8
- * announce/revoke ride the same socket via `intent_begin` /
9
- * `intent_abandon` frames.
7
+ * Hub piggybacks `activeClaims` on every presence frame). Outbound
8
+ * announce/revoke ride the same socket via `claim_begin` /
9
+ * `claim_abandon` frames.
10
10
  *
11
11
  * Wire contract (apps/sync-server/src/hub/types.ts):
12
- * • Outbound: `{ type: 'intent_begin', payload: { intentId,
12
+ * • Outbound: `{ type: 'claim_begin', payload: { claimId,
13
13
  * entityType, entityId, action, field?, estimatedMs? } }`
14
- * • Outbound: `{ type: 'intent_abandon', payload: { intentId,
14
+ * • Outbound: `{ type: 'claim_abandon', payload: { claimId,
15
15
  * entityType?, entityId? } }`
16
- * • Inbound (via presence): `event.activeIntents: IntentClaim[]`
16
+ * • Inbound (via presence): `event.activeClaims: Claim[]`
17
17
  * stamped with `declaredAt`, `expiresAt`.
18
- * • Inbound: `intent_rejected` event with conflict metadata.
18
+ * • Inbound: `claim_rejected` event with conflict metadata.
19
19
  *
20
20
  * After the dual-engine collapse (step #36), this is the only
21
- * IntentStream factory in the SDK; the older compatibility path
21
+ * ClaimStream factory in the SDK; the older compatibility path
22
22
  * deletes.
23
23
  */
24
24
  import { asyncIteratorFrom } from '../utils/asyncIterator.js';
25
25
  import { toMs } from '../utils/duration.js';
26
- export function createIntentStream(config, transport = null) {
26
+ import { descriptionFromMeta, participantKindFromWire, } from '../coordination/schema.js';
27
+ export function createClaimStream(config, transport = null) {
27
28
  const { participantId } = config;
28
- // ── State: others' open intents, keyed by intentId ───────────────
29
- const activeByIntentId = new Map();
30
- let intentsSnapshot = Object.freeze([]);
31
- // ── State: our own open intents (for re-announce on reconnect) ───
32
- const ownIntents = new Map();
33
- // ── State: per-entity wait queues, from `intent_queue` frames ────
34
- // Keyed `type:id`; the value is the FIFO line of queued intents. Powers
29
+ // ── State: others' open claims, keyed by claimId ───────────────
30
+ const activeByClaimId = new Map();
31
+ let claimsSnapshot = Object.freeze([]);
32
+ // ── State: our own open claims (for re-announce on reconnect) ───
33
+ const ownClaims = new Map();
34
+ // ── State: per-entity wait queues, from `claim_queue` frames ────
35
+ // Keyed `type:id`; the value is the FIFO line of queued claims. Powers
35
36
  // the reactive `queue(target)` read — who's waiting and what they intend.
36
37
  const queueByEntity = new Map();
37
38
  const entityKey = (type, id) => `${type}:${id}`;
@@ -41,7 +42,7 @@ export function createIntentStream(config, transport = null) {
41
42
  const rejectionListeners = new Set();
42
43
  const lostListeners = new Set();
43
44
  const notifyListeners = () => {
44
- intentsSnapshot = Object.freeze(Array.from(activeByIntentId.values()));
45
+ claimsSnapshot = Object.freeze(Array.from(activeByClaimId.values()));
45
46
  for (const l of listeners) {
46
47
  try {
47
48
  l();
@@ -59,9 +60,9 @@ export function createIntentStream(config, transport = null) {
59
60
  return;
60
61
  attached = t;
61
62
  // (1) Inbound presence frames carry every participant's full
62
- // active-intent set. Prune previous claims by holder, then
63
+ // active-claim set. Prune previous claims by holder, then
63
64
  // re-add from the frame — the frame is authoritative for that
64
- // participant's open intents at that moment.
65
+ // participant's open claims at that moment.
65
66
  unsubs.push(t.subscribe('presence_update', (event) => {
66
67
  if (!event.userId)
67
68
  return;
@@ -69,9 +70,9 @@ export function createIntentStream(config, transport = null) {
69
70
  return;
70
71
  let mutated = false;
71
72
  if (event.kind === 'leave') {
72
- for (const [id, intent] of activeByIntentId) {
73
- if (intent.heldBy === event.userId) {
74
- activeByIntentId.delete(id);
73
+ for (const [id, claim] of activeByClaimId) {
74
+ if (claim.heldBy === event.userId) {
75
+ activeByClaimId.delete(id);
75
76
  mutated = true;
76
77
  }
77
78
  }
@@ -79,13 +80,13 @@ export function createIntentStream(config, transport = null) {
79
80
  notifyListeners();
80
81
  return;
81
82
  }
82
- for (const [id, intent] of activeByIntentId) {
83
- if (intent.heldBy === event.userId) {
84
- activeByIntentId.delete(id);
83
+ for (const [id, claim] of activeByClaimId) {
84
+ if (claim.heldBy === event.userId) {
85
+ activeByClaimId.delete(id);
85
86
  mutated = true;
86
87
  }
87
88
  }
88
- for (const claim of event.activeIntents ?? []) {
89
+ for (const claim of event.activeClaims ?? []) {
89
90
  // Terminal-status entries (committed / expired / canceled) are
90
91
  // one-shot "this claim ended" signals. The holder sweep above
91
92
  // already removed the prior active entry; skipping the re-add
@@ -93,13 +94,11 @@ export function createIntentStream(config, transport = null) {
93
94
  // `settled()`. Absent status means active (wire back-compat).
94
95
  if (claim.status && claim.status !== 'active')
95
96
  continue;
96
- const description = typeof claim.meta?.description === 'string'
97
- ? claim.meta.description
98
- : undefined;
99
- activeByIntentId.set(claim.intentId, {
100
- id: claim.intentId,
97
+ const description = descriptionFromMeta(claim.meta);
98
+ activeByClaimId.set(claim.claimId, {
99
+ id: claim.claimId,
101
100
  heldBy: event.userId,
102
- participantKind: event.isAgent ? 'agent' : 'human',
101
+ participantKind: participantKindFromWire(event.participantKind, event.isAgent),
103
102
  target: {
104
103
  type: claim.entityType,
105
104
  id: claim.entityId,
@@ -111,8 +110,8 @@ export function createIntentStream(config, transport = null) {
111
110
  reason: claim.action,
112
111
  ...(description ? { description } : {}),
113
112
  ttlSeconds: Math.max(0, Math.floor((claim.expiresAt - Date.now()) / 1000)),
114
- announcedAt: new Date(claim.declaredAt).toISOString(),
115
- expiresAt: new Date(claim.expiresAt).toISOString(),
113
+ announcedAt: claim.declaredAt,
114
+ expiresAt: claim.expiresAt,
116
115
  });
117
116
  mutated = true;
118
117
  }
@@ -120,14 +119,13 @@ export function createIntentStream(config, transport = null) {
120
119
  notifyListeners();
121
120
  }));
122
121
  // (2) Server-side rejection frames.
123
- unsubs.push(t.subscribe('intent_rejected', (payload) => {
124
- const rejection = payload;
125
- if (!rejection.intentId)
122
+ unsubs.push(t.subscribe('claim_rejected', (rejection) => {
123
+ if (!rejection.claimId)
126
124
  return;
127
125
  // Drop the rejected own-claim so reconnect doesn't re-announce
128
126
  // a claim the server already rejected (would just spam both
129
127
  // sides with conflicts).
130
- ownIntents.delete(rejection.intentId);
128
+ ownClaims.delete(rejection.claimId);
131
129
  for (const l of rejectionListeners) {
132
130
  try {
133
131
  l(rejection);
@@ -139,13 +137,13 @@ export function createIntentStream(config, transport = null) {
139
137
  }));
140
138
  // (2a) Server-side LOSS frames — you held it, then lost it (preempted /
141
139
  // expired). Distinct from a rejection (a claim the server refused).
142
- unsubs.push(t.subscribe('intent_lost', (payload) => {
140
+ unsubs.push(t.subscribe('claim_lost', (payload) => {
143
141
  const lost = payload;
144
- if (!lost.intentId)
142
+ if (!lost.claimId)
145
143
  return;
146
144
  // Drop the lost own-claim so reconnect doesn't re-announce a lease we
147
145
  // no longer hold.
148
- ownIntents.delete(lost.intentId);
146
+ ownClaims.delete(lost.claimId);
149
147
  for (const l of lostListeners) {
150
148
  try {
151
149
  l(lost);
@@ -158,7 +156,7 @@ export function createIntentStream(config, transport = null) {
158
156
  // (2b) Per-entity wait-queue snapshots. The server fans the full line
159
157
  // out on every queue mutation; we replace our cached line for that
160
158
  // entity and notify so `queue(target)` reads reactively.
161
- unsubs.push(t.subscribe('intent_queue', (payload) => {
159
+ unsubs.push(t.subscribe('claim_queue', (payload) => {
162
160
  const p = payload;
163
161
  if (!p.target?.type || !p.target.id)
164
162
  return;
@@ -171,34 +169,34 @@ export function createIntentStream(config, transport = null) {
171
169
  notifyListeners();
172
170
  }));
173
171
  // (3) On reconnect, re-announce every open self-claim — the
174
- // server's intent state is in-memory and is lost across
172
+ // server's claim state is in-memory and is lost across
175
173
  // restarts. Without this, peers would see our claims vanish
176
174
  // whenever the connection blipped.
177
175
  unsubs.push(t.subscribe('connected', () => {
178
- for (const [intentId, intent] of ownIntents) {
179
- sendBegin(intentId, intent);
176
+ for (const [claimId, claim] of ownClaims) {
177
+ sendBegin(claimId, claim);
180
178
  }
181
179
  }));
182
180
  }
183
181
  if (transport)
184
182
  attach(transport);
185
183
  // ── Outbound ────────────────────────────────────────────────────
186
- function sendBegin(intentId, intent) {
184
+ function sendBegin(claimId, claim) {
187
185
  if (!attached?.isConnected())
188
186
  return;
189
187
  attached.send({
190
- type: 'intent_begin',
188
+ type: 'claim_begin',
191
189
  payload: {
192
- intentId,
193
- entityType: intent.entityType,
194
- entityId: intent.entityId,
195
- path: intent.path,
196
- range: intent.range,
197
- action: intent.action,
198
- field: intent.field,
199
- meta: intent.meta,
200
- estimatedMs: intent.estimatedMs,
201
- queue: intent.queue,
190
+ claimId,
191
+ entityType: claim.entityType,
192
+ entityId: claim.entityId,
193
+ path: claim.path,
194
+ range: claim.range,
195
+ action: claim.action,
196
+ field: claim.field,
197
+ meta: claim.meta,
198
+ estimatedMs: claim.estimatedMs,
199
+ queue: claim.queue,
202
200
  },
203
201
  });
204
202
  }
@@ -206,28 +204,28 @@ export function createIntentStream(config, transport = null) {
206
204
  if (!attached?.isConnected())
207
205
  return;
208
206
  attached.send({
209
- type: 'intent_reorder',
207
+ type: 'claim_reorder',
210
208
  payload: {
211
209
  entityType,
212
210
  entityId,
213
- // The wire shape identifies a waiter by heldBy + intentId; map the
214
- // ergonomic `Intent[]` (what `queueFor` returns) down to that.
215
- order: order.map((i) => ({ heldBy: i.heldBy, intentId: i.id })),
211
+ // The wire shape identifies a waiter by heldBy + claimId; map the
212
+ // ergonomic `Claim[]` (what `queueFor` returns) down to that.
213
+ order: order.map((i) => ({ heldBy: i.heldBy, claimId: i.id })),
216
214
  },
217
215
  });
218
216
  }
219
- function sendAbandon(intentId, intent) {
217
+ function sendAbandon(claimId, claim) {
220
218
  if (!attached?.isConnected())
221
219
  return;
222
220
  // Carry the target so the server can dequeue us if we were only *waiting*
223
221
  // (a queued claim isn't in the holder set it would otherwise scan). Held
224
- // claims are found by intentId regardless; the target is harmless there.
222
+ // claims are found by claimId regardless; the target is harmless there.
225
223
  attached.send({
226
- type: 'intent_abandon',
224
+ type: 'claim_abandon',
227
225
  payload: {
228
- intentId,
229
- entityType: intent?.entityType,
230
- entityId: intent?.entityId,
226
+ claimId,
227
+ entityType: claim?.entityType,
228
+ entityId: claim?.entityId,
231
229
  },
232
230
  });
233
231
  }
@@ -237,9 +235,9 @@ export function createIntentStream(config, transport = null) {
237
235
  return { ...(meta ?? {}), description };
238
236
  }
239
237
  function mintHandle(args) {
240
- const intentId = crypto.randomUUID();
238
+ const claimId = crypto.randomUUID();
241
239
  const estimatedMs = args.ttl !== undefined ? toMs(args.ttl) : undefined;
242
- const intent = {
240
+ const claim = {
243
241
  entityType: args.entityType,
244
242
  entityId: args.entityId,
245
243
  path: args.path,
@@ -250,18 +248,31 @@ export function createIntentStream(config, transport = null) {
250
248
  estimatedMs,
251
249
  queue: args.queue,
252
250
  };
253
- ownIntents.set(intentId, intent);
254
- sendBegin(intentId, intent);
251
+ ownClaims.set(claimId, claim);
252
+ sendBegin(claimId, claim);
255
253
  let revoked = false;
256
254
  const revoke = () => {
257
255
  if (revoked)
258
256
  return;
259
257
  revoked = true;
260
- ownIntents.delete(intentId);
261
- sendAbandon(intentId, intent);
258
+ ownClaims.delete(claimId);
259
+ sendAbandon(claimId, claim);
262
260
  };
263
261
  return {
264
- id: intentId,
262
+ object: 'claim',
263
+ claimId,
264
+ action: args.action,
265
+ target: {
266
+ model: args.entityType,
267
+ id: args.entityId,
268
+ path: args.path,
269
+ range: args.range,
270
+ field: args.field,
271
+ meta: args.meta,
272
+ },
273
+ release: async () => {
274
+ revoke();
275
+ },
265
276
  revoke,
266
277
  [Symbol.asyncDispose]: async () => {
267
278
  revoke();
@@ -289,7 +300,7 @@ export function createIntentStream(config, transport = null) {
289
300
  });
290
301
  },
291
302
  get others() {
292
- return intentsSnapshot;
303
+ return claimsSnapshot;
293
304
  },
294
305
  queueFor(target) {
295
306
  const ref = resolveTarget(target);
@@ -323,7 +334,7 @@ export function createIntentStream(config, transport = null) {
323
334
  return () => {
324
335
  listeners.delete(onChange);
325
336
  };
326
- }, () => intentsSnapshot);
337
+ }, () => claimsSnapshot);
327
338
  },
328
339
  attach,
329
340
  dispose() {
@@ -333,10 +344,10 @@ export function createIntentStream(config, transport = null) {
333
344
  listeners.clear();
334
345
  rejectionListeners.clear();
335
346
  lostListeners.clear();
336
- activeByIntentId.clear();
337
- ownIntents.clear();
347
+ activeByClaimId.clear();
348
+ ownClaims.clear();
338
349
  queueByEntity.clear();
339
- intentsSnapshot = Object.freeze([]);
350
+ claimsSnapshot = Object.freeze([]);
340
351
  attached = null;
341
352
  },
342
353
  };
@@ -22,11 +22,12 @@
22
22
  * • Inbound: same frame, with `kind: 'enter' | 'update' | 'leave'`.
23
23
  */
24
24
  import { asyncIteratorFrom } from '../utils/asyncIterator.js';
25
+ import { participantKindFromWire } from '../coordination/schema.js';
25
26
  export function createPresenceStream(config, transport = null) {
26
27
  const { participantId, label, syncGroups, isAgent = false } = config;
27
28
  // ── Self ─────────────────────────────────────────────────────────
28
29
  const self = {
29
- participantKind: isAgent ? 'agent' : 'human',
30
+ participantKind: isAgent ? 'agent' : 'user',
30
31
  participantId,
31
32
  label,
32
33
  syncGroups: [...syncGroups],
@@ -84,7 +85,7 @@ export function createPresenceStream(config, transport = null) {
84
85
  case 'update':
85
86
  case undefined: {
86
87
  const entry = {
87
- participantKind: event.isAgent ? 'agent' : 'human',
88
+ participantKind: participantKindFromWire(event.participantKind, event.isAgent),
88
89
  participantId: event.userId,
89
90
  syncGroups: event.syncGroups ?? [],
90
91
  activity: event.activity
@@ -1,6 +1,6 @@
1
1
  import type { SyncWebSocket } from './SyncWebSocket.js';
2
2
  import type { Schema, SchemaRecord } from '../schema/schema.js';
3
- import type { ActiveIntent, Activity, EntityRef, Claim, IntentStream, Peer, PresenceStream, PresenceTarget } from '../types/streams.js';
3
+ import type { ActiveClaim, Activity, EntityRef, ClaimHandle, ClaimStream, Peer, PresenceStream, PresenceTarget } from '../types/streams.js';
4
4
  /**
5
5
  * Scope accepted by participant APIs. The normal SDK shape is an
6
6
  * entity target (`{ type, id }`). Raw sync-group strings remain an
@@ -14,7 +14,7 @@ export type ParticipantScope = EntityRef | readonly EntityRef[] | string | reado
14
14
  export type ParticipantStatus = 'idle' | 'connecting' | 'connected' | 'error' | 'disconnected';
15
15
  export interface EngineParticipant {
16
16
  readonly presence: PresenceStream;
17
- readonly intents: IntentStream;
17
+ readonly claims: ClaimStream;
18
18
  }
19
19
  export interface ParticipantJoinOptions {
20
20
  /**
@@ -65,17 +65,17 @@ export interface ScopedClaimOptions {
65
65
  /** TTL — server auto-expires the claim after this. */
66
66
  readonly ttl?: import('../types/streams.js').Duration;
67
67
  }
68
- export interface ScopedIntents {
68
+ export interface ScopedClaims {
69
69
  readonly focus: EntityRef | null;
70
- readonly others: ReadonlyArray<ActiveIntent>;
70
+ readonly others: ReadonlyArray<ActiveClaim>;
71
71
  /**
72
- * Claim an exclusive intent on the participant's focus target (or
72
+ * Claim an exclusive claim on the participant's focus target (or
73
73
  * an explicit override via `opts.target`). Single verb — the old
74
74
  * `editing / writing / announce / claim(reason, opts)` overloads
75
75
  * collapsed into this one method.
76
76
  */
77
- claim(opts?: ScopedClaimOptions): Claim;
78
- onRejected(listener: Parameters<IntentStream['onRejected']>[0]): () => void;
77
+ claim(opts?: ScopedClaimOptions): ClaimHandle;
78
+ onRejected(listener: Parameters<ClaimStream['onRejected']>[0]): () => void;
79
79
  onChange(listener: () => void): () => void;
80
80
  }
81
81
  export interface ParticipantFocusOptions {
@@ -89,9 +89,9 @@ export interface JoinedParticipant {
89
89
  /** Transport scopes this participant is joined to for visibility/fan-out. */
90
90
  readonly syncGroups: readonly string[];
91
91
  readonly presence: ScopedPresence;
92
- readonly intents: ScopedIntents;
92
+ readonly claims: ScopedClaims;
93
93
  readonly peers: ReadonlyArray<Peer>;
94
- readonly claims: ReadonlyArray<ActiveIntent>;
94
+ readonly activeClaims: ReadonlyArray<ActiveClaim>;
95
95
  focus(target: PresenceTarget, options?: ParticipantFocusOptions): JoinedParticipant;
96
96
  leave(): void;
97
97
  [Symbol.asyncDispose](): Promise<void>;
@@ -104,7 +104,7 @@ export interface ParticipantManagerConfig {
104
104
  readonly ready: () => Promise<void>;
105
105
  readonly getTransport: () => SyncWebSocket | null;
106
106
  readonly presence: PresenceStream;
107
- readonly intents: IntentStream;
107
+ readonly claims: ClaimStream;
108
108
  readonly schema?: Schema<SchemaRecord>;
109
109
  }
110
110
  export declare function createParticipantManager(config: ParticipantManagerConfig): ParticipantManager;
@@ -26,7 +26,7 @@ export function createParticipantManager(config) {
26
26
  claimId,
27
27
  transport,
28
28
  presence: config.presence,
29
- intents: config.intents,
29
+ claims: config.claims,
30
30
  });
31
31
  if (target && options.activity !== false) {
32
32
  const activity = options.activity ?? 'reading';
@@ -219,7 +219,14 @@ function createJoinedParticipant(args) {
219
219
  const track = (handle) => {
220
220
  ownHandles.add(handle);
221
221
  return {
222
- id: handle.id,
222
+ object: 'claim',
223
+ claimId: handle.claimId,
224
+ action: handle.action,
225
+ target: handle.target,
226
+ async release() {
227
+ ownHandles.delete(handle);
228
+ await handle.release();
229
+ },
223
230
  revoke() {
224
231
  ownHandles.delete(handle);
225
232
  handle.revoke();
@@ -230,24 +237,24 @@ function createJoinedParticipant(args) {
230
237
  },
231
238
  };
232
239
  };
233
- const scopedIntents = {
240
+ const scopedClaims = {
234
241
  get focus() {
235
242
  return currentTarget;
236
243
  },
237
244
  get others() {
238
- return args.intents.others.filter((intent) => currentTarget ? targetsOverlap(intent.target, currentTarget) : true);
245
+ return args.claims.others.filter((claim) => currentTarget ? targetsOverlap(claim.target, currentTarget) : true);
239
246
  },
240
247
  claim(opts) {
241
- return track(args.intents.claim(requireTarget(opts?.target), {
248
+ return track(args.claims.claim(requireTarget(opts?.target), {
242
249
  reason: opts?.reason,
243
250
  ttl: opts?.ttl,
244
251
  }));
245
252
  },
246
253
  onRejected(listener) {
247
- return args.intents.onRejected(listener);
254
+ return args.claims.onRejected(listener);
248
255
  },
249
256
  onChange(listener) {
250
- return args.intents.onChange(listener);
257
+ return args.claims.onChange(listener);
251
258
  },
252
259
  };
253
260
  const leave = () => {
@@ -272,12 +279,12 @@ function createJoinedParticipant(args) {
272
279
  },
273
280
  syncGroups: [...args.syncGroups],
274
281
  presence: scopedPresence,
275
- intents: scopedIntents,
282
+ claims: scopedClaims,
276
283
  get peers() {
277
284
  return scopedPresence.others;
278
285
  },
279
- get claims() {
280
- return scopedIntents.others;
286
+ get activeClaims() {
287
+ return scopedClaims.others;
281
288
  },
282
289
  focus: setFocus,
283
290
  leave,
@@ -8,24 +8,24 @@ import { z } from 'zod';
8
8
  export declare const ServerDeltaSchema: z.ZodObject<{
9
9
  id: z.ZodNumber;
10
10
  operation: z.ZodOptional<z.ZodEnum<{
11
- A: "A";
12
11
  I: "I";
13
12
  U: "U";
14
13
  D: "D";
14
+ A: "A";
15
+ V: "V";
15
16
  C: "C";
16
17
  G: "G";
17
18
  S: "S";
18
- V: "V";
19
19
  }>>;
20
20
  action: z.ZodOptional<z.ZodEnum<{
21
- A: "A";
22
21
  I: "I";
23
22
  U: "U";
24
23
  D: "D";
24
+ A: "A";
25
+ V: "V";
25
26
  C: "C";
26
27
  G: "G";
27
28
  S: "S";
28
- V: "V";
29
29
  }>>;
30
30
  modelName: z.ZodString;
31
31
  entityId: z.ZodOptional<z.ZodString>;
@@ -43,24 +43,24 @@ export declare const BootstrapResponseSchema: z.ZodObject<{
43
43
  deltas: z.ZodOptional<z.ZodArray<z.ZodObject<{
44
44
  id: z.ZodNumber;
45
45
  operation: z.ZodOptional<z.ZodEnum<{
46
- A: "A";
47
46
  I: "I";
48
47
  U: "U";
49
48
  D: "D";
49
+ A: "A";
50
+ V: "V";
50
51
  C: "C";
51
52
  G: "G";
52
53
  S: "S";
53
- V: "V";
54
54
  }>>;
55
55
  action: z.ZodOptional<z.ZodEnum<{
56
- A: "A";
57
56
  I: "I";
58
57
  U: "U";
59
58
  D: "D";
59
+ A: "A";
60
+ V: "V";
60
61
  C: "C";
61
62
  G: "G";
62
63
  S: "S";
63
- V: "V";
64
64
  }>>;
65
65
  modelName: z.ZodString;
66
66
  entityId: z.ZodOptional<z.ZodString>;
@@ -36,6 +36,8 @@ export interface Transaction {
36
36
  priorityScore: number;
37
37
  writeOptions?: WriteOptions;
38
38
  batchId?: string;
39
+ /** Completed locally without a server operation; no sync echo will arrive. */
40
+ localOnly?: boolean;
39
41
  /** LINEAR PATTERN: syncId threshold - transaction confirms when delta.id >= this value */
40
42
  syncIdNeededForCompletion?: number;
41
43
  /**
@@ -130,11 +132,21 @@ export declare class TransactionQueue extends EventEmitter {
130
132
  private commitScheduled;
131
133
  private inFlightByModel;
132
134
  private pendingMergeByModel;
135
+ private deferredDeletesByCreate;
133
136
  private commitLane;
134
137
  private commitStore;
135
138
  private commitProcessing;
136
139
  private computePriorityScore;
137
140
  private ensureDerivedFields;
141
+ private entityKey;
142
+ private isTransactionForModel;
143
+ private resolveConfirmation;
144
+ private takeUnsentCreateForModel;
145
+ private cancelUnsentCreateForDelete;
146
+ private findCreateBarrierForDelete;
147
+ private completeLocalDelete;
148
+ private deferDeleteUntilCreateSettles;
149
+ private releaseDeferredDeletesForCreate;
138
150
  private mergeUpdateData;
139
151
  private config;
140
152
  private executingCount;