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