@abloatai/ablo 0.35.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 (622) hide show
  1. package/AGENTS.md +2 -2
  2. package/CHANGELOG.md +71 -1929
  3. package/NOTICE +2 -2
  4. package/README.md +23 -532
  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 -112
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +3 -161
  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 +2 -2
  59. package/docs/api-keys.md +13 -12
  60. package/docs/api.md +15 -53
  61. package/docs/audit.md +4 -3
  62. package/docs/cli.md +11 -11
  63. package/docs/client-behavior.md +9 -9
  64. package/docs/concurrency-convention.md +28 -42
  65. package/docs/coordination.md +228 -86
  66. package/docs/data-sources.md +5 -5
  67. package/docs/debugging.md +34 -12
  68. package/docs/deployment.md +8 -8
  69. package/docs/examples/agent-human.md +4 -4
  70. package/docs/examples/ai-sdk-tool.md +1 -1
  71. package/docs/examples/existing-python-backend.md +15 -4
  72. package/docs/examples/nextjs.md +27 -6
  73. package/docs/examples/scoped-agent.md +3 -3
  74. package/docs/examples/server-agent.md +2 -2
  75. package/docs/groups.md +57 -3
  76. package/docs/guarantees.md +37 -10
  77. package/docs/how-it-works.md +32 -8
  78. package/docs/idempotency.md +6 -6
  79. package/docs/identity.md +24 -24
  80. package/docs/index.md +8 -8
  81. package/docs/integration-guide.md +38 -16
  82. package/docs/internal/README.md +18 -0
  83. package/docs/internal/agent-fleet-coordination-design.md +171 -0
  84. package/docs/internal/agent-orchestration.md +58 -0
  85. package/docs/internal/commit-identifiers.md +91 -0
  86. package/docs/internal/concurrency-open-decisions.md +37 -0
  87. package/docs/internal/data-source-reverse-channel.md +150 -0
  88. package/docs/internal/per-field-conflict-detection.md +165 -0
  89. package/docs/internal/postgres-replication.md +64 -0
  90. package/docs/internal/serializable-schema.md +119 -0
  91. package/docs/internal/structure.md +32 -0
  92. package/docs/mcp.md +9 -9
  93. package/docs/migration.md +37 -18
  94. package/docs/projects.md +1 -1
  95. package/docs/quickstart.md +2 -2
  96. package/docs/react.md +24 -13
  97. package/docs/schema-contract.md +3 -3
  98. package/docs/sessions.md +91 -37
  99. package/docs/webhooks.md +9 -9
  100. package/examples/README.md +2 -2
  101. package/examples/data-source/README.md +1 -1
  102. package/examples/data-source/ablo-driver.ts +1 -1
  103. package/examples/data-source/customer-server.ts +1 -1
  104. package/examples/data-source/run.ts +1 -1
  105. package/examples/data-source/schema.ts +1 -1
  106. package/examples/quickstart.ts +2 -2
  107. package/llms.txt +12 -12
  108. package/package.json +64 -174
  109. package/dist/BaseSyncedStore.d.ts +0 -823
  110. package/dist/BaseSyncedStore.js +0 -1955
  111. package/dist/Database.d.ts +0 -335
  112. package/dist/Database.js +0 -1500
  113. package/dist/InstanceCache.d.ts +0 -233
  114. package/dist/InstanceCache.js +0 -1164
  115. package/dist/LazyReferenceCollection.d.ts +0 -177
  116. package/dist/LazyReferenceCollection.js +0 -461
  117. package/dist/Model.d.ts +0 -444
  118. package/dist/Model.js +0 -909
  119. package/dist/ModelRegistry.d.ts +0 -221
  120. package/dist/ModelRegistry.js +0 -537
  121. package/dist/NetworkMonitor.d.ts +0 -26
  122. package/dist/NetworkMonitor.js +0 -77
  123. package/dist/RuntimeContext.d.ts +0 -52
  124. package/dist/RuntimeContext.js +0 -80
  125. package/dist/SyncClient.d.ts +0 -551
  126. package/dist/SyncClient.js +0 -2199
  127. package/dist/adapters/alwaysOnline.d.ts +0 -14
  128. package/dist/adapters/alwaysOnline.js +0 -17
  129. package/dist/adapters/inMemoryStorage.d.ts +0 -31
  130. package/dist/adapters/inMemoryStorage.js +0 -110
  131. package/dist/ai-sdk/coordinatedTool.d.ts +0 -120
  132. package/dist/ai-sdk/coordinatedTool.js +0 -134
  133. package/dist/ai-sdk/coordinationContext.d.ts +0 -46
  134. package/dist/ai-sdk/coordinationContext.js +0 -106
  135. package/dist/ai-sdk/index.d.ts +0 -121
  136. package/dist/ai-sdk/index.js +0 -121
  137. package/dist/ai-sdk/wrap.d.ts +0 -65
  138. package/dist/ai-sdk/wrap.js +0 -39
  139. package/dist/auth/index.d.ts +0 -1
  140. package/dist/auth/index.js +0 -8
  141. package/dist/batching/index.d.ts +0 -55
  142. package/dist/batching/index.js +0 -147
  143. package/dist/cli.cjs +0 -288600
  144. package/dist/client/Ablo.d.ts +0 -231
  145. package/dist/client/Ablo.js +0 -149
  146. package/dist/client/abloClient.d.ts +0 -309
  147. package/dist/client/abloClient.js +0 -13
  148. package/dist/client/clientPrelude.d.ts +0 -52
  149. package/dist/client/clientPrelude.js +0 -60
  150. package/dist/client/consoleLogger.d.ts +0 -35
  151. package/dist/client/consoleLogger.js +0 -44
  152. package/dist/client/coreClient.d.ts +0 -60
  153. package/dist/client/coreClient.js +0 -118
  154. package/dist/client/createInternalComponents.d.ts +0 -46
  155. package/dist/client/createInternalComponents.js +0 -92
  156. package/dist/client/createModelProxy.d.ts +0 -228
  157. package/dist/client/createModelProxy.js +0 -818
  158. package/dist/client/humans.d.ts +0 -48
  159. package/dist/client/humans.js +0 -52
  160. package/dist/client/modelRegistration.d.ts +0 -10
  161. package/dist/client/modelRegistration.js +0 -312
  162. package/dist/client/options.d.ts +0 -461
  163. package/dist/client/options.js +0 -7
  164. package/dist/client/reactiveEngine.d.ts +0 -48
  165. package/dist/client/reactiveEngine.js +0 -910
  166. package/dist/client/resourceTypes.d.ts +0 -12
  167. package/dist/client/resourceTypes.js +0 -10
  168. package/dist/client/schemaConfig.d.ts +0 -44
  169. package/dist/client/schemaConfig.js +0 -185
  170. package/dist/client/validateAbloOptions.d.ts +0 -42
  171. package/dist/client/validateAbloOptions.js +0 -43
  172. package/dist/client/wsMutationExecutor.d.ts +0 -27
  173. package/dist/client/wsMutationExecutor.js +0 -72
  174. package/dist/context.d.ts +0 -29
  175. package/dist/context.js +0 -58
  176. package/dist/coordination/ClaimLog.d.ts +0 -26
  177. package/dist/coordination/ClaimLog.js +0 -32
  178. package/dist/coordination/index.d.ts +0 -1
  179. package/dist/coordination/index.js +0 -8
  180. package/dist/core/DatabaseManager.d.ts +0 -105
  181. package/dist/core/DatabaseManager.js +0 -387
  182. package/dist/core/QueryProcessor.d.ts +0 -75
  183. package/dist/core/QueryProcessor.js +0 -255
  184. package/dist/core/QueryView.d.ts +0 -79
  185. package/dist/core/QueryView.js +0 -218
  186. package/dist/core/StoreManager.d.ts +0 -112
  187. package/dist/core/StoreManager.js +0 -302
  188. package/dist/core/ViewRegistry.d.ts +0 -20
  189. package/dist/core/ViewRegistry.js +0 -55
  190. package/dist/core/index.d.ts +0 -33
  191. package/dist/core/index.js +0 -48
  192. package/dist/core/openIDBWithTimeout.d.ts +0 -65
  193. package/dist/core/openIDBWithTimeout.js +0 -153
  194. package/dist/core/queryUtils.d.ts +0 -45
  195. package/dist/core/queryUtils.js +0 -69
  196. package/dist/core/storeContract.d.ts +0 -145
  197. package/dist/core/storeContract.js +0 -12
  198. package/dist/docs/catalog.d.ts +0 -72
  199. package/dist/docs/catalog.js +0 -227
  200. package/dist/docs/index.d.ts +0 -10
  201. package/dist/docs/index.js +0 -10
  202. package/dist/environment.d.ts +0 -1
  203. package/dist/environment.js +0 -8
  204. package/dist/interfaces/index.d.ts +0 -311
  205. package/dist/interfaces/index.js +0 -9
  206. package/dist/keys/index.d.ts +0 -1
  207. package/dist/keys/index.js +0 -8
  208. package/dist/mutators/RecordingMutation.d.ts +0 -36
  209. package/dist/mutators/RecordingMutation.js +0 -182
  210. package/dist/mutators/Transaction.d.ts +0 -40
  211. package/dist/mutators/Transaction.js +0 -58
  212. package/dist/mutators/UndoManager.d.ts +0 -258
  213. package/dist/mutators/UndoManager.js +0 -658
  214. package/dist/mutators/defineMutators.d.ts +0 -60
  215. package/dist/mutators/defineMutators.js +0 -18
  216. package/dist/mutators/inverseOp.d.ts +0 -126
  217. package/dist/mutators/inverseOp.js +0 -71
  218. package/dist/mutators/mutateActions.d.ts +0 -45
  219. package/dist/mutators/mutateActions.js +0 -105
  220. package/dist/mutators/readerActions.d.ts +0 -33
  221. package/dist/mutators/readerActions.js +0 -57
  222. package/dist/mutators/undoApply.d.ts +0 -51
  223. package/dist/mutators/undoApply.js +0 -117
  224. package/dist/policy/index.d.ts +0 -21
  225. package/dist/policy/index.js +0 -20
  226. package/dist/query/client.d.ts +0 -61
  227. package/dist/query/client.js +0 -137
  228. package/dist/query/types.d.ts +0 -85
  229. package/dist/query/types.js +0 -16
  230. package/dist/react/AbloProvider.d.ts +0 -230
  231. package/dist/react/AbloProvider.js +0 -455
  232. package/dist/react/ClientSideSuspense.d.ts +0 -36
  233. package/dist/react/ClientSideSuspense.js +0 -17
  234. package/dist/react/DefaultFallback.d.ts +0 -24
  235. package/dist/react/DefaultFallback.js +0 -43
  236. package/dist/react/context.d.ts +0 -55
  237. package/dist/react/context.js +0 -29
  238. package/dist/react/index.d.ts +0 -61
  239. package/dist/react/index.js +0 -66
  240. package/dist/react/internalContext.d.ts +0 -33
  241. package/dist/react/internalContext.js +0 -3
  242. package/dist/react/useAblo.d.ts +0 -75
  243. package/dist/react/useAblo.js +0 -102
  244. package/dist/react/useCurrentUserId.d.ts +0 -22
  245. package/dist/react/useCurrentUserId.js +0 -34
  246. package/dist/react/useErrorListener.d.ts +0 -20
  247. package/dist/react/useErrorListener.js +0 -38
  248. package/dist/react/useMutationFailureListener.d.ts +0 -26
  249. package/dist/react/useMutationFailureListener.js +0 -38
  250. package/dist/react/useMutators.d.ts +0 -56
  251. package/dist/react/useMutators.js +0 -84
  252. package/dist/react/useReactive.d.ts +0 -35
  253. package/dist/react/useReactive.js +0 -123
  254. package/dist/react/useSyncStatus.d.ts +0 -59
  255. package/dist/react/useSyncStatus.js +0 -76
  256. package/dist/react/useUndoScope.d.ts +0 -34
  257. package/dist/react/useUndoScope.js +0 -81
  258. package/dist/schema/coordination.d.ts +0 -112
  259. package/dist/schema/coordination.js +0 -129
  260. package/dist/schema/ddl.d.ts +0 -97
  261. package/dist/schema/ddl.js +0 -491
  262. package/dist/schema/ddlLock.d.ts +0 -35
  263. package/dist/schema/ddlLock.js +0 -46
  264. package/dist/schema/diff.d.ts +0 -225
  265. package/dist/schema/diff.js +0 -289
  266. package/dist/schema/generate.d.ts +0 -19
  267. package/dist/schema/generate.js +0 -86
  268. package/dist/schema/index.d.ts +0 -41
  269. package/dist/schema/index.js +0 -76
  270. package/dist/schema/queries.d.ts +0 -201
  271. package/dist/schema/queries.js +0 -144
  272. package/dist/schema/select.d.ts +0 -40
  273. package/dist/schema/select.js +0 -87
  274. package/dist/schema/serialize.d.ts +0 -115
  275. package/dist/schema/serialize.js +0 -262
  276. package/dist/schema/sugar.d.ts +0 -109
  277. package/dist/schema/sugar.js +0 -83
  278. package/dist/schema/syncDeltaRow.d.ts +0 -6
  279. package/dist/schema/syncDeltaRow.js +0 -6
  280. package/dist/server/adapter.d.ts +0 -173
  281. package/dist/server/adapter.js +0 -18
  282. package/dist/server/commit.d.ts +0 -107
  283. package/dist/server/commit.js +0 -1
  284. package/dist/server/index.d.ts +0 -14
  285. package/dist/server/index.js +0 -2
  286. package/dist/server/readConfig.d.ts +0 -80
  287. package/dist/server/readConfig.js +0 -8
  288. package/dist/server/storageMode.d.ts +0 -23
  289. package/dist/server/storageMode.js +0 -17
  290. package/dist/source/adapter.d.ts +0 -81
  291. package/dist/source/adapter.js +0 -22
  292. package/dist/source/adapters/drizzle.d.ts +0 -48
  293. package/dist/source/adapters/drizzle.js +0 -219
  294. package/dist/source/adapters/kysely.d.ts +0 -42
  295. package/dist/source/adapters/kysely.js +0 -205
  296. package/dist/source/adapters/kyselyMutationCore.d.ts +0 -76
  297. package/dist/source/adapters/kyselyMutationCore.js +0 -125
  298. package/dist/source/adapters/memory.d.ts +0 -13
  299. package/dist/source/adapters/memory.js +0 -130
  300. package/dist/source/adapters/prisma.d.ts +0 -63
  301. package/dist/source/adapters/prisma.js +0 -202
  302. package/dist/source/conformance.d.ts +0 -37
  303. package/dist/source/conformance.js +0 -215
  304. package/dist/source/connector.d.ts +0 -95
  305. package/dist/source/connector.js +0 -266
  306. package/dist/source/connectorProtocol.d.ts +0 -154
  307. package/dist/source/connectorProtocol.js +0 -163
  308. package/dist/source/contract.d.ts +0 -195
  309. package/dist/source/contract.js +0 -164
  310. package/dist/source/factory.d.ts +0 -92
  311. package/dist/source/factory.js +0 -286
  312. package/dist/source/footprint.d.ts +0 -111
  313. package/dist/source/footprint.js +0 -0
  314. package/dist/source/idempotency.d.ts +0 -61
  315. package/dist/source/idempotency.js +0 -144
  316. package/dist/source/index.d.ts +0 -23
  317. package/dist/source/index.js +0 -30
  318. package/dist/source/migrations.d.ts +0 -21
  319. package/dist/source/migrations.js +0 -103
  320. package/dist/source/next.d.ts +0 -32
  321. package/dist/source/next.js +0 -25
  322. package/dist/source/pushQueue.d.ts +0 -134
  323. package/dist/source/pushQueue.js +0 -256
  324. package/dist/source/signing.d.ts +0 -92
  325. package/dist/source/signing.js +0 -162
  326. package/dist/source/types.d.ts +0 -401
  327. package/dist/source/types.js +0 -59
  328. package/dist/stores/ObjectStore.d.ts +0 -115
  329. package/dist/stores/ObjectStore.js +0 -393
  330. package/dist/stores/ObjectStoreContract.d.ts +0 -38
  331. package/dist/stores/ObjectStoreContract.js +0 -1
  332. package/dist/stores/SyncActionStore.d.ts +0 -97
  333. package/dist/stores/SyncActionStore.js +0 -504
  334. package/dist/stores/syncAction.d.ts +0 -26
  335. package/dist/stores/syncAction.js +0 -16
  336. package/dist/surface.d.ts +0 -36
  337. package/dist/surface.js +0 -75
  338. package/dist/sync/BootstrapFetcher.d.ts +0 -280
  339. package/dist/sync/BootstrapFetcher.js +0 -962
  340. package/dist/sync/ConnectionManager.d.ts +0 -8
  341. package/dist/sync/ConnectionManager.js +0 -8
  342. package/dist/sync/OnDemandLoader.d.ts +0 -228
  343. package/dist/sync/OnDemandLoader.js +0 -742
  344. package/dist/sync/SubscriptionManager.d.ts +0 -159
  345. package/dist/sync/SubscriptionManager.js +0 -243
  346. package/dist/sync/SyncWebSocket.d.ts +0 -173
  347. package/dist/sync/SyncWebSocket.js +0 -438
  348. package/dist/sync/awaitClaimGrant.d.ts +0 -6
  349. package/dist/sync/awaitClaimGrant.js +0 -6
  350. package/dist/sync/bootstrapApply.d.ts +0 -70
  351. package/dist/sync/bootstrapApply.js +0 -73
  352. package/dist/sync/commitFrames.d.ts +0 -8
  353. package/dist/sync/commitFrames.js +0 -8
  354. package/dist/sync/contextPorts.d.ts +0 -18
  355. package/dist/sync/contextPorts.js +0 -31
  356. package/dist/sync/createClaimStream.d.ts +0 -7
  357. package/dist/sync/createClaimStream.js +0 -7
  358. package/dist/sync/createPresenceStream.d.ts +0 -69
  359. package/dist/sync/createPresenceStream.js +0 -200
  360. package/dist/sync/createSnapshot.d.ts +0 -29
  361. package/dist/sync/createSnapshot.js +0 -118
  362. package/dist/sync/credentialLifecycle.d.ts +0 -7
  363. package/dist/sync/credentialLifecycle.js +0 -7
  364. package/dist/sync/deltaPipeline.d.ts +0 -113
  365. package/dist/sync/deltaPipeline.js +0 -261
  366. package/dist/sync/groupChange.d.ts +0 -113
  367. package/dist/sync/groupChange.js +0 -242
  368. package/dist/sync/participants.d.ts +0 -115
  369. package/dist/sync/participants.js +0 -344
  370. package/dist/sync/persistedPrefix.d.ts +0 -12
  371. package/dist/sync/persistedPrefix.js +0 -22
  372. package/dist/sync/schemaDrift.d.ts +0 -55
  373. package/dist/sync/schemaDrift.js +0 -53
  374. package/dist/sync/schemas.d.ts +0 -70
  375. package/dist/sync/schemas.js +0 -94
  376. package/dist/sync/syncCursor.d.ts +0 -40
  377. package/dist/sync/syncCursor.js +0 -55
  378. package/dist/sync/syncPlan.d.ts +0 -54
  379. package/dist/sync/syncPlan.js +0 -50
  380. package/dist/sync/wsFrameHandlers.d.ts +0 -8
  381. package/dist/sync/wsFrameHandlers.js +0 -8
  382. package/dist/testing/fixtures/bootstrap.d.ts +0 -49
  383. package/dist/testing/fixtures/bootstrap.js +0 -59
  384. package/dist/testing/fixtures/deltas.d.ts +0 -83
  385. package/dist/testing/fixtures/deltas.js +0 -136
  386. package/dist/testing/fixtures/httpResponses.d.ts +0 -70
  387. package/dist/testing/fixtures/httpResponses.js +0 -90
  388. package/dist/testing/fixtures/models.d.ts +0 -83
  389. package/dist/testing/fixtures/models.js +0 -272
  390. package/dist/testing/helpers/reactWrapper.d.ts +0 -69
  391. package/dist/testing/helpers/reactWrapper.js +0 -67
  392. package/dist/testing/helpers/syncEngineHarness.d.ts +0 -54
  393. package/dist/testing/helpers/syncEngineHarness.js +0 -73
  394. package/dist/testing/helpers/wait.d.ts +0 -30
  395. package/dist/testing/helpers/wait.js +0 -49
  396. package/dist/testing/index.d.ts +0 -23
  397. package/dist/testing/index.js +0 -33
  398. package/dist/testing/mocks/FakeDatabase.d.ts +0 -18
  399. package/dist/testing/mocks/FakeDatabase.js +0 -10
  400. package/dist/testing/mocks/MockMutationExecutor.d.ts +0 -87
  401. package/dist/testing/mocks/MockMutationExecutor.js +0 -186
  402. package/dist/testing/mocks/MockNetworkMonitor.d.ts +0 -20
  403. package/dist/testing/mocks/MockNetworkMonitor.js +0 -46
  404. package/dist/testing/mocks/MockSyncContext.d.ts +0 -51
  405. package/dist/testing/mocks/MockSyncContext.js +0 -72
  406. package/dist/testing/mocks/MockSyncStore.d.ts +0 -88
  407. package/dist/testing/mocks/MockSyncStore.js +0 -171
  408. package/dist/testing/mocks/MockWebSocket.d.ts +0 -71
  409. package/dist/testing/mocks/MockWebSocket.js +0 -118
  410. package/dist/transaction/ablo.d.ts +0 -88
  411. package/dist/transaction/ablo.js +0 -33
  412. package/dist/transaction/auth/apiKey.d.ts +0 -152
  413. package/dist/transaction/auth/apiKey.js +0 -419
  414. package/dist/transaction/auth/bootstrapScope.d.ts +0 -15
  415. package/dist/transaction/auth/bootstrapScope.js +0 -1
  416. package/dist/transaction/auth/capability.d.ts +0 -177
  417. package/dist/transaction/auth/capability.js +0 -199
  418. package/dist/transaction/auth/credentialEndpoint.d.ts +0 -61
  419. package/dist/transaction/auth/credentialEndpoint.js +0 -86
  420. package/dist/transaction/auth/credentialPolicy.d.ts +0 -148
  421. package/dist/transaction/auth/credentialPolicy.js +0 -125
  422. package/dist/transaction/auth/credentialSource.d.ts +0 -30
  423. package/dist/transaction/auth/credentialSource.js +0 -55
  424. package/dist/transaction/auth/hostedEndpoints.d.ts +0 -21
  425. package/dist/transaction/auth/hostedEndpoints.js +0 -21
  426. package/dist/transaction/auth/identity.d.ts +0 -55
  427. package/dist/transaction/auth/identity.js +0 -210
  428. package/dist/transaction/auth/index.d.ts +0 -162
  429. package/dist/transaction/auth/index.js +0 -304
  430. package/dist/transaction/auth/schemas.d.ts +0 -59
  431. package/dist/transaction/auth/schemas.js +0 -85
  432. package/dist/transaction/auth/sessionMint.d.ts +0 -28
  433. package/dist/transaction/auth/sessionMint.js +0 -85
  434. package/dist/transaction/coordination/awaitClaimGrant.d.ts +0 -49
  435. package/dist/transaction/coordination/awaitClaimGrant.js +0 -112
  436. package/dist/transaction/coordination/claimHeartbeatLoop.d.ts +0 -50
  437. package/dist/transaction/coordination/claimHeartbeatLoop.js +0 -88
  438. package/dist/transaction/coordination/claimMeta.d.ts +0 -49
  439. package/dist/transaction/coordination/claimMeta.js +0 -52
  440. package/dist/transaction/coordination/createClaimStream.d.ts +0 -64
  441. package/dist/transaction/coordination/createClaimStream.js +0 -475
  442. package/dist/transaction/coordination/events.d.ts +0 -74
  443. package/dist/transaction/coordination/events.js +0 -7
  444. package/dist/transaction/coordination/index.d.ts +0 -19
  445. package/dist/transaction/coordination/index.js +0 -44
  446. package/dist/transaction/coordination/locator.d.ts +0 -83
  447. package/dist/transaction/coordination/locator.js +0 -82
  448. package/dist/transaction/coordination/schema.d.ts +0 -1473
  449. package/dist/transaction/coordination/schema.js +0 -1013
  450. package/dist/transaction/coordination/targetConflict.d.ts +0 -2
  451. package/dist/transaction/coordination/targetConflict.js +0 -103
  452. package/dist/transaction/coordination/trace.d.ts +0 -78
  453. package/dist/transaction/coordination/trace.js +0 -138
  454. package/dist/transaction/durableWrites.d.ts +0 -62
  455. package/dist/transaction/durableWrites.js +0 -71
  456. package/dist/transaction/environment.d.ts +0 -105
  457. package/dist/transaction/environment.js +0 -108
  458. package/dist/transaction/errorCodes.d.ts +0 -403
  459. package/dist/transaction/errorCodes.js +0 -480
  460. package/dist/transaction/errors.d.ts +0 -428
  461. package/dist/transaction/errors.js +0 -686
  462. package/dist/transaction/index.d.ts +0 -20
  463. package/dist/transaction/index.js +0 -20
  464. package/dist/transaction/keys/index.d.ts +0 -87
  465. package/dist/transaction/keys/index.js +0 -207
  466. package/dist/transaction/log/syncDeltaRow.d.ts +0 -158
  467. package/dist/transaction/log/syncDeltaRow.js +0 -95
  468. package/dist/transaction/logPosition.d.ts +0 -97
  469. package/dist/transaction/logPosition.js +0 -125
  470. package/dist/transaction/logger.d.ts +0 -16
  471. package/dist/transaction/logger.js +0 -7
  472. package/dist/transaction/observability.d.ts +0 -53
  473. package/dist/transaction/observability.js +0 -19
  474. package/dist/transaction/persistence.d.ts +0 -12
  475. package/dist/transaction/persistence.js +0 -11
  476. package/dist/transaction/plugin.d.ts +0 -192
  477. package/dist/transaction/plugin.js +0 -87
  478. package/dist/transaction/policy/types.d.ts +0 -217
  479. package/dist/transaction/policy/types.js +0 -126
  480. package/dist/transaction/resources/functionalUpdate.d.ts +0 -79
  481. package/dist/transaction/resources/functionalUpdate.js +0 -87
  482. package/dist/transaction/resources/httpResources.d.ts +0 -266
  483. package/dist/transaction/resources/httpResources.js +0 -7
  484. package/dist/transaction/resources/modelOperations.d.ts +0 -319
  485. package/dist/transaction/resources/modelOperations.js +0 -12
  486. package/dist/transaction/resources/mutationOptions.d.ts +0 -66
  487. package/dist/transaction/resources/mutationOptions.js +0 -9
  488. package/dist/transaction/resources/where.d.ts +0 -85
  489. package/dist/transaction/resources/where.js +0 -70
  490. package/dist/transaction/resources/writeOptionsSchema.d.ts +0 -47
  491. package/dist/transaction/resources/writeOptionsSchema.js +0 -73
  492. package/dist/transaction/schema/field.d.ts +0 -126
  493. package/dist/transaction/schema/field.js +0 -265
  494. package/dist/transaction/schema/loadStrategy.d.ts +0 -45
  495. package/dist/transaction/schema/loadStrategy.js +0 -46
  496. package/dist/transaction/schema/model.d.ts +0 -379
  497. package/dist/transaction/schema/model.js +0 -123
  498. package/dist/transaction/schema/openapi.d.ts +0 -57
  499. package/dist/transaction/schema/openapi.js +0 -340
  500. package/dist/transaction/schema/relation.d.ts +0 -199
  501. package/dist/transaction/schema/relation.js +0 -104
  502. package/dist/transaction/schema/residency.d.ts +0 -29
  503. package/dist/transaction/schema/residency.js +0 -25
  504. package/dist/transaction/schema/roles.d.ts +0 -249
  505. package/dist/transaction/schema/roles.js +0 -230
  506. package/dist/transaction/schema/schema.d.ts +0 -324
  507. package/dist/transaction/schema/schema.js +0 -305
  508. package/dist/transaction/schema/tenancy.d.ts +0 -139
  509. package/dist/transaction/schema/tenancy.js +0 -190
  510. package/dist/transaction/transactionLayer.d.ts +0 -82
  511. package/dist/transaction/transactionLayer.js +0 -24
  512. package/dist/transaction/transactions/settlement/commitEnvelope.d.ts +0 -143
  513. package/dist/transaction/transactions/settlement/commitEnvelope.js +0 -161
  514. package/dist/transaction/transactions/settlement/httpCommitEnvelope.d.ts +0 -53
  515. package/dist/transaction/transactions/settlement/httpCommitEnvelope.js +0 -207
  516. package/dist/transaction/transactions/settlement/idempotencyKey.d.ts +0 -10
  517. package/dist/transaction/transactions/settlement/idempotencyKey.js +0 -9
  518. package/dist/transaction/transactions/settlement/pendingWrite.d.ts +0 -112
  519. package/dist/transaction/transactions/settlement/pendingWrite.js +0 -20
  520. package/dist/transaction/transport/commitFrames.d.ts +0 -90
  521. package/dist/transaction/transport/commitFrames.js +0 -134
  522. package/dist/transaction/transport/connectionManager.d.ts +0 -215
  523. package/dist/transaction/transport/connectionManager.js +0 -673
  524. package/dist/transaction/transport/credentialLifecycle.d.ts +0 -177
  525. package/dist/transaction/transport/credentialLifecycle.js +0 -324
  526. package/dist/transaction/transport/heartbeat.d.ts +0 -65
  527. package/dist/transaction/transport/heartbeat.js +0 -93
  528. package/dist/transaction/transport/httpClient.d.ts +0 -123
  529. package/dist/transaction/transport/httpClient.js +0 -145
  530. package/dist/transaction/transport/httpOptions.d.ts +0 -33
  531. package/dist/transaction/transport/httpOptions.js +0 -12
  532. package/dist/transaction/transport/httpTransport.d.ts +0 -8
  533. package/dist/transaction/transport/httpTransport.js +0 -1276
  534. package/dist/transaction/transport/networkProbe.d.ts +0 -84
  535. package/dist/transaction/transport/networkProbe.js +0 -207
  536. package/dist/transaction/transport/wsFrameHandlers.d.ts +0 -128
  537. package/dist/transaction/transport/wsFrameHandlers.js +0 -429
  538. package/dist/transaction/transport/wsTransport.d.ts +0 -576
  539. package/dist/transaction/transport/wsTransport.js +0 -1017
  540. package/dist/transaction/types/assertExact.d.ts +0 -17
  541. package/dist/transaction/types/assertExact.js +0 -1
  542. package/dist/transaction/types/global.d.ts +0 -107
  543. package/dist/transaction/types/global.js +0 -40
  544. package/dist/transaction/types/index.d.ts +0 -205
  545. package/dist/transaction/types/index.js +0 -56
  546. package/dist/transaction/types/modelData.d.ts +0 -10
  547. package/dist/transaction/types/modelData.js +0 -9
  548. package/dist/transaction/types/participant.d.ts +0 -20
  549. package/dist/transaction/types/participant.js +0 -10
  550. package/dist/transaction/types/streams.d.ts +0 -540
  551. package/dist/transaction/types/streams.js +0 -11
  552. package/dist/transaction/utils/asyncIterator.d.ts +0 -34
  553. package/dist/transaction/utils/asyncIterator.js +0 -135
  554. package/dist/transaction/utils/duration.d.ts +0 -25
  555. package/dist/transaction/utils/duration.js +0 -45
  556. package/dist/transaction/utils/json.d.ts +0 -57
  557. package/dist/transaction/utils/json.js +0 -276
  558. package/dist/transaction/wire/accountResponses.d.ts +0 -351
  559. package/dist/transaction/wire/accountResponses.js +0 -255
  560. package/dist/transaction/wire/auth.d.ts +0 -49
  561. package/dist/transaction/wire/auth.js +0 -57
  562. package/dist/transaction/wire/bootstrapReason.d.ts +0 -9
  563. package/dist/transaction/wire/bootstrapReason.js +0 -8
  564. package/dist/transaction/wire/claimEvent.d.ts +0 -76
  565. package/dist/transaction/wire/claimEvent.js +0 -73
  566. package/dist/transaction/wire/claims.d.ts +0 -463
  567. package/dist/transaction/wire/claims.js +0 -229
  568. package/dist/transaction/wire/commit.d.ts +0 -603
  569. package/dist/transaction/wire/commit.js +0 -321
  570. package/dist/transaction/wire/delta.d.ts +0 -250
  571. package/dist/transaction/wire/delta.js +0 -147
  572. package/dist/transaction/wire/errorEnvelope.d.ts +0 -72
  573. package/dist/transaction/wire/errorEnvelope.js +0 -123
  574. package/dist/transaction/wire/feedCursor.d.ts +0 -60
  575. package/dist/transaction/wire/feedCursor.js +0 -82
  576. package/dist/transaction/wire/feedEvent.d.ts +0 -177
  577. package/dist/transaction/wire/feedEvent.js +0 -39
  578. package/dist/transaction/wire/frames.d.ts +0 -194
  579. package/dist/transaction/wire/frames.js +0 -50
  580. package/dist/transaction/wire/inboundFrames.d.ts +0 -552
  581. package/dist/transaction/wire/inboundFrames.js +0 -116
  582. package/dist/transaction/wire/index.d.ts +0 -50
  583. package/dist/transaction/wire/index.js +0 -74
  584. package/dist/transaction/wire/listEnvelope.d.ts +0 -37
  585. package/dist/transaction/wire/listEnvelope.js +0 -42
  586. package/dist/transaction/wire/modelResponses.d.ts +0 -85
  587. package/dist/transaction/wire/modelResponses.js +0 -43
  588. package/dist/transaction/wire/protocol.d.ts +0 -38
  589. package/dist/transaction/wire/protocol.js +0 -38
  590. package/dist/transaction/wire/protocolVersion.d.ts +0 -73
  591. package/dist/transaction/wire/protocolVersion.js +0 -83
  592. package/dist/transactions/mutations/MutationQueue.d.ts +0 -655
  593. package/dist/transactions/mutations/MutationQueue.js +0 -2797
  594. package/dist/transactions/mutations/MutationStore.d.ts +0 -20
  595. package/dist/transactions/mutations/MutationStore.js +0 -53
  596. package/dist/transactions/mutations/UnconfirmedWrites.d.ts +0 -82
  597. package/dist/transactions/mutations/UnconfirmedWrites.js +0 -104
  598. package/dist/transactions/mutations/coalesceRules.d.ts +0 -58
  599. package/dist/transactions/mutations/coalesceRules.js +0 -140
  600. package/dist/transactions/mutations/commitLatency.d.ts +0 -52
  601. package/dist/transactions/mutations/commitLatency.js +0 -130
  602. package/dist/transactions/mutations/commitOutboxStore.d.ts +0 -28
  603. package/dist/transactions/mutations/commitOutboxStore.js +0 -26
  604. package/dist/transactions/mutations/commitPayload.d.ts +0 -164
  605. package/dist/transactions/mutations/commitPayload.js +0 -152
  606. package/dist/transactions/mutations/deltaConfirmation.d.ts +0 -59
  607. package/dist/transactions/mutations/deltaConfirmation.js +0 -233
  608. package/dist/transactions/mutations/durableWriteStore.d.ts +0 -14
  609. package/dist/transactions/mutations/durableWriteStore.js +0 -12
  610. package/dist/transactions/mutations/optimisticApply.d.ts +0 -49
  611. package/dist/transactions/mutations/optimisticApply.js +0 -65
  612. package/dist/transactions/mutations/replayValidation.d.ts +0 -186
  613. package/dist/transactions/mutations/replayValidation.js +0 -163
  614. package/dist/utils/mobxSetup.d.ts +0 -53
  615. package/dist/utils/mobxSetup.js +0 -330
  616. package/dist/webhooks/events.d.ts +0 -43
  617. package/dist/webhooks/events.js +0 -42
  618. package/dist/webhooks/index.d.ts +0 -8
  619. package/dist/webhooks/index.js +0 -8
  620. package/dist/wire/index.d.ts +0 -1
  621. package/dist/wire/index.js +0 -8
  622. package/docs/interaction-model.md +0 -99
@@ -1,1276 +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, claimHeartbeatReplySchema, claimListResponseSchema, } from '../wire/claims.js';
16
- import { modelListResponseSchema, modelReadResponseSchema, } from '../wire/modelResponses.js';
17
- import { toMs } from '../utils/duration.js';
18
- import { heartbeatCadenceMs, resolveHeartbeatOptions, 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 } from '../coordination/schema.js';
38
- import { subTarget, streamTarget } 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, fenceToken) {
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
- // The batch's claim (if any) supplies one token for every op, mirroring
711
- // how it supplies the batch `readAt`.
712
- fenceToken: op.fenceToken ?? fenceToken ?? null,
713
- };
714
- }
715
- function normalizeCommitOperations(commitOptions, fenceToken) {
716
- if (commitOptions.operations.length === 0) {
717
- throw new AbloValidationError('Commit requires a non-empty `operations` array.', {
718
- code: 'commit_operation_required',
719
- });
720
- }
721
- return commitOptions.operations.map((op) => normalizeCommitOperation(op, commitOptions, fenceToken));
722
- }
723
- async function listClaimState(target) {
724
- const params = new URLSearchParams();
725
- if (target?.model)
726
- params.set('model', target.model);
727
- if (target?.id)
728
- params.set('id', target.id);
729
- if (target?.field)
730
- params.set('field', target.field);
731
- const suffix = params.toString();
732
- const body = await requestJson(`/v1/claims${suffix ? `?${suffix}` : ''}`, { method: 'GET' }, claimListResponseSchema);
733
- return { active: body.claims, queue: body.queue };
734
- }
735
- async function applyClaimedPolicy(target, options, defaultPolicy = 'return') {
736
- const policy = options?.ifClaimed ?? defaultPolicy;
737
- if (policy === 'return')
738
- return;
739
- // policy === 'fail' — gate the read only when the caller opts in.
740
- const state = await listClaimState(target);
741
- if (state.active.length === 0)
742
- return;
743
- throw claimedError(target, state.active, 'model_claimed');
744
- }
745
- const commits = {
746
- async create(commitOptions) {
747
- // Same runtime contract as every other write door — one schema.
748
- assertWriteOptions({
749
- idempotencyKey: commitOptions.idempotencyKey,
750
- readAt: commitOptions.readAt,
751
- onStale: commitOptions.onStale,
752
- wait: commitOptions.wait,
753
- claim: commitOptions.claim,
754
- }, 'commits.create');
755
- const clientTxId = createClientTxId(commitOptions.idempotencyKey);
756
- // Same claim vocabulary as the WS client's `commits.create`: a handle
757
- // supplies the batch stale-guard defaults; explicit options win.
758
- const claim = commitOptions.claim ?? null;
759
- const operations = normalizeCommitOperations({
760
- ...commitOptions,
761
- readAt: commitOptions.readAt ?? claim?.readAt ?? null,
762
- onStale: commitOptions.onStale ?? (claim?.readAt !== undefined ? 'reject' : null),
763
- }, claim?.fenceToken ?? null);
764
- const requestBody = {
765
- operations,
766
- reads: commitOptions.reads,
767
- track: commitOptions.track,
768
- };
769
- const wait = commitOptions.wait ?? 'confirmed';
770
- let body;
771
- try {
772
- body = await dispatchHttpCommit({
773
- path: '/v1/commits',
774
- method: 'POST',
775
- idempotencyKey: clientTxId,
776
- body: requestBody,
777
- wait,
778
- });
779
- }
780
- catch (error) {
781
- // Coordination collision over HTTP — surface it to observability on the
782
- // same footing as the WS transport, then rethrow unchanged. Fall back to
783
- // the ops we tried to write so the collision always names a row.
784
- recordCoordinationConflict(error, clientTxId, operations.map((o) => ({
785
- model: typeof o.model === 'string' ? o.model : 'unknown',
786
- id: typeof o.id === 'string' ? o.id : 'unknown',
787
- })));
788
- throw error;
789
- }
790
- // `requestJson` throws via `translateHttpError` on any non-2xx, so
791
- // reaching here implies success and `body` is already the success-only
792
- // receipt union — a rejection is a separate type that never arrives here.
793
- // The settlement status therefore passes through verbatim: no branch may
794
- // collapse a state the server reported into a different one.
795
- return {
796
- id: body.id ?? body.clientTxId,
797
- status: body.status,
798
- lastSyncId: body.lastSyncId,
799
- ...(body.notifications && body.notifications.length > 0
800
- ? { notifications: body.notifications }
801
- : {}),
802
- ...(body.missingIds && body.missingIds.length > 0 ? { missingIds: body.missingIds } : {}),
803
- };
804
- },
805
- };
806
- async function listModel(modelName, options) {
807
- const params = new URLSearchParams();
808
- if (options?.limit !== undefined)
809
- params.set('limit', String(options.limit));
810
- if (options?.orderBy) {
811
- const [col, dir] = Object.entries(options.orderBy)[0] ?? [];
812
- if (col) {
813
- params.set('order_by', col);
814
- if (dir === 'desc')
815
- params.set('order', 'desc');
816
- }
817
- }
818
- // The collection route turns any non-reserved query param into an equality
819
- // filter (`?status=todo`). The wire is AND-only equality — matches what a
820
- // stateless reactor needs; richer predicates stay on the stateful path.
821
- if (options?.where && typeof options.where === 'object') {
822
- for (const [k, v] of Object.entries(options.where)) {
823
- if (v !== undefined && v !== null && typeof v !== 'object')
824
- params.set(k, String(v));
825
- }
826
- }
827
- const qs = params.toString();
828
- const res = await requestJson(`/v1/models/${encodeURIComponent(modelName)}${qs ? `?${qs}` : ''}`, { method: 'GET' }, modelListResponseSchema);
829
- // The envelope is checked; the rows are not, and cannot be here. This
830
- // transport is schema-agnostic — it moves rows for whatever schema the
831
- // caller declared, and `T` is that declaration. Row validation belongs to
832
- // the typed facade above, which holds the model's schema.
833
- return res.data;
834
- }
835
- async function retrieveModel(modelName, params) {
836
- await applyClaimedPolicy({ model: modelName, id: params.id }, params);
837
- const query = await requestJson(`/v1/models/${encodeURIComponent(modelName)}/${encodeURIComponent(params.id)}`, { method: 'GET' }, modelReadResponseSchema);
838
- // A miss is `data: undefined`, not a thrown error. The WebSocket client's
839
- // `retrieve` returns `T | undefined` for a missing row; throwing only here
840
- // made the obvious read ("does this row exist?") a hard edge that an agent
841
- // had to wrap in try/catch. Both transports agree: an absent row means absent
842
- // data. Callers branch on `.data` (the documented `.data?.x` usage).
843
- // Normalize a miss to `undefined` (the server may send `null` or omit it).
844
- // The row itself is the caller's declared type — see the note in `listModel`
845
- // on why this transport validates the envelope and not the row.
846
- const data = (query.data ?? undefined);
847
- return { data, stamp: query.stamp, claims: query.claims };
848
- }
849
- /**
850
- * A single-operation mutation over the model-scoped routes — the canonical
851
- * surface that mirrors `ablo.<model>.create/update/delete`:
852
- *
853
- * POST /v1/models/:model create
854
- * PATCH /v1/models/:model/:id update
855
- * DELETE /v1/models/:model/:id delete
856
- *
857
- * The `commits.create(...)` resource remains the path for atomic
858
- * multi-operation envelopes; this helper handles the one-operation,
859
- * one-record case.
860
- */
861
- async function mutateModel(action, modelName, id, data, options, beforeSettlement) {
862
- assertWriteOptions(options && {
863
- idempotencyKey: options.idempotencyKey,
864
- readAt: options.readAt,
865
- onStale: options.onStale,
866
- wait: options.wait,
867
- claim: options.claim,
868
- }, `${modelName} ${action}`);
869
- const clientTxId = createClientTxId(options?.idempotencyKey);
870
- const encModel = encodeURIComponent(modelName);
871
- const path = action === 'create'
872
- ? `/v1/models/${encModel}`
873
- : `/v1/models/${encModel}/${encodeURIComponent(id)}`;
874
- const method = action === 'create' ? 'POST' : action === 'update' ? 'PATCH' : 'DELETE';
875
- // A carried claim handle supplies the stale-guard defaults — one claim
876
- // vocabulary across the WS proxy, `commits.create`, and these routes.
877
- const rawClaim = options?.claim;
878
- const claimHandle = typeof rawClaim === 'object' &&
879
- rawClaim !== null &&
880
- rawClaim.object === 'claim' &&
881
- typeof rawClaim.id === 'string'
882
- ? rawClaim
883
- : undefined;
884
- const readAt = options?.readAt ?? claimHandle?.readAt;
885
- const requestBody = {
886
- claim: normalizeClaimId(options?.claimRef) ?? claimHandle?.id,
887
- onStale: options?.onStale ?? (claimHandle?.readAt !== undefined ? 'reject' : undefined),
888
- readAt,
889
- // The claim's fencing token (Option B), so the per-model HTTP write door
890
- // fences the same as the WS proxy and `commits.create`.
891
- fenceToken: options?.fenceToken ?? claimHandle?.fenceToken,
892
- };
893
- if (action === 'create')
894
- requestBody.id = id;
895
- if (data !== undefined)
896
- requestBody.data = data;
897
- let body;
898
- try {
899
- body = await dispatchHttpCommit({
900
- path,
901
- method,
902
- idempotencyKey: clientTxId,
903
- body: requestBody,
904
- wait: options?.wait ?? 'confirmed',
905
- }, beforeSettlement);
906
- }
907
- catch (error) {
908
- // The per-model write door (`ablo.<model>.update/create/delete`). Capture
909
- // coordination collisions here too; this single row is the fallback target.
910
- recordCoordinationConflict(error, clientTxId, [{ model: modelName, id }]);
911
- throw error;
912
- }
913
- // Same contract as `commits.create` above: a non-2xx already threw, so
914
- // `body` is the success-only receipt union and its settlement status passes
915
- // through verbatim rather than through a catch-all branch.
916
- return {
917
- id: body.serverTxId,
918
- status: body.status,
919
- lastSyncId: body.lastSyncId,
920
- };
921
- }
922
- function model(name) {
923
- // Durable lease + FIFO wait-line over HTTP (the existing claim routes). A
924
- // claim is server state, not a subscription — acquire/hold/release are plain
925
- // request/response, so a stateless agent participates in coordination too.
926
- const claimPath = (id) => `/v1/models/${encodeURIComponent(name)}/${encodeURIComponent(id)}/claim`;
927
- const isClaimHandle = (value) => typeof value === 'object' &&
928
- value !== null &&
929
- value.object === 'claim' &&
930
- typeof value.id === 'string' &&
931
- typeof value.release === 'function';
932
- const acquireClaim = async (params) => {
933
- // The row is named by the URL, so `target` carries only the narrowing a
934
- // claim adds below it. Sending it is what makes a field-scoped claim
935
- // actually field-scoped: the server's conflict rule reads `path`,
936
- // `range`, and `field`, so a claim that keeps them client-side takes a
937
- // lease on the whole row while its handle says otherwise.
938
- // Projected in one move rather than member by member. The member-by-member
939
- // version is how `field` came to be sent while `fields` was not, which
940
- // left a set-scoped claim silently holding the whole row.
941
- const narrowing = subTarget(params);
942
- // Typed as the request contract rather than a bare literal — the omission
943
- // above was invisible for exactly as long as this was an untyped object.
944
- const request = {
945
- description: claimDescription(params),
946
- ...(params.ttl !== undefined ? { ttl: params.ttl } : {}),
947
- // The caller's `meta` is the declared shape; the body is wire-shaped,
948
- // so it crosses through the same conversion `subTarget` used above.
949
- ...(params.meta !== undefined ? { meta: wireMeta(params.meta) } : {}),
950
- ...(Object.keys(narrowing).length > 0 ? { target: narrowing } : {}),
951
- // `queue` (default true) → queue behind the holder; false → fail-fast
952
- // with AbloClaimedError (work-distribution dedup).
953
- queue: params.queue ?? true,
954
- };
955
- const body = await requestJson(claimPath(params.id), { method: 'POST', body: JSON.stringify(request) }, claimAcquireResponseSchema);
956
- // HELD BY ADR 0018 (`docs/decisions/0018-the-premise-is-the-missing-structure.md`).
957
- // Raising a successful outcome as an exception is wrong, and this stays
958
- // wrong on purpose: it is the symptom the ADR is derived from, so removing
959
- // the smell without the structure would hide the reason for the work.
960
- //
961
- // Being queued is not a transport failure — it is "I cannot establish my
962
- // premise yet." Premise is the structure the codebase approximates six
963
- // ways and has never modelled, which is why this was never solvable here.
964
- // A client-side polling loop is specifically ruled out: it would become a
965
- // shape callers build against, and the better it worked the harder it
966
- // would be to replace with the status this should be.
967
- //
968
- // A caller that must wait today can poll `GET /v1/claims/{claimId}`, which
969
- // is served and published. That primitive is safe to build on; a blessed
970
- // wait algorithm here is not.
971
- // The two arms are told apart by `status`, which only the queued reply
972
- // carries — see `claimAcquireResponseSchema` for why they cannot be a
973
- // discriminated union.
974
- if ('status' in body) {
975
- throw new AbloClaimedError(`Target ${name}/${params.id} is held; queued at position ${body.position}. ` +
976
- `Poll \`GET /v1/claims/{claimId}\` for the grant — the HTTP client does not await it.`, { code: 'claim_queued' });
977
- }
978
- const { id, fenceToken } = body.claim;
979
- return fenceToken !== undefined ? { id, fenceToken } : { id };
980
- };
981
- const releaseClaim = (params) => requestRaw(claimPath(isClaimHandle(params) ? params.target.id : params.id), {
982
- method: 'DELETE',
983
- }).then(() => undefined);
984
- // One beat on the held lease. A lapsed lease answers `claim_lost`
985
- // (409), which the wire error mapping surfaces as AbloClaimedError —
986
- // the definitive signal that stops the auto-heartbeat loop.
987
- const heartbeatClaim = async (id, claimId, options) => {
988
- const reply = await requestJson(`${claimPath(id)}/heartbeat`, {
989
- method: 'POST',
990
- body: JSON.stringify({
991
- claimId,
992
- ...(options.ttl !== undefined ? { ttl: options.ttl } : {}),
993
- ...(options.details !== undefined ? { details: options.details } : {}),
994
- }),
995
- }, claimHeartbeatReplySchema);
996
- return heldHeartbeatReply(reply, `claim ${claimId} on ${name}/${id}`);
997
- };
998
- async function claimImpl(params) {
999
- const { id: claimId, fenceToken } = await acquireClaim(params);
1000
- observability?.captureClaim({
1001
- phase: 'acquired',
1002
- claimId,
1003
- model: name,
1004
- id: params.id,
1005
- ...(params.field ? { field: params.field } : {}),
1006
- description: claimDescription(params),
1007
- });
1008
- const { data, stamp } = await retrieveModel(name, { id: params.id });
1009
- // A held claim hands back a snapshot; the typed `HeldClaim.data` is `T`.
1010
- // `retrieve` now reports a miss as `undefined` rather than throwing, but a
1011
- // claim on a row that doesn't exist has nothing to hold — surface it.
1012
- if (data === undefined) {
1013
- throw new AbloNotFoundError(`Cannot claim ${name}/${params.id}: it does not exist (or is outside this credential's scope).`, [params.id]);
1014
- }
1015
- const heartbeat = async (beatOptions) => {
1016
- const resolved = resolveHeartbeatOptions(beatOptions);
1017
- const beat = await heartbeatClaim(params.id, claimId, {
1018
- ttl: resolved.ttl ?? params.ttl,
1019
- ...(resolved.details !== undefined ? { details: resolved.details } : {}),
1020
- });
1021
- params.onHeartbeat?.(beat);
1022
- return beat;
1023
- };
1024
- // Opt-in auto-heartbeat — the background-worker cadence. The stateless
1025
- // HTTP claim defaults to the server's acquire window when no TTL
1026
- // was requested, so the default cadence lands at 20s beats.
1027
- const stopHeartbeatLoop = params.heartbeat
1028
- ? startClaimHeartbeatLoop({
1029
- beat: () => heartbeat(),
1030
- intervalMs: heartbeatCadenceMs(params.ttl !== undefined ? toMs(params.ttl) : DEFAULT_CLAIM_TTL_MS, params.heartbeat),
1031
- ...(params.onHeartbeatLost ? { onLost: params.onHeartbeatLost } : {}),
1032
- })
1033
- : undefined;
1034
- const release = () => {
1035
- stopHeartbeatLoop?.();
1036
- return releaseClaim(params);
1037
- };
1038
- // The handle handed back is a public claim, so its `meta` is the declared
1039
- // shape — the same crossing the two decodes above make, spelled the same
1040
- // way. `subTarget` is wire-shaped by contract, including here, where the
1041
- // value happens to have started out declared.
1042
- const { meta, ...narrowed } = subTarget(params);
1043
- return {
1044
- object: 'claim',
1045
- id: claimId,
1046
- readAt: stamp,
1047
- ...(fenceToken !== undefined ? { fenceToken } : {}),
1048
- target: {
1049
- ...streamTarget({ model: name, id: params.id }),
1050
- ...narrowed,
1051
- ...(meta !== undefined ? { meta: declaredMeta(meta) } : {}),
1052
- },
1053
- description: claimDescription(params),
1054
- data,
1055
- release,
1056
- revoke: () => {
1057
- void release().catch(() => { });
1058
- },
1059
- heartbeat,
1060
- [Symbol.asyncDispose]: release,
1061
- };
1062
- }
1063
- const claimsForEntity = (params) => requestJson(`/v1/claims?model=${encodeURIComponent(name)}&id=${encodeURIComponent(params.id)}${params.field ? `&field=${encodeURIComponent(params.field)}` : ''}`, { method: 'GET' }, claimListResponseSchema);
1064
- const claim = Object.assign(claimImpl, {
1065
- release: releaseClaim,
1066
- state: async (params) => {
1067
- const res = await claimsForEntity(params);
1068
- const first = res.claims?.[0];
1069
- return first ? claimFromModelClaim(first) : null;
1070
- },
1071
- queue: async (params) => {
1072
- const res = await claimsForEntity(params);
1073
- return {
1074
- object: 'list',
1075
- data: (res.queue ?? []).map(claimFromModelClaim),
1076
- };
1077
- },
1078
- reorder: async (params) => {
1079
- await requestRaw(`${claimPath(params.id)}/reorder`, {
1080
- method: 'POST',
1081
- // The reorder route's payload is `{ heldBy, claimId }[]` — a Claim's id
1082
- // is the claimId.
1083
- body: JSON.stringify({
1084
- order: params.order.map((i) => ({ heldBy: i.heldBy, claimId: i.id })),
1085
- }),
1086
- });
1087
- },
1088
- });
1089
- const withMutationClaim = async (id, input, run) => {
1090
- const claimInput = input?.claim;
1091
- if (!claimInput)
1092
- return run(input);
1093
- if (isClaimHandle(claimInput)) {
1094
- return run({ ...input, claimRef: { id: claimInput.id }, claim: undefined });
1095
- }
1096
- // `isClaimHandle` ruled out the handle form above; the generic mismatch
1097
- // (the union carries `Claim`, the guard narrows `Claim<T>`) keeps the
1098
- // compiler from subtracting it, so narrow to the inline-options form.
1099
- const { id: claimId, fenceToken } = await acquireClaim({
1100
- id,
1101
- ...claimInput,
1102
- });
1103
- try {
1104
- return await run({
1105
- ...input,
1106
- claimRef: { id: claimId },
1107
- ...(fenceToken !== undefined ? { fenceToken } : {}),
1108
- claim: undefined,
1109
- });
1110
- }
1111
- finally {
1112
- await releaseClaim({ id }).catch(() => { });
1113
- }
1114
- };
1115
- function updateModel(arg, updater, contention) {
1116
- // Functional form: update(id, current => next). The SDK owns the
1117
- // read-fresh → compute → compare-and-swap → reconcile loop; correctness
1118
- // rides on the row's watermark (readAt + onStale:'reject'), so no claim
1119
- // or per-participant identity is needed and contention never clobbers.
1120
- if (typeof arg === 'string') {
1121
- const id = arg;
1122
- if (typeof updater !== 'function') {
1123
- throw new AbloValidationError(`${name}.update('${id}', updater): the second argument must be an updater ` +
1124
- `function (current) => next. To write a fixed value, use update({ id, data }).`, { code: 'write_options_invalid' });
1125
- }
1126
- return reconcileFunctionalUpdate(updater, contention, {
1127
- model: name,
1128
- id,
1129
- readFresh: async () => {
1130
- const read = await retrieveModel(name, { id });
1131
- return { data: read.data, stamp: read.stamp };
1132
- },
1133
- writeNext: (patch, readAt) => mutateModel('update', name, id, patch, {
1134
- readAt,
1135
- onStale: 'reject',
1136
- wait: 'confirmed',
1137
- }),
1138
- });
1139
- }
1140
- const params = arg;
1141
- return withMutationClaim(params.id, params, async (options) => {
1142
- await applyClaimedPolicy({ model: name, id: params.id }, options);
1143
- return mutateModel('update', name, params.id, params.data, options);
1144
- });
1145
- }
1146
- return {
1147
- claim,
1148
- retrieve(params) {
1149
- return retrieveModel(name, params);
1150
- },
1151
- list(options) {
1152
- return listModel(name, options);
1153
- },
1154
- async create(params) {
1155
- const id = params.id ?? createModelId(name, params.idempotencyKey);
1156
- return withMutationClaim(id, params, async (options) => {
1157
- await applyClaimedPolicy({ model: name, id }, options);
1158
- // Confirm the write, then return the row — the obvious expectation of
1159
- // "create" (the WebSocket client already returns the row). The read-
1160
- // back is the authoritative server row, so it carries the framework
1161
- // defaults (createdAt, createdBy, …) and, for an idempotent re-create of
1162
- // an existing id, the existing row rather than the caller's input.
1163
- let created;
1164
- await mutateModel('create', name, id, params.data, {
1165
- ...options,
1166
- // This method returns the authoritative row, not a receipt. A
1167
- // queued source acceptance cannot satisfy that return contract,
1168
- // even when the caller supplied `wait: 'queued'`.
1169
- wait: 'confirmed',
1170
- }, async () => {
1171
- const read = await retrieveModel(name, { id });
1172
- if (read.data === undefined) {
1173
- throw new AbloNotFoundError(`create ${name}/${id} did not yield a readable row (the write did not confirm).`, [id]);
1174
- }
1175
- created = read.data;
1176
- });
1177
- if (created === undefined) {
1178
- throw new AbloConnectionError('Create settlement did not return its row.', {
1179
- code: 'commit_no_result',
1180
- });
1181
- }
1182
- return created;
1183
- });
1184
- },
1185
- update: updateModel,
1186
- async delete(params) {
1187
- return withMutationClaim(params.id, params, async (options) => {
1188
- await applyClaimedPolicy({ model: name, id: params.id }, options);
1189
- return mutateModel('delete', name, params.id, undefined, options);
1190
- });
1191
- },
1192
- async track(params) {
1193
- const dependency = {
1194
- model: name.toLowerCase(),
1195
- id: params.id,
1196
- ...(params.readAt !== undefined ? { readAt: params.readAt } : {}),
1197
- };
1198
- // A track carries no write, so it rides the commit lane as a
1199
- // zero-operation body — the shape `/v1/commits` accepts for registering
1200
- // a premise without one. Going through the same durable lane as every
1201
- // other commit means a disconnect replays the registration rather than
1202
- // dropping it, and a notification that had already fired is not lost to
1203
- // a retry.
1204
- const body = await dispatchHttpCommit({
1205
- path: '/v1/commits',
1206
- method: 'POST',
1207
- idempotencyKey: createClientTxId(),
1208
- body: { track: [dependency] },
1209
- wait: 'confirmed',
1210
- });
1211
- return body.notifications && body.notifications.length > 0
1212
- ? { notifications: body.notifications }
1213
- : {};
1214
- },
1215
- };
1216
- }
1217
- return {
1218
- ready,
1219
- waitForFlush: () => runInHttpCommitLane(async () => {
1220
- await ready();
1221
- const replayed = await replayHttpCommitOutbox();
1222
- await confirmReplayedHttpCommits(replayed);
1223
- }),
1224
- async dispose() { },
1225
- async purge() { },
1226
- commits,
1227
- model,
1228
- sessions: {
1229
- async create(params) {
1230
- // Stateless mint: the configured key is the control-plane credential here
1231
- // (no startup `rk_` exchange runs on this client). It reuses the resolved
1232
- // base URL and fetch; the shared `mintSession` handles the two server routes.
1233
- const apiKey = await resolveApiKeyValue(configuredApiKey);
1234
- if (!apiKey) {
1235
- throw new AbloAuthenticationError('sessions.create requires a secret (sk_) API key — call it from your backend, not the browser.', { code: 'apikey_missing' });
1236
- }
1237
- // A transport built without a schema has no way to translate `can`'s
1238
- // schema keys into the type names the server gates on. Minting anyway
1239
- // would spell every override wrong and surface as
1240
- // `capability_scope_denied` on the agent's first write, so refuse here
1241
- // instead of guessing.
1242
- if (!options.modelTypenames) {
1243
- throw new AbloValidationError('sessions.create needs the schema this client is bound to. Construct it ' +
1244
- "through Ablo({ schema, apiKey, transport: 'http' }) rather than the " +
1245
- 'bare transport.', { code: 'invalid_options', param: 'schema' });
1246
- }
1247
- return mintSession(params, {
1248
- apiKey,
1249
- baseUrl: apiBaseUrl,
1250
- modelTypenames: options.modelTypenames,
1251
- ...(options.fetch ? { fetch: options.fetch } : {}),
1252
- });
1253
- },
1254
- },
1255
- async getAuthToken() {
1256
- // Mirror `authHeaders()`: a configured API key wins, else the
1257
- // construction-time auth token. Resolve the (possibly async) key setter.
1258
- return (await resolveApiKeyValue(configuredApiKey)) ?? configuredAuthToken ?? null;
1259
- },
1260
- };
1261
- }
1262
- function normalizeClaimId(claim) {
1263
- if (typeof claim === 'string')
1264
- return claim;
1265
- return claim?.id;
1266
- }
1267
- function parseBody(bodyText) {
1268
- if (bodyText.length === 0)
1269
- return null;
1270
- try {
1271
- return JSON.parse(bodyText);
1272
- }
1273
- catch {
1274
- return bodyText;
1275
- }
1276
- }