@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.
Files changed (246) hide show
  1. package/AGENTS.md +5 -3
  2. package/CHANGELOG.md +33 -0
  3. package/README.md +45 -36
  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 +583 -287
  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/data-sources.md +12 -5
  220. package/docs/examples/existing-python-backend.md +3 -3
  221. package/docs/identity.md +4 -4
  222. package/docs/integration-guide.md +1 -1
  223. package/docs/react.md +1 -1
  224. package/docs/sessions.md +5 -7
  225. package/llms.txt +4 -2
  226. package/package.json +15 -13
  227. package/dist/client/index.d.ts +0 -36
  228. package/dist/client/index.js +0 -33
  229. package/dist/config/index.d.ts +0 -10
  230. package/dist/config/index.js +0 -12
  231. package/dist/interfaces/headless.d.ts +0 -95
  232. package/dist/interfaces/headless.js +0 -41
  233. package/dist/query/index.d.ts +0 -6
  234. package/dist/query/index.js +0 -5
  235. package/dist/realtime/index.d.ts +0 -10
  236. package/dist/realtime/index.js +0 -9
  237. package/dist/schema/plane.d.ts +0 -23
  238. package/dist/schema/plane.js +0 -19
  239. package/dist/server/next.d.ts +0 -51
  240. package/dist/server/next.js +0 -47
  241. package/dist/sync/OfflineFlush.d.ts +0 -9
  242. package/dist/sync/OfflineFlush.js +0 -22
  243. package/dist/sync/OfflineTransactionStore.d.ts +0 -37
  244. package/dist/sync/OfflineTransactionStore.js +0 -263
  245. package/dist/transactions/index.d.ts +0 -16
  246. package/dist/transactions/index.js +0 -7
@@ -0,0 +1,223 @@
1
+ /**
2
+ * Delta-confirmation tracking — the queue's "did my write's echo arrive?"
3
+ * machinery, lifted out of `TransactionQueue.ts` as a stateful leaf.
4
+ *
5
+ * Owns the ack watermark (via the shared `SyncPosition`), the per-transaction
6
+ * confirmation timeout map, and the retry-with-backoff/reconciliation policy
7
+ * for `awaiting_delta` transactions. Talks back to the queue through the
8
+ * minimal `DeltaConfirmationContext` interface (never the host class type),
9
+ * so the leaf stays cycle-free and testable in isolation.
10
+ */
11
+ import { getContext } from '../context.js';
12
+ export class DeltaConfirmationTracker {
13
+ ctx;
14
+ // Delta confirmation retry config (Replicache-style exponential backoff)
15
+ // Max retries before requesting full reconciliation
16
+ static DELTA_MAX_RETRIES = 5;
17
+ // Initial timeout (first attempt)
18
+ static DELTA_INITIAL_TIMEOUT_MS = 30_000;
19
+ // Max timeout cap (like Replicache's maxDelayMs of 60s)
20
+ static DELTA_MAX_TIMEOUT_MS = 120_000;
21
+ // LINEAR PATTERN: Track delta confirmation timeouts for awaiting_delta transactions
22
+ // Following Replicache/PowerSync pattern: retry with backoff instead of rolling back
23
+ deltaConfirmationTimeouts = new Map();
24
+ // Track retry attempts per transaction for exponential backoff
25
+ deltaConfirmationRetries = new Map();
26
+ constructor(ctx) {
27
+ this.ctx = ctx;
28
+ }
29
+ /** Applied-cursor alias, kept so the read sites below stay legible. */
30
+ get lastSeenSyncId() {
31
+ return this.ctx.position.applied;
32
+ }
33
+ noteAck(lastSyncId) {
34
+ this.ctx.position.noteAck(lastSyncId);
35
+ }
36
+ /**
37
+ * LINEAR PATTERN: Confirm all awaiting transactions when delta with syncId >= threshold arrives.
38
+ * This replaces clientMutationId echoing - transactions are confirmed by sync ID threshold.
39
+ * @param syncId - The sync ID of the received delta
40
+ */
41
+ onDeltaReceived(syncId) {
42
+ // Cursor advancing happens where the delta is APPLIED (the store calls
43
+ // position.advanceApplied / advancePersisted); this hook only resolves
44
+ // confirmation thresholds against the incoming id.
45
+ const awaitingTxs = this.ctx.store.getByStatus('awaiting_delta');
46
+ const executingTxs = this.ctx.store.getByStatus('executing');
47
+ // Debug: Show state when delta arrives
48
+ if (awaitingTxs.length > 0 || executingTxs.length > 0) {
49
+ getContext().logger.debug('tx:delta_received', {
50
+ syncId,
51
+ lastSeenSyncId: this.lastSeenSyncId,
52
+ awaitingCount: awaitingTxs.length,
53
+ executingCount: executingTxs.length,
54
+ awaitingThresholds: awaitingTxs.map((tx) => ({
55
+ txId: tx.id.slice(0, 8),
56
+ model: tx.modelName,
57
+ needed: tx.syncIdNeededForCompletion,
58
+ willConfirm: tx.syncIdNeededForCompletion !== undefined && syncId >= tx.syncIdNeededForCompletion,
59
+ })),
60
+ });
61
+ }
62
+ // Fast path: no awaiting transactions
63
+ if (awaitingTxs.length === 0)
64
+ return;
65
+ let confirmedCount = 0;
66
+ for (const tx of awaitingTxs) {
67
+ // Confirm if this delta's ID meets or exceeds the threshold
68
+ if (tx.syncIdNeededForCompletion !== undefined && syncId >= tx.syncIdNeededForCompletion) {
69
+ this.cancelDeltaConfirmationTimeout(tx.id);
70
+ this.ctx.store.updateStatus(tx.id, 'completed');
71
+ this.ctx.emit('transaction:completed', tx);
72
+ this.ctx.emit(`transaction:completed:${tx.id}`, tx);
73
+ this.ctx.optimisticUpdates.delete(tx.id);
74
+ confirmedCount++;
75
+ getContext().logger.debug('tx:confirm_via_delta', {
76
+ txId: tx.id.slice(0, 8),
77
+ model: tx.modelName,
78
+ neededSyncId: tx.syncIdNeededForCompletion,
79
+ receivedSyncId: syncId,
80
+ });
81
+ }
82
+ }
83
+ // Log batch summary only if we confirmed something
84
+ if (confirmedCount > 0) {
85
+ // Use warn for staging visibility when transactions confirm
86
+ getContext().observability.breadcrumb('Transactions confirmed via delta', 'sync.transaction', 'info', {
87
+ count: confirmedCount,
88
+ syncId,
89
+ remainingAwaiting: awaitingTxs.length - confirmedCount,
90
+ });
91
+ }
92
+ }
93
+ // REPLICACHE/POWERSYNC PATTERN: Schedule delta confirmation with retry + reconciliation
94
+ // Instead of rolling back on timeout (which destroys confirmed server state),
95
+ // retry with exponential backoff and request reconciliation to catch up on missed deltas.
96
+ // Only rollback on explicit server rejection, never on timeout.
97
+ scheduleDeltaConfirmationTimeout(tx, timeoutMs) {
98
+ // Cancel any existing timeout for this transaction
99
+ this.cancelDeltaConfirmationTimeout(tx.id);
100
+ // NB: deliberately NOT an async callback — the body is fully synchronous,
101
+ // and `setTimeout(async …)` turns any throw into an unhandled promise
102
+ // rejection instead of a catchable synchronous error.
103
+ const timeoutHandle = setTimeout(() => {
104
+ const currentTx = this.ctx.store.get(tx.id);
105
+ if (!currentTx || currentTx.status !== 'awaiting_delta') {
106
+ this.deltaConfirmationRetries.delete(tx.id);
107
+ return; // Already confirmed or failed
108
+ }
109
+ // If disconnected, re-schedule with same timeout (no backoff while offline)
110
+ if (!this.ctx.isConnected()) {
111
+ // Self-healing: re-schedule the confirmation wait while offline, no
112
+ // consumer action needed → debug.
113
+ getContext().logger.debug('[TransactionQueue] Timeout fired while disconnected - re-scheduling', {
114
+ txId: tx.id.slice(0, 8),
115
+ model: tx.modelName,
116
+ });
117
+ this.deltaConfirmationTimeouts.delete(tx.id);
118
+ this.scheduleDeltaConfirmationTimeout(tx, timeoutMs);
119
+ return;
120
+ }
121
+ const retryCount = this.deltaConfirmationRetries.get(tx.id) ?? 0;
122
+ const diagnosis = this.lastSeenSyncId === 0
123
+ ? 'No deltas received - delta pipeline may be broken'
124
+ : currentTx.syncIdNeededForCompletion &&
125
+ this.lastSeenSyncId < currentTx.syncIdNeededForCompletion
126
+ ? 'Delta not yet received - may be lost or delayed'
127
+ : 'Delta should have confirmed - possible race condition';
128
+ getContext().observability.captureReconciliation({
129
+ reason: 'delta_timeout',
130
+ model: tx.modelName,
131
+ modelId: tx.modelId,
132
+ syncIdNeeded: currentTx.syncIdNeededForCompletion,
133
+ lastSeenSyncId: this.lastSeenSyncId,
134
+ retryCount,
135
+ connectionState: this.ctx.isConnected() ? 'connected' : 'disconnected',
136
+ });
137
+ if (retryCount < DeltaConfirmationTracker.DELTA_MAX_RETRIES) {
138
+ // RETRY: Request reconciliation and re-schedule with exponential backoff
139
+ // The server already committed this mutation — we just need the delta to arrive
140
+ this.deltaConfirmationRetries.set(tx.id, retryCount + 1);
141
+ this.deltaConfirmationTimeouts.delete(tx.id);
142
+ // Exponential backoff: 30s → 60s → 120s → 120s → 120s (capped)
143
+ const nextTimeout = Math.min(timeoutMs * 2, DeltaConfirmationTracker.DELTA_MAX_TIMEOUT_MS);
144
+ // Emit reconciliation request so SyncedStore can cycle the WebSocket
145
+ // to trigger delta catch-up from the server
146
+ this.ctx.emit('reconciliation:needed', {
147
+ reason: 'delta_confirmation_timeout',
148
+ txId: tx.id,
149
+ model: tx.modelName,
150
+ modelId: tx.modelId,
151
+ syncIdNeeded: currentTx.syncIdNeededForCompletion,
152
+ lastSeenSyncId: this.lastSeenSyncId,
153
+ retryCount: retryCount + 1,
154
+ });
155
+ // Self-healing retry with backoff — the server already committed; we're
156
+ // just waiting on the delta. No consumer action → debug.
157
+ getContext().logger.debug('[TransactionQueue] Re-scheduling with backoff', {
158
+ txId: tx.id.slice(0, 8),
159
+ model: tx.modelName,
160
+ nextTimeoutMs: nextTimeout,
161
+ retry: retryCount + 1,
162
+ });
163
+ this.scheduleDeltaConfirmationTimeout(tx, nextTimeout);
164
+ }
165
+ else {
166
+ // LINEAR PATTERN: Retries exhausted — persist to IndexedDB instead of rolling back.
167
+ // The transaction succeeded on the server (HTTP 200), so the data exists server-side.
168
+ // Persist the awaiting state so it survives tab close. On next session, the WebSocket
169
+ // reconnect + delta catch-up will naturally confirm it (like Linear's IndexedDB caching).
170
+ this.deltaConfirmationRetries.delete(tx.id);
171
+ this.deltaConfirmationTimeouts.delete(tx.id);
172
+ getContext().observability.captureDeltaRetryExhausted({
173
+ txId: tx.id,
174
+ model: tx.modelName,
175
+ modelId: tx.modelId,
176
+ retryCount: DeltaConfirmationTracker.DELTA_MAX_RETRIES,
177
+ syncIdNeeded: currentTx.syncIdNeededForCompletion,
178
+ });
179
+ // Emit persist event — SyncClient handles the IDB write
180
+ this.ctx.emit('transaction:persist_awaiting', {
181
+ txId: tx.id,
182
+ model: tx.modelName,
183
+ modelId: tx.modelId,
184
+ operationType: tx.type,
185
+ syncIdNeeded: currentTx.syncIdNeededForCompletion,
186
+ });
187
+ // Also request one final reconciliation cycle
188
+ this.ctx.emit('reconciliation:needed', {
189
+ reason: 'delta_retries_exhausted',
190
+ txId: tx.id,
191
+ model: tx.modelName,
192
+ modelId: tx.modelId,
193
+ syncIdNeeded: currentTx.syncIdNeededForCompletion,
194
+ lastSeenSyncId: this.lastSeenSyncId,
195
+ retryCount: DeltaConfirmationTracker.DELTA_MAX_RETRIES,
196
+ });
197
+ }
198
+ }, timeoutMs);
199
+ this.deltaConfirmationTimeouts.set(tx.id, timeoutHandle);
200
+ }
201
+ // Cancel a pending delta confirmation timeout and clean up retry tracking
202
+ cancelDeltaConfirmationTimeout(id) {
203
+ const timeoutHandle = this.deltaConfirmationTimeouts.get(id);
204
+ if (timeoutHandle) {
205
+ clearTimeout(timeoutHandle);
206
+ this.deltaConfirmationTimeouts.delete(id);
207
+ }
208
+ this.deltaConfirmationRetries.delete(id);
209
+ }
210
+ /**
211
+ * Tear down every armed confirmation timer (30–120s each, one per
212
+ * in-flight transaction). Called from `TransactionQueue.dispose()` —
213
+ * without it a disposed queue kept the Node process alive and fired
214
+ * callbacks against an already-cleared store (T1.19).
215
+ */
216
+ dispose() {
217
+ for (const timeoutHandle of this.deltaConfirmationTimeouts.values()) {
218
+ clearTimeout(timeoutHandle);
219
+ }
220
+ this.deltaConfirmationTimeouts.clear();
221
+ this.deltaConfirmationRetries.clear();
222
+ }
223
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Optimistic apply/rollback — the queue's local-first bookkeeping, lifted out
3
+ * of `TransactionQueue.ts` as a pure leaf. The queue never mutates the pool
4
+ * itself: applying/rolling back records an entry in the host-owned ledger and
5
+ * emits `optimistic:*` events that the pool-side listeners act on. These
6
+ * functions take that ledger plus a minimal emitter interface (never the host
7
+ * class type), so the rules stay cycle-free and testable in isolation.
8
+ */
9
+ import type { Model } from '../Model.js';
10
+ import type { MutationInput, Transaction } from './commitPayload.js';
11
+ /** One tracked optimistic mutation: the live model + its pre-image. */
12
+ export interface OptimisticUpdateEntry {
13
+ model: Model;
14
+ previousState: MutationInput | null | undefined;
15
+ transaction: Transaction;
16
+ }
17
+ /** The host's event surface — the only queue capability these rules need. */
18
+ export interface OptimisticEmitter {
19
+ emit(event: string, payload: unknown): void;
20
+ }
21
+ export declare function applyOptimisticCreate(optimisticUpdates: Map<string, OptimisticUpdateEntry>, emitter: OptimisticEmitter, model: Model, transaction: Transaction): void;
22
+ export declare function applyOptimisticUpdate(optimisticUpdates: Map<string, OptimisticUpdateEntry>, emitter: OptimisticEmitter, model: Model, transaction: Transaction): void;
23
+ export declare function applyOptimisticDelete(optimisticUpdates: Map<string, OptimisticUpdateEntry>, emitter: OptimisticEmitter, model: Model, transaction: Transaction): void;
24
+ export declare function rollbackOptimistic(optimisticUpdates: Map<string, OptimisticUpdateEntry>, emitter: OptimisticEmitter, transaction: Transaction, reason?: string, error?: Error): Promise<void>;
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Optimistic apply/rollback — the queue's local-first bookkeeping, lifted out
3
+ * of `TransactionQueue.ts` as a pure leaf. The queue never mutates the pool
4
+ * itself: applying/rolling back records an entry in the host-owned ledger and
5
+ * emits `optimistic:*` events that the pool-side listeners act on. These
6
+ * functions take that ledger plus a minimal emitter interface (never the host
7
+ * class type), so the rules stay cycle-free and testable in isolation.
8
+ */
9
+ export function applyOptimisticCreate(optimisticUpdates, emitter, model, transaction) {
10
+ optimisticUpdates.set(transaction.id, {
11
+ model,
12
+ previousState: null,
13
+ transaction,
14
+ });
15
+ emitter.emit('optimistic:create', { model, transaction });
16
+ }
17
+ export function applyOptimisticUpdate(optimisticUpdates, emitter, model, transaction) {
18
+ optimisticUpdates.set(transaction.id, {
19
+ model,
20
+ previousState: transaction.previousData,
21
+ transaction,
22
+ });
23
+ emitter.emit('optimistic:update', { model, transaction });
24
+ }
25
+ export function applyOptimisticDelete(optimisticUpdates, emitter, model, transaction) {
26
+ optimisticUpdates.set(transaction.id, {
27
+ model,
28
+ previousState: transaction.previousData,
29
+ transaction,
30
+ });
31
+ emitter.emit('optimistic:delete', { model, transaction });
32
+ }
33
+ export async function rollbackOptimistic(optimisticUpdates, emitter, transaction, reason, error) {
34
+ const optimistic = optimisticUpdates.get(transaction.id);
35
+ if (!optimistic)
36
+ return;
37
+ emitter.emit('optimistic:rollback', {
38
+ model: optimistic.model,
39
+ previousState: optimistic.previousState,
40
+ transaction,
41
+ reason: reason ?? 'unknown',
42
+ error,
43
+ });
44
+ optimisticUpdates.delete(transaction.id);
45
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * persistedReplay — the Zod boundary for IDB → commit replay (T1.8).
3
+ *
4
+ * Rows read back from the persisted-transaction store were written by a
5
+ * PREVIOUS session — possibly by an older SDK version, possibly corrupted.
6
+ * Spreading them straight into a `Transaction` re-entered the commit path
7
+ * with zero validation. These schemas validate exactly the fields the queue
8
+ * (and the offline mutation-queue restore) actually read on replay; rows
9
+ * that don't parse are DROPPED (and surfaced through observability), never
10
+ * replayed as garbage commits.
11
+ *
12
+ * The same IDB store also holds two NON-replayable row kinds written by
13
+ * other subsystems — the `'queue'` record (`SyncClient.persistMutationQueue`)
14
+ * and `'awaiting_delta'` markers (`setupAwaitingTransactionPersistence`).
15
+ * Those are recognized and skipped silently: they are expected neighbors,
16
+ * not corruption.
17
+ */
18
+ import { z } from 'zod';
19
+ import type { Transaction } from './commitPayload.js';
20
+ /**
21
+ * A replayable persisted transaction — the fields `TransactionQueue`
22
+ * actually reads when re-enqueueing (id/type/model addressing + payload +
23
+ * identity context). Everything else is defaulted at rehydration time.
24
+ */
25
+ export declare const persistedTransactionSchema: z.ZodObject<{
26
+ id: z.ZodString;
27
+ type: z.ZodEnum<{
28
+ update: "update";
29
+ create: "create";
30
+ delete: "delete";
31
+ archive: "archive";
32
+ unarchive: "unarchive";
33
+ }>;
34
+ modelName: z.ZodString;
35
+ modelId: z.ZodString;
36
+ modelKey: z.ZodOptional<z.ZodString>;
37
+ data: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
38
+ previousData: z.ZodOptional<z.ZodNullable<z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
39
+ context: z.ZodObject<{
40
+ userId: z.ZodString;
41
+ organizationId: z.ZodString;
42
+ role: z.ZodOptional<z.ZodString>;
43
+ teamIds: z.ZodOptional<z.ZodArray<z.ZodString>>;
44
+ }, z.core.$strip>;
45
+ createdAt: z.ZodOptional<z.ZodNumber>;
46
+ batchId: z.ZodOptional<z.ZodString>;
47
+ writeOptions: z.ZodOptional<z.ZodObject<{
48
+ readAt: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
49
+ onStale: z.ZodOptional<z.ZodNullable<z.ZodEnum<{
50
+ reject: "reject";
51
+ overwrite: "overwrite";
52
+ notify: "notify";
53
+ }>>>;
54
+ idempotencyKey: z.ZodOptional<z.ZodString>;
55
+ label: z.ZodOptional<z.ZodString>;
56
+ }, z.core.$loose>>;
57
+ localOnly: z.ZodOptional<z.ZodBoolean>;
58
+ }, z.core.$loose>;
59
+ export type PersistedReplayableTransaction = z.infer<typeof persistedTransactionSchema>;
60
+ /** Whether a persisted row belongs to another subsystem (skip, don't flag). */
61
+ export declare function isNonReplayablePersistedRow(row: unknown): boolean;
62
+ /**
63
+ * Validate + rehydrate one persisted row into a replayable `Transaction`,
64
+ * or `null` when the row doesn't parse. Missing bookkeeping fields are
65
+ * re-derived exactly like a fresh staging would derive them.
66
+ */
67
+ export declare function deserializePersistedTransaction(row: unknown): Transaction | null;
68
+ /**
69
+ * One entry of the persisted offline mutation queue (the `'queue'` row's
70
+ * `mutations` array) — the fields `SyncClient.restoreMutationQueue` reads.
71
+ */
72
+ export declare const persistedMutationSchema: z.ZodObject<{
73
+ type: z.ZodEnum<{
74
+ update: "update";
75
+ create: "create";
76
+ delete: "delete";
77
+ archive: "archive";
78
+ }>;
79
+ modelData: z.ZodRecord<z.ZodString, z.ZodUnknown>;
80
+ modelName: z.ZodString;
81
+ timestamp: z.ZodString;
82
+ writeOptions: z.ZodOptional<z.ZodObject<{
83
+ readAt: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
84
+ onStale: z.ZodOptional<z.ZodNullable<z.ZodEnum<{
85
+ reject: "reject";
86
+ overwrite: "overwrite";
87
+ notify: "notify";
88
+ }>>>;
89
+ idempotencyKey: z.ZodOptional<z.ZodString>;
90
+ label: z.ZodOptional<z.ZodString>;
91
+ }, z.core.$loose>>;
92
+ }, z.core.$loose>;
93
+ export type PersistedQueuedMutation = z.infer<typeof persistedMutationSchema>;
@@ -0,0 +1,105 @@
1
+ /**
2
+ * persistedReplay — the Zod boundary for IDB → commit replay (T1.8).
3
+ *
4
+ * Rows read back from the persisted-transaction store were written by a
5
+ * PREVIOUS session — possibly by an older SDK version, possibly corrupted.
6
+ * Spreading them straight into a `Transaction` re-entered the commit path
7
+ * with zero validation. These schemas validate exactly the fields the queue
8
+ * (and the offline mutation-queue restore) actually read on replay; rows
9
+ * that don't parse are DROPPED (and surfaced through observability), never
10
+ * replayed as garbage commits.
11
+ *
12
+ * The same IDB store also holds two NON-replayable row kinds written by
13
+ * other subsystems — the `'queue'` record (`SyncClient.persistMutationQueue`)
14
+ * and `'awaiting_delta'` markers (`setupAwaitingTransactionPersistence`).
15
+ * Those are recognized and skipped silently: they are expected neighbors,
16
+ * not corruption.
17
+ */
18
+ import { z } from 'zod';
19
+ import { computePriorityScore, normalizeModelKey } from './commitPayload.js';
20
+ /** Write-option subset persisted per transaction / mutation. */
21
+ const persistedWriteOptionsSchema = z
22
+ .object({
23
+ readAt: z.number().nullable().optional(),
24
+ onStale: z.enum(['reject', 'overwrite', 'notify']).nullable().optional(),
25
+ idempotencyKey: z.string().optional(),
26
+ label: z.string().optional(),
27
+ })
28
+ .passthrough();
29
+ /**
30
+ * A replayable persisted transaction — the fields `TransactionQueue`
31
+ * actually reads when re-enqueueing (id/type/model addressing + payload +
32
+ * identity context). Everything else is defaulted at rehydration time.
33
+ */
34
+ export const persistedTransactionSchema = z
35
+ .object({
36
+ id: z.string().min(1),
37
+ type: z.enum(['create', 'update', 'delete', 'archive', 'unarchive']),
38
+ modelName: z.string().min(1),
39
+ modelId: z.string().min(1),
40
+ modelKey: z.string().min(1).optional(),
41
+ data: z.record(z.string(), z.unknown()).optional(),
42
+ previousData: z.record(z.string(), z.unknown()).nullable().optional(),
43
+ context: z.object({
44
+ userId: z.string().min(1),
45
+ organizationId: z.string().min(1),
46
+ role: z.string().optional(),
47
+ teamIds: z.array(z.string()).optional(),
48
+ }),
49
+ createdAt: z.number().optional(),
50
+ batchId: z.string().optional(),
51
+ writeOptions: persistedWriteOptionsSchema.optional(),
52
+ localOnly: z.boolean().optional(),
53
+ })
54
+ .passthrough();
55
+ /** Row kinds that share the store but are owned by other subsystems. */
56
+ const NON_REPLAYABLE_TYPES = new Set(['queue', 'awaiting_delta']);
57
+ /** Whether a persisted row belongs to another subsystem (skip, don't flag). */
58
+ export function isNonReplayablePersistedRow(row) {
59
+ return (typeof row === 'object' &&
60
+ row !== null &&
61
+ typeof row.type === 'string' &&
62
+ NON_REPLAYABLE_TYPES.has(row.type));
63
+ }
64
+ /**
65
+ * Validate + rehydrate one persisted row into a replayable `Transaction`,
66
+ * or `null` when the row doesn't parse. Missing bookkeeping fields are
67
+ * re-derived exactly like a fresh staging would derive them.
68
+ */
69
+ export function deserializePersistedTransaction(row) {
70
+ const parsed = persistedTransactionSchema.safeParse(row);
71
+ if (!parsed.success)
72
+ return null;
73
+ const tx = parsed.data;
74
+ return {
75
+ id: tx.id,
76
+ type: tx.type,
77
+ modelName: tx.modelName,
78
+ modelId: tx.modelId,
79
+ modelKey: tx.modelKey ?? normalizeModelKey(tx.modelName),
80
+ ...(tx.data !== undefined ? { data: tx.data } : {}),
81
+ ...(tx.previousData !== undefined ? { previousData: tx.previousData } : {}),
82
+ context: tx.context,
83
+ status: 'pending',
84
+ createdAt: tx.createdAt ?? Date.now(),
85
+ attempts: 0,
86
+ priority: 'normal',
87
+ priorityScore: computePriorityScore(tx.type, tx.modelName),
88
+ ...(tx.batchId !== undefined ? { batchId: tx.batchId } : {}),
89
+ ...(tx.writeOptions !== undefined ? { writeOptions: tx.writeOptions } : {}),
90
+ ...(tx.localOnly !== undefined ? { localOnly: tx.localOnly } : {}),
91
+ };
92
+ }
93
+ /**
94
+ * One entry of the persisted offline mutation queue (the `'queue'` row's
95
+ * `mutations` array) — the fields `SyncClient.restoreMutationQueue` reads.
96
+ */
97
+ export const persistedMutationSchema = z
98
+ .object({
99
+ type: z.enum(['create', 'update', 'delete', 'archive']),
100
+ modelData: z.record(z.string(), z.unknown()),
101
+ modelName: z.string().min(1),
102
+ timestamp: z.string(),
103
+ writeOptions: persistedWriteOptionsSchema.optional(),
104
+ })
105
+ .passthrough();
@@ -129,10 +129,10 @@ export interface ModelMetadata {
129
129
  * product-neutral: the engine no longer hardcodes which models carry
130
130
  * `organizationId` / `createdBy` — the consumer's schema declares it.
131
131
  */
132
- autoFill?: ReadonlyArray<{
132
+ autoFill?: readonly {
133
133
  field: string;
134
134
  from: 'organizationId' | 'userId';
135
- }>;
135
+ }[];
136
136
  /**
137
137
  * Fields whose absence makes a stored row orphaned. When healing
138
138
  * encounters a record missing any of these fields, it returns `null`
@@ -240,4 +240,4 @@ export interface PartialIndexInfo {
240
240
  depth: number;
241
241
  path: string[];
242
242
  }
243
- export * from "./streams.js";
243
+ export type { TargetRange, OnStaleMode, WireClaim, ClaimRejection, PresenceKind, ParticipantKind, ParticipantRef, JsonValue, ConfirmationState, AgentDelta, Snapshot, ContextChange, ClaimTarget, PresenceTarget, PresenceStream, Activity, Peer, PresenceUpdatePayload, ClaimLeaseOptions, Duration, ClaimOptions, ClaimStream, ClaimLost, ClaimStatus, ClaimWaitOptions, Claim, HeldClaim, } from './streams.js';
@@ -63,8 +63,3 @@ export var MutationOperationType;
63
63
  MutationOperationType["UNARCHIVE"] = "UNARCHIVE";
64
64
  MutationOperationType["UPDATE"] = "UPDATE";
65
65
  })(MutationOperationType || (MutationOperationType = {}));
66
- // Re-export stream + snapshot + principal types for the engine surface
67
- // (PresenceStream,
68
- // ClaimStream, Snapshot, etc.) consumed by `Ablo({...}).presence`,
69
- // `.claims`, `.snapshot()`.
70
- export * from "./streams.js";
@@ -0,0 +1,12 @@
1
+ /**
2
+ * ModelData — the generic record shape for model payloads.
3
+ *
4
+ * Lives in its own dependency-free leaf so BOTH `BaseSyncedStore` and
5
+ * `SyncClient` can import it without importing each other. (The alias
6
+ * used to live in BaseSyncedStore.ts, which made SyncClient.ts and
7
+ * BaseSyncedStore.ts a mutual type cycle: the store needs the SyncClient
8
+ * class type, and SyncClient needed this alias back from the store.)
9
+ * `BaseSyncedStore` re-exports it, so existing importers are unchanged.
10
+ */
11
+ /** Generic record type for model data */
12
+ export type ModelData = Record<string, unknown>;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * ModelData — the generic record shape for model payloads.
3
+ *
4
+ * Lives in its own dependency-free leaf so BOTH `BaseSyncedStore` and
5
+ * `SyncClient` can import it without importing each other. (The alias
6
+ * used to live in BaseSyncedStore.ts, which made SyncClient.ts and
7
+ * BaseSyncedStore.ts a mutual type cycle: the store needs the SyncClient
8
+ * class type, and SyncClient needed this alias back from the store.)
9
+ * `BaseSyncedStore` re-exports it, so existing importers are unchanged.
10
+ */
11
+ export {};
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Participant identity — a zero-import leaf of the type graph.
3
+ *
4
+ * `ParticipantRef` (and the participant-kind literals it carries) is the one
5
+ * identity shape shared by the streams surface (`types/streams.ts`) and the
6
+ * conflict-policy layer (`policy/types.ts`). It lives in its own leaf module —
7
+ * importing nothing — so both layers can depend on it without forming the
8
+ * foundation triangle `types/streams → schema/model → policy/types →
9
+ * types/streams`. The runtime validator for the same literals stays in
10
+ * `coordination/schema.ts` (`participantKindSchema`).
11
+ */
12
+ /** The participant-kind vocabulary: human session, AI agent, or system actor. */
13
+ export type ParticipantKind = 'user' | 'agent' | 'system';
14
+ /**
15
+ * Identity reference for an actor / on-behalf-of slot. Generic
16
+ * protocol vocabulary; works for sessions, agents, and system roles.
17
+ */
18
+ export interface ParticipantRef {
19
+ kind: ParticipantKind;
20
+ id: string;
21
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Participant identity — a zero-import leaf of the type graph.
3
+ *
4
+ * `ParticipantRef` (and the participant-kind literals it carries) is the one
5
+ * identity shape shared by the streams surface (`types/streams.ts`) and the
6
+ * conflict-policy layer (`policy/types.ts`). It lives in its own leaf module —
7
+ * importing nothing — so both layers can depend on it without forming the
8
+ * foundation triangle `types/streams → schema/model → policy/types →
9
+ * types/streams`. The runtime validator for the same literals stays in
10
+ * `coordination/schema.ts` (`participantKindSchema`).
11
+ */
12
+ export {};
@@ -19,14 +19,8 @@ export type { TargetRange, OnStaleMode, WireClaim, ClaimRejection, PresenceKind,
19
19
  export type JsonValue = string | number | boolean | null | readonly JsonValue[] | {
20
20
  readonly [key: string]: JsonValue;
21
21
  };
22
- /**
23
- * Identity reference for an actor / on-behalf-of slot. Generic
24
- * protocol vocabulary; works for sessions, agents, and system roles.
25
- */
26
- export interface ParticipantRef {
27
- kind: 'user' | 'agent' | 'system';
28
- id: string;
29
- }
22
+ import type { ParticipantRef } from './participant.js';
23
+ export type { ParticipantRef } from './participant.js';
30
24
  /**
31
25
  * Whether the human explicitly approved a change. Open-source
32
26
  * consumers that don't track approval keep `auto` as the default.
@@ -156,9 +150,9 @@ export interface PresenceStream {
156
150
  * agents are doing right now: "copy-bot is generating a new title
157
151
  * for slide 5; don't duplicate that work."
158
152
  */
159
- readonly others: ReadonlyArray<Peer>;
153
+ readonly others: readonly Peer[];
160
154
  /** Subset of `others` filtered to a specific sync group. */
161
- othersIn(syncGroup: string): ReadonlyArray<Peer>;
155
+ othersIn(syncGroup: string): readonly Peer[];
162
156
  /**
163
157
  * Framework-agnostic reactivity primitive. Register a callback that
164
158
  * fires every time `others` / `othersIn(...)` content changes (a
@@ -202,7 +196,7 @@ export interface PresenceStream {
202
196
  * down the underlying subscription cleanly via the iterator's
203
197
  * `return()` hook.
204
198
  */
205
- [Symbol.asyncIterator](): AsyncIterableIterator<ReadonlyArray<Peer>>;
199
+ [Symbol.asyncIterator](): AsyncIterableIterator<readonly Peer[]>;
206
200
  }
207
201
  /**
208
202
  * What a participant is currently doing. This is BOTH the SDK and the
@@ -276,7 +270,7 @@ export interface Peer {
276
270
  /** Server timestamp of the most recent frame from this participant. */
277
271
  readonly lastActive: string;
278
272
  /** Pending-mutation claims this participant has declared. */
279
- readonly activeClaims?: ReadonlyArray<Claim>;
273
+ readonly activeClaims?: readonly Claim[];
280
274
  }
281
275
  /** Outbound `presence_update` payload. */
282
276
  export interface PresenceUpdatePayload {
@@ -341,7 +335,7 @@ export interface ClaimStream {
341
335
  * Reads return the current snapshot; pair with `subscribe(...)`
342
336
  * below to get notified on change.
343
337
  */
344
- readonly others: ReadonlyArray<Claim>;
338
+ readonly others: readonly Claim[];
345
339
  /**
346
340
  * Reactive view of the wait queue on one target — the FIFO line of
347
341
  * `status: 'queued'` claims behind the current holder, each with its
@@ -410,7 +404,7 @@ export interface ClaimStream {
410
404
  * }
411
405
  * ```
412
406
  */
413
- [Symbol.asyncIterator](): AsyncIterableIterator<ReadonlyArray<Claim>>;
407
+ [Symbol.asyncIterator](): AsyncIterableIterator<readonly Claim[]>;
414
408
  }
415
409
  /**
416
410
  * You LOST an claim you were HOLDING — distinct from `ClaimRejection` (a
@@ -4,7 +4,7 @@
4
4
  * Fixed version that doesn't conflict with existing getters/setters
5
5
  */
6
6
  import { type AnnotationMapEntry } from 'mobx';
7
- import { PropertyMetadata, ReferenceMetadata } from '../types/index.js';
7
+ import { type PropertyMetadata, type ReferenceMetadata } from '../types/index.js';
8
8
  /**
9
9
  * The internal contract M1 relies on. Models invoke M1 from inside
10
10
  * their constructor, where these fields are guaranteed to exist on