@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
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AreaOfInterestManager — client-side hysteresis + prominence policy over
|
|
3
|
+
* the `update_subscription` read primitive.
|
|
4
|
+
*
|
|
5
|
+
* Game netcode never thrashes its area-of-interest on a boundary: a cell
|
|
6
|
+
* you walk out of stays subscribed for a margin before it's dropped
|
|
7
|
+
* (hysteresis), and "important" entities stay relevant from farther away
|
|
8
|
+
* (prominence). This manager applies both to Ablo sync groups:
|
|
9
|
+
*
|
|
10
|
+
* - `enter(group)` / `leave(group)` move read interest as the user opens
|
|
11
|
+
* and closes entities (decks, sheets, docs). A `leave` does NOT
|
|
12
|
+
* immediately unsubscribe — the group goes WARM with a TTL and stays
|
|
13
|
+
* in the effective set. Re-entering within the window is a no-op
|
|
14
|
+
* (already subscribed → no bootstrap), and only when the warm TTL
|
|
15
|
+
* lapses does the group actually drop. This is the boundary hysteresis
|
|
16
|
+
* that turns deck-tab flipping from a re-bootstrap storm into a
|
|
17
|
+
* cache hit.
|
|
18
|
+
*
|
|
19
|
+
* - `pin(group)` / `unpin(group)` express prominence: a group that holds
|
|
20
|
+
* an active claim (write-claim) is pinned and never goes warm or
|
|
21
|
+
* expires while pinned. The claim machinery is the prominence oracle —
|
|
22
|
+
* the row two agents are fighting over stays subscribed regardless of
|
|
23
|
+
* navigation.
|
|
24
|
+
*
|
|
25
|
+
* - `baseGroups` are permanent infrastructure scopes (e.g. `org:<id>`,
|
|
26
|
+
* `user:<id>`) that are always in the effective set.
|
|
27
|
+
*
|
|
28
|
+
* The effective set is recomputed and diffed against what was last sent;
|
|
29
|
+
* the transport's `update_subscription` is only called when it actually
|
|
30
|
+
* changes, so hysteresis genuinely suppresses network churn rather than
|
|
31
|
+
* just deferring it.
|
|
32
|
+
*
|
|
33
|
+
* Transport-agnostic: it depends only on {@link SubscriptionTransport},
|
|
34
|
+
* which `SyncWebSocket` satisfies structurally. `now` and the sweep timer
|
|
35
|
+
* are injectable so the policy is deterministic under test.
|
|
36
|
+
*/
|
|
37
|
+
function setsEqual(a, b) {
|
|
38
|
+
if (a.size !== b.size)
|
|
39
|
+
return false;
|
|
40
|
+
for (const v of a)
|
|
41
|
+
if (!b.has(v))
|
|
42
|
+
return false;
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
export class AreaOfInterestManager {
|
|
46
|
+
transport;
|
|
47
|
+
baseGroups;
|
|
48
|
+
warmTtlMs;
|
|
49
|
+
maxWarm;
|
|
50
|
+
now;
|
|
51
|
+
/** Groups currently in view (open entities). */
|
|
52
|
+
active = new Set();
|
|
53
|
+
/** Claim-pinned groups — prominence; never warm/expire while pinned. */
|
|
54
|
+
pinned = new Set();
|
|
55
|
+
/** Left-but-warm groups → epoch-ms at which they drop. */
|
|
56
|
+
warm = new Map();
|
|
57
|
+
/** Last set the transport confirmed — the diff baseline. */
|
|
58
|
+
lastSent = new Set();
|
|
59
|
+
/** Coalescing state so concurrent mutations collapse into one in-flight call. */
|
|
60
|
+
inFlight = null;
|
|
61
|
+
dirty = false;
|
|
62
|
+
cancelSweep;
|
|
63
|
+
constructor(options) {
|
|
64
|
+
this.transport = options.transport;
|
|
65
|
+
this.baseGroups = new Set(options.baseGroups ?? []);
|
|
66
|
+
this.warmTtlMs = options.warmTtlMs ?? 30_000;
|
|
67
|
+
this.maxWarm = options.maxWarm ?? 16;
|
|
68
|
+
this.now = options.now ?? (() => Date.now());
|
|
69
|
+
const sweepInterval = options.sweepIntervalMs ?? this.warmTtlMs;
|
|
70
|
+
if (sweepInterval > 0) {
|
|
71
|
+
const schedule = options.scheduler ??
|
|
72
|
+
((fn, ms) => {
|
|
73
|
+
const handle = setInterval(fn, ms);
|
|
74
|
+
return () => clearInterval(handle);
|
|
75
|
+
});
|
|
76
|
+
this.cancelSweep = schedule(() => {
|
|
77
|
+
void this.sweep();
|
|
78
|
+
}, sweepInterval);
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
this.cancelSweep = null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Move a group into the warm set with a fresh TTL, maintaining LRU order
|
|
86
|
+
* and the `maxWarm` cap. JS `Map` preserves insertion order, so deleting
|
|
87
|
+
* then re-setting moves the group to the most-recently-warmed position;
|
|
88
|
+
* eviction then drops from the front (oldest). Base/pinned groups never
|
|
89
|
+
* warm — callers guard before calling this.
|
|
90
|
+
*/
|
|
91
|
+
warmGroup(group) {
|
|
92
|
+
this.warm.delete(group);
|
|
93
|
+
this.warm.set(group, this.now() + this.warmTtlMs);
|
|
94
|
+
while (this.warm.size > this.maxWarm) {
|
|
95
|
+
const oldest = this.warm.keys().next().value;
|
|
96
|
+
if (oldest === undefined)
|
|
97
|
+
break;
|
|
98
|
+
this.warm.delete(oldest);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/** The effective read set: base ∪ active ∪ pinned ∪ (warm not yet expired). */
|
|
102
|
+
desiredGroups() {
|
|
103
|
+
const now = this.now();
|
|
104
|
+
const desired = new Set(this.baseGroups);
|
|
105
|
+
for (const g of this.active)
|
|
106
|
+
desired.add(g);
|
|
107
|
+
for (const g of this.pinned)
|
|
108
|
+
desired.add(g);
|
|
109
|
+
for (const [g, expiry] of this.warm) {
|
|
110
|
+
if (expiry > now)
|
|
111
|
+
desired.add(g);
|
|
112
|
+
}
|
|
113
|
+
return desired;
|
|
114
|
+
}
|
|
115
|
+
/** Bring a group into view. Cancels any warm timer for it. Idempotent. */
|
|
116
|
+
enter(group) {
|
|
117
|
+
this.warm.delete(group);
|
|
118
|
+
this.active.add(group);
|
|
119
|
+
return this.reconcile();
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Leave a group. It does not drop immediately — it goes warm for
|
|
123
|
+
* `warmTtlMs` (unless pinned, in which case it stays via the pin).
|
|
124
|
+
* Re-entering within the window is free.
|
|
125
|
+
*/
|
|
126
|
+
leave(group) {
|
|
127
|
+
this.active.delete(group);
|
|
128
|
+
if (!this.pinned.has(group) && !this.baseGroups.has(group)) {
|
|
129
|
+
this.warmGroup(group);
|
|
130
|
+
}
|
|
131
|
+
return this.reconcile();
|
|
132
|
+
}
|
|
133
|
+
/** Pin a group (active claim / prominence). Never warm or expires while pinned. */
|
|
134
|
+
pin(group) {
|
|
135
|
+
this.warm.delete(group);
|
|
136
|
+
this.pinned.add(group);
|
|
137
|
+
return this.reconcile();
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Unpin a group. If it's not currently in view, it transitions to warm
|
|
141
|
+
* (so dropping a claim gets the same hysteresis as closing a tab) rather
|
|
142
|
+
* than dropping instantly.
|
|
143
|
+
*/
|
|
144
|
+
unpin(group) {
|
|
145
|
+
this.pinned.delete(group);
|
|
146
|
+
if (!this.active.has(group) && !this.baseGroups.has(group)) {
|
|
147
|
+
this.warmGroup(group);
|
|
148
|
+
}
|
|
149
|
+
return this.reconcile();
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Drop warm groups whose TTL has lapsed and reconcile. Auto-invoked on
|
|
153
|
+
* the sweep timer; call manually (with an injected `now`) in tests.
|
|
154
|
+
*/
|
|
155
|
+
sweep() {
|
|
156
|
+
const now = this.now();
|
|
157
|
+
for (const [g, expiry] of this.warm) {
|
|
158
|
+
if (expiry <= now)
|
|
159
|
+
this.warm.delete(g);
|
|
160
|
+
}
|
|
161
|
+
return this.reconcile();
|
|
162
|
+
}
|
|
163
|
+
/** The set the manager believes is subscribed (post-confirmation). */
|
|
164
|
+
effectiveGroups() {
|
|
165
|
+
return [...this.lastSent];
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Re-assert the full desired set against the transport, forgetting what
|
|
169
|
+
* was previously confirmed. Call after a reconnect: a fresh
|
|
170
|
+
* `SyncWebSocket` instance starts from the connect-time URL groups, so
|
|
171
|
+
* the manager's `lastSent` diff baseline is stale. Clearing it forces
|
|
172
|
+
* one `update_subscription` that re-establishes the live interest on the
|
|
173
|
+
* new socket.
|
|
174
|
+
*
|
|
175
|
+
* Resetting `lastSent` makes the next reconcile unconditionally re-push
|
|
176
|
+
* the current desired set (one `update_subscription` frame) so the fresh
|
|
177
|
+
* socket's server-side index matches local interest, even if warm/pinned
|
|
178
|
+
* groups drifted across the disconnect window. The connect-time URL
|
|
179
|
+
* already carries the last-acked set, so this is a correction frame, not
|
|
180
|
+
* the primary mechanism.
|
|
181
|
+
*/
|
|
182
|
+
resync() {
|
|
183
|
+
this.lastSent = new Set();
|
|
184
|
+
return this.reconcile();
|
|
185
|
+
}
|
|
186
|
+
/** Stop the sweep timer. The connection is unaffected. */
|
|
187
|
+
dispose() {
|
|
188
|
+
this.cancelSweep?.();
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Push the desired set to the transport iff it differs from the last
|
|
192
|
+
* confirmed set. Coalesces concurrent mutations: if a call is already in
|
|
193
|
+
* flight, mark dirty and let the in-flight loop pick up the newest state
|
|
194
|
+
* — so a burst of enter/leave collapses into the minimum number of
|
|
195
|
+
* `update_subscription` round-trips.
|
|
196
|
+
*/
|
|
197
|
+
reconcile() {
|
|
198
|
+
if (this.inFlight) {
|
|
199
|
+
this.dirty = true;
|
|
200
|
+
return this.inFlight;
|
|
201
|
+
}
|
|
202
|
+
if (setsEqual(this.desiredGroups(), this.lastSent)) {
|
|
203
|
+
return Promise.resolve();
|
|
204
|
+
}
|
|
205
|
+
this.inFlight = (async () => {
|
|
206
|
+
try {
|
|
207
|
+
do {
|
|
208
|
+
this.dirty = false;
|
|
209
|
+
const target = this.desiredGroups();
|
|
210
|
+
if (setsEqual(target, this.lastSent))
|
|
211
|
+
break;
|
|
212
|
+
try {
|
|
213
|
+
const result = await this.transport.updateSubscription([...target]);
|
|
214
|
+
this.lastSent = new Set(result.syncGroups);
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
// Transport unavailable (offline / socket not open) or the
|
|
218
|
+
// server rejected the set. Interest is SOFT state — never throw
|
|
219
|
+
// out of enter/leave/sweep for an expected transient. Leave
|
|
220
|
+
// `lastSent` unchanged so the diff persists; `resync()` on the
|
|
221
|
+
// next `connected` re-pushes the then-current desired set,
|
|
222
|
+
// which is what recovers "interest changed while offline."
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
} while (this.dirty);
|
|
226
|
+
}
|
|
227
|
+
finally {
|
|
228
|
+
this.inFlight = null;
|
|
229
|
+
}
|
|
230
|
+
})();
|
|
231
|
+
return this.inFlight;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
@@ -100,7 +100,15 @@ export declare class BootstrapHelper {
|
|
|
100
100
|
* @param lastSyncId - Optional: client's current lastSyncId for partial bootstrap
|
|
101
101
|
* @returns Bootstrap data (either full snapshot or delta batch)
|
|
102
102
|
*/
|
|
103
|
-
fetchBootstrap(lastSyncId?: number
|
|
103
|
+
fetchBootstrap(lastSyncId?: number,
|
|
104
|
+
/**
|
|
105
|
+
* Per-call sync-group override for SCOPED hydrate-on-enter. When provided,
|
|
106
|
+
* the request uses THESE groups instead of `this.options.syncGroups`,
|
|
107
|
+
* WITHOUT mutating the shared options (so a concurrent full bootstrap is
|
|
108
|
+
* unaffected). Also bypasses the offline full-snapshot cache below, which
|
|
109
|
+
* holds the connection's full bootstrap and would be wrong for a subset.
|
|
110
|
+
*/
|
|
111
|
+
syncGroupsOverride?: readonly string[]): Promise<BootstrapData>;
|
|
104
112
|
/**
|
|
105
113
|
* Fetch bootstrap with ETag, returning 304 hints
|
|
106
114
|
*/
|
|
@@ -83,7 +83,15 @@ export class BootstrapHelper {
|
|
|
83
83
|
* @param lastSyncId - Optional: client's current lastSyncId for partial bootstrap
|
|
84
84
|
* @returns Bootstrap data (either full snapshot or delta batch)
|
|
85
85
|
*/
|
|
86
|
-
async fetchBootstrap(lastSyncId
|
|
86
|
+
async fetchBootstrap(lastSyncId,
|
|
87
|
+
/**
|
|
88
|
+
* Per-call sync-group override for SCOPED hydrate-on-enter. When provided,
|
|
89
|
+
* the request uses THESE groups instead of `this.options.syncGroups`,
|
|
90
|
+
* WITHOUT mutating the shared options (so a concurrent full bootstrap is
|
|
91
|
+
* unaffected). Also bypasses the offline full-snapshot cache below, which
|
|
92
|
+
* holds the connection's full bootstrap and would be wrong for a subset.
|
|
93
|
+
*/
|
|
94
|
+
syncGroupsOverride) {
|
|
87
95
|
// organizationId omitted — server reads it from auth identity.
|
|
88
96
|
// See `fetchBootstrapWithETag` for the full rationale.
|
|
89
97
|
const params = new URLSearchParams();
|
|
@@ -91,8 +99,8 @@ export class BootstrapHelper {
|
|
|
91
99
|
if (lastSyncId !== undefined && lastSyncId > 0) {
|
|
92
100
|
params.append('lastSyncId', lastSyncId.toString());
|
|
93
101
|
}
|
|
94
|
-
// Add sync groups
|
|
95
|
-
this.options.syncGroups.forEach((group) => {
|
|
102
|
+
// Add sync groups (per-call override wins over the configured set).
|
|
103
|
+
(syncGroupsOverride ?? this.options.syncGroups).forEach((group) => {
|
|
96
104
|
params.append('syncGroups', group);
|
|
97
105
|
});
|
|
98
106
|
// Selective bootstrap: only request instant-strategy models.
|
|
@@ -102,8 +110,10 @@ export class BootstrapHelper {
|
|
|
102
110
|
params.append('models', this.options.instantModels.join(','));
|
|
103
111
|
}
|
|
104
112
|
const url = `${this.options.baseUrl}/sync/bootstrap?${params.toString()}`;
|
|
105
|
-
// If offline, try cached bootstrap
|
|
106
|
-
|
|
113
|
+
// If offline, try cached bootstrap. Skipped for a scoped override — the
|
|
114
|
+
// cache holds the FULL snapshot, which is not a valid answer to a subset
|
|
115
|
+
// request; a scoped hydrate just soft-fails offline and retries on re-enter.
|
|
116
|
+
if (!syncGroupsOverride && typeof navigator !== 'undefined' && navigator && navigator.onLine === false) {
|
|
107
117
|
const cached = this.options.cacheScope
|
|
108
118
|
? this.loadCachedBootstrap(this.options.cacheScope)
|
|
109
119
|
: null;
|
|
@@ -29,7 +29,7 @@ import { type AuthTokenGetter } from '../auth/credentialSource.js';
|
|
|
29
29
|
/**
|
|
30
30
|
* The closed set of probe outcomes — one value carrying both reachability and
|
|
31
31
|
* credential disposition, so the {@link ConnectionManager} branches on a single
|
|
32
|
-
* exhaustive discriminant instead of reconstructing
|
|
32
|
+
* exhaustive discriminant instead of reconstructing claim from a trio of
|
|
33
33
|
* booleans. Mirrors the {@link RecoveryClass} taxonomy at the connectivity tier.
|
|
34
34
|
*/
|
|
35
35
|
export declare const PROBE_OUTCOMES: readonly ["reachable", "unreachable", "session_expired", "credential_stale", "auth_blocked"];
|
|
@@ -31,7 +31,7 @@ import { withAuthHeaders } from '../auth/credentialSource.js';
|
|
|
31
31
|
/**
|
|
32
32
|
* The closed set of probe outcomes — one value carrying both reachability and
|
|
33
33
|
* credential disposition, so the {@link ConnectionManager} branches on a single
|
|
34
|
-
* exhaustive discriminant instead of reconstructing
|
|
34
|
+
* exhaustive discriminant instead of reconstructing claim from a trio of
|
|
35
35
|
* booleans. Mirrors the {@link RecoveryClass} taxonomy at the connectivity tier.
|
|
36
36
|
*/
|
|
37
37
|
export const PROBE_OUTCOMES = [
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import { EventEmitter } from 'events';
|
|
11
11
|
import type { MutationOperation } from '../interfaces/index.js';
|
|
12
12
|
import type { ClientSyncDelta } from '../schema/sync-delta-wire.js';
|
|
13
|
+
import type { ClaimError, ClaimRejection } from '../coordination/schema.js';
|
|
13
14
|
import { type AuthTokenGetter } from '../auth/credentialSource.js';
|
|
14
15
|
/**
|
|
15
16
|
* The wire delta the client receives. Derived from the canonical
|
|
@@ -160,12 +161,19 @@ export interface PresenceUpdateEvent {
|
|
|
160
161
|
/** Server-derived from the connection's userId prefix. Clients must
|
|
161
162
|
* not self-declare — server is the source of truth. */
|
|
162
163
|
isAgent?: boolean;
|
|
164
|
+
/**
|
|
165
|
+
* Server-stamped canonical kind (`'user' | 'agent' | 'system'`). Additive:
|
|
166
|
+
* older servers omit it and readers fall back to the lossy `isAgent`
|
|
167
|
+
* boolean (which cannot express `'system'`). Typed `string` because it is
|
|
168
|
+
* raw wire input — normalize via `participantKindFromWire`.
|
|
169
|
+
*/
|
|
170
|
+
participantKind?: string;
|
|
163
171
|
timestamp?: number;
|
|
164
172
|
/** Server stamps every presence frame with this participant's open
|
|
165
|
-
*
|
|
166
|
-
* shape mirrors `apps/sync-server/src/hub/types.ts
|
|
167
|
-
|
|
168
|
-
|
|
173
|
+
* claim claims so peers see them without a separate channel. Wire
|
|
174
|
+
* shape mirrors `apps/sync-server/src/hub/types.ts Claim`. */
|
|
175
|
+
activeClaims?: Array<{
|
|
176
|
+
claimId: string;
|
|
169
177
|
entityType: string;
|
|
170
178
|
entityId: string;
|
|
171
179
|
path?: string;
|
|
@@ -187,13 +195,7 @@ export interface PresenceUpdateEvent {
|
|
|
187
195
|
* learn *how* it resolved before it drops from the active set.
|
|
188
196
|
*/
|
|
189
197
|
status?: 'active' | 'committed' | 'expired' | 'canceled';
|
|
190
|
-
error?:
|
|
191
|
-
code: string;
|
|
192
|
-
message?: string;
|
|
193
|
-
heldBy?: string;
|
|
194
|
-
heldByIntentId?: string;
|
|
195
|
-
heldByExpiresAt?: number;
|
|
196
|
-
};
|
|
198
|
+
error?: ClaimError;
|
|
197
199
|
}>;
|
|
198
200
|
localTime?: string;
|
|
199
201
|
type?: string;
|
|
@@ -240,30 +242,30 @@ export interface CoreSyncEventMap {
|
|
|
240
242
|
claimId: string;
|
|
241
243
|
}];
|
|
242
244
|
/**
|
|
243
|
-
* Server rejected an `
|
|
245
|
+
* Server rejected an `claim_begin` because another participant
|
|
244
246
|
* already holds an open claim on the same target (cooperative
|
|
245
247
|
* mutex enforced server-side). Surfaces to the participant-level
|
|
246
|
-
*
|
|
248
|
+
* ClaimStream so the caller knows their announce was denied.
|
|
247
249
|
* Payload mirrors the wire frame's `payload`.
|
|
248
250
|
*/
|
|
249
|
-
|
|
251
|
+
claim_rejected: [ClaimRejection];
|
|
250
252
|
/**
|
|
251
|
-
* Fair-queue frames (opt-in `queue: true` on `
|
|
252
|
-
* means the target was free and the lease is ours immediately; `
|
|
253
|
-
* means the claim is waiting in line (carries `position`); `
|
|
254
|
-
* means it reached the head and the lease is now ours; `
|
|
253
|
+
* Fair-queue frames (opt-in `queue: true` on `claim_begin`). `claim_acquired`
|
|
254
|
+
* means the target was free and the lease is ours immediately; `claim_queued`
|
|
255
|
+
* means the claim is waiting in line (carries `position`); `claim_granted`
|
|
256
|
+
* means it reached the head and the lease is now ours; `claim_lost` means a
|
|
255
257
|
* held/granted claim was taken away (TTL lapse on disconnect, revoke).
|
|
256
258
|
*/
|
|
257
259
|
/**
|
|
258
|
-
* Per-entity wait-queue snapshot: `{ target, queue:
|
|
260
|
+
* Per-entity wait-queue snapshot: `{ target, queue: Claim[] }` with each
|
|
259
261
|
* entry `status: 'queued'` + `position`. Broadcast to entity peers on every
|
|
260
262
|
* queue mutation — powers the reactive `ablo.<model>.claim.queue({ id })` read.
|
|
261
263
|
*/
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
264
|
+
claim_queue: [Record<string, unknown>];
|
|
265
|
+
claim_acquired: [Record<string, unknown>];
|
|
266
|
+
claim_queued: [Record<string, unknown>];
|
|
267
|
+
claim_granted: [Record<string, unknown>];
|
|
268
|
+
claim_lost: [Record<string, unknown>];
|
|
267
269
|
}
|
|
268
270
|
/**
|
|
269
271
|
* Collaboration event — app-specific real-time events (selection, cursors, etc.)
|
|
@@ -370,6 +372,15 @@ export declare class SyncWebSocket<TCollaboration extends EventMap<TCollaboratio
|
|
|
370
372
|
* over a multiplexed connection.
|
|
371
373
|
*/
|
|
372
374
|
private pendingClaims;
|
|
375
|
+
/**
|
|
376
|
+
* In-flight `update_subscription` frames awaiting `subscription_ack`.
|
|
377
|
+
* A FIFO queue rather than a keyed Map because the wire ack carries no
|
|
378
|
+
* correlation id — the server applies subscription updates in receive
|
|
379
|
+
* order and acks in the same order, so `shift()` on ack matches the
|
|
380
|
+
* oldest pending request. (Read-interest changes are infrequent and
|
|
381
|
+
* usually settle before the next one, so depth is ~1 in practice.)
|
|
382
|
+
*/
|
|
383
|
+
private pendingSubscriptions;
|
|
373
384
|
constructor(options: SyncWebSocketOptions);
|
|
374
385
|
/**
|
|
375
386
|
* Mark that a session error has been detected (e.g. 401 from HTTP bootstrap).
|
|
@@ -497,6 +508,29 @@ export declare class SyncWebSocket<TCollaboration extends EventMap<TCollaboratio
|
|
|
497
508
|
* we reject it locally — the user explicitly chose to release.
|
|
498
509
|
*/
|
|
499
510
|
sendRelease(claimId: string): void;
|
|
511
|
+
/**
|
|
512
|
+
* Move this connection's READ interest — replace the connection-level
|
|
513
|
+
* sync groups mid-session as the user opens/closes entities. This is the
|
|
514
|
+
* area-of-interest (AOI) navigation primitive: the server fans out
|
|
515
|
+
* deltas only for groups currently in view, instead of the frozen set
|
|
516
|
+
* chosen at connect.
|
|
517
|
+
*
|
|
518
|
+
* Full-set replace semantics — pass the complete new group list, not a
|
|
519
|
+
* delta. Resolves with the server's effective set once `subscription_ack`
|
|
520
|
+
* arrives; rejects (typed) on a scope denial (a restricted `rk_` key
|
|
521
|
+
* requesting a group outside its allowlist), timeout, or disconnect. On
|
|
522
|
+
* success the new set is recorded as `options.syncGroups` so a later
|
|
523
|
+
* reconnect re-subscribes to current interest, not the connect-time set.
|
|
524
|
+
*
|
|
525
|
+
* Distinct from {@link sendClaim} (write-claim, per-op, TTL'd) — this is
|
|
526
|
+
* the read side and carries no capability token of its own; it's bounded
|
|
527
|
+
* by the connection credential's grant.
|
|
528
|
+
*/
|
|
529
|
+
updateSubscription(syncGroups: ReadonlyArray<string>, options?: {
|
|
530
|
+
timeoutMs?: number;
|
|
531
|
+
}): Promise<{
|
|
532
|
+
syncGroups: string[];
|
|
533
|
+
}>;
|
|
500
534
|
/**
|
|
501
535
|
* Compatibility setter for direct SyncWebSocket users. The SDK-owned
|
|
502
536
|
* `Ablo()` path passes `getAuthToken`, so reconnect URL auth reads the
|
|
@@ -676,7 +710,7 @@ export declare class SyncWebSocket<TCollaboration extends EventMap<TCollaboratio
|
|
|
676
710
|
*
|
|
677
711
|
* Wire frame (apps/sync-server/src/hub/types.ts PresenceUpdateMessage):
|
|
678
712
|
* { type: 'presence_update', payload: { kind, userId, status,
|
|
679
|
-
* syncGroups, activity, isAgent, timestamp,
|
|
713
|
+
* syncGroups, activity, isAgent, timestamp, activeClaims } }
|
|
680
714
|
*/
|
|
681
715
|
private handlePresenceUpdate;
|
|
682
716
|
}
|