@abloatai/ablo 0.24.0 → 0.26.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/AGENTS.md +5 -3
- package/CHANGELOG.md +33 -0
- package/README.md +45 -36
- package/dist/BaseSyncedStore.d.ts +72 -195
- package/dist/BaseSyncedStore.js +240 -625
- package/dist/Database.d.ts +8 -9
- package/dist/Database.js +9 -14
- package/dist/LazyReferenceCollection.js +5 -0
- package/dist/Model.js +6 -8
- package/dist/ModelRegistry.d.ts +25 -6
- package/dist/ModelRegistry.js +12 -6
- package/dist/NetworkMonitor.js +1 -1
- package/dist/ObjectPool.js +9 -3
- package/dist/SyncClient.d.ts +15 -5
- package/dist/SyncClient.js +152 -66
- package/dist/SyncEngineContext.d.ts +1 -3
- package/dist/SyncEngineContext.js +1 -2
- package/dist/agent/Agent.d.ts +21 -8
- package/dist/agent/Agent.js +26 -13
- package/dist/ai-sdk/coordination-context.js +9 -3
- package/dist/ai-sdk/wrap.d.ts +1 -1
- package/dist/ai-sdk/wrap.js +1 -1
- package/dist/auth/credentialPolicy.d.ts +10 -2
- package/dist/auth/credentialSource.d.ts +1 -13
- package/dist/auth/credentialSource.js +6 -13
- package/dist/auth/index.js +3 -3
- package/dist/cli.cjs +583 -287
- package/dist/client/Ablo.d.ts +35 -764
- package/dist/client/Ablo.js +64 -688
- package/dist/client/ApiClient.d.ts +14 -1
- package/dist/client/ApiClient.js +63 -9
- package/dist/client/auth.d.ts +8 -17
- package/dist/client/auth.js +42 -6
- package/dist/client/consoleLogger.d.ts +36 -0
- package/dist/client/consoleLogger.js +45 -0
- package/dist/client/createInternalComponents.js +1 -1
- package/dist/client/createModelProxy.js +10 -6
- package/dist/client/credentialEndpoint.d.ts +63 -0
- package/dist/client/credentialEndpoint.js +87 -0
- package/dist/client/hostedEndpoints.d.ts +24 -0
- package/dist/client/hostedEndpoints.js +24 -0
- package/dist/client/httpClient.d.ts +1 -1
- package/dist/client/identity.js +2 -1
- package/dist/client/modelRegistration.d.ts +14 -0
- package/dist/client/modelRegistration.js +321 -0
- package/dist/client/options.d.ts +396 -0
- package/dist/client/options.js +10 -0
- package/dist/client/resourceTypes.d.ts +344 -0
- package/dist/client/resourceTypes.js +13 -0
- package/dist/client/schemaConfig.d.ts +56 -0
- package/dist/client/schemaConfig.js +188 -0
- package/dist/client/sessionMint.d.ts +1 -1
- package/dist/client/validateAbloOptions.js +1 -1
- package/dist/client/wsMutationExecutor.d.ts +33 -0
- package/dist/client/wsMutationExecutor.js +77 -0
- package/dist/context.js +0 -3
- package/dist/coordination/index.d.ts +8 -1
- package/dist/coordination/index.js +24 -1
- package/dist/coordination/trace.d.ts +0 -1
- package/dist/coordination/trace.js +0 -1
- package/dist/core/DatabaseManager.d.ts +0 -1
- package/dist/core/DatabaseManager.js +26 -24
- package/dist/core/QueryProcessor.js +7 -8
- package/dist/core/QueryView.d.ts +14 -2
- package/dist/core/QueryView.js +4 -5
- package/dist/core/StoreManager.d.ts +2 -2
- package/dist/core/StoreManager.js +5 -1
- package/dist/core/index.d.ts +1 -1
- package/dist/core/openIDBWithTimeout.js +15 -11
- package/dist/core/query-utils.js +1 -1
- package/dist/core/storeContract.d.ts +143 -0
- package/dist/core/storeContract.js +16 -0
- package/dist/errorCodes.d.ts +11 -2
- package/dist/errorCodes.js +155 -137
- package/dist/errors.d.ts +18 -7
- package/dist/errors.js +14 -1
- package/dist/index.js +5 -0
- package/dist/interfaces/index.d.ts +6 -20
- package/dist/keys/index.js +18 -9
- package/dist/mutators/UndoManager.js +6 -1
- package/dist/mutators/defineMutators.d.ts +1 -3
- package/dist/policy/types.d.ts +1 -1
- package/dist/query/client.d.ts +11 -0
- package/dist/query/client.js +81 -52
- package/dist/react/AbloProvider.d.ts +6 -10
- package/dist/react/AbloProvider.js +22 -15
- package/dist/react/context.d.ts +2 -118
- package/dist/react/useAblo.js +1 -1
- package/dist/react/useErrorListener.js +1 -1
- package/dist/react/useMutationFailureListener.js +1 -1
- package/dist/react/useMutators.js +7 -1
- package/dist/react/useReactive.js +0 -1
- package/dist/react/useUndoScope.js +2 -2
- package/dist/schema/ddlLock.d.ts +39 -0
- package/dist/schema/ddlLock.js +51 -0
- package/dist/schema/field.js +1 -1
- package/dist/schema/index.d.ts +4 -3
- package/dist/schema/index.js +15 -5
- package/dist/schema/model.d.ts +3 -3
- package/dist/schema/model.js +2 -2
- package/dist/schema/openapi.js +5 -1
- package/dist/schema/queries.d.ts +1 -1
- package/dist/schema/queries.js +1 -0
- package/dist/schema/residency.d.ts +35 -0
- package/dist/schema/residency.js +27 -0
- package/dist/schema/roles.d.ts +25 -0
- package/dist/schema/roles.js +37 -0
- package/dist/schema/schema.d.ts +3 -3
- package/dist/schema/schema.js +11 -7
- package/dist/schema/serialize.d.ts +8 -8
- package/dist/schema/sync-delta-row.d.ts +2 -2
- package/dist/schema/sync-delta-row.js +2 -2
- package/dist/server/storage-mode.d.ts +8 -1
- package/dist/server/storage-mode.js +14 -4
- package/dist/source/adapter.d.ts +1 -1
- package/dist/source/adapters/drizzle.js +4 -3
- package/dist/source/adapters/kysely.js +4 -3
- package/dist/source/adapters/memory.js +1 -1
- package/dist/source/adapters/prisma.js +5 -4
- package/dist/source/conformance.js +10 -8
- package/dist/source/connector.js +4 -2
- package/dist/source/factory.d.ts +106 -0
- package/dist/source/factory.js +273 -0
- package/dist/source/index.d.ts +11 -461
- package/dist/source/index.js +14 -416
- package/dist/source/next.d.ts +1 -1
- package/dist/source/next.js +1 -1
- package/dist/source/pushQueue.d.ts +1 -1
- package/dist/source/pushQueue.js +6 -3
- package/dist/source/signing.d.ts +59 -0
- package/dist/source/signing.js +142 -0
- package/dist/source/types.d.ts +334 -0
- package/dist/source/types.js +44 -0
- package/dist/stores/ObjectStore.d.ts +1 -1
- package/dist/stores/ObjectStore.js +23 -23
- package/dist/stores/SyncActionStore.d.ts +1 -1
- package/dist/stores/SyncActionStore.js +70 -35
- package/dist/surface.d.ts +1 -1
- package/dist/surface.js +1 -0
- package/dist/sync/AreaOfInterestManager.d.ts +2 -2
- package/dist/sync/AreaOfInterestManager.js +1 -1
- package/dist/sync/BootstrapHelper.d.ts +1 -3
- package/dist/sync/BootstrapHelper.js +29 -8
- package/dist/sync/ConnectionManager.d.ts +8 -0
- package/dist/sync/ConnectionManager.js +19 -4
- package/dist/sync/HydrationCoordinator.d.ts +12 -0
- package/dist/sync/HydrationCoordinator.js +25 -5
- package/dist/sync/NetworkProbe.d.ts +3 -2
- package/dist/sync/NetworkProbe.js +7 -6
- package/dist/sync/SyncWebSocket.d.ts +71 -120
- package/dist/sync/SyncWebSocket.js +324 -630
- package/dist/sync/awaitClaimGrant.js +27 -19
- package/dist/sync/bootstrapApply.d.ts +60 -0
- package/dist/sync/bootstrapApply.js +65 -0
- package/dist/sync/commitFrames.d.ts +43 -0
- package/dist/sync/commitFrames.js +94 -0
- package/dist/sync/createClaimStream.js +3 -2
- package/dist/sync/createSnapshot.d.ts +1 -3
- package/dist/sync/credentialLifecycle.d.ts +175 -0
- package/dist/sync/credentialLifecycle.js +329 -0
- package/dist/sync/deltaPipeline.d.ts +111 -0
- package/dist/sync/deltaPipeline.js +262 -0
- package/dist/sync/groupChange.d.ts +111 -0
- package/dist/sync/groupChange.js +246 -0
- package/dist/sync/heartbeat.d.ts +62 -0
- package/dist/sync/heartbeat.js +91 -0
- package/dist/sync/participants.d.ts +8 -8
- package/dist/sync/syncCursor.d.ts +44 -0
- package/dist/sync/syncCursor.js +59 -0
- package/dist/sync/syncPlan.d.ts +62 -0
- package/dist/sync/syncPlan.js +51 -0
- package/dist/sync/wsFrameHandlers.d.ts +117 -0
- package/dist/sync/wsFrameHandlers.js +378 -0
- package/dist/testing/fixtures/bootstrap.d.ts +9 -11
- package/dist/testing/fixtures/deltas.d.ts +1 -1
- package/dist/testing/fixtures/models.js +1 -0
- package/dist/testing/helpers/sync-engine-harness.d.ts +0 -3
- package/dist/testing/helpers/sync-engine-harness.js +2 -1
- package/dist/testing/index.d.ts +1 -1
- package/dist/testing/index.js +1 -1
- package/dist/testing/mocks/MockSyncContext.d.ts +1 -17
- package/dist/testing/mocks/MockSyncContext.js +1 -32
- package/dist/testing/mocks/MockSyncStore.d.ts +4 -4
- package/dist/testing/mocks/MockWebSocket.d.ts +2 -2
- package/dist/transactions/TransactionQueue.d.ts +20 -56
- package/dist/transactions/TransactionQueue.js +101 -487
- package/dist/transactions/TransactionStore.d.ts +18 -0
- package/dist/transactions/TransactionStore.js +51 -0
- package/dist/transactions/coalesceRules.d.ts +34 -0
- package/dist/transactions/coalesceRules.js +117 -0
- package/dist/transactions/commitPayload.d.ts +134 -0
- package/dist/transactions/commitPayload.js +152 -0
- package/dist/transactions/deltaConfirmation.d.ts +60 -0
- package/dist/transactions/deltaConfirmation.js +223 -0
- package/dist/transactions/optimistic.d.ts +24 -0
- package/dist/transactions/optimistic.js +45 -0
- package/dist/transactions/persistedReplay.d.ts +93 -0
- package/dist/transactions/persistedReplay.js +105 -0
- package/dist/types/index.d.ts +3 -3
- package/dist/types/index.js +0 -5
- package/dist/types/modelData.d.ts +12 -0
- package/dist/types/modelData.js +11 -0
- package/dist/types/participant.d.ts +21 -0
- package/dist/types/participant.js +12 -0
- package/dist/types/streams.d.ts +8 -14
- package/dist/utils/mobx-setup.d.ts +1 -1
- package/dist/utils/mobx-setup.js +2 -2
- package/dist/wire/errorEnvelope.d.ts +11 -2
- package/dist/wire/errorEnvelope.js +14 -2
- package/dist/wire/frames.d.ts +90 -0
- package/dist/wire/frames.js +55 -1
- package/dist/wire/index.d.ts +4 -1
- package/dist/wire/index.js +20 -1
- package/dist/wire/protocol.d.ts +45 -0
- package/dist/wire/protocol.js +45 -0
- package/dist/wire/protocolVersion.d.ts +56 -0
- package/dist/wire/protocolVersion.js +63 -0
- package/docs/api-keys.md +4 -3
- package/docs/data-sources.md +12 -5
- package/docs/examples/existing-python-backend.md +3 -3
- package/docs/identity.md +4 -4
- package/docs/integration-guide.md +1 -1
- package/docs/react.md +1 -1
- package/docs/sessions.md +5 -7
- package/llms.txt +4 -2
- package/package.json +15 -13
- package/dist/client/index.d.ts +0 -36
- package/dist/client/index.js +0 -33
- package/dist/config/index.d.ts +0 -10
- package/dist/config/index.js +0 -12
- package/dist/interfaces/headless.d.ts +0 -95
- package/dist/interfaces/headless.js +0 -41
- package/dist/query/index.d.ts +0 -6
- package/dist/query/index.js +0 -5
- package/dist/realtime/index.d.ts +0 -10
- package/dist/realtime/index.js +0 -9
- package/dist/schema/plane.d.ts +0 -23
- package/dist/schema/plane.js +0 -19
- package/dist/server/next.d.ts +0 -51
- package/dist/server/next.js +0 -47
- package/dist/sync/OfflineFlush.d.ts +0 -9
- package/dist/sync/OfflineFlush.js +0 -22
- package/dist/sync/OfflineTransactionStore.d.ts +0 -37
- package/dist/sync/OfflineTransactionStore.js +0 -263
- package/dist/transactions/index.d.ts +0 -16
- package/dist/transactions/index.js +0 -7
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Application-level heartbeat for the sync WebSocket.
|
|
3
|
+
*
|
|
4
|
+
* The browser WebSocket API hides RFC 6455 protocol-level ping/pong from
|
|
5
|
+
* JavaScript, so the server's `ws.ping()` keepalive can't be observed by
|
|
6
|
+
* client code — meaning the client cannot tell a healthy idle connection
|
|
7
|
+
* apart from a "zombie" socket where TCP silently broke (laptop sleep,
|
|
8
|
+
* NAT timeout, mobile handoff). We send an application-level
|
|
9
|
+
* `{ type: 'ping' }` every 30s and force-close the socket if no inbound
|
|
10
|
+
* traffic arrives within 10s. ANY inbound message counts as
|
|
11
|
+
* proof-of-life — the explicit `pong` is just a guarantee that something
|
|
12
|
+
* will arrive even on an idle stream.
|
|
13
|
+
*/
|
|
14
|
+
import { getContext } from '../context.js';
|
|
15
|
+
import { PING_INTERVAL_MS } from '../wire/protocol.js';
|
|
16
|
+
/**
|
|
17
|
+
* One cadence for both sides: the SDK's application-level ping runs at the
|
|
18
|
+
* same `PING_INTERVAL_MS` as the server's RFC 6455 keepalive (and the claim
|
|
19
|
+
* lease window is derived from it — see `wire/protocol.ts`).
|
|
20
|
+
*/
|
|
21
|
+
export const HEARTBEAT_INTERVAL_MS = PING_INTERVAL_MS;
|
|
22
|
+
export const HEARTBEAT_TIMEOUT_MS = 10_000;
|
|
23
|
+
/**
|
|
24
|
+
* Every `HEARTBEAT_INTERVAL_MS` while `OPEN`, send `{ type: 'ping' }`
|
|
25
|
+
* and arm a `HEARTBEAT_TIMEOUT_MS` watchdog. Any inbound frame (the
|
|
26
|
+
* owner's `onmessage` calls {@link clearHeartbeatTimeout}) clears the
|
|
27
|
+
* watchdog. If the watchdog fires, we treat the connection as zombie
|
|
28
|
+
* and force-close it — `onclose` then triggers the existing reconnect
|
|
29
|
+
* path.
|
|
30
|
+
*
|
|
31
|
+
* Why both sides need this:
|
|
32
|
+
* - The server sends RFC 6455 protocol pings via `ws.ping()` every
|
|
33
|
+
* 30s. Browsers auto-respond with a pong but DO NOT expose either
|
|
34
|
+
* frame to JavaScript, so the client is blind to its own keepalive.
|
|
35
|
+
* - On a half-open TCP (laptop wake, NAT timeout, mobile handoff)
|
|
36
|
+
* the browser may keep `readyState === OPEN` for minutes before
|
|
37
|
+
* the OS surfaces the broken connection. App-level traffic is
|
|
38
|
+
* the only signal we can observe.
|
|
39
|
+
*/
|
|
40
|
+
export class HeartbeatController {
|
|
41
|
+
transport;
|
|
42
|
+
heartbeatTimer = null;
|
|
43
|
+
heartbeatTimeoutTimer = null;
|
|
44
|
+
constructor(transport) {
|
|
45
|
+
this.transport = transport;
|
|
46
|
+
}
|
|
47
|
+
start() {
|
|
48
|
+
this.stop();
|
|
49
|
+
this.heartbeatTimer = setInterval(() => {
|
|
50
|
+
if (!this.transport.isSocketOpen())
|
|
51
|
+
return;
|
|
52
|
+
// Send the ping. If `send` throws, the socket is already dead —
|
|
53
|
+
// force-close so onclose triggers the reconnect cycle.
|
|
54
|
+
try {
|
|
55
|
+
this.transport.sendPing();
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
getContext().observability.captureWebSocketError({
|
|
59
|
+
context: 'heartbeat-send-failed',
|
|
60
|
+
error: err instanceof Error ? err.message : String(err),
|
|
61
|
+
});
|
|
62
|
+
this.transport.forceClose('heartbeat-send-failed');
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
// Arm the timeout. ANY inbound message clears it (see the owner's
|
|
66
|
+
// onmessage). We don't require an explicit `pong` — a delta or any
|
|
67
|
+
// other frame is equally good proof-of-life.
|
|
68
|
+
if (this.heartbeatTimeoutTimer)
|
|
69
|
+
clearTimeout(this.heartbeatTimeoutTimer);
|
|
70
|
+
this.heartbeatTimeoutTimer = setTimeout(() => {
|
|
71
|
+
getContext().observability.captureWebSocketError({
|
|
72
|
+
context: 'heartbeat-timeout',
|
|
73
|
+
});
|
|
74
|
+
this.transport.forceClose('heartbeat-timeout');
|
|
75
|
+
}, HEARTBEAT_TIMEOUT_MS);
|
|
76
|
+
}, HEARTBEAT_INTERVAL_MS);
|
|
77
|
+
}
|
|
78
|
+
stop() {
|
|
79
|
+
if (this.heartbeatTimer) {
|
|
80
|
+
clearInterval(this.heartbeatTimer);
|
|
81
|
+
this.heartbeatTimer = null;
|
|
82
|
+
}
|
|
83
|
+
this.clearHeartbeatTimeout();
|
|
84
|
+
}
|
|
85
|
+
clearHeartbeatTimeout() {
|
|
86
|
+
if (this.heartbeatTimeoutTimer) {
|
|
87
|
+
clearTimeout(this.heartbeatTimeoutTimer);
|
|
88
|
+
this.heartbeatTimeoutTimer = null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { SyncWebSocket } from './SyncWebSocket.js';
|
|
2
|
-
import type { Schema
|
|
2
|
+
import type { Schema } from '../schema/schema.js';
|
|
3
3
|
import type { Claim, Activity, ClaimTarget, ClaimStream, Peer, PresenceStream, PresenceTarget } from '../types/streams.js';
|
|
4
4
|
import type { AttachableClaimStream } from './createClaimStream.js';
|
|
5
5
|
/**
|
|
@@ -46,7 +46,7 @@ export interface ParticipantJoinOptions {
|
|
|
46
46
|
export interface ScopedPresence {
|
|
47
47
|
readonly self: Peer;
|
|
48
48
|
readonly focus: ClaimTarget | null;
|
|
49
|
-
readonly others:
|
|
49
|
+
readonly others: readonly Peer[];
|
|
50
50
|
update(activity: Activity): void;
|
|
51
51
|
reading(detail?: string): void;
|
|
52
52
|
reading(target: PresenceTarget, detail?: string): void;
|
|
@@ -68,7 +68,7 @@ export interface ScopedClaimOptions {
|
|
|
68
68
|
}
|
|
69
69
|
export interface ScopedClaims {
|
|
70
70
|
readonly focus: ClaimTarget | null;
|
|
71
|
-
readonly others:
|
|
71
|
+
readonly others: readonly Claim[];
|
|
72
72
|
/**
|
|
73
73
|
* Claim an exclusive claim on the participant's focus target (or
|
|
74
74
|
* an explicit override via `opts.target`). Single verb — the old
|
|
@@ -91,8 +91,8 @@ export interface JoinedParticipant {
|
|
|
91
91
|
readonly syncGroups: readonly string[];
|
|
92
92
|
readonly presence: ScopedPresence;
|
|
93
93
|
readonly claims: ScopedClaims;
|
|
94
|
-
readonly peers:
|
|
95
|
-
readonly activeClaims:
|
|
94
|
+
readonly peers: readonly Peer[];
|
|
95
|
+
readonly activeClaims: readonly Claim[];
|
|
96
96
|
focus(target: PresenceTarget, options?: ParticipantFocusOptions): JoinedParticipant;
|
|
97
97
|
leave(): void;
|
|
98
98
|
[Symbol.asyncDispose](): Promise<void>;
|
|
@@ -106,10 +106,10 @@ export interface ParticipantManagerConfig {
|
|
|
106
106
|
readonly getTransport: () => SyncWebSocket | null;
|
|
107
107
|
readonly presence: PresenceStream;
|
|
108
108
|
readonly claims: AttachableClaimStream;
|
|
109
|
-
readonly schema?: Schema
|
|
109
|
+
readonly schema?: Schema;
|
|
110
110
|
}
|
|
111
111
|
export declare function createParticipantManager(config: ParticipantManagerConfig): ParticipantManager;
|
|
112
|
-
export declare function resolveParticipantSyncGroups(scope: ParticipantScope | undefined, schema?: Schema
|
|
113
|
-
export declare function syncGroupFromEntityRef(ref: ClaimTarget, schema?: Schema
|
|
112
|
+
export declare function resolveParticipantSyncGroups(scope: ParticipantScope | undefined, schema?: Schema): string[];
|
|
113
|
+
export declare function syncGroupFromEntityRef(ref: ClaimTarget, schema?: Schema): string;
|
|
114
114
|
export declare function parseParticipantTtlSeconds(value: number | string | null | undefined): number | undefined;
|
|
115
115
|
export declare function createParticipantClaimId(): string;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SyncCursor — the sync-position state of one SyncWebSocket session.
|
|
3
|
+
*
|
|
4
|
+
* Owns the two pieces of resume state the transport carries between
|
|
5
|
+
* connects: the `lastSyncId` watermark and the opaque server cursor.
|
|
6
|
+
* Extracted from SyncWebSocket so cursor bookkeeping is one cohesive
|
|
7
|
+
* leaf instead of fields + accessors spread through the transport
|
|
8
|
+
* class. The advance DISCIPLINE (only move `lastSyncId` on a
|
|
9
|
+
* persistence-gated ack) is documented at the call sites in
|
|
10
|
+
* SyncWebSocket (`sendAck` / `handleDelta`).
|
|
11
|
+
*
|
|
12
|
+
* (The per-entity version vector that used to live here was a Go-era
|
|
13
|
+
* ghost — zero decision reads client- or server-side; one total-ordered
|
|
14
|
+
* log per plane makes the scalar `sync_id` watermark the causality
|
|
15
|
+
* token. Removed in W4a.)
|
|
16
|
+
*/
|
|
17
|
+
export declare class SyncCursor {
|
|
18
|
+
lastSyncId: number;
|
|
19
|
+
syncCursor: string | null;
|
|
20
|
+
constructor(lastSyncId: number);
|
|
21
|
+
/**
|
|
22
|
+
* Advance the local cursor for an ack — this is what
|
|
23
|
+
* `requestIncrementalSync` and the connect handshake will send next,
|
|
24
|
+
* and what `getLastSyncId()` reports for clean-shutdown persistence.
|
|
25
|
+
* Monotonic: a stale (lower) ack never moves the cursor backward.
|
|
26
|
+
*/
|
|
27
|
+
ackAdvance(syncId: number): void;
|
|
28
|
+
/**
|
|
29
|
+
* Update last sync ID (for persistence)
|
|
30
|
+
*/
|
|
31
|
+
setLastSyncId(syncId: number): void;
|
|
32
|
+
/**
|
|
33
|
+
* Update sync cursor (for incremental sync)
|
|
34
|
+
*/
|
|
35
|
+
setSyncCursor(cursor: string | null): void;
|
|
36
|
+
/**
|
|
37
|
+
* Get current sync cursor
|
|
38
|
+
*/
|
|
39
|
+
getSyncCursor(): string | null;
|
|
40
|
+
/**
|
|
41
|
+
* Get the highest syncId seen this session (for persistence on clean shutdown)
|
|
42
|
+
*/
|
|
43
|
+
getLastSyncId(): number;
|
|
44
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SyncCursor — the sync-position state of one SyncWebSocket session.
|
|
3
|
+
*
|
|
4
|
+
* Owns the two pieces of resume state the transport carries between
|
|
5
|
+
* connects: the `lastSyncId` watermark and the opaque server cursor.
|
|
6
|
+
* Extracted from SyncWebSocket so cursor bookkeeping is one cohesive
|
|
7
|
+
* leaf instead of fields + accessors spread through the transport
|
|
8
|
+
* class. The advance DISCIPLINE (only move `lastSyncId` on a
|
|
9
|
+
* persistence-gated ack) is documented at the call sites in
|
|
10
|
+
* SyncWebSocket (`sendAck` / `handleDelta`).
|
|
11
|
+
*
|
|
12
|
+
* (The per-entity version vector that used to live here was a Go-era
|
|
13
|
+
* ghost — zero decision reads client- or server-side; one total-ordered
|
|
14
|
+
* log per plane makes the scalar `sync_id` watermark the causality
|
|
15
|
+
* token. Removed in W4a.)
|
|
16
|
+
*/
|
|
17
|
+
export class SyncCursor {
|
|
18
|
+
lastSyncId;
|
|
19
|
+
syncCursor;
|
|
20
|
+
constructor(lastSyncId) {
|
|
21
|
+
this.lastSyncId = lastSyncId;
|
|
22
|
+
this.syncCursor = null;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Advance the local cursor for an ack — this is what
|
|
26
|
+
* `requestIncrementalSync` and the connect handshake will send next,
|
|
27
|
+
* and what `getLastSyncId()` reports for clean-shutdown persistence.
|
|
28
|
+
* Monotonic: a stale (lower) ack never moves the cursor backward.
|
|
29
|
+
*/
|
|
30
|
+
ackAdvance(syncId) {
|
|
31
|
+
if (syncId > this.lastSyncId) {
|
|
32
|
+
this.lastSyncId = syncId;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Update last sync ID (for persistence)
|
|
37
|
+
*/
|
|
38
|
+
setLastSyncId(syncId) {
|
|
39
|
+
this.lastSyncId = syncId;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Update sync cursor (for incremental sync)
|
|
43
|
+
*/
|
|
44
|
+
setSyncCursor(cursor) {
|
|
45
|
+
this.syncCursor = cursor;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Get current sync cursor
|
|
49
|
+
*/
|
|
50
|
+
getSyncCursor() {
|
|
51
|
+
return this.syncCursor;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Get the highest syncId seen this session (for persistence on clean shutdown)
|
|
55
|
+
*/
|
|
56
|
+
getLastSyncId() {
|
|
57
|
+
return this.lastSyncId || 0;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* syncPlan — schema → sync-plan derivation.
|
|
3
|
+
*
|
|
4
|
+
* Pure leaf extracted from BaseSyncedStore.ts: walks a `Schema` and derives
|
|
5
|
+
* the two declarative arrays the store's constructor consumes (FK indexes,
|
|
6
|
+
* enrichment plan). No class state, no side effects — `BaseSyncedStore`
|
|
7
|
+
* re-exports everything here so importers are unchanged.
|
|
8
|
+
*/
|
|
9
|
+
import type { Schema } from '../schema/schema.js';
|
|
10
|
+
/** A foreign-key index to register on the ObjectPool at construction time. */
|
|
11
|
+
export interface ForeignKeyIndexSpec {
|
|
12
|
+
/**
|
|
13
|
+
* The child model name (where the FK field lives) — this is the type
|
|
14
|
+
* that will be passed to `pool.registerForeignKey(modelName, fieldName)`
|
|
15
|
+
* and later to `pool.getByForeignKey(modelName, fieldName, value)`.
|
|
16
|
+
*
|
|
17
|
+
* Use the wire `__typename` casing (e.g., `'SlideLayer'`, not
|
|
18
|
+
* `'slideLayer'`) — that's the value `createFromData` stamps onto
|
|
19
|
+
* models and the pool indexes by.
|
|
20
|
+
*/
|
|
21
|
+
readonly modelName: string;
|
|
22
|
+
/** The FK field name on the child model, e.g. `'slideId'`. */
|
|
23
|
+
readonly fieldName: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* A declarative enrichment rule for the delta-apply path.
|
|
27
|
+
*
|
|
28
|
+
* When a delta for `modelName` arrives, after the model is constructed
|
|
29
|
+
* the base store reads `data[foreignKey]` from the payload, looks up
|
|
30
|
+
* the matching parent in the ObjectPool, and attaches it as
|
|
31
|
+
* `data[relationKey]`. Best-effort: if the parent isn't yet in the
|
|
32
|
+
* pool (e.g., arrived later in the same bootstrap batch), enrichment
|
|
33
|
+
* silently no-ops.
|
|
34
|
+
*
|
|
35
|
+
* Replaces the previous pattern of overriding `enrichRelations` on a
|
|
36
|
+
* subclass to hardcode per-model enrichment logic.
|
|
37
|
+
*/
|
|
38
|
+
export interface EnrichmentPlanEntry {
|
|
39
|
+
/** The child model whose incoming deltas should be enriched. */
|
|
40
|
+
readonly modelName: string;
|
|
41
|
+
/** The FK field on the child that points at the parent's id. */
|
|
42
|
+
readonly foreignKey: string;
|
|
43
|
+
/** The property name under which to attach the parent model. */
|
|
44
|
+
readonly relationKey: string;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Walk a schema and derive the two sync-plan arrays consumed by
|
|
48
|
+
* `BaseSyncedStore`'s constructor: FK indexes to register on the pool,
|
|
49
|
+
* and the enrichment plan.
|
|
50
|
+
*
|
|
51
|
+
* FK indexes and enrichment entries are pulled from each `belongsTo`
|
|
52
|
+
* relation where `options.index` / `options.enrich` is set. Relations
|
|
53
|
+
* without those options are skipped — this is an opt-in mechanism so
|
|
54
|
+
* adding a `belongsTo` never silently changes delta or lookup semantics.
|
|
55
|
+
*
|
|
56
|
+
* Pure function: takes a Schema, returns two arrays. No side effects,
|
|
57
|
+
* no class state. Called once at construction time from `BaseSyncedStore`.
|
|
58
|
+
*/
|
|
59
|
+
export declare function deriveSyncPlanFromSchema(schema: Schema): {
|
|
60
|
+
enrichmentPlan: EnrichmentPlanEntry[];
|
|
61
|
+
foreignKeyIndexes: ForeignKeyIndexSpec[];
|
|
62
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* syncPlan — schema → sync-plan derivation.
|
|
3
|
+
*
|
|
4
|
+
* Pure leaf extracted from BaseSyncedStore.ts: walks a `Schema` and derives
|
|
5
|
+
* the two declarative arrays the store's constructor consumes (FK indexes,
|
|
6
|
+
* enrichment plan). No class state, no side effects — `BaseSyncedStore`
|
|
7
|
+
* re-exports everything here so importers are unchanged.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Walk a schema and derive the two sync-plan arrays consumed by
|
|
11
|
+
* `BaseSyncedStore`'s constructor: FK indexes to register on the pool,
|
|
12
|
+
* and the enrichment plan.
|
|
13
|
+
*
|
|
14
|
+
* FK indexes and enrichment entries are pulled from each `belongsTo`
|
|
15
|
+
* relation where `options.index` / `options.enrich` is set. Relations
|
|
16
|
+
* without those options are skipped — this is an opt-in mechanism so
|
|
17
|
+
* adding a `belongsTo` never silently changes delta or lookup semantics.
|
|
18
|
+
*
|
|
19
|
+
* Pure function: takes a Schema, returns two arrays. No side effects,
|
|
20
|
+
* no class state. Called once at construction time from `BaseSyncedStore`.
|
|
21
|
+
*/
|
|
22
|
+
export function deriveSyncPlanFromSchema(schema) {
|
|
23
|
+
const enrichmentPlan = [];
|
|
24
|
+
const foreignKeyIndexes = [];
|
|
25
|
+
for (const [modelName, def] of Object.entries(schema.models)) {
|
|
26
|
+
const typename = def.typename ?? modelName;
|
|
27
|
+
for (const [relationKey, rel] of Object.entries(def.relations)) {
|
|
28
|
+
if (rel.type === 'belongsTo') {
|
|
29
|
+
if (rel.options?.index) {
|
|
30
|
+
foreignKeyIndexes.push({ modelName: typename, fieldName: rel.foreignKey });
|
|
31
|
+
}
|
|
32
|
+
if (rel.options?.enrich) {
|
|
33
|
+
enrichmentPlan.push({
|
|
34
|
+
modelName: typename,
|
|
35
|
+
foreignKey: rel.foreignKey,
|
|
36
|
+
relationKey,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
else if (rel.type === 'hasMany' || rel.type === 'hasOne') {
|
|
41
|
+
// hasMany/hasOne: the FK lives on the TARGET model, not the current model.
|
|
42
|
+
// Register the FK index on the target so getByForeignKey works.
|
|
43
|
+
// Target typename is resolved at registration time from the schema.
|
|
44
|
+
const targetDef = schema.models[rel.target];
|
|
45
|
+
const targetTypename = targetDef?.typename ?? rel.target;
|
|
46
|
+
foreignKeyIndexes.push({ modelName: targetTypename, fieldName: rel.foreignKey });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return { enrichmentPlan, foreignKeyIndexes };
|
|
51
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inbound frame dispatch for the sync WebSocket.
|
|
3
|
+
*
|
|
4
|
+
* Replaces the monolithic frame `switch` that used to live inside
|
|
5
|
+
* `SyncWebSocket.setupEventHandlers` with a frame-type → handler table of
|
|
6
|
+
* functions over a minimal {@link WsSession} interface — only the members
|
|
7
|
+
* the handlers actually touch, never the transport class itself (no
|
|
8
|
+
* import cycle). The host's `onmessage` stays responsible for JSON
|
|
9
|
+
* parsing, heartbeat proof-of-life, and the outer try/catch; everything
|
|
10
|
+
* after that funnels through {@link dispatchWsFrame}.
|
|
11
|
+
*/
|
|
12
|
+
import { type CommitAck } from './commitFrames.js';
|
|
13
|
+
/**
|
|
14
|
+
* In-flight `commit` request record, keyed by clientTxId in the session.
|
|
15
|
+
* Resolved when a matching `mutation_result` frame arrives from the
|
|
16
|
+
* server, or rejected on timeout / disconnect.
|
|
17
|
+
*/
|
|
18
|
+
export interface PendingCommit {
|
|
19
|
+
resolve: (value: CommitAck) => void;
|
|
20
|
+
reject: (err: Error) => void;
|
|
21
|
+
timeout: ReturnType<typeof setTimeout>;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* In-flight `claim` request record, keyed by claimId. Resolved when the
|
|
25
|
+
* matching `claim_ack` arrives, or rejected on timeout/disconnect.
|
|
26
|
+
*/
|
|
27
|
+
export interface PendingClaim {
|
|
28
|
+
resolve: (value: {
|
|
29
|
+
syncGroups: string[];
|
|
30
|
+
ttlSeconds?: number;
|
|
31
|
+
}) => void;
|
|
32
|
+
reject: (err: Error) => void;
|
|
33
|
+
timeout: ReturnType<typeof setTimeout>;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* In-flight `update_subscription` record awaiting `subscription_ack`.
|
|
37
|
+
* FIFO-matched (no correlation id on the wire) — see the session field
|
|
38
|
+
* doc on SyncWebSocket.pendingSubscriptions.
|
|
39
|
+
*/
|
|
40
|
+
export interface PendingSubscription {
|
|
41
|
+
resolve: (value: {
|
|
42
|
+
syncGroups: string[];
|
|
43
|
+
}) => void;
|
|
44
|
+
reject: (err: Error) => void;
|
|
45
|
+
timeout: ReturnType<typeof setTimeout>;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Parsed inbound wire frame (the raw `JSON.parse` result). Untrusted
|
|
49
|
+
* data — every payload is loose and each handler narrows defensively,
|
|
50
|
+
* the same posture the inline switch had.
|
|
51
|
+
*/
|
|
52
|
+
export interface WsInboundFrame {
|
|
53
|
+
type?: string;
|
|
54
|
+
payload?: unknown;
|
|
55
|
+
/** Legacy bare-delta frames carry delta fields at the top level. */
|
|
56
|
+
actionType?: unknown;
|
|
57
|
+
modelName?: unknown;
|
|
58
|
+
[key: string]: unknown;
|
|
59
|
+
}
|
|
60
|
+
/** Narrow arbitrary wire data to a plain string-keyed record. */
|
|
61
|
+
export declare function isRecord(value: unknown): value is Record<string, unknown>;
|
|
62
|
+
/**
|
|
63
|
+
* Envelope guard for the raw `JSON.parse` result of an inbound WS message.
|
|
64
|
+
* A frame is any plain object whose `type`, when present, is a string —
|
|
65
|
+
* payload-level validation stays with each handler (deltas go through the
|
|
66
|
+
* canonical `clientSyncDeltaSchema` at the `normalizeWireDelta` seam).
|
|
67
|
+
*/
|
|
68
|
+
export declare function isWsInboundFrame(value: unknown): value is WsInboundFrame;
|
|
69
|
+
/**
|
|
70
|
+
* The slice of SyncWebSocket the frame handlers operate on. The host
|
|
71
|
+
* builds one adapter object over its private state; closures read live
|
|
72
|
+
* fields so host-side reassignment (e.g. the pendingSubscriptions reset
|
|
73
|
+
* on close) can't strand the handlers on stale references.
|
|
74
|
+
*/
|
|
75
|
+
export interface WsSession {
|
|
76
|
+
/** EventEmitter surface — handlers emit the typed transport events. */
|
|
77
|
+
emit(event: string, ...args: unknown[]): boolean;
|
|
78
|
+
/** In-flight commit acks keyed by clientTxId. */
|
|
79
|
+
pendingMutations: Map<string, PendingCommit>;
|
|
80
|
+
/** In-flight claim acks keyed by claimId. */
|
|
81
|
+
pendingClaims: Map<string, PendingClaim>;
|
|
82
|
+
/** FIFO pop of the oldest in-flight `update_subscription` request. */
|
|
83
|
+
shiftPendingSubscription(): PendingSubscription | undefined;
|
|
84
|
+
/** Connection options subset the handlers write back (acked sync groups). */
|
|
85
|
+
options: {
|
|
86
|
+
syncGroups: string[];
|
|
87
|
+
};
|
|
88
|
+
/** Registered collaboration event keys (colon format). */
|
|
89
|
+
collaborationEventTypes: ReadonlySet<string>;
|
|
90
|
+
/**
|
|
91
|
+
* Receive-boundary delta processing. Takes UNTRUSTED wire data — the
|
|
92
|
+
* host validates against the canonical `clientSyncDeltaSchema` (and
|
|
93
|
+
* drops malformed deltas) at its `normalizeWireDelta` seam, so the
|
|
94
|
+
* handlers here never need to cast.
|
|
95
|
+
*/
|
|
96
|
+
handleDelta(delta: unknown): void;
|
|
97
|
+
handleSyncResponse(payload: unknown): void;
|
|
98
|
+
handleBootstrapResponse(payload: unknown): void;
|
|
99
|
+
handlePresenceUpdate(message: {
|
|
100
|
+
payload?: unknown;
|
|
101
|
+
[k: string]: unknown;
|
|
102
|
+
}): void;
|
|
103
|
+
}
|
|
104
|
+
export type WsFrameHandler = (session: WsSession, message: WsInboundFrame) => void;
|
|
105
|
+
/**
|
|
106
|
+
* Frame-type → handler table. Every named server frame the SDK
|
|
107
|
+
* understands dispatches through here; anything else falls to the
|
|
108
|
+
* collaboration-event / unknown-type path in {@link dispatchWsFrame}.
|
|
109
|
+
*/
|
|
110
|
+
export declare const wsFrameHandlers: Record<string, WsFrameHandler>;
|
|
111
|
+
/**
|
|
112
|
+
* Route one parsed inbound frame to its handler. Mirrors the original
|
|
113
|
+
* inline switch exactly: keepalives are ignored, a missing `type` is
|
|
114
|
+
* the legacy bare-delta form, unknown types fall through to the
|
|
115
|
+
* collaboration-event map (underscore wire format → colon event key).
|
|
116
|
+
*/
|
|
117
|
+
export declare function dispatchWsFrame(session: WsSession, message: WsInboundFrame): void;
|