@abloatai/ablo 0.35.0 → 0.36.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 +713 -629
- package/README.md +56 -519
- package/bin/ablo.cjs +39 -0
- package/dist/BaseSyncedStore.d.ts +24 -4
- package/dist/BaseSyncedStore.js +53 -37
- package/dist/Database.d.ts +8 -20
- package/dist/Database.js +61 -59
- package/dist/InstanceCache.d.ts +6 -2
- package/dist/InstanceCache.js +18 -16
- package/dist/Model.d.ts +17 -7
- package/dist/Model.js +17 -7
- package/dist/ModelRegistry.d.ts +4 -0
- package/dist/ModelRegistry.js +17 -15
- package/dist/NetworkMonitor.d.ts +3 -1
- package/dist/NetworkMonitor.js +7 -5
- package/dist/SyncClient.d.ts +5 -15
- package/dist/SyncClient.js +60 -57
- package/dist/client/Ablo.js +30 -19
- package/dist/client/createInternalComponents.d.ts +4 -0
- package/dist/client/createInternalComponents.js +8 -2
- package/dist/client/createModelProxy.d.ts +21 -1
- package/dist/client/createModelProxy.js +123 -57
- package/dist/client/humans.d.ts +30 -9
- package/dist/client/humans.js +45 -19
- package/dist/client/reactiveEngine.d.ts +16 -11
- package/dist/client/reactiveEngine.js +113 -335
- package/dist/client/storeCluster.d.ts +47 -0
- package/dist/client/storeCluster.js +118 -0
- package/dist/client/storeLifecycle.d.ts +61 -0
- package/dist/client/storeLifecycle.js +231 -0
- package/dist/context.d.ts +13 -0
- package/dist/context.js +23 -0
- package/dist/core/index.d.ts +1 -1
- package/dist/docs/catalog.js +6 -3
- package/dist/index.d.ts +4 -2
- package/dist/index.js +4 -2
- package/dist/query/client.d.ts +3 -0
- package/dist/query/client.js +6 -5
- package/dist/react/AbloProvider.d.ts +13 -1
- package/dist/react/AbloProvider.js +5 -2
- package/dist/react/context.d.ts +2 -2
- package/dist/react/createAbloReact.d.ts +56 -0
- package/dist/react/createAbloReact.js +51 -0
- package/dist/react/index.d.ts +1 -0
- package/dist/react/index.js +3 -0
- package/dist/react/useAblo.d.ts +9 -2
- package/dist/react/useAblo.js +25 -7
- package/dist/schema/coordination.js +5 -1
- package/dist/schema/index.d.ts +1 -0
- package/dist/schema/index.js +4 -0
- package/dist/schema/select.js +3 -0
- package/dist/schema/serialize.js +3 -0
- package/dist/source/adapter.d.ts +7 -5
- package/dist/source/adapter.js +7 -5
- package/dist/source/index.d.ts +1 -1
- package/dist/source/index.js +1 -1
- package/dist/{core/storeContract.d.ts → storeContract.d.ts} +5 -5
- package/dist/{core → stores}/DatabaseManager.d.ts +3 -1
- package/dist/{core → stores}/DatabaseManager.js +13 -12
- package/dist/{core → stores}/StoreManager.d.ts +5 -3
- package/dist/{core → stores}/StoreManager.js +24 -22
- package/dist/stores/SyncActionStore.d.ts +3 -1
- package/dist/stores/SyncActionStore.js +9 -7
- package/dist/sync/BootstrapFetcher.d.ts +4 -0
- package/dist/sync/BootstrapFetcher.js +28 -26
- package/dist/sync/OnDemandLoader.d.ts +3 -0
- package/dist/sync/OnDemandLoader.js +1 -0
- package/dist/sync/bootstrapApply.d.ts +3 -0
- package/dist/sync/bootstrapApply.js +2 -2
- package/dist/sync/deltaPipeline.d.ts +13 -12
- package/dist/sync/deltaPipeline.js +21 -4
- package/dist/sync/groupChange.d.ts +3 -0
- package/dist/sync/groupChange.js +16 -14
- package/dist/sync/participants.d.ts +19 -2
- package/dist/sync/participants.js +3 -1
- package/dist/sync/schemas.d.ts +2 -1
- package/dist/sync/schemas.js +3 -3
- package/dist/syncLog/contract.d.ts +20 -0
- package/dist/syncLog/contract.js +19 -0
- package/dist/syncLog/index.d.ts +1 -0
- package/dist/syncLog/index.js +1 -0
- package/dist/transaction/auth/capability.d.ts +35 -0
- package/dist/transaction/auth/capability.js +25 -0
- package/dist/transaction/coordination/awaitClaimGrant.d.ts +7 -0
- package/dist/transaction/coordination/awaitClaimGrant.js +12 -0
- package/dist/transaction/coordination/claimHeartbeatLoop.d.ts +34 -0
- package/dist/transaction/coordination/claimHeartbeatLoop.js +20 -0
- package/dist/transaction/coordination/index.d.ts +4 -4
- package/dist/transaction/coordination/index.js +4 -3
- package/dist/transaction/coordination/locator.d.ts +23 -2
- package/dist/transaction/coordination/locator.js +22 -2
- package/dist/transaction/coordination/schema.d.ts +125 -62
- package/dist/transaction/coordination/schema.js +228 -64
- package/dist/transaction/coordination/targetConflict.js +32 -28
- package/dist/transaction/errorCodes.d.ts +2 -2
- package/dist/transaction/errorCodes.js +13 -9
- package/dist/transaction/plugin.d.ts +95 -2
- package/dist/transaction/plugin.js +21 -2
- package/dist/transaction/resources/httpResources.d.ts +57 -2
- package/dist/transaction/resources/modelOperations.d.ts +148 -40
- package/dist/transaction/resources/where.d.ts +16 -0
- package/dist/transaction/resources/where.js +45 -0
- package/dist/transaction/schema/field.d.ts +12 -18
- package/dist/transaction/schema/fieldRef.d.ts +38 -0
- package/dist/transaction/schema/fieldRef.js +11 -0
- package/dist/transaction/schema/openapi.d.ts +16 -15
- package/dist/transaction/schema/openapi.js +186 -25
- package/dist/transaction/schema/relation.d.ts +7 -2
- package/dist/transaction/schema/schema.d.ts +27 -0
- package/dist/transaction/schema/schema.js +20 -0
- package/dist/transaction/transactions/settlement/commitEnvelope.d.ts +1 -1
- package/dist/transaction/transactions/settlement/pendingWrite.d.ts +1 -1
- package/dist/transaction/transport/httpClient.d.ts +9 -1
- package/dist/transaction/transport/httpClient.js +1 -0
- package/dist/transaction/transport/httpTransport.js +156 -44
- package/dist/transaction/transport/wsTransport.d.ts +2 -4
- package/dist/transaction/transport/wsTransport.js +16 -10
- package/dist/transaction/types/streams.d.ts +10 -0
- package/dist/transaction/utils/duration.d.ts +25 -0
- package/dist/transaction/utils/duration.js +32 -0
- package/dist/transaction/wire/accountResponses.d.ts +69 -0
- package/dist/transaction/wire/accountResponses.js +36 -1
- package/dist/transaction/wire/auth.d.ts +9 -2
- package/dist/transaction/wire/auth.js +7 -1
- package/dist/transaction/wire/claims.d.ts +164 -97
- package/dist/transaction/wire/claims.js +126 -28
- package/dist/transaction/wire/commit.d.ts +1 -1
- package/dist/transaction/wire/feedEvent.d.ts +27 -0
- package/dist/transaction/wire/feedEvent.js +27 -1
- package/dist/transaction/wire/frames.d.ts +2 -2
- package/dist/transaction/wire/inboundFrames.d.ts +12 -2
- package/dist/transaction/wire/index.d.ts +10 -6
- package/dist/transaction/wire/index.js +12 -3
- package/dist/transaction/wire/modelMutations.d.ts +31 -0
- package/dist/transaction/wire/modelMutations.js +52 -0
- package/dist/transaction/wire/modelShape.d.ts +78 -0
- package/dist/transaction/wire/modelShape.js +74 -0
- package/dist/transactions/mutations/MutationQueue.d.ts +6 -0
- package/dist/transactions/mutations/MutationQueue.js +55 -45
- package/dist/transactions/mutations/commitPayload.d.ts +3 -2
- package/dist/transactions/mutations/commitPayload.js +5 -5
- package/dist/transactions/mutations/deltaConfirmation.d.ts +4 -0
- package/dist/transactions/mutations/deltaConfirmation.js +10 -8
- package/dist/transactions/mutations/replayValidation.d.ts +2 -1
- package/dist/transactions/mutations/replayValidation.js +3 -2
- package/dist/{core → views}/QueryView.d.ts +1 -1
- package/dist/{core → views}/QueryView.js +1 -1
- package/dist/{core → views}/ViewRegistry.d.ts +1 -1
- package/dist/{core/queryUtils.d.ts → views/incrementalView.d.ts} +6 -6
- package/dist/{core/queryUtils.js → views/incrementalView.js} +6 -6
- package/docs/agents.md +1 -1
- package/docs/api-keys.md +6 -6
- package/docs/api.md +5 -43
- package/docs/audit.md +4 -3
- package/docs/cli.md +11 -11
- package/docs/client-behavior.md +4 -4
- package/docs/concurrency-convention.md +28 -42
- package/docs/coordination.md +235 -83
- package/docs/data-sources.md +4 -4
- package/docs/debugging.md +34 -12
- package/docs/deployment.md +8 -8
- package/docs/examples/scoped-agent.md +3 -3
- package/docs/groups.md +57 -3
- package/docs/guarantees.md +37 -10
- package/docs/how-it-works.md +29 -5
- package/docs/idempotency.md +6 -6
- package/docs/identity.md +22 -23
- package/docs/index.md +8 -8
- package/docs/integration-guide.md +14 -3
- package/docs/mcp.md +7 -7
- package/docs/migration.md +34 -15
- package/docs/projects.md +1 -1
- package/docs/react.md +19 -8
- package/docs/sessions.md +1 -1
- package/docs/webhooks.md +9 -9
- package/llms.txt +4 -4
- package/package.json +13 -20
- package/dist/cli.cjs +0 -288600
- package/dist/testing/fixtures/bootstrap.d.ts +0 -49
- package/dist/testing/fixtures/bootstrap.js +0 -59
- package/dist/testing/fixtures/deltas.d.ts +0 -83
- package/dist/testing/fixtures/deltas.js +0 -136
- package/dist/testing/fixtures/httpResponses.d.ts +0 -70
- package/dist/testing/fixtures/httpResponses.js +0 -90
- package/dist/testing/fixtures/models.d.ts +0 -83
- package/dist/testing/fixtures/models.js +0 -272
- package/dist/testing/helpers/reactWrapper.d.ts +0 -69
- package/dist/testing/helpers/reactWrapper.js +0 -67
- package/dist/testing/helpers/syncEngineHarness.d.ts +0 -54
- package/dist/testing/helpers/syncEngineHarness.js +0 -73
- package/dist/testing/helpers/wait.d.ts +0 -30
- package/dist/testing/helpers/wait.js +0 -49
- package/dist/testing/index.d.ts +0 -23
- package/dist/testing/index.js +0 -33
- package/dist/testing/mocks/FakeDatabase.d.ts +0 -18
- package/dist/testing/mocks/FakeDatabase.js +0 -10
- package/dist/testing/mocks/MockMutationExecutor.d.ts +0 -87
- package/dist/testing/mocks/MockMutationExecutor.js +0 -186
- package/dist/testing/mocks/MockNetworkMonitor.d.ts +0 -20
- package/dist/testing/mocks/MockNetworkMonitor.js +0 -46
- package/dist/testing/mocks/MockSyncContext.d.ts +0 -51
- package/dist/testing/mocks/MockSyncContext.js +0 -72
- package/dist/testing/mocks/MockSyncStore.d.ts +0 -88
- package/dist/testing/mocks/MockSyncStore.js +0 -171
- package/dist/testing/mocks/MockWebSocket.d.ts +0 -71
- package/dist/testing/mocks/MockWebSocket.js +0 -118
- package/docs/interaction-model.md +0 -99
- /package/dist/{core → query}/QueryProcessor.d.ts +0 -0
- /package/dist/{core → query}/QueryProcessor.js +0 -0
- /package/dist/{core/storeContract.js → storeContract.js} +0 -0
- /package/dist/{core → stores}/openIDBWithTimeout.d.ts +0 -0
- /package/dist/{core → stores}/openIDBWithTimeout.js +0 -0
- /package/dist/{source → transaction}/footprint.d.ts +0 -0
- /package/dist/{source → transaction}/footprint.js +0 -0
- /package/dist/{core → views}/ViewRegistry.js +0 -0
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* no path carried one, so the fix looked complete and changed nothing. Widening
|
|
16
16
|
* the locator again should touch this module and the schema, not every hop.
|
|
17
17
|
*/
|
|
18
|
+
import { partName } from './schema.js';
|
|
18
19
|
// The one declared→wire conversion for claim metadata; `declaredMeta` is its
|
|
19
20
|
// counterpart, called by the decodes that build a public claim.
|
|
20
21
|
import { wireMeta } from './claimMeta.js';
|
|
@@ -41,8 +42,10 @@ source) {
|
|
|
41
42
|
return {
|
|
42
43
|
...(source.path !== undefined ? { path: source.path } : {}),
|
|
43
44
|
...(source.range !== undefined ? { range: source.range } : {}),
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
// Part names cross to their wire spelling here — the one declared→wire
|
|
46
|
+
// seam — so a `part('B2')` object never leaks into a frame or a URL.
|
|
47
|
+
...(source.field !== undefined ? { field: partName(source.field) } : {}),
|
|
48
|
+
...(source.fields !== undefined ? { fields: source.fields.map(partName) } : {}),
|
|
46
49
|
...(source.meta !== undefined ? { meta: wireMeta(source.meta) } : {}),
|
|
47
50
|
};
|
|
48
51
|
}
|
|
@@ -80,3 +83,20 @@ export function streamTarget(source) {
|
|
|
80
83
|
const [type, id] = entityOf(source);
|
|
81
84
|
return { type, id };
|
|
82
85
|
}
|
|
86
|
+
/** The batch's grant, or nothing when there is no claim, or none with a token. */
|
|
87
|
+
export function batchFence(source, token) {
|
|
88
|
+
return source == null || token == null
|
|
89
|
+
? null
|
|
90
|
+
: { ...modelTarget(source), token };
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* The token an operation carries: its own if it names one, the batch's when it
|
|
94
|
+
* writes the claimed row, otherwise none.
|
|
95
|
+
*/
|
|
96
|
+
export function fenceTokenFor(fence, model, id) {
|
|
97
|
+
if (fence === null || id === null)
|
|
98
|
+
return null;
|
|
99
|
+
return model.toLowerCase() === fence.model.toLowerCase() && id === fence.id
|
|
100
|
+
? fence.token
|
|
101
|
+
: null;
|
|
102
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { type FieldRef } from '../schema/fieldRef.js';
|
|
2
3
|
import type { ParticipantKind } from '../types/participant.js';
|
|
3
4
|
/**
|
|
4
5
|
* The wire schemas for coordination — the shapes that keep agents and people
|
|
@@ -16,7 +17,15 @@ import type { ParticipantKind } from '../types/participant.js';
|
|
|
16
17
|
* rather than re-declaring the shapes, and the server validates inbound frames
|
|
17
18
|
* against them at runtime.
|
|
18
19
|
*/
|
|
19
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* A span within a text-bearing part of a row (section body, doc, cell).
|
|
22
|
+
*
|
|
23
|
+
* Overlap is judged on the line axis alone: two ranges conflict when their
|
|
24
|
+
* `[startLine, endLine]` intervals intersect, and columns ride along for
|
|
25
|
+
* display. An editor that addresses by integer position rather than by line
|
|
26
|
+
* maps that position axis straight onto `startLine`/`endLine` — the test is
|
|
27
|
+
* plain interval intersection and never assumes the units are lines of a file.
|
|
28
|
+
*/
|
|
20
29
|
export declare const targetRangeSchema: z.ZodObject<{
|
|
21
30
|
startLine: z.ZodNumber;
|
|
22
31
|
endLine: z.ZodNumber;
|
|
@@ -24,6 +33,42 @@ export declare const targetRangeSchema: z.ZodObject<{
|
|
|
24
33
|
endColumn: z.ZodOptional<z.ZodNumber>;
|
|
25
34
|
}, z.core.$strip>;
|
|
26
35
|
export type TargetRange = z.infer<typeof targetRangeSchema>;
|
|
36
|
+
/**
|
|
37
|
+
* An app-defined claimable part name — a cell (`'B2'`), a section id, a
|
|
38
|
+
* block — made explicit with {@link part}. The schema's own field names need
|
|
39
|
+
* no marker; a name that is NOT a field does, so looseness is a visible
|
|
40
|
+
* decision at the call site rather than a silent absorber of typos.
|
|
41
|
+
*
|
|
42
|
+
* A small object rather than a branded string on purpose: a brand makes a
|
|
43
|
+
* concrete schema's claim params mutually unassignable with the erased
|
|
44
|
+
* `SchemaRecord` view, and the react context boundary erases and restores
|
|
45
|
+
* exactly that way. The object member stays pairwise comparable, so no
|
|
46
|
+
* boundary needs a cast through `unknown`.
|
|
47
|
+
*/
|
|
48
|
+
export interface ClaimPart {
|
|
49
|
+
readonly part: string;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Name an app-defined part of a row for a claim target: `part('B2')` for a
|
|
53
|
+
* cell, `part('sec_intro')` for a section. The conflict rule compares part
|
|
54
|
+
* names as opaque case-insensitive strings, so any name is legal on the
|
|
55
|
+
* wire — this marker exists purely so the type surface stays definite about
|
|
56
|
+
* the model's own fields.
|
|
57
|
+
*/
|
|
58
|
+
export declare function part(name: string): ClaimPart;
|
|
59
|
+
/**
|
|
60
|
+
* The wire spelling of a part name, from whichever spelling the caller used.
|
|
61
|
+
*
|
|
62
|
+
* Three, because they are three different promises. A {@link FieldRef} —
|
|
63
|
+
* `schema.fields.tasks.status` — is a field the schema declares, so a name that
|
|
64
|
+
* does not exist never compiles. `part('B2')` is a name the schema does not
|
|
65
|
+
* know and says so. A bare string is neither, and survives only because the
|
|
66
|
+
* erased `SchemaRecord` view and untyped callers still need it.
|
|
67
|
+
*
|
|
68
|
+
* All three become the same string here: the wire has always carried names, and
|
|
69
|
+
* what differs is how much was known before the crossing.
|
|
70
|
+
*/
|
|
71
|
+
export declare function partName(value: string | ClaimPart | FieldRef): string;
|
|
27
72
|
export declare const participantKindSchema: z.ZodEnum<{
|
|
28
73
|
user: "user";
|
|
29
74
|
agent: "agent";
|
|
@@ -174,24 +219,6 @@ export declare const staleNotificationSchema: z.ZodObject<{
|
|
|
174
219
|
group: z.ZodOptional<z.ZodString>;
|
|
175
220
|
}, z.core.$strip>;
|
|
176
221
|
export type StaleNotification = z.infer<typeof staleNotificationSchema>;
|
|
177
|
-
/**
|
|
178
|
-
* One entry in a commit's batch premise — a read it was based on, so the
|
|
179
|
-
* server can ask "did anything I looked at change?" — broader than the
|
|
180
|
-
* write-target check, which only validates the rows being written. The server
|
|
181
|
-
* re-runs stale detection against each entry at its `readAt`; a moved premise
|
|
182
|
-
* fires the entry's `onStale` disposition (default `reject`) across the whole
|
|
183
|
-
* batch (`notify` holds every write and notifies, `reject` aborts, `overwrite`
|
|
184
|
-
* proceeds silently). An entry comes at one of two granularities:
|
|
185
|
-
*
|
|
186
|
-
* • Row — `{ model, id, readAt, fields? }`: did this specific row, or these
|
|
187
|
-
* specific fields, change?
|
|
188
|
-
* • Group — `{ group, readAt }`: did anything in this sync group change?
|
|
189
|
-
* `group` is a sync-group key such as `report:abc` or `section:s1`, the
|
|
190
|
-
* same unit a participant watches and claims.
|
|
191
|
-
*
|
|
192
|
-
* See `packages/sync-engine/docs/concurrency-convention.md` (§4) for the
|
|
193
|
-
* governing convention and the receive → reconcile loop.
|
|
194
|
-
*/
|
|
195
222
|
export declare const readDependencySchema: z.ZodUnion<readonly [z.ZodObject<{
|
|
196
223
|
model: z.ZodString;
|
|
197
224
|
id: z.ZodString;
|
|
@@ -212,21 +239,9 @@ export declare const readDependencySchema: z.ZodUnion<readonly [z.ZodObject<{
|
|
|
212
239
|
}>>;
|
|
213
240
|
}, z.core.$strip>]>;
|
|
214
241
|
export type ReadDependency = z.infer<typeof readDependencySchema>;
|
|
215
|
-
/**
|
|
216
|
-
* A durable premise — what a participant is watching so that a later
|
|
217
|
-
* change to it opens a {@link StaleNotification}. It is the persisted sibling of
|
|
218
|
-
* a {@link ReadDependency}: the same reference shape, minus the disposition (a
|
|
219
|
-
* track always notifies — that is what tracking is), with an optional `readAt`
|
|
220
|
-
* that defaults to the watermark of the commit that registered it. The row form
|
|
221
|
-
* watches one object; the group form watches a whole sync group ("anything in
|
|
222
|
-
* `report:abc`"). Where a `ReadDependency` is checked once at commit and
|
|
223
|
-
* discarded, a `TrackDependency` is kept and re-checked against every future
|
|
224
|
-
* delta. See `packages/sync-engine/docs/groups.md` for how it drives change
|
|
225
|
-
* propagation.
|
|
226
|
-
*/
|
|
227
242
|
export declare const trackDependencySchema: z.ZodUnion<readonly [z.ZodObject<{
|
|
228
|
-
model: z.ZodString;
|
|
229
243
|
id: z.ZodString;
|
|
244
|
+
model: z.ZodString;
|
|
230
245
|
readAt: z.ZodOptional<z.ZodNumber>;
|
|
231
246
|
}, z.core.$strip>, z.ZodObject<{
|
|
232
247
|
group: z.ZodString;
|
|
@@ -351,6 +366,29 @@ export declare const claimErrorSchema: z.ZodObject<{
|
|
|
351
366
|
policyReason: z.ZodOptional<z.ZodString>;
|
|
352
367
|
}, z.core.$strip>;
|
|
353
368
|
export type ClaimError = z.infer<typeof claimErrorSchema>;
|
|
369
|
+
/**
|
|
370
|
+
* Why a claim ended without its holder releasing it, or was refused.
|
|
371
|
+
*
|
|
372
|
+
* A closed set, because the two refusals are different guarantees and a reader
|
|
373
|
+
* has to be able to tell them apart: `conflict` means someone holds the row
|
|
374
|
+
* right now and you may queue behind them, while `coordination_unavailable`
|
|
375
|
+
* means the coordinator could not answer, so nothing is known about the row.
|
|
376
|
+
* `expired` and `preempted` are the two ways a lease you held ends.
|
|
377
|
+
*
|
|
378
|
+
* The rejection frame's `reason` stays a plain string on the wire — it is
|
|
379
|
+
* frozen, and an older server may send a word not listed here. This is the
|
|
380
|
+
* reader's side of it: a value that parses becomes the typed reason, and one
|
|
381
|
+
* that does not is simply absent rather than smuggled through as prose.
|
|
382
|
+
* {@link claimExpiredSchema} and {@link claimLostSchema} already spelled their
|
|
383
|
+
* reasons as enums; this brings the refusals into line.
|
|
384
|
+
*/
|
|
385
|
+
export declare const claimEventReasonSchema: z.ZodEnum<{
|
|
386
|
+
conflict: "conflict";
|
|
387
|
+
expired: "expired";
|
|
388
|
+
coordination_unavailable: "coordination_unavailable";
|
|
389
|
+
preempted: "preempted";
|
|
390
|
+
}>;
|
|
391
|
+
export type ClaimEventReason = z.infer<typeof claimEventReasonSchema>;
|
|
354
392
|
/**
|
|
355
393
|
* A declared, pending-mutation claim — the unit broadcast inside a presence
|
|
356
394
|
* frame's `activeClaims`. The client supplies the descriptive `targetRef`
|
|
@@ -428,7 +466,12 @@ export declare const wireClaimSchema: z.ZodObject<{
|
|
|
428
466
|
export type WireClaim = z.infer<typeof wireClaimSchema>;
|
|
429
467
|
export declare const claimRejectionSchema: z.ZodObject<{
|
|
430
468
|
claimId: z.ZodString;
|
|
431
|
-
reason: z.
|
|
469
|
+
reason: z.ZodPreprocess<z.ZodOptional<z.ZodEnum<{
|
|
470
|
+
conflict: "conflict";
|
|
471
|
+
expired: "expired";
|
|
472
|
+
coordination_unavailable: "coordination_unavailable";
|
|
473
|
+
preempted: "preempted";
|
|
474
|
+
}>>>;
|
|
432
475
|
target: z.ZodOptional<z.ZodObject<{
|
|
433
476
|
entityType: z.ZodString;
|
|
434
477
|
entityId: z.ZodString;
|
|
@@ -626,7 +669,12 @@ export declare const claimQueuedSchema: z.ZodObject<{
|
|
|
626
669
|
}, z.core.$strip>>;
|
|
627
670
|
policyReason: z.ZodOptional<z.ZodString>;
|
|
628
671
|
position: z.ZodNumber;
|
|
629
|
-
reason: z.ZodOptional<z.
|
|
672
|
+
reason: z.ZodOptional<z.ZodPreprocess<z.ZodOptional<z.ZodEnum<{
|
|
673
|
+
conflict: "conflict";
|
|
674
|
+
expired: "expired";
|
|
675
|
+
coordination_unavailable: "coordination_unavailable";
|
|
676
|
+
preempted: "preempted";
|
|
677
|
+
}>>>>;
|
|
630
678
|
}, z.core.$strip>;
|
|
631
679
|
export type ClaimQueued = z.infer<typeof claimQueuedSchema>;
|
|
632
680
|
/**
|
|
@@ -721,29 +769,6 @@ export declare const claimExpiredSchema: z.ZodObject<{
|
|
|
721
769
|
claimId: z.ZodString;
|
|
722
770
|
}, z.core.$strip>;
|
|
723
771
|
export type ClaimExpired = z.infer<typeof claimExpiredSchema>;
|
|
724
|
-
/**
|
|
725
|
-
* Why a claim ended without its holder releasing it, or was refused.
|
|
726
|
-
*
|
|
727
|
-
* A closed set, because the two refusals are different guarantees and a reader
|
|
728
|
-
* has to be able to tell them apart: `conflict` means someone holds the row
|
|
729
|
-
* right now and you may queue behind them, while `coordination_unavailable`
|
|
730
|
-
* means the coordinator could not answer, so nothing is known about the row.
|
|
731
|
-
* `expired` and `preempted` are the two ways a lease you held ends.
|
|
732
|
-
*
|
|
733
|
-
* The rejection frame's `reason` stays a plain string on the wire — it is
|
|
734
|
-
* frozen, and an older server may send a word not listed here. This is the
|
|
735
|
-
* reader's side of it: a value that parses becomes the typed reason, and one
|
|
736
|
-
* that does not is simply absent rather than smuggled through as prose.
|
|
737
|
-
* {@link claimExpiredSchema} and {@link claimLostSchema} already spelled their
|
|
738
|
-
* reasons as enums; this brings the refusals into line.
|
|
739
|
-
*/
|
|
740
|
-
export declare const claimEventReasonSchema: z.ZodEnum<{
|
|
741
|
-
conflict: "conflict";
|
|
742
|
-
expired: "expired";
|
|
743
|
-
preempted: "preempted";
|
|
744
|
-
coordination_unavailable: "coordination_unavailable";
|
|
745
|
-
}>;
|
|
746
|
-
export type ClaimEventReason = z.infer<typeof claimEventReasonSchema>;
|
|
747
772
|
/**
|
|
748
773
|
* What a {@link ModelClaim} points at — the target locator as SDK callers see
|
|
749
774
|
* it, keyed by `model` and `id` rather than the wire schema's `entityType` and
|
|
@@ -1017,13 +1042,51 @@ export declare const claimHeartbeatBatchAckPayloadSchema: z.ZodObject<{
|
|
|
1017
1042
|
}, z.core.$strip>>;
|
|
1018
1043
|
}, z.core.$strip>;
|
|
1019
1044
|
export type ClaimHeartbeatBatchAckPayload = z.infer<typeof claimHeartbeatBatchAckPayloadSchema>;
|
|
1045
|
+
/**
|
|
1046
|
+
* How many scopes one frame may name. A coarse abuse ceiling, not a business
|
|
1047
|
+
* limit: a connection legitimately watches a handful of entities, and a list
|
|
1048
|
+
* this long is an amplification attempt rather than a workload. Declared here,
|
|
1049
|
+
* beside the two frames it bounds, so neither can be given a different answer.
|
|
1050
|
+
*/
|
|
1051
|
+
export declare const MAX_FRAME_SYNC_GROUPS = 200;
|
|
1052
|
+
/**
|
|
1053
|
+
* The `claim` payload a client sends — the frame behind `join`.
|
|
1054
|
+
*
|
|
1055
|
+
* It opens one participant claim: the connection is added to each named
|
|
1056
|
+
* scope's fan-out under `claimId`, announced into its presence roster, and
|
|
1057
|
+
* holds that interest until `release`, the TTL lapses, or the socket closes.
|
|
1058
|
+
* The handle is client-chosen because the client must be able to `release`
|
|
1059
|
+
* the exact claim it opened while others stay open.
|
|
1060
|
+
*
|
|
1061
|
+
* This shape was, for a long time, written three times — built as a literal in
|
|
1062
|
+
* the transport, restated as an interface on the server, and read back through
|
|
1063
|
+
* a cast in the frame handler — which is how the frame came to be the only
|
|
1064
|
+
* coordination message with no runtime check on the way in.
|
|
1065
|
+
*/
|
|
1066
|
+
export declare const participantClaimPayloadSchema: z.ZodObject<{
|
|
1067
|
+
claimId: z.ZodString;
|
|
1068
|
+
syncGroups: z.ZodArray<z.ZodUnion<readonly [z.ZodLiteral<"default">, z.core.$ZodBranded<z.ZodTemplateLiteral<`${string}:${string}`>, "SyncGroup", "out">]>>;
|
|
1069
|
+
capabilityToken: z.ZodOptional<z.ZodString>;
|
|
1070
|
+
ttlSeconds: z.ZodOptional<z.ZodNumber>;
|
|
1071
|
+
}, z.core.$strip>;
|
|
1072
|
+
export type ParticipantClaimPayload = z.infer<typeof participantClaimPayloadSchema>;
|
|
1073
|
+
/**
|
|
1074
|
+
* The `release` payload — drop one participant claim by its handle.
|
|
1075
|
+
*
|
|
1076
|
+
* A projection of the claim it releases rather than a second object, so the
|
|
1077
|
+
* handle cannot be spelled one way when opened and another when dropped.
|
|
1078
|
+
* Idempotent by contract: the server accepts an unknown handle silently, so a
|
|
1079
|
+
* client releasing everything at shutdown never has to check what is still open.
|
|
1080
|
+
*/
|
|
1081
|
+
export declare const participantReleasePayloadSchema: z.ZodObject<{
|
|
1082
|
+
claimId: z.ZodString;
|
|
1083
|
+
}, z.core.$strip>;
|
|
1084
|
+
export type ParticipantReleasePayload = z.infer<typeof participantReleasePayloadSchema>;
|
|
1020
1085
|
/**
|
|
1021
1086
|
* The `update_subscription` payload a client sends. It replaces the
|
|
1022
|
-
* connection's read interest with the complete set of sync groups — the
|
|
1023
|
-
* counterpart to
|
|
1024
|
-
*
|
|
1025
|
-
* malformed group is rejected on ingest rather than silently indexed. The
|
|
1026
|
-
* element type is strict because this is untrusted client input.
|
|
1087
|
+
* connection's read interest with the complete set of sync groups — the
|
|
1088
|
+
* unleased counterpart to {@link participantClaimPayloadSchema}, with no
|
|
1089
|
+
* handle, no TTL, and no roster entry.
|
|
1027
1090
|
*/
|
|
1028
1091
|
export declare const updateSubscriptionPayloadSchema: z.ZodObject<{
|
|
1029
1092
|
syncGroups: z.ZodArray<z.ZodUnion<readonly [z.ZodLiteral<"default">, z.core.$ZodBranded<z.ZodTemplateLiteral<`${string}:${string}`>, "SyncGroup", "out">]>>;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { syncGroupInputSchema } from '../schema/roles.js';
|
|
3
|
+
import { isFieldRef } from '../schema/fieldRef.js';
|
|
3
4
|
/**
|
|
4
5
|
* The wire schemas for coordination — the shapes that keep agents and people
|
|
5
6
|
* from overwriting each other on a shared row. Coordination works in three
|
|
@@ -19,12 +20,71 @@ import { syncGroupInputSchema } from '../schema/roles.js';
|
|
|
19
20
|
// ─────────────────────────────────────────────────────────────────────────
|
|
20
21
|
// Shared primitives
|
|
21
22
|
// ─────────────────────────────────────────────────────────────────────────
|
|
22
|
-
/**
|
|
23
|
-
|
|
23
|
+
/**
|
|
24
|
+
* A span within a text-bearing part of a row (section body, doc, cell).
|
|
25
|
+
*
|
|
26
|
+
* Overlap is judged on the line axis alone: two ranges conflict when their
|
|
27
|
+
* `[startLine, endLine]` intervals intersect, and columns ride along for
|
|
28
|
+
* display. An editor that addresses by integer position rather than by line
|
|
29
|
+
* maps that position axis straight onto `startLine`/`endLine` — the test is
|
|
30
|
+
* plain interval intersection and never assumes the units are lines of a file.
|
|
31
|
+
*/
|
|
32
|
+
export const targetRangeSchema = z
|
|
33
|
+
.object({
|
|
24
34
|
startLine: z.number(),
|
|
25
35
|
endLine: z.number(),
|
|
26
36
|
startColumn: z.number().optional(),
|
|
27
37
|
endColumn: z.number().optional(),
|
|
38
|
+
})
|
|
39
|
+
// An inverted span claims nothing and collides with nothing — reject it at
|
|
40
|
+
// the boundary instead of granting a lease that never excludes anyone.
|
|
41
|
+
.refine((range) => range.startLine <= range.endLine, {
|
|
42
|
+
message: 'range.startLine must not be greater than range.endLine.',
|
|
43
|
+
});
|
|
44
|
+
/**
|
|
45
|
+
* Name an app-defined part of a row for a claim target: `part('B2')` for a
|
|
46
|
+
* cell, `part('sec_intro')` for a section. The conflict rule compares part
|
|
47
|
+
* names as opaque case-insensitive strings, so any name is legal on the
|
|
48
|
+
* wire — this marker exists purely so the type surface stays definite about
|
|
49
|
+
* the model's own fields.
|
|
50
|
+
*/
|
|
51
|
+
export function part(name) {
|
|
52
|
+
return { part: name };
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* The wire spelling of a part name, from whichever spelling the caller used.
|
|
56
|
+
*
|
|
57
|
+
* Three, because they are three different promises. A {@link FieldRef} —
|
|
58
|
+
* `schema.fields.tasks.status` — is a field the schema declares, so a name that
|
|
59
|
+
* does not exist never compiles. `part('B2')` is a name the schema does not
|
|
60
|
+
* know and says so. A bare string is neither, and survives only because the
|
|
61
|
+
* erased `SchemaRecord` view and untyped callers still need it.
|
|
62
|
+
*
|
|
63
|
+
* All three become the same string here: the wire has always carried names, and
|
|
64
|
+
* what differs is how much was known before the crossing.
|
|
65
|
+
*/
|
|
66
|
+
export function partName(value) {
|
|
67
|
+
if (typeof value === 'string')
|
|
68
|
+
return value;
|
|
69
|
+
return isFieldRef(value) ? value.field : value.part;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* One claimable part name.
|
|
73
|
+
*
|
|
74
|
+
* Names compare as opaque strings, so any name is legal — except one that is
|
|
75
|
+
* plainly several. A caller who needed to claim two parts and had only `field`
|
|
76
|
+
* to say it in packed them into one delimited string, and because
|
|
77
|
+
* `blocks:b_1,b_2` and `blocks:b_1` are different names, both writers were
|
|
78
|
+
* granted a lease on `b_1` and one of their updates was lost with nothing
|
|
79
|
+
* raised. `fields` exists to say that, and refusing the packed spelling is what
|
|
80
|
+
* makes the mistake visible at the moment it is made rather than as a missing
|
|
81
|
+
* update later.
|
|
82
|
+
*
|
|
83
|
+
* Deliberately narrow: only the comma, because that is what a caller reaches
|
|
84
|
+
* for to join a list. A part name is otherwise free.
|
|
85
|
+
*/
|
|
86
|
+
const partNameSchema = z.string().refine((name) => !name.includes(','), {
|
|
87
|
+
message: 'A part name cannot contain a comma. Claim several parts with `fields: [a, b]` — two names in one `field` compare as a single unrelated name, so both writers would be granted the same part.',
|
|
28
88
|
});
|
|
29
89
|
export const participantKindSchema = z.enum(['user', 'agent', 'system']);
|
|
30
90
|
const _participantKindContract = true;
|
|
@@ -104,7 +164,7 @@ export const targetRefSchema = z.object({
|
|
|
104
164
|
entityId: z.string(),
|
|
105
165
|
path: z.string().optional(),
|
|
106
166
|
range: targetRangeSchema.optional(),
|
|
107
|
-
field:
|
|
167
|
+
field: partNameSchema.optional(),
|
|
108
168
|
/**
|
|
109
169
|
* Several named parts of one row, claimed together — three sections of a
|
|
110
170
|
* document, two cells of a table.
|
|
@@ -120,7 +180,7 @@ export const targetRefSchema = z.object({
|
|
|
120
180
|
* a claim naming `field` and a claim naming `fields` still compare correctly
|
|
121
181
|
* against each other.
|
|
122
182
|
*/
|
|
123
|
-
fields: z.array(
|
|
183
|
+
fields: z.array(partNameSchema).readonly().optional(),
|
|
124
184
|
meta: z.record(z.string(), z.unknown()).optional(),
|
|
125
185
|
});
|
|
126
186
|
/**
|
|
@@ -231,42 +291,54 @@ export const staleNotificationSchema = z.object({
|
|
|
231
291
|
* See `packages/sync-engine/docs/concurrency-convention.md` (§4) for the
|
|
232
292
|
* governing convention and the receive → reconcile loop.
|
|
233
293
|
*/
|
|
294
|
+
const readRowDependencySchema = z.object({
|
|
295
|
+
model: z.string(),
|
|
296
|
+
id: z.string(),
|
|
297
|
+
readAt: z.number(),
|
|
298
|
+
fields: z.array(z.string()).readonly().optional(),
|
|
299
|
+
onStale: onStaleModeSchema.optional(),
|
|
300
|
+
});
|
|
301
|
+
const readGroupDependencySchema = z.object({
|
|
302
|
+
group: z.string(),
|
|
303
|
+
readAt: z.number(),
|
|
304
|
+
onStale: onStaleModeSchema.optional(),
|
|
305
|
+
});
|
|
234
306
|
export const readDependencySchema = z.union([
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
id: z.string(),
|
|
238
|
-
readAt: z.number(),
|
|
239
|
-
fields: z.array(z.string()).readonly().optional(),
|
|
240
|
-
onStale: onStaleModeSchema.optional(),
|
|
241
|
-
}),
|
|
242
|
-
z.object({
|
|
243
|
-
group: z.string(),
|
|
244
|
-
readAt: z.number(),
|
|
245
|
-
onStale: onStaleModeSchema.optional(),
|
|
246
|
-
}),
|
|
307
|
+
readRowDependencySchema,
|
|
308
|
+
readGroupDependencySchema,
|
|
247
309
|
]);
|
|
248
310
|
/**
|
|
249
311
|
* A durable premise — what a participant is watching so that a later
|
|
250
|
-
* change to it opens a {@link StaleNotification}.
|
|
251
|
-
*
|
|
252
|
-
*
|
|
253
|
-
*
|
|
254
|
-
*
|
|
255
|
-
*
|
|
256
|
-
*
|
|
257
|
-
*
|
|
258
|
-
*
|
|
312
|
+
* change to it opens a {@link StaleNotification}. Where a {@link ReadDependency}
|
|
313
|
+
* is checked once at commit and discarded, a `TrackDependency` is kept and
|
|
314
|
+
* re-checked against every future delta. The row form watches one object; the
|
|
315
|
+
* group form watches a whole sync group ("anything in `report:abc`"). See
|
|
316
|
+
* `packages/sync-engine/docs/groups.md` for how it drives change propagation.
|
|
317
|
+
*
|
|
318
|
+
* It is a PROJECTION of the ephemeral premise, not a second declaration of the
|
|
319
|
+
* same reference: a track names its target exactly as a read does, and the
|
|
320
|
+
* three ways it differs are stated here as omissions the compiler holds.
|
|
321
|
+
*
|
|
322
|
+
* • no `onStale` — a track always notifies; that is what tracking is;
|
|
323
|
+
* • no `fields` — a track fires at row grain, because the server keeps one
|
|
324
|
+
* row per tracked target and reports that the target moved, not which
|
|
325
|
+
* column did (`track_dependencies` has no field axis to store one in);
|
|
326
|
+
* • `readAt` optional — it defaults to the watermark of the commit that
|
|
327
|
+
* registered the track, so a caller with nothing to say about when it last
|
|
328
|
+
* looked gets "from here on".
|
|
329
|
+
*
|
|
330
|
+
* A field added to the premise reaches both halves; a field the durable half
|
|
331
|
+
* genuinely cannot carry has to be omitted here on purpose, in one line, rather
|
|
332
|
+
* than by being quietly left out of a copy.
|
|
259
333
|
*/
|
|
334
|
+
const trackReadAtSchema = { readAt: z.number().optional() };
|
|
260
335
|
export const trackDependencySchema = z.union([
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
group: z.string(),
|
|
268
|
-
readAt: z.number().optional(),
|
|
269
|
-
}),
|
|
336
|
+
readRowDependencySchema
|
|
337
|
+
.omit({ onStale: true, fields: true, readAt: true })
|
|
338
|
+
.extend(trackReadAtSchema),
|
|
339
|
+
readGroupDependencySchema
|
|
340
|
+
.omit({ onStale: true, readAt: true })
|
|
341
|
+
.extend(trackReadAtSchema),
|
|
270
342
|
]);
|
|
271
343
|
// ─────────────────────────────────────────────────────────────────────────
|
|
272
344
|
// Layer 2 — pessimistic claims and leases
|
|
@@ -391,6 +463,28 @@ export const claimErrorSchema = z.object({
|
|
|
391
463
|
/** Optional conflict-policy explanation. Additive: older frames omit it. */
|
|
392
464
|
policyReason: z.string().optional(),
|
|
393
465
|
});
|
|
466
|
+
/**
|
|
467
|
+
* Why a claim ended without its holder releasing it, or was refused.
|
|
468
|
+
*
|
|
469
|
+
* A closed set, because the two refusals are different guarantees and a reader
|
|
470
|
+
* has to be able to tell them apart: `conflict` means someone holds the row
|
|
471
|
+
* right now and you may queue behind them, while `coordination_unavailable`
|
|
472
|
+
* means the coordinator could not answer, so nothing is known about the row.
|
|
473
|
+
* `expired` and `preempted` are the two ways a lease you held ends.
|
|
474
|
+
*
|
|
475
|
+
* The rejection frame's `reason` stays a plain string on the wire — it is
|
|
476
|
+
* frozen, and an older server may send a word not listed here. This is the
|
|
477
|
+
* reader's side of it: a value that parses becomes the typed reason, and one
|
|
478
|
+
* that does not is simply absent rather than smuggled through as prose.
|
|
479
|
+
* {@link claimExpiredSchema} and {@link claimLostSchema} already spelled their
|
|
480
|
+
* reasons as enums; this brings the refusals into line.
|
|
481
|
+
*/
|
|
482
|
+
export const claimEventReasonSchema = z.enum([
|
|
483
|
+
'conflict',
|
|
484
|
+
'coordination_unavailable',
|
|
485
|
+
'expired',
|
|
486
|
+
'preempted',
|
|
487
|
+
]);
|
|
394
488
|
/**
|
|
395
489
|
* A declared, pending-mutation claim — the unit broadcast inside a presence
|
|
396
490
|
* frame's `activeClaims`. The client supplies the descriptive `targetRef`
|
|
@@ -404,7 +498,26 @@ export const wireClaimSchema = wireClaimBaseSchema.extend({
|
|
|
404
498
|
});
|
|
405
499
|
export const claimRejectionSchema = z.object({
|
|
406
500
|
claimId: z.string(),
|
|
407
|
-
|
|
501
|
+
/**
|
|
502
|
+
* Why the claim was refused, as one of {@link claimEventReasonSchema}'s
|
|
503
|
+
* words — so a caller can branch on it. `conflict` means someone holds the
|
|
504
|
+
* row right now and you may queue behind them; `coordination_unavailable`
|
|
505
|
+
* means the coordinator could not answer, so nothing is known about it.
|
|
506
|
+
* Those are different decisions, and a free string made them one.
|
|
507
|
+
*
|
|
508
|
+
* The wire spelling is frozen and an older server may send a word not listed
|
|
509
|
+
* there, so this reads the way {@link wireParticipantKindSchema} reads its
|
|
510
|
+
* dialect: a value that parses becomes the typed reason, and one that does
|
|
511
|
+
* not is absent rather than smuggled through as prose. Prose has its own
|
|
512
|
+
* field — `policyReason` below — which is why nothing is lost by refusing to
|
|
513
|
+
* carry it here.
|
|
514
|
+
*
|
|
515
|
+
* The registry code a caller finally sees on `AbloClaimedError`
|
|
516
|
+
* (`claim_conflict`) stays a separate vocabulary, mapped at the throw. Two
|
|
517
|
+
* small vocabularies with one mapping point beat one vocabulary and a
|
|
518
|
+
* projection of it that has to be maintained as the registry grows.
|
|
519
|
+
*/
|
|
520
|
+
reason: z.preprocess((value) => (claimEventReasonSchema.safeParse(value).success ? value : undefined), claimEventReasonSchema.optional()),
|
|
408
521
|
target: targetRefSchema.optional(),
|
|
409
522
|
heldBy: z.string().optional(),
|
|
410
523
|
/**
|
|
@@ -523,28 +636,6 @@ export const claimQueueSchema = z.object({
|
|
|
523
636
|
export const claimExpiredSchema = z.object({
|
|
524
637
|
claimId: z.string(),
|
|
525
638
|
});
|
|
526
|
-
/**
|
|
527
|
-
* Why a claim ended without its holder releasing it, or was refused.
|
|
528
|
-
*
|
|
529
|
-
* A closed set, because the two refusals are different guarantees and a reader
|
|
530
|
-
* has to be able to tell them apart: `conflict` means someone holds the row
|
|
531
|
-
* right now and you may queue behind them, while `coordination_unavailable`
|
|
532
|
-
* means the coordinator could not answer, so nothing is known about the row.
|
|
533
|
-
* `expired` and `preempted` are the two ways a lease you held ends.
|
|
534
|
-
*
|
|
535
|
-
* The rejection frame's `reason` stays a plain string on the wire — it is
|
|
536
|
-
* frozen, and an older server may send a word not listed here. This is the
|
|
537
|
-
* reader's side of it: a value that parses becomes the typed reason, and one
|
|
538
|
-
* that does not is simply absent rather than smuggled through as prose.
|
|
539
|
-
* {@link claimExpiredSchema} and {@link claimLostSchema} already spelled their
|
|
540
|
-
* reasons as enums; this brings the refusals into line.
|
|
541
|
-
*/
|
|
542
|
-
export const claimEventReasonSchema = z.enum([
|
|
543
|
-
'conflict',
|
|
544
|
-
'coordination_unavailable',
|
|
545
|
-
'expired',
|
|
546
|
-
'preempted',
|
|
547
|
-
]);
|
|
548
639
|
/**
|
|
549
640
|
* What a {@link ModelClaim} points at — the target locator as SDK callers see
|
|
550
641
|
* it, keyed by `model` and `id` rather than the wire schema's `entityType` and
|
|
@@ -849,18 +940,91 @@ export const claimHeartbeatBatchAckPayloadSchema = z.object({
|
|
|
849
940
|
results: z.array(claimHeartbeatAckPayloadSchema),
|
|
850
941
|
});
|
|
851
942
|
// ─────────────────────────────────────────────────────────────────────────
|
|
852
|
-
// Read interest —
|
|
943
|
+
// Read interest — what a connection receives
|
|
944
|
+
//
|
|
945
|
+
// Two frames set it, and they differ in exactly one way: whether the
|
|
946
|
+
// interest is leased.
|
|
947
|
+
//
|
|
948
|
+
// • `claim` — a PARTICIPANT claim. Adds a scope under a handle, with a
|
|
949
|
+
// TTL and an optional capability token, and announces the sender into
|
|
950
|
+
// that scope's roster. This is the frame `ablo.<model>.join(...)` sends;
|
|
951
|
+
// `release` drops it. Several may be open on one connection at once.
|
|
952
|
+
// • `update_subscription` — REPLACES the connection's whole read set. No
|
|
953
|
+
// handle, no lease, no roster entry.
|
|
954
|
+
//
|
|
955
|
+
// Both are bounded by the connection credential's grant, and both name their
|
|
956
|
+
// groups the same way, so both parse their `syncGroups` through the same
|
|
957
|
+
// element schema. Neither is the row lease — that is `claim_begin`, in the
|
|
958
|
+
// pessimistic-claims block above, which shares only a word.
|
|
853
959
|
// ─────────────────────────────────────────────────────────────────────────
|
|
854
960
|
/**
|
|
855
|
-
*
|
|
856
|
-
*
|
|
857
|
-
*
|
|
961
|
+
* How many scopes one frame may name. A coarse abuse ceiling, not a business
|
|
962
|
+
* limit: a connection legitimately watches a handful of entities, and a list
|
|
963
|
+
* this long is an amplification attempt rather than a workload. Declared here,
|
|
964
|
+
* beside the two frames it bounds, so neither can be given a different answer.
|
|
965
|
+
*/
|
|
966
|
+
export const MAX_FRAME_SYNC_GROUPS = 200;
|
|
967
|
+
/**
|
|
968
|
+
* The sync groups a scope-subscription frame names. Each entry is a
|
|
858
969
|
* {@link syncGroupInputSchema} (`'default'` or a branded `kind:id`), so a
|
|
859
|
-
* malformed group is rejected on ingest rather than silently indexed
|
|
860
|
-
*
|
|
970
|
+
* malformed group is rejected on ingest rather than silently indexed — a group
|
|
971
|
+
* that does not parse matches nothing, and subscribing to nothing quietly is
|
|
972
|
+
* the failure this element type exists to prevent.
|
|
973
|
+
*
|
|
974
|
+
* Strict because this is untrusted client input, and shared because the two
|
|
975
|
+
* frames below carry the same value: when they disagreed, `claim` accepted a
|
|
976
|
+
* malformed group that `update_subscription` refused, and the connection
|
|
977
|
+
* ended up leased to a scope it could never receive.
|
|
978
|
+
*/
|
|
979
|
+
const frameSyncGroupsSchema = z
|
|
980
|
+
.array(syncGroupInputSchema)
|
|
981
|
+
.max(MAX_FRAME_SYNC_GROUPS);
|
|
982
|
+
/**
|
|
983
|
+
* The `claim` payload a client sends — the frame behind `join`.
|
|
984
|
+
*
|
|
985
|
+
* It opens one participant claim: the connection is added to each named
|
|
986
|
+
* scope's fan-out under `claimId`, announced into its presence roster, and
|
|
987
|
+
* holds that interest until `release`, the TTL lapses, or the socket closes.
|
|
988
|
+
* The handle is client-chosen because the client must be able to `release`
|
|
989
|
+
* the exact claim it opened while others stay open.
|
|
990
|
+
*
|
|
991
|
+
* This shape was, for a long time, written three times — built as a literal in
|
|
992
|
+
* the transport, restated as an interface on the server, and read back through
|
|
993
|
+
* a cast in the frame handler — which is how the frame came to be the only
|
|
994
|
+
* coordination message with no runtime check on the way in.
|
|
995
|
+
*/
|
|
996
|
+
export const participantClaimPayloadSchema = z.object({
|
|
997
|
+
/** Client-chosen handle. Echoed on `claim_ack`; names the claim to `release`. */
|
|
998
|
+
claimId: z.string().min(1),
|
|
999
|
+
syncGroups: frameSyncGroupsSchema,
|
|
1000
|
+
/**
|
|
1001
|
+
* A narrower capability to present for this claim than the connection's own.
|
|
1002
|
+
* Absent means the connection's credential governs it.
|
|
1003
|
+
*/
|
|
1004
|
+
capabilityToken: z.string().optional(),
|
|
1005
|
+
/**
|
|
1006
|
+
* Crash cleanup, in seconds. The server caps it at the capability's own TTL;
|
|
1007
|
+
* absent means the claim lives until `release` or disconnect.
|
|
1008
|
+
*/
|
|
1009
|
+
ttlSeconds: z.number().optional(),
|
|
1010
|
+
});
|
|
1011
|
+
/**
|
|
1012
|
+
* The `release` payload — drop one participant claim by its handle.
|
|
1013
|
+
*
|
|
1014
|
+
* A projection of the claim it releases rather than a second object, so the
|
|
1015
|
+
* handle cannot be spelled one way when opened and another when dropped.
|
|
1016
|
+
* Idempotent by contract: the server accepts an unknown handle silently, so a
|
|
1017
|
+
* client releasing everything at shutdown never has to check what is still open.
|
|
1018
|
+
*/
|
|
1019
|
+
export const participantReleasePayloadSchema = participantClaimPayloadSchema.pick({ claimId: true });
|
|
1020
|
+
/**
|
|
1021
|
+
* The `update_subscription` payload a client sends. It replaces the
|
|
1022
|
+
* connection's read interest with the complete set of sync groups — the
|
|
1023
|
+
* unleased counterpart to {@link participantClaimPayloadSchema}, with no
|
|
1024
|
+
* handle, no TTL, and no roster entry.
|
|
861
1025
|
*/
|
|
862
1026
|
export const updateSubscriptionPayloadSchema = z.object({
|
|
863
|
-
syncGroups:
|
|
1027
|
+
syncGroups: frameSyncGroupsSchema,
|
|
864
1028
|
});
|
|
865
1029
|
/**
|
|
866
1030
|
* `subscription_ack` payload (server → client). Echoes the connection's
|