@abloatai/ablo 0.36.0 → 0.37.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 (597) hide show
  1. package/AGENTS.md +2 -2
  2. package/CHANGELOG.md +71 -2013
  3. package/NOTICE +2 -2
  4. package/README.md +25 -71
  5. package/assets/banner.png +0 -0
  6. package/dist/auth.d.ts +2 -0
  7. package/dist/auth.d.ts.map +1 -0
  8. package/dist/auth.js +2 -0
  9. package/dist/auth.js.map +1 -0
  10. package/dist/client.d.ts +3 -0
  11. package/dist/client.d.ts.map +1 -0
  12. package/dist/client.js +3 -0
  13. package/dist/client.js.map +1 -0
  14. package/dist/coordination.d.ts +2 -0
  15. package/dist/coordination.d.ts.map +1 -0
  16. package/dist/coordination.js +2 -0
  17. package/dist/coordination.js.map +1 -0
  18. package/dist/index.d.ts +3 -114
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +3 -163
  21. package/dist/index.js.map +1 -0
  22. package/dist/react.d.ts +4 -0
  23. package/dist/react.d.ts.map +1 -0
  24. package/dist/react.js +3 -0
  25. package/dist/react.js.map +1 -0
  26. package/dist/schema.d.ts +2 -0
  27. package/dist/schema.d.ts.map +1 -0
  28. package/dist/schema.js +2 -0
  29. package/dist/schema.js.map +1 -0
  30. package/dist/server.d.ts +2 -0
  31. package/dist/server.d.ts.map +1 -0
  32. package/dist/server.js +2 -0
  33. package/dist/server.js.map +1 -0
  34. package/dist/source-conformance.d.ts +2 -0
  35. package/dist/source-conformance.d.ts.map +1 -0
  36. package/dist/source-conformance.js +2 -0
  37. package/dist/source-conformance.js.map +1 -0
  38. package/dist/source-drizzle.d.ts +2 -0
  39. package/dist/source-drizzle.d.ts.map +1 -0
  40. package/dist/source-drizzle.js +2 -0
  41. package/dist/source-drizzle.js.map +1 -0
  42. package/dist/source-kysely.d.ts +2 -0
  43. package/dist/source-kysely.d.ts.map +1 -0
  44. package/dist/source-kysely.js +2 -0
  45. package/dist/source-kysely.js.map +1 -0
  46. package/dist/source-next.d.ts +2 -0
  47. package/dist/source-next.d.ts.map +1 -0
  48. package/dist/source-next.js +2 -0
  49. package/dist/source-next.js.map +1 -0
  50. package/dist/source.d.ts +2 -0
  51. package/dist/source.d.ts.map +1 -0
  52. package/dist/source.js +2 -0
  53. package/dist/source.js.map +1 -0
  54. package/dist/wire.d.ts +2 -0
  55. package/dist/wire.d.ts.map +1 -0
  56. package/dist/wire.js +2 -0
  57. package/dist/wire.js.map +1 -0
  58. package/docs/agents.md +1 -1
  59. package/docs/api-keys.md +7 -6
  60. package/docs/api.md +10 -10
  61. package/docs/client-behavior.md +5 -5
  62. package/docs/coordination.md +52 -62
  63. package/docs/data-sources.md +1 -1
  64. package/docs/examples/agent-human.md +4 -4
  65. package/docs/examples/ai-sdk-tool.md +1 -1
  66. package/docs/examples/existing-python-backend.md +15 -4
  67. package/docs/examples/nextjs.md +27 -6
  68. package/docs/examples/server-agent.md +2 -2
  69. package/docs/how-it-works.md +4 -4
  70. package/docs/identity.md +2 -1
  71. package/docs/integration-guide.md +24 -13
  72. package/docs/internal/README.md +18 -0
  73. package/docs/internal/agent-fleet-coordination-design.md +171 -0
  74. package/docs/internal/agent-orchestration.md +58 -0
  75. package/docs/internal/commit-identifiers.md +91 -0
  76. package/docs/internal/concurrency-open-decisions.md +37 -0
  77. package/docs/internal/data-source-reverse-channel.md +150 -0
  78. package/docs/internal/per-field-conflict-detection.md +165 -0
  79. package/docs/internal/postgres-replication.md +64 -0
  80. package/docs/internal/serializable-schema.md +119 -0
  81. package/docs/internal/structure.md +32 -0
  82. package/docs/mcp.md +2 -2
  83. package/docs/migration.md +3 -3
  84. package/docs/quickstart.md +2 -2
  85. package/docs/react.md +5 -5
  86. package/docs/schema-contract.md +3 -3
  87. package/docs/sessions.md +91 -37
  88. package/examples/README.md +2 -2
  89. package/examples/data-source/README.md +1 -1
  90. package/examples/data-source/ablo-driver.ts +1 -1
  91. package/examples/data-source/customer-server.ts +1 -1
  92. package/examples/data-source/run.ts +1 -1
  93. package/examples/data-source/schema.ts +1 -1
  94. package/examples/quickstart.ts +2 -2
  95. package/llms.txt +8 -8
  96. package/package.json +63 -166
  97. package/bin/ablo.cjs +0 -39
  98. package/dist/BaseSyncedStore.d.ts +0 -843
  99. package/dist/BaseSyncedStore.js +0 -1971
  100. package/dist/Database.d.ts +0 -323
  101. package/dist/Database.js +0 -1502
  102. package/dist/InstanceCache.d.ts +0 -237
  103. package/dist/InstanceCache.js +0 -1166
  104. package/dist/LazyReferenceCollection.d.ts +0 -177
  105. package/dist/LazyReferenceCollection.js +0 -461
  106. package/dist/Model.d.ts +0 -454
  107. package/dist/Model.js +0 -919
  108. package/dist/ModelRegistry.d.ts +0 -225
  109. package/dist/ModelRegistry.js +0 -539
  110. package/dist/NetworkMonitor.d.ts +0 -28
  111. package/dist/NetworkMonitor.js +0 -79
  112. package/dist/RuntimeContext.d.ts +0 -52
  113. package/dist/RuntimeContext.js +0 -80
  114. package/dist/SyncClient.d.ts +0 -541
  115. package/dist/SyncClient.js +0 -2202
  116. package/dist/adapters/alwaysOnline.d.ts +0 -14
  117. package/dist/adapters/alwaysOnline.js +0 -17
  118. package/dist/adapters/inMemoryStorage.d.ts +0 -31
  119. package/dist/adapters/inMemoryStorage.js +0 -110
  120. package/dist/ai-sdk/coordinatedTool.d.ts +0 -120
  121. package/dist/ai-sdk/coordinatedTool.js +0 -134
  122. package/dist/ai-sdk/coordinationContext.d.ts +0 -46
  123. package/dist/ai-sdk/coordinationContext.js +0 -106
  124. package/dist/ai-sdk/index.d.ts +0 -121
  125. package/dist/ai-sdk/index.js +0 -121
  126. package/dist/ai-sdk/wrap.d.ts +0 -65
  127. package/dist/ai-sdk/wrap.js +0 -39
  128. package/dist/auth/index.d.ts +0 -1
  129. package/dist/auth/index.js +0 -8
  130. package/dist/batching/index.d.ts +0 -55
  131. package/dist/batching/index.js +0 -147
  132. package/dist/client/Ablo.d.ts +0 -231
  133. package/dist/client/Ablo.js +0 -160
  134. package/dist/client/abloClient.d.ts +0 -309
  135. package/dist/client/abloClient.js +0 -13
  136. package/dist/client/clientPrelude.d.ts +0 -52
  137. package/dist/client/clientPrelude.js +0 -60
  138. package/dist/client/consoleLogger.d.ts +0 -35
  139. package/dist/client/consoleLogger.js +0 -44
  140. package/dist/client/coreClient.d.ts +0 -60
  141. package/dist/client/coreClient.js +0 -118
  142. package/dist/client/createInternalComponents.d.ts +0 -50
  143. package/dist/client/createInternalComponents.js +0 -98
  144. package/dist/client/createModelProxy.d.ts +0 -248
  145. package/dist/client/createModelProxy.js +0 -884
  146. package/dist/client/humans.d.ts +0 -69
  147. package/dist/client/humans.js +0 -78
  148. package/dist/client/modelRegistration.d.ts +0 -10
  149. package/dist/client/modelRegistration.js +0 -312
  150. package/dist/client/options.d.ts +0 -461
  151. package/dist/client/options.js +0 -7
  152. package/dist/client/reactiveEngine.d.ts +0 -53
  153. package/dist/client/reactiveEngine.js +0 -688
  154. package/dist/client/resourceTypes.d.ts +0 -12
  155. package/dist/client/resourceTypes.js +0 -10
  156. package/dist/client/schemaConfig.d.ts +0 -44
  157. package/dist/client/schemaConfig.js +0 -185
  158. package/dist/client/storeCluster.d.ts +0 -47
  159. package/dist/client/storeCluster.js +0 -118
  160. package/dist/client/storeLifecycle.d.ts +0 -61
  161. package/dist/client/storeLifecycle.js +0 -231
  162. package/dist/client/validateAbloOptions.d.ts +0 -42
  163. package/dist/client/validateAbloOptions.js +0 -43
  164. package/dist/client/wsMutationExecutor.d.ts +0 -27
  165. package/dist/client/wsMutationExecutor.js +0 -72
  166. package/dist/context.d.ts +0 -42
  167. package/dist/context.js +0 -81
  168. package/dist/coordination/ClaimLog.d.ts +0 -26
  169. package/dist/coordination/ClaimLog.js +0 -32
  170. package/dist/coordination/index.d.ts +0 -1
  171. package/dist/coordination/index.js +0 -8
  172. package/dist/core/index.d.ts +0 -33
  173. package/dist/core/index.js +0 -48
  174. package/dist/docs/catalog.d.ts +0 -72
  175. package/dist/docs/catalog.js +0 -230
  176. package/dist/docs/index.d.ts +0 -10
  177. package/dist/docs/index.js +0 -10
  178. package/dist/environment.d.ts +0 -1
  179. package/dist/environment.js +0 -8
  180. package/dist/interfaces/index.d.ts +0 -311
  181. package/dist/interfaces/index.js +0 -9
  182. package/dist/keys/index.d.ts +0 -1
  183. package/dist/keys/index.js +0 -8
  184. package/dist/mutators/RecordingMutation.d.ts +0 -36
  185. package/dist/mutators/RecordingMutation.js +0 -182
  186. package/dist/mutators/Transaction.d.ts +0 -40
  187. package/dist/mutators/Transaction.js +0 -58
  188. package/dist/mutators/UndoManager.d.ts +0 -258
  189. package/dist/mutators/UndoManager.js +0 -658
  190. package/dist/mutators/defineMutators.d.ts +0 -60
  191. package/dist/mutators/defineMutators.js +0 -18
  192. package/dist/mutators/inverseOp.d.ts +0 -126
  193. package/dist/mutators/inverseOp.js +0 -71
  194. package/dist/mutators/mutateActions.d.ts +0 -45
  195. package/dist/mutators/mutateActions.js +0 -105
  196. package/dist/mutators/readerActions.d.ts +0 -33
  197. package/dist/mutators/readerActions.js +0 -57
  198. package/dist/mutators/undoApply.d.ts +0 -51
  199. package/dist/mutators/undoApply.js +0 -117
  200. package/dist/policy/index.d.ts +0 -21
  201. package/dist/policy/index.js +0 -20
  202. package/dist/query/QueryProcessor.d.ts +0 -75
  203. package/dist/query/QueryProcessor.js +0 -255
  204. package/dist/query/client.d.ts +0 -64
  205. package/dist/query/client.js +0 -138
  206. package/dist/query/types.d.ts +0 -85
  207. package/dist/query/types.js +0 -16
  208. package/dist/react/AbloProvider.d.ts +0 -242
  209. package/dist/react/AbloProvider.js +0 -458
  210. package/dist/react/ClientSideSuspense.d.ts +0 -36
  211. package/dist/react/ClientSideSuspense.js +0 -17
  212. package/dist/react/DefaultFallback.d.ts +0 -24
  213. package/dist/react/DefaultFallback.js +0 -43
  214. package/dist/react/context.d.ts +0 -55
  215. package/dist/react/context.js +0 -29
  216. package/dist/react/createAbloReact.d.ts +0 -56
  217. package/dist/react/createAbloReact.js +0 -51
  218. package/dist/react/index.d.ts +0 -62
  219. package/dist/react/index.js +0 -69
  220. package/dist/react/internalContext.d.ts +0 -33
  221. package/dist/react/internalContext.js +0 -3
  222. package/dist/react/useAblo.d.ts +0 -82
  223. package/dist/react/useAblo.js +0 -120
  224. package/dist/react/useCurrentUserId.d.ts +0 -22
  225. package/dist/react/useCurrentUserId.js +0 -34
  226. package/dist/react/useErrorListener.d.ts +0 -20
  227. package/dist/react/useErrorListener.js +0 -38
  228. package/dist/react/useMutationFailureListener.d.ts +0 -26
  229. package/dist/react/useMutationFailureListener.js +0 -38
  230. package/dist/react/useMutators.d.ts +0 -56
  231. package/dist/react/useMutators.js +0 -84
  232. package/dist/react/useReactive.d.ts +0 -35
  233. package/dist/react/useReactive.js +0 -123
  234. package/dist/react/useSyncStatus.d.ts +0 -59
  235. package/dist/react/useSyncStatus.js +0 -76
  236. package/dist/react/useUndoScope.d.ts +0 -34
  237. package/dist/react/useUndoScope.js +0 -81
  238. package/dist/schema/coordination.d.ts +0 -112
  239. package/dist/schema/coordination.js +0 -133
  240. package/dist/schema/ddl.d.ts +0 -97
  241. package/dist/schema/ddl.js +0 -491
  242. package/dist/schema/ddlLock.d.ts +0 -35
  243. package/dist/schema/ddlLock.js +0 -46
  244. package/dist/schema/diff.d.ts +0 -225
  245. package/dist/schema/diff.js +0 -289
  246. package/dist/schema/generate.d.ts +0 -19
  247. package/dist/schema/generate.js +0 -86
  248. package/dist/schema/index.d.ts +0 -42
  249. package/dist/schema/index.js +0 -80
  250. package/dist/schema/queries.d.ts +0 -201
  251. package/dist/schema/queries.js +0 -144
  252. package/dist/schema/select.d.ts +0 -40
  253. package/dist/schema/select.js +0 -90
  254. package/dist/schema/serialize.d.ts +0 -115
  255. package/dist/schema/serialize.js +0 -265
  256. package/dist/schema/sugar.d.ts +0 -109
  257. package/dist/schema/sugar.js +0 -83
  258. package/dist/schema/syncDeltaRow.d.ts +0 -6
  259. package/dist/schema/syncDeltaRow.js +0 -6
  260. package/dist/server/adapter.d.ts +0 -173
  261. package/dist/server/adapter.js +0 -18
  262. package/dist/server/commit.d.ts +0 -107
  263. package/dist/server/commit.js +0 -1
  264. package/dist/server/index.d.ts +0 -14
  265. package/dist/server/index.js +0 -2
  266. package/dist/server/readConfig.d.ts +0 -80
  267. package/dist/server/readConfig.js +0 -8
  268. package/dist/server/storageMode.d.ts +0 -23
  269. package/dist/server/storageMode.js +0 -17
  270. package/dist/source/adapter.d.ts +0 -83
  271. package/dist/source/adapter.js +0 -24
  272. package/dist/source/adapters/drizzle.d.ts +0 -48
  273. package/dist/source/adapters/drizzle.js +0 -219
  274. package/dist/source/adapters/kysely.d.ts +0 -42
  275. package/dist/source/adapters/kysely.js +0 -205
  276. package/dist/source/adapters/kyselyMutationCore.d.ts +0 -76
  277. package/dist/source/adapters/kyselyMutationCore.js +0 -125
  278. package/dist/source/adapters/memory.d.ts +0 -13
  279. package/dist/source/adapters/memory.js +0 -130
  280. package/dist/source/adapters/prisma.d.ts +0 -63
  281. package/dist/source/adapters/prisma.js +0 -202
  282. package/dist/source/conformance.d.ts +0 -37
  283. package/dist/source/conformance.js +0 -215
  284. package/dist/source/connector.d.ts +0 -95
  285. package/dist/source/connector.js +0 -266
  286. package/dist/source/connectorProtocol.d.ts +0 -154
  287. package/dist/source/connectorProtocol.js +0 -163
  288. package/dist/source/contract.d.ts +0 -195
  289. package/dist/source/contract.js +0 -164
  290. package/dist/source/factory.d.ts +0 -92
  291. package/dist/source/factory.js +0 -286
  292. package/dist/source/idempotency.d.ts +0 -61
  293. package/dist/source/idempotency.js +0 -144
  294. package/dist/source/index.d.ts +0 -23
  295. package/dist/source/index.js +0 -30
  296. package/dist/source/migrations.d.ts +0 -21
  297. package/dist/source/migrations.js +0 -103
  298. package/dist/source/next.d.ts +0 -32
  299. package/dist/source/next.js +0 -25
  300. package/dist/source/pushQueue.d.ts +0 -134
  301. package/dist/source/pushQueue.js +0 -256
  302. package/dist/source/signing.d.ts +0 -92
  303. package/dist/source/signing.js +0 -162
  304. package/dist/source/types.d.ts +0 -401
  305. package/dist/source/types.js +0 -59
  306. package/dist/storeContract.d.ts +0 -145
  307. package/dist/storeContract.js +0 -12
  308. package/dist/stores/DatabaseManager.d.ts +0 -107
  309. package/dist/stores/DatabaseManager.js +0 -388
  310. package/dist/stores/ObjectStore.d.ts +0 -115
  311. package/dist/stores/ObjectStore.js +0 -393
  312. package/dist/stores/ObjectStoreContract.d.ts +0 -38
  313. package/dist/stores/ObjectStoreContract.js +0 -1
  314. package/dist/stores/StoreManager.d.ts +0 -114
  315. package/dist/stores/StoreManager.js +0 -304
  316. package/dist/stores/SyncActionStore.d.ts +0 -99
  317. package/dist/stores/SyncActionStore.js +0 -506
  318. package/dist/stores/openIDBWithTimeout.d.ts +0 -65
  319. package/dist/stores/openIDBWithTimeout.js +0 -153
  320. package/dist/stores/syncAction.d.ts +0 -26
  321. package/dist/stores/syncAction.js +0 -16
  322. package/dist/surface.d.ts +0 -36
  323. package/dist/surface.js +0 -75
  324. package/dist/sync/BootstrapFetcher.d.ts +0 -284
  325. package/dist/sync/BootstrapFetcher.js +0 -964
  326. package/dist/sync/ConnectionManager.d.ts +0 -8
  327. package/dist/sync/ConnectionManager.js +0 -8
  328. package/dist/sync/OnDemandLoader.d.ts +0 -231
  329. package/dist/sync/OnDemandLoader.js +0 -743
  330. package/dist/sync/SubscriptionManager.d.ts +0 -159
  331. package/dist/sync/SubscriptionManager.js +0 -243
  332. package/dist/sync/SyncWebSocket.d.ts +0 -173
  333. package/dist/sync/SyncWebSocket.js +0 -438
  334. package/dist/sync/awaitClaimGrant.d.ts +0 -6
  335. package/dist/sync/awaitClaimGrant.js +0 -6
  336. package/dist/sync/bootstrapApply.d.ts +0 -73
  337. package/dist/sync/bootstrapApply.js +0 -73
  338. package/dist/sync/commitFrames.d.ts +0 -8
  339. package/dist/sync/commitFrames.js +0 -8
  340. package/dist/sync/contextPorts.d.ts +0 -18
  341. package/dist/sync/contextPorts.js +0 -31
  342. package/dist/sync/createClaimStream.d.ts +0 -7
  343. package/dist/sync/createClaimStream.js +0 -7
  344. package/dist/sync/createPresenceStream.d.ts +0 -69
  345. package/dist/sync/createPresenceStream.js +0 -200
  346. package/dist/sync/createSnapshot.d.ts +0 -29
  347. package/dist/sync/createSnapshot.js +0 -118
  348. package/dist/sync/credentialLifecycle.d.ts +0 -7
  349. package/dist/sync/credentialLifecycle.js +0 -7
  350. package/dist/sync/deltaPipeline.d.ts +0 -114
  351. package/dist/sync/deltaPipeline.js +0 -278
  352. package/dist/sync/groupChange.d.ts +0 -116
  353. package/dist/sync/groupChange.js +0 -244
  354. package/dist/sync/participants.d.ts +0 -132
  355. package/dist/sync/participants.js +0 -346
  356. package/dist/sync/persistedPrefix.d.ts +0 -12
  357. package/dist/sync/persistedPrefix.js +0 -22
  358. package/dist/sync/schemaDrift.d.ts +0 -55
  359. package/dist/sync/schemaDrift.js +0 -53
  360. package/dist/sync/schemas.d.ts +0 -71
  361. package/dist/sync/schemas.js +0 -94
  362. package/dist/sync/syncCursor.d.ts +0 -40
  363. package/dist/sync/syncCursor.js +0 -55
  364. package/dist/sync/syncPlan.d.ts +0 -54
  365. package/dist/sync/syncPlan.js +0 -50
  366. package/dist/sync/wsFrameHandlers.d.ts +0 -8
  367. package/dist/sync/wsFrameHandlers.js +0 -8
  368. package/dist/syncLog/contract.d.ts +0 -20
  369. package/dist/syncLog/contract.js +0 -19
  370. package/dist/syncLog/index.d.ts +0 -1
  371. package/dist/syncLog/index.js +0 -1
  372. package/dist/transaction/ablo.d.ts +0 -88
  373. package/dist/transaction/ablo.js +0 -33
  374. package/dist/transaction/auth/apiKey.d.ts +0 -152
  375. package/dist/transaction/auth/apiKey.js +0 -419
  376. package/dist/transaction/auth/bootstrapScope.d.ts +0 -15
  377. package/dist/transaction/auth/bootstrapScope.js +0 -1
  378. package/dist/transaction/auth/capability.d.ts +0 -212
  379. package/dist/transaction/auth/capability.js +0 -224
  380. package/dist/transaction/auth/credentialEndpoint.d.ts +0 -61
  381. package/dist/transaction/auth/credentialEndpoint.js +0 -86
  382. package/dist/transaction/auth/credentialPolicy.d.ts +0 -148
  383. package/dist/transaction/auth/credentialPolicy.js +0 -125
  384. package/dist/transaction/auth/credentialSource.d.ts +0 -30
  385. package/dist/transaction/auth/credentialSource.js +0 -55
  386. package/dist/transaction/auth/hostedEndpoints.d.ts +0 -21
  387. package/dist/transaction/auth/hostedEndpoints.js +0 -21
  388. package/dist/transaction/auth/identity.d.ts +0 -55
  389. package/dist/transaction/auth/identity.js +0 -210
  390. package/dist/transaction/auth/index.d.ts +0 -162
  391. package/dist/transaction/auth/index.js +0 -304
  392. package/dist/transaction/auth/schemas.d.ts +0 -59
  393. package/dist/transaction/auth/schemas.js +0 -85
  394. package/dist/transaction/auth/sessionMint.d.ts +0 -28
  395. package/dist/transaction/auth/sessionMint.js +0 -85
  396. package/dist/transaction/coordination/awaitClaimGrant.d.ts +0 -56
  397. package/dist/transaction/coordination/awaitClaimGrant.js +0 -124
  398. package/dist/transaction/coordination/claimHeartbeatLoop.d.ts +0 -84
  399. package/dist/transaction/coordination/claimHeartbeatLoop.js +0 -108
  400. package/dist/transaction/coordination/claimMeta.d.ts +0 -49
  401. package/dist/transaction/coordination/claimMeta.js +0 -52
  402. package/dist/transaction/coordination/createClaimStream.d.ts +0 -64
  403. package/dist/transaction/coordination/createClaimStream.js +0 -475
  404. package/dist/transaction/coordination/events.d.ts +0 -74
  405. package/dist/transaction/coordination/events.js +0 -7
  406. package/dist/transaction/coordination/index.d.ts +0 -19
  407. package/dist/transaction/coordination/index.js +0 -45
  408. package/dist/transaction/coordination/locator.d.ts +0 -104
  409. package/dist/transaction/coordination/locator.js +0 -102
  410. package/dist/transaction/coordination/schema.d.ts +0 -1536
  411. package/dist/transaction/coordination/schema.js +0 -1177
  412. package/dist/transaction/coordination/targetConflict.d.ts +0 -2
  413. package/dist/transaction/coordination/targetConflict.js +0 -107
  414. package/dist/transaction/coordination/trace.d.ts +0 -78
  415. package/dist/transaction/coordination/trace.js +0 -138
  416. package/dist/transaction/durableWrites.d.ts +0 -62
  417. package/dist/transaction/durableWrites.js +0 -71
  418. package/dist/transaction/environment.d.ts +0 -105
  419. package/dist/transaction/environment.js +0 -108
  420. package/dist/transaction/errorCodes.d.ts +0 -403
  421. package/dist/transaction/errorCodes.js +0 -484
  422. package/dist/transaction/errors.d.ts +0 -428
  423. package/dist/transaction/errors.js +0 -686
  424. package/dist/transaction/footprint.d.ts +0 -111
  425. package/dist/transaction/footprint.js +0 -0
  426. package/dist/transaction/index.d.ts +0 -20
  427. package/dist/transaction/index.js +0 -20
  428. package/dist/transaction/keys/index.d.ts +0 -87
  429. package/dist/transaction/keys/index.js +0 -207
  430. package/dist/transaction/log/syncDeltaRow.d.ts +0 -158
  431. package/dist/transaction/log/syncDeltaRow.js +0 -95
  432. package/dist/transaction/logPosition.d.ts +0 -97
  433. package/dist/transaction/logPosition.js +0 -125
  434. package/dist/transaction/logger.d.ts +0 -16
  435. package/dist/transaction/logger.js +0 -7
  436. package/dist/transaction/observability.d.ts +0 -53
  437. package/dist/transaction/observability.js +0 -19
  438. package/dist/transaction/persistence.d.ts +0 -12
  439. package/dist/transaction/persistence.js +0 -11
  440. package/dist/transaction/plugin.d.ts +0 -285
  441. package/dist/transaction/plugin.js +0 -106
  442. package/dist/transaction/policy/types.d.ts +0 -217
  443. package/dist/transaction/policy/types.js +0 -126
  444. package/dist/transaction/resources/functionalUpdate.d.ts +0 -79
  445. package/dist/transaction/resources/functionalUpdate.js +0 -87
  446. package/dist/transaction/resources/httpResources.d.ts +0 -321
  447. package/dist/transaction/resources/httpResources.js +0 -7
  448. package/dist/transaction/resources/modelOperations.d.ts +0 -427
  449. package/dist/transaction/resources/modelOperations.js +0 -12
  450. package/dist/transaction/resources/mutationOptions.d.ts +0 -66
  451. package/dist/transaction/resources/mutationOptions.js +0 -9
  452. package/dist/transaction/resources/where.d.ts +0 -101
  453. package/dist/transaction/resources/where.js +0 -115
  454. package/dist/transaction/resources/writeOptionsSchema.d.ts +0 -47
  455. package/dist/transaction/resources/writeOptionsSchema.js +0 -73
  456. package/dist/transaction/schema/field.d.ts +0 -120
  457. package/dist/transaction/schema/field.js +0 -265
  458. package/dist/transaction/schema/fieldRef.d.ts +0 -38
  459. package/dist/transaction/schema/fieldRef.js +0 -11
  460. package/dist/transaction/schema/loadStrategy.d.ts +0 -45
  461. package/dist/transaction/schema/loadStrategy.js +0 -46
  462. package/dist/transaction/schema/model.d.ts +0 -379
  463. package/dist/transaction/schema/model.js +0 -123
  464. package/dist/transaction/schema/openapi.d.ts +0 -58
  465. package/dist/transaction/schema/openapi.js +0 -501
  466. package/dist/transaction/schema/relation.d.ts +0 -204
  467. package/dist/transaction/schema/relation.js +0 -104
  468. package/dist/transaction/schema/residency.d.ts +0 -29
  469. package/dist/transaction/schema/residency.js +0 -25
  470. package/dist/transaction/schema/roles.d.ts +0 -249
  471. package/dist/transaction/schema/roles.js +0 -230
  472. package/dist/transaction/schema/schema.d.ts +0 -351
  473. package/dist/transaction/schema/schema.js +0 -325
  474. package/dist/transaction/schema/tenancy.d.ts +0 -139
  475. package/dist/transaction/schema/tenancy.js +0 -190
  476. package/dist/transaction/transactionLayer.d.ts +0 -82
  477. package/dist/transaction/transactionLayer.js +0 -24
  478. package/dist/transaction/transactions/settlement/commitEnvelope.d.ts +0 -143
  479. package/dist/transaction/transactions/settlement/commitEnvelope.js +0 -161
  480. package/dist/transaction/transactions/settlement/httpCommitEnvelope.d.ts +0 -53
  481. package/dist/transaction/transactions/settlement/httpCommitEnvelope.js +0 -207
  482. package/dist/transaction/transactions/settlement/idempotencyKey.d.ts +0 -10
  483. package/dist/transaction/transactions/settlement/idempotencyKey.js +0 -9
  484. package/dist/transaction/transactions/settlement/pendingWrite.d.ts +0 -112
  485. package/dist/transaction/transactions/settlement/pendingWrite.js +0 -20
  486. package/dist/transaction/transport/commitFrames.d.ts +0 -90
  487. package/dist/transaction/transport/commitFrames.js +0 -134
  488. package/dist/transaction/transport/connectionManager.d.ts +0 -215
  489. package/dist/transaction/transport/connectionManager.js +0 -673
  490. package/dist/transaction/transport/credentialLifecycle.d.ts +0 -177
  491. package/dist/transaction/transport/credentialLifecycle.js +0 -324
  492. package/dist/transaction/transport/heartbeat.d.ts +0 -65
  493. package/dist/transaction/transport/heartbeat.js +0 -93
  494. package/dist/transaction/transport/httpClient.d.ts +0 -131
  495. package/dist/transaction/transport/httpClient.js +0 -146
  496. package/dist/transaction/transport/httpOptions.d.ts +0 -33
  497. package/dist/transaction/transport/httpOptions.js +0 -12
  498. package/dist/transaction/transport/httpTransport.d.ts +0 -8
  499. package/dist/transaction/transport/httpTransport.js +0 -1388
  500. package/dist/transaction/transport/networkProbe.d.ts +0 -84
  501. package/dist/transaction/transport/networkProbe.js +0 -207
  502. package/dist/transaction/transport/wsFrameHandlers.d.ts +0 -128
  503. package/dist/transaction/transport/wsFrameHandlers.js +0 -429
  504. package/dist/transaction/transport/wsTransport.d.ts +0 -574
  505. package/dist/transaction/transport/wsTransport.js +0 -1023
  506. package/dist/transaction/types/assertExact.d.ts +0 -17
  507. package/dist/transaction/types/assertExact.js +0 -1
  508. package/dist/transaction/types/global.d.ts +0 -107
  509. package/dist/transaction/types/global.js +0 -40
  510. package/dist/transaction/types/index.d.ts +0 -205
  511. package/dist/transaction/types/index.js +0 -56
  512. package/dist/transaction/types/modelData.d.ts +0 -10
  513. package/dist/transaction/types/modelData.js +0 -9
  514. package/dist/transaction/types/participant.d.ts +0 -20
  515. package/dist/transaction/types/participant.js +0 -10
  516. package/dist/transaction/types/streams.d.ts +0 -550
  517. package/dist/transaction/types/streams.js +0 -11
  518. package/dist/transaction/utils/asyncIterator.d.ts +0 -34
  519. package/dist/transaction/utils/asyncIterator.js +0 -135
  520. package/dist/transaction/utils/duration.d.ts +0 -50
  521. package/dist/transaction/utils/duration.js +0 -77
  522. package/dist/transaction/utils/json.d.ts +0 -57
  523. package/dist/transaction/utils/json.js +0 -276
  524. package/dist/transaction/wire/accountResponses.d.ts +0 -420
  525. package/dist/transaction/wire/accountResponses.js +0 -290
  526. package/dist/transaction/wire/auth.d.ts +0 -56
  527. package/dist/transaction/wire/auth.js +0 -63
  528. package/dist/transaction/wire/bootstrapReason.d.ts +0 -9
  529. package/dist/transaction/wire/bootstrapReason.js +0 -8
  530. package/dist/transaction/wire/claimEvent.d.ts +0 -76
  531. package/dist/transaction/wire/claimEvent.js +0 -73
  532. package/dist/transaction/wire/claims.d.ts +0 -530
  533. package/dist/transaction/wire/claims.js +0 -327
  534. package/dist/transaction/wire/commit.d.ts +0 -603
  535. package/dist/transaction/wire/commit.js +0 -321
  536. package/dist/transaction/wire/delta.d.ts +0 -250
  537. package/dist/transaction/wire/delta.js +0 -147
  538. package/dist/transaction/wire/errorEnvelope.d.ts +0 -72
  539. package/dist/transaction/wire/errorEnvelope.js +0 -123
  540. package/dist/transaction/wire/feedCursor.d.ts +0 -60
  541. package/dist/transaction/wire/feedCursor.js +0 -82
  542. package/dist/transaction/wire/feedEvent.d.ts +0 -204
  543. package/dist/transaction/wire/feedEvent.js +0 -65
  544. package/dist/transaction/wire/frames.d.ts +0 -194
  545. package/dist/transaction/wire/frames.js +0 -50
  546. package/dist/transaction/wire/inboundFrames.d.ts +0 -562
  547. package/dist/transaction/wire/inboundFrames.js +0 -116
  548. package/dist/transaction/wire/index.d.ts +0 -54
  549. package/dist/transaction/wire/index.js +0 -83
  550. package/dist/transaction/wire/listEnvelope.d.ts +0 -37
  551. package/dist/transaction/wire/listEnvelope.js +0 -42
  552. package/dist/transaction/wire/modelMutations.d.ts +0 -31
  553. package/dist/transaction/wire/modelMutations.js +0 -52
  554. package/dist/transaction/wire/modelResponses.d.ts +0 -85
  555. package/dist/transaction/wire/modelResponses.js +0 -43
  556. package/dist/transaction/wire/modelShape.d.ts +0 -78
  557. package/dist/transaction/wire/modelShape.js +0 -74
  558. package/dist/transaction/wire/protocol.d.ts +0 -38
  559. package/dist/transaction/wire/protocol.js +0 -38
  560. package/dist/transaction/wire/protocolVersion.d.ts +0 -73
  561. package/dist/transaction/wire/protocolVersion.js +0 -83
  562. package/dist/transactions/mutations/MutationQueue.d.ts +0 -661
  563. package/dist/transactions/mutations/MutationQueue.js +0 -2807
  564. package/dist/transactions/mutations/MutationStore.d.ts +0 -20
  565. package/dist/transactions/mutations/MutationStore.js +0 -53
  566. package/dist/transactions/mutations/UnconfirmedWrites.d.ts +0 -82
  567. package/dist/transactions/mutations/UnconfirmedWrites.js +0 -104
  568. package/dist/transactions/mutations/coalesceRules.d.ts +0 -58
  569. package/dist/transactions/mutations/coalesceRules.js +0 -140
  570. package/dist/transactions/mutations/commitLatency.d.ts +0 -52
  571. package/dist/transactions/mutations/commitLatency.js +0 -130
  572. package/dist/transactions/mutations/commitOutboxStore.d.ts +0 -28
  573. package/dist/transactions/mutations/commitOutboxStore.js +0 -26
  574. package/dist/transactions/mutations/commitPayload.d.ts +0 -165
  575. package/dist/transactions/mutations/commitPayload.js +0 -152
  576. package/dist/transactions/mutations/deltaConfirmation.d.ts +0 -63
  577. package/dist/transactions/mutations/deltaConfirmation.js +0 -235
  578. package/dist/transactions/mutations/durableWriteStore.d.ts +0 -14
  579. package/dist/transactions/mutations/durableWriteStore.js +0 -12
  580. package/dist/transactions/mutations/optimisticApply.d.ts +0 -49
  581. package/dist/transactions/mutations/optimisticApply.js +0 -65
  582. package/dist/transactions/mutations/replayValidation.d.ts +0 -187
  583. package/dist/transactions/mutations/replayValidation.js +0 -164
  584. package/dist/utils/mobxSetup.d.ts +0 -53
  585. package/dist/utils/mobxSetup.js +0 -330
  586. package/dist/views/QueryView.d.ts +0 -79
  587. package/dist/views/QueryView.js +0 -218
  588. package/dist/views/ViewRegistry.d.ts +0 -20
  589. package/dist/views/ViewRegistry.js +0 -55
  590. package/dist/views/incrementalView.d.ts +0 -45
  591. package/dist/views/incrementalView.js +0 -69
  592. package/dist/webhooks/events.d.ts +0 -43
  593. package/dist/webhooks/events.js +0 -42
  594. package/dist/webhooks/index.d.ts +0 -8
  595. package/dist/webhooks/index.js +0 -8
  596. package/dist/wire/index.d.ts +0 -1
  597. package/dist/wire/index.js +0 -8
@@ -1,1388 +0,0 @@
1
- /**
2
- * Private HTTP protocol client behind `Ablo({ schema, transport: 'http' })`.
3
- * It carries no object pool, local database, or WebSocket and maps Model,
4
- * Claim, and Commit protocol shapes directly to server routes. The typed
5
- * facade in `httpClient.ts` is the application boundary; this module owns
6
- * transport envelopes, watermarks, replay, and route details.
7
- */
8
- import { AbloClaimedError, AbloAuthenticationError, AbloConnectionError, AbloIdempotencyError, AbloValidationError, AbloNotFoundError, claimedError, translateHttpError, } from '../errors.js';
9
- import { v5 as uuidv5 } from 'uuid';
10
- import { z } from 'zod';
11
- import { reconcileFunctionalUpdate, } from '../resources/functionalUpdate.js';
12
- import { assertBrowserSafety, readProcessEnv, resolveApiKey, resolveApiKeyValue, resolveAuthToken, resolveBaseURL, resolveBootstrapBaseUrl, rejectRemovedDatabaseUrlOption, warnIfCliKeyMismatch, } from '../auth/apiKey.js';
13
- import { PROTOCOL_VERSION, PROTOCOL_VERSION_HEADER } from '../wire/protocolVersion.js';
14
- import { commitReceiptSchema } from '../wire/commit.js';
15
- import { claimAcquireResponseSchema, claimHeartbeatBatchReplySchema, claimHeartbeatReplySchema, claimListResponseSchema, claimStateSchema, } from '../wire/claims.js';
16
- import { modelListResponseSchema, modelReadResponseSchema, } from '../wire/modelResponses.js';
17
- import { toMs } from '../utils/duration.js';
18
- import { heartbeatCadenceMs, resolveHeartbeatOptions, resolveHeartbeatPlan, startClaimHeartbeatLoop, } from '../coordination/claimHeartbeatLoop.js';
19
- import { mintSession } from '../auth/sessionMint.js';
20
- import { parseIdentityResolveResponse } from '../auth/schemas.js';
21
- /**
22
- * Interpret a heartbeat reply for a lease this handle HOLDS: anything other
23
- * than `held` means the lease is no longer ours (a holder cannot be `queued`;
24
- * `lost` rides a 409 that the wire error mapping already surfaces as
25
- * AbloClaimedError before reaching here). The thrown loss is the definitive
26
- * signal that stops the auto-heartbeat loop.
27
- */
28
- function heldHeartbeatReply(reply, label) {
29
- if (reply.status === 'held' && typeof reply.expiresAt === 'number') {
30
- return {
31
- expiresAt: reply.expiresAt,
32
- ...(reply.queueDepth !== undefined ? { queueDepth: reply.queueDepth } : {}),
33
- };
34
- }
35
- throw new AbloClaimedError(`The lease behind ${label} is no longer held — it expired or was granted onward. Re-acquire the claim and retry; a write attempted under the old lease is rejected by its \`readAt\` guard.`, { code: 'claim_lost' });
36
- }
37
- import { claimDescription, partName } from '../coordination/schema.js';
38
- import { subTarget, streamTarget, batchFence, fenceTokenFor, } from '../coordination/locator.js';
39
- import { declaredMeta, wireMeta } from '../coordination/claimMeta.js';
40
- import { assertWriteOptions } from '../resources/writeOptionsSchema.js';
41
- import { createDurableHttpCommitEnvelope, canonicalHttpCommitBody, durableHttpCommitEnvelopeSchema, httpCommitEnvelopeRecordId, isHttpCommitReplayExpired, } from '../transactions/settlement/httpCommitEnvelope.js';
42
- import { resolveDurableWrites } from '../durableWrites.js';
43
- /** @internal Default per-request deadline for the private HTTP transport. */
44
- export const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
45
- const HTTP_CONFIRMATION_POLL_INTERVAL_MS = 250;
46
- /**
47
- * The server's acquire window, mirrored here as the client-side default for a
48
- * claim that names no `ttl` — it sets the auto-heartbeat cadence.
49
- */
50
- const DEFAULT_CLAIM_TTL_MS = 60_000;
51
- function parseSuccessfulCommitResponse(value, idempotencyKey) {
52
- const parsed = commitReceiptSchema.safeParse(value);
53
- if (!parsed.success || parsed.data.clientTxId !== idempotencyKey) {
54
- throw new AbloConnectionError('The commit endpoint returned an invalid success receipt; its outcome remains pending and is safe to retry.', {
55
- code: 'commit_no_result',
56
- cause: parsed.success
57
- ? new Error('Commit receipt clientTxId did not match its idempotency key')
58
- : parsed.error,
59
- });
60
- }
61
- return parsed.data;
62
- }
63
- /** Decode the HTTP claim DTO into the one public Claim shape. */
64
- function claimFromModelClaim(claim) {
65
- // The handle a caller reads back is a public claim, so its `meta` is the
66
- // declared shape; the rest of the sub-entity locator crosses whole rather
67
- // than member by member, which is how `fields` used to die on this hop.
68
- const { meta, ...details } = subTarget(claim.target);
69
- return {
70
- object: 'claim',
71
- id: claim.id,
72
- ...(claim.status ? { status: claim.status } : {}),
73
- // The server always stamps a description; default only for total safety.
74
- description: claim.description ?? 'editing',
75
- heldBy: claim.actor,
76
- participantKind: claim.participantKind,
77
- expiresAt: claim.expiresAt,
78
- ...(claim.position !== undefined ? { position: claim.position } : {}),
79
- target: {
80
- ...streamTarget(claim.target),
81
- ...details,
82
- ...(meta !== undefined ? { meta: declaredMeta(meta) } : {}),
83
- },
84
- };
85
- }
86
- /** @internal Constructed only by the typed HTTP facade. */
87
- export function createHttpTransport(options) {
88
- const env = readProcessEnv();
89
- const authInput = { options, env };
90
- const configuredApiKey = resolveApiKey(authInput);
91
- const configuredAuthToken = resolveAuthToken(authInput);
92
- void warnIfCliKeyMismatch(authInput);
93
- rejectRemovedDatabaseUrlOption(options);
94
- assertBrowserSafety({
95
- apiKey: configuredApiKey,
96
- dangerouslyAllowBrowser: options.dangerouslyAllowBrowser,
97
- });
98
- // Observability hook for the stateless HTTP transport. The WebSocket transport
99
- // emits claim and conflict events; the HTTP path (server-side agents,
100
- // `transport: 'http'`) emitted nothing, so a `ClaimLog` handed to a headless
101
- // agent evaluation stayed empty. This mirrors the two WebSocket events here:
102
- // claim acquired and coordination-conflict rejection. A no-op when no provider
103
- // is configured.
104
- const observability = options.observability;
105
- // Shared by the two HTTP write doors (`commits.create` + per-model
106
- // `mutateModel`): a rejected write whose code is a coordination conflict is
107
- // the collision ClaimLog exists to surface. Prefer the server's `conflicts`
108
- // detail (carried on the typed error / envelope); fall back to the rows the
109
- // caller tried to write so the collision always names a target. Inert without
110
- // a provider or for non-conflict errors. Never throws (capture is best-effort).
111
- const recordCoordinationConflict = (error, clientTxId, fallbackRows) => {
112
- if (!observability)
113
- return;
114
- const errorRecord = typeof error === 'object' && error !== null
115
- ? error
116
- : undefined;
117
- const code = errorRecord?.code;
118
- const isConflict = code === 'stale_context' ||
119
- code === 'claim_conflict' ||
120
- code === 'entity_claimed' ||
121
- (typeof code === 'string' && code.startsWith('policy:'));
122
- if (!isConflict)
123
- return;
124
- const rawConflicts = errorRecord?.conflicts;
125
- const rows = Array.isArray(rawConflicts) && rawConflicts.length > 0
126
- ? rawConflicts.map((r) => ({
127
- model: typeof r.model === 'string' ? r.model : 'unknown',
128
- id: typeof r.id === 'string' ? r.id : 'unknown',
129
- fields: [],
130
- }))
131
- : fallbackRows.map((r) => ({ model: r.model, id: r.id, fields: [] }));
132
- observability.captureConflict({ clientTxId, rows });
133
- };
134
- const fetchImpl = options.fetch ?? globalThis.fetch;
135
- if (typeof fetchImpl !== 'function') {
136
- throw new AbloConnectionError('Ablo API client requires a fetch implementation. Pass `fetch` in Ablo({ ... }) for this runtime.', { code: 'fetch_unavailable' });
137
- }
138
- const url = resolveBaseURL(authInput);
139
- const apiBaseUrl = resolveBootstrapBaseUrl({
140
- url,
141
- bootstrapBaseUrl: options.bootstrapBaseUrl,
142
- }).replace(/\/+$/, '');
143
- const durableWrites = resolveDurableWrites(options);
144
- // Internal replay code retains transactional-outbox terminology. The public
145
- // constructor exposes the behavior as `durableWrites`.
146
- const commitOutbox = durableWrites.store;
147
- const durableWriteNamespace = durableWrites.namespace ?? 'http';
148
- const legacyCommitOutboxScope = options
149
- .commitOutboxScope;
150
- const httpOutboxPlaneNamespace = canonicalHttpCommitBody({
151
- apiBaseUrl,
152
- defaultQuery: Object.entries(options.defaultQuery ?? {}).sort(([a], [b]) => a.localeCompare(b)),
153
- });
154
- let httpOutboxScopeNamespace = null;
155
- let readyPromise = null;
156
- let httpCommitLane = Promise.resolve();
157
- function runInHttpCommitLane(work) {
158
- const result = httpCommitLane.then(work);
159
- httpCommitLane = result.then(() => undefined, () => undefined);
160
- return result;
161
- }
162
- async function resolveHttpOutboxScope() {
163
- if (!commitOutbox)
164
- return null;
165
- if (httpOutboxScopeNamespace)
166
- return httpOutboxScopeNamespace;
167
- let scope = legacyCommitOutboxScope
168
- ? {
169
- ...legacyCommitOutboxScope,
170
- namespace: durableWriteNamespace,
171
- }
172
- : undefined;
173
- if (!scope) {
174
- const rawIdentity = await requestRaw('/auth/identity', { method: 'GET' }, true);
175
- const identity = parseIdentityResolveResponse(rawIdentity);
176
- scope = {
177
- organizationId: identity.accountScope,
178
- participantId: identity.participantId,
179
- namespace: durableWriteNamespace,
180
- };
181
- }
182
- httpOutboxScopeNamespace = canonicalHttpCommitBody({
183
- ...scope,
184
- plane: httpOutboxPlaneNamespace,
185
- });
186
- return httpOutboxScopeNamespace;
187
- }
188
- async function ready() {
189
- if (readyPromise)
190
- return readyPromise;
191
- readyPromise = (async () => {
192
- await resolveHttpOutboxScope();
193
- await replayHttpCommitOutbox();
194
- })();
195
- try {
196
- await readyPromise;
197
- }
198
- catch (error) {
199
- readyPromise = null;
200
- throw error;
201
- }
202
- }
203
- async function authHeaders(sealedProtocolVersion) {
204
- const apiKey = await resolveApiKeyValue(configuredApiKey);
205
- const token = apiKey ?? configuredAuthToken;
206
- if (!token) {
207
- throw new AbloAuthenticationError('The HTTP client requires an API key. Pass `apiKey` or set ABLO_API_KEY.', { code: 'api_key_required' });
208
- }
209
- const headers = {
210
- 'Content-Type': 'application/json',
211
- Authorization: `Bearer ${token}`,
212
- // Protocol handshake for the HTTP transport (wire/protocolVersion.ts):
213
- // the server answers an out-of-range version with a typed 426.
214
- [PROTOCOL_VERSION_HEADER]: String(PROTOCOL_VERSION),
215
- };
216
- for (const [key, value] of Object.entries(options.defaultHeaders ?? {})) {
217
- if (value == null) {
218
- delete headers[key];
219
- }
220
- else {
221
- headers[key] = value;
222
- }
223
- }
224
- // A durable write owns its wire version. Force the sealed value after
225
- // caller defaults so a restarted (or rolled-back) SDK cannot rewrite the
226
- // protocol identity of a request that may already have reached the server.
227
- if (sealedProtocolVersion !== undefined) {
228
- headers[PROTOCOL_VERSION_HEADER] = String(sealedProtocolVersion);
229
- }
230
- return headers;
231
- }
232
- function endpoint(path) {
233
- const target = new URL(`${apiBaseUrl}${path.startsWith('/') ? path : `/${path}`}`);
234
- for (const [key, value] of Object.entries(options.defaultQuery ?? {})) {
235
- if (value !== undefined)
236
- target.searchParams.set(key, value);
237
- }
238
- return target.toString();
239
- }
240
- const requestTimeoutMs = options.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
241
- /**
242
- * Issues one request and returns its decoded body without a contract.
243
- *
244
- * Use this only where the response has no shape worth checking — a release
245
- * that answers `{}` — or where the caller runs a richer check of its own, as
246
- * the commit paths do with their receipt schema. Everywhere else, go through
247
- * {@link requestJson}, which will not let a response past unvalidated.
248
- */
249
- async function requestRaw(path, init, skipReady = false) {
250
- if (!skipReady)
251
- await ready();
252
- const { idempotencyKey, sealedProtocolVersion, ...requestInit } = init;
253
- const headers = await authHeaders(sealedProtocolVersion);
254
- if (idempotencyKey)
255
- headers['Idempotency-Key'] = idempotencyKey;
256
- // Deadline: abort the request after `timeoutMs` so a black-holed server
257
- // can't hang the caller forever (fetch has NO default timeout in browsers,
258
- // and only undici's generous defaults in Node). A caller-supplied signal
259
- // is combined with the deadline via a shared controller — the portable
260
- // equivalent of `AbortSignal.any([caller, AbortSignal.timeout(t)])`,
261
- // which older runtimes (and the jsdom test env) don't implement. The
262
- // same pattern already guards `query/client.ts` and `BootstrapFetcher`.
263
- const callerSignal = requestInit.signal ?? undefined;
264
- const controller = new AbortController();
265
- const onCallerAbort = () => {
266
- controller.abort(callerSignal?.reason);
267
- };
268
- if (callerSignal) {
269
- if (callerSignal.aborted)
270
- onCallerAbort();
271
- else
272
- callerSignal.addEventListener('abort', onCallerAbort, { once: true });
273
- }
274
- let timedOut = false;
275
- const deadline = requestTimeoutMs > 0
276
- ? setTimeout(() => {
277
- timedOut = true;
278
- controller.abort();
279
- }, requestTimeoutMs)
280
- : null;
281
- let res;
282
- let bodyText;
283
- try {
284
- res = await fetchImpl(endpoint(path), {
285
- ...requestInit,
286
- signal: controller.signal,
287
- headers: {
288
- ...headers,
289
- ...requestInit.headers,
290
- },
291
- });
292
- // Keep the deadline armed while the body streams — a server that sends
293
- // headers then stalls the body is the same hang with better manners.
294
- bodyText = await res.text();
295
- }
296
- catch (error) {
297
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- assigned asynchronously by the deadline callback
298
- if (timedOut) {
299
- // Retryable by contract: `wait_for_timeout` is a registered transient
300
- // transport code, so `isRetryableCode` steers callers to retry.
301
- throw new AbloConnectionError(`The Ablo API did not respond within ${requestTimeoutMs}ms ` +
302
- `(${requestInit.method ?? 'GET'} ${path}). The request was aborted; ` +
303
- 'it is safe to retry.', { code: 'wait_for_timeout', cause: error });
304
- }
305
- throw error;
306
- }
307
- finally {
308
- if (deadline)
309
- clearTimeout(deadline);
310
- callerSignal?.removeEventListener('abort', onCallerAbort);
311
- }
312
- const body = parseBody(bodyText);
313
- if (!res.ok) {
314
- throw translateHttpError(res.status, body ?? `Ablo API request failed: ${res.status} ${res.statusText}`, res.headers.get('x-request-id') ?? undefined);
315
- }
316
- return body;
317
- }
318
- /**
319
- * Issues one request and validates its body against the route's schema.
320
- *
321
- * The schema is the route's response contract, declared once in `wire/` and
322
- * shared with the server that produces it. A body that does not match is a
323
- * version disagreement between the two, so it is refused whole rather than
324
- * read field by field and half-trusted.
325
- */
326
- async function requestJson(path, init, responseSchema, skipReady = false) {
327
- const body = await requestRaw(path, init, skipReady);
328
- const parsed = responseSchema.safeParse(body);
329
- if (!parsed.success) {
330
- throw new AbloConnectionError(`The Ablo API returned a response for ${init.method ?? 'GET'} ${path} that this client could not read; nothing was applied.`, { code: 'malformed_response', cause: parsed.error });
331
- }
332
- return parsed.data;
333
- }
334
- function isDefinitiveHttpRejection(error) {
335
- if (typeof error !== 'object' || error === null)
336
- return false;
337
- const candidate = error;
338
- const status = typeof candidate.httpStatus === 'number'
339
- ? candidate.httpStatus
340
- : typeof candidate.status === 'number'
341
- ? candidate.status
342
- : undefined;
343
- return (status !== undefined &&
344
- status >= 400 &&
345
- status < 500 &&
346
- status !== 408 &&
347
- status !== 425 &&
348
- status !== 429);
349
- }
350
- async function settleHttpEnvelope(recordId) {
351
- if (!commitOutbox)
352
- return;
353
- try {
354
- await commitOutbox.remove(recordId);
355
- }
356
- catch (cause) {
357
- // Do not report the remote outcome until local settlement is durable.
358
- // The retained record can still be replayed inside the safe window.
359
- throw new AbloConnectionError('The server settled the commit, but its local outbox record could not be cleared.', { code: 'db_not_opened', cause });
360
- }
361
- }
362
- /**
363
- * Persist the first queued source receipt before exposing acceptance to the
364
- * caller. This is a monotonic upgrade of the same sealed request: connected
365
- * source keys are permanent, so the envelope may safely remain replayable
366
- * after the hosted 24-hour idempotency window while it awaits its WAL echo.
367
- */
368
- async function persistHttpAcceptance(envelope, response) {
369
- if (!commitOutbox || response.status !== 'queued')
370
- return envelope;
371
- const correlationId = response.correlationId;
372
- if (!correlationId) {
373
- throw new AbloConnectionError('The source accepted the commit without durable correlation evidence.', { code: 'commit_no_result' });
374
- }
375
- if (envelope.correlationId !== undefined && envelope.correlationId !== correlationId) {
376
- throw new AbloIdempotencyError('The same HTTP commit replay returned a different source correlation.', { code: 'idempotency_conflict' });
377
- }
378
- if (envelope.acceptedAt !== undefined)
379
- return envelope;
380
- const accepted = durableHttpCommitEnvelopeSchema.parse({
381
- ...envelope,
382
- acceptedAt: Date.now(),
383
- correlationId,
384
- });
385
- try {
386
- await commitOutbox.seal(accepted, []);
387
- }
388
- catch (cause) {
389
- throw new AbloConnectionError('The source accepted the commit, but that acceptance could not be persisted locally.', { code: 'db_not_opened', cause });
390
- }
391
- return accepted;
392
- }
393
- function replicationLagTimeout(request, response) {
394
- return new AbloConnectionError(`The source accepted commit ${request.idempotencyKey}, but its replication echo did not arrive within ${requestTimeoutMs}ms.`, {
395
- code: 'replication_lag_timeout',
396
- httpStatus: 504,
397
- details: {
398
- clientTxId: request.idempotencyKey,
399
- ...(response.correlationId ? { correlationId: response.correlationId } : {}),
400
- timeoutMs: requestTimeoutMs,
401
- accepted: true,
402
- },
403
- });
404
- }
405
- /**
406
- * Replays one byte-identical, idempotent HTTP commit until mutation-log
407
- * replay reports the source echo as confirmed. `queued` is acceptance only:
408
- * this loop never clears the durable envelope and never converts it into a
409
- * successful `wait: 'confirmed'` result.
410
- */
411
- async function pollHttpCommitConfirmation(request, initial) {
412
- let current = initial;
413
- const correlationId = initial.correlationId;
414
- const deadlineAt = requestTimeoutMs > 0 ? Date.now() + requestTimeoutMs : null;
415
- while (current.status === 'queued') {
416
- const remaining = deadlineAt === null ? null : deadlineAt - Date.now();
417
- if (remaining !== null && remaining <= 0) {
418
- throw replicationLagTimeout(request, current);
419
- }
420
- const confirmationController = new AbortController();
421
- const confirmationDeadline = remaining !== null
422
- ? setTimeout(() => {
423
- confirmationController.abort();
424
- }, remaining)
425
- : null;
426
- try {
427
- const raw = await requestRaw(request.path, {
428
- method: request.method,
429
- idempotencyKey: request.idempotencyKey,
430
- ...(request.sealedProtocolVersion !== undefined
431
- ? { sealedProtocolVersion: request.sealedProtocolVersion }
432
- : {}),
433
- body: request.body,
434
- signal: confirmationController.signal,
435
- }, true);
436
- const next = parseSuccessfulCommitResponse(raw, request.idempotencyKey);
437
- if (next.correlationId !== correlationId) {
438
- throw new AbloIdempotencyError('The same HTTP commit replay returned different source correlation evidence.', { code: 'idempotency_conflict' });
439
- }
440
- current = next;
441
- }
442
- catch (error) {
443
- if (confirmationController.signal.aborted ||
444
- (deadlineAt !== null && Date.now() >= deadlineAt)) {
445
- throw replicationLagTimeout(request, current);
446
- }
447
- throw error;
448
- }
449
- finally {
450
- if (confirmationDeadline)
451
- clearTimeout(confirmationDeadline);
452
- }
453
- if (current.status === 'confirmed')
454
- return current;
455
- const delayMs = deadlineAt === null
456
- ? HTTP_CONFIRMATION_POLL_INTERVAL_MS
457
- : Math.min(HTTP_CONFIRMATION_POLL_INTERVAL_MS, Math.max(0, deadlineAt - Date.now()));
458
- if (delayMs > 0) {
459
- await new Promise((resolve) => {
460
- setTimeout(resolve, delayMs);
461
- });
462
- }
463
- }
464
- return current;
465
- }
466
- async function replayHttpCommitOutbox() {
467
- const replayed = new Map();
468
- if (!commitOutbox)
469
- return replayed;
470
- const scopeNamespace = await resolveHttpOutboxScope();
471
- if (!scopeNamespace)
472
- return replayed;
473
- const rows = await commitOutbox.list();
474
- const envelopes = [];
475
- for (const row of rows) {
476
- if (typeof row !== 'object' ||
477
- row === null ||
478
- row.type !== 'http_commit_envelope')
479
- continue;
480
- const parsed = durableHttpCommitEnvelopeSchema.safeParse(row);
481
- if (!parsed.success) {
482
- throw new AbloValidationError('A saved HTTP write is unreadable; replay stopped before any newer write was sent.', { code: 'write_options_invalid', cause: parsed.error });
483
- }
484
- if (parsed.data.scopeNamespace !== scopeNamespace)
485
- continue;
486
- if (isHttpCommitReplayExpired(parsed.data)) {
487
- throw new AbloIdempotencyError('A saved HTTP write is older than the server idempotency window and cannot be replayed safely.', { code: 'idempotency_conflict' });
488
- }
489
- envelopes.push(parsed.data);
490
- }
491
- envelopes.sort((a, b) => (a.sequence ?? a.sealedAt * 1_000) - (b.sequence ?? b.sealedAt * 1_000) ||
492
- a.id.localeCompare(b.id));
493
- for (const envelope of envelopes) {
494
- try {
495
- const raw = await requestRaw(envelope.request.path, {
496
- method: envelope.request.method,
497
- idempotencyKey: envelope.idempotencyKey,
498
- sealedProtocolVersion: envelope.protocolVersion,
499
- body: envelope.request.body,
500
- }, true);
501
- const response = parseSuccessfulCommitResponse(raw, envelope.idempotencyKey);
502
- if (envelope.correlationId !== undefined &&
503
- response.correlationId !== envelope.correlationId) {
504
- throw new AbloIdempotencyError('The saved HTTP commit replay returned different source correlation evidence.', { code: 'idempotency_conflict' });
505
- }
506
- const replayEnvelope = await persistHttpAcceptance(envelope, response);
507
- // A queued source receipt is only acceptance. Keep the exact request
508
- // durable so startup/retry can ask mutation-log replay whether its WAL
509
- // echo has materialized; only confirmed is a definitive success.
510
- if (response.status === 'confirmed') {
511
- await settleHttpEnvelope(envelope.id);
512
- }
513
- replayed.set(envelope.idempotencyKey, {
514
- envelope: replayEnvelope,
515
- response,
516
- });
517
- }
518
- catch (error) {
519
- if (isDefinitiveHttpRejection(error)) {
520
- await settleHttpEnvelope(envelope.id);
521
- }
522
- throw error;
523
- }
524
- }
525
- return replayed;
526
- }
527
- /**
528
- * A flush is stronger than startup replay: it promises that every retained
529
- * envelope has reached a definitive outcome, not merely that the server
530
- * accepted it for forwarding. Poll queued receipts through mutation-log
531
- * replay and leave their envelopes intact if the confirmation deadline
532
- * expires.
533
- */
534
- async function confirmReplayedHttpCommits(replayed) {
535
- for (const { envelope, response } of replayed.values()) {
536
- if (response.status !== 'queued')
537
- continue;
538
- try {
539
- const confirmed = await pollHttpCommitConfirmation({
540
- idempotencyKey: envelope.idempotencyKey,
541
- method: envelope.request.method,
542
- path: envelope.request.path,
543
- body: envelope.request.body,
544
- sealedProtocolVersion: envelope.protocolVersion,
545
- }, response);
546
- if (confirmed.status === 'confirmed') {
547
- await settleHttpEnvelope(envelope.id);
548
- }
549
- }
550
- catch (error) {
551
- if (isDefinitiveHttpRejection(error)) {
552
- await settleHttpEnvelope(envelope.id);
553
- }
554
- throw error;
555
- }
556
- }
557
- }
558
- let lastHttpCommitSequence = 0;
559
- function nextHttpCommitSequence() {
560
- const wallSequence = Date.now() * 1_000;
561
- lastHttpCommitSequence = Math.max(wallSequence, lastHttpCommitSequence + 1);
562
- return lastHttpCommitSequence;
563
- }
564
- async function sealHttpCommit(input) {
565
- if (!commitOutbox)
566
- return null;
567
- const scopeNamespace = await resolveHttpOutboxScope();
568
- if (!scopeNamespace) {
569
- throw new AbloValidationError('HTTP durable-write scope was not resolved', {
570
- code: 'write_options_invalid',
571
- });
572
- }
573
- const recordId = httpCommitEnvelopeRecordId(input.idempotencyKey, scopeNamespace);
574
- const legacyRecordId = httpCommitEnvelopeRecordId(input.idempotencyKey);
575
- const existingRows = await commitOutbox.list();
576
- const existingRaw = existingRows.find((row) => typeof row === 'object' &&
577
- row !== null &&
578
- (row.id === recordId ||
579
- row.id === legacyRecordId));
580
- const serializedBody = canonicalHttpCommitBody(input.body);
581
- if (existingRaw !== undefined) {
582
- const existing = durableHttpCommitEnvelopeSchema.parse(existingRaw);
583
- if (isHttpCommitReplayExpired(existing)) {
584
- throw new AbloIdempotencyError('This saved HTTP write is older than the server idempotency window and cannot be retried safely.', { code: 'idempotency_conflict' });
585
- }
586
- if (existing.scopeNamespace !== scopeNamespace ||
587
- existing.request.method !== input.method ||
588
- existing.request.path !== input.path ||
589
- existing.request.body !== serializedBody) {
590
- throw new AbloIdempotencyError('Idempotency key reused with a different HTTP commit request', { code: 'idempotency_conflict' });
591
- }
592
- return existing;
593
- }
594
- const envelope = createDurableHttpCommitEnvelope({
595
- idempotencyKey: input.idempotencyKey,
596
- request: { method: input.method, path: input.path, body: input.body },
597
- scopeNamespace,
598
- sequence: nextHttpCommitSequence(),
599
- });
600
- await commitOutbox.seal(envelope, []);
601
- return envelope;
602
- }
603
- async function dispatchHttpCommit(input, beforeSettlement) {
604
- return runInHttpCommitLane(async () => {
605
- await ready();
606
- // `ready()` covers startup. Re-draining here makes every later write wait
607
- // behind an ambiguous predecessor from this same process.
608
- const replayed = await replayHttpCommitOutbox();
609
- const prior = replayed.get(input.idempotencyKey);
610
- if (prior) {
611
- const serializedBody = canonicalHttpCommitBody(input.body);
612
- if (prior.envelope.request.method !== input.method ||
613
- prior.envelope.request.path !== input.path ||
614
- prior.envelope.request.body !== serializedBody) {
615
- throw new AbloIdempotencyError('Idempotency key reused with a different HTTP commit request', { code: 'idempotency_conflict' });
616
- }
617
- let priorResponse = prior.response;
618
- if (priorResponse.status === 'queued' && input.wait === 'confirmed') {
619
- try {
620
- priorResponse = await pollHttpCommitConfirmation({
621
- idempotencyKey: prior.envelope.idempotencyKey,
622
- method: prior.envelope.request.method,
623
- path: prior.envelope.request.path,
624
- body: prior.envelope.request.body,
625
- sealedProtocolVersion: prior.envelope.protocolVersion,
626
- }, priorResponse);
627
- }
628
- catch (error) {
629
- if (isDefinitiveHttpRejection(error)) {
630
- await settleHttpEnvelope(prior.envelope.id);
631
- }
632
- throw error;
633
- }
634
- }
635
- if (priorResponse.status === 'confirmed') {
636
- await beforeSettlement?.(priorResponse);
637
- await settleHttpEnvelope(prior.envelope.id);
638
- }
639
- return priorResponse;
640
- }
641
- const durableEnvelope = await sealHttpCommit(input);
642
- const requestBody = durableEnvelope?.request.body ?? canonicalHttpCommitBody(input.body);
643
- const exactRequest = {
644
- idempotencyKey: input.idempotencyKey,
645
- method: input.method,
646
- path: input.path,
647
- body: requestBody,
648
- ...(durableEnvelope ? { sealedProtocolVersion: durableEnvelope.protocolVersion } : {}),
649
- };
650
- let response;
651
- try {
652
- const raw = await requestRaw(exactRequest.path, {
653
- method: exactRequest.method,
654
- idempotencyKey: exactRequest.idempotencyKey,
655
- ...(exactRequest.sealedProtocolVersion !== undefined
656
- ? { sealedProtocolVersion: exactRequest.sealedProtocolVersion }
657
- : {}),
658
- body: exactRequest.body,
659
- }, true);
660
- response = parseSuccessfulCommitResponse(raw, input.idempotencyKey);
661
- if (durableEnvelope && response.status === 'queued') {
662
- await persistHttpAcceptance(durableEnvelope, response);
663
- }
664
- if (response.status === 'queued' && input.wait === 'confirmed') {
665
- response = await pollHttpCommitConfirmation(exactRequest, response);
666
- }
667
- }
668
- catch (error) {
669
- if (durableEnvelope && isDefinitiveHttpRejection(error)) {
670
- await settleHttpEnvelope(durableEnvelope.id);
671
- }
672
- throw error;
673
- }
674
- // A model-create readback can participate in settlement: if it fails,
675
- // retain the exact write so a same-key retry recovers the generated id.
676
- // A queued source receipt cannot be read back from the log yet and stays
677
- // durable until a later confirmed replay.
678
- if (response.status === 'confirmed') {
679
- await beforeSettlement?.(response);
680
- if (durableEnvelope)
681
- await settleHttpEnvelope(durableEnvelope.id);
682
- }
683
- return response;
684
- });
685
- }
686
- function createClientTxId(idempotencyKey) {
687
- if (idempotencyKey && idempotencyKey.length > 0)
688
- return idempotencyKey;
689
- return typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'
690
- ? crypto.randomUUID()
691
- : `tx_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
692
- }
693
- function createModelId(modelName, idempotencyKey) {
694
- if (idempotencyKey) {
695
- return uuidv5(`${modelName}:${idempotencyKey}`, 'aa4ba6d4-bf0b-5b38-9c45-116f79a6e548');
696
- }
697
- return typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'
698
- ? crypto.randomUUID()
699
- : `id_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
700
- }
701
- function normalizeCommitOperation(op, defaults, fence) {
702
- return {
703
- action: op.action,
704
- model: op.model,
705
- id: op.id ?? null,
706
- data: op.data ?? null,
707
- transactionId: op.transactionId ?? null,
708
- readAt: op.readAt ?? defaults.readAt ?? null,
709
- onStale: op.onStale ?? defaults.onStale ?? null,
710
- fenceToken: op.fenceToken ?? fenceTokenFor(fence, op.model, op.id ?? null),
711
- };
712
- }
713
- function normalizeCommitOperations(commitOptions, fence) {
714
- if (commitOptions.operations.length === 0) {
715
- throw new AbloValidationError('Commit requires a non-empty `operations` array.', {
716
- code: 'commit_operation_required',
717
- });
718
- }
719
- return commitOptions.operations.map((op) => normalizeCommitOperation(op, commitOptions, fence));
720
- }
721
- async function listClaimState(target) {
722
- const params = new URLSearchParams();
723
- if (target?.model)
724
- params.set('model', target.model);
725
- if (target?.id)
726
- params.set('id', target.id);
727
- if (target?.field)
728
- params.set('field', target.field);
729
- const suffix = params.toString();
730
- const body = await requestJson(`/v1/claims${suffix ? `?${suffix}` : ''}`, { method: 'GET' }, claimListResponseSchema);
731
- // One list, one resource in two states: holders and waiters are told
732
- // apart by each entry's `status`, not by bespoke envelope members.
733
- return {
734
- active: body.data.filter((row) => row.status !== 'queued'),
735
- queue: body.data.filter((row) => row.status === 'queued'),
736
- };
737
- }
738
- // The claim-ticket surface: everything a caller does holding only a
739
- // `claimId` — which is all a queued acquire leaves in its hand. Each method
740
- // is a thin cast of its route; the reply schemas are the wire's own, so the
741
- // surface cannot describe a response the server does not send.
742
- const claims = {
743
- retrieve({ claimId }) {
744
- return requestJson(`/v1/claims/${encodeURIComponent(claimId)}`, { method: 'GET' }, claimStateSchema);
745
- },
746
- heartbeat({ claimId, ttl }) {
747
- return requestJson(`/v1/claims/${encodeURIComponent(claimId)}/heartbeat`, {
748
- method: 'POST',
749
- body: JSON.stringify(ttl !== undefined ? { ttl } : {}),
750
- }, claimHeartbeatReplySchema);
751
- },
752
- async heartbeatAll(options) {
753
- const reply = await requestJson('/v1/claims/heartbeat', {
754
- method: 'POST',
755
- body: JSON.stringify(options?.ttl !== undefined ? { ttl: options.ttl } : {}),
756
- }, claimHeartbeatBatchReplySchema);
757
- return reply.results;
758
- },
759
- async release({ claimId }) {
760
- await requestRaw(`/v1/claims/${encodeURIComponent(claimId)}`, {
761
- method: 'DELETE',
762
- });
763
- },
764
- };
765
- // How the stateless client waits its turn. The queued slot is real server
766
- // state, so one heartbeat per tick does both jobs: it refreshes this
767
- // waiter's slot and reports the line's answer — `queued` (still waiting) or
768
- // `held` (granted). The first check comes quickly because most holds are a
769
- // short claim→write→release; after that the cadence relaxes, with jitter so
770
- // a fleet of waiters doesn't beat in step.
771
- const GRANT_POLL_FIRST_MS = 250;
772
- const GRANT_POLL_INTERVAL_MS = 1_000;
773
- // An abort cuts the sleep short so the wait ends within a tick of the
774
- // signal, not at the next scheduled beat.
775
- const sleep = (ms, signal) => new Promise((resolve) => {
776
- const done = () => {
777
- clearTimeout(timer);
778
- signal?.removeEventListener('abort', done);
779
- resolve();
780
- };
781
- const timer = setTimeout(done, ms);
782
- signal?.addEventListener('abort', done, { once: true });
783
- });
784
- async function awaitGrantOverHttp(targetLabel, queued, options) {
785
- // The queued reply is a claim resource in its waiting state, so the
786
- // handle is its `id` — same rule as the 201 and the poll.
787
- const claimId = queued.id;
788
- const { signal } = options;
789
- // Leave the line before rejecting: an abandoned slot would otherwise sit
790
- // in the queue until its TTL lapses, stalling every waiter behind it.
791
- const rejectAndLeave = async (error) => {
792
- await claims.release({ claimId }).catch(() => { });
793
- throw error;
794
- };
795
- if (options.maxQueueDepth !== undefined && queued.position >= options.maxQueueDepth) {
796
- return rejectAndLeave(new AbloClaimedError(`Claim queue for ${targetLabel} is ${queued.position} deep (max ${options.maxQueueDepth}).`, { code: 'queue_too_deep' }));
797
- }
798
- const deadline = options.waitTimeoutMs !== undefined ? Date.now() + options.waitTimeoutMs : undefined;
799
- let delay = GRANT_POLL_FIRST_MS;
800
- for (;;) {
801
- if (signal?.aborted) {
802
- return rejectAndLeave(new AbloClaimedError(`The wait for the claim on ${targetLabel} was aborted before the grant arrived.`, { code: 'claim_wait_aborted' }));
803
- }
804
- if (deadline !== undefined && Date.now() >= deadline) {
805
- return rejectAndLeave(new AbloClaimedError(`Timed out after ${options.waitTimeoutMs}ms waiting for the queue grant on ${targetLabel}.`, { code: 'grant_timeout' }));
806
- }
807
- await sleep(deadline !== undefined ? Math.min(delay, Math.max(0, deadline - Date.now())) : delay, signal);
808
- if (signal?.aborted) {
809
- return rejectAndLeave(new AbloClaimedError(`The wait for the claim on ${targetLabel} was aborted before the grant arrived.`, { code: 'claim_wait_aborted' }));
810
- }
811
- delay = GRANT_POLL_INTERVAL_MS * (0.85 + Math.random() * 0.3);
812
- // A lease that ended answers the beat with 409 `claim_lost`, which the
813
- // wire error mapping raises as AbloClaimedError before this reads
814
- // anything — the wait fails with the loss, as the socket wait does.
815
- const beat = await claims.heartbeat({ claimId });
816
- if (beat.status !== 'held')
817
- continue;
818
- // Granted. The heartbeat ack does not carry the fence token — the claim
819
- // state does, server-stamped at grant.
820
- const state = await claims.retrieve({ claimId });
821
- if (state.status !== 'active') {
822
- return rejectAndLeave(new AbloClaimedError(`Claim lost while queued for ${targetLabel}.`, {
823
- code: 'claim_lost',
824
- }));
825
- }
826
- return state.fenceToken !== undefined
827
- ? { id: claimId, fenceToken: state.fenceToken }
828
- : { id: claimId };
829
- }
830
- }
831
- async function applyClaimedPolicy(target, options, defaultPolicy = 'return') {
832
- const policy = options?.ifClaimed ?? defaultPolicy;
833
- if (policy === 'return')
834
- return;
835
- // policy === 'fail' — gate the read only when the caller opts in.
836
- const state = await listClaimState(target);
837
- if (state.active.length === 0)
838
- return;
839
- throw claimedError(target, state.active, 'model_claimed');
840
- }
841
- const commits = {
842
- async create(commitOptions) {
843
- // Same runtime contract as every other write door — one schema.
844
- assertWriteOptions({
845
- idempotencyKey: commitOptions.idempotencyKey,
846
- readAt: commitOptions.readAt,
847
- onStale: commitOptions.onStale,
848
- wait: commitOptions.wait,
849
- claim: commitOptions.claim,
850
- }, 'commits.create');
851
- const clientTxId = createClientTxId(commitOptions.idempotencyKey);
852
- // Same claim vocabulary as the WS client's `commits.create`: a handle
853
- // supplies the batch stale-guard defaults; explicit options win.
854
- const claim = commitOptions.claim ?? null;
855
- const operations = normalizeCommitOperations({
856
- ...commitOptions,
857
- readAt: commitOptions.readAt ?? claim?.readAt ?? null,
858
- onStale: commitOptions.onStale ?? (claim?.readAt !== undefined ? 'reject' : null),
859
- }, batchFence(claim?.target, claim?.fenceToken));
860
- const requestBody = {
861
- operations,
862
- reads: commitOptions.reads,
863
- track: commitOptions.track,
864
- };
865
- const wait = commitOptions.wait ?? 'confirmed';
866
- let body;
867
- try {
868
- body = await dispatchHttpCommit({
869
- path: '/v1/commits',
870
- method: 'POST',
871
- idempotencyKey: clientTxId,
872
- body: requestBody,
873
- wait,
874
- });
875
- }
876
- catch (error) {
877
- // Coordination collision over HTTP — surface it to observability on the
878
- // same footing as the WS transport, then rethrow unchanged. Fall back to
879
- // the ops we tried to write so the collision always names a row.
880
- recordCoordinationConflict(error, clientTxId, operations.map((o) => ({
881
- model: typeof o.model === 'string' ? o.model : 'unknown',
882
- id: typeof o.id === 'string' ? o.id : 'unknown',
883
- })));
884
- throw error;
885
- }
886
- // `requestJson` throws via `translateHttpError` on any non-2xx, so
887
- // reaching here implies success and `body` is already the success-only
888
- // receipt union — a rejection is a separate type that never arrives here.
889
- // The settlement status therefore passes through verbatim: no branch may
890
- // collapse a state the server reported into a different one.
891
- return {
892
- id: body.id ?? body.clientTxId,
893
- status: body.status,
894
- lastSyncId: body.lastSyncId,
895
- ...(body.notifications && body.notifications.length > 0
896
- ? { notifications: body.notifications }
897
- : {}),
898
- ...(body.missingIds && body.missingIds.length > 0 ? { missingIds: body.missingIds } : {}),
899
- };
900
- },
901
- };
902
- async function listModel(modelName, options) {
903
- const params = new URLSearchParams();
904
- if (options?.limit !== undefined)
905
- params.set('limit', String(options.limit));
906
- if (options?.orderBy) {
907
- const [col, dir] = Object.entries(options.orderBy)[0] ?? [];
908
- if (col) {
909
- params.set('order_by', col);
910
- if (dir === 'desc')
911
- params.set('order', 'desc');
912
- }
913
- }
914
- // The collection route turns any non-reserved query param into an equality
915
- // filter (`?status=todo`). The wire is AND-only equality — matches what a
916
- // stateless reactor needs; richer predicates stay on the stateful path.
917
- if (options?.where && typeof options.where === 'object') {
918
- for (const [k, v] of Object.entries(options.where)) {
919
- if (v !== undefined && v !== null && typeof v !== 'object')
920
- params.set(k, String(v));
921
- }
922
- }
923
- const qs = params.toString();
924
- const res = await requestJson(`/v1/models/${encodeURIComponent(modelName)}${qs ? `?${qs}` : ''}`, { method: 'GET' }, modelListResponseSchema);
925
- // The envelope is checked; the rows are not, and cannot be here. This
926
- // transport is schema-agnostic — it moves rows for whatever schema the
927
- // caller declared, and `T` is that declaration. Row validation belongs to
928
- // the typed facade above, which holds the model's schema.
929
- return res.data;
930
- }
931
- async function retrieveModel(modelName, params) {
932
- await applyClaimedPolicy({ model: modelName, id: params.id }, params);
933
- const query = await requestJson(`/v1/models/${encodeURIComponent(modelName)}/${encodeURIComponent(params.id)}`, { method: 'GET' }, modelReadResponseSchema);
934
- // A miss is `data: undefined`, not a thrown error. The WebSocket client's
935
- // `retrieve` returns `T | undefined` for a missing row; throwing only here
936
- // made the obvious read ("does this row exist?") a hard edge that an agent
937
- // had to wrap in try/catch. Both transports agree: an absent row means absent
938
- // data. Callers branch on `.data` (the documented `.data?.x` usage).
939
- // Normalize a miss to `undefined` (the server may send `null` or omit it).
940
- // The row itself is the caller's declared type — see the note in `listModel`
941
- // on why this transport validates the envelope and not the row.
942
- const data = (query.data ?? undefined);
943
- return { data, stamp: query.stamp, claims: query.claims };
944
- }
945
- /**
946
- * A single-operation mutation over the model-scoped routes — the canonical
947
- * surface that mirrors `ablo.<model>.create/update/delete`:
948
- *
949
- * POST /v1/models/:model create
950
- * PATCH /v1/models/:model/:id update
951
- * DELETE /v1/models/:model/:id delete
952
- *
953
- * The `commits.create(...)` resource remains the path for atomic
954
- * multi-operation envelopes; this helper handles the one-operation,
955
- * one-record case.
956
- */
957
- async function mutateModel(action, modelName, id, data, options, beforeSettlement) {
958
- assertWriteOptions(options && {
959
- idempotencyKey: options.idempotencyKey,
960
- readAt: options.readAt,
961
- onStale: options.onStale,
962
- wait: options.wait,
963
- claim: options.claim,
964
- }, `${modelName} ${action}`);
965
- const clientTxId = createClientTxId(options?.idempotencyKey);
966
- const encModel = encodeURIComponent(modelName);
967
- const path = action === 'create'
968
- ? `/v1/models/${encModel}`
969
- : `/v1/models/${encModel}/${encodeURIComponent(id)}`;
970
- const method = action === 'create' ? 'POST' : action === 'update' ? 'PATCH' : 'DELETE';
971
- // A carried claim handle supplies the stale-guard defaults — one claim
972
- // vocabulary across the WS proxy, `commits.create`, and these routes.
973
- const rawClaim = options?.claim;
974
- const claimHandle = typeof rawClaim === 'object' &&
975
- rawClaim !== null &&
976
- rawClaim.object === 'claim' &&
977
- typeof rawClaim.id === 'string'
978
- ? rawClaim
979
- : undefined;
980
- const readAt = options?.readAt ?? claimHandle?.readAt;
981
- const requestBody = {
982
- claim: normalizeClaimId(options?.claimRef) ?? claimHandle?.id,
983
- onStale: options?.onStale ?? (claimHandle?.readAt !== undefined ? 'reject' : undefined),
984
- readAt,
985
- // The claim's fencing token (Option B), so the per-model HTTP write door
986
- // fences the same as the WS proxy and `commits.create`.
987
- fenceToken: options?.fenceToken ?? claimHandle?.fenceToken,
988
- };
989
- if (action === 'create')
990
- requestBody.id = id;
991
- if (data !== undefined)
992
- requestBody.data = data;
993
- let body;
994
- try {
995
- body = await dispatchHttpCommit({
996
- path,
997
- method,
998
- idempotencyKey: clientTxId,
999
- body: requestBody,
1000
- wait: options?.wait ?? 'confirmed',
1001
- }, beforeSettlement);
1002
- }
1003
- catch (error) {
1004
- // The per-model write door (`ablo.<model>.update/create/delete`). Capture
1005
- // coordination collisions here too; this single row is the fallback target.
1006
- recordCoordinationConflict(error, clientTxId, [{ model: modelName, id }]);
1007
- throw error;
1008
- }
1009
- // Same contract as `commits.create` above: a non-2xx already threw, so
1010
- // `body` is the success-only receipt union and its settlement status passes
1011
- // through verbatim rather than through a catch-all branch.
1012
- return {
1013
- id: body.serverTxId,
1014
- status: body.status,
1015
- lastSyncId: body.lastSyncId,
1016
- };
1017
- }
1018
- function model(name) {
1019
- // Durable lease + FIFO wait-line over HTTP (the existing claim routes). A
1020
- // claim is server state, not a subscription — acquire/hold/release are plain
1021
- // request/response, so a stateless agent participates in coordination too.
1022
- const claimPath = (id) => `/v1/models/${encodeURIComponent(name)}/${encodeURIComponent(id)}/claim`;
1023
- const isClaimHandle = (value) => typeof value === 'object' &&
1024
- value !== null &&
1025
- value.object === 'claim' &&
1026
- typeof value.id === 'string' &&
1027
- typeof value.release === 'function';
1028
- const acquireClaim = async (params) => {
1029
- // The row is named by the URL, so `target` carries only the narrowing a
1030
- // claim adds below it. Sending it is what makes a field-scoped claim
1031
- // actually field-scoped: the server's conflict rule reads `path`,
1032
- // `range`, and `field`, so a claim that keeps them client-side takes a
1033
- // lease on the whole row while its handle says otherwise.
1034
- // Projected in one move rather than member by member. The member-by-member
1035
- // version is how `field` came to be sent while `fields` was not, which
1036
- // left a set-scoped claim silently holding the whole row.
1037
- const narrowing = subTarget(params);
1038
- // Typed as the request contract rather than a bare literal — the omission
1039
- // above was invisible for exactly as long as this was an untyped object.
1040
- const request = {
1041
- description: claimDescription(params),
1042
- ...(params.ttl !== undefined ? { ttl: params.ttl } : {}),
1043
- // The caller's `meta` is the declared shape; the body is wire-shaped,
1044
- // so it crosses through the same conversion `subTarget` used above.
1045
- ...(params.meta !== undefined ? { meta: wireMeta(params.meta) } : {}),
1046
- ...(Object.keys(narrowing).length > 0 ? { target: narrowing } : {}),
1047
- // `queue` (default true) → queue behind the holder; false → fail-fast
1048
- // with AbloClaimedError (work-distribution dedup).
1049
- queue: params.queue ?? true,
1050
- };
1051
- const body = await requestJson(claimPath(params.id), { method: 'POST', body: JSON.stringify(request) }, claimAcquireResponseSchema);
1052
- // One resource, two states, discriminated by `status`. The queued arm
1053
- // WAITS, exactly as the socket client does: `claim({ id })` means
1054
- // "serialize me behind the holder" on every transport, and the grant
1055
- // machinery is the SDK's to own, not a loop each caller re-derives.
1056
- // (Being queued is still not an error — ADR 0018 — which is precisely
1057
- // why it no longer surfaces as one here. The `claims` namespace remains
1058
- // the manual ticket surface.)
1059
- if (body.status === 'queued') {
1060
- return awaitGrantOverHttp(`${name}/${params.id}`, body, params);
1061
- }
1062
- // The lease's own fields are mirrored at the top level, the same place
1063
- // the poll puts them — one reader for both answers.
1064
- return body.fenceToken !== undefined
1065
- ? { id: body.id, fenceToken: body.fenceToken }
1066
- : { id: body.id };
1067
- };
1068
- const releaseClaim = (params) => requestRaw(claimPath(isClaimHandle(params) ? params.target.id : params.id), {
1069
- method: 'DELETE',
1070
- }).then(() => undefined);
1071
- // One beat on the held lease. A lapsed lease answers `claim_lost`
1072
- // (409), which the wire error mapping surfaces as AbloClaimedError —
1073
- // the definitive signal that stops the auto-heartbeat loop.
1074
- const heartbeatClaim = async (id, claimId, options) => {
1075
- const reply = await requestJson(`${claimPath(id)}/heartbeat`, {
1076
- method: 'POST',
1077
- body: JSON.stringify({
1078
- claimId,
1079
- ...(options.ttl !== undefined ? { ttl: options.ttl } : {}),
1080
- ...(options.details !== undefined ? { details: options.details } : {}),
1081
- }),
1082
- }, claimHeartbeatReplySchema);
1083
- return heldHeartbeatReply(reply, `claim ${claimId} on ${name}/${id}`);
1084
- };
1085
- async function claimImpl(params) {
1086
- let acquired;
1087
- try {
1088
- acquired = await acquireClaim(params);
1089
- }
1090
- catch (error) {
1091
- // The try-claim: a held target is an expected outcome of `queue:
1092
- // false`, not an error — resolve `null` and let the caller move on.
1093
- // Every other failure (auth, validation, network) stays a rejection,
1094
- // and the write-site claim path calls `acquireClaim` directly, so a
1095
- // write that could not claim still fails loudly.
1096
- if (params.queue === false &&
1097
- error instanceof AbloClaimedError &&
1098
- (error.code === 'entity_claimed' || error.code === 'claim_conflict')) {
1099
- return null;
1100
- }
1101
- throw error;
1102
- }
1103
- const { id: claimId, fenceToken } = acquired;
1104
- observability?.captureClaim({
1105
- phase: 'acquired',
1106
- claimId,
1107
- model: name,
1108
- id: params.id,
1109
- ...(params.field ? { field: partName(params.field) } : {}),
1110
- description: claimDescription(params),
1111
- });
1112
- const { data, stamp } = await retrieveModel(name, { id: params.id });
1113
- // A held claim hands back a snapshot; the typed `HeldClaim.data` is `T`.
1114
- // `retrieve` now reports a miss as `undefined` rather than throwing, but a
1115
- // claim on a row that doesn't exist has nothing to hold — surface it.
1116
- if (data === undefined) {
1117
- throw new AbloNotFoundError(`Cannot claim ${name}/${params.id}: it does not exist (or is outside this credential's scope).`, [params.id]);
1118
- }
1119
- // One reading of the heartbeat options — cadence and callbacks from
1120
- // whichever spelling the caller used (plan object, shorthand, or the
1121
- // deprecated flat callbacks).
1122
- const plan = resolveHeartbeatPlan(params);
1123
- const heartbeat = async (beatOptions) => {
1124
- const resolved = resolveHeartbeatOptions(beatOptions);
1125
- const beat = await heartbeatClaim(params.id, claimId, {
1126
- ttl: resolved.ttl ?? params.ttl,
1127
- ...(resolved.details !== undefined ? { details: resolved.details } : {}),
1128
- });
1129
- plan.onBeat?.(beat);
1130
- return beat;
1131
- };
1132
- // Opt-in auto-heartbeat — the background-worker cadence. The stateless
1133
- // HTTP claim defaults to the server's acquire window when no TTL
1134
- // was requested, so the default cadence lands at 20s beats.
1135
- const stopHeartbeatLoop = plan.loop
1136
- ? startClaimHeartbeatLoop({
1137
- beat: () => heartbeat(),
1138
- intervalMs: heartbeatCadenceMs(params.ttl !== undefined ? toMs(params.ttl) : DEFAULT_CLAIM_TTL_MS, plan.cadence),
1139
- ...(plan.onLost ? { onLost: plan.onLost } : {}),
1140
- })
1141
- : undefined;
1142
- const release = () => {
1143
- stopHeartbeatLoop?.();
1144
- return releaseClaim(params);
1145
- };
1146
- // The handle handed back is a public claim, so its `meta` is the declared
1147
- // shape — the same crossing the two decodes above make, spelled the same
1148
- // way. `subTarget` is wire-shaped by contract, including here, where the
1149
- // value happens to have started out declared.
1150
- const { meta, ...narrowed } = subTarget(params);
1151
- return {
1152
- object: 'claim',
1153
- id: claimId,
1154
- readAt: stamp,
1155
- ...(fenceToken !== undefined ? { fenceToken } : {}),
1156
- target: {
1157
- ...streamTarget({ model: name, id: params.id }),
1158
- ...narrowed,
1159
- ...(meta !== undefined ? { meta: declaredMeta(meta) } : {}),
1160
- },
1161
- description: claimDescription(params),
1162
- data,
1163
- release,
1164
- revoke: () => {
1165
- void release().catch(() => { });
1166
- },
1167
- heartbeat,
1168
- [Symbol.asyncDispose]: release,
1169
- };
1170
- }
1171
- const claimsForEntity = (params) => requestJson(`/v1/claims?model=${encodeURIComponent(name)}&id=${encodeURIComponent(params.id)}${params.field ? `&field=${encodeURIComponent(partName(params.field))}` : ''}`, { method: 'GET' }, claimListResponseSchema);
1172
- const claim = Object.assign(claimImpl, {
1173
- release: releaseClaim,
1174
- state: async (params) => {
1175
- const res = await claimsForEntity(params);
1176
- // Holders come first in the one list; a `queued` entry is a waiter.
1177
- const first = res.data.find((row) => row.status !== 'queued');
1178
- return first ? claimFromModelClaim(first) : null;
1179
- },
1180
- queue: async (params) => {
1181
- const res = await claimsForEntity(params);
1182
- return {
1183
- object: 'list',
1184
- data: res.data
1185
- .filter((row) => row.status === 'queued')
1186
- .map(claimFromModelClaim),
1187
- };
1188
- },
1189
- reorder: async (params) => {
1190
- await requestRaw(`${claimPath(params.id)}/reorder`, {
1191
- method: 'POST',
1192
- // The reorder route's payload is `{ heldBy, claimId }[]` — a Claim's id
1193
- // is the claimId.
1194
- body: JSON.stringify({
1195
- order: params.order.map((i) => ({ heldBy: i.heldBy, claimId: i.id })),
1196
- }),
1197
- });
1198
- },
1199
- });
1200
- const withMutationClaim = async (id, input, run) => {
1201
- const claimInput = input?.claim;
1202
- if (!claimInput)
1203
- return run(input);
1204
- if (isClaimHandle(claimInput)) {
1205
- return run({ ...input, claimRef: { id: claimInput.id }, claim: undefined });
1206
- }
1207
- // `isClaimHandle` ruled out the handle form above; the generic mismatch
1208
- // (the union carries `Claim`, the guard narrows `Claim<T>`) keeps the
1209
- // compiler from subtracting it, so narrow to the inline-options form.
1210
- const { id: claimId, fenceToken } = await acquireClaim({
1211
- id,
1212
- ...claimInput,
1213
- });
1214
- try {
1215
- return await run({
1216
- ...input,
1217
- claimRef: { id: claimId },
1218
- ...(fenceToken !== undefined ? { fenceToken } : {}),
1219
- claim: undefined,
1220
- });
1221
- }
1222
- finally {
1223
- await releaseClaim({ id }).catch(() => { });
1224
- }
1225
- };
1226
- function updateModel(arg, updater, contention) {
1227
- // Functional form: update(id, current => next). The SDK owns the
1228
- // read-fresh → compute → compare-and-swap → reconcile loop; correctness
1229
- // rides on the row's watermark (readAt + onStale:'reject'), so no claim
1230
- // or per-participant identity is needed and contention never clobbers.
1231
- if (typeof arg === 'string') {
1232
- const id = arg;
1233
- if (typeof updater !== 'function') {
1234
- throw new AbloValidationError(`${name}.update('${id}', updater): the second argument must be an updater ` +
1235
- `function (current) => next. To write a fixed value, use update({ id, data }).`, { code: 'write_options_invalid' });
1236
- }
1237
- return reconcileFunctionalUpdate(updater, contention, {
1238
- model: name,
1239
- id,
1240
- readFresh: async () => {
1241
- const read = await retrieveModel(name, { id });
1242
- return { data: read.data, stamp: read.stamp };
1243
- },
1244
- writeNext: (patch, readAt) => mutateModel('update', name, id, patch, {
1245
- readAt,
1246
- onStale: 'reject',
1247
- wait: 'confirmed',
1248
- }),
1249
- });
1250
- }
1251
- const params = arg;
1252
- return withMutationClaim(params.id, params, async (options) => {
1253
- await applyClaimedPolicy({ model: name, id: params.id }, options);
1254
- return mutateModel('update', name, params.id, params.data, options);
1255
- });
1256
- }
1257
- return {
1258
- claim,
1259
- retrieve(params) {
1260
- return retrieveModel(name, params);
1261
- },
1262
- list(options) {
1263
- return listModel(name, options);
1264
- },
1265
- async create(params) {
1266
- const id = params.id ?? createModelId(name, params.idempotencyKey);
1267
- return withMutationClaim(id, params, async (options) => {
1268
- await applyClaimedPolicy({ model: name, id }, options);
1269
- // Confirm the write, then return the row — the obvious expectation of
1270
- // "create" (the WebSocket client already returns the row). The read-
1271
- // back is the authoritative server row, so it carries the framework
1272
- // defaults (createdAt, createdBy, …) and, for an idempotent re-create of
1273
- // an existing id, the existing row rather than the caller's input.
1274
- let created;
1275
- await mutateModel('create', name, id, params.data, {
1276
- ...options,
1277
- // This method returns the authoritative row, not a receipt. A
1278
- // queued source acceptance cannot satisfy that return contract,
1279
- // even when the caller supplied `wait: 'queued'`.
1280
- wait: 'confirmed',
1281
- }, async () => {
1282
- const read = await retrieveModel(name, { id });
1283
- if (read.data === undefined) {
1284
- throw new AbloNotFoundError(`create ${name}/${id} did not yield a readable row (the write did not confirm).`, [id]);
1285
- }
1286
- created = read.data;
1287
- });
1288
- if (created === undefined) {
1289
- throw new AbloConnectionError('Create settlement did not return its row.', {
1290
- code: 'commit_no_result',
1291
- });
1292
- }
1293
- return created;
1294
- });
1295
- },
1296
- update: updateModel,
1297
- async delete(params) {
1298
- return withMutationClaim(params.id, params, async (options) => {
1299
- await applyClaimedPolicy({ model: name, id: params.id }, options);
1300
- return mutateModel('delete', name, params.id, undefined, options);
1301
- });
1302
- },
1303
- async track(params) {
1304
- const dependency = {
1305
- model: name.toLowerCase(),
1306
- id: params.id,
1307
- ...(params.readAt !== undefined ? { readAt: params.readAt } : {}),
1308
- };
1309
- // A track carries no write, so it rides the commit lane as a
1310
- // zero-operation body — the shape `/v1/commits` accepts for registering
1311
- // a premise without one. Going through the same durable lane as every
1312
- // other commit means a disconnect replays the registration rather than
1313
- // dropping it, and a notification that had already fired is not lost to
1314
- // a retry.
1315
- const body = await dispatchHttpCommit({
1316
- path: '/v1/commits',
1317
- method: 'POST',
1318
- idempotencyKey: createClientTxId(),
1319
- body: { track: [dependency] },
1320
- wait: 'confirmed',
1321
- });
1322
- return body.notifications && body.notifications.length > 0
1323
- ? { notifications: body.notifications }
1324
- : {};
1325
- },
1326
- };
1327
- }
1328
- return {
1329
- ready,
1330
- waitForFlush: () => runInHttpCommitLane(async () => {
1331
- await ready();
1332
- const replayed = await replayHttpCommitOutbox();
1333
- await confirmReplayedHttpCommits(replayed);
1334
- }),
1335
- async dispose() { },
1336
- async purge() { },
1337
- commits,
1338
- claims,
1339
- model,
1340
- sessions: {
1341
- async create(params) {
1342
- // Stateless mint: the configured key is the control-plane credential here
1343
- // (no startup `rk_` exchange runs on this client). It reuses the resolved
1344
- // base URL and fetch; the shared `mintSession` handles the two server routes.
1345
- const apiKey = await resolveApiKeyValue(configuredApiKey);
1346
- if (!apiKey) {
1347
- throw new AbloAuthenticationError('sessions.create requires a secret (sk_) API key — call it from your backend, not the browser.', { code: 'apikey_missing' });
1348
- }
1349
- // A transport built without a schema has no way to translate `can`'s
1350
- // schema keys into the type names the server gates on. Minting anyway
1351
- // would spell every override wrong and surface as
1352
- // `capability_scope_denied` on the agent's first write, so refuse here
1353
- // instead of guessing.
1354
- if (!options.modelTypenames) {
1355
- throw new AbloValidationError('sessions.create needs the schema this client is bound to. Construct it ' +
1356
- "through Ablo({ schema, apiKey, transport: 'http' }) rather than the " +
1357
- 'bare transport.', { code: 'invalid_options', param: 'schema' });
1358
- }
1359
- return mintSession(params, {
1360
- apiKey,
1361
- baseUrl: apiBaseUrl,
1362
- modelTypenames: options.modelTypenames,
1363
- ...(options.fetch ? { fetch: options.fetch } : {}),
1364
- });
1365
- },
1366
- },
1367
- async getAuthToken() {
1368
- // Mirror `authHeaders()`: a configured API key wins, else the
1369
- // construction-time auth token. Resolve the (possibly async) key setter.
1370
- return (await resolveApiKeyValue(configuredApiKey)) ?? configuredAuthToken ?? null;
1371
- },
1372
- };
1373
- }
1374
- function normalizeClaimId(claim) {
1375
- if (typeof claim === 'string')
1376
- return claim;
1377
- return claim?.id;
1378
- }
1379
- function parseBody(bodyText) {
1380
- if (bodyText.length === 0)
1381
- return null;
1382
- try {
1383
- return JSON.parse(bodyText);
1384
- }
1385
- catch {
1386
- return bodyText;
1387
- }
1388
- }