@abloatai/ablo 0.25.0 → 0.27.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 (425) hide show
  1. package/AGENTS.md +5 -3
  2. package/CHANGELOG.md +34 -0
  3. package/README.md +104 -88
  4. package/dist/BaseSyncedStore.d.ts +140 -266
  5. package/dist/BaseSyncedStore.js +338 -739
  6. package/dist/Database.d.ts +62 -77
  7. package/dist/Database.js +106 -127
  8. package/dist/{ObjectPool.d.ts → InstanceCache.d.ts} +10 -13
  9. package/dist/{ObjectPool.js → InstanceCache.js} +91 -83
  10. package/dist/LazyReferenceCollection.d.ts +11 -15
  11. package/dist/LazyReferenceCollection.js +16 -15
  12. package/dist/Model.d.ts +37 -52
  13. package/dist/Model.js +52 -69
  14. package/dist/ModelRegistry.d.ts +46 -25
  15. package/dist/ModelRegistry.js +32 -30
  16. package/dist/NetworkMonitor.d.ts +5 -6
  17. package/dist/NetworkMonitor.js +6 -7
  18. package/dist/SyncClient.d.ts +119 -109
  19. package/dist/SyncClient.js +303 -224
  20. package/dist/SyncEngineContext.d.ts +1 -3
  21. package/dist/SyncEngineContext.js +1 -2
  22. package/dist/adapters/alwaysOnline.d.ts +6 -8
  23. package/dist/adapters/alwaysOnline.js +6 -8
  24. package/dist/adapters/inMemoryStorage.d.ts +9 -9
  25. package/dist/adapters/inMemoryStorage.js +9 -9
  26. package/dist/agent/Agent.d.ts +39 -31
  27. package/dist/agent/Agent.js +35 -23
  28. package/dist/agent/index.d.ts +4 -4
  29. package/dist/agent/index.js +5 -5
  30. package/dist/agent/session.d.ts +47 -44
  31. package/dist/agent/session.js +37 -48
  32. package/dist/agent/types.d.ts +26 -31
  33. package/dist/agent/types.js +6 -7
  34. package/dist/ai-sdk/coordinatedTool.d.ts +108 -0
  35. package/dist/ai-sdk/{coordinated-tool.js → coordinatedTool.js} +44 -38
  36. package/dist/ai-sdk/coordinationContext.d.ts +46 -0
  37. package/dist/ai-sdk/{coordination-context.js → coordinationContext.js} +30 -31
  38. package/dist/ai-sdk/index.d.ts +25 -22
  39. package/dist/ai-sdk/index.js +25 -22
  40. package/dist/ai-sdk/wrap.d.ts +7 -8
  41. package/dist/ai-sdk/wrap.js +2 -2
  42. package/dist/auth/credentialPolicy.d.ts +74 -71
  43. package/dist/auth/credentialPolicy.js +51 -56
  44. package/dist/auth/credentialSource.d.ts +7 -18
  45. package/dist/auth/credentialSource.js +10 -18
  46. package/dist/auth/index.d.ts +59 -58
  47. package/dist/auth/index.js +34 -40
  48. package/dist/auth/schemas.d.ts +5 -4
  49. package/dist/auth/schemas.js +5 -4
  50. package/dist/batching/index.d.ts +19 -21
  51. package/dist/batching/index.js +14 -17
  52. package/dist/cli.cjs +483 -369
  53. package/dist/client/Ablo.d.ts +107 -836
  54. package/dist/client/Ablo.js +174 -833
  55. package/dist/client/ApiClient.d.ts +44 -20
  56. package/dist/client/ApiClient.js +193 -44
  57. package/dist/client/auth.d.ts +51 -60
  58. package/dist/client/auth.js +137 -110
  59. package/dist/client/claimHeartbeatLoop.d.ts +50 -0
  60. package/dist/client/claimHeartbeatLoop.js +88 -0
  61. package/dist/client/consoleLogger.d.ts +35 -0
  62. package/dist/client/consoleLogger.js +44 -0
  63. package/dist/client/createInternalComponents.d.ts +14 -17
  64. package/dist/client/createInternalComponents.js +26 -31
  65. package/dist/client/createModelProxy.d.ts +130 -120
  66. package/dist/client/createModelProxy.js +158 -124
  67. package/dist/client/credentialEndpoint.d.ts +61 -0
  68. package/dist/client/credentialEndpoint.js +86 -0
  69. package/dist/client/functionalUpdate.d.ts +29 -27
  70. package/dist/client/functionalUpdate.js +21 -21
  71. package/dist/client/hostedEndpoints.d.ts +21 -0
  72. package/dist/client/hostedEndpoints.js +21 -0
  73. package/dist/client/httpClient.d.ts +58 -54
  74. package/dist/client/httpClient.js +29 -31
  75. package/dist/client/identity.d.ts +15 -20
  76. package/dist/client/identity.js +49 -59
  77. package/dist/client/modelRegistration.d.ts +10 -0
  78. package/dist/client/modelRegistration.js +301 -0
  79. package/dist/client/options.d.ts +373 -0
  80. package/dist/client/options.js +6 -0
  81. package/dist/client/registerDataSource.d.ts +9 -9
  82. package/dist/client/registerDataSource.js +15 -16
  83. package/dist/client/resourceTypes.d.ts +333 -0
  84. package/dist/client/resourceTypes.js +7 -0
  85. package/dist/client/schemaConfig.d.ts +44 -0
  86. package/dist/client/schemaConfig.js +176 -0
  87. package/dist/client/sessionMint.d.ts +17 -13
  88. package/dist/client/sessionMint.js +26 -31
  89. package/dist/client/validateAbloOptions.d.ts +12 -14
  90. package/dist/client/validateAbloOptions.js +9 -10
  91. package/dist/client/writeOptionsSchema.d.ts +18 -16
  92. package/dist/client/writeOptionsSchema.js +23 -20
  93. package/dist/client/wsMutationExecutor.d.ts +28 -0
  94. package/dist/client/wsMutationExecutor.js +71 -0
  95. package/dist/context.d.ts +6 -4
  96. package/dist/context.js +6 -7
  97. package/dist/coordination/index.d.ts +13 -4
  98. package/dist/coordination/index.js +29 -4
  99. package/dist/coordination/schema.d.ts +176 -128
  100. package/dist/coordination/schema.js +197 -133
  101. package/dist/coordination/trace.d.ts +9 -11
  102. package/dist/coordination/trace.js +13 -15
  103. package/dist/core/DatabaseManager.d.ts +5 -8
  104. package/dist/core/DatabaseManager.js +38 -40
  105. package/dist/core/QueryProcessor.d.ts +7 -9
  106. package/dist/core/QueryProcessor.js +27 -34
  107. package/dist/core/QueryView.d.ts +17 -5
  108. package/dist/core/QueryView.js +6 -7
  109. package/dist/core/StoreManager.d.ts +14 -16
  110. package/dist/core/StoreManager.js +26 -25
  111. package/dist/core/ViewRegistry.d.ts +5 -5
  112. package/dist/core/ViewRegistry.js +4 -4
  113. package/dist/core/index.d.ts +18 -13
  114. package/dist/core/index.js +32 -26
  115. package/dist/core/openIDBWithTimeout.d.ts +38 -36
  116. package/dist/core/openIDBWithTimeout.js +57 -54
  117. package/dist/core/queryUtils.d.ts +45 -0
  118. package/dist/core/queryUtils.js +69 -0
  119. package/dist/core/storeContract.d.ts +145 -0
  120. package/dist/core/storeContract.js +12 -0
  121. package/dist/environment.d.ts +28 -0
  122. package/dist/environment.js +21 -0
  123. package/dist/errorCodes.d.ts +118 -101
  124. package/dist/errorCodes.js +277 -260
  125. package/dist/errors.d.ts +170 -165
  126. package/dist/errors.js +161 -151
  127. package/dist/index.d.ts +30 -27
  128. package/dist/index.js +90 -82
  129. package/dist/interfaces/index.d.ts +108 -133
  130. package/dist/interfaces/index.js +5 -4
  131. package/dist/keys/index.d.ts +27 -29
  132. package/dist/keys/index.js +59 -49
  133. package/dist/mutators/RecordingTransaction.d.ts +16 -16
  134. package/dist/mutators/RecordingTransaction.js +31 -37
  135. package/dist/mutators/Transaction.d.ts +18 -26
  136. package/dist/mutators/Transaction.js +14 -20
  137. package/dist/mutators/UndoManager.d.ts +122 -131
  138. package/dist/mutators/UndoManager.js +149 -155
  139. package/dist/mutators/defineMutators.d.ts +24 -37
  140. package/dist/mutators/defineMutators.js +14 -20
  141. package/dist/mutators/inverseOp.d.ts +12 -15
  142. package/dist/mutators/inverseOp.js +12 -15
  143. package/dist/mutators/mutateActions.d.ts +10 -9
  144. package/dist/mutators/mutateActions.js +1 -1
  145. package/dist/mutators/readerActions.d.ts +9 -8
  146. package/dist/mutators/readerActions.js +2 -2
  147. package/dist/mutators/undoApply.d.ts +31 -27
  148. package/dist/mutators/undoApply.js +26 -24
  149. package/dist/policy/index.d.ts +5 -3
  150. package/dist/policy/index.js +5 -3
  151. package/dist/policy/types.d.ts +105 -101
  152. package/dist/policy/types.js +67 -66
  153. package/dist/query/client.d.ts +32 -16
  154. package/dist/query/client.js +103 -72
  155. package/dist/query/types.d.ts +37 -60
  156. package/dist/query/types.js +13 -33
  157. package/dist/react/AbloProvider.d.ts +7 -11
  158. package/dist/react/AbloProvider.js +24 -17
  159. package/dist/react/context.d.ts +27 -146
  160. package/dist/react/context.js +9 -10
  161. package/dist/react/index.d.ts +41 -42
  162. package/dist/react/index.js +37 -38
  163. package/dist/react/internalContext.d.ts +17 -19
  164. package/dist/react/useAblo.d.ts +23 -22
  165. package/dist/react/useAblo.js +17 -15
  166. package/dist/react/useCurrentUserId.d.ts +8 -7
  167. package/dist/react/useCurrentUserId.js +8 -7
  168. package/dist/react/useErrorListener.d.ts +7 -7
  169. package/dist/react/useErrorListener.js +11 -12
  170. package/dist/react/useMutationFailureListener.d.ts +8 -8
  171. package/dist/react/useMutationFailureListener.js +9 -9
  172. package/dist/react/useMutators.d.ts +11 -11
  173. package/dist/react/useMutators.js +10 -4
  174. package/dist/react/useReactive.js +2 -3
  175. package/dist/react/useSyncStatus.d.ts +4 -6
  176. package/dist/react/useUndoScope.d.ts +7 -9
  177. package/dist/react/useUndoScope.js +3 -3
  178. package/dist/schema/coordination.d.ts +21 -25
  179. package/dist/schema/coordination.js +21 -25
  180. package/dist/schema/ddl.d.ts +43 -39
  181. package/dist/schema/ddl.js +75 -68
  182. package/dist/schema/ddlLock.d.ts +35 -0
  183. package/dist/schema/ddlLock.js +46 -0
  184. package/dist/schema/diff.d.ts +99 -61
  185. package/dist/schema/diff.js +43 -34
  186. package/dist/schema/field.d.ts +37 -42
  187. package/dist/schema/field.js +36 -49
  188. package/dist/schema/generate.d.ts +12 -12
  189. package/dist/schema/generate.js +12 -12
  190. package/dist/schema/index.d.ts +5 -4
  191. package/dist/schema/index.js +29 -21
  192. package/dist/schema/model.d.ts +121 -146
  193. package/dist/schema/model.js +24 -35
  194. package/dist/schema/openapi.d.ts +10 -9
  195. package/dist/schema/openapi.js +7 -1
  196. package/dist/schema/queries.d.ts +30 -32
  197. package/dist/schema/queries.js +24 -25
  198. package/dist/schema/relation.d.ts +89 -99
  199. package/dist/schema/relation.js +13 -13
  200. package/dist/schema/residency.d.ts +38 -0
  201. package/dist/schema/residency.js +30 -0
  202. package/dist/schema/roles.d.ts +45 -27
  203. package/dist/schema/roles.js +52 -21
  204. package/dist/schema/schema.d.ts +36 -45
  205. package/dist/schema/schema.js +42 -39
  206. package/dist/schema/select.d.ts +13 -13
  207. package/dist/schema/select.js +13 -13
  208. package/dist/schema/serialize.d.ts +36 -39
  209. package/dist/schema/serialize.js +27 -31
  210. package/dist/schema/sugar.d.ts +17 -32
  211. package/dist/schema/sugar.js +14 -29
  212. package/dist/schema/{sync-delta-row.d.ts → syncDeltaRow.d.ts} +27 -50
  213. package/dist/schema/syncDeltaRow.js +89 -0
  214. package/dist/schema/tenancy.d.ts +44 -46
  215. package/dist/schema/tenancy.js +46 -48
  216. package/dist/server/adapter.d.ts +58 -58
  217. package/dist/server/adapter.js +13 -14
  218. package/dist/server/commit.d.ts +60 -64
  219. package/dist/server/index.d.ts +9 -10
  220. package/dist/server/index.js +1 -1
  221. package/dist/server/readConfig.d.ts +70 -0
  222. package/dist/server/readConfig.js +8 -0
  223. package/dist/server/storageMode.d.ts +23 -0
  224. package/dist/server/storageMode.js +17 -0
  225. package/dist/source/adapter.d.ts +31 -26
  226. package/dist/source/adapter.js +10 -10
  227. package/dist/source/adapters/drizzle.d.ts +28 -23
  228. package/dist/source/adapters/drizzle.js +34 -28
  229. package/dist/source/adapters/kysely.d.ts +27 -25
  230. package/dist/source/adapters/kysely.js +28 -26
  231. package/dist/source/adapters/memory.d.ts +8 -7
  232. package/dist/source/adapters/memory.js +10 -9
  233. package/dist/source/adapters/prisma.d.ts +13 -12
  234. package/dist/source/adapters/prisma.js +27 -29
  235. package/dist/source/conformance.d.ts +18 -11
  236. package/dist/source/conformance.js +27 -19
  237. package/dist/source/connector.d.ts +31 -32
  238. package/dist/source/connector.js +30 -28
  239. package/dist/source/connectorProtocol.d.ts +160 -0
  240. package/dist/source/connectorProtocol.js +162 -0
  241. package/dist/source/contract.d.ts +26 -27
  242. package/dist/source/contract.js +28 -29
  243. package/dist/source/factory.d.ts +94 -0
  244. package/dist/source/factory.js +268 -0
  245. package/dist/source/index.d.ts +10 -462
  246. package/dist/source/index.js +17 -421
  247. package/dist/source/migrations.d.ts +9 -9
  248. package/dist/source/migrations.js +9 -9
  249. package/dist/source/next.d.ts +10 -11
  250. package/dist/source/next.js +7 -8
  251. package/dist/source/pushQueue.d.ts +70 -48
  252. package/dist/source/pushQueue.js +36 -29
  253. package/dist/source/signing.d.ts +88 -0
  254. package/dist/source/signing.js +159 -0
  255. package/dist/source/types.d.ts +351 -0
  256. package/dist/source/types.js +43 -0
  257. package/dist/stores/ObjectStore.d.ts +11 -12
  258. package/dist/stores/ObjectStore.js +34 -35
  259. package/dist/stores/ObjectStoreContract.d.ts +12 -15
  260. package/dist/stores/SyncActionStore.d.ts +8 -12
  261. package/dist/stores/SyncActionStore.js +77 -46
  262. package/dist/surface.d.ts +28 -21
  263. package/dist/surface.js +28 -20
  264. package/dist/sync/{BootstrapHelper.d.ts → BootstrapFetcher.d.ts} +37 -45
  265. package/dist/sync/{BootstrapHelper.js → BootstrapFetcher.js} +101 -80
  266. package/dist/sync/ConnectionManager.d.ts +47 -50
  267. package/dist/sync/ConnectionManager.js +74 -70
  268. package/dist/sync/NetworkProbe.d.ts +27 -31
  269. package/dist/sync/NetworkProbe.js +67 -72
  270. package/dist/sync/{HydrationCoordinator.d.ts → OnDemandLoader.d.ts} +49 -36
  271. package/dist/sync/{HydrationCoordinator.js → OnDemandLoader.js} +79 -54
  272. package/dist/sync/{AreaOfInterestManager.d.ts → SubscriptionManager.d.ts} +45 -59
  273. package/dist/sync/{AreaOfInterestManager.js → SubscriptionManager.js} +44 -52
  274. package/dist/sync/SyncWebSocket.d.ts +175 -250
  275. package/dist/sync/SyncWebSocket.js +431 -769
  276. package/dist/sync/awaitClaimGrant.d.ts +18 -18
  277. package/dist/sync/awaitClaimGrant.js +38 -30
  278. package/dist/sync/bootstrapApply.d.ts +70 -0
  279. package/dist/sync/bootstrapApply.js +73 -0
  280. package/dist/sync/commitFrames.d.ts +44 -0
  281. package/dist/sync/commitFrames.js +94 -0
  282. package/dist/sync/createClaimStream.d.ts +23 -22
  283. package/dist/sync/createClaimStream.js +108 -25
  284. package/dist/sync/createPresenceStream.d.ts +19 -18
  285. package/dist/sync/createPresenceStream.js +25 -26
  286. package/dist/sync/createSnapshot.d.ts +13 -17
  287. package/dist/sync/createSnapshot.js +20 -26
  288. package/dist/sync/credentialLifecycle.d.ts +175 -0
  289. package/dist/sync/credentialLifecycle.js +322 -0
  290. package/dist/sync/deltaPipeline.d.ts +113 -0
  291. package/dist/sync/deltaPipeline.js +261 -0
  292. package/dist/sync/groupChange.d.ts +113 -0
  293. package/dist/sync/groupChange.js +242 -0
  294. package/dist/sync/heartbeat.d.ts +63 -0
  295. package/dist/sync/heartbeat.js +91 -0
  296. package/dist/sync/participants.d.ts +27 -27
  297. package/dist/sync/schemas.d.ts +3 -2
  298. package/dist/sync/schemas.js +14 -10
  299. package/dist/sync/syncCursor.d.ts +40 -0
  300. package/dist/sync/syncCursor.js +55 -0
  301. package/dist/sync/syncPlan.d.ts +54 -0
  302. package/dist/sync/syncPlan.js +50 -0
  303. package/dist/sync/syncPosition.d.ts +54 -49
  304. package/dist/sync/syncPosition.js +57 -52
  305. package/dist/sync/wsFrameHandlers.d.ts +116 -0
  306. package/dist/sync/wsFrameHandlers.js +374 -0
  307. package/dist/testing/fixtures/bootstrap.d.ts +21 -17
  308. package/dist/testing/fixtures/bootstrap.js +12 -6
  309. package/dist/testing/fixtures/deltas.d.ts +31 -34
  310. package/dist/testing/fixtures/deltas.js +30 -33
  311. package/dist/testing/fixtures/models.d.ts +11 -10
  312. package/dist/testing/fixtures/models.js +12 -10
  313. package/dist/testing/helpers/{react-wrapper.d.ts → reactWrapper.d.ts} +13 -10
  314. package/dist/testing/helpers/{react-wrapper.js → reactWrapper.js} +15 -12
  315. package/dist/testing/helpers/{sync-engine-harness.d.ts → syncEngineHarness.d.ts} +17 -18
  316. package/dist/testing/helpers/{sync-engine-harness.js → syncEngineHarness.js} +14 -11
  317. package/dist/testing/helpers/wait.d.ts +13 -8
  318. package/dist/testing/helpers/wait.js +13 -8
  319. package/dist/testing/index.d.ts +4 -4
  320. package/dist/testing/index.js +3 -3
  321. package/dist/testing/mocks/MockMutationExecutor.d.ts +18 -17
  322. package/dist/testing/mocks/MockMutationExecutor.js +15 -14
  323. package/dist/testing/mocks/MockNetworkMonitor.d.ts +8 -8
  324. package/dist/testing/mocks/MockNetworkMonitor.js +8 -8
  325. package/dist/testing/mocks/MockSyncContext.d.ts +21 -34
  326. package/dist/testing/mocks/MockSyncContext.js +16 -45
  327. package/dist/testing/mocks/MockSyncStore.d.ts +14 -14
  328. package/dist/testing/mocks/MockSyncStore.js +11 -11
  329. package/dist/testing/mocks/MockWebSocket.d.ts +28 -24
  330. package/dist/testing/mocks/MockWebSocket.js +22 -21
  331. package/dist/transactions/TransactionQueue.d.ts +190 -221
  332. package/dist/transactions/TransactionQueue.js +424 -822
  333. package/dist/transactions/TransactionStore.d.ts +20 -0
  334. package/dist/transactions/TransactionStore.js +53 -0
  335. package/dist/transactions/UnconfirmedWrites.d.ts +82 -0
  336. package/dist/transactions/UnconfirmedWrites.js +104 -0
  337. package/dist/transactions/coalesceRules.d.ts +58 -0
  338. package/dist/transactions/coalesceRules.js +140 -0
  339. package/dist/transactions/commitPayload.d.ts +130 -0
  340. package/dist/transactions/commitPayload.js +143 -0
  341. package/dist/transactions/deltaConfirmation.d.ts +58 -0
  342. package/dist/transactions/deltaConfirmation.js +215 -0
  343. package/dist/transactions/optimisticApply.d.ts +49 -0
  344. package/dist/transactions/optimisticApply.js +65 -0
  345. package/dist/transactions/replayValidation.d.ts +99 -0
  346. package/dist/transactions/replayValidation.js +111 -0
  347. package/dist/types/global.d.ts +46 -41
  348. package/dist/types/global.js +20 -19
  349. package/dist/types/index.d.ts +74 -80
  350. package/dist/types/index.js +22 -27
  351. package/dist/types/modelData.d.ts +10 -0
  352. package/dist/types/modelData.js +9 -0
  353. package/dist/types/participant.d.ts +20 -0
  354. package/dist/types/participant.js +10 -0
  355. package/dist/types/streams.d.ts +216 -209
  356. package/dist/types/streams.js +7 -7
  357. package/dist/utils/asyncIterator.d.ts +25 -32
  358. package/dist/utils/asyncIterator.js +25 -32
  359. package/dist/utils/duration.d.ts +12 -15
  360. package/dist/utils/duration.js +12 -15
  361. package/dist/utils/mobxSetup.d.ts +53 -0
  362. package/dist/utils/{mobx-setup.js → mobxSetup.js} +44 -100
  363. package/dist/webhooks/events.d.ts +21 -16
  364. package/dist/webhooks/events.js +10 -8
  365. package/dist/webhooks/index.d.ts +5 -7
  366. package/dist/webhooks/index.js +5 -7
  367. package/dist/{schema/sync-delta-wire.d.ts → wire/delta.d.ts} +58 -41
  368. package/dist/wire/delta.js +114 -0
  369. package/dist/wire/errorEnvelope.d.ts +35 -27
  370. package/dist/wire/errorEnvelope.js +38 -32
  371. package/dist/wire/frames.d.ts +150 -67
  372. package/dist/wire/frames.js +48 -1
  373. package/dist/wire/index.d.ts +18 -13
  374. package/dist/wire/index.js +36 -13
  375. package/dist/wire/listEnvelope.d.ts +16 -23
  376. package/dist/wire/listEnvelope.js +7 -6
  377. package/dist/wire/protocol.d.ts +38 -0
  378. package/dist/wire/protocol.js +38 -0
  379. package/dist/wire/protocolVersion.d.ts +60 -0
  380. package/dist/wire/protocolVersion.js +67 -0
  381. package/docs/api-keys.md +4 -3
  382. package/docs/coordination.md +59 -0
  383. package/docs/examples/existing-python-backend.md +3 -3
  384. package/docs/identity.md +4 -4
  385. package/docs/integration-guide.md +1 -1
  386. package/docs/react.md +1 -1
  387. package/docs/sessions.md +5 -7
  388. package/package.json +24 -21
  389. package/dist/ai-sdk/coordinated-tool.d.ts +0 -101
  390. package/dist/ai-sdk/coordination-context.d.ts +0 -52
  391. package/dist/client/index.d.ts +0 -36
  392. package/dist/client/index.js +0 -33
  393. package/dist/config/index.d.ts +0 -10
  394. package/dist/config/index.js +0 -12
  395. package/dist/core/query-utils.d.ts +0 -34
  396. package/dist/core/query-utils.js +0 -59
  397. package/dist/interfaces/headless.d.ts +0 -95
  398. package/dist/interfaces/headless.js +0 -41
  399. package/dist/query/index.d.ts +0 -6
  400. package/dist/query/index.js +0 -5
  401. package/dist/realtime/index.d.ts +0 -10
  402. package/dist/realtime/index.js +0 -9
  403. package/dist/schema/plane.d.ts +0 -23
  404. package/dist/schema/plane.js +0 -19
  405. package/dist/schema/sync-delta-row.js +0 -103
  406. package/dist/schema/sync-delta-wire.js +0 -102
  407. package/dist/server/next.d.ts +0 -51
  408. package/dist/server/next.js +0 -47
  409. package/dist/server/read-config.d.ts +0 -67
  410. package/dist/server/read-config.js +0 -8
  411. package/dist/server/storage-mode.d.ts +0 -1
  412. package/dist/server/storage-mode.js +0 -18
  413. package/dist/source/connector-protocol.d.ts +0 -159
  414. package/dist/source/connector-protocol.js +0 -161
  415. package/dist/sync/OfflineFlush.d.ts +0 -9
  416. package/dist/sync/OfflineFlush.js +0 -22
  417. package/dist/sync/OfflineTransactionStore.d.ts +0 -37
  418. package/dist/sync/OfflineTransactionStore.js +0 -263
  419. package/dist/transactions/OptimisticEchoTracker.d.ts +0 -82
  420. package/dist/transactions/OptimisticEchoTracker.js +0 -104
  421. package/dist/transactions/index.d.ts +0 -16
  422. package/dist/transactions/index.js +0 -7
  423. package/dist/transactions/mutation-error-handler.d.ts +0 -5
  424. package/dist/transactions/mutation-error-handler.js +0 -39
  425. package/dist/utils/mobx-setup.d.ts +0 -42
@@ -1,25 +1,23 @@
1
1
  /**
2
- * Per-model client factory.
2
+ * Builds the typed client for a single schema model — the object reached as
3
+ * `ablo.<model>`.
3
4
  *
4
- * Mirrors Anthropic SDK's per-endpoint module pattern: each model client
5
- * has its own file, and the root client just instantiates
6
- * one per model. Extracted from `Ablo.ts` so the proxy logic is
7
- * testable in isolation and the constructor doesn't carry it.
8
- *
9
- * Each schema model gets one `ModelOperations<T, CreateInput>`
10
- * exposes the async server reads `retrieve` / `list`, the synchronous
11
- * local-graph snapshots `get` / `getAll` / `getCount`, the writes
12
- * `create` / `update` / `delete`, the coordination namespace `claim`
13
- * (`claim({ id })` plus `claim.state` / `claim.queue` / `claim.release` /
14
- * `claim.reorder`), and `onChange`. The factory returns a plain object; the
15
- * client assembles the `ablo.<model>` lookup table from these.
5
+ * Each schema model gets one {@link ModelOperations}: the async server reads
6
+ * `retrieve` and `list`, the synchronous local-graph snapshots `get`, `getAll`,
7
+ * and `getCount`, the writes `create`, `update`, and `delete`, the coordination
8
+ * namespace `claim` (callable as `claim({ id })`, plus `claim.state`,
9
+ * `claim.queue`, `claim.release`, and `claim.reorder`), `watch`, and `onChange`.
10
+ * The factory returns a plain object; the client assembles the `ablo.<model>`
11
+ * lookup table from one of these per model.
16
12
  */
17
13
  import { autorun } from 'mobx';
18
- import { AbloClaimedError, AbloValidationError, formatClaimedErrorMessage, toAbloError, } from '../errors.js';
14
+ import { AbloClaimedError, AbloStaleContextError, AbloValidationError, formatClaimedErrorMessage, toAbloError, } from '../errors.js';
19
15
  import { descriptionFromMeta } from '../coordination/schema.js';
20
16
  import { reconcileFunctionalUpdate, } from './functionalUpdate.js';
21
17
  import { Model, modelAsRow } from '../Model.js';
22
18
  import { toMs } from '../utils/duration.js';
19
+ import { LEASE_TTL_MS } from '../wire/protocol.js';
20
+ import { heartbeatCadenceMs, resolveHeartbeatOptions, startClaimHeartbeatLoop, } from './claimHeartbeatLoop.js';
23
21
  import { assertWriteOptions } from './writeOptionsSchema.js';
24
22
  import { ModelScope } from '../types/index.js';
25
23
  const modelClientMeta = new WeakMap();
@@ -34,15 +32,15 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
34
32
  throw new AbloValidationError(`Ablo: schema model "${schemaKey}" resolved to "${registeredModelName}", ` +
35
33
  'but no matching constructor was registered.', { code: 'model_not_registered' });
36
34
  }
37
- // The coordination plane (claims/claims) must speak the SAME wire dialect
38
- // as the commit plane: the lowercased TYPENAME (`task`), not the schema key
39
- // (`tasks`). The server's commit-time claim guard probes the lease store
40
- // with the commit op's model name; a lease recorded under the schema key
41
- // never collides with it — which silently disarmed the guard for every
42
- // model whose schema key differs from its typename (i.e. nearly all of
43
- // them, plural key vs singular typename). Public surfaces (Claim.
44
- // target.model) keep the schema key; only the wire/coordination targets
45
- // use this.
35
+ // The coordination plane must speak the same wire dialect as the commit
36
+ // plane: the lowercased typename (`task`), not the schema key (`tasks`). The
37
+ // server's commit-time claim guard probes the lease store with the commit
38
+ // operation's model name, so a lease recorded under the schema key never
39
+ // matches — which would silently disarm the guard for every model whose
40
+ // schema key differs from its typename (a plural key against a singular
41
+ // typename, i.e. nearly all of them). Public surfaces such as
42
+ // `Claim.target.model` keep the schema key; only the wire and coordination
43
+ // targets use this.
46
44
  const wireModel = registeredModelName.toLowerCase();
47
45
  // Last-line guarantee for the public surface: any rejection from a lower
48
46
  // layer (transport timeout, IndexedDB failure, a third-party throw) is
@@ -75,19 +73,22 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
75
73
  await syncClient.waitForConfirmation(model.getModelName(), model.id);
76
74
  };
77
75
  // Claims this proxy currently holds, keyed by entity id. Lets the flat
78
- // `release({ id })` and `update({ id, data })` find the lease + snapshot a `claim({ id })`
79
- // took no per-call handle. Released on dispose, explicit release, or TTL.
76
+ // `release({ id })` and `update({ id, data })` find the lease and snapshot a
77
+ // `claim({ id })` took, without a per-call handle. Released on dispose,
78
+ // explicit release, or TTL expiry.
80
79
  //
81
- // `target` / `reason` / `expiresAt` are kept alongside the lease so
80
+ // `target`, `reason`, and `expiresAt` are kept alongside the lease so
82
81
  // `claim.state` can synthesize a self-claim: the server excludes a holder's
83
- // own presence frames, so the local proxy is the ONLY place that knows "I
84
- // hold this." `expiresAt` is the client's best estimate from the requested
85
- // TTL (a genuine epoch-ms expiry, not a fabricated watermark), defaulting to
86
- // the server's keepalive lease window when no TTL was requested.
82
+ // own presence frames, so this proxy is the only place that knows the client
83
+ // holds the row. `expiresAt` is the client's best estimate from the requested
84
+ // TTL (a real epoch-millisecond expiry, not a fabricated watermark), defaulting
85
+ // to the server's keepalive lease window when no TTL was requested.
87
86
  const activeClaims = new Map();
88
- // Server keepalive lease window (Hub `LEASE_RENEW_TTL_MS`). The fallback
89
- // expiry estimate when a claim is taken without an explicit TTL.
90
- const DEFAULT_LEASE_TTL_MS = 90_000;
87
+ // Server keepalive lease window the same `LEASE_TTL_MS` the wire protocol
88
+ // declares, so the client's estimate and the server's lease cannot drift.
89
+ // This is the fallback expiry estimate when a claim is taken without an
90
+ // explicit TTL.
91
+ const DEFAULT_LEASE_TTL_MS = LEASE_TTL_MS;
91
92
  const isClaimHandle = (value) => typeof value === 'object' &&
92
93
  value !== null &&
93
94
  value.object === 'claim' &&
@@ -121,9 +122,9 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
121
122
  };
122
123
  const mutationOptions = (params) => {
123
124
  const { id: _id, data: _data, claim: _claim, ...rest } = params;
124
- // THE write-options schema — runtime twin of the compile-time params.
125
- // Catches plain-JS callers (`onStale: 'rejct'`) at the call site with
126
- // a typed error instead of a silent no-op or a server 400.
125
+ // The write-options schema — the runtime twin of the compile-time params.
126
+ // Catches plain-JavaScript callers (for example `onStale: 'rejct'`) at the
127
+ // call site with a typed error instead of a silent no-op or a server 400.
127
128
  assertWriteOptions(rest, `${schemaKey} write`);
128
129
  return rest;
129
130
  };
@@ -139,12 +140,12 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
139
140
  throw new AbloValidationError(`Model "${schemaKey}" was built without the collaboration runtime, so claim() is unavailable here. Claiming needs no per-model config — use the standard Ablo({ schema, apiKey }) client and every model is claimable.`, { code: 'model_claim_not_configured' });
140
141
  }
141
142
  const { id, ...options } = params;
142
- // Is someone ELSE already on this target? Read the local coordination
143
- // snapshot up front — it decides whether we'll need to re-read after the
144
- // claim (a free / already-mine target can't have changed under us).
143
+ // Is someone else already on this target? Read the local coordination
144
+ // snapshot up front — it decides whether a re-read is needed after the
145
+ // claim (a free or already-held target cannot have changed underneath us).
145
146
  const held = collaboration.state({ model: wireModel, id });
146
147
  const contended = !!held && held.heldBy !== collaboration.selfParticipantId;
147
- const failFast = options?.queue === false;
148
+ const failFast = options.queue === false;
148
149
  // Fail-fast (`queue: false`): if another participant already holds it,
149
150
  // reject now instead of queuing. Best-effort at the client (a racing
150
151
  // claim not yet synced into our snapshot slips through here) — the
@@ -152,13 +153,13 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
152
153
  // the loser's first write. For work-distribution dedup that's exactly
153
154
  // right: don't wait (that would double-process), skip.
154
155
  if (failFast && contended) {
155
- const claim = held ? claimContextFromClaim(held) : undefined;
156
+ const claim = claimContextFromClaim(held);
156
157
  throw new AbloClaimedError(formatClaimedErrorMessage({
157
158
  targetLabel: `${registeredModelName}/${id}`,
158
- heldBy: held?.heldBy,
159
+ heldBy: held.heldBy,
159
160
  claim,
160
- fallback: `${registeredModelName}/${id} is held by ${held?.heldBy ?? 'another participant'}.`,
161
- }), { code: 'entity_claimed', claims: claim ? [claim] : undefined });
161
+ fallback: `${registeredModelName}/${id} is held by ${held.heldBy ?? 'another participant'}.`,
162
+ }), { code: 'entity_claimed', claims: [claim] });
162
163
  }
163
164
  // Ensure the row exists locally before claiming.
164
165
  let model = objectPool.get(id);
@@ -169,65 +170,65 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
169
170
  if (!model) {
170
171
  throw new AbloValidationError(`Entity not found: ${registeredModelName}/${id}`, { code: 'entity_not_found' });
171
172
  }
172
- // Write-intent: enter the entity scope BEFORE acquiring the lease so the
173
- // holder's claim presence broadcasts to whoever is in this entity group
174
- // including a peer that subscribed just before us. Pinning before the
175
- // lease (rather than after) closes the subscribe-vs-broadcast race: the
176
- // server fans `broadcastPresenceChange` out at claim time, so we must be
177
- // in the group when `createClaim` lands. Awaited because the broadcast
178
- // ordering depends on it; still soft (the store swallows reconcile errors).
173
+ // Write intent: enter the entity scope before acquiring the lease so the
174
+ // holder's claim presence broadcasts to everyone in this entity group,
175
+ // including a peer that subscribed just before us. Pinning before the lease
176
+ // rather than after closes the subscribe-versus-broadcast race: the server
177
+ // fans presence out at claim time, so this client must be in the group when
178
+ // the claim lands. Awaited because the broadcast ordering depends on it;
179
+ // still best-effort (the store swallows reconcile errors).
179
180
  await collaboration.pinScope?.({ [schemaKey]: id });
180
- // Acquire the lease. Default (`queue` !== false) goes through the server's
181
- // fair FIFO queue `queue: true` resolves only once the lease is genuinely
182
- // ours, blocking behind any current holder, with no TOCTOU gap (the server
183
- // orders contenders). Fail-fast skips the queue: we already rejected an
184
- // observed conflict above, so this just records our lease.
181
+ // Acquire the lease. By default (`queue` is not false) this goes through the
182
+ // server's fair FIFO queue: `queue: true` resolves only once the lease is
183
+ // genuinely ours, blocking behind any current holder, with no check-then-act
184
+ // gap because the server orders contenders. Fail-fast skips the queue: an
185
+ // observed conflict was already rejected above, so this just records the lease.
185
186
  const lease = await collaboration.createClaim({
186
187
  target: {
187
188
  model: wireModel,
188
189
  id,
189
- ...(options?.field ? { field: options.field } : {}),
190
- ...(options?.path ? { path: options.path } : {}),
191
- ...(options?.range ? { range: options.range } : {}),
190
+ ...(options.field ? { field: options.field } : {}),
191
+ ...(options.path ? { path: options.path } : {}),
192
+ ...(options.range ? { range: options.range } : {}),
192
193
  ...(claimMeta(options) ? { meta: claimMeta(options) } : {}),
193
194
  },
194
- reason: options?.reason ?? 'editing',
195
- ttl: options?.ttl,
195
+ reason: options.reason ?? 'editing',
196
+ ttl: options.ttl,
196
197
  queue: !failFast,
197
- maxQueueDepth: options?.maxQueueDepth,
198
+ maxQueueDepth: options.maxQueueDepth,
198
199
  });
199
- // Only when we actually waited behind another holder can the row have
200
- // changed underneath us — re-read so the claimed snapshot reflects what
201
- // they committed before releasing. Two signals, either suffices:
202
- // - `lease.waited` — the server granted via `claim_granted`, i.e. we
203
- // provably queued behind a holder. Authoritative; works even when
204
- // the local snapshot is blind (claim fan-out is entity-scoped, so
205
- // org-wide-subscribed clients never observe peers' claims).
206
- // - `contended` — the local snapshot saw a holder up front. Kept for
207
- // the no-queue paths where no grant frame exists.
200
+ // Only when the claim actually waited behind another holder can the row have
201
+ // changed underneath us — re-read so the claimed snapshot reflects what that
202
+ // holder committed before releasing. Either of two signals suffices:
203
+ // - `lease.waited` — the server granted the claim after the client
204
+ // provably queued behind a holder. Authoritative; it works even when the
205
+ // local snapshot is blind, since claim fan-out is entity-scoped and a
206
+ // broadly-subscribed client never observes peers' claims.
207
+ // - `contended` — the local snapshot saw a holder up front. Kept for the
208
+ // no-queue paths, where no grant frame exists.
208
209
  if ((contended || lease.waited === true) && !failFast) {
209
- // `type: 'complete'` forces the round-trip: the hydration ledger
210
- // otherwise serves the LOCAL row for an already-hydrated id, and the
211
- // holder's final write may not have fanned out to us yet — the exact
212
- // stale-snapshot race this re-read exists to close.
210
+ // `type: 'complete'` forces the round-trip: the hydration ledger would
211
+ // otherwise serve the local row for an already-hydrated id, and the
212
+ // holder's final write may not have fanned out yet — the exact
213
+ // stale-snapshot race this re-read closes.
213
214
  await load({ where: [['id', id]], type: 'complete' });
214
215
  model = objectPool.get(id) ?? model;
215
216
  }
216
217
  const snapshot = collaboration.createSnapshot(schemaKey, id);
217
- const reason = options?.reason ?? 'editing';
218
+ const reason = options.reason ?? 'editing';
218
219
  // The self-claim's `ClaimTarget` mirrors what a peer's `claim.state` would
219
- // report (`state` maps `held.target.model` `type`), so a holder and a
220
- // peer see the SAME target.type for one row — the wire model token.
220
+ // report (`state` maps `held.target.model` to `type`), so a holder and a
221
+ // peer see the same `target.type` for one row — the wire model token.
221
222
  const selfTarget = {
222
223
  type: wireModel,
223
224
  id,
224
- ...(options?.field ? { field: options.field } : {}),
225
- ...(options?.path ? { path: options.path } : {}),
226
- ...(options?.range ? { range: options.range } : {}),
225
+ ...(options.field ? { field: options.field } : {}),
226
+ ...(options.path ? { path: options.path } : {}),
227
+ ...(options.range ? { range: options.range } : {}),
227
228
  ...(claimMeta(options) ? { meta: claimMeta(options) } : {}),
228
229
  };
229
- const expiresAt = Date.now() +
230
- (options?.ttl !== undefined ? toMs(options.ttl) : DEFAULT_LEASE_TTL_MS);
230
+ const ttlMs = options.ttl !== undefined ? toMs(options.ttl) : DEFAULT_LEASE_TTL_MS;
231
+ const expiresAt = Date.now() + ttlMs;
231
232
  activeClaims.set(id, {
232
233
  lease,
233
234
  snapshot,
@@ -238,24 +239,57 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
238
239
  const target = {
239
240
  type: schemaKey,
240
241
  id,
241
- ...(options?.field ? { field: options.field } : {}),
242
- ...(options?.path ? { path: options.path } : {}),
243
- ...(options?.range ? { range: options.range } : {}),
242
+ ...(options.field ? { field: options.field } : {}),
243
+ ...(options.path ? { path: options.path } : {}),
244
+ ...(options.range ? { range: options.range } : {}),
244
245
  ...(claimMeta(options) ? { meta: claimMeta(options) } : {}),
245
246
  };
246
- const release = () => releaseClaim(id);
247
+ // A beat resolves with the server's extended expiry; keep the local
248
+ // self-claim estimate in step so `claim.state` renders the real window,
249
+ // and surface every answer through `onHeartbeat` (pressure signal).
250
+ const heartbeat = async (beatOptions) => {
251
+ if (!lease.heartbeat) {
252
+ throw new AbloValidationError('This claim handle has no heartbeat wiring, which the standard Ablo({ schema, apiKey }) client provides on every claim. This appears only when a claim is minted through an internal path that predates heartbeats.', { code: 'claim_not_wired' });
253
+ }
254
+ const resolved = resolveHeartbeatOptions(beatOptions);
255
+ const beat = await lease.heartbeat({
256
+ ttl: resolved.ttl ?? options.ttl,
257
+ ...(resolved.details !== undefined ? { details: resolved.details } : {}),
258
+ });
259
+ const held = activeClaims.get(id);
260
+ if (held)
261
+ held.expiresAt = beat.expiresAt;
262
+ options.onHeartbeat?.(beat);
263
+ return beat;
264
+ };
265
+ // Opt-in auto-heartbeat: the loop beats until release, and a definitive
266
+ // loss stops it and surfaces through `onHeartbeatLost`.
267
+ const stopHeartbeatLoop = options.heartbeat
268
+ ? startClaimHeartbeatLoop({
269
+ beat: () => heartbeat(),
270
+ intervalMs: heartbeatCadenceMs(ttlMs, options.heartbeat),
271
+ ...(options.onHeartbeatLost
272
+ ? { onLost: options.onHeartbeatLost }
273
+ : {}),
274
+ })
275
+ : undefined;
276
+ const release = () => {
277
+ stopHeartbeatLoop?.();
278
+ return releaseClaim(id);
279
+ };
247
280
  return {
248
281
  object: 'claim',
249
282
  id: lease.id,
250
283
  readAt: snapshot.stamp,
251
284
  target,
252
285
  reason,
253
- ...(options?.description ? { description: options.description } : {}),
286
+ ...(options.description ? { description: options.description } : {}),
254
287
  data: modelAsRow(model),
255
288
  release,
256
289
  revoke: () => {
257
290
  void release();
258
291
  },
292
+ heartbeat,
259
293
  [Symbol.asyncDispose]: release,
260
294
  };
261
295
  };
@@ -266,15 +300,15 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
266
300
  // are the same object.
267
301
  const claimApi = Object.assign(guard(claim), {
268
302
  state(params) {
269
- // Read-interest: a passive observer subscribing to a row's claim state
270
- // must enter that row's entity scope, or it sits on `org:`/`user:`
271
- // groups only and never receives the holder's entity-scoped claim
272
- // presence. Soft + fire-and-forget — never blocks or rejects the read.
303
+ // Read interest: a passive observer of a row's claim state must enter that
304
+ // row's entity scope, or it sits only on broader `org:`/`user:` groups and
305
+ // never receives the holder's entity-scoped claim presence. Best-effort
306
+ // and fire-and-forget — it never blocks or rejects the read.
273
307
  void collaboration?.enterScope?.({ [schemaKey]: params.id });
274
- // Self-awareness: the server excludes a holder's OWN presence frames and
275
- // the client skips them, so `state` returns null for a row WE hold.
276
- // Synthesize the active claim for self from the stored lease so the
277
- // holder sees its own claim (the JSDoc contract on `claim.state`).
308
+ // Self-awareness: the server excludes a holder's own presence frames and
309
+ // the client skips them, so `state` would return null for a row this client
310
+ // holds. Synthesize the active claim from the stored lease so the holder
311
+ // sees its own claim, honoring the documented contract on `claim.state`.
278
312
  const own = activeClaims.get(params.id);
279
313
  if (own) {
280
314
  return {
@@ -303,10 +337,10 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
303
337
  });
304
338
  const operations = {
305
339
  retrieve: guard(async (params) => {
306
- // Read-interest enrolment: READ a row enter its entity scope, so a
307
- // Node/agent client lands in the same group the holder's claim presence
308
- // fans out on and `claim.state`/`claim.queue` report peers. Soft +
309
- // fire-and-forget — never make the read reject or slower.
340
+ // Read-interest enrolment: reading a row enters its entity scope, so a
341
+ // client lands in the same group the holder's claim presence fans out
342
+ // on and `claim.state`/`claim.queue` report peers. Best-effort and
343
+ // fire-and-forget — it never makes the read reject or run slower.
310
344
  void collaboration?.enterScope?.({ [schemaKey]: params.id });
311
345
  const rows = await load({
312
346
  ...params,
@@ -315,9 +349,8 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
315
349
  });
316
350
  return rows[0];
317
351
  }),
318
- // NB: no auto scope enrolment on bulk `list`/`getAll` that would
319
- // subscribe to an unbounded set of rows' entity groups. Bulk-list scope
320
- // enrolment is a deliberate follow-up (a bounded, opt-in policy).
352
+ // No automatic scope enrolment on bulk `list`/`getAll`: that would subscribe
353
+ // to an unbounded set of rows' entity groups.
321
354
  list: guard(load),
322
355
  get(id) {
323
356
  return objectPool.get(id);
@@ -338,8 +371,9 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
338
371
  if (options?.filter) {
339
372
  result = result.filter(options.filter);
340
373
  }
341
- if (options?.orderBy) {
342
- const [field, dir] = Object.entries(options.orderBy)[0];
374
+ const orderEntry = options?.orderBy ? Object.entries(options.orderBy)[0] : undefined;
375
+ if (orderEntry) {
376
+ const [field, dir] = orderEntry;
343
377
  result = [...result].sort((a, b) => {
344
378
  const av = a[field];
345
379
  const bv = b[field];
@@ -367,11 +401,11 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
367
401
  if (!collaboration) {
368
402
  throw new AbloValidationError(`Model "${schemaKey}" was built without the collaboration runtime, so claim() is unavailable here. Claiming needs no per-model config — use the standard Ablo({ schema, apiKey }) client and every model is claimable.`, { code: 'model_claim_not_configured' });
369
403
  }
370
- // Write-intent: enter the new row's entity scope BEFORE acquiring the
371
- // create-claim so the holder's claim presence broadcasts to whoever is
372
- // already in this entity group (closing the subscribe-vs-broadcast
404
+ // Write intent: enter the new row's entity scope before acquiring the
405
+ // create-claim so the holder's claim presence broadcasts to everyone
406
+ // already in this entity group (closing the subscribe-versus-broadcast
373
407
  // race — see `takeClaim`). Released with the lease in the `finally`
374
- // below. Awaited for broadcast ordering; still soft.
408
+ // below. Awaited for broadcast ordering; still best-effort.
375
409
  await collaboration.pinScope?.({ [schemaKey]: id });
376
410
  autoLease = await collaboration.createClaim({
377
411
  target: {
@@ -388,10 +422,10 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
388
422
  maxQueueDepth: claim.maxQueueDepth,
389
423
  });
390
424
  }
391
- // Default `organizationId` from the client's identity exactly like the
392
- // mutator path (`buildModelForCreate`) — without this, a caller that
393
- // omits it creates an org-unscoped row on one write door but not the
394
- // other. An explicit value in `data` still wins via the spread.
425
+ // Default `organizationId` from the client's identity, matching the other
426
+ // write path — without this, a caller that omits it would create an
427
+ // org-unscoped row on one write path but not the other. An explicit value
428
+ // in `data` still wins via the spread.
395
429
  const orgDefault = params.data.organizationId ??
396
430
  syncClient.getOrganizationId();
397
431
  const model = new ModelClass({
@@ -477,7 +511,7 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
477
511
  return await operations.update({ ...params, claim: handle });
478
512
  }
479
513
  finally {
480
- await handle.release?.();
514
+ await handle.release();
481
515
  }
482
516
  }
483
517
  const { id } = params;
@@ -511,10 +545,10 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
511
545
  ...opts,
512
546
  ...(handle ? { claim: { id: handle.id } } : {}),
513
547
  };
514
- // Local user update: `applyChanges` keeps change tracking ON so
515
- // the edited fields land in `modifiedProperties` and actually get
516
- // sent to the server. (`updateFromData` is the hydration path and
517
- // would discard the tracking empty `input: {}` no-op mutation.)
548
+ // Local user update: `applyChanges` keeps change tracking on so the
549
+ // edited fields land in `modifiedProperties` and are actually sent to
550
+ // the server. (`updateFromData` is the hydration path and would discard
551
+ // the tracking, producing an empty `input: {}` no-op mutation.)
518
552
  model.applyChanges(params.data);
519
553
  syncClient.update(model, effective);
520
554
  await waitForMutation(model, effective);
@@ -533,17 +567,17 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
533
567
  await operations.delete({ ...params, claim: handle });
534
568
  }
535
569
  finally {
536
- await handle.release?.();
570
+ await handle.release();
537
571
  }
538
572
  return;
539
573
  }
540
574
  const { id } = params;
541
575
  const model = objectPool.get(id);
542
- // Idempotent delete (AIP-135 for client-assigned ids): "ensure absent". A
543
- // row that isn't in our replicated view is already gone from our
544
- // perspective, so a delete is a no-op success not an `entity_not_found`
545
- // error. This matches the HTTP client (which never threw here) and makes
546
- // delete safe to retry / race (two actors deleting the same row).
576
+ // Idempotent delete: "ensure absent". A row that isn't in this client's
577
+ // replicated view is already gone from its perspective, so a delete is a
578
+ // no-op success rather than an `entity_not_found` error. This matches the
579
+ // HTTP client and makes delete safe to retry or race (two actors deleting
580
+ // the same row).
547
581
  if (!model)
548
582
  return;
549
583
  const claimed = activeClaims.get(id);
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Support for the endpoint-string form of `apiKey`.
3
+ *
4
+ * With `Ablo({ schema, apiKey: '/api/ablo-session' })`, you point the client at
5
+ * your own session-mint route and the client owns the exchange: it POSTs to the
6
+ * endpoint, parses the minted token, keeps it fresh, and classifies failures
7
+ * onto the resolver's three outcomes. The string form covers the common case;
8
+ * the function form of `apiKey` remains the escape hatch for custom headers,
9
+ * bodies, or non-HTTP mints.
10
+ *
11
+ * The form is detected by prefix: a string starting with `/`, `http://`, or
12
+ * `https://` is an endpoint, and anything else is a literal key. Ablo keys are
13
+ * prefixed (`sk_`/`pk_`/`ek_`/`rk_`), so the two shapes cannot collide. An
14
+ * `ABLO_API_KEY` environment value is never treated as an endpoint — it is
15
+ * always a literal key (see `resolveApiKey`).
16
+ *
17
+ * Wire contract:
18
+ * POST <endpoint> (same-origin, `credentials: 'include'` so cookies flow)
19
+ * → 200 `{ token, expiresAt? }` a fresh short-lived `ek_`/`rk_`
20
+ * → 200 `{ token: null }` or 401/403 the login itself is gone (sign out)
21
+ * → anything else transient — retry, do not sign out
22
+ *
23
+ * The three-way mapping is the reason to build this in. Hand-written token
24
+ * fetchers routinely get it wrong — mapping any non-OK response to `null` signs
25
+ * the user out on a 500. Encoded here once, every consumer inherits the correct
26
+ * split between a terminal sign-out and a transient retry.
27
+ */
28
+ /**
29
+ * An async callable that resolves the current credential. It serves two uses:
30
+ * credential rotation (for example against AWS STS, GCP IAM, or Vault) and the
31
+ * short-lived per-user browser path (minting a fresh `ek_`/`rk_` from the
32
+ * signed-in session). It is re-exported from `./auth` so existing import paths
33
+ * keep working, and defined here so the resolver it types has no import cycle.
34
+ *
35
+ * The contract has three outcomes: resolve a token; resolve `null` when the
36
+ * login itself is gone (terminal — the credential lifecycle treats this as
37
+ * `session_expired` and signs out); or throw on a transient failure (back off
38
+ * and retry, without signing out). A long-lived static `apiKey` string needs
39
+ * none of this and is used as-is.
40
+ */
41
+ export type ApiKeySetter = () => Promise<string | null>;
42
+ /**
43
+ * Is this `apiKey` string a session-mint endpoint rather than a literal key?
44
+ * Prefix rule: `/relative/path`, `http://…`, or `https://…`.
45
+ */
46
+ export declare function isCredentialEndpoint(value: string): boolean;
47
+ /**
48
+ * Builds the resolver behind an endpoint-string `apiKey`. It follows the
49
+ * {@link ApiKeySetter} contract end to end:
50
+ * - resolves the minted token string on success;
51
+ * - resolves `null` when the login is gone (a 401 or 403, or an explicit
52
+ * `{ token: null }`) — terminal, so the client signs out;
53
+ * - throws on anything transient (a network failure, a 5xx or 429, or a
54
+ * malformed response) — so the lifecycle backs off and retries without
55
+ * signing out.
56
+ *
57
+ * A relative endpoint invoked on a server (where `fetch` has no origin) throws,
58
+ * which is transient by contract; the credential lifecycle translates that exact
59
+ * failure into an actionable "use an absolute URL server-side" warning.
60
+ */
61
+ export declare function createEndpointCredentialResolver(endpoint: string): ApiKeySetter;
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Support for the endpoint-string form of `apiKey`.
3
+ *
4
+ * With `Ablo({ schema, apiKey: '/api/ablo-session' })`, you point the client at
5
+ * your own session-mint route and the client owns the exchange: it POSTs to the
6
+ * endpoint, parses the minted token, keeps it fresh, and classifies failures
7
+ * onto the resolver's three outcomes. The string form covers the common case;
8
+ * the function form of `apiKey` remains the escape hatch for custom headers,
9
+ * bodies, or non-HTTP mints.
10
+ *
11
+ * The form is detected by prefix: a string starting with `/`, `http://`, or
12
+ * `https://` is an endpoint, and anything else is a literal key. Ablo keys are
13
+ * prefixed (`sk_`/`pk_`/`ek_`/`rk_`), so the two shapes cannot collide. An
14
+ * `ABLO_API_KEY` environment value is never treated as an endpoint — it is
15
+ * always a literal key (see `resolveApiKey`).
16
+ *
17
+ * Wire contract:
18
+ * POST <endpoint> (same-origin, `credentials: 'include'` so cookies flow)
19
+ * → 200 `{ token, expiresAt? }` a fresh short-lived `ek_`/`rk_`
20
+ * → 200 `{ token: null }` or 401/403 the login itself is gone (sign out)
21
+ * → anything else transient — retry, do not sign out
22
+ *
23
+ * The three-way mapping is the reason to build this in. Hand-written token
24
+ * fetchers routinely get it wrong — mapping any non-OK response to `null` signs
25
+ * the user out on a 500. Encoded here once, every consumer inherits the correct
26
+ * split between a terminal sign-out and a transient retry.
27
+ */
28
+ /**
29
+ * Is this `apiKey` string a session-mint endpoint rather than a literal key?
30
+ * Prefix rule: `/relative/path`, `http://…`, or `https://…`.
31
+ */
32
+ export function isCredentialEndpoint(value) {
33
+ return value.startsWith('/') || /^https?:\/\//i.test(value);
34
+ }
35
+ /**
36
+ * Builds the resolver behind an endpoint-string `apiKey`. It follows the
37
+ * {@link ApiKeySetter} contract end to end:
38
+ * - resolves the minted token string on success;
39
+ * - resolves `null` when the login is gone (a 401 or 403, or an explicit
40
+ * `{ token: null }`) — terminal, so the client signs out;
41
+ * - throws on anything transient (a network failure, a 5xx or 429, or a
42
+ * malformed response) — so the lifecycle backs off and retries without
43
+ * signing out.
44
+ *
45
+ * A relative endpoint invoked on a server (where `fetch` has no origin) throws,
46
+ * which is transient by contract; the credential lifecycle translates that exact
47
+ * failure into an actionable "use an absolute URL server-side" warning.
48
+ */
49
+ export function createEndpointCredentialResolver(endpoint) {
50
+ return async () => {
51
+ // `fetch()` rejections (offline, DNS, a relative URL on a server) propagate
52
+ // as-is: a throw is the transient signal in the resolver contract.
53
+ const res = await fetch(endpoint, {
54
+ method: 'POST',
55
+ credentials: 'include',
56
+ });
57
+ // The login itself is gone — terminal. Only these two statuses sign out.
58
+ if (res.status === 401 || res.status === 403)
59
+ return null;
60
+ // 5xx / 429 / anything unexpected: the login may be perfectly valid —
61
+ // transient, retry later.
62
+ if (!res.ok) {
63
+ throw new Error(`credential endpoint ${endpoint} answered ${res.status} — transient, will retry`);
64
+ }
65
+ let body;
66
+ try {
67
+ body = await res.json();
68
+ }
69
+ catch {
70
+ throw new Error(`credential endpoint ${endpoint} returned non-JSON — expected { token }`);
71
+ }
72
+ // Distinguish "explicitly signed out" (`{ token: null }`) from "not a mint
73
+ // endpoint at all" (no `token` key). A misconfiguration must fail loudly and
74
+ // transiently, never as a silent sign-out.
75
+ if (typeof body !== 'object' || body === null || !('token' in body)) {
76
+ throw new Error(`credential endpoint ${endpoint} returned no \`token\` field — expected { token }`);
77
+ }
78
+ const token = body.token;
79
+ if (token === null || token === undefined)
80
+ return null;
81
+ if (typeof token !== 'string') {
82
+ throw new Error(`credential endpoint ${endpoint} returned a non-string \`token\``);
83
+ }
84
+ return token;
85
+ };
86
+ }