@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,77 @@
1
+ /**
2
+ * The default `MutationExecutor` — sends `{ type: 'commit', ... }` frames over
3
+ * the engine's own WebSocket, resolved lazily at commit time (the WS doesn't
4
+ * exist when the executor is constructed).
5
+ *
6
+ * Extracted from `Ablo.ts`; the factory wires it up with a `getWs` closure
7
+ * over the store holder unless the caller supplies its own executor.
8
+ */
9
+ import { AbloError, AbloConnectionError } from '../errors.js';
10
+ // ── Default mutation executor (wire: `commit` frame over WebSocket) ──────
11
+ /**
12
+ * Default mutation executor: sends `{ type: 'commit', payload: ... }` over
13
+ * the sync engine's own WebSocket.
14
+ *
15
+ * Transport ownership follows the Zero / Liveblocks pattern — the engine
16
+ * owns its socket end-to-end and the executor is internal. Apps pass URLs
17
+ * and auth; they do NOT inject transport callbacks. That's why this
18
+ * factory takes a `getWs` closure instead of a full SyncWebSocket: the WS
19
+ * doesn't exist when the executor is constructed (it's created later in
20
+ * `Ablo` during `BaseSyncedStore` init), so we resolve it
21
+ * lazily at commit time. Same trick Zero uses internally — see
22
+ * `packages/zero-client/src/client/zero.ts` where `Pusher`/`Puller` are
23
+ * constructed before the socket then wired up at connect time.
24
+ *
25
+ * `options.idempotencyKey` becomes the wire-level `clientTxId` when set,
26
+ * matching Stripe-style retry semantics. Otherwise the SDK generates one.
27
+ */
28
+ export function createDefaultMutationExecutor(getWs) {
29
+ async function commit(operations, options) {
30
+ const ws = getWs();
31
+ if (!ws?.sendCommit) {
32
+ throw new AbloConnectionError('SyncWebSocket not ready for commit. The engine must finish bootstrap ' +
33
+ 'before mutations can be sent.', { code: 'ws_not_ready' });
34
+ }
35
+ const clientTxId = options?.idempotencyKey ??
36
+ (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'
37
+ ? crypto.randomUUID()
38
+ : `tx_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`);
39
+ try {
40
+ return await ws.sendCommit(operations, clientTxId, undefined, // use sendCommit's built-in 15s default; no per-call override
41
+ options?.causedByTaskId, options?.reads);
42
+ }
43
+ catch (err) {
44
+ // Wrap transport-level failures as connection errors so the
45
+ // TransactionQueue's retry classifier treats them as transient
46
+ // (matches the old HTTP path's network-error handling).
47
+ if (err instanceof AbloError)
48
+ throw err;
49
+ if (err instanceof Error) {
50
+ if (/not connected|timed out|connection|ECONN/i.test(err.message)) {
51
+ const wrapped = new AbloConnectionError(err.message, { cause: err });
52
+ // Preserve any `diagnostics` snapshot the underlying SyncWebSocket
53
+ // attached to the rejection. Without this, the wrapped error
54
+ // bottoms out at "AbloConnectionError: not connected" with no
55
+ // attribution to which close code / heartbeat trip / session
56
+ // error caused it. See SyncWebSocket.notConnectedError().
57
+ if (err &&
58
+ typeof err === 'object' &&
59
+ 'diagnostics' in err &&
60
+ err.diagnostics) {
61
+ wrapped.diagnostics = err.diagnostics;
62
+ }
63
+ throw wrapped;
64
+ }
65
+ }
66
+ throw err;
67
+ }
68
+ }
69
+ return {
70
+ commit,
71
+ executeCreate: (model, id, input, _txId, options) => commit([{ type: 'CREATE', model: model.toLowerCase(), id, input }], options).then(() => { }),
72
+ executeUpdate: (model, id, data, _txId, options) => commit([{ type: 'UPDATE', model: model.toLowerCase(), id, input: data }], options),
73
+ executeDelete: (model, id, _txId, options) => commit([{ type: 'DELETE', model: model.toLowerCase(), id }], options).then(() => { }),
74
+ executeArchive: (model, id, _txId, options) => commit([{ type: 'ARCHIVE', model: model.toLowerCase(), id }], options).then(() => { }),
75
+ executeUnarchive: (model, id, _txId, options) => commit([{ type: 'UNARCHIVE', model: model.toLowerCase(), id }], options).then(() => { }),
76
+ };
77
+ }
package/dist/context.js CHANGED
@@ -52,7 +52,4 @@ const _fallback = {
52
52
  executeArchive: () => Promise.resolve(),
53
53
  executeUnarchive: () => Promise.resolve(),
54
54
  },
55
- mutationDispatcher: {
56
- dispatch: () => Promise.resolve(),
57
- },
58
55
  };
@@ -2,5 +2,12 @@
2
2
  * `@abloatai/ablo/coordination` — the canonical wire schema for the three
3
3
  * coordination layers (presence, pessimistic claims, optimistic stale-context).
4
4
  * See `./schema.ts` for the model and the per-layer schemas.
5
+ *
6
+ * Explicit named list (not `export *`): every addition to schema.ts must be a
7
+ * deliberate export decision here, so new symbols don't silently become public
8
+ * API of the published package.
5
9
  */
6
- export * from './schema.js';
10
+ export { targetRangeSchema, participantKindSchema, wireParticipantKindSchema, participantKindFromWire, descriptionFromMeta, targetRefSchema, onStaleModeSchema, writeGuardSchema, staleNotificationSchema, readDependencySchema, claimStatusSchema, wireClaimSummarySchema, claimErrorSchema, wireClaimSchema, claimRejectionSchema, modelTargetSchema, modelClaimSchema, claimBeginPayloadSchema, claimAbandonPayloadSchema, claimReorderPayloadSchema, updateSubscriptionPayloadSchema, subscriptionAckPayloadSchema, commitOperationTypeSchema, commitOperationSchema, presenceKindSchema, presenceActivitySchema, presenceUpdateFrameSchema, } from './schema.js';
11
+ export { defaultPolicy, capabilityPreemptPolicy, interpretConflictAxis } from '../policy/types.js';
12
+ export type { Conflict, ConflictAxis, ConflictDecision, ConflictKind, ConflictOperation, ConflictPolicy, StaleContextConflict, ClaimHeldConflict, } from '../policy/types.js';
13
+ export type { TargetRange, ParticipantKind, TargetRef, OnStaleMode, WriteGuard, StaleNotification, ReadDependency, ClaimStatus, WireClaimSummary, ClaimError, WireClaim, ClaimRejection, ModelTarget, ModelClaim, ClaimBeginPayload, ClaimAbandonPayload, ClaimReorderPayload, UpdateSubscriptionPayload, SubscriptionAckPayload, CommitOperationType, CommitOperation, AnyCommitOperation, PresenceKind, PresenceActivity, PresenceUpdateFrame, } from './schema.js';
@@ -2,5 +2,28 @@
2
2
  * `@abloatai/ablo/coordination` — the canonical wire schema for the three
3
3
  * coordination layers (presence, pessimistic claims, optimistic stale-context).
4
4
  * See `./schema.ts` for the model and the per-layer schemas.
5
+ *
6
+ * Explicit named list (not `export *`): every addition to schema.ts must be a
7
+ * deliberate export decision here, so new symbols don't silently become public
8
+ * API of the published package.
5
9
  */
6
- export * from './schema.js';
10
+ // Runtime schemas + helpers.
11
+ export {
12
+ // Shared primitives
13
+ targetRangeSchema, participantKindSchema, wireParticipantKindSchema, participantKindFromWire, descriptionFromMeta, targetRefSchema,
14
+ // Layer 3 — optimistic stale-context
15
+ onStaleModeSchema, writeGuardSchema, staleNotificationSchema, readDependencySchema,
16
+ // Layer 2 — pessimistic claim / claim-lease
17
+ claimStatusSchema, wireClaimSummarySchema, claimErrorSchema, wireClaimSchema, claimRejectionSchema, modelTargetSchema, modelClaimSchema, claimBeginPayloadSchema, claimAbandonPayloadSchema, claimReorderPayloadSchema,
18
+ // Read interest — area-of-interest navigation
19
+ updateSubscriptionPayloadSchema, subscriptionAckPayloadSchema,
20
+ // Commit operation — carries the optimistic write-guard
21
+ commitOperationTypeSchema, commitOperationSchema,
22
+ // Layer 1 — presence
23
+ presenceKindSchema, presenceActivitySchema, presenceUpdateFrameSchema, } from './schema.js';
24
+ // Conflict-policy runtime — the engine detects, the policy decides. Lives in
25
+ // `../policy/types.js` (a dependency-free leaf: its only imports are types),
26
+ // exported here so a server-side consumer reaches the coordination vocabulary
27
+ // AND its default resolution through one leaf subpath instead of the root
28
+ // barrel (which would evaluate the whole browser client stack in Node).
29
+ export { defaultPolicy, capabilityPreemptPolicy, interpretConflictAxis } from '../policy/types.js';
@@ -83,7 +83,6 @@ export declare class ClaimLog implements SyncObservabilityProvider {
83
83
  captureReconciliation(): void;
84
84
  captureDeltaRetryExhausted(): void;
85
85
  captureWebSocketError(): void;
86
- captureOfflineFlushFailure(): void;
87
86
  captureSelfHealing(): void;
88
87
  captureCommitZeroSyncId(): void;
89
88
  startSpan<T>(_name: string, _op: string, fn: () => T): T;
@@ -135,7 +135,6 @@ export class ClaimLog {
135
135
  captureReconciliation() { }
136
136
  captureDeltaRetryExhausted() { }
137
137
  captureWebSocketError() { }
138
- captureOfflineFlushFailure() { }
139
138
  captureSelfHealing() { }
140
139
  captureCommitZeroSyncId() { }
141
140
  startSpan(_name, _op, fn) {
@@ -25,7 +25,6 @@ export interface WorkspaceMetadata {
25
25
  updatedAt: Date;
26
26
  schemaHash?: string;
27
27
  syncGroups?: string[];
28
- versions?: Record<string, number>;
29
28
  }
30
29
  /**
31
30
  * DatabaseManager - Manages Ablo's two-tier database architecture
@@ -107,7 +107,7 @@ export class DatabaseManager {
107
107
  schemaHash,
108
108
  schemaVersion,
109
109
  userVersion,
110
- createdAt: existingInfo?.createdAt || new Date(),
110
+ createdAt: existingInfo?.createdAt ?? new Date(),
111
111
  updatedAt: new Date(),
112
112
  };
113
113
  }
@@ -143,8 +143,8 @@ export class DatabaseManager {
143
143
  tx.oncomplete = () => {
144
144
  resolve();
145
145
  };
146
- tx.onerror = () => reject(tx.error);
147
- request.onerror = () => reject(request.error);
146
+ tx.onerror = () => { reject(tx.error); };
147
+ request.onerror = () => { reject(request.error); };
148
148
  });
149
149
  }
150
150
  /**
@@ -157,8 +157,8 @@ export class DatabaseManager {
157
157
  const tx = this.metaDb.transaction(['databases'], 'readonly');
158
158
  const store = tx.objectStore('databases');
159
159
  const request = store.get(name);
160
- request.onsuccess = () => resolve(request.result || null);
161
- request.onerror = () => reject(request.error);
160
+ request.onsuccess = () => { resolve(request.result ?? null); };
161
+ request.onerror = () => { reject(request.error); };
162
162
  });
163
163
  }
164
164
  /**
@@ -172,8 +172,8 @@ export class DatabaseManager {
172
172
  const store = tx.objectStore('databases');
173
173
  const index = store.index('userId');
174
174
  const request = index.getAll(userId);
175
- request.onsuccess = () => resolve(request.result || []);
176
- request.onerror = () => reject(request.error);
175
+ request.onsuccess = () => { resolve(request.result || []); };
176
+ request.onerror = () => { reject(request.error); };
177
177
  });
178
178
  }
179
179
  /**
@@ -231,18 +231,20 @@ export class DatabaseManager {
231
231
  return;
232
232
  }
233
233
  const meta = {
234
- lastSyncId: data.lastSyncId || 0,
235
- firstSyncId: data.firstSyncId || 0,
236
- backendDatabaseVersion: data.backendDatabaseVersion || 1,
237
- subscribedSyncGroups: data.subscribedSyncGroups || [],
234
+ lastSyncId: data.lastSyncId ?? 0,
235
+ firstSyncId: data.firstSyncId ?? 0,
236
+ backendDatabaseVersion: data.backendDatabaseVersion ?? 1,
237
+ subscribedSyncGroups: data.subscribedSyncGroups ?? [],
238
238
  updatedAt: data.updatedAt ? new Date(data.updatedAt) : new Date(),
239
239
  schemaHash: data.schemaHash,
240
240
  syncGroups: data.syncGroups,
241
- versions: data.versions || undefined,
241
+ // NOTE: old persisted records may still carry a `versions` key
242
+ // (the removed per-entity version vector) — tolerated by simply
243
+ // not picking it here.
242
244
  };
243
245
  resolve(meta);
244
246
  };
245
- request.onerror = () => reject(request.error);
247
+ request.onerror = () => { reject(request.error); };
246
248
  });
247
249
  }
248
250
  /**
@@ -253,9 +255,9 @@ export class DatabaseManager {
253
255
  const tx = db.transaction(['__meta'], 'readwrite');
254
256
  const store = tx.objectStore('__meta');
255
257
  const request = store.put(metadata, 'metadata');
256
- tx.oncomplete = () => resolve();
257
- tx.onerror = () => reject(tx.error);
258
- request.onerror = () => reject(request.error);
258
+ tx.oncomplete = () => { resolve(); };
259
+ tx.onerror = () => { reject(tx.error); };
260
+ request.onerror = () => { reject(request.error); };
259
261
  });
260
262
  }
261
263
  /**
@@ -270,7 +272,7 @@ export class DatabaseManager {
270
272
  const data = request.result;
271
273
  resolve(data?.persisted === true);
272
274
  };
273
- request.onerror = () => reject(request.error);
275
+ request.onerror = () => { reject(request.error); };
274
276
  });
275
277
  }
276
278
  /**
@@ -287,9 +289,9 @@ export class DatabaseManager {
287
289
  updatedAt: new Date().toISOString(),
288
290
  };
289
291
  const request = store.put(persistenceData, modelName);
290
- tx.oncomplete = () => resolve();
291
- tx.onerror = () => reject(tx.error);
292
- request.onerror = () => reject(request.error);
292
+ tx.oncomplete = () => { resolve(); };
293
+ tx.onerror = () => { reject(tx.error); };
294
+ request.onerror = () => { reject(request.error); };
293
295
  });
294
296
  }
295
297
  /**
@@ -312,7 +314,7 @@ export class DatabaseManager {
312
314
  }
313
315
  resolve(states);
314
316
  };
315
- request.onerror = () => reject(request.error);
317
+ request.onerror = () => { reject(request.error); };
316
318
  });
317
319
  }
318
320
  /**
@@ -362,10 +364,10 @@ export class DatabaseManager {
362
364
  const schemaVersions = {};
363
365
  for (const db of databases) {
364
366
  // Count by user
365
- databasesByUser[db.userId] = (databasesByUser[db.userId] || 0) + 1;
367
+ databasesByUser[db.userId] = (databasesByUser[db.userId] ?? 0) + 1;
366
368
  // Count schema versions
367
369
  const versionKey = `v${db.schemaVersion}`;
368
- schemaVersions[versionKey] = (schemaVersions[versionKey] || 0) + 1;
370
+ schemaVersions[versionKey] = (schemaVersions[versionKey] ?? 0) + 1;
369
371
  }
370
372
  resolve({
371
373
  metaDatabaseSize: databases.length,
@@ -374,7 +376,7 @@ export class DatabaseManager {
374
376
  schemaVersions,
375
377
  });
376
378
  };
377
- request.onerror = () => reject(request.error);
379
+ request.onerror = () => { reject(request.error); };
378
380
  });
379
381
  }
380
382
  /**
@@ -48,9 +48,8 @@ class BasicQueryCache {
48
48
  return;
49
49
  }
50
50
  // Fast path: Check if pattern is a simple model type match like ".*ModelType.*"
51
- const simpleMatch = pattern.match(/^\.\*(\w+)\.\*$/);
52
- if (simpleMatch) {
53
- const modelType = simpleMatch[1];
51
+ const modelType = /^\.\*(\w+)\.\*$/.exec(pattern)?.[1];
52
+ if (modelType !== undefined) {
54
53
  const keysToDelete = this.modelTypeIndex.get(modelType);
55
54
  if (keysToDelete) {
56
55
  for (const key of keysToDelete) {
@@ -88,8 +87,9 @@ class BasicQueryCache {
88
87
  * Cache key format: "operation:ModelType:options"
89
88
  */
90
89
  extractModelType(key) {
91
- const parts = key.split(':');
92
- return parts.length >= 2 ? parts[1] : null;
90
+ // `?? null` is equivalent to the old length check: a missing second
91
+ // segment reads as undefined, and split never yields undefined otherwise.
92
+ return key.split(':')[1] ?? null;
93
93
  }
94
94
  }
95
95
  export class QueryProcessor {
@@ -229,9 +229,8 @@ export class QueryProcessor {
229
229
  this.cache.invalidate(pattern);
230
230
  // Also invalidate predicate result cache for this model type
231
231
  if (pattern) {
232
- const simpleMatch = pattern.match(/^\.\*(\w+)\.\*$/);
233
- if (simpleMatch) {
234
- const modelType = simpleMatch[1];
232
+ const modelType = /^\.\*(\w+)\.\*$/.exec(pattern)?.[1];
233
+ if (modelType !== undefined) {
235
234
  for (const key of this.predicateResultCache.keys()) {
236
235
  if (key.includes(modelType)) {
237
236
  this.predicateResultCache.delete(key);
@@ -9,10 +9,22 @@
9
9
  * directly and receive granular updates (splice/push, never replacement).
10
10
  */
11
11
  import { type IObservableArray } from 'mobx';
12
+ import { type Model } from '../Model.js';
12
13
  import { ModelScope } from '../types/index.js';
13
- import type { ObjectPool } from '../ObjectPool.js';
14
14
  import type { ViewRegistry } from './ViewRegistry.js';
15
15
  import type { IncrementalView } from './query-utils.js';
16
+ /**
17
+ * The slice of `ObjectPool` a QueryView reads — a minimal structural
18
+ * interface (LogFoldContext-style) instead of the concrete class, so this
19
+ * module never imports `../ObjectPool.js` back (ObjectPool imports QueryView
20
+ * at runtime for `createView()`; the reverse edge was the last core-layer
21
+ * import cycle). `ObjectPool` satisfies it structurally.
22
+ */
23
+ export interface QueryViewPool {
24
+ hasForeignKeyIndex(typename: string, fieldName: string): boolean;
25
+ getByForeignKey(modelName: string, fieldName: string, fieldValue: string): Model[];
26
+ getByTypeName(modelName: string, scope?: ModelScope): Model[];
27
+ }
16
28
  export interface QueryViewOptions<T> {
17
29
  where?: Partial<T>;
18
30
  filter?: (entity: T) => boolean;
@@ -47,7 +59,7 @@ export declare class QueryView<T extends Record<string, unknown>> implements Inc
47
59
  private readonly fkField;
48
60
  private readonly fkValue;
49
61
  private disposed;
50
- constructor(typename: string, pool: ObjectPool, registry: ViewRegistry, options?: QueryViewOptions<T>);
62
+ constructor(typename: string, pool: QueryViewPool, registry: ViewRegistry, options?: QueryViewOptions<T>);
51
63
  private initialScan;
52
64
  handleAdded(model: Record<string, unknown>): void;
53
65
  handleUpdated(model: Record<string, unknown>): void;
@@ -54,10 +54,9 @@ export class QueryView {
54
54
  // Check for FK-index optimization: single-field where with an indexed FK
55
55
  this.fkField = null;
56
56
  this.fkValue = null;
57
- if (this.whereEntries &&
58
- this.whereEntries.length === 1 &&
59
- typeof this.whereEntries[0][1] === 'string') {
60
- const [field, value] = this.whereEntries[0];
57
+ const soleWhereEntry = this.whereEntries && this.whereEntries.length === 1 ? this.whereEntries[0] : undefined;
58
+ if (soleWhereEntry && typeof soleWhereEntry[1] === 'string') {
59
+ const [field, value] = soleWhereEntry;
61
60
  if (pool.hasForeignKeyIndex(typename, field)) {
62
61
  this.fkField = field;
63
62
  this.fkValue = value;
@@ -120,7 +119,7 @@ export class QueryView {
120
119
  if (this.disposed)
121
120
  return;
122
121
  const entity = model;
123
- const id = entity['id'];
122
+ const id = entity.id;
124
123
  const idx = id !== undefined ? this.findIndexById(id) : -1;
125
124
  const matchesNow = this.matchesFilter(entity);
126
125
  runInAction(() => {
@@ -120,12 +120,12 @@ export declare class StoreManager {
120
120
  notReady: number;
121
121
  };
122
122
  totalRecords: number;
123
- storeDetails: Array<{
123
+ storeDetails: {
124
124
  modelName: string;
125
125
  storeName: string;
126
126
  strategy: LoadStrategy;
127
127
  ready: boolean;
128
128
  count: number;
129
- }>;
129
+ }[];
130
130
  }>;
131
131
  }
@@ -5,6 +5,7 @@
5
5
  * Creates appropriate store types based on model load strategies.
6
6
  * Follows Linear's architecture with 80+ ObjectStore instances.
7
7
  */
8
+ import { ModelRegistry } from '../ModelRegistry.js';
8
9
  import { ObjectStore } from '../stores/ObjectStore.js';
9
10
  import { SyncActionStore } from '../stores/SyncActionStore.js';
10
11
  import { LoadStrategy } from '../types/index.js';
@@ -198,7 +199,10 @@ export class StoreManager {
198
199
  // Sample a few stores — don't check all 30+ if the first one has data
199
200
  const samplesToCheck = Math.min(storeEntries.length, 3);
200
201
  for (let i = 0; i < samplesToCheck; i++) {
201
- const [, store] = storeEntries[i];
202
+ const entry = storeEntries[i];
203
+ if (!entry)
204
+ break;
205
+ const [, store] = entry;
202
206
  try {
203
207
  const count = await store.count();
204
208
  if (count > 0)
@@ -20,7 +20,7 @@ export { LazyReferenceCollection, type LazyCollectionOptions, } from '../LazyRef
20
20
  export { ModelRegistry, getActiveRegistry, } from '../ModelRegistry.js';
21
21
  export { postQuery, type PostQueryOptions } from '../query/client.js';
22
22
  export { computeFKDepthPriority, type InternalAbloOptions } from '../client/Ablo.js';
23
- export type { SyncLogger, SyncObservabilityProvider, MutationExecutor, MutationDispatcher, SessionErrorDetector, OnlineStatusProvider, CommitResult, MutationOperation, } from '../interfaces/index.js';
23
+ export type { SyncLogger, SyncObservabilityProvider, MutationExecutor, SessionErrorDetector, OnlineStatusProvider, CommitResult, MutationOperation, } from '../interfaces/index.js';
24
24
  export { SyncWebSocket, type SyncDelta, type SyncWebSocketOptions, } from '../sync/SyncWebSocket.js';
25
25
  export { BootstrapHelper } from '../sync/BootstrapHelper.js';
26
26
  export { createClaimStream, type AttachableClaimStream, type ClaimStreamConfig, } from '../sync/createClaimStream.js';
@@ -95,23 +95,27 @@ export function openIDBWithTimeout(name, version, options = {}) {
95
95
  // A consumer reaction must never break the close.
96
96
  }
97
97
  };
98
- settle(() => resolve(db));
98
+ settle(() => { resolve(db); });
99
99
  };
100
- request.onerror = () => settle(() => reject(request.error));
100
+ request.onerror = () => { settle(() => { reject(request.error); }); };
101
101
  // The critical handler: another tab is blocking us. Native API leaves
102
102
  // the request pending indefinitely; we fail fast with a clear error so
103
103
  // the UI can tell the user to close other tabs.
104
104
  request.onblocked = () => {
105
- settle(() => reject(new IDBOpenTimeoutError(name, 'blocked', `IndexedDB \"${name}\" open blocked — another tab is holding an ` +
106
- `older version. Close other Ablo tabs and reload.`)));
105
+ settle(() => {
106
+ reject(new IDBOpenTimeoutError(name, 'blocked', `IndexedDB \"${name}\" open blocked another tab is holding an ` +
107
+ `older version. Close other Ablo tabs and reload.`));
108
+ });
107
109
  };
108
110
  // Catch-all timeout: even without `onblocked`, some browsers in some
109
111
  // storage states hang without firing any event. Bounded wait →
110
112
  // deterministic error.
111
113
  const timer = setTimeout(() => {
112
- settle(() => reject(new IDBOpenTimeoutError(name, 'timeout', `IndexedDB \"${name}\" open did not resolve within ${timeoutMs}ms. ` +
113
- `Storage may be in a bad state clearing site data and reloading ` +
114
- `usually fixes this.`)));
114
+ settle(() => {
115
+ reject(new IDBOpenTimeoutError(name, 'timeout', `IndexedDB \"${name}\" open did not resolve within ${timeoutMs}ms. ` +
116
+ `Storage may be in a bad state — clearing site data and reloading ` +
117
+ `usually fixes this.`));
118
+ });
115
119
  }, timeoutMs);
116
120
  });
117
121
  }
@@ -142,9 +146,9 @@ export function deleteIDBWithTimeout(name, timeoutMs = 5_000) {
142
146
  resolve(value);
143
147
  };
144
148
  const request = indexedDB.deleteDatabase(name);
145
- request.onsuccess = () => settle(true);
146
- request.onerror = () => settle(false);
147
- request.onblocked = () => settle(false);
148
- const timer = setTimeout(() => settle(false), timeoutMs);
149
+ request.onsuccess = () => { settle(true); };
150
+ request.onerror = () => { settle(false); };
151
+ request.onblocked = () => { settle(false); };
152
+ const timer = setTimeout(() => { settle(false); }, timeoutMs);
149
153
  });
150
154
  }
@@ -52,7 +52,7 @@ export function matchesWhere(entity, where) {
52
52
  */
53
53
  export function findIndexById(arr, id) {
54
54
  for (let i = 0; i < arr.length; i++) {
55
- if (arr[i]['id'] === id)
55
+ if (arr[i]?.id === id)
56
56
  return i;
57
57
  }
58
58
  return -1;
@@ -0,0 +1,143 @@
1
+ /**
2
+ * storeContract — the framework-neutral store contract.
3
+ *
4
+ * `SyncStoreContract` is the minimal store interface the SDK's hooks and
5
+ * mutators program against; `BaseSyncedStore` is the concrete engine class
6
+ * that implements it. The contract used to live in `react/context.ts`, which
7
+ * meant the CORE store layer imported its own contract from the React
8
+ * adapter (a module that runtime-imports 'react') — an L2-core →
9
+ * react-integration inversion and a module cycle. It now lives here, in a
10
+ * dependency-free core leaf: `react/context.ts` re-exports these types so
11
+ * React consumers are unchanged, and the core layer never touches 'react'.
12
+ *
13
+ * Everything in this module is type-only — no runtime imports, no runtime
14
+ * exports beyond erased interfaces.
15
+ */
16
+ import type { Model } from '../Model.js';
17
+ import type { ModelScope } from '../types/index.js';
18
+ import type { QueryView, QueryViewOptions } from './QueryView.js';
19
+ import type { ViewRegistry } from './ViewRegistry.js';
20
+ import type { ParticipantScope } from '../sync/participants.js';
21
+ /** Sync status for UI binding */
22
+ export interface SyncStatus {
23
+ state: 'idle' | 'syncing' | 'error' | 'offline' | 'reconnecting';
24
+ progress: number;
25
+ error?: Error;
26
+ /** When true, the error is a session/auth error requiring re-authentication. */
27
+ isSessionError: boolean;
28
+ lastSyncAt?: Date;
29
+ pendingChanges: number;
30
+ offlineSince?: Date;
31
+ }
32
+ /**
33
+ * A single LOCAL mutation as observed off the commit stream — the substrate
34
+ * the undo system records from. One is emitted per local create/update/
35
+ * delete/archive (remote/collaborator deltas never appear here: they apply
36
+ * through a separate pool path that doesn't queue mutations). `previousData`
37
+ * holds the pre-edit field values (captured from the model's
38
+ * `modifiedProperties` first-old-wins baseline), so an inverse op is fully
39
+ * derivable from the event alone — no separate snapshot pass.
40
+ *
41
+ * This mirrors how Yjs's `UndoManager` derives reverse-ops by observing the
42
+ * doc and Liveblocks' `room.history` records room ops: undo listens to the
43
+ * one place all local writes converge, rather than wrapping the write call.
44
+ */
45
+ export interface LocalMutation {
46
+ type: 'create' | 'update' | 'delete' | 'archive' | 'unarchive';
47
+ /** Registered model name (e.g. `'SlideLayer'`); resolved to a schema key by the recorder. */
48
+ modelName: string;
49
+ modelId: string;
50
+ /** New field values (create/update). */
51
+ data?: Record<string, unknown> | null;
52
+ /** Pre-edit field values (update → inverse patch; delete → full re-create row). */
53
+ previousData?: Record<string, unknown> | null;
54
+ }
55
+ /**
56
+ * Minimal store interface that the SDK hooks need.
57
+ * Consumers provide their concrete store (e.g., SyncedStore) that implements this.
58
+ */
59
+ export interface SyncStoreContract {
60
+ /**
61
+ * Subscribe to the LOCAL mutation stream (optimistic, pre-ack) for undo
62
+ * recording. Optional so minimal test doubles can omit it — when absent,
63
+ * undo scopes simply record nothing. The concrete store
64
+ * (`BaseSyncedStore`) wires this to the TransactionQueue's
65
+ * `transaction:created` event. Returns an unsubscribe function.
66
+ */
67
+ subscribeLocalMutations?(handler: (mutation: LocalMutation) => void): () => void;
68
+ retrieve(modelClass: abstract new (...args: never[]) => Model, id: string): Model | undefined;
69
+ queryByClass(modelClass: abstract new (...args: never[]) => Model, options?: {
70
+ predicate?: (model: Model) => boolean;
71
+ scope?: ModelScope;
72
+ orderBy?: keyof Model;
73
+ order?: 'asc' | 'desc';
74
+ limit?: number;
75
+ offset?: number;
76
+ }): {
77
+ data: Model[];
78
+ };
79
+ /**
80
+ * Save (create or update) one entity. Calling `save` in a tight loop
81
+ * produces a single wire commit with one `batchIndex`: the SyncClient
82
+ * debounces IDB persistence and the server push to one microtask, and
83
+ * TransactionQueue coalesces every transaction staged in the tick into
84
+ * one batch. There is intentionally no `saveMany` — Zero, Replicache,
85
+ * and the rest of the local-first lineage all expose one-row writes
86
+ * and rely on the implicit tick boundary.
87
+ *
88
+ * `skipValidation` exists for trusted bulk paths (AI sandbox layer
89
+ * generation, PPTX import, hydration) where the producer has already
90
+ * type-checked and per-row Zod is a measurable cost.
91
+ */
92
+ save(model: Model, options?: {
93
+ skipValidation?: boolean;
94
+ }): Promise<void>;
95
+ delete(model: Model): Promise<void>;
96
+ archive(model: Model): Promise<void>;
97
+ unarchive(model: Model): Promise<void>;
98
+ /** The ObjectPool — for entity/collection lookups by ID or typename. */
99
+ pool: {
100
+ get(id: string): Model | undefined;
101
+ getByTypeName(typename: string, scope?: ModelScope): Model[];
102
+ getByForeignKey(modelName: string, fieldName: string, fieldValue: string): Model[];
103
+ createFromData(data: Record<string, unknown>): Model | null;
104
+ hasForeignKeyIndex(typename: string, fieldName: string): boolean;
105
+ createView<T extends Record<string, unknown>>(typename: string, options?: QueryViewOptions<T>): QueryView<T>;
106
+ viewRegistry: ViewRegistry;
107
+ };
108
+ /**
109
+ * Reactive sync-status getters. Powered by MobX `computed` inside
110
+ * `BaseSyncedStore`, so they're safe to read in `observer` components
111
+ * and inside `reaction(() => store.isReady, ...)`. Consumers that
112
+ * don't want to touch MobX should prefer the `useSyncStatus()` hook.
113
+ */
114
+ readonly isReady: boolean;
115
+ readonly isSyncing: boolean;
116
+ readonly isOffline: boolean;
117
+ readonly isReconnecting: boolean;
118
+ readonly isError: boolean;
119
+ readonly hasUnsyncedChanges: boolean;
120
+ /**
121
+ * Area-of-interest (dynamic read subscription). `enterScope`/`leaveScope`
122
+ * move the connection's read interest as the user navigates (open/close a
123
+ * deck, sheet, doc); `pinScope`/`unpinScope` express prominence (an active
124
+ * claim keeps a group subscribed). Each resolves the scope through the same
125
+ * resolver the claim path uses, so read interest and write claims agree on
126
+ * the sync-group string. Optional so minimal test doubles can omit them;
127
+ * no-ops before the socket exists. The concrete store (`BaseSyncedStore`)
128
+ * forwards to its `AreaOfInterestManager`.
129
+ */
130
+ enterScope?(scope: ParticipantScope, opts?: {
131
+ hydrate?: boolean;
132
+ }): Promise<void>;
133
+ leaveScope?(scope: ParticipantScope): Promise<void>;
134
+ pinScope?(scope: ParticipantScope): Promise<void>;
135
+ unpinScope?(scope: ParticipantScope): Promise<void>;
136
+ /**
137
+ * Raw MobX-observable `SyncStatus` record. `useSyncStatus()` reads
138
+ * `state`, `progress`, `pendingChanges`, `isSessionError`, `error`
139
+ * from this to build its tagged union. Exposed on the contract so
140
+ * consumer-facing hooks and test doubles can manipulate it directly.
141
+ */
142
+ readonly syncStatus: SyncStatus;
143
+ }