@abloatai/ablo 0.34.1 → 0.35.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 (483) hide show
  1. package/AGENTS.md +2 -1
  2. package/CHANGELOG.md +674 -5
  3. package/README.md +39 -22
  4. package/dist/BaseSyncedStore.d.ts +152 -44
  5. package/dist/BaseSyncedStore.js +300 -184
  6. package/dist/Database.d.ts +9 -24
  7. package/dist/Database.js +37 -22
  8. package/dist/InstanceCache.d.ts +25 -4
  9. package/dist/InstanceCache.js +48 -15
  10. package/dist/LazyReferenceCollection.d.ts +3 -3
  11. package/dist/LazyReferenceCollection.js +4 -4
  12. package/dist/Model.d.ts +6 -6
  13. package/dist/Model.js +10 -10
  14. package/dist/ModelRegistry.d.ts +4 -4
  15. package/dist/ModelRegistry.js +3 -3
  16. package/dist/{SyncEngineContext.d.ts → RuntimeContext.d.ts} +20 -13
  17. package/dist/{SyncEngineContext.js → RuntimeContext.js} +11 -12
  18. package/dist/SyncClient.d.ts +42 -32
  19. package/dist/SyncClient.js +166 -110
  20. package/dist/ai-sdk/coordinatedTool.d.ts +2 -2
  21. package/dist/ai-sdk/coordinatedTool.js +1 -1
  22. package/dist/ai-sdk/coordinationContext.d.ts +2 -2
  23. package/dist/ai-sdk/coordinationContext.js +1 -1
  24. package/dist/ai-sdk/wrap.d.ts +3 -3
  25. package/dist/ai-sdk/wrap.js +2 -2
  26. package/dist/auth/index.d.ts +1 -156
  27. package/dist/auth/index.js +8 -301
  28. package/dist/cli.cjs +3344 -1073
  29. package/dist/client/Ablo.d.ts +42 -287
  30. package/dist/client/Ablo.js +118 -963
  31. package/dist/client/abloClient.d.ts +309 -0
  32. package/dist/client/abloClient.js +13 -0
  33. package/dist/client/clientPrelude.d.ts +52 -0
  34. package/dist/client/clientPrelude.js +60 -0
  35. package/dist/client/consoleLogger.d.ts +2 -2
  36. package/dist/client/coreClient.d.ts +60 -0
  37. package/dist/client/coreClient.js +118 -0
  38. package/dist/client/createInternalComponents.d.ts +4 -4
  39. package/dist/client/createInternalComponents.js +9 -8
  40. package/dist/client/createModelProxy.d.ts +78 -373
  41. package/dist/client/createModelProxy.js +114 -86
  42. package/dist/client/humans.d.ts +48 -0
  43. package/dist/client/humans.js +52 -0
  44. package/dist/client/modelRegistration.d.ts +1 -1
  45. package/dist/client/modelRegistration.js +9 -9
  46. package/dist/client/options.d.ts +73 -17
  47. package/dist/client/reactiveEngine.d.ts +48 -0
  48. package/dist/client/reactiveEngine.js +910 -0
  49. package/dist/client/resourceTypes.d.ts +9 -250
  50. package/dist/client/resourceTypes.js +8 -5
  51. package/dist/client/schemaConfig.d.ts +4 -4
  52. package/dist/client/schemaConfig.js +6 -2
  53. package/dist/client/validateAbloOptions.d.ts +3 -2
  54. package/dist/client/validateAbloOptions.js +1 -1
  55. package/dist/client/wsMutationExecutor.d.ts +3 -3
  56. package/dist/client/wsMutationExecutor.js +3 -3
  57. package/dist/context.d.ts +9 -9
  58. package/dist/context.js +10 -9
  59. package/dist/coordination/ClaimLog.d.ts +26 -0
  60. package/dist/coordination/ClaimLog.js +32 -0
  61. package/dist/coordination/index.d.ts +1 -15
  62. package/dist/coordination/index.js +8 -31
  63. package/dist/core/DatabaseManager.js +1 -1
  64. package/dist/core/QueryView.d.ts +1 -1
  65. package/dist/core/QueryView.js +1 -1
  66. package/dist/core/StoreManager.d.ts +4 -23
  67. package/dist/core/StoreManager.js +5 -55
  68. package/dist/core/index.d.ts +2 -2
  69. package/dist/core/index.js +2 -2
  70. package/dist/core/storeContract.d.ts +2 -2
  71. package/dist/docs/catalog.d.ts +72 -0
  72. package/dist/docs/catalog.js +227 -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 +40 -34
  78. package/dist/index.js +26 -20
  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 +2 -2
  99. package/dist/query/client.js +4 -4
  100. package/dist/query/types.d.ts +6 -41
  101. package/dist/query/types.js +2 -2
  102. package/dist/react/AbloProvider.d.ts +6 -8
  103. package/dist/react/AbloProvider.js +5 -7
  104. package/dist/react/context.d.ts +1 -1
  105. package/dist/react/context.js +1 -1
  106. package/dist/react/index.d.ts +5 -5
  107. package/dist/react/index.js +3 -3
  108. package/dist/react/internalContext.d.ts +1 -1
  109. package/dist/react/useAblo.d.ts +3 -3
  110. package/dist/react/useAblo.js +1 -1
  111. package/dist/react/useCurrentUserId.js +1 -1
  112. package/dist/react/useErrorListener.js +1 -1
  113. package/dist/react/useMutationFailureListener.d.ts +2 -2
  114. package/dist/react/useMutationFailureListener.js +1 -1
  115. package/dist/react/useMutators.d.ts +3 -3
  116. package/dist/react/useMutators.js +3 -3
  117. package/dist/react/useUndoScope.d.ts +5 -5
  118. package/dist/react/useUndoScope.js +1 -1
  119. package/dist/schema/coordination.d.ts +69 -10
  120. package/dist/schema/coordination.js +86 -9
  121. package/dist/schema/ddl.js +2 -2
  122. package/dist/schema/diff.d.ts +1 -1
  123. package/dist/schema/generate.js +1 -1
  124. package/dist/schema/index.d.ts +10 -10
  125. package/dist/schema/index.js +18 -18
  126. package/dist/schema/queries.d.ts +27 -27
  127. package/dist/schema/queries.js +23 -23
  128. package/dist/schema/select.d.ts +3 -3
  129. package/dist/schema/select.js +3 -3
  130. package/dist/schema/serialize.d.ts +15 -6
  131. package/dist/schema/serialize.js +17 -3
  132. package/dist/schema/sugar.d.ts +6 -7
  133. package/dist/schema/sugar.js +9 -12
  134. package/dist/schema/syncDeltaRow.d.ts +4 -152
  135. package/dist/schema/syncDeltaRow.js +4 -105
  136. package/dist/server/adapter.d.ts +18 -1
  137. package/dist/server/commit.d.ts +10 -16
  138. package/dist/server/index.d.ts +1 -1
  139. package/dist/server/index.js +1 -1
  140. package/dist/server/readConfig.d.ts +1 -1
  141. package/dist/source/adapters/drizzle.d.ts +1 -1
  142. package/dist/source/adapters/drizzle.js +2 -2
  143. package/dist/source/adapters/kysely.d.ts +1 -1
  144. package/dist/source/adapters/kysely.js +1 -1
  145. package/dist/source/adapters/kyselyMutationCore.d.ts +1 -1
  146. package/dist/source/adapters/kyselyMutationCore.js +2 -2
  147. package/dist/source/adapters/memory.js +1 -1
  148. package/dist/source/adapters/prisma.d.ts +8 -3
  149. package/dist/source/adapters/prisma.js +1 -1
  150. package/dist/source/connector.js +1 -1
  151. package/dist/source/connectorProtocol.d.ts +2 -8
  152. package/dist/source/connectorProtocol.js +3 -2
  153. package/dist/source/contract.d.ts +29 -17
  154. package/dist/source/contract.js +27 -22
  155. package/dist/source/factory.d.ts +1 -1
  156. package/dist/source/footprint.d.ts +111 -0
  157. package/dist/source/footprint.js +0 -0
  158. package/dist/source/idempotency.js +2 -2
  159. package/dist/source/index.d.ts +1 -0
  160. package/dist/source/index.js +3 -0
  161. package/dist/source/next.d.ts +1 -1
  162. package/dist/source/signing.d.ts +9 -2
  163. package/dist/source/signing.js +4 -1
  164. package/dist/source/types.d.ts +6 -4
  165. package/dist/source/types.js +1 -1
  166. package/dist/stores/ObjectStore.d.ts +1 -1
  167. package/dist/stores/SyncActionStore.d.ts +1 -1
  168. package/dist/stores/SyncActionStore.js +2 -10
  169. package/dist/stores/syncAction.d.ts +26 -0
  170. package/dist/stores/syncAction.js +16 -0
  171. package/dist/surface.d.ts +3 -3
  172. package/dist/surface.js +6 -4
  173. package/dist/sync/BootstrapFetcher.d.ts +123 -6
  174. package/dist/sync/BootstrapFetcher.js +492 -66
  175. package/dist/sync/ConnectionManager.d.ts +6 -198
  176. package/dist/sync/ConnectionManager.js +6 -677
  177. package/dist/sync/OnDemandLoader.d.ts +2 -2
  178. package/dist/sync/OnDemandLoader.js +60 -21
  179. package/dist/sync/SubscriptionManager.d.ts +13 -2
  180. package/dist/sync/SubscriptionManager.js +23 -5
  181. package/dist/sync/SyncWebSocket.d.ts +27 -510
  182. package/dist/sync/SyncWebSocket.js +76 -954
  183. package/dist/sync/awaitClaimGrant.d.ts +4 -44
  184. package/dist/sync/awaitClaimGrant.js +4 -109
  185. package/dist/sync/commitFrames.d.ts +6 -40
  186. package/dist/sync/commitFrames.js +6 -97
  187. package/dist/sync/contextPorts.d.ts +18 -0
  188. package/dist/sync/contextPorts.js +31 -0
  189. package/dist/sync/createClaimStream.d.ts +5 -49
  190. package/dist/sync/createClaimStream.js +5 -469
  191. package/dist/sync/createPresenceStream.d.ts +26 -4
  192. package/dist/sync/createPresenceStream.js +28 -20
  193. package/dist/sync/createSnapshot.d.ts +2 -2
  194. package/dist/sync/createSnapshot.js +1 -1
  195. package/dist/sync/credentialLifecycle.d.ts +5 -173
  196. package/dist/sync/credentialLifecycle.js +5 -320
  197. package/dist/sync/deltaPipeline.d.ts +1 -1
  198. package/dist/sync/participants.d.ts +5 -4
  199. package/dist/sync/participants.js +29 -22
  200. package/dist/sync/schemaDrift.d.ts +55 -0
  201. package/dist/sync/schemaDrift.js +53 -0
  202. package/dist/sync/schemas.d.ts +21 -32
  203. package/dist/sync/schemas.js +26 -17
  204. package/dist/sync/syncPlan.d.ts +3 -3
  205. package/dist/sync/wsFrameHandlers.d.ts +6 -114
  206. package/dist/sync/wsFrameHandlers.js +6 -392
  207. package/dist/testing/fixtures/bootstrap.d.ts +1 -1
  208. package/dist/testing/fixtures/deltas.d.ts +1 -1
  209. package/dist/testing/fixtures/httpResponses.d.ts +70 -0
  210. package/dist/testing/fixtures/httpResponses.js +90 -0
  211. package/dist/testing/fixtures/models.js +1 -1
  212. package/dist/testing/helpers/wait.js +1 -1
  213. package/dist/testing/mocks/MockMutationExecutor.d.ts +2 -2
  214. package/dist/testing/mocks/MockMutationExecutor.js +8 -14
  215. package/dist/testing/mocks/MockSyncContext.d.ts +11 -11
  216. package/dist/testing/mocks/MockSyncContext.js +10 -9
  217. package/dist/testing/mocks/MockSyncStore.js +1 -1
  218. package/dist/testing/mocks/MockWebSocket.d.ts +2 -2
  219. package/dist/transaction/ablo.d.ts +88 -0
  220. package/dist/transaction/ablo.js +33 -0
  221. package/dist/{client/auth.d.ts → transaction/auth/apiKey.d.ts} +43 -8
  222. package/dist/{client/auth.js → transaction/auth/apiKey.js} +19 -0
  223. package/dist/transaction/auth/bootstrapScope.d.ts +15 -0
  224. package/dist/transaction/auth/bootstrapScope.js +1 -0
  225. package/dist/transaction/auth/capability.d.ts +177 -0
  226. package/dist/transaction/auth/capability.js +199 -0
  227. package/dist/{auth → transaction/auth}/credentialSource.d.ts +8 -1
  228. package/dist/{client → transaction/auth}/identity.d.ts +8 -7
  229. package/dist/{client → transaction/auth}/identity.js +1 -1
  230. package/dist/transaction/auth/index.d.ts +162 -0
  231. package/dist/transaction/auth/index.js +304 -0
  232. package/dist/{auth → transaction/auth}/schemas.d.ts +1 -1
  233. package/dist/{auth → transaction/auth}/schemas.js +13 -13
  234. package/dist/{client → transaction/auth}/sessionMint.d.ts +8 -8
  235. package/dist/{client → transaction/auth}/sessionMint.js +4 -7
  236. package/dist/transaction/coordination/awaitClaimGrant.d.ts +49 -0
  237. package/dist/transaction/coordination/awaitClaimGrant.js +112 -0
  238. package/dist/transaction/coordination/claimMeta.d.ts +49 -0
  239. package/dist/transaction/coordination/claimMeta.js +52 -0
  240. package/dist/transaction/coordination/createClaimStream.d.ts +64 -0
  241. package/dist/transaction/coordination/createClaimStream.js +475 -0
  242. package/dist/transaction/coordination/events.d.ts +74 -0
  243. package/dist/transaction/coordination/events.js +7 -0
  244. package/dist/transaction/coordination/index.d.ts +19 -0
  245. package/dist/transaction/coordination/index.js +44 -0
  246. package/dist/transaction/coordination/locator.d.ts +83 -0
  247. package/dist/transaction/coordination/locator.js +82 -0
  248. package/dist/transaction/coordination/schema.d.ts +1473 -0
  249. package/dist/{coordination → transaction/coordination}/schema.js +490 -55
  250. package/dist/transaction/coordination/targetConflict.d.ts +2 -0
  251. package/dist/transaction/coordination/targetConflict.js +103 -0
  252. package/dist/{coordination → transaction/coordination}/trace.d.ts +7 -18
  253. package/dist/{coordination → transaction/coordination}/trace.js +18 -25
  254. package/dist/transaction/durableWrites.d.ts +62 -0
  255. package/dist/{client → transaction}/durableWrites.js +28 -3
  256. package/dist/transaction/environment.d.ts +105 -0
  257. package/dist/transaction/environment.js +108 -0
  258. package/dist/{errorCodes.d.ts → transaction/errorCodes.d.ts} +11 -11
  259. package/dist/{errorCodes.js → transaction/errorCodes.js} +35 -12
  260. package/dist/{errors.d.ts → transaction/errors.d.ts} +37 -13
  261. package/dist/{errors.js → transaction/errors.js} +85 -16
  262. package/dist/transaction/index.d.ts +20 -0
  263. package/dist/transaction/index.js +20 -0
  264. package/dist/transaction/keys/index.d.ts +87 -0
  265. package/dist/transaction/keys/index.js +207 -0
  266. package/dist/transaction/log/syncDeltaRow.d.ts +158 -0
  267. package/dist/transaction/log/syncDeltaRow.js +95 -0
  268. package/dist/{sync/syncPosition.d.ts → transaction/logPosition.d.ts} +22 -8
  269. package/dist/{sync/syncPosition.js → transaction/logPosition.js} +15 -6
  270. package/dist/transaction/logger.d.ts +16 -0
  271. package/dist/transaction/logger.js +7 -0
  272. package/dist/transaction/observability.d.ts +53 -0
  273. package/dist/transaction/observability.js +19 -0
  274. package/dist/transaction/plugin.d.ts +192 -0
  275. package/dist/transaction/plugin.js +87 -0
  276. package/dist/{policy → transaction/policy}/types.d.ts +3 -3
  277. package/dist/{policy → transaction/policy}/types.js +2 -0
  278. package/dist/transaction/resources/httpResources.d.ts +266 -0
  279. package/dist/transaction/resources/httpResources.js +7 -0
  280. package/dist/transaction/resources/modelOperations.d.ts +319 -0
  281. package/dist/transaction/resources/modelOperations.js +12 -0
  282. package/dist/transaction/resources/mutationOptions.d.ts +66 -0
  283. package/dist/transaction/resources/mutationOptions.js +9 -0
  284. package/dist/transaction/resources/where.d.ts +85 -0
  285. package/dist/transaction/resources/where.js +70 -0
  286. package/dist/{client → transaction/resources}/writeOptionsSchema.d.ts +2 -6
  287. package/dist/{client → transaction/resources}/writeOptionsSchema.js +9 -5
  288. package/dist/{schema → transaction/schema}/field.d.ts +5 -5
  289. package/dist/{schema → transaction/schema}/field.js +5 -5
  290. package/dist/transaction/schema/loadStrategy.d.ts +45 -0
  291. package/dist/transaction/schema/loadStrategy.js +46 -0
  292. package/dist/{schema → transaction/schema}/model.d.ts +50 -35
  293. package/dist/{schema → transaction/schema}/model.js +30 -20
  294. package/dist/transaction/schema/openapi.d.ts +57 -0
  295. package/dist/transaction/schema/openapi.js +340 -0
  296. package/dist/{schema → transaction/schema}/relation.d.ts +14 -14
  297. package/dist/{schema → transaction/schema}/relation.js +7 -7
  298. package/dist/{schema → transaction/schema}/residency.d.ts +0 -9
  299. package/dist/{schema → transaction/schema}/residency.js +0 -5
  300. package/dist/{schema → transaction/schema}/roles.d.ts +5 -5
  301. package/dist/{schema → transaction/schema}/roles.js +5 -5
  302. package/dist/{schema → transaction/schema}/schema.d.ts +12 -10
  303. package/dist/{schema → transaction/schema}/schema.js +4 -3
  304. package/dist/{schema → transaction/schema}/tenancy.d.ts +1 -1
  305. package/dist/{schema → transaction/schema}/tenancy.js +7 -4
  306. package/dist/transaction/transactionLayer.d.ts +82 -0
  307. package/dist/transaction/transactionLayer.js +24 -0
  308. package/dist/{transactions → transaction/transactions/settlement}/commitEnvelope.d.ts +4 -5
  309. package/dist/{transactions → transaction/transactions/settlement}/commitEnvelope.js +9 -13
  310. package/dist/{transactions → transaction/transactions/settlement}/httpCommitEnvelope.d.ts +9 -2
  311. package/dist/{transactions → transaction/transactions/settlement}/httpCommitEnvelope.js +5 -9
  312. package/dist/{transactions/durableWriteStore.d.ts → transaction/transactions/settlement/pendingWrite.d.ts} +10 -36
  313. package/dist/transaction/transactions/settlement/pendingWrite.js +20 -0
  314. package/dist/transaction/transport/commitFrames.d.ts +90 -0
  315. package/dist/transaction/transport/commitFrames.js +134 -0
  316. package/dist/transaction/transport/connectionManager.d.ts +215 -0
  317. package/dist/transaction/transport/connectionManager.js +673 -0
  318. package/dist/transaction/transport/credentialLifecycle.d.ts +177 -0
  319. package/dist/transaction/transport/credentialLifecycle.js +324 -0
  320. package/dist/{sync → transaction/transport}/heartbeat.d.ts +3 -1
  321. package/dist/{sync → transaction/transport}/heartbeat.js +6 -4
  322. package/dist/{client → transaction/transport}/httpClient.d.ts +59 -16
  323. package/dist/{client → transaction/transport}/httpClient.js +5 -5
  324. package/dist/transaction/transport/httpOptions.d.ts +33 -0
  325. package/dist/transaction/transport/httpOptions.js +12 -0
  326. package/dist/{client → transaction/transport}/httpTransport.js +171 -85
  327. package/dist/{sync/NetworkProbe.d.ts → transaction/transport/networkProbe.d.ts} +7 -4
  328. package/dist/{sync/NetworkProbe.js → transaction/transport/networkProbe.js} +14 -13
  329. package/dist/transaction/transport/wsFrameHandlers.d.ts +128 -0
  330. package/dist/transaction/transport/wsFrameHandlers.js +429 -0
  331. package/dist/transaction/transport/wsTransport.d.ts +576 -0
  332. package/dist/transaction/transport/wsTransport.js +1017 -0
  333. package/dist/transaction/types/assertExact.d.ts +17 -0
  334. package/dist/transaction/types/assertExact.js +1 -0
  335. package/dist/{types → transaction/types}/global.d.ts +17 -2
  336. package/dist/{types → transaction/types}/global.js +2 -1
  337. package/dist/{types → transaction/types}/index.d.ts +14 -46
  338. package/dist/{types → transaction/types}/index.js +7 -16
  339. package/dist/{types → transaction/types}/streams.d.ts +63 -45
  340. package/dist/{utils → transaction/utils}/json.d.ts +18 -0
  341. package/dist/transaction/utils/json.js +276 -0
  342. package/dist/transaction/wire/accountResponses.d.ts +351 -0
  343. package/dist/transaction/wire/accountResponses.js +255 -0
  344. package/dist/transaction/wire/auth.d.ts +49 -0
  345. package/dist/transaction/wire/auth.js +57 -0
  346. package/dist/transaction/wire/claimEvent.d.ts +76 -0
  347. package/dist/transaction/wire/claimEvent.js +73 -0
  348. package/dist/transaction/wire/claims.d.ts +463 -0
  349. package/dist/transaction/wire/claims.js +229 -0
  350. package/dist/{wire → transaction/wire}/commit.d.ts +125 -193
  351. package/dist/{wire → transaction/wire}/commit.js +68 -47
  352. package/dist/{wire → transaction/wire}/delta.d.ts +66 -17
  353. package/dist/{wire → transaction/wire}/delta.js +37 -13
  354. package/dist/transaction/wire/errorEnvelope.d.ts +72 -0
  355. package/dist/{wire → transaction/wire}/errorEnvelope.js +36 -5
  356. package/dist/transaction/wire/feedCursor.d.ts +60 -0
  357. package/dist/transaction/wire/feedCursor.js +82 -0
  358. package/dist/transaction/wire/feedEvent.d.ts +177 -0
  359. package/dist/transaction/wire/feedEvent.js +39 -0
  360. package/dist/transaction/wire/frames.d.ts +194 -0
  361. package/dist/transaction/wire/frames.js +50 -0
  362. package/dist/transaction/wire/inboundFrames.d.ts +552 -0
  363. package/dist/transaction/wire/inboundFrames.js +116 -0
  364. package/dist/transaction/wire/index.d.ts +50 -0
  365. package/dist/transaction/wire/index.js +74 -0
  366. package/dist/{wire → transaction/wire}/listEnvelope.d.ts +13 -14
  367. package/dist/transaction/wire/listEnvelope.js +42 -0
  368. package/dist/transaction/wire/modelResponses.d.ts +85 -0
  369. package/dist/transaction/wire/modelResponses.js +43 -0
  370. package/dist/transactions/{TransactionQueue.d.ts → mutations/MutationQueue.d.ts} +79 -38
  371. package/dist/transactions/{TransactionQueue.js → mutations/MutationQueue.js} +110 -59
  372. package/dist/transactions/{TransactionStore.d.ts → mutations/MutationStore.d.ts} +8 -8
  373. package/dist/transactions/{TransactionStore.js → mutations/MutationStore.js} +2 -2
  374. package/dist/transactions/{coalesceRules.d.ts → mutations/coalesceRules.d.ts} +10 -10
  375. package/dist/transactions/{coalesceRules.js → mutations/coalesceRules.js} +1 -1
  376. package/dist/transactions/mutations/commitLatency.d.ts +52 -0
  377. package/dist/transactions/mutations/commitLatency.js +130 -0
  378. package/dist/transactions/{commitOutboxStore.d.ts → mutations/commitOutboxStore.d.ts} +1 -5
  379. package/dist/transactions/{commitOutboxStore.js → mutations/commitOutboxStore.js} +1 -1
  380. package/dist/transactions/{commitPayload.d.ts → mutations/commitPayload.d.ts} +16 -15
  381. package/dist/transactions/{commitPayload.js → mutations/commitPayload.js} +12 -12
  382. package/dist/transactions/{deltaConfirmation.d.ts → mutations/deltaConfirmation.d.ts} +11 -11
  383. package/dist/transactions/{deltaConfirmation.js → mutations/deltaConfirmation.js} +7 -7
  384. package/dist/transactions/mutations/durableWriteStore.d.ts +14 -0
  385. package/dist/transactions/mutations/durableWriteStore.js +12 -0
  386. package/dist/transactions/{optimisticApply.d.ts → mutations/optimisticApply.d.ts} +7 -7
  387. package/dist/transactions/{replayValidation.d.ts → mutations/replayValidation.d.ts} +3 -3
  388. package/dist/transactions/{replayValidation.js → mutations/replayValidation.js} +4 -3
  389. package/dist/utils/mobxSetup.d.ts +1 -1
  390. package/dist/utils/mobxSetup.js +5 -2
  391. package/dist/webhooks/events.d.ts +2 -2
  392. package/dist/wire/index.d.ts +1 -34
  393. package/dist/wire/index.js +8 -49
  394. package/docs/agent-messaging.md +3 -3
  395. package/docs/agents.md +19 -12
  396. package/docs/api-keys.md +8 -4
  397. package/docs/api.md +22 -18
  398. package/docs/audit.md +2 -0
  399. package/docs/cli.md +31 -3
  400. package/docs/client-behavior.md +8 -6
  401. package/docs/concurrency-convention.md +30 -24
  402. package/docs/coordination.md +48 -38
  403. package/docs/data-sources.md +3 -1
  404. package/docs/debugging.md +5 -3
  405. package/docs/deployment.md +267 -0
  406. package/docs/examples/agent-human.md +49 -42
  407. package/docs/examples/ai-sdk-tool.md +69 -44
  408. package/docs/examples/existing-python-backend.md +8 -6
  409. package/docs/examples/nextjs.md +129 -47
  410. package/docs/examples/scoped-agent.md +45 -44
  411. package/docs/examples/server-agent.md +46 -26
  412. package/docs/groups.md +32 -29
  413. package/docs/guarantees.md +4 -2
  414. package/docs/how-it-works.md +9 -7
  415. package/docs/idempotency.md +126 -0
  416. package/docs/identity.md +58 -54
  417. package/docs/index.md +172 -86
  418. package/docs/integration-guide.md +17 -16
  419. package/docs/interaction-model.md +6 -4
  420. package/docs/mcp.md +41 -16
  421. package/docs/migration.md +63 -5
  422. package/docs/operating-on-your-database.md +3 -1
  423. package/docs/projects.md +2 -0
  424. package/docs/quickstart.md +22 -5
  425. package/docs/react.md +12 -10
  426. package/docs/schema-contract.md +5 -3
  427. package/docs/session-settings.md +108 -0
  428. package/docs/sessions.md +3 -1
  429. package/docs/webhooks.md +3 -1
  430. package/llms.txt +47 -17
  431. package/package.json +10 -8
  432. package/dist/agent/Agent.d.ts +0 -366
  433. package/dist/agent/Agent.js +0 -514
  434. package/dist/agent/index.d.ts +0 -115
  435. package/dist/agent/index.js +0 -128
  436. package/dist/agent/session.d.ts +0 -93
  437. package/dist/agent/session.js +0 -149
  438. package/dist/agent/types.d.ts +0 -68
  439. package/dist/agent/types.js +0 -9
  440. package/dist/client/durableWrites.d.ts +0 -21
  441. package/dist/coordination/schema.d.ts +0 -722
  442. package/dist/schema/openapi.d.ts +0 -29
  443. package/dist/schema/openapi.js +0 -124
  444. package/dist/transactions/durableWriteStore.js +0 -30
  445. package/dist/utils/json.js +0 -88
  446. package/dist/wire/errorEnvelope.d.ts +0 -55
  447. package/dist/wire/frames.d.ts +0 -197
  448. package/dist/wire/frames.js +0 -49
  449. package/dist/wire/listEnvelope.js +0 -18
  450. /package/dist/{client → transaction/auth}/credentialEndpoint.d.ts +0 -0
  451. /package/dist/{client → transaction/auth}/credentialEndpoint.js +0 -0
  452. /package/dist/{auth → transaction/auth}/credentialPolicy.d.ts +0 -0
  453. /package/dist/{auth → transaction/auth}/credentialPolicy.js +0 -0
  454. /package/dist/{auth → transaction/auth}/credentialSource.js +0 -0
  455. /package/dist/{client → transaction/auth}/hostedEndpoints.d.ts +0 -0
  456. /package/dist/{client → transaction/auth}/hostedEndpoints.js +0 -0
  457. /package/dist/{client → transaction/coordination}/claimHeartbeatLoop.d.ts +0 -0
  458. /package/dist/{client → transaction/coordination}/claimHeartbeatLoop.js +0 -0
  459. /package/dist/{client → transaction}/persistence.d.ts +0 -0
  460. /package/dist/{client → transaction}/persistence.js +0 -0
  461. /package/dist/{client → transaction/resources}/functionalUpdate.d.ts +0 -0
  462. /package/dist/{client → transaction/resources}/functionalUpdate.js +0 -0
  463. /package/dist/{transactions → transaction/transactions/settlement}/idempotencyKey.d.ts +0 -0
  464. /package/dist/{transactions → transaction/transactions/settlement}/idempotencyKey.js +0 -0
  465. /package/dist/{client → transaction/transport}/httpTransport.d.ts +0 -0
  466. /package/dist/{types → transaction/types}/modelData.d.ts +0 -0
  467. /package/dist/{types → transaction/types}/modelData.js +0 -0
  468. /package/dist/{types → transaction/types}/participant.d.ts +0 -0
  469. /package/dist/{types → transaction/types}/participant.js +0 -0
  470. /package/dist/{types → transaction/types}/streams.js +0 -0
  471. /package/dist/{utils → transaction/utils}/asyncIterator.d.ts +0 -0
  472. /package/dist/{utils → transaction/utils}/asyncIterator.js +0 -0
  473. /package/dist/{utils → transaction/utils}/duration.d.ts +0 -0
  474. /package/dist/{utils → transaction/utils}/duration.js +0 -0
  475. /package/dist/{wire → transaction/wire}/bootstrapReason.d.ts +0 -0
  476. /package/dist/{wire → transaction/wire}/bootstrapReason.js +0 -0
  477. /package/dist/{wire → transaction/wire}/protocol.d.ts +0 -0
  478. /package/dist/{wire → transaction/wire}/protocol.js +0 -0
  479. /package/dist/{wire → transaction/wire}/protocolVersion.d.ts +0 -0
  480. /package/dist/{wire → transaction/wire}/protocolVersion.js +0 -0
  481. /package/dist/transactions/{UnconfirmedWrites.d.ts → mutations/UnconfirmedWrites.d.ts} +0 -0
  482. /package/dist/transactions/{UnconfirmedWrites.js → mutations/UnconfirmedWrites.js} +0 -0
  483. /package/dist/transactions/{optimisticApply.js → mutations/optimisticApply.js} +0 -0
@@ -0,0 +1,910 @@
1
+ /**
2
+ * The reactive engine build — everything `humans()` means at construction
3
+ * time (ADR 0016). `Ablo({ ... })` resolves auth and capabilities, then hands
4
+ * this module the prelude; from here on it is all materialisation: the
5
+ * runtime context, the internal components, the store and its credential
6
+ * lifecycle, presence and claim streams, the typed model proxies, and the
7
+ * commit/claim/session resources — assembled into the reactive client.
8
+ *
9
+ * Extracted from the factory so the composition root stays a root: resolve,
10
+ * dispatch, return. The construction moves behind `humans().init` proper when
11
+ * the plugin context can carry these inputs — until then the factory calls
12
+ * this directly for the humans-installed path.
13
+ */
14
+ import { durableCommitOperationSchema, } from '../transaction/transactions/settlement/commitEnvelope.js';
15
+ import { AbloAuthenticationError, AbloConnectionError, AbloValidationError, toAbloError, claimedError } from '../transaction/errors.js';
16
+ import { modelTarget, streamTarget, subTarget } from '../transaction/coordination/index.js';
17
+ import { initRuntime } from '../context.js';
18
+ import { getActiveRegistry } from '../ModelRegistry.js';
19
+ import { noopObservability, browserOnlineStatus, defaultSessionErrorDetector, noopAnalytics, } from '../RuntimeContext.js';
20
+ import { alwaysOnline } from '../adapters/alwaysOnline.js';
21
+ import { validateAbloOptions } from './validateAbloOptions.js';
22
+ import {} from '../transaction/auth/index.js';
23
+ import { mintSession } from '../transaction/auth/sessionMint.js';
24
+ import { modelWireNames } from '../transaction/auth/capability.js';
25
+ import { createInternalComponents } from './createInternalComponents.js';
26
+ import { resolveParticipantIdentity } from '../transaction/auth/identity.js';
27
+ import { BaseSyncedStore } from '../BaseSyncedStore.js';
28
+ import { createClaimStream } from '../sync/createClaimStream.js';
29
+ import { awaitClaimGrant } from '../sync/awaitClaimGrant.js';
30
+ import { createSnapshot } from '../sync/createSnapshot.js';
31
+ import { createParticipantManager } from '../sync/participants.js';
32
+ import { resolveApiKeyValue, resolveBootstrapBaseUrl } from '../transaction/auth/apiKey.js';
33
+ import { shouldUseInMemoryPersistence } from '../transaction/persistence.js';
34
+ import { deriveConfigFromSchema } from './schemaConfig.js';
35
+ import { createModelProxy } from './createModelProxy.js';
36
+ import { assertWriteOptions } from '../transaction/resources/writeOptionsSchema.js';
37
+ import { registerModelsFromSchema } from './modelRegistration.js';
38
+ export function buildReactiveEngine(inputs) {
39
+ const { options, internalOptions, url, logger, configuredApiKey, configuredAuthToken, credentialResolver, authCredentials, executor, transport, participantId, kind, presence, createSibling, } = inputs;
40
+ const schema = options.schema;
41
+ // 1. Derive config from schema
42
+ // 1. Derive config from schema, then layer caller-supplied overrides on top.
43
+ // `configOverrides` is a shallow merge: caller takes precedence per key.
44
+ const config = {
45
+ ...deriveConfigFromSchema(schema),
46
+ ...internalOptions.configOverrides,
47
+ };
48
+ // 3. Initialize SDK context (one call — hides all DI wiring).
49
+ // Each provider can be overridden individually; the noop defaults
50
+ // are preserved for the zero-config consumer path.
51
+ initRuntime({
52
+ logger,
53
+ observability: internalOptions.observability ?? noopObservability,
54
+ analytics: internalOptions.analytics ?? noopAnalytics,
55
+ sessionErrorDetector: internalOptions.sessionErrorDetector ?? defaultSessionErrorDetector,
56
+ onlineStatus: internalOptions.onlineStatus ??
57
+ (shouldUseInMemoryPersistence(options)
58
+ ? alwaysOnline()
59
+ : browserOnlineStatus),
60
+ config,
61
+ mutationExecutor: executor,
62
+ getModelMetadata: (name) => getActiveRegistry().getMetadata(name),
63
+ });
64
+ // 4. Create internal components (user never sees these). See
65
+ // `./createInternalComponents.ts` for the construction order
66
+ // and what each component does. Model registration happens
67
+ // here (via `registerModelsFromSchema`, in `./modelRegistration.ts`)
68
+ // because the schema-to-Model-class translation is client-construction
69
+ // wiring that isn't worth pulling into the components module.
70
+ const { modelRegistry, objectPool, bootstrapHelper, database, syncClient, hydration, } = createInternalComponents({
71
+ schema,
72
+ url,
73
+ options: internalOptions,
74
+ auth: authCredentials,
75
+ });
76
+ registerModelsFromSchema(schema, modelRegistry);
77
+ // 5. BaseSyncedStore handles the initialization orchestration
78
+ // (open DB → hydrate IDB → connect WS → fetch bootstrap → hydrate again →
79
+ // ready) and exposes the observable `syncStatus` we expose on the engine.
80
+ //
81
+ // Phase 2: pass the schema into the store so `deriveSyncPlanFromSchema`
82
+ // can auto-populate version vector keys, FK indexes, and enrichment
83
+ // rules from the declarative `belongsTo({ index, enrich })` annotations.
84
+ // Consumers using class-based subclasses with `new SyncedStore(...)`
85
+ // directly can pass explicit config arrays instead.
86
+ const store = new BaseSyncedStore({
87
+ syncClient,
88
+ database,
89
+ objectPool,
90
+ modelRegistry,
91
+ syncWebSocket: transport,
92
+ schema,
93
+ url,
94
+ auth: authCredentials,
95
+ },
96
+ // Collaboration vocabulary is the application's: the SDK subscribes to the
97
+ // event types the caller declares and to nothing by default.
98
+ { collaborationEvents: internalOptions.collaborationEvents ?? [] });
99
+ // Hand the credential lifecycle to the client (refresher + proactive refresh
100
+ // timer + wake/online/focus re-mint). Installed once here so refresh works for
101
+ // any consumer of `Ablo({ auth })`, not only those who render `<AbloProvider>`.
102
+ // The first mint happens in `ready()` so the first connection carries a token.
103
+ //
104
+ // Long-lived server clients also get the pre-roll timer on windowless hosts
105
+ // (`proactiveInNode`): their socket must renew its `rk_` or `ek_` before the
106
+ // server's keepalive reaper closes it (4001 `credential_expired`). Two signals
107
+ // qualify — an agent or system participant, and an absolute endpoint-string
108
+ // `apiKey` (a relative one can't be fetched in Node, so an absolute URL is
109
+ // unambiguously a deliberate server client). User-kind clients in Node (an
110
+ // SSR/RSC module evaluating scaffolded browser code) stay reactive-only.
111
+ if (credentialResolver) {
112
+ const rawEndpoint = internalOptions.authEndpoint ?? internalOptions.apiKey;
113
+ const absoluteEndpoint = typeof rawEndpoint === 'string' && /^https?:\/\//i.test(rawEndpoint);
114
+ store.startCredentialLifecycle(credentialResolver, {
115
+ /* eslint-disable @typescript-eslint/no-deprecated -- `kind` gates the self-hosted proactive pre-roll; hosted path derives it from the apiKey scope */
116
+ proactiveInNode: internalOptions.kind === 'agent' ||
117
+ internalOptions.kind === 'system' ||
118
+ absoluteEndpoint,
119
+ /* eslint-enable @typescript-eslint/no-deprecated */
120
+ });
121
+ }
122
+ // Put the lazy-query lane on the same auth-recovery path as the WebSocket probe
123
+ // and the proactive pre-roll: a 401 on `/sync/query` re-mints via the store's
124
+ // single-flight lifecycle and replays once, instead of silently returning empty
125
+ // rows against an expired `ek_` until the next proactive tick. Late-bound
126
+ // because the coordinator is constructed before the store exists.
127
+ hydration.setCredentialRecovery((recovery) => store.recoverFromAuthRejection(recovery));
128
+ // Bind this executor to this client's MutationQueue. Without it, the queue
129
+ // resolves `mutationExecutor` from the module-level `getContext()`, which
130
+ // `initRuntime()` overwrites on every client construction. In multi-client
131
+ // flows (for example a worker plus a per-job peer) the second `initRuntime()`
132
+ // call would silently redirect the first client's queue through the second
133
+ // client's executor closure, so the first client's commits would dispatch
134
+ // over the wrong connection.
135
+ syncClient.getMutationQueue().setMutationExecutor(executor);
136
+ // Self identity, late-bound the same way the connection's values are: the
137
+ // construction-time guess seeds it (correct on the self-hosted path, empty
138
+ // on the hosted path), and `ready()` overwrites it with what identity
139
+ // resolution derives from the credential's scope. The model proxies read
140
+ // it through getters, so the is-this-claim-mine checks always compare
141
+ // against the resolved identity.
142
+ let selfParticipantId = participantId;
143
+ let selfParticipantKind = kind;
144
+ // Presence + claim streams — the same reference for the engine's lifetime,
145
+ // attached to the connection at construction (it exists — the host built
146
+ // it; sends before the socket opens are dropped by the transport's
147
+ // send-during-reconnect contract, and each stream re-announces on
148
+ // `connected`). The presence stream is the humans() plugin's contribution,
149
+ // built and attached by its `init` (ADR 0016); the claim stream is core
150
+ // coordination, so the root constructs it regardless of the list. Both
151
+ // filter own echoes by participant id, seeded in `ready()` alongside the
152
+ // locals above.
153
+ const presenceStream = presence;
154
+ const claimStream = createClaimStream({ participantId, logger }, transport);
155
+ const participantManager = createParticipantManager({
156
+ ready,
157
+ transport,
158
+ presence: presenceStream,
159
+ claims: claimStream,
160
+ schema,
161
+ });
162
+ // 6. Validate options up front — fail loudly on obviously wrong inputs so
163
+ // strangers don't get silent empty results. Validation errors are written
164
+ // into `store.syncStatus` (the single source of truth).
165
+ const _validationError = validateAbloOptions({
166
+ options: internalOptions,
167
+ url,
168
+ configuredApiKey,
169
+ configuredAuthToken,
170
+ });
171
+ if (_validationError) {
172
+ logger.error(_validationError.message);
173
+ store.syncStatus.state = 'error';
174
+ store.syncStatus.error = _validationError;
175
+ }
176
+ // Deprecated identity overrides are a silent no-op under hosted cloud: when an
177
+ // `apiKey` is configured the SERVER derives participant kind + id from the
178
+ // key's scope, so `kind` / `agentId` passed here are ignored. Setting them and
179
+ // trusting them is the trap (you think you're an agent; the key says user).
180
+ // Warn loudly rather than removing the fields — `agentId` is still load-bearing
181
+ // on the self-hosted path (no apiKey; paired with `capabilityToken`).
182
+ // eslint-disable-next-line @typescript-eslint/no-deprecated -- reads the deprecated fields precisely to warn callers off them under a configured apiKey
183
+ if (configuredApiKey && (internalOptions.kind || internalOptions.agentId)) {
184
+ logger.warn('Ablo: `kind` / `agentId` are ignored when an `apiKey` is configured — ' +
185
+ 'the server derives participant identity from the key’s scope. Remove ' +
186
+ 'them (or mint a scoped session via `ablo.sessions.create({ agent })` ' +
187
+ 'for a distinct agent identity). They apply only to the self-hosted ' +
188
+ '`capabilityToken` path.');
189
+ }
190
+ // 7. The ready() promise drives the BaseSyncedStore.initialize() generator
191
+ // to completion. First call kicks off the initialization; subsequent
192
+ // calls return the same promise (idempotent).
193
+ //
194
+ // Status is tracked in store.syncStatus (MobX observable) — the single
195
+ // source of truth. No duplicate closure variables.
196
+ let _readyPromise = null;
197
+ let _refreshScheduler = null;
198
+ /** Resolved account scope — set once identity resolution completes in
199
+ * `ready()`; exposed as the readonly `ablo.organizationId` accessor. */
200
+ let _resolvedOrganizationId = null;
201
+ async function ready() {
202
+ if (_readyPromise)
203
+ return _readyPromise;
204
+ if (_validationError) {
205
+ _readyPromise = Promise.reject(_validationError);
206
+ return _readyPromise;
207
+ }
208
+ _readyPromise = (async () => {
209
+ try {
210
+ // Mint the first access credential before we connect, so the initial
211
+ // WebSocket upgrade and bootstrap carry a valid bearer (no tokenless first
212
+ // connect that has to self-heal). Only when a refreshing resolver is wired
213
+ // and no static credential is already present. Follows the `apiKey`
214
+ // resolver contract: `null` means the login is gone (terminal — fail ready
215
+ // so the app shows sign-in); a throw means transient (rethrown; autoStart
216
+ // swallows it and the lifecycle's online/wake triggers retry).
217
+ if (credentialResolver && !authCredentials.getAuthToken()) {
218
+ const token = await credentialResolver();
219
+ if (!token) {
220
+ throw new AbloAuthenticationError('Auth resolver returned null before connect — the user is not signed in.', { code: 'auth_no_credentials' });
221
+ }
222
+ authCredentials.setAuthToken(token);
223
+ }
224
+ // Resolve participant identity + scope. Three branches —
225
+ // hosted-cloud apiKey exchange, self-derived from capability
226
+ // token, or legacy explicit options. See `./identity.ts`.
227
+ const resolved = await resolveParticipantIdentity({
228
+ options: internalOptions,
229
+ internalOptions,
230
+ url,
231
+ kind,
232
+ configuredApiKey,
233
+ // Resolve identity against the live token, not the construction-time
234
+ // `configuredAuthToken`. Consumers using a function `apiKey` never pass
235
+ // `authToken` at construction — the lifecycle mints the first `ek_` or
236
+ // `rk_` and calls `setAuthToken()` before `ready()`, which updates the
237
+ // shared credential source. Reading the frozen `configuredAuthToken`
238
+ // here made `/auth/identity` fire with no bearer (returning
239
+ // `no_matching_provider` / `session_expired`) even though the token was
240
+ // present. This reads the shared credential source, like every other
241
+ // transport.
242
+ configuredAuthToken: authCredentials.getAuthToken() ?? configuredAuthToken,
243
+ bootstrapHelper,
244
+ auth: authCredentials,
245
+ logger,
246
+ });
247
+ const { userId, accountScope, teamIds, capabilityToken, syncGroups, participantKind, } = resolved;
248
+ // Fail-loud guard: detect the degenerate "no real sync groups
249
+ // resolved" state before opening the socket. It is the same class of bug as
250
+ // a
251
+ // sensible-looking default that's functionally broken: the
252
+ // SDK ends up subscribing only to the server-side
253
+ // `['default']` fallback, no
254
+ // delta has that tag, live fan-out silently never delivers.
255
+ // For human users (kind:'user') this is almost certainly a
256
+ // misconfiguration upstream — either the caller didn't pass
257
+ // `syncGroups`, or auth resolution didn't derive them, or
258
+ // both. Warn loudly so the next debugging session starts here
259
+ // instead of with "live updates don't work, hard reload fixes
260
+ // it."
261
+ const resolvedSyncGroups = syncGroups ?? [];
262
+ if (participantKind === 'user' &&
263
+ (resolvedSyncGroups.length === 0 ||
264
+ (resolvedSyncGroups.length === 1 && resolvedSyncGroups[0] === 'default'))) {
265
+ // Actionable and not self-healing (no live updates until fixed):
266
+ // kept at warn level for consumers; the low-level diagnostic
267
+ // fields ride the debug log below.
268
+ logger.warn('This client was started without sync groups, so it will not receive ' +
269
+ 'live updates. Pass `syncGroups` (for example ' +
270
+ '`["org:<id>", "user:<id>"]`) or check that your auth provider supplies them.');
271
+ logger.debug('degenerate syncGroups — details', { participantKind, resolvedSyncGroups });
272
+ }
273
+ _resolvedOrganizationId = accountScope;
274
+ // Seed the resolved identity into everything that filters or stamps
275
+ // by participant: the presence and claim streams (own-echo filters,
276
+ // the presence `self` entry) and the model proxies' collaboration
277
+ // checks (via the getters over the locals). This runs before the
278
+ // store connects, so no frame is ever filtered against the
279
+ // construction-time guess.
280
+ selfParticipantId = userId;
281
+ selfParticipantKind = participantKind;
282
+ presenceStream.setParticipant({
283
+ id: userId,
284
+ kind: participantKind,
285
+ syncGroups: resolvedSyncGroups,
286
+ });
287
+ claimStream.setParticipant({ id: userId });
288
+ if (resolved.refreshScheduler) {
289
+ _refreshScheduler = resolved.refreshScheduler;
290
+ }
291
+ // Drive the generator to completion. Each yielded promise is awaited
292
+ // then fed back — this is standard generator consumption.
293
+ //
294
+ // The store.initialize() generator updates store.syncStatus as it
295
+ // progresses (syncing → idle on success, error on failure), so the
296
+ // consumer's `sync.syncStatus` observable reflects real-time state.
297
+ // Resolve bootstrap mode: explicit option wins; otherwise
298
+ // agents default to 'none' (transactional participant — see
299
+ // option doc) and everyone else defaults to 'full'.
300
+ const resolvedBootstrapMode = internalOptions.bootstrapMode ?? (participantKind === 'agent' ? 'none' : 'full');
301
+ const gen = store.initialize({
302
+ userId,
303
+ organizationId: accountScope,
304
+ teamIds,
305
+ kind: participantKind,
306
+ capabilityToken,
307
+ syncGroups,
308
+ bootstrapMode: resolvedBootstrapMode,
309
+ });
310
+ let current = gen.next();
311
+ while (!current.done) {
312
+ const yielded = current.value;
313
+ const resolved = yielded instanceof Promise ? await yielded : yielded;
314
+ current = gen.next(resolved);
315
+ }
316
+ const result = current.value;
317
+ if (!result.success) {
318
+ throw result.error
319
+ ? toAbloError(result.error)
320
+ : new AbloConnectionError('Sync engine initialization failed', {
321
+ code: 'bootstrap_fetch_timeout',
322
+ });
323
+ }
324
+ logger.info('Sync engine ready', { models: Object.keys(schema.models).length });
325
+ }
326
+ catch (err) {
327
+ // Coerce so the rejection a consumer awaiting `ready()` catches is
328
+ // always an AbloError — connection setup is held to the same
329
+ // never-leak-untagged contract as the model operations.
330
+ const error = toAbloError(err);
331
+ // Make sure syncStatus reflects the failure for observer() components
332
+ store.syncStatus.state = 'error';
333
+ store.syncStatus.error = error;
334
+ // Log the typed envelope (type + code + status), not just the bare
335
+ // message — so the console line names it as an Ablo error and carries
336
+ // the code (e.g. AbloAuthenticationError/identity_resolve_failed on a
337
+ // 401) instead of reading like an untagged failure.
338
+ logger.error('Sync engine failed to initialize', {
339
+ type: error.type,
340
+ code: error.code,
341
+ httpStatus: error.httpStatus,
342
+ error: error.message,
343
+ });
344
+ // Clear the memo so a future `ready()` re-attempts bootstrap instead of
345
+ // replaying this rejection forever. Bootstrap failures here are transient
346
+ // by nature — offline, an IndexedDB open timeout, a bootstrap fetch
347
+ // hiccup — and the early `if (_readyPromise) return _readyPromise` guard
348
+ // would otherwise hand every later caller this same dead promise, bricking
349
+ // the engine until a full page reload. Nulling it lets the provider's
350
+ // online/wake/retry triggers drive a clean re-bootstrap. (The terminal
351
+ // `_validationError` branch above intentionally stays cached — config
352
+ // can't change without recreating the engine.)
353
+ _readyPromise = null;
354
+ throw error;
355
+ }
356
+ })();
357
+ return _readyPromise;
358
+ }
359
+ // 9. Optional auto-start for convenience. Opt-in because silent background
360
+ // init has historically been the #1 source of "why isn't my data loading"
361
+ // bug reports. Explicit `await sync.ready()` is the default — errors
362
+ // surface immediately instead of being swallowed.
363
+ if (!_validationError && internalOptions.autoStart) {
364
+ void ready().catch(() => {
365
+ // Error is captured in store.syncStatus; consumers should check
366
+ // `sync.syncStatus.state === 'error'` to detect failures.
367
+ });
368
+ }
369
+ // 9b. waitForFlush — drains pending mutations using the store's
370
+ // pendingChanges counter (already maintained by BaseSyncedStore based
371
+ // on MutationQueue events). Polls every 50ms; uses the existing
372
+ // observable rather than introducing a new event channel.
373
+ async function waitForFlush(timeoutMs) {
374
+ const start = Date.now();
375
+ while (store.syncStatus.pendingChanges > 0) {
376
+ if (timeoutMs !== undefined && Date.now() - start > timeoutMs) {
377
+ throw new AbloConnectionError(`Flush timeout: ${store.syncStatus.pendingChanges} pending mutations after ${timeoutMs}ms`, { code: 'flush_timeout' });
378
+ }
379
+ await new Promise((resolve) => setTimeout(resolve, 50));
380
+ }
381
+ }
382
+ function createClientTxId(idempotencyKey) {
383
+ if (idempotencyKey && idempotencyKey.length > 0)
384
+ return idempotencyKey;
385
+ return typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'
386
+ ? crypto.randomUUID()
387
+ : `tx_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
388
+ }
389
+ function normalizeCommitOperation(op, defaults, fenceToken) {
390
+ const type = op.action.toUpperCase();
391
+ const id = op.id ?? '';
392
+ return durableCommitOperationSchema.parse({
393
+ type,
394
+ model: op.model.toLowerCase(),
395
+ id,
396
+ input: op.data ?? undefined,
397
+ transactionId: op.transactionId ?? undefined,
398
+ readAt: op.readAt ?? defaults.readAt ?? undefined,
399
+ onStale: op.onStale ?? defaults.onStale ?? undefined,
400
+ // The batch's claim (if any) supplies one token for every op, mirroring
401
+ // how it supplies the batch `readAt`.
402
+ fenceToken: op.fenceToken ?? fenceToken ?? undefined,
403
+ });
404
+ }
405
+ function normalizeCommitOperations(commitOptions, fenceToken) {
406
+ if (commitOptions.operations.length === 0) {
407
+ throw new AbloValidationError('Commit requires a non-empty `operations` array.', { code: 'commit_operation_required' });
408
+ }
409
+ return commitOptions.operations.map((op) => normalizeCommitOperation(op, commitOptions, fenceToken));
410
+ }
411
+ function modelClaimFromActive(claim) {
412
+ const target = {
413
+ ...modelTarget(claim.target),
414
+ ...subTarget(claim.target),
415
+ };
416
+ return {
417
+ id: claim.id,
418
+ actor: claim.heldBy ?? "",
419
+ participantKind: claim.participantKind ?? "user",
420
+ description: claim.description,
421
+ field: claim.target.field,
422
+ status: 'active',
423
+ expiresAt: claim.expiresAt ?? 0,
424
+ target,
425
+ // The claim's metadata read as the open record it is on the wire, so a
426
+ // key the coordinator wrote — a heartbeat's `progress` — is readable.
427
+ // `target.meta` is the same bag under the shape the program declared,
428
+ // and a declared shape has no member for something the holder did not
429
+ // write.
430
+ ...(target.meta !== undefined ? { meta: target.meta } : {}),
431
+ };
432
+ }
433
+ function targetMatchesModel(target, claim) {
434
+ if (target.model &&
435
+ claim.target.type.toLowerCase() !== target.model.toLowerCase()) {
436
+ return false;
437
+ }
438
+ if (target.id && claim.target.id !== target.id)
439
+ return false;
440
+ if (target.field && claim.target.field !== target.field)
441
+ return false;
442
+ return true;
443
+ }
444
+ function listModelClaims(target) {
445
+ return claimStream.others
446
+ .filter((claim) => (target ? targetMatchesModel(target, claim) : true))
447
+ .map(modelClaimFromActive);
448
+ }
449
+ function waitForModelUnclaimed(target, options) {
450
+ if (listModelClaims(target).length === 0)
451
+ return Promise.resolve();
452
+ return new Promise((resolve, reject) => {
453
+ let settled = false;
454
+ let timeoutId;
455
+ const cleanup = () => {
456
+ if (timeoutId)
457
+ clearTimeout(timeoutId);
458
+ unsubscribe();
459
+ options?.signal?.removeEventListener('abort', onAbort);
460
+ };
461
+ const finish = (fn) => {
462
+ if (settled)
463
+ return;
464
+ settled = true;
465
+ cleanup();
466
+ fn();
467
+ };
468
+ const check = () => {
469
+ if (listModelClaims(target).length === 0) {
470
+ finish(resolve);
471
+ }
472
+ };
473
+ const abortError = () => new AbloConnectionError('Claim wait aborted.', {
474
+ code: 'claim_wait_aborted',
475
+ cause: options?.signal?.reason,
476
+ });
477
+ const onAbort = () => {
478
+ finish(() => { reject(abortError()); });
479
+ };
480
+ // Answered before the subscription, the listener, and the timer exist,
481
+ // so there is nothing for `cleanup` to undo — and nothing that would
482
+ // read `unsubscribe` ahead of the line that binds it.
483
+ if (options?.signal?.aborted) {
484
+ reject(abortError());
485
+ return;
486
+ }
487
+ const unsubscribe = claimStream.onChange(check);
488
+ options?.signal?.addEventListener('abort', onAbort, { once: true });
489
+ if (options?.timeout != null) {
490
+ timeoutId = setTimeout(() => {
491
+ finish(() => {
492
+ reject(claimedError(target, listModelClaims(target), 'model_claimed_timeout'));
493
+ });
494
+ }, options.timeout);
495
+ }
496
+ });
497
+ }
498
+ function wrapClaimHandle(claim, waited = false, fenceToken) {
499
+ const release = () => {
500
+ claim.revoke?.();
501
+ return Promise.resolve();
502
+ };
503
+ // The token is server-stamped and arrives on the grant frame, so prefer
504
+ // the one `awaitClaimGrant` read there; fall back to any the local handle
505
+ // already carried (immediate, non-queued grants).
506
+ const resolvedFenceToken = fenceToken ?? claim.fenceToken;
507
+ return {
508
+ object: 'claim',
509
+ id: claim.id,
510
+ description: claim.description,
511
+ target: claim.target,
512
+ waited,
513
+ ...(resolvedFenceToken !== undefined ? { fenceToken: resolvedFenceToken } : {}),
514
+ release,
515
+ revoke: claim.revoke,
516
+ // The lease-control members are forwarded explicitly — this wrapper
517
+ // rebuilds the handle field by field, so anything not named here is
518
+ // silently dropped from the public claim.
519
+ heartbeat: claim.heartbeat,
520
+ [Symbol.asyncDispose]: release,
521
+ };
522
+ }
523
+ const publicClaims = Object.assign(claimStream, {
524
+ async create(claimOptions) {
525
+ await ready();
526
+ const claim = claimStream.claim({
527
+ ...streamTarget(claimOptions.target),
528
+ ...subTarget(claimOptions.target),
529
+ }, {
530
+ description: claimOptions.description,
531
+ ttl: claimOptions.ttl,
532
+ queue: claimOptions.queue,
533
+ });
534
+ // With `queue`, the claim is only really *ours* once the server says
535
+ // so (`claim_acquired` if the target was free, `claim_granted` once
536
+ // we reach the head of the FIFO line). Block here on that grant so
537
+ // callers — chiefly `ablo.<model>.claim` — get a handle that already
538
+ // holds the lease, never a half-claimed one racing the queue.
539
+ let waited = false;
540
+ let fenceToken;
541
+ if (claimOptions.queue) {
542
+ try {
543
+ ({ waited, fenceToken } = await awaitClaimGrant(transport, claim.id, {
544
+ timeoutMs: claimOptions.waitTimeoutMs,
545
+ maxQueueDepth: claimOptions.maxQueueDepth,
546
+ logger,
547
+ }));
548
+ }
549
+ catch (err) {
550
+ // Gave up waiting (queue too deep, timed out, or lost) — abandon
551
+ // the queued claim so we don't leave a phantom entry in the
552
+ // line that would block or mislead other claimers.
553
+ claim.revoke?.();
554
+ throw err;
555
+ }
556
+ }
557
+ return wrapClaimHandle(claim, waited, fenceToken);
558
+ },
559
+ list(target) {
560
+ return listModelClaims(target);
561
+ },
562
+ waitFor(target, options) {
563
+ return waitForModelUnclaimed(target, options);
564
+ },
565
+ });
566
+ // Build the typed proxy — one property per model. Done after publicClaims
567
+ // exists so model clients can expose workflow helpers such as
568
+ // `ablo.files.edit(...)` without importing protocol wiring.
569
+ const modelProxies = {};
570
+ for (const [schemaKey, modelDef] of Object.entries(schema.models)) {
571
+ const registeredModelName = modelDef.typename ?? schemaKey;
572
+ modelProxies[schemaKey] = createModelProxy(schemaKey, registeredModelName, objectPool, syncClient, modelRegistry, hydration, {
573
+ createClaim: (claimOptions) => publicClaims.create(claimOptions),
574
+ createSnapshot: (modelKey, id) => createSnapshot({
575
+ pool: objectPool,
576
+ transport,
577
+ // `position.readFloor` is the value claims and snapshots stamp as
578
+ // `readAt` (max of the pool-applied cursor and the acked
579
+ // watermark for our own writes — see logPosition.ts).
580
+ // Stamping a bare stream cursor made a claim taken right after
581
+ // an ack-confirmed write stale against that write's own delta.
582
+ // The socket/store cursors are persistence-gated and therefore
583
+ // never ahead of `applied` — no extra max() needed here.
584
+ getLastSyncId: () => syncClient.position.readFloor,
585
+ entities: { [modelKey]: id },
586
+ }),
587
+ queue: (target) => publicClaims.queueFor(streamTarget(target)),
588
+ reorder: (target, order) => { publicClaims.reorder(streamTarget(target), order); },
589
+ state: (target) => {
590
+ // The live claim stream only tracks *open* (active) claims;
591
+ // terminal states (committed / expired / canceled) drop out of
592
+ // the list entirely — exactly the ephemeral coordination model.
593
+ // So a present entry is, by definition, `status: 'active'`.
594
+ const held = publicClaims.list(modelTarget(target))[0];
595
+ if (!held)
596
+ return null;
597
+ return {
598
+ object: 'claim',
599
+ id: held.id,
600
+ status: 'active',
601
+ target: {
602
+ ...streamTarget(held.target),
603
+ ...subTarget(held.target),
604
+ },
605
+ description: held.description ?? 'editing',
606
+ heldBy: held.actor,
607
+ participantKind: held.participantKind,
608
+ expiresAt: held.expiresAt,
609
+ };
610
+ },
611
+ waitFor: (target, waitOptions) => publicClaims.waitFor(modelTarget(target), waitOptions),
612
+ // Getters, not copies: identity is late-bound (seeded in `ready()`),
613
+ // and the contended / heldBy checks must compare against whoever
614
+ // this client resolved to, not the construction-time guess.
615
+ get selfParticipantId() { return selfParticipantId; },
616
+ get selfParticipantKind() { return selfParticipantKind; },
617
+ // Read-interest / write-intent enrolment for the typed surface.
618
+ // `enterScope`/`pinScope` resolve the `{ [schemaKey]: id }` scope
619
+ // through the same resolver the claim path uses, landing this client in
620
+ // the entity-scoped group the holder's claim presence fans out on.
621
+ // Returns the store promise so the claim write path can await pinScope
622
+ // before acquiring the lease (closing the subscribe-vs-broadcast race);
623
+ // read-interest callers (`retrieve`/`claim.state`) still `void` it and
624
+ // stay fire-and-forget. It's soft either way — the store swallows
625
+ // reconcile errors so read interest never makes a read reject or stall.
626
+ enterScope: (scope) => store.enterScope(scope),
627
+ pinScope: (scope) => store.pinScope(scope),
628
+ // `ablo.<model>.join(ids, { ttl })` performs a scoped participant join
629
+ // on this model's sync group(s). WebSocket only — `join` throws
630
+ // `AbloConnectionError` if the socket isn't ready.
631
+ createJoin: (modelKey, ids, options) => participantManager.join({
632
+ scope: { [modelKey]: ids },
633
+ ...(options?.ttl !== undefined ? { ttlSeconds: options.ttl } : {}),
634
+ }),
635
+ },
636
+ // The client-wide `wait` default; a per-call `wait` still wins.
637
+ internalOptions.wait);
638
+ }
639
+ const commits = {
640
+ async create(commitOptions) {
641
+ await ready();
642
+ // Same runtime contract as the per-model writes — one schema.
643
+ assertWriteOptions({
644
+ idempotencyKey: commitOptions.idempotencyKey,
645
+ readAt: commitOptions.readAt,
646
+ onStale: commitOptions.onStale,
647
+ wait: commitOptions.wait,
648
+ claim: commitOptions.claim,
649
+ }, 'commits.create');
650
+ const clientTxId = createClientTxId(commitOptions.idempotencyKey);
651
+ // A claim handle supplies the batch stale-guard defaults — same
652
+ // semantics as `ablo.<model>.update({ id, data, claim })`, so the
653
+ // two write doors speak one claim vocabulary. Explicit options win.
654
+ const claim = commitOptions.claim ?? null;
655
+ const operations = normalizeCommitOperations({
656
+ ...commitOptions,
657
+ readAt: commitOptions.readAt ?? claim?.readAt ?? null,
658
+ onStale: commitOptions.onStale ?? (claim?.readAt !== undefined ? 'reject' : null),
659
+ }, claim?.fenceToken ?? null);
660
+ const wait = commitOptions.wait ?? 'confirmed';
661
+ // Route through the MutationQueue's commit lane so the call
662
+ // tolerates WS disconnects: the envelope stays in memory until
663
+ // reconnect, mutationExecutor.commit() owns transport-level
664
+ // retry, and `mutation_log` server-side dedupes replays by
665
+ // clientTxId. Replaces the direct ws.sendCommit /
666
+ // sendCommitQueued path that threw synchronously on
667
+ // `ws.readyState !== OPEN`. The queue lives on the internal
668
+ // SyncClient we already hold from createInternalComponents —
669
+ // no need to leak an accessor through BaseSyncedStore.
670
+ const queue = syncClient.getMutationQueue();
671
+ await queue.enqueueCommit(clientTxId, operations, {
672
+ ...(commitOptions.reads ? { reads: [...commitOptions.reads] } : {}),
673
+ ...(commitOptions.track ? { track: [...commitOptions.track] } : {}),
674
+ });
675
+ if (wait === 'queued') {
676
+ return { id: clientTxId, status: 'queued' };
677
+ }
678
+ const { lastSyncId, notifications, missingIds } = await queue.waitForCommitReceipt(clientTxId);
679
+ return {
680
+ id: clientTxId,
681
+ status: 'confirmed',
682
+ lastSyncId,
683
+ ...(notifications && notifications.length > 0 ? { notifications } : {}),
684
+ ...(missingIds && missingIds.length > 0 ? { missingIds } : {}),
685
+ };
686
+ },
687
+ };
688
+ /**
689
+ * The control-plane credential: always the original configured secret key.
690
+ * Never reads `authCredentials` — that holds the exchanged sync credential
691
+ * (a wide-scope `rk_` on the hosted path), which control-plane routes
692
+ * rightly refuse (e.g. the user-session mint is sk_-gated). Counterpart to
693
+ * `getAuthToken()`, which resolves the sync-plane token.
694
+ *
695
+ * The secret-key-only rule is enforced on the server; the credential-kind taxonomy
696
+ * (secret/restricted/ephemeral/publishable) lives in `auth/credentialPolicy`.
697
+ */
698
+ async function controlPlaneApiKey() {
699
+ return resolveApiKeyValue(configuredApiKey);
700
+ }
701
+ /**
702
+ * Resolve the control-plane context a session/agent mint needs (sk_ +
703
+ * bootstrap base URL + the schema-key→typename map the server gates on).
704
+ * Shared by `sessions.create` and `agents.create` so the two mint doors
705
+ * can never drift on how a token is minted. Throws if no `sk_` is present —
706
+ * minting is a backend-only operation.
707
+ */
708
+ async function buildMintContext(resource) {
709
+ const apiKey = await controlPlaneApiKey();
710
+ if (!apiKey) {
711
+ throw new AbloAuthenticationError(`${resource} requires a secret (sk_) API key — call it from your backend, not the browser.`, { code: 'apikey_missing' });
712
+ }
713
+ return {
714
+ apiKey,
715
+ baseUrl: resolveBootstrapBaseUrl({
716
+ url,
717
+ bootstrapBaseUrl: internalOptions.bootstrapBaseUrl,
718
+ }),
719
+ ...(internalOptions.fetch ? { fetch: internalOptions.fetch } : {}),
720
+ // Map every `can` schema-key to the wire typename the server gates on, so a
721
+ // typename override (`documents` → `Document`) doesn't mint a capability
722
+ // the server then denies. Derived from this client's schema by the one rule
723
+ // the HTTP client and the mint route also read. See `MintSessionContext`.
724
+ modelTypenames: modelWireNames(schema.models),
725
+ };
726
+ }
727
+ const engine = {
728
+ ...modelProxies,
729
+ ready,
730
+ waitForFlush,
731
+ /** Durable frame subscription — delegates to the store's registry, which
732
+ * re-attaches across socket rebuilds. */
733
+ subscribe: (event, handler) => store.subscribe(event, handler),
734
+ setAuthToken(token) {
735
+ // The single credential source is read lazily by bootstrap HTTP,
736
+ // lazy query HTTP, network probes, and WebSocket reconnect URL auth.
737
+ // Updating it here is enough for the next request/connect to use the
738
+ // refreshed token; no per-transport patching.
739
+ authCredentials.setAuthToken(token);
740
+ // A fresh credential is useless to a connection parked in offline /
741
+ // backoff / auth_blocked until the next probe trigger — so kick one now.
742
+ // Harmless while connected (the FSM ignores the nudge there).
743
+ store.nudgeReconnect();
744
+ },
745
+ async getAuthToken() {
746
+ // The live short-lived bearer (set via `setAuthToken` / `apiKey`-resolver refresh)
747
+ // is the canonical credential; fall back to a configured API key.
748
+ //
749
+ // This is the sync-plane token (bootstrap, WebSocket, query HTTP). Control-plane
750
+ // calls (sessions.create, datasource registration) never use it — they
751
+ // present the original secret key via `controlPlaneApiKey()` below. The
752
+ // split matters: after the startup exchange this resolver returns the
753
+ // derived wide-scope `rk_`, a credential the control-plane routes
754
+ // correctly refuse (an agent token must never mint humans).
755
+ return (authCredentials.getAuthToken() ??
756
+ (await resolveApiKeyValue(configuredApiKey)) ??
757
+ configuredAuthToken ??
758
+ null);
759
+ },
760
+ setCredentialRefresher(refresher) {
761
+ store.setCredentialRefresher(refresher);
762
+ },
763
+ // The org this client resolved to — null until `ready()` completes. Exposed
764
+ // as a property so integrators can read it programmatically.
765
+ get organizationId() {
766
+ return _resolvedOrganizationId;
767
+ },
768
+ nudgeReconnect() {
769
+ store.nudgeReconnect();
770
+ },
771
+ sessions: {
772
+ // A backend (holding `sk_`) mints a short-lived scoped token for one end
773
+ // user or one agent.
774
+ //
775
+ // Both arms authenticate with the original secret key
776
+ // (`controlPlaneApiKey()`), never the wide-scope `rk_` the startup exchange
777
+ // installed as the sync credential. A derived agent credential silently
778
+ // replacing the secret key on control-plane calls is how humans would get
779
+ // minted as agents — and correct attribution is the point.
780
+ async create(params) {
781
+ // Both mint paths (`{ user }` → /auth/ephemeral-keys → `ek_`,
782
+ // `{ agent, can }` → /auth/capability → scoped `rk_`) resolve their
783
+ // control-plane context through the shared `buildMintContext`, so this
784
+ // client, `agents.create`, and the stateless HTTP client can't drift on
785
+ // how a token is minted.
786
+ return mintSession(params, await buildMintContext('sessions.create'));
787
+ },
788
+ },
789
+ // Mint a scoped agent identity and hand back a connected client bound to it —
790
+ // `sessions.create({ agent })` plus a typed `Ablo({ schema, apiKey })` client,
791
+ // for agents that run in this (secret-key-holding) process. Omitting `id`
792
+ // yields a fresh uuid per call, so concurrent agents are distinct participants
793
+ // that queue behind each other (even when they share a `name`). Humans don't
794
+ // get a server-built client — ship them a token via `sessions.create({ user })`.
795
+ agents: {
796
+ async create(params) {
797
+ // Distinct participant by default: omit `id` → a fresh uuid, so even two
798
+ // agents that share a `name` are independent participants and queue
799
+ // behind one another. `name` is display only (→ userMeta.name); it never
800
+ // derives the id. Pass an explicit `id` only to re-attach an agent to
801
+ // its own held claims.
802
+ const id = params.id ?? globalThis.crypto.randomUUID();
803
+ const userMeta = params.name !== undefined ? { ...params.userMeta, name: params.name } : params.userMeta;
804
+ const sessionParams = {
805
+ agent: { id },
806
+ can: params.can,
807
+ ...(params.syncGroups ? { syncGroups: params.syncGroups } : {}),
808
+ ...(params.ttlSeconds !== undefined ? { ttlSeconds: params.ttlSeconds } : {}),
809
+ ...(userMeta ? { userMeta } : {}),
810
+ };
811
+ // Re-mint the `rk_` on every resolver call so a long-lived agent client
812
+ // never hits token expiry; the `sk_` stays in this process — the child
813
+ // only ever sees its own short-lived `rk_`.
814
+ const mintToken = async () => (await mintSession(sessionParams, await buildMintContext('agents.create')))
815
+ .token;
816
+ // Mint once up front so a bad key / denied scope throws HERE, not later
817
+ // inside the child's bootstrap; reuse that first token, re-mint on refresh.
818
+ let pending = await mintToken();
819
+ const apiKey = async () => {
820
+ if (pending !== null) {
821
+ const token = pending;
822
+ pending = null;
823
+ return token;
824
+ }
825
+ return mintToken();
826
+ };
827
+ return createSibling({ ...internalOptions, apiKey });
828
+ },
829
+ },
830
+ async dispose() {
831
+ _refreshScheduler?.dispose();
832
+ _refreshScheduler = null;
833
+ try {
834
+ await store.disconnect();
835
+ }
836
+ catch (err) {
837
+ // Best-effort teardown — a disposal hiccup isn't consumer-actionable → debug.
838
+ logger.debug('Error during sync engine disposal', { error: err.message });
839
+ }
840
+ presenceStream.dispose();
841
+ claimStream.dispose();
842
+ syncClient.dispose();
843
+ },
844
+ /**
845
+ * Destroy every IndexedDB database owned by this engine. Disconnects
846
+ * the WebSocket, releases timers, and deletes all `ablo_*` / `ablo-*`
847
+ * databases. Typically called on session expiry or explicit logout.
848
+ * Best-effort — errors from individual deletions are swallowed.
849
+ */
850
+ async purge() {
851
+ await store.purge();
852
+ syncClient.dispose();
853
+ },
854
+ /**
855
+ * Subscribe to session-error events. Fires when the server rejects
856
+ * the session (WebSocket close code 1008/4001/4003 or a session_error
857
+ * frame). Multiple subscribers supported; returns an unsubscribe
858
+ * function. Consumers typically use this to trigger auth-failed UI
859
+ * flows (e.g., redirect to sign-in). Does not automatically purge the
860
+ * IndexedDB — call `engine.purge()` from the listener if you need
861
+ * that behavior (the SDK's `<AbloProvider>` does this by default).
862
+ */
863
+ onSessionError(listener) {
864
+ return store.subscribeSessionError(listener);
865
+ },
866
+ onMutationFailure(listener) {
867
+ return store.subscribeMutationFailure(listener);
868
+ },
869
+ onCommitLatency(listener) {
870
+ return store.subscribeCommitLatency(listener);
871
+ },
872
+ waitForConfirmation(modelName, modelId) {
873
+ return store.waitForConfirmation(modelName, modelId);
874
+ },
875
+ // Expose the store's MobX observable directly — single source of truth.
876
+ // React components using observer() will re-render automatically on
877
+ // any state change (syncing, error, offline, pendingChanges, progress).
878
+ get syncStatus() {
879
+ return store.syncStatus;
880
+ },
881
+ schema,
882
+ // ── Internal accessors for framework integration ─────────────────
883
+ // These expose internal components for consumers that need direct
884
+ // access (e.g., SyncEngineProvider wiring SyncContext, collaboration
885
+ // events accessing the WebSocket handle, demand loaders accessing
886
+ // the pool). Prefixed with _ to signal "internal but stable."
887
+ /** The BaseSyncedStore — implements SyncStoreContract for SyncContext.Provider. */
888
+ get _store() { return store; },
889
+ /** The InstanceCache — for demand loaders that need pool.createFromData(). */
890
+ get _pool() { return objectPool; },
891
+ /** The SyncWebSocket — for collaboration events (selection, cursors). */
892
+ get _ws() { return store.getSyncWebSocket(); },
893
+ /** Presence livestream — same socket as entity sync, no second
894
+ * connection. Stable reference across the engine's lifetime. */
895
+ presence: presenceStream,
896
+ /** Claim livestream — same socket. Stable reference. */
897
+ claims: publicClaims,
898
+ commits,
899
+ /** Context-staleness snapshot — see `engine.snapshot(...)` JSDoc. */
900
+ snapshot(entities) {
901
+ return createSnapshot({
902
+ pool: objectPool,
903
+ transport,
904
+ getLastSyncId: () => transport.getLastSyncId(),
905
+ entities,
906
+ });
907
+ },
908
+ };
909
+ return engine;
910
+ }