@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.
- package/CHANGELOG.md +16 -0
- package/README.md +2 -1
- package/dist/BaseSyncedStore.d.ts +75 -0
- package/dist/BaseSyncedStore.js +193 -8
- package/dist/Database.d.ts +10 -2
- package/dist/Database.js +15 -1
- package/dist/SyncClient.d.ts +12 -1
- package/dist/SyncClient.js +110 -26
- package/dist/agent/Agent.d.ts +9 -9
- package/dist/agent/Agent.js +16 -16
- package/dist/agent/index.d.ts +1 -1
- package/dist/agent/index.js +2 -2
- package/dist/agent/types.d.ts +1 -1
- package/dist/agent/types.js +1 -1
- package/dist/ai-sdk/{intent-broadcast.d.ts → claim-broadcast.d.ts} +10 -10
- package/dist/ai-sdk/{intent-broadcast.js → claim-broadcast.js} +6 -6
- package/dist/ai-sdk/coordination-context.d.ts +9 -9
- package/dist/ai-sdk/coordination-context.js +8 -8
- package/dist/ai-sdk/index.d.ts +1 -1
- package/dist/ai-sdk/index.js +1 -1
- package/dist/ai-sdk/wrap.d.ts +4 -4
- package/dist/ai-sdk/wrap.js +4 -4
- package/dist/api/index.d.ts +2 -2
- package/dist/cli.cjs +254 -48
- package/dist/client/Ablo.d.ts +30 -63
- package/dist/client/Ablo.js +108 -102
- package/dist/client/ApiClient.d.ts +6 -5
- package/dist/client/ApiClient.js +83 -62
- package/dist/client/createModelProxy.d.ts +16 -54
- package/dist/client/createModelProxy.js +44 -16
- package/dist/client/httpClient.d.ts +2 -0
- package/dist/client/httpClient.js +1 -1
- package/dist/client/index.d.ts +3 -3
- package/dist/client/writeOptionsSchema.d.ts +4 -4
- package/dist/client/writeOptionsSchema.js +4 -4
- package/dist/coordination/schema.d.ts +249 -38
- package/dist/coordination/schema.js +172 -39
- package/dist/core/index.d.ts +2 -2
- package/dist/core/index.js +4 -4
- package/dist/errorCodes.d.ts +9 -9
- package/dist/errorCodes.js +15 -15
- package/dist/errors.d.ts +51 -2
- package/dist/errors.js +94 -5
- package/dist/interfaces/index.d.ts +8 -4
- package/dist/policy/index.d.ts +1 -1
- package/dist/policy/types.d.ts +13 -13
- package/dist/policy/types.js +8 -8
- package/dist/react/AbloProvider.d.ts +51 -4
- package/dist/react/AbloProvider.js +95 -11
- package/dist/react/context.d.ts +26 -9
- package/dist/react/context.js +2 -2
- package/dist/react/index.d.ts +4 -4
- package/dist/react/index.js +4 -4
- package/dist/react/useAblo.js +5 -5
- package/dist/react/{useIntent.d.ts → useClaim.d.ts} +9 -9
- package/dist/react/useClaim.js +42 -0
- package/dist/schema/index.js +1 -1
- package/dist/schema/sugar.d.ts +3 -3
- package/dist/schema/sugar.js +3 -3
- package/dist/schema/sync-delta-wire.d.ts +8 -8
- package/dist/server/commit.d.ts +2 -2
- package/dist/sync/AreaOfInterestManager.d.ts +162 -0
- package/dist/sync/AreaOfInterestManager.js +233 -0
- package/dist/sync/BootstrapHelper.d.ts +9 -1
- package/dist/sync/BootstrapHelper.js +15 -5
- package/dist/sync/NetworkProbe.d.ts +1 -1
- package/dist/sync/NetworkProbe.js +1 -1
- package/dist/sync/SyncWebSocket.d.ts +59 -25
- package/dist/sync/SyncWebSocket.js +123 -26
- package/dist/sync/awaitClaimGrant.d.ts +40 -0
- package/dist/sync/awaitClaimGrant.js +86 -0
- package/dist/sync/createClaimStream.d.ts +34 -0
- package/dist/sync/{createIntentStream.js → createClaimStream.js} +92 -81
- package/dist/sync/createPresenceStream.js +3 -2
- package/dist/sync/participants.d.ts +10 -10
- package/dist/sync/participants.js +17 -10
- package/dist/sync/schemas.d.ts +8 -8
- package/dist/transactions/TransactionQueue.d.ts +12 -0
- package/dist/transactions/TransactionQueue.js +126 -8
- package/dist/types/global.d.ts +10 -10
- package/dist/types/global.js +3 -3
- package/dist/types/index.d.ts +9 -7
- package/dist/types/index.js +2 -2
- package/dist/types/streams.d.ts +114 -98
- package/dist/types/streams.js +1 -1
- package/dist/utils/asyncIterator.d.ts +1 -1
- package/dist/utils/asyncIterator.js +1 -1
- package/dist/wire/frames.d.ts +2 -2
- package/docs/migration.md +52 -0
- package/package.json +3 -2
- package/dist/react/useIntent.js +0 -42
- package/dist/sync/awaitIntentGrant.d.ts +0 -40
- package/dist/sync/awaitIntentGrant.js +0 -62
- package/dist/sync/createIntentStream.d.ts +0 -34
|
@@ -1,37 +1,38 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Transport-driven
|
|
2
|
+
* Transport-driven ClaimStream factory.
|
|
3
3
|
*
|
|
4
4
|
* Mirrors `createPresenceStream` — built directly on `SyncWebSocket`,
|
|
5
|
-
* no SyncAgent wrapper.
|
|
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 `
|
|
8
|
-
* announce/revoke ride the same socket via `
|
|
9
|
-
* `
|
|
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: '
|
|
12
|
+
* • Outbound: `{ type: 'claim_begin', payload: { claimId,
|
|
13
13
|
* entityType, entityId, action, field?, estimatedMs? } }`
|
|
14
|
-
* • Outbound: `{ type: '
|
|
14
|
+
* • Outbound: `{ type: 'claim_abandon', payload: { claimId,
|
|
15
15
|
* entityType?, entityId? } }`
|
|
16
|
-
* • Inbound (via presence): `event.
|
|
16
|
+
* • Inbound (via presence): `event.activeClaims: Claim[]`
|
|
17
17
|
* stamped with `declaredAt`, `expiresAt`.
|
|
18
|
-
* • Inbound: `
|
|
18
|
+
* • Inbound: `claim_rejected` event with conflict metadata.
|
|
19
19
|
*
|
|
20
20
|
* After the dual-engine collapse (step #36), this is the only
|
|
21
|
-
*
|
|
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
|
-
|
|
26
|
+
import { descriptionFromMeta, participantKindFromWire, } from '../coordination/schema.js';
|
|
27
|
+
export function createClaimStream(config, transport = null) {
|
|
27
28
|
const { participantId } = config;
|
|
28
|
-
// ── State: others' open
|
|
29
|
-
const
|
|
30
|
-
let
|
|
31
|
-
// ── State: our own open
|
|
32
|
-
const
|
|
33
|
-
// ── State: per-entity wait queues, from `
|
|
34
|
-
// Keyed `type:id`; the value is the FIFO line of queued
|
|
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
|
-
|
|
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-
|
|
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
|
|
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,
|
|
73
|
-
if (
|
|
74
|
-
|
|
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,
|
|
83
|
-
if (
|
|
84
|
-
|
|
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.
|
|
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 =
|
|
97
|
-
|
|
98
|
-
:
|
|
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
|
|
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:
|
|
115
|
-
expiresAt:
|
|
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('
|
|
124
|
-
|
|
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
|
-
|
|
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('
|
|
140
|
+
unsubs.push(t.subscribe('claim_lost', (payload) => {
|
|
143
141
|
const lost = payload;
|
|
144
|
-
if (!lost.
|
|
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
|
-
|
|
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('
|
|
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
|
|
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 [
|
|
179
|
-
sendBegin(
|
|
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(
|
|
184
|
+
function sendBegin(claimId, claim) {
|
|
187
185
|
if (!attached?.isConnected())
|
|
188
186
|
return;
|
|
189
187
|
attached.send({
|
|
190
|
-
type: '
|
|
188
|
+
type: 'claim_begin',
|
|
191
189
|
payload: {
|
|
192
|
-
|
|
193
|
-
entityType:
|
|
194
|
-
entityId:
|
|
195
|
-
path:
|
|
196
|
-
range:
|
|
197
|
-
action:
|
|
198
|
-
field:
|
|
199
|
-
meta:
|
|
200
|
-
estimatedMs:
|
|
201
|
-
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: '
|
|
207
|
+
type: 'claim_reorder',
|
|
210
208
|
payload: {
|
|
211
209
|
entityType,
|
|
212
210
|
entityId,
|
|
213
|
-
// The wire shape identifies a waiter by heldBy +
|
|
214
|
-
// ergonomic `
|
|
215
|
-
order: order.map((i) => ({ heldBy: i.heldBy,
|
|
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(
|
|
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
|
|
222
|
+
// claims are found by claimId regardless; the target is harmless there.
|
|
225
223
|
attached.send({
|
|
226
|
-
type: '
|
|
224
|
+
type: 'claim_abandon',
|
|
227
225
|
payload: {
|
|
228
|
-
|
|
229
|
-
entityType:
|
|
230
|
-
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
|
|
238
|
+
const claimId = crypto.randomUUID();
|
|
241
239
|
const estimatedMs = args.ttl !== undefined ? toMs(args.ttl) : undefined;
|
|
242
|
-
const
|
|
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
|
-
|
|
254
|
-
sendBegin(
|
|
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
|
-
|
|
261
|
-
sendAbandon(
|
|
258
|
+
ownClaims.delete(claimId);
|
|
259
|
+
sendAbandon(claimId, claim);
|
|
262
260
|
};
|
|
263
261
|
return {
|
|
264
|
-
|
|
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
|
|
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
|
-
}, () =>
|
|
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
|
-
|
|
337
|
-
|
|
347
|
+
activeByClaimId.clear();
|
|
348
|
+
ownClaims.clear();
|
|
338
349
|
queueByEntity.clear();
|
|
339
|
-
|
|
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' : '
|
|
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
|
|
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 {
|
|
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
|
|
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
|
|
68
|
+
export interface ScopedClaims {
|
|
69
69
|
readonly focus: EntityRef | null;
|
|
70
|
-
readonly others: ReadonlyArray<
|
|
70
|
+
readonly others: ReadonlyArray<ActiveClaim>;
|
|
71
71
|
/**
|
|
72
|
-
* Claim an exclusive
|
|
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):
|
|
78
|
-
onRejected(listener: Parameters<
|
|
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
|
|
92
|
+
readonly claims: ScopedClaims;
|
|
93
93
|
readonly peers: ReadonlyArray<Peer>;
|
|
94
|
-
readonly
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
240
|
+
const scopedClaims = {
|
|
234
241
|
get focus() {
|
|
235
242
|
return currentTarget;
|
|
236
243
|
},
|
|
237
244
|
get others() {
|
|
238
|
-
return args.
|
|
245
|
+
return args.claims.others.filter((claim) => currentTarget ? targetsOverlap(claim.target, currentTarget) : true);
|
|
239
246
|
},
|
|
240
247
|
claim(opts) {
|
|
241
|
-
return track(args.
|
|
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.
|
|
254
|
+
return args.claims.onRejected(listener);
|
|
248
255
|
},
|
|
249
256
|
onChange(listener) {
|
|
250
|
-
return args.
|
|
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
|
-
|
|
282
|
+
claims: scopedClaims,
|
|
276
283
|
get peers() {
|
|
277
284
|
return scopedPresence.others;
|
|
278
285
|
},
|
|
279
|
-
get
|
|
280
|
-
return
|
|
286
|
+
get activeClaims() {
|
|
287
|
+
return scopedClaims.others;
|
|
281
288
|
},
|
|
282
289
|
focus: setFocus,
|
|
283
290
|
leave,
|
package/dist/sync/schemas.d.ts
CHANGED
|
@@ -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;
|