@abloatai/ablo 0.34.1 → 0.36.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 (536) hide show
  1. package/AGENTS.md +2 -1
  2. package/CHANGELOG.md +758 -5
  3. package/README.md +56 -502
  4. package/bin/ablo.cjs +39 -0
  5. package/dist/BaseSyncedStore.d.ts +176 -48
  6. package/dist/BaseSyncedStore.js +346 -214
  7. package/dist/Database.d.ts +17 -44
  8. package/dist/Database.js +96 -79
  9. package/dist/InstanceCache.d.ts +31 -6
  10. package/dist/InstanceCache.js +65 -30
  11. package/dist/LazyReferenceCollection.d.ts +3 -3
  12. package/dist/LazyReferenceCollection.js +4 -4
  13. package/dist/Model.d.ts +23 -13
  14. package/dist/Model.js +27 -17
  15. package/dist/ModelRegistry.d.ts +8 -4
  16. package/dist/ModelRegistry.js +20 -18
  17. package/dist/NetworkMonitor.d.ts +3 -1
  18. package/dist/NetworkMonitor.js +7 -5
  19. package/dist/{SyncEngineContext.d.ts → RuntimeContext.d.ts} +20 -13
  20. package/dist/{SyncEngineContext.js → RuntimeContext.js} +11 -12
  21. package/dist/SyncClient.d.ts +47 -47
  22. package/dist/SyncClient.js +215 -156
  23. package/dist/ai-sdk/coordinatedTool.d.ts +2 -2
  24. package/dist/ai-sdk/coordinatedTool.js +1 -1
  25. package/dist/ai-sdk/coordinationContext.d.ts +2 -2
  26. package/dist/ai-sdk/coordinationContext.js +1 -1
  27. package/dist/ai-sdk/wrap.d.ts +3 -3
  28. package/dist/ai-sdk/wrap.js +2 -2
  29. package/dist/auth/index.d.ts +1 -156
  30. package/dist/auth/index.js +8 -301
  31. package/dist/client/Ablo.d.ts +42 -287
  32. package/dist/client/Ablo.js +129 -963
  33. package/dist/client/abloClient.d.ts +309 -0
  34. package/dist/client/abloClient.js +13 -0
  35. package/dist/client/clientPrelude.d.ts +52 -0
  36. package/dist/client/clientPrelude.js +60 -0
  37. package/dist/client/consoleLogger.d.ts +2 -2
  38. package/dist/client/coreClient.d.ts +60 -0
  39. package/dist/client/coreClient.js +118 -0
  40. package/dist/client/createInternalComponents.d.ts +8 -4
  41. package/dist/client/createInternalComponents.js +17 -10
  42. package/dist/client/createModelProxy.d.ts +98 -373
  43. package/dist/client/createModelProxy.js +233 -139
  44. package/dist/client/humans.d.ts +69 -0
  45. package/dist/client/humans.js +78 -0
  46. package/dist/client/modelRegistration.d.ts +1 -1
  47. package/dist/client/modelRegistration.js +9 -9
  48. package/dist/client/options.d.ts +73 -17
  49. package/dist/client/reactiveEngine.d.ts +53 -0
  50. package/dist/client/reactiveEngine.js +688 -0
  51. package/dist/client/resourceTypes.d.ts +9 -250
  52. package/dist/client/resourceTypes.js +8 -5
  53. package/dist/client/schemaConfig.d.ts +4 -4
  54. package/dist/client/schemaConfig.js +6 -2
  55. package/dist/client/storeCluster.d.ts +47 -0
  56. package/dist/client/storeCluster.js +118 -0
  57. package/dist/client/storeLifecycle.d.ts +61 -0
  58. package/dist/client/storeLifecycle.js +231 -0
  59. package/dist/client/validateAbloOptions.d.ts +3 -2
  60. package/dist/client/validateAbloOptions.js +1 -1
  61. package/dist/client/wsMutationExecutor.d.ts +3 -3
  62. package/dist/client/wsMutationExecutor.js +3 -3
  63. package/dist/context.d.ts +22 -9
  64. package/dist/context.js +33 -9
  65. package/dist/coordination/ClaimLog.d.ts +26 -0
  66. package/dist/coordination/ClaimLog.js +32 -0
  67. package/dist/coordination/index.d.ts +1 -15
  68. package/dist/coordination/index.js +8 -31
  69. package/dist/core/index.d.ts +3 -3
  70. package/dist/core/index.js +2 -2
  71. package/dist/docs/catalog.d.ts +72 -0
  72. package/dist/docs/catalog.js +230 -0
  73. package/dist/docs/index.d.ts +10 -0
  74. package/dist/docs/index.js +10 -0
  75. package/dist/environment.d.ts +1 -40
  76. package/dist/environment.js +8 -37
  77. package/dist/index.d.ts +44 -36
  78. package/dist/index.js +30 -22
  79. package/dist/interfaces/index.d.ts +44 -134
  80. package/dist/keys/index.d.ts +1 -77
  81. package/dist/keys/index.js +8 -190
  82. package/dist/mutators/{RecordingTransaction.d.ts → RecordingMutation.d.ts} +4 -4
  83. package/dist/mutators/{RecordingTransaction.js → RecordingMutation.js} +2 -2
  84. package/dist/mutators/Transaction.d.ts +1 -1
  85. package/dist/mutators/Transaction.js +1 -1
  86. package/dist/mutators/UndoManager.d.ts +6 -6
  87. package/dist/mutators/UndoManager.js +5 -5
  88. package/dist/mutators/defineMutators.d.ts +3 -3
  89. package/dist/mutators/defineMutators.js +1 -1
  90. package/dist/mutators/inverseOp.js +2 -2
  91. package/dist/mutators/mutateActions.d.ts +3 -3
  92. package/dist/mutators/mutateActions.js +1 -1
  93. package/dist/mutators/readerActions.d.ts +1 -1
  94. package/dist/mutators/undoApply.d.ts +1 -1
  95. package/dist/mutators/undoApply.js +1 -1
  96. package/dist/policy/index.d.ts +2 -2
  97. package/dist/policy/index.js +1 -1
  98. package/dist/query/client.d.ts +5 -2
  99. package/dist/query/client.js +10 -9
  100. package/dist/query/types.d.ts +6 -41
  101. package/dist/query/types.js +2 -2
  102. package/dist/react/AbloProvider.d.ts +18 -8
  103. package/dist/react/AbloProvider.js +10 -9
  104. package/dist/react/context.d.ts +3 -3
  105. package/dist/react/context.js +1 -1
  106. package/dist/react/createAbloReact.d.ts +56 -0
  107. package/dist/react/createAbloReact.js +51 -0
  108. package/dist/react/index.d.ts +6 -5
  109. package/dist/react/index.js +6 -3
  110. package/dist/react/internalContext.d.ts +1 -1
  111. package/dist/react/useAblo.d.ts +12 -5
  112. package/dist/react/useAblo.js +26 -8
  113. package/dist/react/useCurrentUserId.js +1 -1
  114. package/dist/react/useErrorListener.js +1 -1
  115. package/dist/react/useMutationFailureListener.d.ts +2 -2
  116. package/dist/react/useMutationFailureListener.js +1 -1
  117. package/dist/react/useMutators.d.ts +3 -3
  118. package/dist/react/useMutators.js +3 -3
  119. package/dist/react/useUndoScope.d.ts +5 -5
  120. package/dist/react/useUndoScope.js +1 -1
  121. package/dist/schema/coordination.d.ts +69 -10
  122. package/dist/schema/coordination.js +90 -9
  123. package/dist/schema/ddl.js +2 -2
  124. package/dist/schema/diff.d.ts +1 -1
  125. package/dist/schema/generate.js +1 -1
  126. package/dist/schema/index.d.ts +11 -10
  127. package/dist/schema/index.js +22 -18
  128. package/dist/schema/queries.d.ts +27 -27
  129. package/dist/schema/queries.js +23 -23
  130. package/dist/schema/select.d.ts +3 -3
  131. package/dist/schema/select.js +6 -3
  132. package/dist/schema/serialize.d.ts +15 -6
  133. package/dist/schema/serialize.js +20 -3
  134. package/dist/schema/sugar.d.ts +6 -7
  135. package/dist/schema/sugar.js +9 -12
  136. package/dist/schema/syncDeltaRow.d.ts +4 -152
  137. package/dist/schema/syncDeltaRow.js +4 -105
  138. package/dist/server/adapter.d.ts +18 -1
  139. package/dist/server/commit.d.ts +10 -16
  140. package/dist/server/index.d.ts +1 -1
  141. package/dist/server/index.js +1 -1
  142. package/dist/server/readConfig.d.ts +1 -1
  143. package/dist/source/adapter.d.ts +7 -5
  144. package/dist/source/adapter.js +7 -5
  145. package/dist/source/adapters/drizzle.d.ts +1 -1
  146. package/dist/source/adapters/drizzle.js +2 -2
  147. package/dist/source/adapters/kysely.d.ts +1 -1
  148. package/dist/source/adapters/kysely.js +1 -1
  149. package/dist/source/adapters/kyselyMutationCore.d.ts +1 -1
  150. package/dist/source/adapters/kyselyMutationCore.js +2 -2
  151. package/dist/source/adapters/memory.js +1 -1
  152. package/dist/source/adapters/prisma.d.ts +8 -3
  153. package/dist/source/adapters/prisma.js +1 -1
  154. package/dist/source/connector.js +1 -1
  155. package/dist/source/connectorProtocol.d.ts +2 -8
  156. package/dist/source/connectorProtocol.js +3 -2
  157. package/dist/source/contract.d.ts +29 -17
  158. package/dist/source/contract.js +27 -22
  159. package/dist/source/factory.d.ts +1 -1
  160. package/dist/source/idempotency.js +2 -2
  161. package/dist/source/index.d.ts +1 -0
  162. package/dist/source/index.js +3 -0
  163. package/dist/source/next.d.ts +1 -1
  164. package/dist/source/signing.d.ts +9 -2
  165. package/dist/source/signing.js +4 -1
  166. package/dist/source/types.d.ts +6 -4
  167. package/dist/source/types.js +1 -1
  168. package/dist/{core/storeContract.d.ts → storeContract.d.ts} +6 -6
  169. package/dist/{core → stores}/DatabaseManager.d.ts +3 -1
  170. package/dist/{core → stores}/DatabaseManager.js +14 -13
  171. package/dist/stores/ObjectStore.d.ts +1 -1
  172. package/dist/{core → stores}/StoreManager.d.ts +9 -26
  173. package/dist/{core → stores}/StoreManager.js +29 -77
  174. package/dist/stores/SyncActionStore.d.ts +4 -2
  175. package/dist/stores/SyncActionStore.js +11 -17
  176. package/dist/stores/syncAction.d.ts +26 -0
  177. package/dist/stores/syncAction.js +16 -0
  178. package/dist/surface.d.ts +3 -3
  179. package/dist/surface.js +6 -4
  180. package/dist/sync/BootstrapFetcher.d.ts +127 -6
  181. package/dist/sync/BootstrapFetcher.js +511 -83
  182. package/dist/sync/ConnectionManager.d.ts +6 -198
  183. package/dist/sync/ConnectionManager.js +6 -677
  184. package/dist/sync/OnDemandLoader.d.ts +5 -2
  185. package/dist/sync/OnDemandLoader.js +61 -21
  186. package/dist/sync/SubscriptionManager.d.ts +13 -2
  187. package/dist/sync/SubscriptionManager.js +23 -5
  188. package/dist/sync/SyncWebSocket.d.ts +27 -510
  189. package/dist/sync/SyncWebSocket.js +76 -954
  190. package/dist/sync/awaitClaimGrant.d.ts +4 -44
  191. package/dist/sync/awaitClaimGrant.js +4 -109
  192. package/dist/sync/bootstrapApply.d.ts +3 -0
  193. package/dist/sync/bootstrapApply.js +2 -2
  194. package/dist/sync/commitFrames.d.ts +6 -40
  195. package/dist/sync/commitFrames.js +6 -97
  196. package/dist/sync/contextPorts.d.ts +18 -0
  197. package/dist/sync/contextPorts.js +31 -0
  198. package/dist/sync/createClaimStream.d.ts +5 -49
  199. package/dist/sync/createClaimStream.js +5 -469
  200. package/dist/sync/createPresenceStream.d.ts +26 -4
  201. package/dist/sync/createPresenceStream.js +28 -20
  202. package/dist/sync/createSnapshot.d.ts +2 -2
  203. package/dist/sync/createSnapshot.js +1 -1
  204. package/dist/sync/credentialLifecycle.d.ts +5 -173
  205. package/dist/sync/credentialLifecycle.js +5 -320
  206. package/dist/sync/deltaPipeline.d.ts +13 -12
  207. package/dist/sync/deltaPipeline.js +21 -4
  208. package/dist/sync/groupChange.d.ts +3 -0
  209. package/dist/sync/groupChange.js +16 -14
  210. package/dist/sync/participants.d.ts +24 -6
  211. package/dist/sync/participants.js +32 -23
  212. package/dist/sync/schemaDrift.d.ts +55 -0
  213. package/dist/sync/schemaDrift.js +53 -0
  214. package/dist/sync/schemas.d.ts +23 -33
  215. package/dist/sync/schemas.js +29 -20
  216. package/dist/sync/syncPlan.d.ts +3 -3
  217. package/dist/sync/wsFrameHandlers.d.ts +6 -114
  218. package/dist/sync/wsFrameHandlers.js +6 -392
  219. package/dist/syncLog/contract.d.ts +20 -0
  220. package/dist/syncLog/contract.js +19 -0
  221. package/dist/syncLog/index.d.ts +1 -0
  222. package/dist/syncLog/index.js +1 -0
  223. package/dist/transaction/ablo.d.ts +88 -0
  224. package/dist/transaction/ablo.js +33 -0
  225. package/dist/{client/auth.d.ts → transaction/auth/apiKey.d.ts} +43 -8
  226. package/dist/{client/auth.js → transaction/auth/apiKey.js} +19 -0
  227. package/dist/transaction/auth/bootstrapScope.d.ts +15 -0
  228. package/dist/transaction/auth/bootstrapScope.js +1 -0
  229. package/dist/transaction/auth/capability.d.ts +212 -0
  230. package/dist/transaction/auth/capability.js +224 -0
  231. package/dist/{auth → transaction/auth}/credentialSource.d.ts +8 -1
  232. package/dist/{client → transaction/auth}/identity.d.ts +8 -7
  233. package/dist/{client → transaction/auth}/identity.js +1 -1
  234. package/dist/transaction/auth/index.d.ts +162 -0
  235. package/dist/transaction/auth/index.js +304 -0
  236. package/dist/{auth → transaction/auth}/schemas.d.ts +1 -1
  237. package/dist/{auth → transaction/auth}/schemas.js +13 -13
  238. package/dist/{client → transaction/auth}/sessionMint.d.ts +8 -8
  239. package/dist/{client → transaction/auth}/sessionMint.js +4 -7
  240. package/dist/transaction/coordination/awaitClaimGrant.d.ts +56 -0
  241. package/dist/transaction/coordination/awaitClaimGrant.js +124 -0
  242. package/dist/{client → transaction/coordination}/claimHeartbeatLoop.d.ts +34 -0
  243. package/dist/{client → transaction/coordination}/claimHeartbeatLoop.js +20 -0
  244. package/dist/transaction/coordination/claimMeta.d.ts +49 -0
  245. package/dist/transaction/coordination/claimMeta.js +52 -0
  246. package/dist/transaction/coordination/createClaimStream.d.ts +64 -0
  247. package/dist/transaction/coordination/createClaimStream.js +475 -0
  248. package/dist/transaction/coordination/events.d.ts +74 -0
  249. package/dist/transaction/coordination/events.js +7 -0
  250. package/dist/transaction/coordination/index.d.ts +19 -0
  251. package/dist/transaction/coordination/index.js +45 -0
  252. package/dist/transaction/coordination/locator.d.ts +104 -0
  253. package/dist/transaction/coordination/locator.js +102 -0
  254. package/dist/transaction/coordination/schema.d.ts +1536 -0
  255. package/dist/transaction/coordination/schema.js +1177 -0
  256. package/dist/transaction/coordination/targetConflict.d.ts +2 -0
  257. package/dist/transaction/coordination/targetConflict.js +107 -0
  258. package/dist/{coordination → transaction/coordination}/trace.d.ts +7 -18
  259. package/dist/{coordination → transaction/coordination}/trace.js +18 -25
  260. package/dist/transaction/durableWrites.d.ts +62 -0
  261. package/dist/{client → transaction}/durableWrites.js +28 -3
  262. package/dist/transaction/environment.d.ts +105 -0
  263. package/dist/transaction/environment.js +108 -0
  264. package/dist/{errorCodes.d.ts → transaction/errorCodes.d.ts} +12 -12
  265. package/dist/{errorCodes.js → transaction/errorCodes.js} +45 -18
  266. package/dist/{errors.d.ts → transaction/errors.d.ts} +37 -13
  267. package/dist/{errors.js → transaction/errors.js} +85 -16
  268. package/dist/transaction/footprint.d.ts +111 -0
  269. package/dist/transaction/footprint.js +0 -0
  270. package/dist/transaction/index.d.ts +20 -0
  271. package/dist/transaction/index.js +20 -0
  272. package/dist/transaction/keys/index.d.ts +87 -0
  273. package/dist/transaction/keys/index.js +207 -0
  274. package/dist/transaction/log/syncDeltaRow.d.ts +158 -0
  275. package/dist/transaction/log/syncDeltaRow.js +95 -0
  276. package/dist/{sync/syncPosition.d.ts → transaction/logPosition.d.ts} +22 -8
  277. package/dist/{sync/syncPosition.js → transaction/logPosition.js} +15 -6
  278. package/dist/transaction/logger.d.ts +16 -0
  279. package/dist/transaction/logger.js +7 -0
  280. package/dist/transaction/observability.d.ts +53 -0
  281. package/dist/transaction/observability.js +19 -0
  282. package/dist/transaction/plugin.d.ts +285 -0
  283. package/dist/transaction/plugin.js +106 -0
  284. package/dist/{policy → transaction/policy}/types.d.ts +3 -3
  285. package/dist/{policy → transaction/policy}/types.js +2 -0
  286. package/dist/transaction/resources/httpResources.d.ts +321 -0
  287. package/dist/transaction/resources/httpResources.js +7 -0
  288. package/dist/transaction/resources/modelOperations.d.ts +427 -0
  289. package/dist/transaction/resources/modelOperations.js +12 -0
  290. package/dist/transaction/resources/mutationOptions.d.ts +66 -0
  291. package/dist/transaction/resources/mutationOptions.js +9 -0
  292. package/dist/transaction/resources/where.d.ts +101 -0
  293. package/dist/transaction/resources/where.js +115 -0
  294. package/dist/{client → transaction/resources}/writeOptionsSchema.d.ts +2 -6
  295. package/dist/{client → transaction/resources}/writeOptionsSchema.js +9 -5
  296. package/dist/{schema → transaction/schema}/field.d.ts +17 -23
  297. package/dist/{schema → transaction/schema}/field.js +5 -5
  298. package/dist/transaction/schema/fieldRef.d.ts +38 -0
  299. package/dist/transaction/schema/fieldRef.js +11 -0
  300. package/dist/transaction/schema/loadStrategy.d.ts +45 -0
  301. package/dist/transaction/schema/loadStrategy.js +46 -0
  302. package/dist/{schema → transaction/schema}/model.d.ts +50 -35
  303. package/dist/{schema → transaction/schema}/model.js +30 -20
  304. package/dist/transaction/schema/openapi.d.ts +58 -0
  305. package/dist/transaction/schema/openapi.js +501 -0
  306. package/dist/{schema → transaction/schema}/relation.d.ts +21 -16
  307. package/dist/{schema → transaction/schema}/relation.js +7 -7
  308. package/dist/{schema → transaction/schema}/residency.d.ts +0 -9
  309. package/dist/{schema → transaction/schema}/residency.js +0 -5
  310. package/dist/{schema → transaction/schema}/roles.d.ts +5 -5
  311. package/dist/{schema → transaction/schema}/roles.js +5 -5
  312. package/dist/{schema → transaction/schema}/schema.d.ts +39 -10
  313. package/dist/{schema → transaction/schema}/schema.js +24 -3
  314. package/dist/{schema → transaction/schema}/tenancy.d.ts +1 -1
  315. package/dist/{schema → transaction/schema}/tenancy.js +7 -4
  316. package/dist/transaction/transactionLayer.d.ts +82 -0
  317. package/dist/transaction/transactionLayer.js +24 -0
  318. package/dist/{transactions → transaction/transactions/settlement}/commitEnvelope.d.ts +5 -6
  319. package/dist/{transactions → transaction/transactions/settlement}/commitEnvelope.js +9 -13
  320. package/dist/{transactions → transaction/transactions/settlement}/httpCommitEnvelope.d.ts +9 -2
  321. package/dist/{transactions → transaction/transactions/settlement}/httpCommitEnvelope.js +5 -9
  322. package/dist/{transactions/durableWriteStore.d.ts → transaction/transactions/settlement/pendingWrite.d.ts} +11 -37
  323. package/dist/transaction/transactions/settlement/pendingWrite.js +20 -0
  324. package/dist/transaction/transport/commitFrames.d.ts +90 -0
  325. package/dist/transaction/transport/commitFrames.js +134 -0
  326. package/dist/transaction/transport/connectionManager.d.ts +215 -0
  327. package/dist/transaction/transport/connectionManager.js +673 -0
  328. package/dist/transaction/transport/credentialLifecycle.d.ts +177 -0
  329. package/dist/transaction/transport/credentialLifecycle.js +324 -0
  330. package/dist/{sync → transaction/transport}/heartbeat.d.ts +3 -1
  331. package/dist/{sync → transaction/transport}/heartbeat.js +6 -4
  332. package/dist/transaction/transport/httpClient.d.ts +131 -0
  333. package/dist/{client → transaction/transport}/httpClient.js +6 -5
  334. package/dist/transaction/transport/httpOptions.d.ts +33 -0
  335. package/dist/transaction/transport/httpOptions.js +12 -0
  336. package/dist/{client → transaction/transport}/httpTransport.js +295 -97
  337. package/dist/{sync/NetworkProbe.d.ts → transaction/transport/networkProbe.d.ts} +7 -4
  338. package/dist/{sync/NetworkProbe.js → transaction/transport/networkProbe.js} +14 -13
  339. package/dist/transaction/transport/wsFrameHandlers.d.ts +128 -0
  340. package/dist/transaction/transport/wsFrameHandlers.js +429 -0
  341. package/dist/transaction/transport/wsTransport.d.ts +574 -0
  342. package/dist/transaction/transport/wsTransport.js +1023 -0
  343. package/dist/transaction/types/assertExact.d.ts +17 -0
  344. package/dist/transaction/types/assertExact.js +1 -0
  345. package/dist/{types → transaction/types}/global.d.ts +17 -2
  346. package/dist/{types → transaction/types}/global.js +2 -1
  347. package/dist/{types → transaction/types}/index.d.ts +14 -46
  348. package/dist/{types → transaction/types}/index.js +7 -16
  349. package/dist/{types → transaction/types}/streams.d.ts +73 -45
  350. package/dist/transaction/utils/duration.d.ts +50 -0
  351. package/dist/{utils → transaction/utils}/duration.js +32 -0
  352. package/dist/{utils → transaction/utils}/json.d.ts +18 -0
  353. package/dist/transaction/utils/json.js +276 -0
  354. package/dist/transaction/wire/accountResponses.d.ts +420 -0
  355. package/dist/transaction/wire/accountResponses.js +290 -0
  356. package/dist/transaction/wire/auth.d.ts +56 -0
  357. package/dist/transaction/wire/auth.js +63 -0
  358. package/dist/transaction/wire/claimEvent.d.ts +76 -0
  359. package/dist/transaction/wire/claimEvent.js +73 -0
  360. package/dist/transaction/wire/claims.d.ts +530 -0
  361. package/dist/transaction/wire/claims.js +327 -0
  362. package/dist/{wire → transaction/wire}/commit.d.ts +125 -193
  363. package/dist/{wire → transaction/wire}/commit.js +68 -47
  364. package/dist/{wire → transaction/wire}/delta.d.ts +66 -17
  365. package/dist/{wire → transaction/wire}/delta.js +37 -13
  366. package/dist/transaction/wire/errorEnvelope.d.ts +72 -0
  367. package/dist/{wire → transaction/wire}/errorEnvelope.js +36 -5
  368. package/dist/transaction/wire/feedCursor.d.ts +60 -0
  369. package/dist/transaction/wire/feedCursor.js +82 -0
  370. package/dist/transaction/wire/feedEvent.d.ts +204 -0
  371. package/dist/transaction/wire/feedEvent.js +65 -0
  372. package/dist/transaction/wire/frames.d.ts +194 -0
  373. package/dist/transaction/wire/frames.js +50 -0
  374. package/dist/transaction/wire/inboundFrames.d.ts +562 -0
  375. package/dist/transaction/wire/inboundFrames.js +116 -0
  376. package/dist/transaction/wire/index.d.ts +54 -0
  377. package/dist/transaction/wire/index.js +83 -0
  378. package/dist/{wire → transaction/wire}/listEnvelope.d.ts +13 -14
  379. package/dist/transaction/wire/listEnvelope.js +42 -0
  380. package/dist/transaction/wire/modelMutations.d.ts +31 -0
  381. package/dist/transaction/wire/modelMutations.js +52 -0
  382. package/dist/transaction/wire/modelResponses.d.ts +85 -0
  383. package/dist/transaction/wire/modelResponses.js +43 -0
  384. package/dist/transaction/wire/modelShape.d.ts +78 -0
  385. package/dist/transaction/wire/modelShape.js +74 -0
  386. package/dist/transactions/{TransactionQueue.d.ts → mutations/MutationQueue.d.ts} +85 -38
  387. package/dist/transactions/{TransactionQueue.js → mutations/MutationQueue.js} +141 -80
  388. package/dist/transactions/{TransactionStore.d.ts → mutations/MutationStore.d.ts} +8 -8
  389. package/dist/transactions/{TransactionStore.js → mutations/MutationStore.js} +2 -2
  390. package/dist/transactions/{coalesceRules.d.ts → mutations/coalesceRules.d.ts} +10 -10
  391. package/dist/transactions/{coalesceRules.js → mutations/coalesceRules.js} +1 -1
  392. package/dist/transactions/mutations/commitLatency.d.ts +52 -0
  393. package/dist/transactions/mutations/commitLatency.js +130 -0
  394. package/dist/transactions/{commitOutboxStore.d.ts → mutations/commitOutboxStore.d.ts} +1 -5
  395. package/dist/transactions/{commitOutboxStore.js → mutations/commitOutboxStore.js} +1 -1
  396. package/dist/transactions/{commitPayload.d.ts → mutations/commitPayload.d.ts} +18 -16
  397. package/dist/transactions/{commitPayload.js → mutations/commitPayload.js} +15 -15
  398. package/dist/transactions/{deltaConfirmation.d.ts → mutations/deltaConfirmation.d.ts} +15 -11
  399. package/dist/transactions/{deltaConfirmation.js → mutations/deltaConfirmation.js} +14 -12
  400. package/dist/transactions/mutations/durableWriteStore.d.ts +14 -0
  401. package/dist/transactions/mutations/durableWriteStore.js +12 -0
  402. package/dist/transactions/{optimisticApply.d.ts → mutations/optimisticApply.d.ts} +7 -7
  403. package/dist/transactions/{replayValidation.d.ts → mutations/replayValidation.d.ts} +4 -3
  404. package/dist/transactions/{replayValidation.js → mutations/replayValidation.js} +7 -5
  405. package/dist/utils/mobxSetup.d.ts +1 -1
  406. package/dist/utils/mobxSetup.js +5 -2
  407. package/dist/{core → views}/QueryView.d.ts +2 -2
  408. package/dist/{core → views}/QueryView.js +2 -2
  409. package/dist/{core → views}/ViewRegistry.d.ts +1 -1
  410. package/dist/{core/queryUtils.d.ts → views/incrementalView.d.ts} +6 -6
  411. package/dist/{core/queryUtils.js → views/incrementalView.js} +6 -6
  412. package/dist/webhooks/events.d.ts +2 -2
  413. package/dist/wire/index.d.ts +1 -34
  414. package/dist/wire/index.js +8 -49
  415. package/docs/agent-messaging.md +3 -3
  416. package/docs/agents.md +20 -13
  417. package/docs/api-keys.md +14 -10
  418. package/docs/api.md +27 -61
  419. package/docs/audit.md +6 -3
  420. package/docs/cli.md +41 -13
  421. package/docs/client-behavior.md +11 -9
  422. package/docs/concurrency-convention.md +49 -57
  423. package/docs/coordination.md +283 -121
  424. package/docs/data-sources.md +7 -5
  425. package/docs/debugging.md +39 -15
  426. package/docs/deployment.md +267 -0
  427. package/docs/examples/agent-human.md +49 -42
  428. package/docs/examples/ai-sdk-tool.md +69 -44
  429. package/docs/examples/existing-python-backend.md +8 -6
  430. package/docs/examples/nextjs.md +129 -47
  431. package/docs/examples/scoped-agent.md +46 -45
  432. package/docs/examples/server-agent.md +46 -26
  433. package/docs/groups.md +87 -30
  434. package/docs/guarantees.md +41 -12
  435. package/docs/how-it-works.md +38 -12
  436. package/docs/idempotency.md +126 -0
  437. package/docs/identity.md +77 -74
  438. package/docs/index.md +172 -86
  439. package/docs/integration-guide.md +31 -19
  440. package/docs/mcp.md +46 -21
  441. package/docs/migration.md +95 -18
  442. package/docs/operating-on-your-database.md +3 -1
  443. package/docs/projects.md +3 -1
  444. package/docs/quickstart.md +22 -5
  445. package/docs/react.md +31 -18
  446. package/docs/schema-contract.md +5 -3
  447. package/docs/session-settings.md +108 -0
  448. package/docs/sessions.md +4 -2
  449. package/docs/webhooks.md +12 -10
  450. package/llms.txt +48 -18
  451. package/package.json +21 -26
  452. package/dist/agent/Agent.d.ts +0 -366
  453. package/dist/agent/Agent.js +0 -514
  454. package/dist/agent/index.d.ts +0 -115
  455. package/dist/agent/index.js +0 -128
  456. package/dist/agent/session.d.ts +0 -93
  457. package/dist/agent/session.js +0 -149
  458. package/dist/agent/types.d.ts +0 -68
  459. package/dist/agent/types.js +0 -9
  460. package/dist/cli.cjs +0 -286329
  461. package/dist/client/durableWrites.d.ts +0 -21
  462. package/dist/client/httpClient.d.ts +0 -80
  463. package/dist/coordination/schema.d.ts +0 -722
  464. package/dist/coordination/schema.js +0 -578
  465. package/dist/schema/openapi.d.ts +0 -29
  466. package/dist/schema/openapi.js +0 -124
  467. package/dist/testing/fixtures/bootstrap.d.ts +0 -49
  468. package/dist/testing/fixtures/bootstrap.js +0 -59
  469. package/dist/testing/fixtures/deltas.d.ts +0 -83
  470. package/dist/testing/fixtures/deltas.js +0 -136
  471. package/dist/testing/fixtures/models.d.ts +0 -83
  472. package/dist/testing/fixtures/models.js +0 -272
  473. package/dist/testing/helpers/reactWrapper.d.ts +0 -69
  474. package/dist/testing/helpers/reactWrapper.js +0 -67
  475. package/dist/testing/helpers/syncEngineHarness.d.ts +0 -54
  476. package/dist/testing/helpers/syncEngineHarness.js +0 -73
  477. package/dist/testing/helpers/wait.d.ts +0 -30
  478. package/dist/testing/helpers/wait.js +0 -49
  479. package/dist/testing/index.d.ts +0 -23
  480. package/dist/testing/index.js +0 -33
  481. package/dist/testing/mocks/FakeDatabase.d.ts +0 -18
  482. package/dist/testing/mocks/FakeDatabase.js +0 -10
  483. package/dist/testing/mocks/MockMutationExecutor.d.ts +0 -87
  484. package/dist/testing/mocks/MockMutationExecutor.js +0 -192
  485. package/dist/testing/mocks/MockNetworkMonitor.d.ts +0 -20
  486. package/dist/testing/mocks/MockNetworkMonitor.js +0 -46
  487. package/dist/testing/mocks/MockSyncContext.d.ts +0 -51
  488. package/dist/testing/mocks/MockSyncContext.js +0 -71
  489. package/dist/testing/mocks/MockSyncStore.d.ts +0 -88
  490. package/dist/testing/mocks/MockSyncStore.js +0 -171
  491. package/dist/testing/mocks/MockWebSocket.d.ts +0 -71
  492. package/dist/testing/mocks/MockWebSocket.js +0 -118
  493. package/dist/transactions/durableWriteStore.js +0 -30
  494. package/dist/utils/duration.d.ts +0 -25
  495. package/dist/utils/json.js +0 -88
  496. package/dist/wire/errorEnvelope.d.ts +0 -55
  497. package/dist/wire/frames.d.ts +0 -197
  498. package/dist/wire/frames.js +0 -49
  499. package/dist/wire/listEnvelope.js +0 -18
  500. package/docs/interaction-model.md +0 -97
  501. /package/dist/{core → query}/QueryProcessor.d.ts +0 -0
  502. /package/dist/{core → query}/QueryProcessor.js +0 -0
  503. /package/dist/{core/storeContract.js → storeContract.js} +0 -0
  504. /package/dist/{core → stores}/openIDBWithTimeout.d.ts +0 -0
  505. /package/dist/{core → stores}/openIDBWithTimeout.js +0 -0
  506. /package/dist/{client → transaction/auth}/credentialEndpoint.d.ts +0 -0
  507. /package/dist/{client → transaction/auth}/credentialEndpoint.js +0 -0
  508. /package/dist/{auth → transaction/auth}/credentialPolicy.d.ts +0 -0
  509. /package/dist/{auth → transaction/auth}/credentialPolicy.js +0 -0
  510. /package/dist/{auth → transaction/auth}/credentialSource.js +0 -0
  511. /package/dist/{client → transaction/auth}/hostedEndpoints.d.ts +0 -0
  512. /package/dist/{client → transaction/auth}/hostedEndpoints.js +0 -0
  513. /package/dist/{client → transaction}/persistence.d.ts +0 -0
  514. /package/dist/{client → transaction}/persistence.js +0 -0
  515. /package/dist/{client → transaction/resources}/functionalUpdate.d.ts +0 -0
  516. /package/dist/{client → transaction/resources}/functionalUpdate.js +0 -0
  517. /package/dist/{transactions → transaction/transactions/settlement}/idempotencyKey.d.ts +0 -0
  518. /package/dist/{transactions → transaction/transactions/settlement}/idempotencyKey.js +0 -0
  519. /package/dist/{client → transaction/transport}/httpTransport.d.ts +0 -0
  520. /package/dist/{types → transaction/types}/modelData.d.ts +0 -0
  521. /package/dist/{types → transaction/types}/modelData.js +0 -0
  522. /package/dist/{types → transaction/types}/participant.d.ts +0 -0
  523. /package/dist/{types → transaction/types}/participant.js +0 -0
  524. /package/dist/{types → transaction/types}/streams.js +0 -0
  525. /package/dist/{utils → transaction/utils}/asyncIterator.d.ts +0 -0
  526. /package/dist/{utils → transaction/utils}/asyncIterator.js +0 -0
  527. /package/dist/{wire → transaction/wire}/bootstrapReason.d.ts +0 -0
  528. /package/dist/{wire → transaction/wire}/bootstrapReason.js +0 -0
  529. /package/dist/{wire → transaction/wire}/protocol.d.ts +0 -0
  530. /package/dist/{wire → transaction/wire}/protocol.js +0 -0
  531. /package/dist/{wire → transaction/wire}/protocolVersion.d.ts +0 -0
  532. /package/dist/{wire → transaction/wire}/protocolVersion.js +0 -0
  533. /package/dist/transactions/{UnconfirmedWrites.d.ts → mutations/UnconfirmedWrites.d.ts} +0 -0
  534. /package/dist/transactions/{UnconfirmedWrites.js → mutations/UnconfirmedWrites.js} +0 -0
  535. /package/dist/transactions/{optimisticApply.js → mutations/optimisticApply.js} +0 -0
  536. /package/dist/{core → views}/ViewRegistry.js +0 -0
@@ -11,17 +11,18 @@
11
11
  * in-memory models, and {@link ModelRegistry} holds their metadata.
12
12
  */
13
13
  import { makeObservable, observable, action, computed, runInAction } from 'mobx';
14
- import { AbloConnectionError, AbloValidationError, toAbloError } from './errors.js';
14
+ import { AbloConnectionError, AbloValidationError, toAbloError } from './transaction/errors.js';
15
15
  import { ConnectionManager } from './sync/ConnectionManager.js';
16
+ import { contextLogger, contextSocketObservability } from './sync/contextPorts.js';
16
17
  import { SubscriptionManager } from './sync/SubscriptionManager.js';
17
18
  import { resolveParticipantSyncGroups, } from './sync/participants.js';
18
19
  import { ModelRegistry } from './ModelRegistry.js';
19
- import { PropertyType } from './types/index.js';
20
+ import { PropertyType } from './transaction/types/index.js';
20
21
  import { SyncWebSocket, } from './sync/SyncWebSocket.js';
21
- import { QueryProcessor } from './core/QueryProcessor.js';
22
+ import { QueryProcessor } from './query/QueryProcessor.js';
22
23
  import { Model, rowAsModel } from './Model.js';
23
- import { getContext } from './context.js';
24
- import { SyncSessionError, isAccessCredentialExpiryCloseReason } from './errors.js';
24
+ import { globalRuntime } from './context.js';
25
+ import { AbloSessionError, isAccessCredentialExpiryCloseReason } from './transaction/errors.js';
25
26
  import { ModelScope } from './InstanceCache.js';
26
27
  import { LazyReferenceCollection } from './LazyReferenceCollection.js';
27
28
  import { deriveSyncPlanFromSchema } from './sync/syncPlan.js';
@@ -29,9 +30,15 @@ import { CredentialLifecycle } from './sync/credentialLifecycle.js';
29
30
  import * as groupChange from './sync/groupChange.js';
30
31
  import * as bootstrapApply from './sync/bootstrapApply.js';
31
32
  import * as deltaPipeline from './sync/deltaPipeline.js';
32
- /** Bootstrap timeout configuration */
33
+ /**
34
+ * Bootstrap retry configuration.
35
+ *
36
+ * There is deliberately no overall timeout here. How long one attempt may run
37
+ * is not a policy this layer gets to invent — it is a property of the fetcher's
38
+ * watchdogs, read from `BootstrapFetcher.budgetMs`. A second number kept here
39
+ * would only be able to disagree with them, which is exactly what it used to do.
40
+ */
33
41
  export const BOOTSTRAP_CONFIG = {
34
- OVERALL_TIMEOUT_MS: 15_000,
35
42
  MAX_RETRY_ATTEMPTS: 3,
36
43
  RETRY_DELAY_MS: 500,
37
44
  };
@@ -58,13 +65,13 @@ export { deriveSyncPlanFromSchema } from './sync/syncPlan.js';
58
65
  * the underlying SyncWebSocket without casts:
59
66
  *
60
67
  * @example
61
- * interface AbloEvents {
62
- * 'sheet:selection': [SheetSelectionEvent];
63
- * 'slide:cursor': [SlideCursorEvent];
68
+ * interface EditorEvents {
69
+ * 'document:selection': [SelectionEvent];
70
+ * 'document:cursor': [CursorEvent];
64
71
  * }
65
- * class SyncedStore extends BaseSyncedStore<AbloEvents> {
66
- * subscribeToSlideCursor(handler: (e: SlideCursorEvent) => void) {
67
- * return this.syncWebSocket?.subscribe('slide:cursor', handler);
72
+ * class EditorStore extends BaseSyncedStore<EditorEvents> {
73
+ * subscribeToCursor(handler: (e: CursorEvent) => void) {
74
+ * return this.syncWebSocket.subscribe('document:cursor', handler);
68
75
  * }
69
76
  * }
70
77
  */
@@ -77,6 +84,10 @@ export class BaseSyncedStore {
77
84
  isSessionError: false,
78
85
  };
79
86
  // ── Injected dependencies ──
87
+ /** The owning client's runtime; the module-global bridge when constructed directly. */
88
+ runtime;
89
+ /** The installed plugins the delta pipeline dispatches stage handlers to. */
90
+ stagePlugins;
80
91
  syncClient;
81
92
  database;
82
93
  objectPool;
@@ -88,15 +99,20 @@ export class BaseSyncedStore {
88
99
  */
89
100
  schema;
90
101
  // ── Real-time sync ──
91
- syncWebSocket = null;
102
+ /**
103
+ * The connection, owned by whoever built this store (ADR 0016 follow-up
104
+ * 3b): the host constructs it and hands it in, the store seeds its late
105
+ * values during `initialize()` and owns the lifecycle from there. One
106
+ * instance for the store's whole lifetime — reconnects replace the socket
107
+ * inside it, never the object.
108
+ */
109
+ syncWebSocket;
92
110
  /**
93
111
  * Dynamic read interest (area-of-interest) over the connection's sync
94
- * groups. Lives alongside `syncWebSocket` and is recreated with it; the
95
- * stable `enterScope`/`leaveScope`/`pinScope`/`unpinScope` methods forward
96
- * to whichever instance is current, so callers (the React participant
97
- * hook) never hold a stale reference. Null until `setupWebSocketSync`.
112
+ * groups. Constructed with the connection; the permanent base scopes are
113
+ * seeded in `setupWebSocketSync` once identity resolves.
98
114
  */
99
- areaOfInterest = null;
115
+ areaOfInterest;
100
116
  /** Sync groups whose current state has been backfilled into the pool
101
117
  * (hydrate-on-enter). Cleared when the pool is reset on (re)bootstrap. */
102
118
  hydratedGroups = new Set();
@@ -104,27 +120,50 @@ export class BaseSyncedStore {
104
120
  * enters of the same scope so they share one fetch. */
105
121
  hydratingGroups = new Map();
106
122
  _syncServerUrl;
123
+ /** Application-declared collaboration event types; empty unless configured. */
124
+ _collaborationEvents = [];
107
125
  /**
108
126
  * Public accessor for the underlying SyncWebSocket. Used by the
109
127
  * factory in `createSyncEngine` to wire the default mutation
110
128
  * executor — the executor needs the WS handle to send commit
111
129
  * frames, and the factory can't reach `protected` state through
112
- * normal typing. Returns null until WS is initialized during
113
- * `initialize()`.
130
+ * normal typing.
114
131
  */
115
132
  getSyncWebSocket() {
116
133
  return this.syncWebSocket;
117
134
  }
135
+ /**
136
+ * Subscribe to pushed frames — deltas, presence updates, claim grants and
137
+ * losses, connection changes, and this store's collaboration events.
138
+ * Durable by construction: the connection object exists for the store's
139
+ * whole lifetime (reconnects replace only the socket inside it), so a
140
+ * subscription made before the first connect starts delivering when the
141
+ * socket opens and keeps delivering across every reconnect. Returns the
142
+ * unsubscribe function.
143
+ */
144
+ subscribe(event, handler) {
145
+ return this.syncWebSocket.subscribe(event, handler);
146
+ }
147
+ /**
148
+ * Send a collaboration event (an app-specific real-time message from this
149
+ * store's `TCollaboration` map). A no-op while the connection is down —
150
+ * presence-grade traffic is not queued.
151
+ */
152
+ sendCollaborationEvent(messageType, payload) {
153
+ this.syncWebSocket.sendCollaborationEvent(messageType, payload);
154
+ }
118
155
  // ── Area-of-interest (dynamic read subscription) ─────────────────
119
156
  //
120
157
  // `enterScope`/`leaveScope` move the connection's read interest as the
121
- // user navigates (open or close a deck, sheet, or doc); `pinScope`/`unpinScope`
158
+ // user navigates (open or close a record); `pinScope`/`unpinScope`
122
159
  // express prominence (an active claim keeps a group subscribed). All four
123
160
  // resolve the scope to sync-group strings through the same resolver the
124
161
  // claim path uses (`resolveParticipantSyncGroups`), so read interest and
125
- // write claims always agree on the string for a given entity. They are
126
- // no-ops before the socket exists, and they never reject when the transport
127
- // is offline (see {@link SubscriptionManager.reconcile}).
162
+ // write claims always agree on the string for a given entity. Before the
163
+ // connection opens they record interest without a wire send, and they
164
+ // never reject when the transport is offline (see
165
+ // {@link SubscriptionManager.reconcile}); the on-connect `resync` pushes
166
+ // whatever interest accumulated.
128
167
  scopeToGroups(scope) {
129
168
  return resolveParticipantSyncGroups(scope, this.schema);
130
169
  }
@@ -137,11 +176,8 @@ export class BaseSyncedStore {
137
176
  * and the live delta stream keeps flowing regardless.
138
177
  */
139
178
  enterScope(scope, opts) {
140
- const mgr = this.areaOfInterest;
141
- if (!mgr)
142
- return Promise.resolve();
143
179
  const groups = this.scopeToGroups(scope);
144
- const subscribed = Promise.all(groups.map((g) => mgr.enter(g))).then(() => undefined);
180
+ const subscribed = Promise.all(groups.map((g) => this.areaOfInterest.enter(g))).then(() => undefined);
145
181
  if (!opts?.hydrate)
146
182
  return subscribed;
147
183
  return subscribed.then(() => this.hydrateGroups(groups));
@@ -171,7 +207,7 @@ export class BaseSyncedStore {
171
207
  this.hydratedGroups.add(g);
172
208
  }
173
209
  catch (err) {
174
- getContext().logger.debug('[BaseSyncedStore] scoped hydrate failed', {
210
+ this.runtime.logger.debug('[BaseSyncedStore] scoped hydrate failed', {
175
211
  syncGroups: need,
176
212
  error: err instanceof Error ? err.message : String(err),
177
213
  });
@@ -188,24 +224,15 @@ export class BaseSyncedStore {
188
224
  }
189
225
  /** Leave a scope → its groups go warm (hysteresis), then drop on sweep. */
190
226
  leaveScope(scope) {
191
- const mgr = this.areaOfInterest;
192
- if (!mgr)
193
- return Promise.resolve();
194
- return Promise.all(this.scopeToGroups(scope).map((g) => mgr.leave(g))).then(() => undefined);
227
+ return Promise.all(this.scopeToGroups(scope).map((g) => this.areaOfInterest.leave(g))).then(() => undefined);
195
228
  }
196
229
  /** Pin a scope (active claim / prominence) → never warms while pinned. */
197
230
  pinScope(scope) {
198
- const mgr = this.areaOfInterest;
199
- if (!mgr)
200
- return Promise.resolve();
201
- return Promise.all(this.scopeToGroups(scope).map((g) => mgr.pin(g))).then(() => undefined);
231
+ return Promise.all(this.scopeToGroups(scope).map((g) => this.areaOfInterest.pin(g))).then(() => undefined);
202
232
  }
203
233
  /** Release a pin → the group transitions to warm rather than dropping. */
204
234
  unpinScope(scope) {
205
- const mgr = this.areaOfInterest;
206
- if (!mgr)
207
- return Promise.resolve();
208
- return Promise.all(this.scopeToGroups(scope).map((g) => mgr.unpin(g))).then(() => undefined);
235
+ return Promise.all(this.scopeToGroups(scope).map((g) => this.areaOfInterest.unpin(g))).then(() => undefined);
209
236
  }
210
237
  // ── Internal helpers ──
211
238
  queryProcessor;
@@ -237,24 +264,28 @@ export class BaseSyncedStore {
237
264
  pendingDeltas = [];
238
265
  batchTimer = null;
239
266
  syncPromise = null;
240
- /** Resume/ack cursor — delegates to the shared SyncPosition (see
241
- * sync/syncPosition.ts). Advances only after IDB persistence. */
267
+ /** Resume/ack cursor — delegates to the shared LogPosition (see
268
+ * logPosition.ts). Advances only after IDB persistence. */
242
269
  get lastAckedId() {
243
270
  return this.syncClient.position.persisted;
244
271
  }
245
- /** Pool-applied cursor — delegates to the shared SyncPosition. */
272
+ /** Pool-applied cursor — delegates to the shared LogPosition. */
246
273
  get highestProcessedSyncId() {
247
274
  return this.syncClient.position.applied;
248
275
  }
249
276
  // ── Delta queuing during bootstrap ──
250
277
  bootstrapDeltaQueue = null;
251
278
  activeBootstrapCount = 0;
279
+ /** The live deadline for the bootstrap attempt in flight, if any. */
280
+ bootstrapDeadlineTimer = null;
252
281
  // ── Delete tracking ──
253
282
  pendingDeletes = new Set();
254
283
  // ── Model type hydration ──
255
284
  modelTypesHydrated = new Set();
256
285
  modelTypeHydrationInFlight = new Map();
257
286
  constructor(dependencies, config = {}) {
287
+ this.runtime = dependencies.runtime ?? globalRuntime;
288
+ this.stagePlugins = dependencies.stagePlugins ?? [];
258
289
  this.syncClient = dependencies.syncClient;
259
290
  this.database = dependencies.database;
260
291
  this.objectPool = dependencies.objectPool;
@@ -262,6 +293,30 @@ export class BaseSyncedStore {
262
293
  this.auth = dependencies.auth;
263
294
  this.schema = dependencies.schema;
264
295
  this._syncServerUrl = dependencies.url;
296
+ this._collaborationEvents = config.collaborationEvents ?? [];
297
+ // The connection exists from construction (ADR 0016 follow-up 3b): the
298
+ // host hands one in, or the store builds its own. `deferConnect` holds
299
+ // it closed until `initialize()` has seeded identity and read scope, so
300
+ // nothing can open an unscoped connection in between.
301
+ this.syncWebSocket =
302
+ dependencies.syncWebSocket ??
303
+ new SyncWebSocket({
304
+ baseUrl: this._syncServerUrl,
305
+ collaborationEvents: [...this._collaborationEvents],
306
+ getAuthToken: this.auth?.getAuthToken,
307
+ deferConnect: true,
308
+ capabilities: {
309
+ partialBootstrap: true,
310
+ compressedDeltas: true,
311
+ streamingBootstrap: true,
312
+ batchedDeltas: true,
313
+ },
314
+ });
315
+ this.areaOfInterest = new SubscriptionManager({ transport: this.syncWebSocket });
316
+ this.wireSocketEvents();
317
+ // QueuedMutation events for pendingChanges tracking — connection-
318
+ // independent, wired once for the store's lifetime.
319
+ this.disposers.push(this.syncClient.onTransactionEvent('created', () => { this.incrementPendingChanges(); }), this.syncClient.onTransactionEvent('completed', () => { this.decrementPendingChanges(); }), this.syncClient.onTransactionEvent('failed', () => { this.decrementPendingChanges(); }));
265
320
  // Set this store as the global Model store
266
321
  Model.setStore(this);
267
322
  // ── Schema-derived sync plan ───────────────────────────────────────
@@ -414,7 +469,7 @@ export class BaseSyncedStore {
414
469
  reportSessionExpired: () => {
415
470
  this.connectionManager?.send({ type: 'BOOTSTRAP_FAILED_SESSION' });
416
471
  },
417
- });
472
+ }, contextLogger);
418
473
  /**
419
474
  * Listeners registered via `subscribeSessionError()`. Fired when the
420
475
  * WebSocket closes with a session-invalid code (1008/4001/4003) or a
@@ -435,7 +490,7 @@ export class BaseSyncedStore {
435
490
  }
436
491
  /**
437
492
  * Subscribe to per-mutation failure payloads. Forwarded from the
438
- * underlying `SyncClient.transactionQueue` so consumers (toast layer,
493
+ * underlying `SyncClient.mutationQueue` so consumers (toast layer,
439
494
  * route-level reverted boundaries, telemetry) can react without
440
495
  * reaching across the store. Returns an unsubscribe function.
441
496
  *
@@ -448,6 +503,15 @@ export class BaseSyncedStore {
448
503
  subscribeMutationFailure(listener) {
449
504
  return this.syncClient.onMutationFailure(listener);
450
505
  }
506
+ /**
507
+ * Subscribe to commit round-trip latency. Forwarded from the underlying
508
+ * `SyncClient` for the same reason as `subscribeMutationFailure` — the
509
+ * React provider binds against this surface, so the engine's wiring stays
510
+ * private while the SDK keeps one hook to expose.
511
+ */
512
+ subscribeCommitLatency(listener) {
513
+ return this.syncClient.onCommitLatency(listener);
514
+ }
451
515
  /**
452
516
  * Wait for the in-flight transaction for (modelName, modelId) to be
453
517
  * confirmed by the server. See `SyncClient.waitForConfirmation` for the
@@ -458,20 +522,20 @@ export class BaseSyncedStore {
458
522
  }
459
523
  /**
460
524
  * Observe the LOCAL mutation stream for undo recording (see
461
- * {@link import('./core/storeContract.js').LocalMutation}). Taps the
462
- * TransactionQueue's `transaction:created` event — fired once per local
525
+ * {@link import('./storeContract.js').LocalMutation}). Taps the
526
+ * MutationQueue's `transaction:created` event — fired once per local
463
527
  * create/update/delete/archive with `previousData` already captured.
464
528
  * Remote/collaborator deltas apply via `applyDeltaBatchToPool` and never
465
529
  * emit here, so undo is naturally local-only (you can't undo a teammate).
466
530
  */
467
531
  subscribeLocalMutations(handler) {
468
- // Tap the TransactionQueue directly via `onLocalTransaction`. The previous
532
+ // Tap the MutationQueue directly via `onLocalTransaction`. The previous
469
533
  // `syncClient.subscribe('transaction:created', …)` route registered the
470
534
  // handler on SyncClient's OWN emitter, which never fires that event (only
471
535
  // the queue's emitter does) — so undo recorded nothing. See
472
536
  // `SyncClient.onLocalTransaction` for the full rationale.
473
537
  return this.syncClient.onLocalTransaction((tx) => {
474
- if (!tx?.type || !tx.modelName || !tx.modelId)
538
+ if (!tx.modelName || !tx.modelId)
475
539
  return;
476
540
  handler({
477
541
  type: tx.type,
@@ -489,69 +553,113 @@ export class BaseSyncedStore {
489
553
  */
490
554
  async executeBootstrapWithTimeout(bootstrapFn, _context, signal) {
491
555
  let lastError = null;
492
- for (let attempt = 1; attempt <= BOOTSTRAP_CONFIG.MAX_RETRY_ATTEMPTS; attempt++) {
493
- if (signal?.aborted) {
494
- throw new DOMException('Initialization aborted', 'AbortError');
495
- }
496
- // `navigator.onLine === false` is the MDN-reliable "definitely
497
- // offline" signal. Don't use `!navigator.onLine`: Node 22+ exposes
498
- // `globalThis.navigator` with `onLine === undefined`, so the
499
- // negation false-positives every server-side bootstrap (e.g. the
500
- // server-side agent.run dispatch path through `connectAgent`).
501
- if (typeof navigator !== 'undefined' && navigator.onLine === false) {
502
- getContext().observability.breadcrumb(`Bootstrap attempt ${attempt} skipped - offline`, 'sync.bootstrap', 'warning');
503
- throw new AbloConnectionError('Bootstrap skipped - device is offline', {
504
- code: 'bootstrap_offline',
505
- });
506
- }
507
- try {
508
- getContext().logger.info(`[BaseSyncedStore] Bootstrap attempt ${attempt}/${BOOTSTRAP_CONFIG.MAX_RETRY_ATTEMPTS}`);
509
- const result = (await Promise.race([
510
- bootstrapFn(),
511
- this.createBootstrapTimeout(attempt),
512
- ]));
513
- getContext().logger.info('[BaseSyncedStore] Bootstrap completed successfully', { attempt });
514
- return result;
515
- }
516
- catch (error) {
517
- lastError = error;
518
- const isTimeout = error instanceof Error && error.message.includes('timed out');
519
- const isAbort = error instanceof DOMException && error.name === 'AbortError';
520
- const isNetworkError = error instanceof TypeError && error.message.includes('fetch');
521
- if (isAbort)
522
- throw error;
523
- if (SyncSessionError.isSessionError(error))
524
- throw error;
525
- if (isNetworkError && typeof navigator !== 'undefined' && navigator.onLine === false) {
526
- getContext().observability.captureBootstrapFailure(error, { type: 'network-offline' });
527
- throw error;
556
+ // An aborted initialize has to stop the transfer, not just stop waiting for
557
+ // it. Without this the caller returns while a cold start keeps downloading,
558
+ // and those chunks are still in flight when the next initialize begins.
559
+ const onCallerAbort = () => { this.database.helper.abort(); };
560
+ signal?.addEventListener('abort', onCallerAbort, { once: true });
561
+ try {
562
+ for (let attempt = 1; attempt <= BOOTSTRAP_CONFIG.MAX_RETRY_ATTEMPTS; attempt++) {
563
+ if (signal?.aborted) {
564
+ throw new DOMException('Initialization aborted', 'AbortError');
565
+ }
566
+ // `navigator.onLine === false` is the MDN-reliable "definitely
567
+ // offline" signal. Don't use `!navigator.onLine`: Node 22+ exposes
568
+ // `globalThis.navigator` with `onLine === undefined`, so the
569
+ // negation false-positives every server-side bootstrap (e.g. the
570
+ // server-side agent.run dispatch path through `connectAgent`).
571
+ if (typeof navigator !== 'undefined' && navigator.onLine === false) {
572
+ this.runtime.observability.breadcrumb(`Bootstrap attempt ${attempt} skipped - offline`, 'sync.bootstrap', 'warning');
573
+ throw new AbloConnectionError('Bootstrap skipped - device is offline', {
574
+ code: 'bootstrap_offline',
575
+ });
576
+ }
577
+ try {
578
+ this.runtime.logger.info(`[BaseSyncedStore] Bootstrap attempt ${attempt}/${BOOTSTRAP_CONFIG.MAX_RETRY_ATTEMPTS}`);
579
+ const result = (await Promise.race([
580
+ bootstrapFn(),
581
+ this.createBootstrapTimeout(attempt),
582
+ ]));
583
+ this.runtime.logger.info('[BaseSyncedStore] Bootstrap completed successfully', { attempt });
584
+ return result;
528
585
  }
529
- getContext().observability.breadcrumb(`Bootstrap attempt ${attempt} failed`, 'sync.bootstrap', 'warning', { isTimeout, isNetworkError, willRetry: attempt < BOOTSTRAP_CONFIG.MAX_RETRY_ATTEMPTS });
530
- if (isTimeout && attempt < BOOTSTRAP_CONFIG.MAX_RETRY_ATTEMPTS) {
531
- getContext().logger.info('[BaseSyncedStore] Resetting state before bootstrap retry');
532
- this.resetBootstrapState();
533
- await new Promise((resolve) => setTimeout(resolve, BOOTSTRAP_CONFIG.RETRY_DELAY_MS));
586
+ catch (error) {
587
+ lastError = error;
588
+ const isTimeout = error instanceof Error && error.message.includes('timed out');
589
+ const isAbort = error instanceof DOMException && error.name === 'AbortError';
590
+ const isNetworkError = error instanceof TypeError && error.message.includes('fetch');
591
+ if (isAbort)
592
+ throw error;
593
+ if (AbloSessionError.isSessionError(error))
594
+ throw error;
595
+ if (isNetworkError && typeof navigator !== 'undefined' && navigator.onLine === false) {
596
+ this.runtime.observability.captureBootstrapFailure(error, { type: 'network-offline' });
597
+ throw error;
598
+ }
599
+ this.runtime.observability.breadcrumb(`Bootstrap attempt ${attempt} failed`, 'sync.bootstrap', 'warning', { isTimeout, isNetworkError, willRetry: attempt < BOOTSTRAP_CONFIG.MAX_RETRY_ATTEMPTS });
600
+ if (isTimeout && attempt < BOOTSTRAP_CONFIG.MAX_RETRY_ATTEMPTS) {
601
+ this.runtime.logger.info('[BaseSyncedStore] Resetting state before bootstrap retry');
602
+ this.resetBootstrapState();
603
+ await new Promise((resolve) => setTimeout(resolve, BOOTSTRAP_CONFIG.RETRY_DELAY_MS));
604
+ }
605
+ else if (!isTimeout && attempt < BOOTSTRAP_CONFIG.MAX_RETRY_ATTEMPTS) {
606
+ await new Promise((resolve) => setTimeout(resolve, 1000));
607
+ }
534
608
  }
535
- else if (!isTimeout && attempt < BOOTSTRAP_CONFIG.MAX_RETRY_ATTEMPTS) {
536
- await new Promise((resolve) => setTimeout(resolve, 1000));
609
+ finally {
610
+ // Disarm this attempt's deadline the moment it settles — a live timer
611
+ // would abort whatever the next attempt puts in flight.
612
+ this.clearBootstrapDeadline();
537
613
  }
538
614
  }
615
+ throw lastError
616
+ ? toAbloError(lastError)
617
+ : new AbloConnectionError('Bootstrap failed after all retry attempts', {
618
+ code: 'bootstrap_fetch_timeout',
619
+ });
620
+ }
621
+ finally {
622
+ signal?.removeEventListener('abort', onCallerAbort);
623
+ this.clearBootstrapDeadline();
539
624
  }
540
- throw lastError
541
- ? toAbloError(lastError)
542
- : new AbloConnectionError('Bootstrap failed after all retry attempts', {
543
- code: 'bootstrap_fetch_timeout',
544
- });
545
625
  }
546
- /** Create a timeout promise for bootstrap attempts */
626
+ /**
627
+ * The outer deadline for one bootstrap attempt.
628
+ *
629
+ * The length is DERIVED from the fetcher's own watchdog budget, not chosen. A
630
+ * chosen number is what broke this: the previous fixed 15s was shorter than a
631
+ * single model chunk's allowance — 20s waiting for response headers plus 15s
632
+ * of stall grace — so on any workspace with one slow model the deadline fired
633
+ * before the watchdogs it was meant to backstop, and every attempt timed out
634
+ * by construction. The watchdogs below are progress-based and already
635
+ * guarantee termination; this deadline exists only for a hang somewhere other
636
+ * than the network, so it must sit above them, and it can only do that
637
+ * reliably by asking them how long they take.
638
+ *
639
+ * Reaching it aborts the work in flight. `Promise.race` merely stops waiting:
640
+ * without the abort the losing bootstrap keeps running, keeps its sockets, and
641
+ * races the retry that replaced it — which is how one page load turned into
642
+ * dozens of overlapping requests.
643
+ */
547
644
  createBootstrapTimeout(attempt) {
548
- const timeoutMs = BOOTSTRAP_CONFIG.OVERALL_TIMEOUT_MS + (attempt - 1) * 3_000;
645
+ const timeoutMs = this.database.helper.budgetMs;
549
646
  return new Promise((_, reject) => {
550
- setTimeout(() => {
647
+ this.clearBootstrapDeadline();
648
+ this.bootstrapDeadlineTimer = setTimeout(() => {
649
+ this.database.helper.abort();
551
650
  reject(new AbloConnectionError(`Bootstrap timed out after ${timeoutMs}ms (attempt ${attempt})`, { code: 'bootstrap_fetch_timeout' }));
552
651
  }, timeoutMs);
553
652
  });
554
653
  }
654
+ /** Disarm the deadline once its attempt has settled. Load-bearing now that
655
+ * firing it aborts real work: a leftover timer would cancel a later,
656
+ * unrelated bootstrap. */
657
+ clearBootstrapDeadline() {
658
+ if (this.bootstrapDeadlineTimer !== null) {
659
+ clearTimeout(this.bootstrapDeadlineTimer);
660
+ this.bootstrapDeadlineTimer = null;
661
+ }
662
+ }
555
663
  /** Reset bootstrap-related state for a clean retry */
556
664
  resetBootstrapState() {
557
665
  try {
@@ -564,10 +672,10 @@ export class BaseSyncedStore {
564
672
  // is stale — clear it so re-entered groups backfill again.
565
673
  this.hydratedGroups.clear();
566
674
  this.hydratingGroups.clear();
567
- getContext().logger.info('[BaseSyncedStore] Bootstrap state reset complete');
675
+ this.runtime.logger.info('[BaseSyncedStore] Bootstrap state reset complete');
568
676
  }
569
677
  catch {
570
- getContext().observability.breadcrumb('Error resetting bootstrap state', 'sync.bootstrap', 'warning');
678
+ this.runtime.observability.breadcrumb('Error resetting bootstrap state', 'sync.bootstrap', 'warning');
571
679
  }
572
680
  }
573
681
  // ── Reconnection ─────────────────────────────────────────────────────────
@@ -588,7 +696,7 @@ export class BaseSyncedStore {
588
696
  await this.syncClient.hydrateFromDatabase();
589
697
  this.dataReady = true;
590
698
  }
591
- if (this.syncWebSocket && !this.syncWebSocket.isConnected()) {
699
+ if (!this.syncWebSocket.isConnected()) {
592
700
  this.syncWebSocket.resetReconnectAttempts();
593
701
  this.syncWebSocket.connect();
594
702
  }
@@ -596,10 +704,10 @@ export class BaseSyncedStore {
596
704
  return 'success';
597
705
  }
598
706
  catch (error) {
599
- getContext().observability.captureBootstrapFailure(error, { type: 'connection-store-reconnect' });
600
- if (SyncSessionError.isSessionError(error)) {
601
- this.syncWebSocket?.setSessionErrorDetected();
602
- this.syncWebSocket?.disconnect();
707
+ this.runtime.observability.captureBootstrapFailure(error, { type: 'connection-store-reconnect' });
708
+ if (AbloSessionError.isSessionError(error)) {
709
+ this.syncWebSocket.setSessionErrorDetected();
710
+ this.syncWebSocket.disconnect();
603
711
  this.updateSyncStatus({ state: 'error', error: error });
604
712
  // SECURITY: Clear locally cached data when session is invalid
605
713
  this.database.clear({ includeWriteJournal: true }).catch(() => { });
@@ -609,15 +717,19 @@ export class BaseSyncedStore {
609
717
  if (!this.dataReady && this.objectPool.size === 0) {
610
718
  try {
611
719
  await this.syncClient.hydrateFromDatabase();
612
- if (this.objectPool.size > 0) {
720
+ // Re-read through a local: the guard above narrowed `size` to 0, and
721
+ // the compiler carries that narrowing across the await even though
722
+ // hydrating is precisely what fills the pool.
723
+ const hydratedSize = this.objectPool.size;
724
+ if (hydratedSize > 0) {
613
725
  this.dataReady = true;
614
- getContext().logger.info('[BaseSyncedStore] Hydrated from local fallback', {
615
- objectPoolSize: this.objectPool.size,
726
+ this.runtime.logger.info('[BaseSyncedStore] Hydrated from local fallback', {
727
+ objectPoolSize: hydratedSize,
616
728
  });
617
729
  }
618
730
  }
619
731
  catch (fallbackError) {
620
- getContext().logger.debug('[BaseSyncedStore] Local fallback failed', {
732
+ this.runtime.logger.debug('[BaseSyncedStore] Local fallback failed', {
621
733
  error: fallbackError.message,
622
734
  });
623
735
  }
@@ -686,12 +798,13 @@ export class BaseSyncedStore {
686
798
  /** Narrow context the group-change leaf talks back through. */
687
799
  groupChangeContext() {
688
800
  return {
801
+ runtime: this.runtime,
689
802
  database: this.database,
690
803
  objectPool: this.objectPool,
691
- getSubscribedSyncGroups: () => this.syncWebSocket?.getSyncGroups() ?? [],
804
+ getSubscribedSyncGroups: () => this.syncWebSocket.getSyncGroups(),
692
805
  getCurrentSyncGroups: () => this.userContext ? this.resolveSyncGroups(this.userContext) : null,
693
806
  getBootstrapMode: () => this.userContext?.bootstrapMode,
694
- disconnectWebSocket: () => { this.syncWebSocket?.disconnect(); },
807
+ disconnectWebSocket: () => { this.syncWebSocket.disconnect(); },
695
808
  emitConnectionEvent: (event) => { this.onConnectionEvent?.(event); },
696
809
  handleGroupAdded: (payload, syncId) => this.handleGroupAdded(payload, syncId),
697
810
  computeUpdatedSyncGroups: (payload) => this.computeUpdatedSyncGroups(payload),
@@ -755,6 +868,7 @@ export class BaseSyncedStore {
755
868
  poolContext() {
756
869
  const store = this;
757
870
  return {
871
+ runtime: this.runtime,
758
872
  applyDeltaBatchToPool: (results) => {
759
873
  this.syncClient.applyDeltaBatchToPool(results, (name, data) => this.enrichRelations(name, data));
760
874
  },
@@ -795,8 +909,8 @@ export class BaseSyncedStore {
795
909
  hasLocalData = this.objectPool.size > 0;
796
910
  }
797
911
  catch (hydrateError) {
798
- getContext().logger.debug('[sync-engine] IDB hydration failed', { error: hydrateError });
799
- getContext().observability.captureBootstrapFailure(hydrateError, { type: 'hydration-from-idb' });
912
+ this.runtime.logger.debug('[sync-engine] IDB hydration failed', { error: hydrateError });
913
+ this.runtime.observability.captureBootstrapFailure(hydrateError, { type: 'hydration-from-idb' });
800
914
  }
801
915
  // Get sync baseline for WebSocket
802
916
  const lastSyncId = (yield this.database.getLastSyncId());
@@ -819,7 +933,7 @@ export class BaseSyncedStore {
819
933
  // delta flow works regardless of this branch.
820
934
  const requirements = (yield this.database.requiredBootstrap());
821
935
  if (context.bootstrapMode === 'none') {
822
- getContext().logger.info('[BaseSyncedStore] Bootstrap skipped (bootstrapMode=none)', { kind: context.kind ?? 'user' });
936
+ this.runtime.logger.info('[BaseSyncedStore] Bootstrap skipped (bootstrapMode=none)', { kind: context.kind ?? 'user' });
823
937
  // `setupWebSocketSync` above creates the SyncWebSocket and
824
938
  // initiates the upgrade, but it does NOT await the 'connected'
825
939
  // event — it returns synchronously after wiring listeners.
@@ -846,7 +960,7 @@ export class BaseSyncedStore {
846
960
  // delta-queue replay in withDeltaQueuing's finally (and the error
847
961
  // handler itself), which would otherwise vanish unhandled.
848
962
  void this.performBackgroundBootstrap(requirements, context, signal).catch((error) => {
849
- getContext().observability.captureBootstrapFailure(error, {
963
+ this.runtime.observability.captureBootstrapFailure(error, {
850
964
  type: 'background-orchestration',
851
965
  });
852
966
  });
@@ -877,11 +991,11 @@ export class BaseSyncedStore {
877
991
  this.updateSyncStatus({ state: 'idle', progress: 0 });
878
992
  return { success: false, error: error };
879
993
  }
880
- const isSession = SyncSessionError.isSessionError(error);
881
- getContext().observability.captureBootstrapFailure(error, { type: 'initialize' });
994
+ const isSession = AbloSessionError.isSessionError(error);
995
+ this.runtime.observability.captureBootstrapFailure(error, { type: 'initialize' });
882
996
  if (isSession) {
883
- this.syncWebSocket?.setSessionErrorDetected();
884
- this.syncWebSocket?.disconnect();
997
+ this.syncWebSocket.setSessionErrorDetected();
998
+ this.syncWebSocket.disconnect();
885
999
  this.updateSyncStatus({ state: 'error', error: error });
886
1000
  return { success: false, error: error };
887
1001
  }
@@ -895,7 +1009,7 @@ export class BaseSyncedStore {
895
1009
  if (this.objectPool.size > 0) {
896
1010
  this.dataReady = true;
897
1011
  this.initialized = true;
898
- this.updateSyncStatus(this.syncWebSocket?.isConnected()
1012
+ this.updateSyncStatus(this.syncWebSocket.isConnected()
899
1013
  ? { state: 'idle', progress: 100 }
900
1014
  : { state: 'offline', offlineSince: new Date() });
901
1015
  return { success: true };
@@ -915,17 +1029,17 @@ export class BaseSyncedStore {
915
1029
  this.updateSyncStatus({ state: 'idle', progress: 100 });
916
1030
  }
917
1031
  catch (error) {
918
- getContext().logger.debug('[sync-engine] Background bootstrap failed', {
1032
+ this.runtime.logger.debug('[sync-engine] Background bootstrap failed', {
919
1033
  error: error instanceof Error ? error.message : String(error),
920
1034
  cause: error,
921
1035
  });
922
- getContext().observability.captureBootstrapFailure(error, { type: 'background' });
923
- if (SyncSessionError.isSessionError(error)) {
924
- this.syncWebSocket?.setSessionErrorDetected();
925
- this.syncWebSocket?.disconnect();
1036
+ this.runtime.observability.captureBootstrapFailure(error, { type: 'background' });
1037
+ if (AbloSessionError.isSessionError(error)) {
1038
+ this.syncWebSocket.setSessionErrorDetected();
1039
+ this.syncWebSocket.disconnect();
926
1040
  this.updateSyncStatus({ state: 'error', error: error });
927
1041
  }
928
- else if (!this.syncWebSocket?.isConnected()) {
1042
+ else if (!this.syncWebSocket.isConnected()) {
929
1043
  this.updateSyncStatus({ state: 'offline', offlineSince: new Date() });
930
1044
  }
931
1045
  }
@@ -984,10 +1098,19 @@ export class BaseSyncedStore {
984
1098
  return null;
985
1099
  return new ConnectionManager({
986
1100
  baseUrl: this._syncServerUrl,
987
- getAuthToken: () => this.auth?.getAuthToken() ?? this.syncWebSocket?.getAuthToken() ?? null,
1101
+ getAuthToken: () => this.auth?.getAuthToken() ?? this.syncWebSocket.getAuthToken() ?? null,
1102
+ logger: contextLogger,
1103
+ observability: contextSocketObservability,
988
1104
  });
989
1105
  }
990
- /** Disconnect and clean up all resources */
1106
+ /**
1107
+ * Disconnect and clean up all resources. Terminal: this means "the client
1108
+ * is finished", not "close and reopen later" — the connection object stays
1109
+ * assigned but closed, the event wiring is torn down, and nothing
1110
+ * re-initializes a disconnected store. (Mid-session closes during recovery
1111
+ * go through the connection FSM's `onDisconnectWebSocket`, which closes
1112
+ * the transport without touching the store.)
1113
+ */
991
1114
  async disconnect() {
992
1115
  this.stopCredentialLifecycle();
993
1116
  if (this.batchTimer) {
@@ -1003,15 +1126,12 @@ export class BaseSyncedStore {
1003
1126
  this.connectionManager = null;
1004
1127
  }
1005
1128
  try {
1006
- const last = this.syncWebSocket?.getLastSyncId?.() || 0;
1129
+ const last = this.syncWebSocket.getLastSyncId();
1007
1130
  if (last > 0)
1008
1131
  await this.database.updateWorkspaceMetadata({ lastSyncId: last });
1009
1132
  }
1010
1133
  catch { }
1011
- if (this.syncWebSocket) {
1012
- this.syncWebSocket.disconnect();
1013
- this.syncWebSocket = null;
1014
- }
1134
+ this.syncWebSocket.disconnect();
1015
1135
  this.syncClient.disconnect();
1016
1136
  this.queryProcessor.clearCache();
1017
1137
  // Stop the pool's GC interval — the one timer the pool arms itself.
@@ -1081,8 +1201,6 @@ export class BaseSyncedStore {
1081
1201
  */
1082
1202
  async waitForWebSocketConnected(timeoutMs) {
1083
1203
  const ws = this.syncWebSocket;
1084
- if (!ws)
1085
- return false;
1086
1204
  if (ws.isConnected())
1087
1205
  return true;
1088
1206
  return new Promise((resolve) => {
@@ -1100,40 +1218,53 @@ export class BaseSyncedStore {
1100
1218
  return;
1101
1219
  resolved = true;
1102
1220
  unsubscribe();
1103
- getContext().logger.debug(`[BaseSyncedStore] waitForWebSocketConnected timed out after ${timeoutMs}ms — initialize() will return but the next mutation may race the upgrade.`);
1221
+ this.runtime.logger.debug(`[BaseSyncedStore] waitForWebSocketConnected timed out after ${timeoutMs}ms — initialize() will return but the next mutation may race the upgrade.`);
1104
1222
  resolve(false);
1105
1223
  }, timeoutMs);
1106
1224
  });
1107
1225
  }
1226
+ /**
1227
+ * Seed the connection's late values and open it. The socket itself exists
1228
+ * from construction; what identity resolution supplies — the participant
1229
+ * kind, the credential, the read scope, and the resume cursor — is seeded
1230
+ * here, and only then is the held first connect released. A retried
1231
+ * `initialize()` after a failed `ready()` re-runs this against the same
1232
+ * connection object: the reconnect counter is reset for a clean slate,
1233
+ * while the session-error latch deliberately survives (only the
1234
+ * credential-expiry recovery clears it).
1235
+ */
1108
1236
  setupWebSocketSync(context, lastSyncId) {
1109
1237
  if (!context.userId || !context.organizationId) {
1110
- getContext().observability.breadcrumb('Cannot setup WebSocket sync without user context', 'sync.websocket', 'warning');
1238
+ this.runtime.observability.breadcrumb('Cannot setup WebSocket sync without user context', 'sync.websocket', 'warning');
1111
1239
  return;
1112
1240
  }
1113
- this.syncWebSocket = new SyncWebSocket({
1114
- baseUrl: this._syncServerUrl,
1115
- userId: context.userId,
1116
- organizationId: context.organizationId,
1117
- syncGroups: [...this.resolveSyncGroups(context)],
1118
- lastSyncId,
1119
- kind: context.kind,
1120
- capabilityToken: context.capabilityToken,
1121
- getAuthToken: this.auth?.getAuthToken,
1122
- capabilities: {
1123
- partialBootstrap: true,
1124
- compressedDeltas: true,
1125
- streamingBootstrap: true,
1126
- batchedDeltas: true,
1127
- },
1128
- });
1129
- // Area-of-interest manager — owns dynamic read-subscription over this
1130
- // connection. baseGroups (the org/user scopes) are always subscribed;
1131
- // enterScope/leaveScope move per-entity interest. Recreated with the
1132
- // socket; torn down via the disposer pushed below.
1133
- this.areaOfInterest = new SubscriptionManager({
1134
- transport: this.syncWebSocket,
1135
- baseGroups: this.resolveSyncGroups(context),
1136
- });
1241
+ if (context.kind)
1242
+ this.syncWebSocket.setKind(context.kind);
1243
+ if (context.capabilityToken) {
1244
+ this.syncWebSocket.setCapabilityToken(context.capabilityToken);
1245
+ }
1246
+ const syncGroups = this.resolveSyncGroups(context);
1247
+ this.syncWebSocket.setSyncGroups(syncGroups);
1248
+ this.syncWebSocket.setLastSyncId(lastSyncId || 0);
1249
+ // The permanent base scopes for read interest — same set the connection
1250
+ // subscribes to at upgrade, so the two can never disagree.
1251
+ this.areaOfInterest.setBaseGroups(syncGroups);
1252
+ // ── Connection FSM ────────────────────────────────────────────
1253
+ // Instantiate + start the SDK's ConnectionManager so every consumer
1254
+ // gets correct online/offline recovery. Guarded: a retried
1255
+ // `initialize()` reuses the manager it already started.
1256
+ if (!this.connectionManager)
1257
+ this.startConnectionManager(context.kind);
1258
+ this.syncWebSocket.resetReconnectAttempts();
1259
+ this.syncWebSocket.allowConnect();
1260
+ this.syncWebSocket.connect();
1261
+ }
1262
+ /**
1263
+ * Wire the store's handlers onto the connection. Runs once, at
1264
+ * construction — the connection object is stable for the store's
1265
+ * lifetime, so the wiring is too.
1266
+ */
1267
+ wireSocketEvents() {
1137
1268
  // Connection events → forward to connection lifecycle callback
1138
1269
  const onConnected = this.syncWebSocket.subscribe('connected', () => {
1139
1270
  this.syncClient.markConnected();
@@ -1149,7 +1280,7 @@ export class BaseSyncedStore {
1149
1280
  // changed while offline; after a full reconnect the new socket's URL
1150
1281
  // carries only base groups. `resync` re-pushes the current desired set
1151
1282
  // so the server-side index matches what the user is actually viewing.
1152
- void this.areaOfInterest?.resync();
1283
+ void this.areaOfInterest.resync();
1153
1284
  });
1154
1285
  const onDisconnected = this.syncWebSocket.subscribe('disconnected', () => {
1155
1286
  this.syncClient.disconnect();
@@ -1157,7 +1288,7 @@ export class BaseSyncedStore {
1157
1288
  this.updateSyncStatus({ state: 'offline', offlineSince: new Date() });
1158
1289
  });
1159
1290
  const onReconnecting = this.syncWebSocket.subscribe('reconnecting', ({ attempt, delay }) => {
1160
- getContext().logger.info('[BaseSyncedStore] WebSocket reconnecting', { attempt, delay });
1291
+ this.runtime.logger.info('[BaseSyncedStore] WebSocket reconnecting', { attempt, delay });
1161
1292
  this.updateSyncStatus({ state: 'reconnecting' });
1162
1293
  });
1163
1294
  // Delta events → feed into processing pipeline
@@ -1190,7 +1321,7 @@ export class BaseSyncedStore {
1190
1321
  // Terminal session loss (revocation / the login itself is gone): notify,
1191
1322
  // route the FSM to its terminal state, and clear local data.
1192
1323
  const handleTerminalSessionError = (error) => {
1193
- getContext().observability.captureWebSocketError({ context: 'session-error', error: error.message });
1324
+ this.runtime.observability.captureWebSocketError({ context: 'session-error', error: error.message });
1194
1325
  this.onConnectionEvent?.('WS_SESSION_ERROR');
1195
1326
  for (const listener of this.sessionErrorListeners) {
1196
1327
  try {
@@ -1203,13 +1334,13 @@ export class BaseSyncedStore {
1203
1334
  // When auth is revoked, locally cached data must not persist on disk.
1204
1335
  this.database.clear({ includeWriteJournal: true }).catch((clearErr) => {
1205
1336
  // consumer register: session ended, but cached data may remain on disk
1206
- getContext().logger.error('Your session ended, but some locally cached data could not be cleared from this device.');
1207
- getContext().logger.debug('[BaseSyncedStore] Failed to clear database on session error', clearErr);
1337
+ this.runtime.logger.error('Your session ended, but some locally cached data could not be cleared from this device.');
1338
+ this.runtime.logger.debug('[BaseSyncedStore] Failed to clear database on session error', clearErr);
1208
1339
  });
1209
1340
  this.objectPool.clear();
1210
1341
  };
1211
1342
  const onSessionError = this.syncWebSocket.subscribe('session_error', (error) => {
1212
- // WS analog of HTTP's `apikey_expired` (see SyncSessionError.
1343
+ // WS analog of HTTP's `apikey_expired` (see AbloSessionError.
1213
1344
  // isSessionErrorResponse): the hub's keepalive reaper closes sockets
1214
1345
  // whose SHORT-LIVED access credential (`ek_`/`rk_`) passed its expiry
1215
1346
  // with `4001 credential_expired`. That is re-mintable from the
@@ -1218,12 +1349,12 @@ export class BaseSyncedStore {
1218
1349
  // Only a mint that answers `null` (the login itself is gone) falls
1219
1350
  // through to the terminal path. Without this branch, every credential
1220
1351
  // TTL elapse wedged the socket behind the write-once session latch.
1221
- if (SyncSessionError.isSessionError(error) && isAccessCredentialExpiryCloseReason(error.message)) {
1222
- getContext().observability.breadcrumb('WebSocket closed for expired access credential — re-minting', 'sync.websocket', 'warning');
1352
+ if (AbloSessionError.isSessionError(error) && isAccessCredentialExpiryCloseReason(error.message)) {
1353
+ this.runtime.observability.breadcrumb('WebSocket closed for expired access credential — re-minting', 'sync.websocket', 'warning');
1223
1354
  // Un-latch BEFORE the async mint so the FSM's own recovery
1224
1355
  // (probe → refreshing_credential → reconnect) is never blocked on
1225
1356
  // our .then() ordering.
1226
- this.syncWebSocket?.clearSessionError();
1357
+ this.syncWebSocket.clearSessionError();
1227
1358
  void this.performCredentialRefresh().then((outcome) => {
1228
1359
  if (outcome === 'refreshed') {
1229
1360
  if (this.connectionManager) {
@@ -1236,8 +1367,8 @@ export class BaseSyncedStore {
1236
1367
  // (createConnectionManager returns null for kind 'agent') —
1237
1368
  // reconnect the socket directly; connect() reads the
1238
1369
  // freshly-minted credential from the credential source.
1239
- this.syncWebSocket?.resetReconnectAttempts();
1240
- this.syncWebSocket?.connect();
1370
+ this.syncWebSocket.resetReconnectAttempts();
1371
+ this.syncWebSocket.connect();
1241
1372
  }
1242
1373
  return;
1243
1374
  }
@@ -1245,7 +1376,7 @@ export class BaseSyncedStore {
1245
1376
  // The mint endpoint rejected: the long-lived login is gone.
1246
1377
  // Re-latch so writes reject with the permanent session type
1247
1378
  // (see SyncWebSocket.notConnectedError) instead of parking.
1248
- this.syncWebSocket?.setSessionErrorDetected();
1379
+ this.syncWebSocket.setSessionErrorDetected();
1249
1380
  handleTerminalSessionError(error);
1250
1381
  }
1251
1382
  // 'network_error' → transient mint failure. The WS_DISCONNECTED
@@ -1267,19 +1398,20 @@ export class BaseSyncedStore {
1267
1398
  });
1268
1399
  const onReconnectFailed = this.syncWebSocket.subscribe('reconnect_failed', ({ attempts }) => {
1269
1400
  // consumer register: reconnection exhausted — the app is now offline
1270
- getContext().logger.warn('Lost connection to the sync service and could not reconnect. Your app is now offline; changes will sync once the connection is restored.');
1271
- getContext().logger.debug('[BaseSyncedStore] WebSocket reconnection gave up', { attempts });
1401
+ this.runtime.logger.warn('Lost connection to the sync service and could not reconnect. Your app is now offline; changes will sync once the connection is restored.');
1402
+ this.runtime.logger.debug('[BaseSyncedStore] WebSocket reconnection gave up', { attempts });
1272
1403
  this.updateSyncStatus({ state: 'reconnecting' });
1273
1404
  });
1274
- this.disposers.push(onConnected, onDisconnected, onReconnecting, onDelta, onDeltaBatch, onBootstrapRequired, onBootstrapData, onPresenceUpdate, onError, onSessionError, onHandshakeFailed, onReconnectFailed, () => { this.areaOfInterest?.dispose(); this.areaOfInterest = null; });
1275
- // ── Connection FSM ────────────────────────────────────────────
1276
- // Instantiate + start the SDK's ConnectionManager so every
1277
- // consumer gets correct online/offline recovery. Previously this
1278
- // was an external concern (each app rebuilt its own FSM); now
1279
- // it's default behavior. The `onConnectionEvent` hook stays as
1280
- // the bridge WS events fire the hook, the hook forwards into
1281
- // the FSM.
1282
- this.connectionManager = this.createConnectionManager(context.kind);
1405
+ this.disposers.push(onConnected, onDisconnected, onReconnecting, onDelta, onDeltaBatch, onBootstrapRequired, onBootstrapData, onPresenceUpdate, onError, onSessionError, onHandshakeFailed, onReconnectFailed, () => { this.areaOfInterest.dispose(); });
1406
+ }
1407
+ /**
1408
+ * Build and start the connection FSM. The `onConnectionEvent` hook is the
1409
+ * bridge WS events fire the hook, the hook forwards into the FSM. Called
1410
+ * from `setupWebSocketSync` because the FSM's shape depends on the resolved
1411
+ * participant kind (agents get none see {@link createConnectionManager}).
1412
+ */
1413
+ startConnectionManager(kind) {
1414
+ this.connectionManager = this.createConnectionManager(kind);
1283
1415
  if (this.connectionManager) {
1284
1416
  const manager = this.connectionManager;
1285
1417
  // Preserve any externally-set onConnectionEvent — chain rather
@@ -1310,7 +1442,7 @@ export class BaseSyncedStore {
1310
1442
  onReconnect: () => this.performReconnect(),
1311
1443
  onRefreshCredential: () => this.performCredentialRefresh(),
1312
1444
  onSessionExpired: () => {
1313
- const err = new SyncSessionError('Session expired');
1445
+ const err = new AbloSessionError('Session expired');
1314
1446
  for (const listener of this.sessionErrorListeners) {
1315
1447
  try {
1316
1448
  listener(err);
@@ -1319,7 +1451,7 @@ export class BaseSyncedStore {
1319
1451
  }
1320
1452
  },
1321
1453
  onDisconnectWebSocket: () => {
1322
- this.syncWebSocket?.disconnect();
1454
+ this.syncWebSocket.disconnect();
1323
1455
  },
1324
1456
  // Mirror FSM transitions into the visible `syncStatus.state` so
1325
1457
  // the UI can show "Reconnecting…" while the FSM cycles through
@@ -1371,12 +1503,6 @@ export class BaseSyncedStore {
1371
1503
  },
1372
1504
  });
1373
1505
  }
1374
- // Transaction events for pendingChanges tracking
1375
- const unsubCreated = this.syncClient.onTransactionEvent('created', () => { this.incrementPendingChanges(); });
1376
- const unsubCompleted = this.syncClient.onTransactionEvent('completed', () => { this.decrementPendingChanges(); });
1377
- const unsubFailed = this.syncClient.onTransactionEvent('failed', () => { this.decrementPendingChanges(); });
1378
- this.disposers.push(unsubCreated, unsubCompleted, unsubFailed);
1379
- this.syncWebSocket.connect();
1380
1506
  }
1381
1507
  // ── Delta processing pipeline ─────────────────────────────────────────────
1382
1508
  //
@@ -1395,6 +1521,8 @@ export class BaseSyncedStore {
1395
1521
  return this._deltaPipelineContext;
1396
1522
  const store = this;
1397
1523
  this._deltaPipelineContext = {
1524
+ runtime: this.runtime,
1525
+ stagePlugins: this.stagePlugins,
1398
1526
  // Shared pipeline state, backed by the host fields.
1399
1527
  get pendingDeltas() { return store.pendingDeltas; },
1400
1528
  set pendingDeltas(deltas) { store.pendingDeltas = deltas; },
@@ -1412,10 +1540,8 @@ export class BaseSyncedStore {
1412
1540
  advancePersisted: (syncId) => { this.syncClient.position.advancePersisted(syncId); },
1413
1541
  // Persistence + pool writes.
1414
1542
  processDeltaBatch: (deltas) => this.database.processDeltaBatch(deltas),
1415
- applyDeltaBatchToPool: (results) => {
1416
- this.syncClient.applyDeltaBatchToPool(results, (name, data) => this.enrichRelations(name, data));
1417
- },
1418
- acknowledge: (syncId) => { this.syncWebSocket?.acknowledge?.(syncId); },
1543
+ applyDeltaBatchToPool: (results) => { this.applyChangesToPool(results); },
1544
+ acknowledge: (syncId) => { this.syncWebSocket.acknowledge(syncId); },
1419
1545
  get objectPool() { return store.objectPool; },
1420
1546
  // Dynamic-dispatch hooks — protected override points on this class.
1421
1547
  getStateFields: (modelName) => this.getStateFields(modelName),
@@ -1433,6 +1559,14 @@ export class BaseSyncedStore {
1433
1559
  };
1434
1560
  return this._deltaPipelineContext;
1435
1561
  }
1562
+ /**
1563
+ * Lands persisted changes in the in-memory pool, with this store's
1564
+ * relation enrichment bound. The one apply path: the pipeline's bridge
1565
+ * (no plugins installed) and the `humans()` apply handler both call it.
1566
+ */
1567
+ applyChangesToPool(changes) {
1568
+ this.syncClient.applyDeltaBatchToPool(changes, (name, data) => this.enrichRelations(name, data));
1569
+ }
1436
1570
  /** Get fields that represent meaningful state for deduplication. Override for model-specific fields. */
1437
1571
  getStateFields(_modelName) {
1438
1572
  return ['status', 'state', 'isActive'];
@@ -1525,7 +1659,7 @@ export class BaseSyncedStore {
1525
1659
  * schema build time) to find children. The previous implementation did
1526
1660
  * `getByType(ctor).filter(e => e.toJSON()[foreignKey] === parentId)` —
1527
1661
  * a full pool scan per child model + a `toJSON()` allocation per
1528
- * candidate. For a deck delete with 10K layers in the pool, that was
1662
+ * candidate. For a report delete with 10K blocks in the pool, that was
1529
1663
  * 10K toJSON allocations per cascade level. The FK-indexed lookup
1530
1664
  * skips both the scan AND the allocation.
1531
1665
  */
@@ -1545,7 +1679,7 @@ export class BaseSyncedStore {
1545
1679
  }
1546
1680
  }
1547
1681
  if (totalCancelled > 0) {
1548
- getContext().logger.info('[BaseSyncedStore] Cascade cancelled orphaned transactions', {
1682
+ this.runtime.logger.info('[BaseSyncedStore] Cascade cancelled orphaned transactions', {
1549
1683
  parentModel: parentModelName,
1550
1684
  parentId: parentId.slice(0, 12),
1551
1685
  totalCancelled,
@@ -1584,7 +1718,7 @@ export class BaseSyncedStore {
1584
1718
  * Save a model (create or update).
1585
1719
  *
1586
1720
  * Accepts any entity shape with `{ id: string }` so consumers can pass the
1587
- * Zod-inferred model types from `InferModel<Schema, K>` without knowing
1721
+ * Zod-inferred model types from `Model<Schema, K>` without knowing
1588
1722
  * about the internal `Model` base class. At runtime, every entity reaching
1589
1723
  * this method came through the object pool (via `store.create`, a query
1590
1724
  * accessor, or an optimistic insert) and IS a `Model` instance — the one
@@ -1607,7 +1741,7 @@ export class BaseSyncedStore {
1607
1741
  this.syncClient.update(model);
1608
1742
  }
1609
1743
  }
1610
- /** Save with an atomic server mutation (e.g., createSlideWithLayers) */
1744
+ /** Save with an atomic server mutation (e.g., createSectionWithBlocks) */
1611
1745
  async saveWithAtomicMutation(model, mutation) {
1612
1746
  this.objectPool.add(model, ModelScope.live);
1613
1747
  await mutation(this.syncClient.gql);
@@ -1632,7 +1766,7 @@ export class BaseSyncedStore {
1632
1766
  this.syncClient.update(model);
1633
1767
  }
1634
1768
  // ── Query API ────────────────────────────────────────────────────────────
1635
- // `ablo.<model>.get` / `ablo.<model>.getAll` is the read surface for
1769
+ // `ablo.<model>.local.retrieve` / `.local.list` is the read surface for
1636
1770
  // application code. Custom mutators read transactionally through
1637
1771
  // `tx.<model>`, backed by `createReaderActions`.
1638
1772
  /** Retrieve a single entity by id. Synchronous pool read. */
@@ -1655,13 +1789,13 @@ export class BaseSyncedStore {
1655
1789
  * Create a model instance locally, typed via the schema.
1656
1790
  *
1657
1791
  * ```ts
1658
- * const sheet = store.create('spreadsheetSheets', { name, spreadsheetId });
1659
- * // sheet: SpreadsheetSheet | null — no cast needed
1792
+ * const ledger = store.create('ledgers', { name, reportId });
1793
+ * // ledger: Ledger | null — no cast needed
1660
1794
  * ```
1661
1795
  *
1662
1796
  * The `typename` arg is the schema key (camelCase plural, e.g.
1663
- * `'spreadsheetSheets'`); the returned instance has the
1664
- * `InferModel<Schema, K>` shape including computeds + relation accessors.
1797
+ * `'ledgers'`); the returned instance has the
1798
+ * `Model<Schema, K>` shape including computeds + relation accessors.
1665
1799
  * Wraps `pool.create(...)` — the underlying runtime is unchanged, just
1666
1800
  * type-narrowed.
1667
1801
  */
@@ -1718,20 +1852,20 @@ export class BaseSyncedStore {
1718
1852
  /**
1719
1853
  * Get all models of a type. Returns Model[] honestly — callers that need
1720
1854
  * narrow types should use `useAblo((ablo) => ablo.<model>.list(...))`
1721
- * which does proper inference via `InferModel<S, K>`.
1855
+ * which does proper inference via `Model<S, K>`.
1722
1856
  */
1723
1857
  allModelsOfType(modelClass, scope) {
1724
1858
  return this.objectPool.getByType(modelClass, scope ?? ModelScope.live);
1725
1859
  }
1726
1860
  /** Error handler for fire-and-forget flushPendingDeltas calls */
1727
1861
  handleFlushError = (error) => {
1728
- getContext().observability.captureTransactionFailure({
1862
+ this.runtime.observability.captureMutationFailure({
1729
1863
  context: 'flush-pending-deltas',
1730
1864
  modelName: 'batch',
1731
1865
  modelId: 'batch',
1732
1866
  error: error instanceof Error ? error : new Error(String(error)),
1733
1867
  });
1734
- getContext().logger.debug('[BaseSyncedStore] Delta flush error', {
1868
+ this.runtime.logger.debug('[BaseSyncedStore] Delta flush error', {
1735
1869
  error: error instanceof Error ? error.message : String(error),
1736
1870
  });
1737
1871
  };
@@ -1744,8 +1878,6 @@ export class BaseSyncedStore {
1744
1878
  modelId: delta.modelId,
1745
1879
  data: typeof delta.data === 'string' ? JSON.parse(delta.data) : delta.data,
1746
1880
  });
1747
- if (!dbResult)
1748
- return;
1749
1881
  // Track pending deletes for query filtering
1750
1882
  if (dbResult.action === 'remove') {
1751
1883
  this.pendingDeletes.add(dbResult.modelId);