@abloatai/ablo 0.25.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.
Files changed (244) hide show
  1. package/AGENTS.md +5 -3
  2. package/CHANGELOG.md +20 -0
  3. package/README.md +3 -3
  4. package/dist/BaseSyncedStore.d.ts +72 -195
  5. package/dist/BaseSyncedStore.js +240 -625
  6. package/dist/Database.d.ts +8 -9
  7. package/dist/Database.js +9 -14
  8. package/dist/LazyReferenceCollection.js +5 -0
  9. package/dist/Model.js +6 -8
  10. package/dist/ModelRegistry.d.ts +25 -6
  11. package/dist/ModelRegistry.js +12 -6
  12. package/dist/NetworkMonitor.js +1 -1
  13. package/dist/ObjectPool.js +9 -3
  14. package/dist/SyncClient.d.ts +15 -5
  15. package/dist/SyncClient.js +152 -66
  16. package/dist/SyncEngineContext.d.ts +1 -3
  17. package/dist/SyncEngineContext.js +1 -2
  18. package/dist/agent/Agent.d.ts +21 -8
  19. package/dist/agent/Agent.js +26 -13
  20. package/dist/ai-sdk/coordination-context.js +9 -3
  21. package/dist/ai-sdk/wrap.d.ts +1 -1
  22. package/dist/ai-sdk/wrap.js +1 -1
  23. package/dist/auth/credentialPolicy.d.ts +10 -2
  24. package/dist/auth/credentialSource.d.ts +1 -13
  25. package/dist/auth/credentialSource.js +6 -13
  26. package/dist/auth/index.js +3 -3
  27. package/dist/cli.cjs +328 -262
  28. package/dist/client/Ablo.d.ts +35 -764
  29. package/dist/client/Ablo.js +64 -688
  30. package/dist/client/ApiClient.d.ts +14 -1
  31. package/dist/client/ApiClient.js +63 -9
  32. package/dist/client/auth.d.ts +8 -17
  33. package/dist/client/auth.js +42 -6
  34. package/dist/client/consoleLogger.d.ts +36 -0
  35. package/dist/client/consoleLogger.js +45 -0
  36. package/dist/client/createInternalComponents.js +1 -1
  37. package/dist/client/createModelProxy.js +10 -6
  38. package/dist/client/credentialEndpoint.d.ts +63 -0
  39. package/dist/client/credentialEndpoint.js +87 -0
  40. package/dist/client/hostedEndpoints.d.ts +24 -0
  41. package/dist/client/hostedEndpoints.js +24 -0
  42. package/dist/client/httpClient.d.ts +1 -1
  43. package/dist/client/identity.js +2 -1
  44. package/dist/client/modelRegistration.d.ts +14 -0
  45. package/dist/client/modelRegistration.js +321 -0
  46. package/dist/client/options.d.ts +396 -0
  47. package/dist/client/options.js +10 -0
  48. package/dist/client/resourceTypes.d.ts +344 -0
  49. package/dist/client/resourceTypes.js +13 -0
  50. package/dist/client/schemaConfig.d.ts +56 -0
  51. package/dist/client/schemaConfig.js +188 -0
  52. package/dist/client/sessionMint.d.ts +1 -1
  53. package/dist/client/validateAbloOptions.js +1 -1
  54. package/dist/client/wsMutationExecutor.d.ts +33 -0
  55. package/dist/client/wsMutationExecutor.js +77 -0
  56. package/dist/context.js +0 -3
  57. package/dist/coordination/index.d.ts +8 -1
  58. package/dist/coordination/index.js +24 -1
  59. package/dist/coordination/trace.d.ts +0 -1
  60. package/dist/coordination/trace.js +0 -1
  61. package/dist/core/DatabaseManager.d.ts +0 -1
  62. package/dist/core/DatabaseManager.js +26 -24
  63. package/dist/core/QueryProcessor.js +7 -8
  64. package/dist/core/QueryView.d.ts +14 -2
  65. package/dist/core/QueryView.js +4 -5
  66. package/dist/core/StoreManager.d.ts +2 -2
  67. package/dist/core/StoreManager.js +5 -1
  68. package/dist/core/index.d.ts +1 -1
  69. package/dist/core/openIDBWithTimeout.js +15 -11
  70. package/dist/core/query-utils.js +1 -1
  71. package/dist/core/storeContract.d.ts +143 -0
  72. package/dist/core/storeContract.js +16 -0
  73. package/dist/errorCodes.d.ts +11 -2
  74. package/dist/errorCodes.js +155 -137
  75. package/dist/errors.d.ts +18 -7
  76. package/dist/errors.js +14 -1
  77. package/dist/index.js +5 -0
  78. package/dist/interfaces/index.d.ts +6 -20
  79. package/dist/keys/index.js +18 -9
  80. package/dist/mutators/UndoManager.js +6 -1
  81. package/dist/mutators/defineMutators.d.ts +1 -3
  82. package/dist/policy/types.d.ts +1 -1
  83. package/dist/query/client.d.ts +11 -0
  84. package/dist/query/client.js +81 -52
  85. package/dist/react/AbloProvider.d.ts +6 -10
  86. package/dist/react/AbloProvider.js +22 -15
  87. package/dist/react/context.d.ts +2 -118
  88. package/dist/react/useAblo.js +1 -1
  89. package/dist/react/useErrorListener.js +1 -1
  90. package/dist/react/useMutationFailureListener.js +1 -1
  91. package/dist/react/useMutators.js +7 -1
  92. package/dist/react/useReactive.js +0 -1
  93. package/dist/react/useUndoScope.js +2 -2
  94. package/dist/schema/ddlLock.d.ts +39 -0
  95. package/dist/schema/ddlLock.js +51 -0
  96. package/dist/schema/field.js +1 -1
  97. package/dist/schema/index.d.ts +4 -3
  98. package/dist/schema/index.js +15 -5
  99. package/dist/schema/model.d.ts +3 -3
  100. package/dist/schema/model.js +2 -2
  101. package/dist/schema/openapi.js +5 -1
  102. package/dist/schema/queries.d.ts +1 -1
  103. package/dist/schema/queries.js +1 -0
  104. package/dist/schema/residency.d.ts +35 -0
  105. package/dist/schema/residency.js +27 -0
  106. package/dist/schema/roles.d.ts +25 -0
  107. package/dist/schema/roles.js +37 -0
  108. package/dist/schema/schema.d.ts +3 -3
  109. package/dist/schema/schema.js +11 -7
  110. package/dist/schema/serialize.d.ts +8 -8
  111. package/dist/schema/sync-delta-row.d.ts +2 -2
  112. package/dist/schema/sync-delta-row.js +2 -2
  113. package/dist/server/storage-mode.d.ts +8 -1
  114. package/dist/server/storage-mode.js +14 -4
  115. package/dist/source/adapter.d.ts +1 -1
  116. package/dist/source/adapters/drizzle.js +4 -3
  117. package/dist/source/adapters/kysely.js +4 -3
  118. package/dist/source/adapters/memory.js +1 -1
  119. package/dist/source/adapters/prisma.js +5 -4
  120. package/dist/source/conformance.js +10 -8
  121. package/dist/source/connector.js +4 -2
  122. package/dist/source/factory.d.ts +106 -0
  123. package/dist/source/factory.js +273 -0
  124. package/dist/source/index.d.ts +11 -461
  125. package/dist/source/index.js +14 -416
  126. package/dist/source/next.d.ts +1 -1
  127. package/dist/source/next.js +1 -1
  128. package/dist/source/pushQueue.d.ts +1 -1
  129. package/dist/source/pushQueue.js +6 -3
  130. package/dist/source/signing.d.ts +59 -0
  131. package/dist/source/signing.js +142 -0
  132. package/dist/source/types.d.ts +334 -0
  133. package/dist/source/types.js +44 -0
  134. package/dist/stores/ObjectStore.d.ts +1 -1
  135. package/dist/stores/ObjectStore.js +23 -23
  136. package/dist/stores/SyncActionStore.d.ts +1 -1
  137. package/dist/stores/SyncActionStore.js +70 -35
  138. package/dist/surface.d.ts +1 -1
  139. package/dist/surface.js +1 -0
  140. package/dist/sync/AreaOfInterestManager.d.ts +2 -2
  141. package/dist/sync/AreaOfInterestManager.js +1 -1
  142. package/dist/sync/BootstrapHelper.d.ts +1 -3
  143. package/dist/sync/BootstrapHelper.js +29 -8
  144. package/dist/sync/ConnectionManager.d.ts +8 -0
  145. package/dist/sync/ConnectionManager.js +19 -4
  146. package/dist/sync/HydrationCoordinator.d.ts +12 -0
  147. package/dist/sync/HydrationCoordinator.js +25 -5
  148. package/dist/sync/NetworkProbe.d.ts +3 -2
  149. package/dist/sync/NetworkProbe.js +7 -6
  150. package/dist/sync/SyncWebSocket.d.ts +71 -120
  151. package/dist/sync/SyncWebSocket.js +324 -630
  152. package/dist/sync/awaitClaimGrant.js +27 -19
  153. package/dist/sync/bootstrapApply.d.ts +60 -0
  154. package/dist/sync/bootstrapApply.js +65 -0
  155. package/dist/sync/commitFrames.d.ts +43 -0
  156. package/dist/sync/commitFrames.js +94 -0
  157. package/dist/sync/createClaimStream.js +3 -2
  158. package/dist/sync/createSnapshot.d.ts +1 -3
  159. package/dist/sync/credentialLifecycle.d.ts +175 -0
  160. package/dist/sync/credentialLifecycle.js +329 -0
  161. package/dist/sync/deltaPipeline.d.ts +111 -0
  162. package/dist/sync/deltaPipeline.js +262 -0
  163. package/dist/sync/groupChange.d.ts +111 -0
  164. package/dist/sync/groupChange.js +246 -0
  165. package/dist/sync/heartbeat.d.ts +62 -0
  166. package/dist/sync/heartbeat.js +91 -0
  167. package/dist/sync/participants.d.ts +8 -8
  168. package/dist/sync/syncCursor.d.ts +44 -0
  169. package/dist/sync/syncCursor.js +59 -0
  170. package/dist/sync/syncPlan.d.ts +62 -0
  171. package/dist/sync/syncPlan.js +51 -0
  172. package/dist/sync/wsFrameHandlers.d.ts +117 -0
  173. package/dist/sync/wsFrameHandlers.js +378 -0
  174. package/dist/testing/fixtures/bootstrap.d.ts +9 -11
  175. package/dist/testing/fixtures/deltas.d.ts +1 -1
  176. package/dist/testing/fixtures/models.js +1 -0
  177. package/dist/testing/helpers/sync-engine-harness.d.ts +0 -3
  178. package/dist/testing/helpers/sync-engine-harness.js +2 -1
  179. package/dist/testing/index.d.ts +1 -1
  180. package/dist/testing/index.js +1 -1
  181. package/dist/testing/mocks/MockSyncContext.d.ts +1 -17
  182. package/dist/testing/mocks/MockSyncContext.js +1 -32
  183. package/dist/testing/mocks/MockSyncStore.d.ts +4 -4
  184. package/dist/testing/mocks/MockWebSocket.d.ts +2 -2
  185. package/dist/transactions/TransactionQueue.d.ts +20 -56
  186. package/dist/transactions/TransactionQueue.js +101 -487
  187. package/dist/transactions/TransactionStore.d.ts +18 -0
  188. package/dist/transactions/TransactionStore.js +51 -0
  189. package/dist/transactions/coalesceRules.d.ts +34 -0
  190. package/dist/transactions/coalesceRules.js +117 -0
  191. package/dist/transactions/commitPayload.d.ts +134 -0
  192. package/dist/transactions/commitPayload.js +152 -0
  193. package/dist/transactions/deltaConfirmation.d.ts +60 -0
  194. package/dist/transactions/deltaConfirmation.js +223 -0
  195. package/dist/transactions/optimistic.d.ts +24 -0
  196. package/dist/transactions/optimistic.js +45 -0
  197. package/dist/transactions/persistedReplay.d.ts +93 -0
  198. package/dist/transactions/persistedReplay.js +105 -0
  199. package/dist/types/index.d.ts +3 -3
  200. package/dist/types/index.js +0 -5
  201. package/dist/types/modelData.d.ts +12 -0
  202. package/dist/types/modelData.js +11 -0
  203. package/dist/types/participant.d.ts +21 -0
  204. package/dist/types/participant.js +12 -0
  205. package/dist/types/streams.d.ts +8 -14
  206. package/dist/utils/mobx-setup.d.ts +1 -1
  207. package/dist/utils/mobx-setup.js +2 -2
  208. package/dist/wire/errorEnvelope.d.ts +11 -2
  209. package/dist/wire/errorEnvelope.js +14 -2
  210. package/dist/wire/frames.d.ts +90 -0
  211. package/dist/wire/frames.js +55 -1
  212. package/dist/wire/index.d.ts +4 -1
  213. package/dist/wire/index.js +20 -1
  214. package/dist/wire/protocol.d.ts +45 -0
  215. package/dist/wire/protocol.js +45 -0
  216. package/dist/wire/protocolVersion.d.ts +56 -0
  217. package/dist/wire/protocolVersion.js +63 -0
  218. package/docs/api-keys.md +4 -3
  219. package/docs/examples/existing-python-backend.md +3 -3
  220. package/docs/identity.md +4 -4
  221. package/docs/integration-guide.md +1 -1
  222. package/docs/react.md +1 -1
  223. package/docs/sessions.md +5 -7
  224. package/package.json +15 -13
  225. package/dist/client/index.d.ts +0 -36
  226. package/dist/client/index.js +0 -33
  227. package/dist/config/index.d.ts +0 -10
  228. package/dist/config/index.js +0 -12
  229. package/dist/interfaces/headless.d.ts +0 -95
  230. package/dist/interfaces/headless.js +0 -41
  231. package/dist/query/index.d.ts +0 -6
  232. package/dist/query/index.js +0 -5
  233. package/dist/realtime/index.d.ts +0 -10
  234. package/dist/realtime/index.js +0 -9
  235. package/dist/schema/plane.d.ts +0 -23
  236. package/dist/schema/plane.js +0 -19
  237. package/dist/server/next.d.ts +0 -51
  238. package/dist/server/next.js +0 -47
  239. package/dist/sync/OfflineFlush.d.ts +0 -9
  240. package/dist/sync/OfflineFlush.js +0 -22
  241. package/dist/sync/OfflineTransactionStore.d.ts +0 -37
  242. package/dist/sync/OfflineTransactionStore.js +0 -263
  243. package/dist/transactions/index.d.ts +0 -16
  244. 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, SchemaRecord } from '../schema/schema.js';
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: ReadonlyArray<Peer>;
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: ReadonlyArray<Claim>;
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: ReadonlyArray<Peer>;
95
- readonly activeClaims: ReadonlyArray<Claim>;
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<SchemaRecord>;
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<SchemaRecord>): string[];
113
- export declare function syncGroupFromEntityRef(ref: ClaimTarget, schema?: Schema<SchemaRecord>): string;
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;