@abloatai/ablo 0.10.1 → 0.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +34 -0
- package/README.md +63 -23
- 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 +369 -67
- package/dist/client/Ablo.d.ts +30 -63
- package/dist/client/Ablo.js +124 -103
- package/dist/client/ApiClient.d.ts +6 -5
- package/dist/client/ApiClient.js +86 -62
- package/dist/client/auth.d.ts +9 -4
- package/dist/client/auth.js +40 -5
- package/dist/client/createModelProxy.d.ts +41 -54
- package/dist/client/createModelProxy.js +123 -20
- 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 +16 -16
- 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/schema.d.ts +3 -3
- 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 +23 -0
- package/dist/transactions/TransactionQueue.js +186 -12
- package/dist/types/global.d.ts +18 -13
- package/dist/types/global.js +11 -6
- 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/api.md +3 -3
- package/docs/client-behavior.md +6 -3
- package/docs/coordination.md +13 -3
- package/docs/data-sources.md +29 -9
- package/docs/migration.md +40 -0
- package/docs/quickstart.md +61 -33
- package/docs/react.md +46 -0
- package/llms-full.txt +25 -8
- package/llms.txt +11 -9
- 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
package/dist/react/index.d.ts
CHANGED
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
* useAblo((ablo) => ablo.<model>.claim.state(...)) — reactive coordination reads
|
|
31
31
|
* useParticipant({ scope }) — join multiplayer for a scope, get peers/claims
|
|
32
32
|
* usePresence() — typed presence view
|
|
33
|
-
*
|
|
33
|
+
* useClaim(name) — typed claim dispatcher
|
|
34
34
|
*
|
|
35
35
|
* ── Breaking changes from v0.2.x ───────────────────────────────────
|
|
36
36
|
* Removed: <SyncProvider>, SyncContext, useSyncContext — folded into
|
|
@@ -43,8 +43,8 @@
|
|
|
43
43
|
* Changed: useSyncStatus() now returns a discriminated union. See the
|
|
44
44
|
* migration notes in CHANGELOG.md.
|
|
45
45
|
*/
|
|
46
|
-
export type { DefaultSyncShape, ResolveSchema, ResolvePresence,
|
|
47
|
-
export { AbloProvider, useParticipant, useSync, useSyncStore, type AbloProviderProps, type ParticipantScope, type ParticipantStatus, type UseParticipantOptions, type UseParticipantReturn, type MeshParticipantStatus, } from './AbloProvider.js';
|
|
46
|
+
export type { DefaultSyncShape, ResolveSchema, ResolvePresence, ResolveClaims, ResolveUserMeta, ResolveModelKey, } from '../types/global.js';
|
|
47
|
+
export { AbloProvider, useParticipant, usePeers, useSync, useSyncStore, type AbloProviderProps, type ParticipantScope, type ParticipantStatus, type UseParticipantOptions, type UseParticipantReturn, type MeshParticipantStatus, } from './AbloProvider.js';
|
|
48
48
|
export { SyncGroupProvider, useSyncGroup, type SyncGroupProviderProps, } from './SyncGroupProvider.js';
|
|
49
49
|
export { ClientSideSuspense, type ClientSideSuspenseProps, } from './ClientSideSuspense.js';
|
|
50
50
|
export { DefaultFallback } from './DefaultFallback.js';
|
|
@@ -60,5 +60,5 @@ export { useMutators, type MutatorInvokers, type InvokerFor, type UseMutatorsOpt
|
|
|
60
60
|
export { useUndoScope, type UseUndoScopeResult } from './useUndoScope.js';
|
|
61
61
|
export { useAblo, type UseAbloHydratedModelResult, type UseAbloModelOptions, type UseAbloModelResult, } from './useAblo.js';
|
|
62
62
|
export { usePresence } from './usePresence.js';
|
|
63
|
-
export {
|
|
63
|
+
export { useClaim } from './useClaim.js';
|
|
64
64
|
export { ModelScope } from '../types/index.js';
|
package/dist/react/index.js
CHANGED
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
* useAblo((ablo) => ablo.<model>.claim.state(...)) — reactive coordination reads
|
|
31
31
|
* useParticipant({ scope }) — join multiplayer for a scope, get peers/claims
|
|
32
32
|
* usePresence() — typed presence view
|
|
33
|
-
*
|
|
33
|
+
* useClaim(name) — typed claim dispatcher
|
|
34
34
|
*
|
|
35
35
|
* ── Breaking changes from v0.2.x ───────────────────────────────────
|
|
36
36
|
* Removed: <SyncProvider>, SyncContext, useSyncContext — folded into
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
* migration notes in CHANGELOG.md.
|
|
45
45
|
*/
|
|
46
46
|
// ── Umbrella provider + lifecycle hooks ────────────────────────────
|
|
47
|
-
export { AbloProvider, useParticipant, useSync, useSyncStore, } from './AbloProvider.js';
|
|
47
|
+
export { AbloProvider, useParticipant, usePeers, useSync, useSyncStore, } from './AbloProvider.js';
|
|
48
48
|
export { SyncGroupProvider, useSyncGroup, } from './SyncGroupProvider.js';
|
|
49
49
|
export { ClientSideSuspense, } from './ClientSideSuspense.js';
|
|
50
50
|
export { DefaultFallback } from './DefaultFallback.js';
|
|
@@ -63,8 +63,8 @@ export { useReactive } from './useReactive.js';
|
|
|
63
63
|
export { useMutators, } from './useMutators.js';
|
|
64
64
|
export { useUndoScope } from './useUndoScope.js';
|
|
65
65
|
export { useAblo, } from './useAblo.js';
|
|
66
|
-
// ── Presence +
|
|
66
|
+
// ── Presence + claim (typed via Register module augmentation) ─────
|
|
67
67
|
export { usePresence } from './usePresence.js';
|
|
68
|
-
export {
|
|
68
|
+
export { useClaim } from './useClaim.js';
|
|
69
69
|
// ── ModelScope re-export ───────────────────────────────────────────
|
|
70
70
|
export { ModelScope } from '../types/index.js';
|
package/dist/react/useAblo.js
CHANGED
|
@@ -12,7 +12,7 @@ function readModelResult(engine, modelClient, id, initial) {
|
|
|
12
12
|
const data = snapshotValue(modelClient.get(id) ?? initial);
|
|
13
13
|
const meta = getModelClientMeta(modelClient);
|
|
14
14
|
const claims = meta && engine
|
|
15
|
-
? engine.
|
|
15
|
+
? engine.claims.list({ model: meta.key, id })
|
|
16
16
|
: EMPTY_CLAIMS;
|
|
17
17
|
return { data, claims, claimed: claims.length > 0 };
|
|
18
18
|
}
|
|
@@ -37,18 +37,18 @@ export function useAblo(modelOrSelect, id, options) {
|
|
|
37
37
|
: typeof modelOrSelect === 'function'
|
|
38
38
|
? undefined
|
|
39
39
|
: modelOrSelect;
|
|
40
|
-
// Claims live on a non-MobX event emitter (engine.
|
|
40
|
+
// Claims live on a non-MobX event emitter (engine.claims), so the useReactive
|
|
41
41
|
// reactions below cannot track them — we bridge changes through a setState bump.
|
|
42
42
|
// ONLY the model-row form (`id !== undefined`) actually reads claims, so gate the
|
|
43
43
|
// subscription on `id`. The selector-only form (`useAblo((a) => a.x.get/getAll)`)
|
|
44
|
-
// never reads claims; subscribing it to the workspace-global
|
|
45
|
-
// re-render + double-compute it on every
|
|
44
|
+
// never reads claims; subscribing it to the workspace-global claim stream would
|
|
45
|
+
// re-render + double-compute it on every claim/presence delta anywhere (a real
|
|
46
46
|
// storm during AI editing / live collaboration) for a value that can't change.
|
|
47
47
|
const [claimVersion, setClaimVersion] = useState(0);
|
|
48
48
|
useEffect(() => {
|
|
49
49
|
if (!engine || id === undefined)
|
|
50
50
|
return;
|
|
51
|
-
return engine.
|
|
51
|
+
return engine.claims.onChange(() => setClaimVersion((version) => version + 1));
|
|
52
52
|
}, [engine, id]);
|
|
53
53
|
const selected = useReactive(() => {
|
|
54
54
|
if (!engine || !isSelectorOnly || typeof modelOrSelect !== 'function') {
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ResolveClaims } from '../types/global.js';
|
|
2
2
|
/**
|
|
3
|
-
* Named-
|
|
3
|
+
* Named-claim invoker, typed via `ResolveClaims[ClaimName]`.
|
|
4
4
|
*
|
|
5
|
-
* The consumer declares their
|
|
5
|
+
* The consumer declares their claim vocabulary in the global:
|
|
6
6
|
*
|
|
7
7
|
* ```ts
|
|
8
8
|
* declare module '@abloatai/ablo' {
|
|
9
9
|
* interface Register {
|
|
10
|
-
*
|
|
10
|
+
* Claims: {
|
|
11
11
|
* editLayer: { slideId: string; layerId: string };
|
|
12
12
|
* generateWithAI: { entityId: string; tool: string };
|
|
13
13
|
* };
|
|
@@ -15,15 +15,15 @@ import type { ResolveIntents } from '../types/global.js';
|
|
|
15
15
|
* }
|
|
16
16
|
* ```
|
|
17
17
|
*
|
|
18
|
-
* Then `
|
|
18
|
+
* Then `useClaim('editLayer')` returns a function whose sole argument
|
|
19
19
|
* is the `editLayer` claim shape — no runtime checks, purely compile-
|
|
20
20
|
* time narrowing.
|
|
21
21
|
*
|
|
22
|
-
* The SDK doesn't own what happens next: the `
|
|
23
|
-
* the React context (supplied via `SyncProvider`) is where the
|
|
22
|
+
* The SDK doesn't own what happens next: the `beginClaim` function on
|
|
23
|
+
* the React context (supplied via `SyncProvider`) is where the claim
|
|
24
24
|
* claim turns into a network effect. A Node-backed consumer wires it
|
|
25
|
-
* through `SyncAgent.
|
|
25
|
+
* through `SyncAgent.beginClaim`; a browser-backed consumer may
|
|
26
26
|
* broadcast it through their own WebSocket. This hook is pure sugar
|
|
27
27
|
* that adds the typed name + claim narrowing.
|
|
28
28
|
*/
|
|
29
|
-
export declare function
|
|
29
|
+
export declare function useClaim<Name extends keyof ResolveClaims & string>(claimName: Name): (claim: ResolveClaims[Name]) => unknown;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useCallback } from 'react';
|
|
3
|
+
import { useSyncContext } from './context.js';
|
|
4
|
+
import { AbloValidationError } from '../errors.js';
|
|
5
|
+
/**
|
|
6
|
+
* Named-claim invoker, typed via `ResolveClaims[ClaimName]`.
|
|
7
|
+
*
|
|
8
|
+
* The consumer declares their claim vocabulary in the global:
|
|
9
|
+
*
|
|
10
|
+
* ```ts
|
|
11
|
+
* declare module '@abloatai/ablo' {
|
|
12
|
+
* interface Register {
|
|
13
|
+
* Claims: {
|
|
14
|
+
* editLayer: { slideId: string; layerId: string };
|
|
15
|
+
* generateWithAI: { entityId: string; tool: string };
|
|
16
|
+
* };
|
|
17
|
+
* }
|
|
18
|
+
* }
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* Then `useClaim('editLayer')` returns a function whose sole argument
|
|
22
|
+
* is the `editLayer` claim shape — no runtime checks, purely compile-
|
|
23
|
+
* time narrowing.
|
|
24
|
+
*
|
|
25
|
+
* The SDK doesn't own what happens next: the `beginClaim` function on
|
|
26
|
+
* the React context (supplied via `SyncProvider`) is where the claim
|
|
27
|
+
* claim turns into a network effect. A Node-backed consumer wires it
|
|
28
|
+
* through `SyncAgent.beginClaim`; a browser-backed consumer may
|
|
29
|
+
* broadcast it through their own WebSocket. This hook is pure sugar
|
|
30
|
+
* that adds the typed name + claim narrowing.
|
|
31
|
+
*/
|
|
32
|
+
export function useClaim(claimName) {
|
|
33
|
+
const { beginClaim } = useSyncContext();
|
|
34
|
+
return useCallback((claim) => {
|
|
35
|
+
if (!beginClaim) {
|
|
36
|
+
throw new AbloValidationError(`useClaim: no \`beginClaim\` wired into SyncProvider. Pass ` +
|
|
37
|
+
`a \`beginClaim\` prop (typically bound to your transport) ` +
|
|
38
|
+
`to enable claim invocations.`, { code: 'claim_not_wired' });
|
|
39
|
+
}
|
|
40
|
+
return beginClaim(claimName, claim);
|
|
41
|
+
}, [beginClaim, claimName]);
|
|
42
|
+
}
|
package/dist/schema/index.js
CHANGED
|
@@ -41,7 +41,7 @@ export { syncDeltaCoreSchema, deltaAttributionSchema, deltaProvenanceSchema, syn
|
|
|
41
41
|
export { syncDeltaActionSchema, wireDeltaDataSchema, participantRefSchema, syncDeltaWireCoreSchema, clientSyncDeltaSchema, serverSyncDeltaSchema, } from './sync-delta-wire.js';
|
|
42
42
|
// Model builder
|
|
43
43
|
export { model, scopeKindOf, } from './model.js';
|
|
44
|
-
//
|
|
44
|
+
// Claim-first shorthand: `mutable.lazy({...})` and friends. Read the
|
|
45
45
|
// safety posture and load shape off the verb tokens; everything else
|
|
46
46
|
// falls back to sensible defaults. See sugar.ts for the full pattern.
|
|
47
47
|
export { mutable, readOnly } from './sugar.js';
|
package/dist/schema/schema.d.ts
CHANGED
|
@@ -116,13 +116,13 @@ export interface Schema<S extends SchemaRecord = SchemaRecord> {
|
|
|
116
116
|
* ```
|
|
117
117
|
*/
|
|
118
118
|
/** The schema bound via `declare module … interface Register { Schema: … }`
|
|
119
|
-
* (the `ablo.
|
|
119
|
+
* (the `ablo/register.ts` the scaffold writes). `never` when not registered. */
|
|
120
120
|
type RegisteredSchema = import('../types/global.js').Register extends {
|
|
121
121
|
Schema: infer S extends Schema;
|
|
122
122
|
} ? S : never;
|
|
123
123
|
/**
|
|
124
|
-
* THE model type helper. With the scaffold's `ablo.
|
|
125
|
-
* place, one parameter is all it takes:
|
|
124
|
+
* THE model type helper. With the scaffold's `ablo/register.ts` registration
|
|
125
|
+
* in place, one parameter is all it takes:
|
|
126
126
|
*
|
|
127
127
|
* ```ts
|
|
128
128
|
* type Task = Model<'tasks'>;
|
package/dist/schema/sugar.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Claim-first shorthand for `model(...)` — the Modal-inspired DX layer.
|
|
3
3
|
*
|
|
4
4
|
* The factory verbs encode the two orthogonal axes that matter for
|
|
5
5
|
* safety and bootstrap behavior:
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* `.lazy` loads on first access, `.manual` requires explicit
|
|
13
13
|
* queries.
|
|
14
14
|
*
|
|
15
|
-
* The two-token form (`mutable.lazy({...})`) reads the safety
|
|
15
|
+
* The two-token form (`mutable.lazy({...})`) reads the safety claim
|
|
16
16
|
* in the first token and the load shape in the second — you know both
|
|
17
17
|
* key facts about the entity before scanning its fields.
|
|
18
18
|
*
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
* load: 'lazy', lazyObservable: true, computed: tasksComputed,
|
|
29
29
|
* }),
|
|
30
30
|
*
|
|
31
|
-
* // After —
|
|
31
|
+
* // After — claim reads off the verb; options carry only the
|
|
32
32
|
* // fields that actually diverge from defaults
|
|
33
33
|
* tasks: mutable.lazy({ title: z.string() }, {
|
|
34
34
|
* typename: 'Task', tableName: 'tasks',
|
package/dist/schema/sugar.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Claim-first shorthand for `model(...)` — the Modal-inspired DX layer.
|
|
3
3
|
*
|
|
4
4
|
* The factory verbs encode the two orthogonal axes that matter for
|
|
5
5
|
* safety and bootstrap behavior:
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* `.lazy` loads on first access, `.manual` requires explicit
|
|
13
13
|
* queries.
|
|
14
14
|
*
|
|
15
|
-
* The two-token form (`mutable.lazy({...})`) reads the safety
|
|
15
|
+
* The two-token form (`mutable.lazy({...})`) reads the safety claim
|
|
16
16
|
* in the first token and the load shape in the second — you know both
|
|
17
17
|
* key facts about the entity before scanning its fields.
|
|
18
18
|
*
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
* load: 'lazy', lazyObservable: true, computed: tasksComputed,
|
|
29
29
|
* }),
|
|
30
30
|
*
|
|
31
|
-
* // After —
|
|
31
|
+
* // After — claim reads off the verb; options carry only the
|
|
32
32
|
* // fields that actually diverge from defaults
|
|
33
33
|
* tasks: mutable.lazy({ title: z.string() }, {
|
|
34
34
|
* typename: 'Task', tableName: 'tasks',
|
|
@@ -32,14 +32,14 @@ import { z } from 'zod';
|
|
|
32
32
|
* `S` groupRemoved.
|
|
33
33
|
*/
|
|
34
34
|
export declare const syncDeltaActionSchema: z.ZodEnum<{
|
|
35
|
-
A: "A";
|
|
36
35
|
I: "I";
|
|
37
36
|
U: "U";
|
|
38
37
|
D: "D";
|
|
38
|
+
A: "A";
|
|
39
|
+
V: "V";
|
|
39
40
|
C: "C";
|
|
40
41
|
G: "G";
|
|
41
42
|
S: "S";
|
|
42
|
-
V: "V";
|
|
43
43
|
}>;
|
|
44
44
|
export type SyncDeltaAction = z.infer<typeof syncDeltaActionSchema>;
|
|
45
45
|
/**
|
|
@@ -73,14 +73,14 @@ export type ParticipantRef = z.infer<typeof participantRefSchema>;
|
|
|
73
73
|
export declare const syncDeltaWireCoreSchema: z.ZodObject<{
|
|
74
74
|
id: z.ZodNumber;
|
|
75
75
|
actionType: z.ZodEnum<{
|
|
76
|
-
A: "A";
|
|
77
76
|
I: "I";
|
|
78
77
|
U: "U";
|
|
79
78
|
D: "D";
|
|
79
|
+
A: "A";
|
|
80
|
+
V: "V";
|
|
80
81
|
C: "C";
|
|
81
82
|
G: "G";
|
|
82
83
|
S: "S";
|
|
83
|
-
V: "V";
|
|
84
84
|
}>;
|
|
85
85
|
modelName: z.ZodString;
|
|
86
86
|
modelId: z.ZodString;
|
|
@@ -98,14 +98,14 @@ export type SyncDeltaWireCore = z.infer<typeof syncDeltaWireCoreSchema>;
|
|
|
98
98
|
export declare const clientSyncDeltaSchema: z.ZodObject<{
|
|
99
99
|
id: z.ZodNumber;
|
|
100
100
|
actionType: z.ZodEnum<{
|
|
101
|
-
A: "A";
|
|
102
101
|
I: "I";
|
|
103
102
|
U: "U";
|
|
104
103
|
D: "D";
|
|
104
|
+
A: "A";
|
|
105
|
+
V: "V";
|
|
105
106
|
C: "C";
|
|
106
107
|
G: "G";
|
|
107
108
|
S: "S";
|
|
108
|
-
V: "V";
|
|
109
109
|
}>;
|
|
110
110
|
modelName: z.ZodString;
|
|
111
111
|
modelId: z.ZodString;
|
|
@@ -127,14 +127,14 @@ export type ClientSyncDelta = z.infer<typeof clientSyncDeltaSchema>;
|
|
|
127
127
|
export declare const serverSyncDeltaSchema: z.ZodObject<{
|
|
128
128
|
id: z.ZodNumber;
|
|
129
129
|
actionType: z.ZodEnum<{
|
|
130
|
-
A: "A";
|
|
131
130
|
I: "I";
|
|
132
131
|
U: "U";
|
|
133
132
|
D: "D";
|
|
133
|
+
A: "A";
|
|
134
|
+
V: "V";
|
|
134
135
|
C: "C";
|
|
135
136
|
G: "G";
|
|
136
137
|
S: "S";
|
|
137
|
-
V: "V";
|
|
138
138
|
}>;
|
|
139
139
|
modelName: z.ZodString;
|
|
140
140
|
modelId: z.ZodString;
|
package/dist/server/commit.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `@abloatai/ablo/server` — the COMMIT contract types.
|
|
3
3
|
*
|
|
4
|
-
* `CommitContext` is the attribution/
|
|
4
|
+
* `CommitContext` is the attribution/claim envelope the host's commit executor
|
|
5
5
|
* stamps onto every delta a batch produces; `CommitResult` is the receipt. Both
|
|
6
6
|
* are PURE descriptors (no `postgres`, no SQL, no functions), which is why they
|
|
7
7
|
* live in the portable package while the SQL engine that consumes them
|
|
@@ -76,7 +76,7 @@ export interface CommitContext {
|
|
|
76
76
|
confirmationState?: ConfirmationState;
|
|
77
77
|
/**
|
|
78
78
|
* Dormant FK to the agent-task id (`agent_tasks.id`). The SDK no longer
|
|
79
|
-
* sets it (turns/tasks removed; attribution rides on the claim/
|
|
79
|
+
* sets it (turns/tasks removed; attribution rides on the claim/claim id
|
|
80
80
|
* + server-stamped actor/capability). Still validated + written onto
|
|
81
81
|
* `caused_by_task_id` when present, but client writes leave it `null`.
|
|
82
82
|
*/
|
|
@@ -0,0 +1,162 @@
|
|
|
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
|
+
/** The single capability this manager needs from the connection. */
|
|
38
|
+
export interface SubscriptionTransport {
|
|
39
|
+
/**
|
|
40
|
+
* Replace the connection's read interest with the COMPLETE group set.
|
|
41
|
+
* Resolves with the server's effective set (which the manager treats as
|
|
42
|
+
* authoritative for its next diff).
|
|
43
|
+
*/
|
|
44
|
+
updateSubscription(syncGroups: ReadonlyArray<string>): Promise<{
|
|
45
|
+
syncGroups: string[];
|
|
46
|
+
}>;
|
|
47
|
+
}
|
|
48
|
+
export interface AreaOfInterestOptions {
|
|
49
|
+
/** Connection to drive. `SyncWebSocket` satisfies this structurally. */
|
|
50
|
+
transport: SubscriptionTransport;
|
|
51
|
+
/**
|
|
52
|
+
* Groups always present in the effective set (e.g. `org:<id>`,
|
|
53
|
+
* `user:<id>`). Never warm, never expired.
|
|
54
|
+
*/
|
|
55
|
+
baseGroups?: ReadonlyArray<string>;
|
|
56
|
+
/**
|
|
57
|
+
* How long a `leave`-ed group stays subscribed before it actually drops.
|
|
58
|
+
* This is the hysteresis margin. Default 30s.
|
|
59
|
+
*/
|
|
60
|
+
warmTtlMs?: number;
|
|
61
|
+
/**
|
|
62
|
+
* Maximum number of warm (left-but-still-subscribed) groups. Under heavy
|
|
63
|
+
* navigation — opening and closing many entities quickly — warm groups
|
|
64
|
+
* would otherwise pile up until each TTL lapses, inflating the connection's
|
|
65
|
+
* subscription set. When the cap is exceeded, the LEAST-recently-warmed
|
|
66
|
+
* group is evicted immediately (dropped) instead of waiting for its TTL.
|
|
67
|
+
* This is the bounded relevant-set discipline from game netcode. Default 16.
|
|
68
|
+
*/
|
|
69
|
+
maxWarm?: number;
|
|
70
|
+
/**
|
|
71
|
+
* Auto-run the warm-expiry sweep on this cadence. Set `0` to disable and
|
|
72
|
+
* drive {@link AreaOfInterestManager.sweep} yourself (tests do this).
|
|
73
|
+
* Default = `warmTtlMs` (checks about once per margin).
|
|
74
|
+
*/
|
|
75
|
+
sweepIntervalMs?: number;
|
|
76
|
+
/** Clock injection point for deterministic tests. Default `Date.now`. */
|
|
77
|
+
now?: () => number;
|
|
78
|
+
/**
|
|
79
|
+
* Schedule a periodic callback. Default wraps `setInterval`/
|
|
80
|
+
* `clearInterval`. Injected so tests avoid real timers.
|
|
81
|
+
*/
|
|
82
|
+
scheduler?: (fn: () => void, intervalMs: number) => () => void;
|
|
83
|
+
}
|
|
84
|
+
export declare class AreaOfInterestManager {
|
|
85
|
+
private readonly transport;
|
|
86
|
+
private readonly baseGroups;
|
|
87
|
+
private readonly warmTtlMs;
|
|
88
|
+
private readonly maxWarm;
|
|
89
|
+
private readonly now;
|
|
90
|
+
/** Groups currently in view (open entities). */
|
|
91
|
+
private readonly active;
|
|
92
|
+
/** Claim-pinned groups — prominence; never warm/expire while pinned. */
|
|
93
|
+
private readonly pinned;
|
|
94
|
+
/** Left-but-warm groups → epoch-ms at which they drop. */
|
|
95
|
+
private readonly warm;
|
|
96
|
+
/** Last set the transport confirmed — the diff baseline. */
|
|
97
|
+
private lastSent;
|
|
98
|
+
/** Coalescing state so concurrent mutations collapse into one in-flight call. */
|
|
99
|
+
private inFlight;
|
|
100
|
+
private dirty;
|
|
101
|
+
private readonly cancelSweep;
|
|
102
|
+
constructor(options: AreaOfInterestOptions);
|
|
103
|
+
/**
|
|
104
|
+
* Move a group into the warm set with a fresh TTL, maintaining LRU order
|
|
105
|
+
* and the `maxWarm` cap. JS `Map` preserves insertion order, so deleting
|
|
106
|
+
* then re-setting moves the group to the most-recently-warmed position;
|
|
107
|
+
* eviction then drops from the front (oldest). Base/pinned groups never
|
|
108
|
+
* warm — callers guard before calling this.
|
|
109
|
+
*/
|
|
110
|
+
private warmGroup;
|
|
111
|
+
/** The effective read set: base ∪ active ∪ pinned ∪ (warm not yet expired). */
|
|
112
|
+
private desiredGroups;
|
|
113
|
+
/** Bring a group into view. Cancels any warm timer for it. Idempotent. */
|
|
114
|
+
enter(group: string): Promise<void>;
|
|
115
|
+
/**
|
|
116
|
+
* Leave a group. It does not drop immediately — it goes warm for
|
|
117
|
+
* `warmTtlMs` (unless pinned, in which case it stays via the pin).
|
|
118
|
+
* Re-entering within the window is free.
|
|
119
|
+
*/
|
|
120
|
+
leave(group: string): Promise<void>;
|
|
121
|
+
/** Pin a group (active claim / prominence). Never warm or expires while pinned. */
|
|
122
|
+
pin(group: string): Promise<void>;
|
|
123
|
+
/**
|
|
124
|
+
* Unpin a group. If it's not currently in view, it transitions to warm
|
|
125
|
+
* (so dropping a claim gets the same hysteresis as closing a tab) rather
|
|
126
|
+
* than dropping instantly.
|
|
127
|
+
*/
|
|
128
|
+
unpin(group: string): Promise<void>;
|
|
129
|
+
/**
|
|
130
|
+
* Drop warm groups whose TTL has lapsed and reconcile. Auto-invoked on
|
|
131
|
+
* the sweep timer; call manually (with an injected `now`) in tests.
|
|
132
|
+
*/
|
|
133
|
+
sweep(): Promise<void>;
|
|
134
|
+
/** The set the manager believes is subscribed (post-confirmation). */
|
|
135
|
+
effectiveGroups(): string[];
|
|
136
|
+
/**
|
|
137
|
+
* Re-assert the full desired set against the transport, forgetting what
|
|
138
|
+
* was previously confirmed. Call after a reconnect: a fresh
|
|
139
|
+
* `SyncWebSocket` instance starts from the connect-time URL groups, so
|
|
140
|
+
* the manager's `lastSent` diff baseline is stale. Clearing it forces
|
|
141
|
+
* one `update_subscription` that re-establishes the live interest on the
|
|
142
|
+
* new socket.
|
|
143
|
+
*
|
|
144
|
+
* Resetting `lastSent` makes the next reconcile unconditionally re-push
|
|
145
|
+
* the current desired set (one `update_subscription` frame) so the fresh
|
|
146
|
+
* socket's server-side index matches local interest, even if warm/pinned
|
|
147
|
+
* groups drifted across the disconnect window. The connect-time URL
|
|
148
|
+
* already carries the last-acked set, so this is a correction frame, not
|
|
149
|
+
* the primary mechanism.
|
|
150
|
+
*/
|
|
151
|
+
resync(): Promise<void>;
|
|
152
|
+
/** Stop the sweep timer. The connection is unaffected. */
|
|
153
|
+
dispose(): void;
|
|
154
|
+
/**
|
|
155
|
+
* Push the desired set to the transport iff it differs from the last
|
|
156
|
+
* confirmed set. Coalesces concurrent mutations: if a call is already in
|
|
157
|
+
* flight, mark dirty and let the in-flight loop pick up the newest state
|
|
158
|
+
* — so a burst of enter/leave collapses into the minimum number of
|
|
159
|
+
* `update_subscription` round-trips.
|
|
160
|
+
*/
|
|
161
|
+
private reconcile;
|
|
162
|
+
}
|