@abloatai/ablo 0.25.0 → 0.27.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 (425) hide show
  1. package/AGENTS.md +5 -3
  2. package/CHANGELOG.md +34 -0
  3. package/README.md +104 -88
  4. package/dist/BaseSyncedStore.d.ts +140 -266
  5. package/dist/BaseSyncedStore.js +338 -739
  6. package/dist/Database.d.ts +62 -77
  7. package/dist/Database.js +106 -127
  8. package/dist/{ObjectPool.d.ts → InstanceCache.d.ts} +10 -13
  9. package/dist/{ObjectPool.js → InstanceCache.js} +91 -83
  10. package/dist/LazyReferenceCollection.d.ts +11 -15
  11. package/dist/LazyReferenceCollection.js +16 -15
  12. package/dist/Model.d.ts +37 -52
  13. package/dist/Model.js +52 -69
  14. package/dist/ModelRegistry.d.ts +46 -25
  15. package/dist/ModelRegistry.js +32 -30
  16. package/dist/NetworkMonitor.d.ts +5 -6
  17. package/dist/NetworkMonitor.js +6 -7
  18. package/dist/SyncClient.d.ts +119 -109
  19. package/dist/SyncClient.js +303 -224
  20. package/dist/SyncEngineContext.d.ts +1 -3
  21. package/dist/SyncEngineContext.js +1 -2
  22. package/dist/adapters/alwaysOnline.d.ts +6 -8
  23. package/dist/adapters/alwaysOnline.js +6 -8
  24. package/dist/adapters/inMemoryStorage.d.ts +9 -9
  25. package/dist/adapters/inMemoryStorage.js +9 -9
  26. package/dist/agent/Agent.d.ts +39 -31
  27. package/dist/agent/Agent.js +35 -23
  28. package/dist/agent/index.d.ts +4 -4
  29. package/dist/agent/index.js +5 -5
  30. package/dist/agent/session.d.ts +47 -44
  31. package/dist/agent/session.js +37 -48
  32. package/dist/agent/types.d.ts +26 -31
  33. package/dist/agent/types.js +6 -7
  34. package/dist/ai-sdk/coordinatedTool.d.ts +108 -0
  35. package/dist/ai-sdk/{coordinated-tool.js → coordinatedTool.js} +44 -38
  36. package/dist/ai-sdk/coordinationContext.d.ts +46 -0
  37. package/dist/ai-sdk/{coordination-context.js → coordinationContext.js} +30 -31
  38. package/dist/ai-sdk/index.d.ts +25 -22
  39. package/dist/ai-sdk/index.js +25 -22
  40. package/dist/ai-sdk/wrap.d.ts +7 -8
  41. package/dist/ai-sdk/wrap.js +2 -2
  42. package/dist/auth/credentialPolicy.d.ts +74 -71
  43. package/dist/auth/credentialPolicy.js +51 -56
  44. package/dist/auth/credentialSource.d.ts +7 -18
  45. package/dist/auth/credentialSource.js +10 -18
  46. package/dist/auth/index.d.ts +59 -58
  47. package/dist/auth/index.js +34 -40
  48. package/dist/auth/schemas.d.ts +5 -4
  49. package/dist/auth/schemas.js +5 -4
  50. package/dist/batching/index.d.ts +19 -21
  51. package/dist/batching/index.js +14 -17
  52. package/dist/cli.cjs +483 -369
  53. package/dist/client/Ablo.d.ts +107 -836
  54. package/dist/client/Ablo.js +174 -833
  55. package/dist/client/ApiClient.d.ts +44 -20
  56. package/dist/client/ApiClient.js +193 -44
  57. package/dist/client/auth.d.ts +51 -60
  58. package/dist/client/auth.js +137 -110
  59. package/dist/client/claimHeartbeatLoop.d.ts +50 -0
  60. package/dist/client/claimHeartbeatLoop.js +88 -0
  61. package/dist/client/consoleLogger.d.ts +35 -0
  62. package/dist/client/consoleLogger.js +44 -0
  63. package/dist/client/createInternalComponents.d.ts +14 -17
  64. package/dist/client/createInternalComponents.js +26 -31
  65. package/dist/client/createModelProxy.d.ts +130 -120
  66. package/dist/client/createModelProxy.js +158 -124
  67. package/dist/client/credentialEndpoint.d.ts +61 -0
  68. package/dist/client/credentialEndpoint.js +86 -0
  69. package/dist/client/functionalUpdate.d.ts +29 -27
  70. package/dist/client/functionalUpdate.js +21 -21
  71. package/dist/client/hostedEndpoints.d.ts +21 -0
  72. package/dist/client/hostedEndpoints.js +21 -0
  73. package/dist/client/httpClient.d.ts +58 -54
  74. package/dist/client/httpClient.js +29 -31
  75. package/dist/client/identity.d.ts +15 -20
  76. package/dist/client/identity.js +49 -59
  77. package/dist/client/modelRegistration.d.ts +10 -0
  78. package/dist/client/modelRegistration.js +301 -0
  79. package/dist/client/options.d.ts +373 -0
  80. package/dist/client/options.js +6 -0
  81. package/dist/client/registerDataSource.d.ts +9 -9
  82. package/dist/client/registerDataSource.js +15 -16
  83. package/dist/client/resourceTypes.d.ts +333 -0
  84. package/dist/client/resourceTypes.js +7 -0
  85. package/dist/client/schemaConfig.d.ts +44 -0
  86. package/dist/client/schemaConfig.js +176 -0
  87. package/dist/client/sessionMint.d.ts +17 -13
  88. package/dist/client/sessionMint.js +26 -31
  89. package/dist/client/validateAbloOptions.d.ts +12 -14
  90. package/dist/client/validateAbloOptions.js +9 -10
  91. package/dist/client/writeOptionsSchema.d.ts +18 -16
  92. package/dist/client/writeOptionsSchema.js +23 -20
  93. package/dist/client/wsMutationExecutor.d.ts +28 -0
  94. package/dist/client/wsMutationExecutor.js +71 -0
  95. package/dist/context.d.ts +6 -4
  96. package/dist/context.js +6 -7
  97. package/dist/coordination/index.d.ts +13 -4
  98. package/dist/coordination/index.js +29 -4
  99. package/dist/coordination/schema.d.ts +176 -128
  100. package/dist/coordination/schema.js +197 -133
  101. package/dist/coordination/trace.d.ts +9 -11
  102. package/dist/coordination/trace.js +13 -15
  103. package/dist/core/DatabaseManager.d.ts +5 -8
  104. package/dist/core/DatabaseManager.js +38 -40
  105. package/dist/core/QueryProcessor.d.ts +7 -9
  106. package/dist/core/QueryProcessor.js +27 -34
  107. package/dist/core/QueryView.d.ts +17 -5
  108. package/dist/core/QueryView.js +6 -7
  109. package/dist/core/StoreManager.d.ts +14 -16
  110. package/dist/core/StoreManager.js +26 -25
  111. package/dist/core/ViewRegistry.d.ts +5 -5
  112. package/dist/core/ViewRegistry.js +4 -4
  113. package/dist/core/index.d.ts +18 -13
  114. package/dist/core/index.js +32 -26
  115. package/dist/core/openIDBWithTimeout.d.ts +38 -36
  116. package/dist/core/openIDBWithTimeout.js +57 -54
  117. package/dist/core/queryUtils.d.ts +45 -0
  118. package/dist/core/queryUtils.js +69 -0
  119. package/dist/core/storeContract.d.ts +145 -0
  120. package/dist/core/storeContract.js +12 -0
  121. package/dist/environment.d.ts +28 -0
  122. package/dist/environment.js +21 -0
  123. package/dist/errorCodes.d.ts +118 -101
  124. package/dist/errorCodes.js +277 -260
  125. package/dist/errors.d.ts +170 -165
  126. package/dist/errors.js +161 -151
  127. package/dist/index.d.ts +30 -27
  128. package/dist/index.js +90 -82
  129. package/dist/interfaces/index.d.ts +108 -133
  130. package/dist/interfaces/index.js +5 -4
  131. package/dist/keys/index.d.ts +27 -29
  132. package/dist/keys/index.js +59 -49
  133. package/dist/mutators/RecordingTransaction.d.ts +16 -16
  134. package/dist/mutators/RecordingTransaction.js +31 -37
  135. package/dist/mutators/Transaction.d.ts +18 -26
  136. package/dist/mutators/Transaction.js +14 -20
  137. package/dist/mutators/UndoManager.d.ts +122 -131
  138. package/dist/mutators/UndoManager.js +149 -155
  139. package/dist/mutators/defineMutators.d.ts +24 -37
  140. package/dist/mutators/defineMutators.js +14 -20
  141. package/dist/mutators/inverseOp.d.ts +12 -15
  142. package/dist/mutators/inverseOp.js +12 -15
  143. package/dist/mutators/mutateActions.d.ts +10 -9
  144. package/dist/mutators/mutateActions.js +1 -1
  145. package/dist/mutators/readerActions.d.ts +9 -8
  146. package/dist/mutators/readerActions.js +2 -2
  147. package/dist/mutators/undoApply.d.ts +31 -27
  148. package/dist/mutators/undoApply.js +26 -24
  149. package/dist/policy/index.d.ts +5 -3
  150. package/dist/policy/index.js +5 -3
  151. package/dist/policy/types.d.ts +105 -101
  152. package/dist/policy/types.js +67 -66
  153. package/dist/query/client.d.ts +32 -16
  154. package/dist/query/client.js +103 -72
  155. package/dist/query/types.d.ts +37 -60
  156. package/dist/query/types.js +13 -33
  157. package/dist/react/AbloProvider.d.ts +7 -11
  158. package/dist/react/AbloProvider.js +24 -17
  159. package/dist/react/context.d.ts +27 -146
  160. package/dist/react/context.js +9 -10
  161. package/dist/react/index.d.ts +41 -42
  162. package/dist/react/index.js +37 -38
  163. package/dist/react/internalContext.d.ts +17 -19
  164. package/dist/react/useAblo.d.ts +23 -22
  165. package/dist/react/useAblo.js +17 -15
  166. package/dist/react/useCurrentUserId.d.ts +8 -7
  167. package/dist/react/useCurrentUserId.js +8 -7
  168. package/dist/react/useErrorListener.d.ts +7 -7
  169. package/dist/react/useErrorListener.js +11 -12
  170. package/dist/react/useMutationFailureListener.d.ts +8 -8
  171. package/dist/react/useMutationFailureListener.js +9 -9
  172. package/dist/react/useMutators.d.ts +11 -11
  173. package/dist/react/useMutators.js +10 -4
  174. package/dist/react/useReactive.js +2 -3
  175. package/dist/react/useSyncStatus.d.ts +4 -6
  176. package/dist/react/useUndoScope.d.ts +7 -9
  177. package/dist/react/useUndoScope.js +3 -3
  178. package/dist/schema/coordination.d.ts +21 -25
  179. package/dist/schema/coordination.js +21 -25
  180. package/dist/schema/ddl.d.ts +43 -39
  181. package/dist/schema/ddl.js +75 -68
  182. package/dist/schema/ddlLock.d.ts +35 -0
  183. package/dist/schema/ddlLock.js +46 -0
  184. package/dist/schema/diff.d.ts +99 -61
  185. package/dist/schema/diff.js +43 -34
  186. package/dist/schema/field.d.ts +37 -42
  187. package/dist/schema/field.js +36 -49
  188. package/dist/schema/generate.d.ts +12 -12
  189. package/dist/schema/generate.js +12 -12
  190. package/dist/schema/index.d.ts +5 -4
  191. package/dist/schema/index.js +29 -21
  192. package/dist/schema/model.d.ts +121 -146
  193. package/dist/schema/model.js +24 -35
  194. package/dist/schema/openapi.d.ts +10 -9
  195. package/dist/schema/openapi.js +7 -1
  196. package/dist/schema/queries.d.ts +30 -32
  197. package/dist/schema/queries.js +24 -25
  198. package/dist/schema/relation.d.ts +89 -99
  199. package/dist/schema/relation.js +13 -13
  200. package/dist/schema/residency.d.ts +38 -0
  201. package/dist/schema/residency.js +30 -0
  202. package/dist/schema/roles.d.ts +45 -27
  203. package/dist/schema/roles.js +52 -21
  204. package/dist/schema/schema.d.ts +36 -45
  205. package/dist/schema/schema.js +42 -39
  206. package/dist/schema/select.d.ts +13 -13
  207. package/dist/schema/select.js +13 -13
  208. package/dist/schema/serialize.d.ts +36 -39
  209. package/dist/schema/serialize.js +27 -31
  210. package/dist/schema/sugar.d.ts +17 -32
  211. package/dist/schema/sugar.js +14 -29
  212. package/dist/schema/{sync-delta-row.d.ts → syncDeltaRow.d.ts} +27 -50
  213. package/dist/schema/syncDeltaRow.js +89 -0
  214. package/dist/schema/tenancy.d.ts +44 -46
  215. package/dist/schema/tenancy.js +46 -48
  216. package/dist/server/adapter.d.ts +58 -58
  217. package/dist/server/adapter.js +13 -14
  218. package/dist/server/commit.d.ts +60 -64
  219. package/dist/server/index.d.ts +9 -10
  220. package/dist/server/index.js +1 -1
  221. package/dist/server/readConfig.d.ts +70 -0
  222. package/dist/server/readConfig.js +8 -0
  223. package/dist/server/storageMode.d.ts +23 -0
  224. package/dist/server/storageMode.js +17 -0
  225. package/dist/source/adapter.d.ts +31 -26
  226. package/dist/source/adapter.js +10 -10
  227. package/dist/source/adapters/drizzle.d.ts +28 -23
  228. package/dist/source/adapters/drizzle.js +34 -28
  229. package/dist/source/adapters/kysely.d.ts +27 -25
  230. package/dist/source/adapters/kysely.js +28 -26
  231. package/dist/source/adapters/memory.d.ts +8 -7
  232. package/dist/source/adapters/memory.js +10 -9
  233. package/dist/source/adapters/prisma.d.ts +13 -12
  234. package/dist/source/adapters/prisma.js +27 -29
  235. package/dist/source/conformance.d.ts +18 -11
  236. package/dist/source/conformance.js +27 -19
  237. package/dist/source/connector.d.ts +31 -32
  238. package/dist/source/connector.js +30 -28
  239. package/dist/source/connectorProtocol.d.ts +160 -0
  240. package/dist/source/connectorProtocol.js +162 -0
  241. package/dist/source/contract.d.ts +26 -27
  242. package/dist/source/contract.js +28 -29
  243. package/dist/source/factory.d.ts +94 -0
  244. package/dist/source/factory.js +268 -0
  245. package/dist/source/index.d.ts +10 -462
  246. package/dist/source/index.js +17 -421
  247. package/dist/source/migrations.d.ts +9 -9
  248. package/dist/source/migrations.js +9 -9
  249. package/dist/source/next.d.ts +10 -11
  250. package/dist/source/next.js +7 -8
  251. package/dist/source/pushQueue.d.ts +70 -48
  252. package/dist/source/pushQueue.js +36 -29
  253. package/dist/source/signing.d.ts +88 -0
  254. package/dist/source/signing.js +159 -0
  255. package/dist/source/types.d.ts +351 -0
  256. package/dist/source/types.js +43 -0
  257. package/dist/stores/ObjectStore.d.ts +11 -12
  258. package/dist/stores/ObjectStore.js +34 -35
  259. package/dist/stores/ObjectStoreContract.d.ts +12 -15
  260. package/dist/stores/SyncActionStore.d.ts +8 -12
  261. package/dist/stores/SyncActionStore.js +77 -46
  262. package/dist/surface.d.ts +28 -21
  263. package/dist/surface.js +28 -20
  264. package/dist/sync/{BootstrapHelper.d.ts → BootstrapFetcher.d.ts} +37 -45
  265. package/dist/sync/{BootstrapHelper.js → BootstrapFetcher.js} +101 -80
  266. package/dist/sync/ConnectionManager.d.ts +47 -50
  267. package/dist/sync/ConnectionManager.js +74 -70
  268. package/dist/sync/NetworkProbe.d.ts +27 -31
  269. package/dist/sync/NetworkProbe.js +67 -72
  270. package/dist/sync/{HydrationCoordinator.d.ts → OnDemandLoader.d.ts} +49 -36
  271. package/dist/sync/{HydrationCoordinator.js → OnDemandLoader.js} +79 -54
  272. package/dist/sync/{AreaOfInterestManager.d.ts → SubscriptionManager.d.ts} +45 -59
  273. package/dist/sync/{AreaOfInterestManager.js → SubscriptionManager.js} +44 -52
  274. package/dist/sync/SyncWebSocket.d.ts +175 -250
  275. package/dist/sync/SyncWebSocket.js +431 -769
  276. package/dist/sync/awaitClaimGrant.d.ts +18 -18
  277. package/dist/sync/awaitClaimGrant.js +38 -30
  278. package/dist/sync/bootstrapApply.d.ts +70 -0
  279. package/dist/sync/bootstrapApply.js +73 -0
  280. package/dist/sync/commitFrames.d.ts +44 -0
  281. package/dist/sync/commitFrames.js +94 -0
  282. package/dist/sync/createClaimStream.d.ts +23 -22
  283. package/dist/sync/createClaimStream.js +108 -25
  284. package/dist/sync/createPresenceStream.d.ts +19 -18
  285. package/dist/sync/createPresenceStream.js +25 -26
  286. package/dist/sync/createSnapshot.d.ts +13 -17
  287. package/dist/sync/createSnapshot.js +20 -26
  288. package/dist/sync/credentialLifecycle.d.ts +175 -0
  289. package/dist/sync/credentialLifecycle.js +322 -0
  290. package/dist/sync/deltaPipeline.d.ts +113 -0
  291. package/dist/sync/deltaPipeline.js +261 -0
  292. package/dist/sync/groupChange.d.ts +113 -0
  293. package/dist/sync/groupChange.js +242 -0
  294. package/dist/sync/heartbeat.d.ts +63 -0
  295. package/dist/sync/heartbeat.js +91 -0
  296. package/dist/sync/participants.d.ts +27 -27
  297. package/dist/sync/schemas.d.ts +3 -2
  298. package/dist/sync/schemas.js +14 -10
  299. package/dist/sync/syncCursor.d.ts +40 -0
  300. package/dist/sync/syncCursor.js +55 -0
  301. package/dist/sync/syncPlan.d.ts +54 -0
  302. package/dist/sync/syncPlan.js +50 -0
  303. package/dist/sync/syncPosition.d.ts +54 -49
  304. package/dist/sync/syncPosition.js +57 -52
  305. package/dist/sync/wsFrameHandlers.d.ts +116 -0
  306. package/dist/sync/wsFrameHandlers.js +374 -0
  307. package/dist/testing/fixtures/bootstrap.d.ts +21 -17
  308. package/dist/testing/fixtures/bootstrap.js +12 -6
  309. package/dist/testing/fixtures/deltas.d.ts +31 -34
  310. package/dist/testing/fixtures/deltas.js +30 -33
  311. package/dist/testing/fixtures/models.d.ts +11 -10
  312. package/dist/testing/fixtures/models.js +12 -10
  313. package/dist/testing/helpers/{react-wrapper.d.ts → reactWrapper.d.ts} +13 -10
  314. package/dist/testing/helpers/{react-wrapper.js → reactWrapper.js} +15 -12
  315. package/dist/testing/helpers/{sync-engine-harness.d.ts → syncEngineHarness.d.ts} +17 -18
  316. package/dist/testing/helpers/{sync-engine-harness.js → syncEngineHarness.js} +14 -11
  317. package/dist/testing/helpers/wait.d.ts +13 -8
  318. package/dist/testing/helpers/wait.js +13 -8
  319. package/dist/testing/index.d.ts +4 -4
  320. package/dist/testing/index.js +3 -3
  321. package/dist/testing/mocks/MockMutationExecutor.d.ts +18 -17
  322. package/dist/testing/mocks/MockMutationExecutor.js +15 -14
  323. package/dist/testing/mocks/MockNetworkMonitor.d.ts +8 -8
  324. package/dist/testing/mocks/MockNetworkMonitor.js +8 -8
  325. package/dist/testing/mocks/MockSyncContext.d.ts +21 -34
  326. package/dist/testing/mocks/MockSyncContext.js +16 -45
  327. package/dist/testing/mocks/MockSyncStore.d.ts +14 -14
  328. package/dist/testing/mocks/MockSyncStore.js +11 -11
  329. package/dist/testing/mocks/MockWebSocket.d.ts +28 -24
  330. package/dist/testing/mocks/MockWebSocket.js +22 -21
  331. package/dist/transactions/TransactionQueue.d.ts +190 -221
  332. package/dist/transactions/TransactionQueue.js +424 -822
  333. package/dist/transactions/TransactionStore.d.ts +20 -0
  334. package/dist/transactions/TransactionStore.js +53 -0
  335. package/dist/transactions/UnconfirmedWrites.d.ts +82 -0
  336. package/dist/transactions/UnconfirmedWrites.js +104 -0
  337. package/dist/transactions/coalesceRules.d.ts +58 -0
  338. package/dist/transactions/coalesceRules.js +140 -0
  339. package/dist/transactions/commitPayload.d.ts +130 -0
  340. package/dist/transactions/commitPayload.js +143 -0
  341. package/dist/transactions/deltaConfirmation.d.ts +58 -0
  342. package/dist/transactions/deltaConfirmation.js +215 -0
  343. package/dist/transactions/optimisticApply.d.ts +49 -0
  344. package/dist/transactions/optimisticApply.js +65 -0
  345. package/dist/transactions/replayValidation.d.ts +99 -0
  346. package/dist/transactions/replayValidation.js +111 -0
  347. package/dist/types/global.d.ts +46 -41
  348. package/dist/types/global.js +20 -19
  349. package/dist/types/index.d.ts +74 -80
  350. package/dist/types/index.js +22 -27
  351. package/dist/types/modelData.d.ts +10 -0
  352. package/dist/types/modelData.js +9 -0
  353. package/dist/types/participant.d.ts +20 -0
  354. package/dist/types/participant.js +10 -0
  355. package/dist/types/streams.d.ts +216 -209
  356. package/dist/types/streams.js +7 -7
  357. package/dist/utils/asyncIterator.d.ts +25 -32
  358. package/dist/utils/asyncIterator.js +25 -32
  359. package/dist/utils/duration.d.ts +12 -15
  360. package/dist/utils/duration.js +12 -15
  361. package/dist/utils/mobxSetup.d.ts +53 -0
  362. package/dist/utils/{mobx-setup.js → mobxSetup.js} +44 -100
  363. package/dist/webhooks/events.d.ts +21 -16
  364. package/dist/webhooks/events.js +10 -8
  365. package/dist/webhooks/index.d.ts +5 -7
  366. package/dist/webhooks/index.js +5 -7
  367. package/dist/{schema/sync-delta-wire.d.ts → wire/delta.d.ts} +58 -41
  368. package/dist/wire/delta.js +114 -0
  369. package/dist/wire/errorEnvelope.d.ts +35 -27
  370. package/dist/wire/errorEnvelope.js +38 -32
  371. package/dist/wire/frames.d.ts +150 -67
  372. package/dist/wire/frames.js +48 -1
  373. package/dist/wire/index.d.ts +18 -13
  374. package/dist/wire/index.js +36 -13
  375. package/dist/wire/listEnvelope.d.ts +16 -23
  376. package/dist/wire/listEnvelope.js +7 -6
  377. package/dist/wire/protocol.d.ts +38 -0
  378. package/dist/wire/protocol.js +38 -0
  379. package/dist/wire/protocolVersion.d.ts +60 -0
  380. package/dist/wire/protocolVersion.js +67 -0
  381. package/docs/api-keys.md +4 -3
  382. package/docs/coordination.md +59 -0
  383. package/docs/examples/existing-python-backend.md +3 -3
  384. package/docs/identity.md +4 -4
  385. package/docs/integration-guide.md +1 -1
  386. package/docs/react.md +1 -1
  387. package/docs/sessions.md +5 -7
  388. package/package.json +24 -21
  389. package/dist/ai-sdk/coordinated-tool.d.ts +0 -101
  390. package/dist/ai-sdk/coordination-context.d.ts +0 -52
  391. package/dist/client/index.d.ts +0 -36
  392. package/dist/client/index.js +0 -33
  393. package/dist/config/index.d.ts +0 -10
  394. package/dist/config/index.js +0 -12
  395. package/dist/core/query-utils.d.ts +0 -34
  396. package/dist/core/query-utils.js +0 -59
  397. package/dist/interfaces/headless.d.ts +0 -95
  398. package/dist/interfaces/headless.js +0 -41
  399. package/dist/query/index.d.ts +0 -6
  400. package/dist/query/index.js +0 -5
  401. package/dist/realtime/index.d.ts +0 -10
  402. package/dist/realtime/index.js +0 -9
  403. package/dist/schema/plane.d.ts +0 -23
  404. package/dist/schema/plane.js +0 -19
  405. package/dist/schema/sync-delta-row.js +0 -103
  406. package/dist/schema/sync-delta-wire.js +0 -102
  407. package/dist/server/next.d.ts +0 -51
  408. package/dist/server/next.js +0 -47
  409. package/dist/server/read-config.d.ts +0 -67
  410. package/dist/server/read-config.js +0 -8
  411. package/dist/server/storage-mode.d.ts +0 -1
  412. package/dist/server/storage-mode.js +0 -18
  413. package/dist/source/connector-protocol.d.ts +0 -159
  414. package/dist/source/connector-protocol.js +0 -161
  415. package/dist/sync/OfflineFlush.d.ts +0 -9
  416. package/dist/sync/OfflineFlush.js +0 -22
  417. package/dist/sync/OfflineTransactionStore.d.ts +0 -37
  418. package/dist/sync/OfflineTransactionStore.js +0 -263
  419. package/dist/transactions/OptimisticEchoTracker.d.ts +0 -82
  420. package/dist/transactions/OptimisticEchoTracker.js +0 -104
  421. package/dist/transactions/index.d.ts +0 -16
  422. package/dist/transactions/index.js +0 -7
  423. package/dist/transactions/mutation-error-handler.d.ts +0 -5
  424. package/dist/transactions/mutation-error-handler.js +0 -39
  425. package/dist/utils/mobx-setup.d.ts +0 -42
@@ -1,14 +1,15 @@
1
1
  /**
2
- * SyncClient - Mutation and offline queue manager
3
- *
4
- * Responsibilities:
5
- * - Handle model mutations (create, update, delete, archive)
6
- * - Manage offline mutation queue with persistence
7
- * - Send mutations to server via API client
8
- * - Handle conflict resolution for local changes
2
+ * Applies model mutations and manages the offline write queue. The
3
+ * SyncClient turns local create, update, delete, and archive calls into
4
+ * optimistic changes, holds them while the client is offline, sends them to
5
+ * the server when connectivity returns, and resolves conflicts when the
6
+ * server's version of a row disagrees with the local one. It sits between the
7
+ * reactive object pool and the {@link TransactionQueue} that delivers writes
8
+ * over the network.
9
9
  */
10
10
  import { runInAction } from 'mobx';
11
- import { ModelScope } from './ObjectPool.js';
11
+ import { InstanceCache, ModelScope } from './InstanceCache.js';
12
+ import { Model } from './Model.js';
12
13
  // ModelRegistry instance accessed via this.objectPool.registry
13
14
  import { LoadStrategy } from './types/index.js';
14
15
  import { getContext } from './context.js';
@@ -16,16 +17,18 @@ import { AbloAuthenticationError, AbloError, AbloValidationError } from './error
16
17
  import { EventEmitter } from 'events';
17
18
  import { NetworkMonitor } from './NetworkMonitor.js';
18
19
  import { TransactionQueue } from './transactions/TransactionQueue.js';
19
- import { OptimisticEchoTracker, } from './transactions/OptimisticEchoTracker.js';
20
+ import { persistedMutationSchema } from './transactions/replayValidation.js';
21
+ import { UnconfirmedWrites, } from './transactions/UnconfirmedWrites.js';
20
22
  import { SyncPosition } from './sync/syncPosition.js';
21
23
  /**
22
- * Is the raw snapshot record strictly newer than the pooled model? Compares
23
- * server-stamped `updatedAt` (the engine has no numeric row version; the delta
24
- * pipeline is arrival-ordered last-write-wins). An undefined incoming time is
25
- * treated as NOT newer (don't clobber a known row); an undefined existing time
26
- * means the pool row is unversioned, so the incoming record wins. Used by the
27
- * scoped hydrate-on-enter apply to drop snapshot rows a live delta already
28
- * advanced past the snapshot watermark.
24
+ * Reports whether an incoming snapshot record is strictly newer than the
25
+ * model already in the pool. The comparison uses the server-stamped
26
+ * `updatedAt` timestamp, since rows carry no numeric version and the delta
27
+ * pipeline resolves order by arrival (last write wins). An undefined incoming
28
+ * timestamp counts as not newer, so a known row is never clobbered; an
29
+ * undefined existing timestamp means the pooled row is unversioned, so the
30
+ * incoming record wins. The scoped hydrate-on-enter path uses this to drop
31
+ * snapshot rows that a live delta has already advanced past.
29
32
  */
30
33
  function rawRecordIsNewer(data, existing) {
31
34
  const raw = data.updatedAt;
@@ -43,6 +46,22 @@ function rawRecordIsNewer(data, existing) {
43
46
  return true;
44
47
  return inMs > exMs;
45
48
  }
49
+ /**
50
+ * Converts an untyped server `updatedAt` value — an ISO string, epoch number,
51
+ * or Date read off an untyped row — into epoch milliseconds for
52
+ * last-write-wins comparison. Falsy or non-date values become 0, matching the
53
+ * conflict resolver's rule that a missing timestamp sorts as the epoch.
54
+ */
55
+ function toEpochMs(value) {
56
+ if (!value)
57
+ return 0;
58
+ if (value instanceof Date)
59
+ return value.getTime();
60
+ if (typeof value === 'string' || typeof value === 'number') {
61
+ return new Date(value).getTime();
62
+ }
63
+ return 0;
64
+ }
46
65
  export class SyncClient extends EventEmitter {
47
66
  objectPool;
48
67
  database;
@@ -56,32 +75,28 @@ export class SyncClient extends EventEmitter {
56
75
  // Pending mutations queue
57
76
  pendingMutations = [];
58
77
  /**
59
- * Tracks transaction ids the client has optimistically applied but
60
- * the server has not yet confirmed. The receive path consults it
61
- * to recognize delta echoes of own mutations and suppress the
62
- * (otherwise-redundant) pool mutation the IDB write still runs
63
- * because the delta is the authoritative version of the row.
64
- *
65
- * The receive-layer discriminator named in
66
- * `apps/sync-server/docs/OPTIMISTIC_RECONCILIATION.md`. Without
67
- * it, an optimistically-applied DELETE followed by a
68
- * server-confirming CREATE echo resurrects the row for the window
69
- * between the two confirmations (the chart-delete flicker).
78
+ * Tracks the ids of transactions the client has applied optimistically but
79
+ * the server has not yet confirmed. When a delta arrives, the receive path
80
+ * consults this set to recognize the echo of the client's own mutation and
81
+ * skip the now-redundant pool update; the IndexedDB write still runs,
82
+ * because the delta is the authoritative version of the row. Without this
83
+ * discriminator, an optimistically applied delete followed by a
84
+ * server-confirmed create echo would resurrect the row for the window
85
+ * between the two confirmations.
70
86
  *
71
- * Bounded with FIFO eviction; observability via `getEchoMetrics()`.
87
+ * The set is bounded with first-in-first-out eviction, and
88
+ * {@link SyncClient.getEchoMetrics} exposes its counters.
72
89
  */
73
- echoTracker = new OptimisticEchoTracker();
90
+ echoTracker = new UnconfirmedWrites();
74
91
  // Connection state
75
92
  connectionState = 'disconnected';
76
- offlineSince;
77
93
  // Configuration
78
- maxRetries = 3;
79
94
  isDisposed = false;
80
95
  /**
81
- * THE client's place in the global delta order the one canonical
82
- * instance (see `sync/syncPosition.ts`). The store advances
83
- * `applied`/`persisted` as deltas land; the queue advances `acked` on
84
- * commit responses; snapshots/claims read `readFloor`.
96
+ * The client's position in the global delta order, held as the single
97
+ * canonical {@link SyncPosition} instance. The store advances `applied` and
98
+ * `persisted` as deltas land, the queue advances `acked` on commit
99
+ * responses, and snapshots and claims read `readFloor`.
85
100
  */
86
101
  position = new SyncPosition();
87
102
  constructor(objectPool, database) {
@@ -92,8 +107,8 @@ export class SyncClient extends EventEmitter {
92
107
  // Initialize TransactionQueue with proper configuration
93
108
  this.transactionQueue = new TransactionQueue({
94
109
  position: this.position,
95
- maxBatchSize: 50, // Increased from 10 to reduce batch count for large operations
96
- // Lower delay for snappier dev UX; batching still happens via coalescing
110
+ maxBatchSize: 50, // Larger batches keep the batch count low for bulk operations
111
+ // A short delay keeps writes responsive; coalescing still groups them
97
112
  batchDelay: 150,
98
113
  maxRetries: 3,
99
114
  enableOptimistic: true,
@@ -104,16 +119,17 @@ export class SyncClient extends EventEmitter {
104
119
  });
105
120
  // Provide connection state to TransactionQueue - prevents rollbacks during disconnection
106
121
  this.transactionQueue.setConnectionChecker(() => this.connectionState === 'connected');
107
- // LINEAR PATTERN: Subscribe to rollback events to restore ObjectPool state
108
- // When a transaction fails (server rejects or timeout), we need to restore the model
109
- // Since we no longer write to IndexedDB optimistically, IndexedDB already has correct state
122
+ // Restore object-pool state when a transaction is rolled back. If the
123
+ // server rejects a write or it times out, the model's previous state is
124
+ // put back. Because writes are no longer applied to IndexedDB
125
+ // optimistically, that store already holds the correct state.
110
126
  this.setupTransactionRollbackHandling();
111
- // REPLICACHE PATTERN: Forward reconciliation requests from TransactionQueue
112
- // When delta confirmation times out, instead of rolling back we request the sync layer
113
- // to cycle the WebSocket connection, triggering a delta catch-up from the server
127
+ // Forward reconciliation requests from the transaction queue. When delta
128
+ // confirmation times out, the client cycles the WebSocket connection to
129
+ // trigger a catch-up from the server rather than rolling the write back.
114
130
  this.setupReconciliationForwarding();
115
- // LINEAR PATTERN: Persist unconfirmed transactions to IndexedDB
116
- // When delta retries exhaust, cache in IDB so they survive tab close
131
+ // Persist unconfirmed transactions to IndexedDB. When delta retries are
132
+ // exhausted, the write is cached so it survives a tab close.
117
133
  this.setupAwaitingTransactionPersistence();
118
134
  // Setup network monitoring
119
135
  this.setupNetworkMonitoring();
@@ -122,8 +138,25 @@ export class SyncClient extends EventEmitter {
122
138
  * Setup network monitoring handlers
123
139
  */
124
140
  setupNetworkMonitoring() {
125
- this.networkMonitor.on('online', () => this.handleReconnection());
126
- this.networkMonitor.on('offline', () => this.handleDisconnection());
141
+ // Both handlers emit to external listeners (which can throw) before/around
142
+ // their own try/catch — route rejections into observability rather than
143
+ // losing a failed reconnect flush silently.
144
+ this.networkMonitor.on('online', () => {
145
+ void this.handleReconnection().catch((error) => {
146
+ getContext().observability.captureTransactionFailure({
147
+ context: 'network-online-reconnection',
148
+ error: error instanceof Error ? error : new Error(String(error)),
149
+ });
150
+ });
151
+ });
152
+ this.networkMonitor.on('offline', () => {
153
+ void this.handleDisconnection().catch((error) => {
154
+ getContext().observability.captureTransactionFailure({
155
+ context: 'network-offline-handler',
156
+ error: error instanceof Error ? error : new Error(String(error)),
157
+ });
158
+ });
159
+ });
127
160
  }
128
161
  /**
129
162
  * Handle transaction rollback. Two distinct shapes flow through this
@@ -254,10 +287,10 @@ export class SyncClient extends EventEmitter {
254
287
  });
255
288
  }
256
289
  /**
257
- * Forward reconciliation requests from TransactionQueue to the sync layer.
258
- * When delta confirmation times out, TransactionQueue emits 'reconciliation:needed'
259
- * instead of rolling back following the Replicache/PowerSync pattern of never
260
- * destroying optimistic state that the server may have committed.
290
+ * Forward reconciliation requests from the {@link TransactionQueue} to the
291
+ * sync layer. When delta confirmation times out, the queue emits
292
+ * `reconciliation:needed` instead of rolling back, so optimistic state the
293
+ * server may already have committed is never destroyed.
261
294
  */
262
295
  setupReconciliationForwarding() {
263
296
  this.transactionQueue.on('reconciliation:needed', (event) => {
@@ -275,52 +308,20 @@ export class SyncClient extends EventEmitter {
275
308
  });
276
309
  }
277
310
  /**
278
- * LINEAR PATTERN: Persist unconfirmed transactions to IndexedDB.
279
- * When delta confirmation retries exhaust, the transaction data is cached in IDB
280
- * so it survives tab close. On next session, WebSocket reconnect + delta catch-up
281
- * will deliver the missing deltas and naturally confirm the transaction.
311
+ * Persist unconfirmed transactions to IndexedDB. When delta-confirmation
312
+ * retries are exhausted, the transaction is cached so it survives a tab
313
+ * close. On the next session, a WebSocket reconnect and delta catch-up
314
+ * deliver the missing deltas and confirm the transaction.
282
315
  */
283
316
  setupAwaitingTransactionPersistence() {
284
- this.transactionQueue.on('transaction:persist_awaiting', async (event) => {
285
- if (!this.database)
286
- return;
287
- try {
288
- await this.database.saveTransaction({
289
- id: `awaiting_${event.txId}`,
290
- type: 'awaiting_delta',
291
- timestamp: Date.now(),
292
- awaitingDelta: {
293
- syncIdNeeded: event.syncIdNeeded ?? 0,
294
- modelName: event.model,
295
- modelId: event.modelId,
296
- operationType: event.operationType,
297
- },
298
- });
299
- getContext().observability.breadcrumb('Persisted unconfirmed transaction to IDB', 'sync.transaction', 'info', {
300
- txId: event.txId,
301
- model: event.model,
302
- modelId: event.modelId,
303
- });
304
- }
305
- catch (error) {
306
- getContext().observability.captureTransactionFailure({
307
- context: 'persist-awaiting-transaction',
308
- modelName: event.model,
309
- modelId: event.modelId,
310
- error: error instanceof Error ? error : new Error(String(error)),
311
- });
312
- }
317
+ this.transactionQueue.on('transaction:persist_awaiting', (event) => {
318
+ // void is safe: the handler's body is fully try/catch'd.
319
+ void this.persistAwaitingTransaction(event);
313
320
  });
314
321
  // Clean up persisted awaiting transactions when they're finally confirmed
315
- this.transactionQueue.on('transaction:completed', async (tx) => {
316
- if (!this.database)
317
- return;
318
- try {
319
- await this.database.removeTransaction(`awaiting_${tx.id}`);
320
- }
321
- catch {
322
- // Ignore — might not have been persisted
323
- }
322
+ this.transactionQueue.on('transaction:completed', (tx) => {
323
+ // void is safe: the handler's body is fully try/catch'd.
324
+ void this.removeAwaitingTransaction(tx.id);
324
325
  });
325
326
  // Echo detection bridge. When the queue stages a transaction, the
326
327
  // client has already optimistically applied the change to the
@@ -338,6 +339,48 @@ export class SyncClient extends EventEmitter {
338
339
  this.echoTracker.drainOnRollback(event.transaction.id);
339
340
  });
340
341
  }
342
+ /** Persist an unconfirmed transaction to IndexedDB (never rejects — failures are captured). */
343
+ async persistAwaitingTransaction(event) {
344
+ if (!this.database)
345
+ return;
346
+ try {
347
+ await this.database.saveTransaction({
348
+ id: `awaiting_${event.txId}`,
349
+ type: 'awaiting_delta',
350
+ timestamp: Date.now(),
351
+ awaitingDelta: {
352
+ syncIdNeeded: event.syncIdNeeded ?? 0,
353
+ modelName: event.model,
354
+ modelId: event.modelId,
355
+ operationType: event.operationType,
356
+ },
357
+ });
358
+ getContext().observability.breadcrumb('Persisted unconfirmed transaction to IDB', 'sync.transaction', 'info', {
359
+ txId: event.txId,
360
+ model: event.model,
361
+ modelId: event.modelId,
362
+ });
363
+ }
364
+ catch (error) {
365
+ getContext().observability.captureTransactionFailure({
366
+ context: 'persist-awaiting-transaction',
367
+ modelName: event.model,
368
+ modelId: event.modelId,
369
+ error: error instanceof Error ? error : new Error(String(error)),
370
+ });
371
+ }
372
+ }
373
+ /** Drop the persisted awaiting-row once confirmed (never rejects). */
374
+ async removeAwaitingTransaction(txId) {
375
+ if (!this.database)
376
+ return;
377
+ try {
378
+ await this.database.removeTransaction(`awaiting_${txId}`);
379
+ }
380
+ catch {
381
+ // Ignore — might not have been persisted
382
+ }
383
+ }
341
384
  /**
342
385
  * Initialize sync client with authentication
343
386
  */
@@ -347,19 +390,17 @@ export class SyncClient extends EventEmitter {
347
390
  getContext().observability.setContext(userId, organizationId);
348
391
  // Restore queued mutations from previous session
349
392
  await this.restoreMutationQueue();
350
- // Check network status via the DI'd OnlineStatusProvider (see interfaces.ts:192).
351
- // In the browser this is wired to the service worker's connectivity signal via
352
- // abloOnlineStatus in ablo-sync-adapters.ts; in Node it returns true (assume
353
- // online) via the browserOnlineStatus fallback. NetworkMonitor still drives
354
- // event-based online/offline transitions below; this read is just the initial
355
- // status snapshot at registerUser() time.
393
+ // Read the initial network status from the injected OnlineStatusProvider.
394
+ // In the browser this reflects the host's connectivity signal; in Node it
395
+ // reports online by default. NetworkMonitor drives the ongoing
396
+ // online/offline transitions below this read is only the initial
397
+ // snapshot taken when identity is set.
356
398
  if (getContext().onlineStatus.isOnline()) {
357
399
  this.setConnectionState('connected');
358
400
  }
359
401
  else {
360
402
  // Offline - start in offline mode
361
403
  this.setConnectionState('disconnected');
362
- this.offlineSince = new Date();
363
404
  this.emit('sync:offline');
364
405
  }
365
406
  }
@@ -375,7 +416,7 @@ export class SyncClient extends EventEmitter {
375
416
  * Self-healing helper for individual model records.
376
417
  *
377
418
  * Two registry-driven repair passes run on every row hydrated from
378
- * IDB or merged from a delta:
419
+ * IndexedDB or merged from a delta:
379
420
  *
380
421
  * 1. **Auto-fill** — for each `autoFill` rule the consumer's schema
381
422
  * declares on this model, copy the corresponding identity value
@@ -431,7 +472,7 @@ export class SyncClient extends EventEmitter {
431
472
  return { data: result, healed };
432
473
  }
433
474
  /**
434
- * Hydrate ObjectPool with data from Database
475
+ * Hydrate InstanceCache with data from Database
435
476
  * Called after bootstrap is complete
436
477
  */
437
478
  async hydrateFromDatabase() {
@@ -483,8 +524,9 @@ export class SyncClient extends EventEmitter {
483
524
  // Persist healed records back to IndexedDB (fire-and-forget, non-blocking)
484
525
  if (recordsToHeal.length > 0 && this.database) {
485
526
  getContext().logger.info(`[SyncClient.hydrate] Persisting ${recordsToHeal.length} healed ${modelType} records to IndexedDB`);
486
- // Use fire-and-forget to not block hydration
487
- Promise.resolve().then(async () => {
527
+ // Use fire-and-forget to not block hydration.
528
+ // void is safe: the handler's body is fully try/catch'd.
529
+ void Promise.resolve().then(async () => {
488
530
  try {
489
531
  for (const { id, data } of recordsToHeal) {
490
532
  await this.database.putRecord(modelType, id, data);
@@ -501,13 +543,6 @@ export class SyncClient extends EventEmitter {
501
543
  });
502
544
  }
503
545
  const typeEnd = typeof performance !== 'undefined' ? performance.now() : Date.now();
504
- // Dev-only hydration summary
505
- if (modelType === 'InboxItem' && process.env.NODE_ENV !== 'production') {
506
- getContext().logger.debug('[SyncClient] InboxItem hydration summary', {
507
- fetched: rawData.length,
508
- added: modelsForType.length,
509
- });
510
- }
511
546
  perTypePerfLogs.push({
512
547
  type: modelType,
513
548
  fetched: rawData.length,
@@ -552,7 +587,7 @@ export class SyncClient extends EventEmitter {
552
587
  catch { }
553
588
  }
554
589
  /**
555
- * Re-hydrate ObjectPool from IndexedDB when the pool already has data.
590
+ * Re-hydrate InstanceCache from IndexedDB when the pool already has data.
556
591
  *
557
592
  * Unlike hydrateFromDatabase() (which uses addBatch and skips existing IDs),
558
593
  * this method properly:
@@ -600,7 +635,8 @@ export class SyncClient extends EventEmitter {
600
635
  if (this.database) {
601
636
  const id = healResult.data.id;
602
637
  const healedData = healResult.data;
603
- Promise.resolve().then(async () => {
638
+ // void is safe: the handler's body is fully try/catch'd.
639
+ void Promise.resolve().then(async () => {
604
640
  try {
605
641
  await this.database.putRecord(modelType, id, healedData);
606
642
  }
@@ -686,13 +722,14 @@ export class SyncClient extends EventEmitter {
686
722
  return stats;
687
723
  }
688
724
  /**
689
- * Mutate model optimistically and queue for server sync.
690
- * IndexedDB is only updated when server confirms via delta packet.
725
+ * Apply a mutation to a model optimistically and queue it for server sync.
726
+ * IndexedDB is updated only once the server confirms the change with a delta
727
+ * packet.
691
728
  *
692
- * CRITICAL: Changes are captured BEFORE poolAction to prevent data loss.
693
- * The captured changes are frozen and passed to queueMutation.
694
- *
695
- * @see src/sync-engine/types/TrackableModel.ts for change capture pattern
729
+ * A model's changes are captured before the pool action runs, because a pool
730
+ * operation such as an upsert can clear the model's local change set;
731
+ * capturing first ensures those changes are never lost. The captured set is
732
+ * frozen and handed to {@link queueMutation}.
696
733
  */
697
734
  mutate(type, model, poolAction, writeOptions) {
698
735
  // No-op UPDATE guard (O(1)). An update with no dirty fields would travel
@@ -709,9 +746,9 @@ export class SyncClient extends EventEmitter {
709
746
  // real write. Only a genuine Model with an empty dirty-set is skipped.
710
747
  if (type === 'update' && model.hasChanges === false)
711
748
  return;
712
- // CRITICAL FIX: Capture changes BEFORE pool action
713
- // Pool operations (especially upsert) can clear _local changes
714
- // By capturing first, we ensure changes are never lost
749
+ // Capture changes before the pool action runs. Pool operations —
750
+ // upsert in particular can clear the model's local changes, so
751
+ // capturing first ensures they are never lost.
715
752
  const capturedChanges = type === 'update' || type === 'create' ? this.captureModelChanges(model) : undefined;
716
753
  poolAction();
717
754
  this.queueMutation({ type, model, timestamp: new Date(), capturedChanges, writeOptions });
@@ -755,11 +792,11 @@ export class SyncClient extends EventEmitter {
755
792
  }
756
793
  /** Add new model (CREATE) - works offline */
757
794
  add(model, options) {
758
- this.mutate('create', model, () => this.objectPool.add(model, ModelScope.live), options);
795
+ this.mutate('create', model, () => { this.objectPool.add(model, ModelScope.live); }, options);
759
796
  }
760
797
  /** Update existing model (UPDATE) - works offline */
761
798
  update(model, options) {
762
- this.mutate('update', model, () => this.objectPool.upsert(model, ModelScope.live), options);
799
+ this.mutate('update', model, () => { this.objectPool.upsert(model, ModelScope.live); }, options);
763
800
  }
764
801
  /**
765
802
  * Update existing model with pre-computed changes.
@@ -822,8 +859,9 @@ export class SyncClient extends EventEmitter {
822
859
  }
823
860
  }
824
861
  /**
825
- * Upload file and create attachment (UPLOAD operation)
826
- * Uses Linear-style pattern with immediate URL generation
862
+ * Upload a file and create its attachment record. The upload runs through
863
+ * the {@link TransactionQueue}, and a model is built from the server's
864
+ * response and added to the pool.
827
865
  */
828
866
  async uploadFile(file, options) {
829
867
  if (!this.userId || !this.organizationId) {
@@ -912,19 +950,20 @@ export class SyncClient extends EventEmitter {
912
950
  }
913
951
  /** Archive model (ARCHIVE) - works offline */
914
952
  archive(model) {
915
- this.mutate('archive', model, () => this.objectPool.updateScope(model.id, ModelScope.archived));
953
+ this.mutate('archive', model, () => { this.objectPool.updateScope(model.id, ModelScope.archived); });
916
954
  }
917
955
  /**
918
- * Append a mutation and schedule its sync work.
956
+ * Append a mutation to the pending queue and schedule its sync work.
919
957
  *
920
- * IDB persistence and the server push are deferred to a microtask so N
921
- * pushes inside the same tick collapse into ONE IDB serialization + ONE
922
- * process call. Without the deferral, queueing 100 mutations (paste,
923
- * PPTX import, AI sandbox layer creation) reserializes the entire
924
- * growing queue 100× O(N²) `model.toJSON()`.
958
+ * IndexedDB persistence and the server push are deferred to a microtask, so
959
+ * many pushes within the same tick collapse into a single serialization and
960
+ * a single process call. Without the deferral, queueing a hundred mutations
961
+ * at once — a large paste, a document import, bulk layer creation would
962
+ * reserialize the whole growing queue a hundred times, an O(N²) cost in
963
+ * `model.toJSON()`.
925
964
  *
926
- * @param mutation.capturedChanges - Pre-captured changes (frozen), used
927
- * to avoid re-reading changes after pool ops that might clear them.
965
+ * @param mutation.capturedChanges - Pre-captured, frozen changes, used to
966
+ * avoid re-reading a model after pool operations that might clear them.
928
967
  */
929
968
  queueMutation(mutation) {
930
969
  this.pendingMutations.push(mutation);
@@ -969,10 +1008,22 @@ export class SyncClient extends EventEmitter {
969
1008
  timestamp: Date.now(),
970
1009
  });
971
1010
  }
972
- catch (error) { }
1011
+ catch (error) {
1012
+ // Best-effort persistence — the in-memory queue still processes; only
1013
+ // a tab close before reconnect loses these. Forensic → debug.
1014
+ getContext().logger.debug('[SyncClient] Failed to persist offline mutation queue', {
1015
+ error: error instanceof Error ? error.message : String(error),
1016
+ });
1017
+ }
973
1018
  }
974
1019
  /**
975
- * Restore mutation queue from IndexedDB
1020
+ * Restore the mutation queue from IndexedDB.
1021
+ *
1022
+ * The persisted record was written by an earlier session, possibly by an
1023
+ * older build of the SDK, so each entry is validated as it is replayed:
1024
+ * corrupt entries are dropped and logged at debug level, and a failure is
1025
+ * never swallowed silently, because the survival of offline writes must be
1026
+ * observable.
976
1027
  */
977
1028
  async restoreMutationQueue() {
978
1029
  if (!this.database || !this.userId)
@@ -982,19 +1033,39 @@ export class SyncClient extends EventEmitter {
982
1033
  const queue = stored.find((t) => t.id === 'mutation-queue');
983
1034
  if (queue?.mutations) {
984
1035
  for (const mutation of queue.mutations) {
985
- const model = this.objectPool.createFromData(mutation.modelData);
1036
+ const parsed = persistedMutationSchema.safeParse(mutation);
1037
+ if (!parsed.success) {
1038
+ getContext().logger.debug('[SyncClient] Dropping malformed persisted mutation', {
1039
+ issues: parsed.error.issues.map((i) => i.path.join('.')).join(', '),
1040
+ });
1041
+ continue;
1042
+ }
1043
+ const model = this.objectPool.createFromData(parsed.data.modelData);
986
1044
  if (model) {
987
1045
  this.pendingMutations.push({
988
- type: mutation.type,
1046
+ type: parsed.data.type,
989
1047
  model,
990
- timestamp: new Date(mutation.timestamp),
991
- writeOptions: mutation.writeOptions,
1048
+ timestamp: new Date(parsed.data.timestamp),
1049
+ ...(parsed.data.writeOptions !== undefined
1050
+ ? { writeOptions: parsed.data.writeOptions }
1051
+ : {}),
992
1052
  });
993
1053
  }
994
1054
  }
995
1055
  }
996
1056
  }
997
- catch (error) { }
1057
+ catch (error) {
1058
+ // A restore failure means queued offline writes did NOT rehydrate.
1059
+ // Self-healing is impossible here (the record may be unreadable), but
1060
+ // the failure must be visible for diagnosis instead of silent loss.
1061
+ getContext().logger.debug('[SyncClient] Failed to restore offline mutation queue', {
1062
+ error: error instanceof Error ? error.message : String(error),
1063
+ });
1064
+ getContext().observability.captureTransactionFailure({
1065
+ context: 'restore-mutation-queue',
1066
+ error: error instanceof Error ? error : String(error),
1067
+ });
1068
+ }
998
1069
  }
999
1070
  /**
1000
1071
  * Process pending mutations - can be called by SyncedStore when online
@@ -1035,8 +1106,8 @@ export class SyncClient extends EventEmitter {
1035
1106
  this.pendingMutations = [];
1036
1107
  // Clear persisted queue before processing
1037
1108
  await this.persistMutationQueue();
1038
- // LINEAR PATTERN: Stage all mutations synchronously in same event loop tick
1039
- // TransactionQueue's microtask will batch and send them together
1109
+ // Stage every mutation synchronously within the same event-loop tick;
1110
+ // the transaction queue's microtask batches and sends them together.
1040
1111
  for (const mutation of mutations) {
1041
1112
  // Skip mutations for deleted models (prevents "not found" errors)
1042
1113
  if (mutation.type !== 'delete' && !this.objectPool.get(mutation.model.id)) {
@@ -1055,20 +1126,33 @@ export class SyncClient extends EventEmitter {
1055
1126
  if (!this.userId || !this.organizationId)
1056
1127
  return;
1057
1128
  const ctx = { userId: this.userId, organizationId: this.organizationId };
1129
+ // Settlement is delivered via transaction.confirmation, not this promise —
1130
+ // it only rejects when staging itself throws (change extraction, optimistic
1131
+ // apply, store add). That means the write never entered the queue, so
1132
+ // capture it instead of dropping it silently.
1133
+ const captureStagingFailure = (error) => {
1134
+ getContext().observability.captureTransactionFailure({
1135
+ context: `stage-mutation-${mutation.type}`,
1136
+ modelName: mutation.model.getModelName(),
1137
+ modelId: mutation.model.id,
1138
+ error: error instanceof Error ? error : new Error(String(error)),
1139
+ });
1140
+ };
1058
1141
  if (mutation.type === 'update') {
1059
- this.transactionQueue.update(mutation.model, ctx, mutation.capturedChanges, mutation.writeOptions);
1142
+ this.transactionQueue
1143
+ .update(mutation.model, ctx, mutation.capturedChanges, mutation.writeOptions)
1144
+ .catch(captureStagingFailure);
1060
1145
  }
1061
1146
  else {
1062
1147
  const handler = this.transactionQueue[mutation.type].bind(this.transactionQueue);
1063
- handler(mutation.model, ctx, mutation.writeOptions);
1148
+ handler(mutation.model, ctx, mutation.writeOptions).catch(captureStagingFailure);
1064
1149
  }
1065
1150
  }
1066
1151
  /**
1067
- * Resolve conflicts between local and server data
1068
- * Used when processing deltas from WebSocket
1069
- *
1070
- * CRITICAL: Always respects certain server states (deletes, deactivations)
1071
- * even when there are local changes, to maintain data consistency.
1152
+ * Resolve a conflict between the local model and incoming server data,
1153
+ * called while processing deltas from the WebSocket. Certain server states,
1154
+ * such as deletions and deactivations, always take precedence even when the
1155
+ * local model has unsynced changes, so the two sides stay consistent.
1072
1156
  */
1073
1157
  resolveConflicts(localModel, serverData) {
1074
1158
  const hasLocalChanges = localModel.hasChanges;
@@ -1078,7 +1162,7 @@ export class SyncClient extends EventEmitter {
1078
1162
  ? localModel.updatedAt.getTime()
1079
1163
  : new Date(localModel.updatedAt).getTime()
1080
1164
  : 0;
1081
- const serverUpdatedAt = serverData?.updatedAt ? new Date(serverData.updatedAt).getTime() : 0;
1165
+ const serverUpdatedAt = toEpochMs(serverData.updatedAt);
1082
1166
  getContext().logger.debug('Conflict resolution', {
1083
1167
  modelId: localModel.id,
1084
1168
  modelType: localModel.getModelName(),
@@ -1114,7 +1198,7 @@ export class SyncClient extends EventEmitter {
1114
1198
  // Merge: server baseline + local dirty fields win
1115
1199
  const merged = { ...serverData, ...(localChanges || {}) };
1116
1200
  // Preserve the most recent updatedAt without clearing dirty flags
1117
- if (serverData?.updatedAt || localModel.updatedAt) {
1201
+ if (serverData.updatedAt || localModel.updatedAt) {
1118
1202
  const mergedUpdatedAt = new Date(Math.max(localUpdatedAt, serverUpdatedAt));
1119
1203
  // updateFromData accepts Date or ISO string for dates
1120
1204
  merged.updatedAt = mergedUpdatedAt;
@@ -1133,8 +1217,9 @@ export class SyncClient extends EventEmitter {
1133
1217
  return localModel;
1134
1218
  }
1135
1219
  /**
1136
- * Extract critical state fields from server data
1137
- * These are states that must always be respected, even with local changes
1220
+ * Extract the critical state fields from server data. These are the states
1221
+ * that must be honored even when the local model has unsynced changes. The
1222
+ * conflict resolver reads exactly these fields and no others.
1138
1223
  */
1139
1224
  extractCriticalState(serverData) {
1140
1225
  const critical = {};
@@ -1181,8 +1266,6 @@ export class SyncClient extends EventEmitter {
1181
1266
  await this.processPendingMutations();
1182
1267
  this.setConnectionState('connected');
1183
1268
  this.emit('sync:reconnected');
1184
- // Clear offline timestamp
1185
- this.offlineSince = undefined;
1186
1269
  }
1187
1270
  catch (error) {
1188
1271
  getContext().observability.captureTransactionFailure({
@@ -1198,7 +1281,6 @@ export class SyncClient extends EventEmitter {
1198
1281
  async handleDisconnection() {
1199
1282
  getContext().observability.breadcrumb('Network disconnected', 'sync.offline');
1200
1283
  this.setConnectionState('disconnected');
1201
- this.offlineSince = new Date();
1202
1284
  this.emit('sync:offline');
1203
1285
  }
1204
1286
  /**
@@ -1295,9 +1377,10 @@ export class SyncClient extends EventEmitter {
1295
1377
  this.removeAllListeners();
1296
1378
  }
1297
1379
  /**
1298
- * LINEAR PATTERN: Notify TransactionQueue of incoming delta for sync ID threshold confirmation.
1299
- * Transactions are confirmed when any delta with id >= their lastSyncId threshold arrives.
1300
- * @param syncId - The sync ID of the received delta
1380
+ * Notify the {@link TransactionQueue} of an incoming delta so it can confirm
1381
+ * transactions by sync-id threshold. A transaction is confirmed once any
1382
+ * delta with an id at or beyond its `lastSyncId` threshold arrives.
1383
+ * @param syncId - The sync id of the received delta.
1301
1384
  */
1302
1385
  onDeltaReceived(syncId) {
1303
1386
  try {
@@ -1310,22 +1393,21 @@ export class SyncClient extends EventEmitter {
1310
1393
  }
1311
1394
  }
1312
1395
  /**
1313
- * LINEAR PATTERN: Cancel transactions for orphaned child entities
1314
- *
1315
- * Called by SyncedStore when a DELETE delta arrives for a parent entity.
1316
- * Cancels pending transactions for children that reference the deleted parent.
1396
+ * Cancel pending transactions for child entities orphaned by a parent's
1397
+ * deletion. The store calls this when a delete delta arrives for a parent,
1398
+ * cancelling any queued writes on children that reference it.
1317
1399
  *
1318
- * @param childModelName - The child model type (e.g., 'SlideLayer')
1319
- * @param foreignKey - The FK property name (e.g., 'slideId')
1320
- * @param parentId - The deleted parent's ID
1321
- * @returns Number of transactions cancelled
1400
+ * @param childModelName - The child model type (for example, `SlideLayer`).
1401
+ * @param foreignKey - The foreign-key property name (for example, `slideId`).
1402
+ * @param parentId - The id of the deleted parent.
1403
+ * @returns The number of transactions cancelled.
1322
1404
  */
1323
1405
  cancelTransactionsByForeignKey(childModelName, foreignKey, parentId) {
1324
1406
  return this.transactionQueue.cancelTransactionsByForeignKey(childModelName, foreignKey, parentId);
1325
1407
  }
1326
1408
  /**
1327
- * Wait for a transaction to be confirmed via delta echo (Linear pattern)
1328
- * Delegates to TransactionQueue which already handles timeouts
1409
+ * Wait for a transaction to be confirmed by its delta echo. Delegates to the
1410
+ * {@link TransactionQueue}, which handles the confirmation timeout.
1329
1411
  */
1330
1412
  waitForDeltaConfirmation(transactionId) {
1331
1413
  return this.transactionQueue.waitForConfirmation(transactionId);
@@ -1339,7 +1421,7 @@ export class SyncClient extends EventEmitter {
1339
1421
  /**
1340
1422
  * Get sync statistics. Return type is inferred from the literal so
1341
1423
  * the call site sees the actual shape — `connectionState` narrowed
1342
- * to its three states, `objectPoolStats` typed by `ObjectPool.getStats`.
1424
+ * to its three states, `objectPoolStats` typed by `InstanceCache.getStats`.
1343
1425
  */
1344
1426
  getSyncStats() {
1345
1427
  return {
@@ -1374,25 +1456,26 @@ export class SyncClient extends EventEmitter {
1374
1456
  * can render typed UI (toast keyed by `AbloError.type`, route-level
1375
1457
  * "this entity reverted" boundaries, telemetry).
1376
1458
  *
1377
- * Distinct from `onTransactionEvent('failed', cb)`, which exists only
1378
- * for the legacy parameterless `pendingChanges` counter and intentionally
1379
- * drops the payload. The two coexist — keep the counter callback fast
1380
- * and the typed listener for user-visible surfaces.
1459
+ * Distinct from `onTransactionEvent('failed', cb)`, which serves the
1460
+ * parameterless `pendingChanges` counter and intentionally drops the
1461
+ * payload. The two coexist: the counter callback stays lightweight, while
1462
+ * this typed listener drives user-visible surfaces.
1381
1463
  */
1382
1464
  onMutationFailure(listener) {
1383
1465
  this.transactionQueue.on('transaction:failed', listener);
1384
1466
  return () => this.transactionQueue.off('transaction:failed', listener);
1385
1467
  }
1386
1468
  /**
1387
- * Subscribe to LOCAL transaction creation with the full {@link Transaction}
1469
+ * Subscribe to local transaction creation with the full {@link Transaction}
1388
1470
  * payload (`type`, `modelName`, `modelId`, `data`, `previousData`). This is
1389
- * the feed `BaseSyncedStore.subscribeLocalMutations` taps for undo recording.
1471
+ * the feed the store's local-mutation subscription taps for undo recording.
1390
1472
  *
1391
- * MUST subscribe to the TransactionQueue's emitter directly — that is the
1392
- * ONLY emitter that fires `transaction:created`. SyncClient's own emitter
1393
- * (reached via `subscribe()`) never re-broadcasts it, so routing undo through
1394
- * `subscribe('transaction:created')` silently records nothing. Mirrors
1395
- * `onMutationFailure`, which taps the queue for the same reason.
1473
+ * It subscribes to the {@link TransactionQueue}'s emitter directly, since
1474
+ * that is the only emitter that fires `transaction:created`. The SyncClient's
1475
+ * own emitter (reached through {@link subscribe}) never rebroadcasts that
1476
+ * event, so routing undo through `subscribe('transaction:created')` would
1477
+ * record nothing. {@link onMutationFailure} taps the queue for the same
1478
+ * reason.
1396
1479
  */
1397
1480
  onLocalTransaction(listener) {
1398
1481
  this.transactionQueue.on('transaction:created', listener);
@@ -1488,11 +1571,11 @@ export class SyncClient extends EventEmitter {
1488
1571
  assigneeId,
1489
1572
  });
1490
1573
  }
1491
- // ── Delta + Bootstrap application (owns ObjectPool writes) ──────────────
1574
+ // ── Delta + Bootstrap application (owns InstanceCache writes) ──────────────
1492
1575
  /**
1493
- * Apply a batch of delta results from Database to the ObjectPool.
1576
+ * Apply a batch of delta results from Database to the InstanceCache.
1494
1577
  * Owns: model creation, upsert, remove, archive, conflict resolution.
1495
- * Returns: nothing — ObjectPool is updated in place.
1578
+ * Returns: nothing — InstanceCache is updated in place.
1496
1579
  */
1497
1580
  /**
1498
1581
  * Mark a local transaction as optimistically applied. The matching
@@ -1514,12 +1597,12 @@ export class SyncClient extends EventEmitter {
1514
1597
  return this.echoTracker.getMetrics();
1515
1598
  }
1516
1599
  /**
1517
- * Package-internal accessor for the TransactionQueue. Used by
1518
- * `Ablo.commits.create()` to route raw multi-op envelopes through the
1519
- * same retry-on-reconnect lane as the Model proxy path, and by tests
1520
- * to exercise the queue markTransactionPending wiring on the real
1521
- * instance the SyncClient subscribes to. NOT re-exported to SDK
1522
- * consumers `Ablo` itself is the public surface.
1600
+ * Package-internal accessor for the {@link TransactionQueue}. Used by
1601
+ * `Ablo.commits.create()` to route raw multi-operation envelopes through the
1602
+ * same retry-on-reconnect lane as the model proxy path, and by tests to
1603
+ * exercise the queue's interaction with {@link markTransactionPending} on the
1604
+ * real instance the SyncClient subscribes to. It is not re-exported to SDK
1605
+ * consumers; `Ablo` is the public surface.
1523
1606
  */
1524
1607
  getTransactionQueue() {
1525
1608
  return this.transactionQueue;
@@ -1547,16 +1630,14 @@ export class SyncClient extends EventEmitter {
1547
1630
  }
1548
1631
  for (const result of dbResults) {
1549
1632
  const { modelName, modelId, action, transactionId } = result;
1550
- // ECHO DETECTION. If this delta carries a transaction id that
1551
- // matches one we've optimistically applied locally, the pool
1552
- // already reflects this mutation skip the pool op. The
1553
- // upstream IDB write (in `Database.processDeltaBatch`) still
1554
- // runs; only the in-memory pool mutation is suppressed. This is
1555
- // the architectural fix for the chart-delete flicker: a
1556
- // server-confirmed CREATE arriving AFTER the user has
1557
- // optimistically deleted the row would otherwise re-add the row
1558
- // for the ~2s window before the matching DELETE confirmation
1559
- // lands. See `OPTIMISTIC_RECONCILIATION.md` for the framing.
1633
+ // Echo detection: if this delta carries a transaction id that matches
1634
+ // one already applied optimistically, the pool already reflects the
1635
+ // mutation, so the pool operation is skipped. The IndexedDB write in
1636
+ // Database.processDeltaBatch still runs; only the in-memory pool update
1637
+ // is suppressed. This prevents a resurrection flicker: a server-confirmed
1638
+ // create arriving after the user has optimistically deleted the row would
1639
+ // otherwise re-add it for the brief window before the matching delete
1640
+ // confirmation lands.
1560
1641
  if (this.echoTracker.consumeEcho(transactionId)) {
1561
1642
  continue;
1562
1643
  }
@@ -1622,17 +1703,15 @@ export class SyncClient extends EventEmitter {
1622
1703
  break;
1623
1704
  }
1624
1705
  }
1625
- // Reveal the whole frame in ONE MobX action. `addBatch`/`upsertBatch`/
1626
- // `removeBatch`/`updateScope` are each individually `action`-wrapped,
1627
- // so calling them sequentially flushes reactions at every action
1628
- // boundary — a catch-up frame that adds + updates + removes would fire
1629
- // every dependent reaction (the decks gallery, each open editor) 3-
1630
- // in a row, re-rendering and re-sorting on each. Wrapping them in a
1631
- // single outer `runInAction` defers all reaction flushes to ONE
1632
- // boundary: dependents recompute exactly once regardless of how many
1633
- // models or how many op-kinds the frame touched. This is the MobX
1634
- // equivalent of Replicache's "atomically reveal the new state" — the
1635
- // app never observes a partially-applied frame.
1706
+ // Reveal the whole frame in a single MobX action. `addBatch`,
1707
+ // `upsertBatch`, `removeBatch`, and `updateScope` are each individually
1708
+ // wrapped in an action, so calling them in sequence flushes reactions at
1709
+ // every action boundary — a catch-up frame that adds, updates, and removes
1710
+ // would fire every dependent reaction several times in a row, re-rendering
1711
+ // and re-sorting on each. Wrapping them in one outer `runInAction` defers
1712
+ // all reaction flushes to a single boundary, so dependents recompute
1713
+ // exactly once regardless of how many models or operation kinds the frame
1714
+ // touched. The app therefore never observes a partially applied frame.
1636
1715
  runInAction(() => {
1637
1716
  if (modelsToAdd.length > 0)
1638
1717
  this.objectPool.addBatch(modelsToAdd, ModelScope.live);
@@ -1651,7 +1730,7 @@ export class SyncClient extends EventEmitter {
1651
1730
  });
1652
1731
  }
1653
1732
  /**
1654
- * Apply bootstrap data to the ObjectPool with ghost removal.
1733
+ * Apply bootstrap data to the InstanceCache with ghost removal.
1655
1734
  * Owns: model creation, batch upsert, ghost detection + removal.
1656
1735
  */
1657
1736
  applyBootstrapDataToPool(bootstrapData, protectedIds, options) {
@@ -1689,12 +1768,12 @@ export class SyncClient extends EventEmitter {
1689
1768
  const recordId = data.id;
1690
1769
  if (recordId)
1691
1770
  idsForType.add(recordId);
1692
- // Scoped backfill (P4 hydrate-on-enter): a subset snapshot is taken at
1693
- // a server watermark. If a concurrent live delta already advanced this
1694
- // row past the snapshot, skip it `createFromData` mutates the pooled
1695
- // model IN PLACE (the "keep instances alive" Linear pattern), so this
1696
- // version guard MUST run BEFORE it; an upsert-layer guard would be too
1697
- // late, the row would already be clobbered.
1771
+ // Scoped backfill for the hydrate-on-enter path: a subset snapshot is
1772
+ // taken at a server watermark. If a concurrent live delta already
1773
+ // advanced this row past the snapshot, skip it. `createFromData`
1774
+ // mutates the pooled model in place to keep instances alive, so this
1775
+ // version guard has to run before it; a guard at the upsert layer would
1776
+ // be too late, because the row would already be clobbered.
1698
1777
  if (options?.scoped && recordId) {
1699
1778
  const existing = this.objectPool.get(recordId);
1700
1779
  if (existing && !rawRecordIsNewer(data, existing)) {
@@ -1718,10 +1797,10 @@ export class SyncClient extends EventEmitter {
1718
1797
  this.objectPool.upsertBatch(allModels, ModelScope.live);
1719
1798
  const addedCount = this.objectPool.size - beforeSize;
1720
1799
  const updatedCount = allModels.length - addedCount;
1721
- // Ghost removal remove pool entities not in the server snapshot. Only
1722
- // valid for a FULL bootstrap, where the snapshot is authoritative for each
1723
- // returned type. A SCOPED subset snapshot must NOT remove rows of the same
1724
- // type that belong to other (unhydrated) groups.
1800
+ // Ghost removal: drop pool entities absent from the server snapshot. This
1801
+ // is valid only for a full bootstrap, where the snapshot is authoritative
1802
+ // for each returned type. A scoped subset snapshot must not remove rows of
1803
+ // the same type that belong to other, unhydrated groups.
1725
1804
  let removedCount = 0;
1726
1805
  if (!options?.scoped) {
1727
1806
  const ghostIds = [];