@abloatai/ablo 0.26.0 → 0.28.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 (418) hide show
  1. package/CHANGELOG.md +42 -2
  2. package/README.md +102 -86
  3. package/dist/BaseSyncedStore.d.ts +85 -88
  4. package/dist/BaseSyncedStore.js +134 -151
  5. package/dist/Database.d.ts +68 -69
  6. package/dist/Database.js +316 -135
  7. package/dist/{ObjectPool.d.ts → InstanceCache.d.ts} +10 -13
  8. package/dist/{ObjectPool.js → InstanceCache.js} +85 -83
  9. package/dist/LazyReferenceCollection.d.ts +11 -15
  10. package/dist/LazyReferenceCollection.js +12 -16
  11. package/dist/Model.d.ts +54 -52
  12. package/dist/Model.js +78 -62
  13. package/dist/ModelRegistry.d.ts +21 -19
  14. package/dist/ModelRegistry.js +23 -27
  15. package/dist/NetworkMonitor.d.ts +5 -6
  16. package/dist/NetworkMonitor.js +5 -6
  17. package/dist/SyncClient.d.ts +122 -118
  18. package/dist/SyncClient.js +541 -245
  19. package/dist/adapters/alwaysOnline.d.ts +6 -8
  20. package/dist/adapters/alwaysOnline.js +6 -8
  21. package/dist/adapters/inMemoryStorage.d.ts +10 -9
  22. package/dist/adapters/inMemoryStorage.js +21 -9
  23. package/dist/agent/Agent.d.ts +27 -32
  24. package/dist/agent/Agent.js +18 -19
  25. package/dist/agent/index.d.ts +4 -4
  26. package/dist/agent/index.js +5 -5
  27. package/dist/agent/session.d.ts +47 -44
  28. package/dist/agent/session.js +37 -48
  29. package/dist/agent/types.d.ts +26 -31
  30. package/dist/agent/types.js +6 -7
  31. package/dist/ai-sdk/coordinatedTool.d.ts +108 -0
  32. package/dist/ai-sdk/{coordinated-tool.js → coordinatedTool.js} +44 -38
  33. package/dist/ai-sdk/coordinationContext.d.ts +46 -0
  34. package/dist/ai-sdk/{coordination-context.js → coordinationContext.js} +26 -33
  35. package/dist/ai-sdk/index.d.ts +25 -22
  36. package/dist/ai-sdk/index.js +25 -22
  37. package/dist/ai-sdk/wrap.d.ts +6 -7
  38. package/dist/ai-sdk/wrap.js +1 -1
  39. package/dist/auth/credentialPolicy.d.ts +69 -74
  40. package/dist/auth/credentialPolicy.js +51 -56
  41. package/dist/auth/credentialSource.d.ts +6 -5
  42. package/dist/auth/credentialSource.js +9 -10
  43. package/dist/auth/index.d.ts +59 -58
  44. package/dist/auth/index.js +31 -37
  45. package/dist/auth/schemas.d.ts +5 -4
  46. package/dist/auth/schemas.js +5 -4
  47. package/dist/batching/index.d.ts +19 -21
  48. package/dist/batching/index.js +14 -17
  49. package/dist/cli.cjs +173 -121
  50. package/dist/client/Ablo.d.ts +97 -74
  51. package/dist/client/Ablo.js +129 -163
  52. package/dist/client/ApiClient.d.ts +30 -19
  53. package/dist/client/ApiClient.js +442 -81
  54. package/dist/client/auth.d.ts +47 -47
  55. package/dist/client/auth.js +108 -117
  56. package/dist/client/claimHeartbeatLoop.d.ts +50 -0
  57. package/dist/client/claimHeartbeatLoop.js +88 -0
  58. package/dist/client/consoleLogger.d.ts +5 -6
  59. package/dist/client/consoleLogger.js +5 -6
  60. package/dist/client/createInternalComponents.d.ts +16 -17
  61. package/dist/client/createInternalComponents.js +26 -31
  62. package/dist/client/createModelProxy.d.ts +130 -120
  63. package/dist/client/createModelProxy.js +152 -122
  64. package/dist/client/credentialEndpoint.d.ts +40 -42
  65. package/dist/client/credentialEndpoint.js +35 -36
  66. package/dist/client/functionalUpdate.d.ts +29 -27
  67. package/dist/client/functionalUpdate.js +21 -21
  68. package/dist/client/hostedEndpoints.d.ts +9 -12
  69. package/dist/client/hostedEndpoints.js +9 -12
  70. package/dist/client/httpClient.d.ts +59 -53
  71. package/dist/client/httpClient.js +29 -31
  72. package/dist/client/identity.d.ts +15 -20
  73. package/dist/client/identity.js +47 -58
  74. package/dist/client/modelRegistration.d.ts +5 -9
  75. package/dist/client/modelRegistration.js +78 -87
  76. package/dist/client/options.d.ts +157 -157
  77. package/dist/client/options.js +3 -7
  78. package/dist/client/registerDataSource.d.ts +9 -9
  79. package/dist/client/registerDataSource.js +15 -16
  80. package/dist/client/resourceTypes.d.ts +64 -75
  81. package/dist/client/resourceTypes.js +4 -10
  82. package/dist/client/schemaConfig.d.ts +31 -43
  83. package/dist/client/schemaConfig.js +38 -50
  84. package/dist/client/sessionMint.d.ts +16 -12
  85. package/dist/client/sessionMint.js +26 -31
  86. package/dist/client/validateAbloOptions.d.ts +12 -14
  87. package/dist/client/validateAbloOptions.js +8 -9
  88. package/dist/client/writeOptionsSchema.d.ts +18 -16
  89. package/dist/client/writeOptionsSchema.js +23 -20
  90. package/dist/client/wsMutationExecutor.d.ts +16 -20
  91. package/dist/client/wsMutationExecutor.js +18 -23
  92. package/dist/commit/contract.d.ts +493 -0
  93. package/dist/commit/contract.js +187 -0
  94. package/dist/commit/index.d.ts +6 -0
  95. package/dist/commit/index.js +5 -0
  96. package/dist/context.d.ts +6 -4
  97. package/dist/context.js +6 -4
  98. package/dist/coordination/index.d.ts +10 -8
  99. package/dist/coordination/index.js +14 -12
  100. package/dist/coordination/schema.d.ts +176 -128
  101. package/dist/coordination/schema.js +197 -133
  102. package/dist/coordination/trace.d.ts +9 -10
  103. package/dist/coordination/trace.js +13 -14
  104. package/dist/core/DatabaseManager.d.ts +5 -7
  105. package/dist/core/DatabaseManager.js +15 -19
  106. package/dist/core/QueryProcessor.d.ts +7 -9
  107. package/dist/core/QueryProcessor.js +22 -28
  108. package/dist/core/QueryView.d.ts +8 -8
  109. package/dist/core/QueryView.js +2 -2
  110. package/dist/core/StoreManager.d.ts +14 -14
  111. package/dist/core/StoreManager.js +33 -24
  112. package/dist/core/ViewRegistry.d.ts +5 -5
  113. package/dist/core/ViewRegistry.js +4 -4
  114. package/dist/core/index.d.ts +17 -12
  115. package/dist/core/index.js +32 -26
  116. package/dist/core/openIDBWithTimeout.d.ts +38 -36
  117. package/dist/core/openIDBWithTimeout.js +42 -43
  118. package/dist/core/queryUtils.d.ts +45 -0
  119. package/dist/core/queryUtils.js +69 -0
  120. package/dist/core/storeContract.d.ts +63 -61
  121. package/dist/core/storeContract.js +8 -12
  122. package/dist/environment.d.ts +28 -0
  123. package/dist/environment.js +21 -0
  124. package/dist/errorCodes.d.ts +107 -99
  125. package/dist/errorCodes.js +137 -134
  126. package/dist/errors.d.ts +160 -166
  127. package/dist/errors.js +155 -158
  128. package/dist/index.d.ts +36 -27
  129. package/dist/index.js +91 -86
  130. package/dist/interfaces/index.d.ts +102 -113
  131. package/dist/interfaces/index.js +5 -4
  132. package/dist/keys/index.d.ts +27 -29
  133. package/dist/keys/index.js +41 -40
  134. package/dist/mutators/RecordingTransaction.d.ts +16 -16
  135. package/dist/mutators/RecordingTransaction.js +31 -37
  136. package/dist/mutators/Transaction.d.ts +18 -26
  137. package/dist/mutators/Transaction.js +14 -20
  138. package/dist/mutators/UndoManager.d.ts +124 -131
  139. package/dist/mutators/UndoManager.js +177 -156
  140. package/dist/mutators/defineMutators.d.ts +23 -34
  141. package/dist/mutators/defineMutators.js +14 -20
  142. package/dist/mutators/inverseOp.d.ts +12 -15
  143. package/dist/mutators/inverseOp.js +12 -15
  144. package/dist/mutators/mutateActions.d.ts +10 -9
  145. package/dist/mutators/mutateActions.js +1 -1
  146. package/dist/mutators/readerActions.d.ts +9 -8
  147. package/dist/mutators/readerActions.js +2 -2
  148. package/dist/mutators/undoApply.d.ts +31 -27
  149. package/dist/mutators/undoApply.js +26 -24
  150. package/dist/policy/index.d.ts +5 -3
  151. package/dist/policy/index.js +5 -3
  152. package/dist/policy/types.d.ts +104 -100
  153. package/dist/policy/types.js +67 -66
  154. package/dist/query/client.d.ts +28 -23
  155. package/dist/query/client.js +45 -43
  156. package/dist/query/types.d.ts +37 -60
  157. package/dist/query/types.js +13 -33
  158. package/dist/react/AbloProvider.d.ts +1 -1
  159. package/dist/react/AbloProvider.js +2 -2
  160. package/dist/react/context.d.ts +25 -28
  161. package/dist/react/context.js +9 -10
  162. package/dist/react/index.d.ts +41 -42
  163. package/dist/react/index.js +37 -38
  164. package/dist/react/internalContext.d.ts +17 -19
  165. package/dist/react/useAblo.d.ts +28 -25
  166. package/dist/react/useAblo.js +41 -17
  167. package/dist/react/useCurrentUserId.d.ts +8 -7
  168. package/dist/react/useCurrentUserId.js +8 -7
  169. package/dist/react/useErrorListener.d.ts +7 -7
  170. package/dist/react/useErrorListener.js +10 -11
  171. package/dist/react/useMutationFailureListener.d.ts +8 -8
  172. package/dist/react/useMutationFailureListener.js +8 -8
  173. package/dist/react/useMutators.d.ts +11 -11
  174. package/dist/react/useMutators.js +3 -3
  175. package/dist/react/useReactive.js +2 -2
  176. package/dist/react/useSyncStatus.d.ts +4 -6
  177. package/dist/react/useUndoScope.d.ts +7 -9
  178. package/dist/react/useUndoScope.js +1 -1
  179. package/dist/schema/coordination.d.ts +21 -25
  180. package/dist/schema/coordination.js +21 -25
  181. package/dist/schema/ddl.d.ts +43 -39
  182. package/dist/schema/ddl.js +75 -68
  183. package/dist/schema/ddlLock.d.ts +20 -24
  184. package/dist/schema/ddlLock.js +18 -23
  185. package/dist/schema/diff.d.ts +99 -61
  186. package/dist/schema/diff.js +43 -34
  187. package/dist/schema/field.d.ts +37 -42
  188. package/dist/schema/field.js +35 -48
  189. package/dist/schema/generate.d.ts +12 -12
  190. package/dist/schema/generate.js +12 -12
  191. package/dist/schema/index.d.ts +3 -3
  192. package/dist/schema/index.js +21 -23
  193. package/dist/schema/model.d.ts +118 -143
  194. package/dist/schema/model.js +22 -33
  195. package/dist/schema/openapi.d.ts +10 -9
  196. package/dist/schema/openapi.js +5 -3
  197. package/dist/schema/queries.d.ts +29 -31
  198. package/dist/schema/queries.js +23 -25
  199. package/dist/schema/relation.d.ts +89 -99
  200. package/dist/schema/relation.js +13 -13
  201. package/dist/schema/residency.d.ts +16 -13
  202. package/dist/schema/residency.js +16 -13
  203. package/dist/schema/roles.d.ts +36 -43
  204. package/dist/schema/roles.js +31 -37
  205. package/dist/schema/schema.d.ts +64 -43
  206. package/dist/schema/schema.js +31 -32
  207. package/dist/schema/select.d.ts +13 -13
  208. package/dist/schema/select.js +13 -13
  209. package/dist/schema/serialize.d.ts +28 -31
  210. package/dist/schema/serialize.js +27 -31
  211. package/dist/schema/sugar.d.ts +17 -32
  212. package/dist/schema/sugar.js +14 -29
  213. package/dist/schema/{sync-delta-row.d.ts → syncDeltaRow.d.ts} +26 -49
  214. package/dist/schema/syncDeltaRow.js +89 -0
  215. package/dist/schema/tenancy.d.ts +44 -46
  216. package/dist/schema/tenancy.js +46 -48
  217. package/dist/server/adapter.d.ts +58 -58
  218. package/dist/server/adapter.js +13 -14
  219. package/dist/server/commit.d.ts +60 -64
  220. package/dist/server/index.d.ts +9 -10
  221. package/dist/server/index.js +1 -1
  222. package/dist/server/readConfig.d.ts +70 -0
  223. package/dist/server/readConfig.js +8 -0
  224. package/dist/server/storageMode.d.ts +23 -0
  225. package/dist/server/storageMode.js +17 -0
  226. package/dist/source/adapter.d.ts +30 -25
  227. package/dist/source/adapter.js +10 -10
  228. package/dist/source/adapters/drizzle.d.ts +28 -23
  229. package/dist/source/adapters/drizzle.js +30 -25
  230. package/dist/source/adapters/kysely.d.ts +27 -25
  231. package/dist/source/adapters/kysely.js +24 -23
  232. package/dist/source/adapters/memory.d.ts +8 -7
  233. package/dist/source/adapters/memory.js +9 -8
  234. package/dist/source/adapters/prisma.d.ts +13 -12
  235. package/dist/source/adapters/prisma.js +22 -25
  236. package/dist/source/conformance.d.ts +18 -11
  237. package/dist/source/conformance.js +17 -11
  238. package/dist/source/connector.d.ts +31 -32
  239. package/dist/source/connector.js +28 -28
  240. package/dist/source/connectorProtocol.d.ts +160 -0
  241. package/dist/source/connectorProtocol.js +162 -0
  242. package/dist/source/contract.d.ts +26 -27
  243. package/dist/source/contract.js +28 -29
  244. package/dist/source/factory.d.ts +46 -58
  245. package/dist/source/factory.js +22 -27
  246. package/dist/source/index.d.ts +7 -9
  247. package/dist/source/index.js +12 -14
  248. package/dist/source/migrations.d.ts +9 -9
  249. package/dist/source/migrations.js +9 -9
  250. package/dist/source/next.d.ts +9 -10
  251. package/dist/source/next.js +6 -7
  252. package/dist/source/pushQueue.d.ts +69 -47
  253. package/dist/source/pushQueue.js +32 -28
  254. package/dist/source/signing.d.ts +46 -17
  255. package/dist/source/signing.js +28 -11
  256. package/dist/source/types.d.ts +121 -104
  257. package/dist/source/types.js +13 -14
  258. package/dist/stores/ObjectStore.d.ts +24 -12
  259. package/dist/stores/ObjectStore.js +38 -16
  260. package/dist/stores/ObjectStoreContract.d.ts +14 -15
  261. package/dist/stores/SyncActionStore.d.ts +7 -11
  262. package/dist/stores/SyncActionStore.js +13 -17
  263. package/dist/surface.d.ts +28 -21
  264. package/dist/surface.js +29 -20
  265. package/dist/sync/{BootstrapHelper.d.ts → BootstrapFetcher.d.ts} +36 -42
  266. package/dist/sync/{BootstrapHelper.js → BootstrapFetcher.js} +76 -76
  267. package/dist/sync/ConnectionManager.d.ts +39 -50
  268. package/dist/sync/ConnectionManager.js +55 -66
  269. package/dist/sync/NetworkProbe.d.ts +24 -29
  270. package/dist/sync/NetworkProbe.js +63 -69
  271. package/dist/sync/{HydrationCoordinator.d.ts → OnDemandLoader.d.ts} +42 -41
  272. package/dist/sync/{HydrationCoordinator.js → OnDemandLoader.js} +59 -54
  273. package/dist/sync/{AreaOfInterestManager.d.ts → SubscriptionManager.d.ts} +43 -57
  274. package/dist/sync/{AreaOfInterestManager.js → SubscriptionManager.js} +43 -51
  275. package/dist/sync/SyncWebSocket.d.ts +141 -166
  276. package/dist/sync/SyncWebSocket.js +191 -223
  277. package/dist/sync/awaitClaimGrant.d.ts +18 -18
  278. package/dist/sync/awaitClaimGrant.js +11 -11
  279. package/dist/sync/bootstrapApply.d.ts +34 -24
  280. package/dist/sync/bootstrapApply.js +27 -19
  281. package/dist/sync/commitFrames.d.ts +21 -20
  282. package/dist/sync/commitFrames.js +18 -18
  283. package/dist/sync/createClaimStream.d.ts +23 -22
  284. package/dist/sync/createClaimStream.js +105 -23
  285. package/dist/sync/createPresenceStream.d.ts +19 -18
  286. package/dist/sync/createPresenceStream.js +25 -26
  287. package/dist/sync/createSnapshot.d.ts +12 -14
  288. package/dist/sync/createSnapshot.js +20 -26
  289. package/dist/sync/credentialLifecycle.d.ts +104 -104
  290. package/dist/sync/credentialLifecycle.js +140 -147
  291. package/dist/sync/deltaPipeline.d.ts +36 -34
  292. package/dist/sync/deltaPipeline.js +64 -65
  293. package/dist/sync/groupChange.d.ts +63 -61
  294. package/dist/sync/groupChange.js +74 -78
  295. package/dist/sync/heartbeat.d.ts +34 -33
  296. package/dist/sync/heartbeat.js +31 -31
  297. package/dist/sync/participants.d.ts +19 -19
  298. package/dist/sync/persistedPrefix.d.ts +12 -0
  299. package/dist/sync/persistedPrefix.js +22 -0
  300. package/dist/sync/schemas.d.ts +3 -2
  301. package/dist/sync/schemas.js +14 -10
  302. package/dist/sync/syncCursor.d.ts +17 -21
  303. package/dist/sync/syncCursor.js +17 -21
  304. package/dist/sync/syncPlan.d.ts +28 -36
  305. package/dist/sync/syncPlan.js +18 -19
  306. package/dist/sync/syncPosition.d.ts +54 -49
  307. package/dist/sync/syncPosition.js +57 -52
  308. package/dist/sync/wsFrameHandlers.d.ts +35 -36
  309. package/dist/sync/wsFrameHandlers.js +63 -67
  310. package/dist/testing/fixtures/bootstrap.d.ts +12 -6
  311. package/dist/testing/fixtures/bootstrap.js +12 -6
  312. package/dist/testing/fixtures/deltas.d.ts +30 -33
  313. package/dist/testing/fixtures/deltas.js +30 -33
  314. package/dist/testing/fixtures/models.d.ts +11 -10
  315. package/dist/testing/fixtures/models.js +11 -10
  316. package/dist/testing/helpers/{react-wrapper.d.ts → reactWrapper.d.ts} +13 -10
  317. package/dist/testing/helpers/{react-wrapper.js → reactWrapper.js} +15 -12
  318. package/dist/testing/helpers/{sync-engine-harness.d.ts → syncEngineHarness.d.ts} +17 -15
  319. package/dist/testing/helpers/{sync-engine-harness.js → syncEngineHarness.js} +12 -10
  320. package/dist/testing/helpers/wait.d.ts +13 -8
  321. package/dist/testing/helpers/wait.js +13 -8
  322. package/dist/testing/index.d.ts +5 -3
  323. package/dist/testing/index.js +3 -2
  324. package/dist/testing/mocks/FakeDatabase.d.ts +18 -0
  325. package/dist/testing/mocks/FakeDatabase.js +10 -0
  326. package/dist/testing/mocks/MockMutationExecutor.d.ts +18 -17
  327. package/dist/testing/mocks/MockMutationExecutor.js +15 -14
  328. package/dist/testing/mocks/MockNetworkMonitor.d.ts +8 -8
  329. package/dist/testing/mocks/MockNetworkMonitor.js +8 -8
  330. package/dist/testing/mocks/MockSyncContext.d.ts +20 -17
  331. package/dist/testing/mocks/MockSyncContext.js +15 -13
  332. package/dist/testing/mocks/MockSyncStore.d.ts +10 -10
  333. package/dist/testing/mocks/MockSyncStore.js +11 -11
  334. package/dist/testing/mocks/MockWebSocket.d.ts +28 -23
  335. package/dist/testing/mocks/MockWebSocket.js +22 -21
  336. package/dist/transactions/TransactionQueue.d.ts +244 -181
  337. package/dist/transactions/TransactionQueue.js +929 -423
  338. package/dist/transactions/TransactionStore.d.ts +6 -4
  339. package/dist/transactions/TransactionStore.js +6 -4
  340. package/dist/transactions/UnconfirmedWrites.d.ts +82 -0
  341. package/dist/transactions/UnconfirmedWrites.js +104 -0
  342. package/dist/transactions/coalesceRules.d.ts +41 -17
  343. package/dist/transactions/coalesceRules.js +40 -17
  344. package/dist/transactions/commitEnvelope.d.ts +132 -0
  345. package/dist/transactions/commitEnvelope.js +139 -0
  346. package/dist/transactions/commitOutboxStore.d.ts +32 -0
  347. package/dist/transactions/commitOutboxStore.js +26 -0
  348. package/dist/transactions/commitPayload.d.ts +63 -52
  349. package/dist/transactions/commitPayload.js +54 -57
  350. package/dist/transactions/deltaConfirmation.d.ts +20 -22
  351. package/dist/transactions/deltaConfirmation.js +37 -45
  352. package/dist/transactions/httpCommitEnvelope.d.ts +43 -0
  353. package/dist/transactions/httpCommitEnvelope.js +179 -0
  354. package/dist/transactions/optimisticApply.d.ts +49 -0
  355. package/dist/transactions/optimisticApply.js +65 -0
  356. package/dist/transactions/replayValidation.d.ts +182 -0
  357. package/dist/transactions/replayValidation.js +156 -0
  358. package/dist/types/global.d.ts +46 -41
  359. package/dist/types/global.js +20 -19
  360. package/dist/types/index.d.ts +71 -77
  361. package/dist/types/index.js +22 -22
  362. package/dist/types/modelData.d.ts +6 -8
  363. package/dist/types/modelData.js +5 -7
  364. package/dist/types/participant.d.ts +10 -11
  365. package/dist/types/participant.js +6 -8
  366. package/dist/types/streams.d.ts +208 -195
  367. package/dist/types/streams.js +7 -7
  368. package/dist/utils/asyncIterator.d.ts +25 -32
  369. package/dist/utils/asyncIterator.js +25 -32
  370. package/dist/utils/duration.d.ts +12 -15
  371. package/dist/utils/duration.js +12 -15
  372. package/dist/utils/mobxSetup.d.ts +53 -0
  373. package/dist/utils/{mobx-setup.js → mobxSetup.js} +42 -98
  374. package/dist/webhooks/events.d.ts +21 -16
  375. package/dist/webhooks/events.js +10 -8
  376. package/dist/webhooks/index.d.ts +5 -7
  377. package/dist/webhooks/index.js +5 -7
  378. package/dist/wire/bootstrapReason.d.ts +9 -0
  379. package/dist/wire/bootstrapReason.js +8 -0
  380. package/dist/{schema/sync-delta-wire.d.ts → wire/delta.d.ts} +58 -41
  381. package/dist/wire/delta.js +114 -0
  382. package/dist/wire/errorEnvelope.d.ts +30 -31
  383. package/dist/wire/errorEnvelope.js +34 -40
  384. package/dist/wire/frames.d.ts +315 -86
  385. package/dist/wire/frames.js +47 -33
  386. package/dist/wire/index.d.ts +18 -14
  387. package/dist/wire/index.js +32 -27
  388. package/dist/wire/listEnvelope.d.ts +16 -23
  389. package/dist/wire/listEnvelope.js +7 -6
  390. package/dist/wire/protocol.d.ts +25 -32
  391. package/dist/wire/protocol.js +25 -32
  392. package/dist/wire/protocolVersion.d.ts +44 -40
  393. package/dist/wire/protocolVersion.js +44 -40
  394. package/docs/api.md +10 -10
  395. package/docs/coordination.md +59 -0
  396. package/docs/mcp.md +1 -1
  397. package/package.json +17 -11
  398. package/dist/ai-sdk/coordinated-tool.d.ts +0 -101
  399. package/dist/ai-sdk/coordination-context.d.ts +0 -52
  400. package/dist/core/query-utils.d.ts +0 -34
  401. package/dist/core/query-utils.js +0 -59
  402. package/dist/schema/sync-delta-row.js +0 -103
  403. package/dist/schema/sync-delta-wire.js +0 -102
  404. package/dist/server/read-config.d.ts +0 -67
  405. package/dist/server/read-config.js +0 -8
  406. package/dist/server/storage-mode.d.ts +0 -8
  407. package/dist/server/storage-mode.js +0 -28
  408. package/dist/source/connector-protocol.d.ts +0 -159
  409. package/dist/source/connector-protocol.js +0 -161
  410. package/dist/transactions/OptimisticEchoTracker.d.ts +0 -82
  411. package/dist/transactions/OptimisticEchoTracker.js +0 -104
  412. package/dist/transactions/mutation-error-handler.d.ts +0 -5
  413. package/dist/transactions/mutation-error-handler.js +0 -39
  414. package/dist/transactions/optimistic.d.ts +0 -24
  415. package/dist/transactions/optimistic.js +0 -45
  416. package/dist/transactions/persistedReplay.d.ts +0 -93
  417. package/dist/transactions/persistedReplay.js +0 -105
  418. package/dist/utils/mobx-setup.d.ts +0 -42
package/dist/Database.js CHANGED
@@ -1,6 +1,9 @@
1
1
  /**
2
- * Database - Simplified persistence layer
3
- * Fixed bootstrap triggering and data flow
2
+ * The local persistence layer for synced models. It stores rows in the
3
+ * browser's IndexedDB (or in-memory maps when run headlessly), applies inbound
4
+ * deltas to that store, and fetches the bootstrap snapshot from your sync
5
+ * server. {@link BaseSyncedStore} drives it, and {@link InstanceCache} holds the
6
+ * in-memory mirror of what this class persists.
4
7
  */
5
8
  import { DatabaseManager } from './core/DatabaseManager.js';
6
9
  import { StoreManager } from './core/StoreManager.js';
@@ -10,6 +13,43 @@ import { getContext } from './context.js';
10
13
  import { AbloConnectionError, AbloValidationError } from './errors.js';
11
14
  import { InMemoryObjectStore } from './adapters/inMemoryStorage.js';
12
15
  import { syncPositionSchema } from './sync/syncPosition.js';
16
+ import { highestPersistedPrefixSyncId } from './sync/persistedPrefix.js';
17
+ /**
18
+ * Request identity excludes local timing metadata for re-entrant seals: a
19
+ * retry rebuilds its envelope with a fresh `sequence`/seal clock, so comparing
20
+ * those volatile fields would reject every legitimate same-request re-seal as
21
+ * an idempotency conflict. Only the fields that define the wire request count.
22
+ */
23
+ function isSameOutboxRecord(existing, candidate) {
24
+ if (existing.type === 'http_commit_envelope' &&
25
+ candidate.type === 'http_commit_envelope') {
26
+ const identity = (record) => ({
27
+ id: record.id,
28
+ type: record.type,
29
+ storageVersion: record.storageVersion,
30
+ idempotencyKey: record.idempotencyKey,
31
+ request: record.request,
32
+ scopeNamespace: record.scopeNamespace,
33
+ });
34
+ return JSON.stringify(identity(existing)) === JSON.stringify(identity(candidate));
35
+ }
36
+ if (existing.type === 'commit_envelope' &&
37
+ candidate.type === 'commit_envelope') {
38
+ const identity = (record) => ({
39
+ id: record.id,
40
+ type: record.type,
41
+ storageVersion: record.storageVersion,
42
+ origin: record.origin,
43
+ idempotencyKey: record.idempotencyKey,
44
+ operations: record.operations,
45
+ sourceMutationIds: record.sourceMutationIds,
46
+ commitOptions: record.commitOptions,
47
+ scope: record.scope,
48
+ });
49
+ return JSON.stringify(identity(existing)) === JSON.stringify(identity(candidate));
50
+ }
51
+ return JSON.stringify(existing) === JSON.stringify(candidate);
52
+ }
13
53
  export class Database {
14
54
  // Core database components
15
55
  databaseManager;
@@ -22,11 +62,11 @@ export class Database {
22
62
  return this.bootstrapHelper;
23
63
  }
24
64
  /**
25
- * PURE scoped snapshot fetch for hydrate-on-enter (P4). Returns the FULL
26
- * current rows of the given sync groups, with NO side effects — unlike
65
+ * Fetch the current rows of the given sync groups as a side-effect-free
66
+ * snapshot, used to hydrate a scope as the user enters it. Unlike
27
67
  * {@link bootstrapFromServer}, it does not persist to IndexedDB and does not
28
- * touch the connection's `subscribedSyncGroups` (which the shrinkage check
29
- * owns). The caller applies the result to the pool via the SCOPED apply path.
68
+ * change the connection's subscribed sync groups. The caller applies the
69
+ * result to the pool through the scoped apply path.
30
70
  */
31
71
  async fetchScopedBootstrapData(syncGroups) {
32
72
  // No lastSyncId → a full snapshot of exactly these groups.
@@ -156,17 +196,14 @@ export class Database {
156
196
  getContext().logger.info(`Database opened: ${this.currentDbInfo.name} (${readiness.readyStores.length}/${readiness.totalStores} stores ready)`);
157
197
  }
158
198
  /**
159
- * Compact a record before persisting to IndexedDB
160
- * - Removes null/undefined fields
161
- * - Removes empty arrays and empty objects
162
- * - Drops redundant fields: __typename, __class, clientId, syncStatus
163
- *
164
- * ARCHITECTURE: By design, this method receives plain objects, not MobX observables:
165
- * - WebSocket deltas: Already JSON-parsed (SyncedStore.ts:889)
166
- * - Optimistic updates: Models call toJSON() which uses toJS() (SlideLayer.ts:224)
167
- * - Bootstrap data: Plain JSON from server
199
+ * Shrink a record before persisting it. Drops `undefined` fields, empty
200
+ * arrays, empty objects, and the redundant markers `__typename`, `__class`,
201
+ * `clientId`, and `syncStatus`. Explicit `null` values are preserved, since
202
+ * a null is a meaningful "clear this field" in a nullable column.
168
203
  *
169
- * Note: We do NOT drop required defaults; server provides them.
204
+ * By design this receives plain objects, never live observables: WebSocket
205
+ * deltas arrive already parsed, optimistic updates come through `toJSON()`,
206
+ * and bootstrap data is plain JSON from the server.
170
207
  */
171
208
  compactRecord(_modelName, data) {
172
209
  if (!data || typeof data !== 'object')
@@ -177,8 +214,8 @@ export class Database {
177
214
  if (key === '__typename' || key === '__class' || key === 'clientId' || key === 'syncStatus') {
178
215
  continue;
179
216
  }
180
- // FIXED: Only skip undefined, preserve explicit null values
181
- // Null is semantically meaningful in Prisma schemas (nullable fields)
217
+ // Skip only `undefined`; preserve explicit `null`, which is a
218
+ // meaningful value for a nullable column.
182
219
  if (value === undefined) {
183
220
  continue;
184
221
  }
@@ -272,26 +309,24 @@ export class Database {
272
309
  // point). Invalid → 0 → full bootstrap, the safe degradation.
273
310
  const metadataLastSyncId = syncPositionSchema.shape.persisted.safeParse(metadata?.lastSyncId).data ?? 0;
274
311
  const dataAge = metadata?.updatedAt ? Date.now() - metadata.updatedAt.getTime() : Infinity;
275
- // ── Zero-style cache-validity check ──────────────────────────
312
+ // ── Cache-validity check ─────────────────────────────────────
276
313
  //
277
314
  // The cursor (lastSyncId) is only valid if the data it refers to
278
- // actually exists in the stores. If IDB was cleared (or this is a
279
- // fresh in-memory session), the metadata's lastSyncId is stale —
280
- // sending it to the server would trigger a partial bootstrap that
281
- // returns zero deltas because the gap is 0, leaving the client
282
- // with an empty ObjectPool.
315
+ // actually exists in the stores. If the local store was cleared (or
316
+ // this is a fresh in-memory session), the metadata's lastSyncId is
317
+ // stale — sending it to the server would trigger a partial bootstrap
318
+ // that returns zero deltas because the gap is 0, leaving the client
319
+ // with an empty InstanceCache.
283
320
  //
284
- // Zero solves this by co-locating the cursor with the cached data:
285
- // if the data is gone, the cursor is gone. We achieve the same
286
- // property by sampling the actual stores — if they're empty, the
287
- // cursor is meaningless regardless of what metadata claims.
321
+ // The fix is to sample the actual stores: if they hold no rows, the
322
+ // cursor is meaningless regardless of what the metadata claims.
288
323
  const dataExists = this.inMemory
289
324
  ? false // In-memory mode: no persistent data across sessions
290
325
  : await this.storeManager.hasAnyData();
291
326
  // The effective lastSyncId: only trust the metadata cursor when
292
327
  // we've confirmed the data it refers to actually exists in the stores.
293
328
  const lastSyncId = dataExists ? metadataLastSyncId : 0;
294
- // 🔍 DIAGNOSTIC: Log database state
329
+ // Log the resolved database state for diagnostics.
295
330
  getContext().logger.debug('[Database.requiredBootstrap] State check', {
296
331
  readinessReady: readiness.ready,
297
332
  hasMetadata: !!metadata,
@@ -312,7 +347,8 @@ export class Database {
312
347
  getContext().logger.info('Offline detected with local data - using local bootstrap');
313
348
  }
314
349
  else {
315
- // SERVER-AUTHORITATIVE: Always use full bootstrap when online.
350
+ // The server is the source of truth: always use a full bootstrap
351
+ // when online.
316
352
  type = 'full';
317
353
  getContext().logger.info('Full bootstrap - server is source of truth', {
318
354
  reason: offline ? 'offline_no_data' : 'server_authoritative',
@@ -329,7 +365,9 @@ export class Database {
329
365
  };
330
366
  }
331
367
  /**
332
- * Bootstrap database with data from Go server
368
+ * Fetch a bootstrap snapshot (or delta batch) from the sync server and load
369
+ * it into the local store, then return a {@link BootstrapResult} the caller
370
+ * applies to the {@link InstanceCache}.
333
371
  */
334
372
  async bootstrapFromServer(requirements,
335
373
  /** Full sync-group subscription list — what the WS subscribes to
@@ -348,8 +386,8 @@ export class Database {
348
386
  modelsToLoad: requirements.modelsToLoad,
349
387
  });
350
388
  try {
351
- // FETCH FIRST (before any destructive operations)
352
- // This prevents data loss if the network request fails
389
+ // Fetch before any destructive operation, so a failed network
390
+ // request can't leave the local store empty.
353
391
  const startTime = typeof performance !== 'undefined' ? performance.now() : Date.now();
354
392
  getContext().logger.info('Fetching bootstrap data from server (before clearing local data)', {
355
393
  type: requirements.type,
@@ -363,8 +401,9 @@ export class Database {
363
401
  hasDeltas: !!bootstrapData.deltas,
364
402
  deltaCount: bootstrapData.deltaCount ?? 0,
365
403
  });
366
- // Only clear AFTER successful fetch (transactional safety)
367
- // IMPORTANT: Clear if the SERVER says it's a full snapshot, regardless of what we asked.
404
+ // Clear only after a successful fetch, for transactional safety.
405
+ // Clear when the server says the response is a full snapshot,
406
+ // regardless of what type was requested.
368
407
  if (bootstrapData.type === 'full') {
369
408
  await this.clear();
370
409
  }
@@ -379,7 +418,7 @@ export class Database {
379
418
  // Apply deltas to IndexedDB using processDeltaBatch for better performance.
380
419
  // Capture the return value so the pool can be updated by the caller —
381
420
  // without this, partial-bootstrap DELETEs persist to IDB but don't
382
- // evict entities from the in-memory ObjectPool, leaving ghost rows
421
+ // evict entities from the in-memory InstanceCache, leaving ghost rows
383
422
  // visible on the canvas until a full reload rebuilds the pool.
384
423
  let deltasApplied = 0;
385
424
  let deltaResults;
@@ -501,17 +540,16 @@ export class Database {
501
540
  }
502
541
  // bootstrapSpecificModels removed per request
503
542
  /**
504
- * Process incoming delta from WebSocket - simplified
543
+ * Apply a single inbound delta from the WebSocket to the local store.
505
544
  *
506
- * ⚠️ PERFORMANCE NOTE: This method is called for each individual delta.
507
- * For batch processing, use processDeltaBatch() instead to avoid
508
- * transaction overhead (2x transactions per delta = major bottleneck).
545
+ * This handles one delta at a time. To apply several, prefer
546
+ * {@link processDeltaBatch}, which commits them in one IndexedDB transaction
547
+ * rather than two transactions per delta.
509
548
  *
510
- * 📝 PARTIAL DELTA PATTERN:
511
- * - Server sends only changed fields: {id, position: {...}, updatedAt}
512
- * - UPDATE deltas are MERGED with existing records: {...existing, ...delta}
513
- * - This preserves fields not included in the delta (e.g., deckId, title)
514
- * - Explicit null values ARE preserved: {position: null} clears the field
549
+ * Update deltas carry only the changed fields, so they are merged onto the
550
+ * existing record rather than replacing it. That preserves fields the delta
551
+ * omits (such as deckId or title), and an explicit null is kept as a value,
552
+ * clearing that field.
515
553
  */
516
554
  async processDelta(delta) {
517
555
  const { actionType, modelName, modelId, data, syncId } = delta;
@@ -519,7 +557,7 @@ export class Database {
519
557
  if (!store) {
520
558
  return { action: 'verify', modelName, modelId };
521
559
  }
522
- // Best-practice gating: ignore already-applied deltas by comparing with persisted lastSyncId
560
+ // Idempotency gate: ignore already-applied deltas by comparing with the persisted lastSyncId
523
561
  try {
524
562
  const lastApplied = await this.getLastSyncId();
525
563
  const incomingId = typeof syncId === 'number' ? syncId : undefined;
@@ -572,11 +610,12 @@ export class Database {
572
610
  return { action: 'add', modelName, modelId, data: compacted };
573
611
  }
574
612
  case 'U': {
575
- // ✅ UPDATE: MUST merge with existing record (partial delta pattern)
576
- // Read existing record first
613
+ // Update: merge onto the existing record (partial-delta pattern).
614
+ // Read the existing record first.
577
615
  const existing = await store.get(modelId);
578
- // CRITICAL FIX: Skip UPDATE if there's no existing record to merge with
579
- // Creating a record from partial UPDATE data causes corruption (missing deckId, etc.)
616
+ // Skip the update when there's no existing record to merge with:
617
+ // building a record from partial update data would corrupt it
618
+ // (missing deckId, and so on).
580
619
  if (!existing) {
581
620
  getContext().observability.breadcrumb('Skipping UPDATE delta - no existing record to merge with', 'sync.database', 'warning', {
582
621
  modelName,
@@ -619,7 +658,7 @@ export class Database {
619
658
  getContext().observability.breadcrumb(`IndexedDB delete failed for ${modelName}:${modelId}`, 'sync.database', 'error', {
620
659
  error: err instanceof Error ? err.message : String(err),
621
660
  });
622
- // Surface failure so caller does not mutate ObjectPool inconsistently
661
+ // Surface failure so caller does not mutate InstanceCache inconsistently
623
662
  throw err;
624
663
  }
625
664
  return { action: 'remove', modelName, modelId };
@@ -659,27 +698,20 @@ export class Database {
659
698
  }
660
699
  }
661
700
  /**
662
- * PERFORMANCE FIX: Process multiple deltas in a single IndexedDB transaction
701
+ * Apply many deltas to the local store in as few IndexedDB transactions as
702
+ * possible. Deltas are grouped by store, and each store's writes commit in a
703
+ * single transaction, so a batch of 186 deltas becomes roughly one
704
+ * transaction per store instead of two per delta.
663
705
  *
664
- * This method dramatically improves sync performance by:
665
- * 1. Batch-reading all existing records for UPDATEs (outside transaction for speed)
666
- * 2. Opening a single transaction per store for all writes
667
- * 3. Merging UPDATE deltas with existing data to preserve unmodified fields
668
- * 4. Updating metadata only once at the end with highest syncId
706
+ * The method reads the existing records for update deltas up front, then
707
+ * merges each update onto its existing record so fields the delta omits are
708
+ * preserved and an explicit null still clears its field. It advances the
709
+ * persisted sync cursor once, to the highest committed sync id.
669
710
  *
670
- * Performance impact: 186 deltas goes from ~372 transactions to just 1 transaction
671
- *
672
- * 📝 PARTIAL DELTA MERGE PATTERN:
673
- * - UPDATE deltas contain only changed fields
674
- * - We merge with existing: {...existing, ...delta}
675
- * - Preserves deckId, title, settings etc. when updating just position
676
- * - Handles explicit null: {field: null} clears the field correctly
677
- *
678
- * 🔄 LINEAR-STYLE CONFLICT RESOLUTION:
679
- * - Builds a map of DELETE deltas with their syncIds
680
- * - Before processing UPDATE/INSERT, checks for DELETE with higher syncId
681
- * - Skips stale updates for entities that will be/were deleted
682
- * - Prevents 404 errors from fetching already-deleted entities
711
+ * Conflict resolution follows a delete-wins rule: it first indexes the
712
+ * delete deltas by entity, then skips any insert or update whose sync id is
713
+ * at or below a delete for the same entity. This avoids resurrecting a
714
+ * deleted entity and avoids fetching one that no longer exists.
683
715
  */
684
716
  async processDeltaBatch(deltas) {
685
717
  if ((!this.workspaceDb && !this.inMemory) || this.isClosing || deltas.length === 0) {
@@ -722,14 +754,11 @@ export class Database {
722
754
  }
723
755
  // Prepare results aligned with input order
724
756
  const results = new Array(deltas.length);
725
- // ========================================================================
726
- // LINEAR-STYLE CONFLICT RESOLUTION: Build DELETE syncId index
727
- // ========================================================================
728
- // Per Linear's architecture: "If the syncId of the deleting action is larger,
729
- // the model will not be created." This prevents processing stale UPDATE deltas
730
- // for entities that have been cascade-deleted (where DELETE delta exists).
731
- // ========================================================================
732
- const deleteSyncIds = new Map(); // key: "ModelName:modelId" -> DELETE syncId
757
+ // Build a delete index for conflict resolution. When a delete has a sync
758
+ // id at or above a later insert or update for the same entity, that entity
759
+ // is not (re)created — which drops stale updates for cascade-deleted
760
+ // entities.
761
+ const deleteSyncIds = new Map(); // key: "ModelName:modelId" -> delete syncId
733
762
  for (const delta of deltas) {
734
763
  if (delta.actionType === 'D' && delta.syncId) {
735
764
  const key = `${delta.modelName}:${delta.modelId}`;
@@ -749,21 +778,20 @@ export class Database {
749
778
  }
750
779
  // Group deltas by store for efficient transaction management.
751
780
  //
752
- // We intentionally track TWO highwater marks: `highestSyncId` for the
753
- // total range seen, and `highestPersistedSyncId` accumulated only from
754
- // deltas whose store transaction actually succeeded. The cursor
755
- // advance (at `updateWorkspaceMetadata`) uses ONLY the persisted one.
781
+ // The method tracks the total range seen plus the exact input indexes whose
782
+ // store transaction committed. The cursor is derived from their ordered
783
+ // prefix after every store finishes; a maximum alone is unsafe because a
784
+ // later store can succeed after an earlier store failed.
756
785
  //
757
- // Without this split, a single store-level IDB failure (e.g. compact
758
- // record missing required field, validation abort) silently advances
759
- // the cursor past deltas that never wrote to IDB. Next partial
760
- // bootstrap asks "what's new since {advanced cursor}?" and the
761
- // skipped rows fall into the already-seen range forever the
762
- // observed "postgres has the deck, IDB doesn't, full reload can't
763
- // recover it" failure mode.
786
+ // Without this split, a single store-level failure (a compacted record
787
+ // missing a required field, a validation abort) would advance the cursor
788
+ // past deltas that never wrote to IndexedDB. The next partial bootstrap
789
+ // would ask "what's new since {advanced cursor}?", the skipped rows would
790
+ // fall into the already-seen range forever, and the local store would stay
791
+ // permanently behind the server with no way to recover on reload.
764
792
  const deltasByStore = new Map();
765
793
  let highestSyncId = 0;
766
- let highestPersistedSyncId = 0;
794
+ const persistedIndexes = new Set();
767
795
  let skippedDueToConflict = 0;
768
796
  deltas.forEach((delta, idx) => {
769
797
  // Normalize to number — postgres sends bigint syncIds as strings.
@@ -773,9 +801,8 @@ export class Database {
773
801
  if (typeof deltaSyncIdNum === 'number' && !isNaN(deltaSyncIdNum) && deltaSyncIdNum > highestSyncId) {
774
802
  highestSyncId = deltaSyncIdNum;
775
803
  }
776
- // ========================================================================
777
- // CONFLICT CHECK: Skip UPDATE/INSERT if DELETE exists with higher syncId
778
- // ========================================================================
804
+ // Conflict check: skip an insert or update when a delete for the same
805
+ // entity has an equal or higher sync id.
779
806
  if (delta.actionType === 'U' ||
780
807
  delta.actionType === 'I' ||
781
808
  delta.actionType === 'C' ||
@@ -795,6 +822,9 @@ export class Database {
795
822
  deleteSyncId,
796
823
  });
797
824
  results[idx] = { action: 'verify', modelName: delta.modelName, modelId: delta.modelId };
825
+ // The later delete in this same ordered frame supersedes this
826
+ // value, so the stale predecessor requires no separate write.
827
+ persistedIndexes.add(idx);
798
828
  skippedDueToConflict++;
799
829
  return; // Skip this delta
800
830
  }
@@ -823,8 +853,8 @@ export class Database {
823
853
  if (!store)
824
854
  continue;
825
855
  try {
826
- // ✅ BEST PRACTICE: Batch read-modify-write pattern
827
- // Step 1: Identify which deltas need existing data (UPDATEs)
856
+ // Batch read-modify-write.
857
+ // Step 1: Identify which deltas need existing data (updates)
828
858
  const updateDeltas = storeDeltas.filter(({ delta }) => delta.actionType === 'U');
829
859
  const updateIds = updateDeltas.map(({ delta }) => delta.modelId);
830
860
  // Step 2: Batch read all existing records in a SINGLE IDB transaction
@@ -849,8 +879,9 @@ export class Database {
849
879
  }
850
880
  }
851
881
  }
852
- // ✅ SELF-HEALING: Fetch missing records for UPDATE deltas
853
- // Track IDs that failed to fetch (404 = entity deleted, skip the delta)
882
+ // Self-heal by fetching missing records for update deltas.
883
+ // Track ids that failed to fetch (a 404 means the entity was deleted,
884
+ // so its delta is skipped).
854
885
  const failedToFetch = new Set();
855
886
  if (missingIds.size > 0) {
856
887
  getContext().logger.info(`[Database.processDeltaBatch] Found ${missingIds.size} missing records for ${modelName}, fetching from server...`);
@@ -924,13 +955,11 @@ export class Database {
924
955
  });
925
956
  break;
926
957
  case 'U': {
927
- // ✅ UPDATE: Merge delta with existing record (already fetched)
958
+ // Update: merge the delta onto the existing record (already fetched).
928
959
  const existing = existingRecords.get(modelId);
929
- // ========================================================================
930
- // SKIP STALE DELTAS: If entity doesn't exist locally AND failed to fetch
931
- // from server (404), this is a stale UPDATE for a deleted entity.
932
- // Per Linear's architecture, skip it instead of creating incomplete data.
933
- // ========================================================================
960
+ // Skip a stale update: if the entity is neither in the local
961
+ // store nor fetchable from the server (a 404), it was deleted,
962
+ // so skip it rather than create an incomplete record.
934
963
  if (!existing && failedToFetch.has(modelId)) {
935
964
  getContext().logger.debug('[Database.processDeltaBatch] Skipping UPDATE for deleted entity', {
936
965
  modelName,
@@ -939,8 +968,9 @@ export class Database {
939
968
  stagedResults.push({ action: 'verify', modelName, modelId, idx });
940
969
  break; // Skip this delta
941
970
  }
942
- // CRITICAL FIX: Skip UPDATE if there's no existing record to merge with
943
- // Creating a record from partial UPDATE data causes corruption (missing deckId, etc.)
971
+ // Skip the update when there's no existing record to merge with:
972
+ // building a record from partial update data would corrupt it
973
+ // (missing deckId, and so on).
944
974
  if (!existing) {
945
975
  getContext().observability.breadcrumb('Batch: Skipping UPDATE delta - no existing record', 'sync.database', 'warning', {
946
976
  modelName,
@@ -993,14 +1023,14 @@ export class Database {
993
1023
  tx.oncomplete = () => { resolve(); };
994
1024
  tx.onerror = () => { reject(tx.error); };
995
1025
  });
996
- // Only commit staged results to the global results if the transaction succeeded.
997
- // Also advance `highestPersistedSyncId` ONLY for deltas in this successful tx
998
- // so the cursor can't advance past rows that never wrote to IDB.
1026
+ // Only commit staged results to the global results if the transaction
1027
+ // succeeded. Record input indexes rather than a maximum sync id; the
1028
+ // durable cursor is the prefix through these indexes.
999
1029
  for (const r of stagedResults) {
1000
1030
  // Resolve the originating delta so we can carry its
1001
1031
  // transactionId through to the result. Echo detection in
1002
1032
  // `SyncClient.applyDeltaBatchToPool` reads it.
1003
- const sourceDelta = storeDeltas.find(({ idx }) => idx === r.idx)?.delta;
1033
+ const sourceDelta = deltas[r.idx];
1004
1034
  results[r.idx] = {
1005
1035
  action: r.action,
1006
1036
  modelName: r.modelName,
@@ -1008,13 +1038,7 @@ export class Database {
1008
1038
  data: r.data,
1009
1039
  transactionId: sourceDelta?.transactionId,
1010
1040
  };
1011
- const rawSyncId = storeDeltas[storeDeltas.findIndex(({ idx }) => idx === r.idx)]?.delta.syncId;
1012
- // SyncDelta.syncId is typed as number but postgres serializes
1013
- // bigint to string on the wire — coerce before compare.
1014
- const syncId = typeof rawSyncId === 'string' ? Number(rawSyncId) : rawSyncId;
1015
- if (typeof syncId === 'number' && !isNaN(syncId) && syncId > highestPersistedSyncId) {
1016
- highestPersistedSyncId = syncId;
1017
- }
1041
+ persistedIndexes.add(r.idx);
1018
1042
  }
1019
1043
  }
1020
1044
  catch (err) {
@@ -1048,11 +1072,12 @@ export class Database {
1048
1072
  }
1049
1073
  }
1050
1074
  }
1051
- // Update metadata only to the highest syncId whose store transaction
1052
- // actually committed. Using `highestSyncId` (the range-seen max) would
1053
- // advance the cursor past deltas that failed to persist — the "cursor
1054
- // ahead of IDB" divergence that makes subsequent partial bootstraps
1055
- // skip the missing rows forever.
1075
+ // Advance only through the durable INPUT PREFIX. IDs need not be
1076
+ // numerically contiguous because other tenants and filtered sync groups
1077
+ // occupy gaps; the server-delivered order is the relevant sequence.
1078
+ const highestPersistedSyncId = highestPersistedPrefixSyncId(deltas, persistedIndexes);
1079
+ // Using `highestSyncId` (the range-seen max) would advance past an earlier
1080
+ // failed store transaction and permanently skip its delta.
1056
1081
  //
1057
1082
  // If `highestPersistedSyncId === 0` (every store tx failed), we leave
1058
1083
  // the metadata alone. Next partial bootstrap will re-deliver the
@@ -1104,13 +1129,7 @@ export class Database {
1104
1129
  const store = this.getRequiredStore(modelName);
1105
1130
  return await store.getAllFromIndex(indexName, value);
1106
1131
  }
1107
- /**
1108
- * Update workspace metadata
1109
- */
1110
- /**
1111
- * Get the last sync ID from workspace metadata
1112
- */
1113
- /** Read workspace metadata from IDB (returns null if db not open). */
1132
+ /** Read workspace metadata from IndexedDB. Returns null when the database is not open. */
1114
1133
  async getWorkspaceMetadata() {
1115
1134
  if (this.inMemory)
1116
1135
  return this.inMemoryMetadata;
@@ -1167,16 +1186,51 @@ export class Database {
1167
1186
  * delete/getAll/getAllFromIndex surface, so callers don't need to
1168
1187
  * branch on which one they got back. */
1169
1188
  get transactionStore() {
1170
- return this.getStore('__transactions');
1189
+ return this.getRequiredStore('__transactions');
1171
1190
  }
1172
1191
  async saveTransaction(transaction) {
1173
- await this.transactionStore?.put(transaction);
1192
+ await this.transactionStore.put(transaction);
1193
+ }
1194
+ /** Persist one burst of journal rows in a single strict durability group. */
1195
+ async saveTransactions(transactions) {
1196
+ if (transactions.length === 0)
1197
+ return;
1198
+ if (this.inMemory) {
1199
+ await Promise.all(transactions.map((transaction) => this.transactionStore.put(transaction)));
1200
+ return;
1201
+ }
1202
+ const db = this.workspaceDb;
1203
+ if (!db || this.isClosing) {
1204
+ throw new AbloConnectionError('Database not opened for mutation journal', {
1205
+ code: 'db_not_opened',
1206
+ });
1207
+ }
1208
+ await new Promise((resolve, reject) => {
1209
+ try {
1210
+ const tx = db.transaction(['__transactions'], 'readwrite', {
1211
+ durability: 'strict',
1212
+ });
1213
+ const store = tx.objectStore('__transactions');
1214
+ for (const transaction of transactions)
1215
+ store.put(transaction);
1216
+ tx.oncomplete = () => { resolve(); };
1217
+ tx.onabort = () => {
1218
+ reject(tx.error ?? new Error('Mutation journal transaction aborted'));
1219
+ };
1220
+ tx.onerror = () => {
1221
+ // onabort owns rejection.
1222
+ };
1223
+ }
1224
+ catch (error) {
1225
+ reject(error instanceof Error ? error : new Error(String(error)));
1226
+ }
1227
+ });
1174
1228
  }
1175
1229
  async removeTransaction(id) {
1176
- await this.transactionStore?.delete(id);
1230
+ await this.transactionStore.delete(id);
1177
1231
  }
1178
1232
  async getPersistedTransactions() {
1179
- const rows = (await this.transactionStore?.getAll()) ?? [];
1233
+ const rows = await this.transactionStore.getAll();
1180
1234
  // Storage layer returns the centralized `Record<string, unknown>`
1181
1235
  // shape from `ObjectStoreContract`. PersistedTransaction adds an
1182
1236
  // index signature so each row already structurally satisfies the
@@ -1184,14 +1238,138 @@ export class Database {
1184
1238
  // here, and it only accepts PersistedTransaction.
1185
1239
  return rows;
1186
1240
  }
1241
+ async getPersistedTransaction(id) {
1242
+ return (await this.transactionStore.get(id));
1243
+ }
1244
+ /**
1245
+ * Atomically seal one exact commit request and consume the staged mutation
1246
+ * records it replaces. The read, optional add, and deletes share one strict
1247
+ * IndexedDB transaction, so a crash can expose the staged records or the
1248
+ * sealed envelope, never a missing handoff. Returns the pre-existing record
1249
+ * when the envelope id was already sealed (retry/re-entrant call).
1250
+ */
1251
+ async sealTransactionRecord(record, consumedRecordIds) {
1252
+ const recordId = record.id;
1253
+ if (!recordId) {
1254
+ throw new AbloValidationError('A sealed transaction record must carry an id', {
1255
+ code: 'invalid_body',
1256
+ });
1257
+ }
1258
+ if (this.inMemory) {
1259
+ const store = this.transactionStore;
1260
+ const existing = (await store.get(recordId));
1261
+ if (existing && !isSameOutboxRecord(existing, record)) {
1262
+ throw new AbloValidationError('Commit outbox key already identifies a different request', {
1263
+ code: 'idempotency_conflict',
1264
+ });
1265
+ }
1266
+ if (!existing) {
1267
+ const sources = await Promise.all(consumedRecordIds.map((id) => store.get(id)));
1268
+ if (sources.some((source) => source === undefined)) {
1269
+ throw new AbloValidationError('Commit outbox source mutations were already claimed by another envelope', { code: 'idempotency_conflict' });
1270
+ }
1271
+ await store.add(record);
1272
+ }
1273
+ for (const id of consumedRecordIds) {
1274
+ if (id !== recordId)
1275
+ await store.delete(id);
1276
+ }
1277
+ return existing;
1278
+ }
1279
+ const db = this.workspaceDb;
1280
+ if (!db || this.isClosing) {
1281
+ throw new AbloConnectionError('Database not opened for commit outbox', {
1282
+ code: 'db_not_opened',
1283
+ });
1284
+ }
1285
+ return new Promise((resolve, reject) => {
1286
+ try {
1287
+ const tx = db.transaction(['__transactions'], 'readwrite', {
1288
+ durability: 'strict',
1289
+ });
1290
+ const store = tx.objectStore('__transactions');
1291
+ const getRequest = store.get(recordId);
1292
+ const sourceIds = [...new Set(consumedRecordIds)].filter((id) => id !== recordId);
1293
+ const sourceRequests = sourceIds.map((id) => store.get(id));
1294
+ const sourceExists = new Array(sourceRequests.length).fill(false);
1295
+ let existing;
1296
+ let collisionError;
1297
+ let envelopeRead = false;
1298
+ let sourcesRead = 0;
1299
+ let promotionStarted = false;
1300
+ const promote = () => {
1301
+ if (promotionStarted ||
1302
+ !envelopeRead ||
1303
+ sourcesRead !== sourceRequests.length)
1304
+ return;
1305
+ promotionStarted = true;
1306
+ if (existing && !isSameOutboxRecord(existing, record)) {
1307
+ collisionError = new AbloValidationError('Commit outbox key already identifies a different request', { code: 'idempotency_conflict' });
1308
+ tx.abort();
1309
+ return;
1310
+ }
1311
+ // A new envelope owns promotion only while every source row still
1312
+ // exists. This is the fleet/tab execution claim: a second tab that
1313
+ // restored the same journal entries under another key loses here and
1314
+ // cannot dispatch. An identical existing envelope is an idempotent
1315
+ // retry, so its already-consumed sources may be absent.
1316
+ if (!existing && sourceExists.some((exists) => !exists)) {
1317
+ collisionError = new AbloValidationError('Commit outbox source mutations were already claimed by another envelope', { code: 'idempotency_conflict' });
1318
+ tx.abort();
1319
+ return;
1320
+ }
1321
+ if (!existing)
1322
+ store.add(record);
1323
+ for (const id of sourceIds)
1324
+ store.delete(id);
1325
+ };
1326
+ getRequest.onsuccess = () => {
1327
+ existing = getRequest.result;
1328
+ envelopeRead = true;
1329
+ promote();
1330
+ };
1331
+ getRequest.onerror = () => {
1332
+ tx.abort();
1333
+ };
1334
+ sourceRequests.forEach((request, index) => {
1335
+ request.onsuccess = () => {
1336
+ sourceExists[index] = request.result !== undefined;
1337
+ sourcesRead += 1;
1338
+ promote();
1339
+ };
1340
+ request.onerror = () => {
1341
+ tx.abort();
1342
+ };
1343
+ });
1344
+ tx.oncomplete = () => { resolve(existing); };
1345
+ tx.onabort = () => {
1346
+ reject(collisionError ??
1347
+ tx.error ??
1348
+ getRequest.error ??
1349
+ new Error('Commit outbox transaction aborted'));
1350
+ };
1351
+ tx.onerror = () => {
1352
+ // onabort owns rejection so the promise settles exactly once.
1353
+ };
1354
+ }
1355
+ catch (error) {
1356
+ reject(error instanceof Error ? error : new Error(String(error)));
1357
+ }
1358
+ });
1359
+ }
1187
1360
  async cleanupOldTransactions(maxAge) {
1188
1361
  const store = this.transactionStore;
1189
- if (!store)
1190
- return 0;
1191
1362
  const rows = (await store.getAll());
1192
1363
  const cutoff = Date.now() - maxAge;
1193
1364
  let cleaned = 0;
1194
1365
  for (const tx of rows) {
1366
+ // Live write intent has no safe age-based expiry. In particular, server
1367
+ // idempotency retention may already have elapsed, so silently deleting or
1368
+ // blindly replaying an old envelope would both be unsafe. Restoration
1369
+ // owns quarantine/reconciliation for these records.
1370
+ if (tx.type === 'commit_envelope' || tx.type === 'pending_mutation') {
1371
+ continue;
1372
+ }
1195
1373
  if (typeof tx.timestamp === 'number' && tx.timestamp < cutoff) {
1196
1374
  await store.delete(tx.id);
1197
1375
  cleaned++;
@@ -1267,8 +1445,11 @@ export class Database {
1267
1445
  this.currentDbInfo = null;
1268
1446
  getContext().logger.debug('Database closed');
1269
1447
  }
1270
- async clear() {
1448
+ async clear(options = {}) {
1271
1449
  await this.storeManager.clearAllStores();
1450
+ if (options.includeWriteJournal) {
1451
+ await this.transactionStore.clear();
1452
+ }
1272
1453
  getContext().logger.info('All stores cleared');
1273
1454
  }
1274
1455
  }