@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,11 +1,11 @@
1
1
  /**
2
- * Ablo — The one-liner consumer API.
3
- *
4
- * Hides all internal wiring (ObjectPool, Database, SyncClient, WebSocket,
5
- * bootstrap, offline queue, DI adapters) behind a single function call.
2
+ * `Ablo`the one-call entry point to the sync engine client. It hides the
3
+ * internal wiring — the object pool, local database, sync client, WebSocket,
4
+ * bootstrap, and offline queue behind a single function that returns a typed
5
+ * client with one property per model in your schema.
6
6
  *
7
7
  * Usage:
8
- * import { Ablo } from '@abloatai/ablo/client';
8
+ * import { Ablo } from '@abloatai/ablo';
9
9
  * import { schema } from './schema';
10
10
  *
11
11
  * const sync = Ablo({ schema, apiKey: process.env.ABLO_API_KEY });
@@ -18,21 +18,18 @@
18
18
  * });
19
19
  * await sync.reports.delete({ id: reportId });
20
20
  */
21
- import { z } from 'zod';
22
- import { baseFieldsSchema } from '../schema/schema.js';
23
- import { schemaHash } from '../schema/serialize.js';
24
- import { AbloError, AbloAuthenticationError, AbloConnectionError, AbloValidationError, AbloNotFoundError, translateHttpError, toAbloError, claimedError } from '../errors.js';
21
+ import { AbloAuthenticationError, AbloConnectionError, AbloValidationError, AbloNotFoundError, translateHttpError, toAbloError, claimedError } from '../errors.js';
25
22
  import { descriptionFromMeta } from '../coordination/schema.js';
26
- import { LoadStrategy, PropertyType } from '../types/index.js';
27
23
  import { initSyncEngine } from '../context.js';
28
24
  import { noopObservability, browserOnlineStatus, defaultSessionErrorDetector, noopAnalytics, } from '../SyncEngineContext.js';
29
25
  import { alwaysOnline } from '../adapters/alwaysOnline.js';
30
26
  import { validateAbloOptions } from './validateAbloOptions.js';
27
+ import { InstanceCache } from '../InstanceCache.js';
28
+ import {} from '../auth/index.js';
31
29
  import { mintSession } from './sessionMint.js';
32
30
  import { createAuthCredentialSource } from '../auth/credentialSource.js';
33
31
  import { createInternalComponents } from './createInternalComponents.js';
34
32
  import { resolveParticipantIdentity } from './identity.js';
35
- import { Model } from '../Model.js';
36
33
  import { BaseSyncedStore } from '../BaseSyncedStore.js';
37
34
  import { createPresenceStream } from '../sync/createPresenceStream.js';
38
35
  import { createClaimStream } from '../sync/createClaimStream.js';
@@ -41,688 +38,35 @@ import { createSnapshot } from '../sync/createSnapshot.js';
41
38
  import { reconcileFunctionalUpdate } from './functionalUpdate.js';
42
39
  import { createParticipantManager } from '../sync/participants.js';
43
40
  import { createProtocolClient, } from './ApiClient.js';
44
- // Value import is cycle-safe: httpClient.js only value-imports ApiClient.js,
45
- // which imports this module type-only.
41
+ // Value import is cycle-safe: httpClient.js and ApiClient.js take the client
42
+ // types from the `options`/`resourceTypes` leaves, never from this module.
46
43
  import { createAbloHttpClient, } from './httpClient.js';
47
44
  import { assertBrowserSafety, readProcessEnv, resolveApiKey, resolveApiKeyValue, resolveAuthToken, resolveBaseURL, resolveBootstrapBaseUrl, resolveDatabaseUrl, warnIfCliKeyMismatch, warnIfDatabaseUrlEnvIgnored, warnIfDatabaseUrlDeprecated, } from './auth.js';
48
45
  import { registerDataSource } from './registerDataSource.js';
49
- import { shouldUseInMemoryPersistence, } from './persistence.js';
46
+ import { shouldUseInMemoryPersistence } from './persistence.js';
47
+ import { deriveConfigFromSchema } from './schemaConfig.js';
48
+ import { registerModelsFromSchema } from './modelRegistration.js';
49
+ import { createConsoleLogger, resolveLogLevel } from './consoleLogger.js';
50
+ import { createDefaultMutationExecutor } from './wsMutationExecutor.js';
51
+ export { computeFKDepthPriority } from './schemaConfig.js';
50
52
  import { createModelProxy } from './createModelProxy.js';
51
53
  import { assertWriteOptions } from './writeOptionsSchema.js';
52
- // ── Config derivation from schema ─────────────────────────────────────────
53
- /**
54
- * Compute a create-priority map from schema `belongsTo` relations using
55
- * Tarjan's strongly-connected-components algorithm.
56
- *
57
- * The FK graph has an edge `child → parent` for every `belongsTo`. Tarjan
58
- * runs a single linear DFS that simultaneously (a) detects cycles by
59
- * grouping mutually-reachable nodes into SCCs and (b) emits those SCCs
60
- * in reverse topological order of the condensation graph. In this edge
61
- * convention a "sink" SCC has no outgoing edges — i.e. no parents — so
62
- * it is an *FK root* (`organizations`, `themes`, etc.). Tarjan emits
63
- * roots first and leaves last, exactly the order in which rows must be
64
- * inserted to satisfy FK constraints.
65
- *
66
- * Priorities are assigned by emit order: SCC #0 → 10, SCC #1 → 20, …
67
- * Members of the same SCC share a priority, so insertion order wins the
68
- * tiebreak inside a cycle (this matters for cyclic schemas like
69
- * `slideDecks ↔ layouts`, where one direction is the user's chosen
70
- * "soft" edge — only the consumer's mutator sequence knows which one).
71
- *
72
- * This algorithm is iteration-order-independent: starting the DFS from
73
- * any node yields the same SCC partitioning, and SCCs always come out
74
- * in valid topological order. The previous DFS-with-memoization
75
- * heuristic broke under cycles by treating the back-edge as depth 0,
76
- * which made priorities depend on which node the walk happened to
77
- * enter the cycle at.
78
- *
79
- * Schema authors can mark one side of a cycle with
80
- * `belongsTo(target, fk, { defer: true })`. Those edges are excluded
81
- * from the dependency graph entirely, which deterministically breaks
82
- * the cycle and turns the SCC into a chain — the marked child gets a
83
- * strictly higher priority than its parent instead of being tied with
84
- * it. Pair with a Postgres `DEFERRABLE INITIALLY DEFERRED` constraint
85
- * if you want the database side of the cycle to also relax. See
86
- * {@link BelongsToOptions.defer}.
87
- *
88
- * The returned map is keyed by {@link ModelDef.typename} (falling back
89
- * to the schema key), because that is what `Model.getModelName()`
90
- * returns at transaction time — keying by schema key would silently
91
- * miss the lookup and every model would fall through to
92
- * `defaultCreatePriority`.
93
- *
94
- * Reference: Tarjan, R. (1972), "Depth-first search and linear graph
95
- * algorithms." Linear in V + E.
96
- */
97
- export function computeFKDepthPriority(schema) {
98
- // schemaKey → typename (wire name used at transaction time)
99
- const keyToTypename = new Map();
100
- for (const [key, def] of Object.entries(schema.models)) {
101
- keyToTypename.set(key, def.typename ?? key);
102
- }
103
- // Adjacency: schemaKey → parent schema keys pulled from `belongsTo`.
104
- // Parents not in the schema (e.g. external types) are dropped so the
105
- // graph stays closed. Edges marked `{ defer: true }` are also
106
- // dropped — the schema author has declared this side of a cycle to
107
- // be the "soft" one (insert with null FK, patch later), so the
108
- // dependency-graph walker treats it as if the edge weren't there.
109
- // That breaks the cycle deterministically and lets the other side
110
- // become a strict topological predecessor.
111
- const parentsOf = new Map();
112
- for (const [key, def] of Object.entries(schema.models)) {
113
- const out = [];
114
- for (const rel of Object.values(def.relations)) {
115
- if (rel.type !== 'belongsTo')
116
- continue;
117
- if (!keyToTypename.has(rel.target))
118
- continue;
119
- if (rel.options?.defer === true)
120
- continue;
121
- out.push(rel.target);
122
- }
123
- parentsOf.set(key, out);
124
- }
125
- // Tarjan SCC bookkeeping
126
- const dfsIndex = new Map();
127
- const lowlink = new Map();
128
- const onStack = new Set();
129
- const stack = [];
130
- const sccs = [];
131
- let counter = 0;
132
- function strongconnect(v) {
133
- dfsIndex.set(v, counter);
134
- lowlink.set(v, counter);
135
- counter++;
136
- stack.push(v);
137
- onStack.add(v);
138
- for (const w of parentsOf.get(v) ?? []) {
139
- if (!dfsIndex.has(w)) {
140
- strongconnect(w);
141
- lowlink.set(v, Math.min(lowlink.get(v), lowlink.get(w)));
142
- }
143
- else if (onStack.has(w)) {
144
- // Back-edge into the active DFS path — w is in the same SCC as v.
145
- lowlink.set(v, Math.min(lowlink.get(v), dfsIndex.get(w)));
146
- }
147
- }
148
- // v is the root of an SCC: pop everything down to v inclusive.
149
- if (lowlink.get(v) === dfsIndex.get(v)) {
150
- const component = [];
151
- let w;
152
- do {
153
- w = stack.pop();
154
- onStack.delete(w);
155
- component.push(w);
156
- } while (w !== v);
157
- sccs.push(component);
158
- }
159
- }
160
- for (const key of keyToTypename.keys()) {
161
- if (!dfsIndex.has(key))
162
- strongconnect(key);
163
- }
164
- // Tarjan emits SCCs in reverse topological order of the condensation.
165
- // In our edge convention (child→parent), reverse-topo of the
166
- // condensation means root-SCCs (no outgoing edges = no parents)
167
- // first, leaf-SCCs (deepest descendants) last. We could just use
168
- // emit-order as the priority — but that gives independent sibling
169
- // SCCs different priorities, which is semantically wrong: siblings
170
- // don't depend on each other and shouldn't be ordered relative to
171
- // each other.
172
- //
173
- // Instead, do one more pass to compute *longest-path depth* on the
174
- // condensation DAG: depth(SCC) = max(depth(parent SCC)) + 1, or 0
175
- // for SCCs with no in-schema parents. SCCs at the same depth get
176
- // the same priority — siblings stay tied, insertion order in the
177
- // queue breaks the tie. Priority = (depth + 1) * 10.
178
- //
179
- // We can compute this in a single pass over the SCCs because
180
- // Tarjan's emit-order *is* a valid topological order of the
181
- // condensation: when we process sccs[i], every parent SCC has
182
- // already been assigned a depth.
183
- const nodeToSccIdx = new Map();
184
- sccs.forEach((scc, i) => {
185
- for (const node of scc)
186
- nodeToSccIdx.set(node, i);
187
- });
188
- const sccDepth = new Map();
189
- sccs.forEach((scc, i) => {
190
- let maxParentDepth = -1;
191
- for (const node of scc) {
192
- for (const parent of parentsOf.get(node) ?? []) {
193
- const parentSccIdx = nodeToSccIdx.get(parent);
194
- if (parentSccIdx === undefined)
195
- continue;
196
- if (parentSccIdx === i)
197
- continue; // intra-SCC edge — not a dep
198
- const d = sccDepth.get(parentSccIdx);
199
- if (d !== undefined && d > maxParentDepth)
200
- maxParentDepth = d;
201
- }
202
- }
203
- sccDepth.set(i, maxParentDepth + 1);
204
- });
205
- const out = new Map();
206
- sccs.forEach((scc, i) => {
207
- const priority = (sccDepth.get(i) + 1) * 10;
208
- for (const key of scc) {
209
- out.set(keyToTypename.get(key), priority);
210
- }
211
- });
212
- return out;
213
- }
214
- function deriveConfigFromSchema(schema) {
215
- // Commit payload projection is done directly inside `TransactionQueue`
216
- // — see `projectCommitPayload` there. Each model's field metadata
217
- // rides on `ModelRegistry` (populated by `registerModelsFromSchema`),
218
- // so there's no config-layer shim: the queue asks the registry for
219
- // the declared fields and serializes accordingly.
220
- return {
221
- modelCreatePriority: computeFKDepthPriority(schema),
222
- defaultCreatePriority: 40,
223
- defaultNonCreatePriority: 50,
224
- essentialFields: {},
225
- classNameFallbackMap: {},
226
- // Hash this client's schema once so bootstrap can detect drift against the
227
- // server's active hash (same `schemaHash` the CLI push + server compute).
228
- expectedSchemaHash: schemaHash(schema),
229
- };
230
- }
231
- // ── Auto model registration from schema ───────────────────────────────────
232
- function registerModelsFromSchema(schema, registry) {
233
- registry.startBatch();
234
- for (const [schemaKey, modelDef] of Object.entries(schema.models)) {
235
- // Use typename as the model name — this is the wire-format name that
236
- // the server sends in bootstrap responses and sync deltas. The pool's
237
- // typeIndex, the ModelRegistry, and getModelName() all use this name.
238
- // Schema key (camelCase plural) is only for the consumer-facing proxy API.
239
- const modelName = modelDef.typename ?? schemaKey;
240
- // Collect JSON sub-property fields to generate ${field}Json getters
241
- const jsonSubFields = [];
242
- for (const [fieldName, zodType] of Object.entries(modelDef.shape)) {
243
- const inner = unwrapZodType(zodType);
244
- if (isZodObject(inner)) {
245
- jsonSubFields.push({ fieldName, subSchema: inner });
246
- }
247
- }
248
- // Create a dynamic Model subclass with JSON sub-property getters.
249
- //
250
- // Field-level MobX observability is ON BY DEFAULT. A reactive read like
251
- // `useAblo((a) => a.documents.get(id))` must re-render when a remote delta
252
- // mutates the row IN PLACE (the common collaborative case); without
253
- // per-field observability that update fires no reaction and the UI silently
254
- // goes stale. Models opt OUT with an explicit `lazyObservable: false` —
255
- // appropriate only for very large read-only list models where per-field
256
- // atoms cost more than the QueryView's entry-replaced reactivity already
257
- // provides. json fields register as `observable.ref` (see
258
- // `registerModelsFromSchema`), so the default is ~one atom per scalar field
259
- // per loaded row — cheap — not a deep atom tree per blob.
260
- const isLazy = modelDef.lazyObservable !== false;
261
- // Base provenance fields (`organizationId`, `createdBy`) live in
262
- // `baseFieldsSchema`, not the per-model `shape`. The server stamps + emits
263
- // them (camelCased on the wire), but hydration (`Model.assignFieldsFromData`)
264
- // only assigns keys that already exist as an own/prototype property — so
265
- // without a slot here, `deck.createdBy` / `deck.organizationId` silently read
266
- // `undefined` (this is why the profile decks tab showed nothing: it filters
267
- // `decks.filter(d => d.createdBy === userId)`). `id`/`createdAt`/`updatedAt`
268
- // are already seeded by the base Model constructor, so they're excluded.
269
- const fieldNames = [
270
- ...Object.keys(modelDef.shape),
271
- ...Object.keys(baseFieldsSchema.shape).filter((f) => f !== 'id' && f !== 'createdAt' && f !== 'updatedAt' && !(f in modelDef.shape)),
272
- ];
273
- const computed = modelDef.computed;
274
- const DynamicModel = createDynamicModelClass(modelName, jsonSubFields, fieldNames, computed, isLazy);
275
- // Respect the schema's load strategy so lazy models skip IDB hydration + bootstrap
276
- const loadStrategy = modelDef.load === 'lazy' || modelDef.load === 'manual'
277
- ? LoadStrategy.lazy
278
- : LoadStrategy.instant;
279
- registry.registerModel(modelName, DynamicModel, {
280
- loadStrategy,
281
- fields: modelDef.fields,
282
- autoFill: modelDef.autoFill,
283
- requiredFields: modelDef.requiredFields,
284
- });
285
- // Collect the set of fields that should get an IDB secondary index.
286
- //
287
- // Matches Linear's opt-in model (see wzhudev/reverse-linear-sync-engine):
288
- // `@Reference(..., { indexed: true })`. Only `belongsTo` relations that
289
- // explicitly set `{ index: true }` in their options get an IDB secondary
290
- // index. Every other FK (and every scalar) is resolved via in-memory
291
- // ObjectPool scans, which are fast enough at org-scope sizes (~10k rows)
292
- // and reactive via MobX.
293
- //
294
- // Auto-indexing every belongsTo was wrong: it bloated write amplification
295
- // for the vast majority of FKs that are never queried by fk. Indexing
296
- // every scalar (like the legacy Go backend did) is even worse.
297
- const indexedFields = new Set();
298
- for (const relDef of Object.values(modelDef.relations)) {
299
- if (relDef.type === 'belongsTo' && relDef.foreignKey && relDef.options?.index === true) {
300
- indexedFields.add(relDef.foreignKey);
301
- }
302
- }
303
- // Register fields as properties (from Zod shape).
304
- for (const [fieldName, rawZodType] of Object.entries(modelDef.shape)) {
305
- const zodType = rawZodType;
306
- const isOptional = zodType.isOptional?.() ?? false;
307
- // A field is indexed if it's the FK of a `belongsTo({ index: true })`
308
- // relation. Legacy `description === 'indexed'` still works for
309
- // consumers using `field.*().indexed()`.
310
- const isIndexed = indexedFields.has(fieldName) || zodType.description === 'indexed';
311
- // JSON-typed fields (per the schema's wire-type tag) are opaque
312
- // blobs from MobX's perspective — chart specs, ProseMirror docs,
313
- // style maps. Deep observability on them recursively walks every
314
- // nested property and creates an atom for each leaf, producing a
315
- // microtask storm on every commit/streaming update. `ref` tracks
316
- // only reassignment, which is how blob consumers actually use them.
317
- const wireType = modelDef.fields?.[fieldName]?.type;
318
- const observability = wireType === 'json' ? 'ref' : undefined;
319
- registry.registerProperty(modelName, fieldName, {
320
- type: PropertyType.property,
321
- indexed: isIndexed,
322
- optional: isOptional,
323
- observability,
324
- });
325
- }
326
- // Register relations
327
- for (const [relName, relDef] of Object.entries(modelDef.relations)) {
328
- if (relDef.type === 'belongsTo') {
329
- registry.registerReference(modelName, relName, {
330
- referencedModel: () => {
331
- const targetModel = registry.getModelByName(relDef.target);
332
- return targetModel ?? DynamicModel;
333
- },
334
- indexed: true,
335
- });
336
- }
337
- else if (relDef.type === 'hasMany') {
338
- // Generate a getter on the parent model that returns all children
339
- // matching the FK via Model.getStore().getByForeignKey(). The FK
340
- // index on the target model is registered by deriveSyncPlanFromSchema.
341
- const targetName = relDef.target;
342
- const foreignKey = relDef.foreignKey;
343
- const orderByField = relDef._orderBy;
344
- // Resolve the target typename from the schema (might differ from the key)
345
- const targetDef = schema.models[targetName];
346
- const targetTypename = targetDef?.typename ?? targetName;
347
- Object.defineProperty(DynamicModel.prototype, relName, {
348
- get() {
349
- const store = Model.getStore();
350
- if (!store)
351
- return [];
352
- const results = store.getByForeignKey(targetTypename, foreignKey, this.id);
353
- if (orderByField && results.length > 1) {
354
- return [...results].sort((a, b) => {
355
- // `orderByField` is a runtime string from the schema's
356
- // hasMany({ orderBy }) — Models have dynamic typed
357
- // fields produced by createDynamicModelClass, so the
358
- // static type doesn't carry an index signature for
359
- // arbitrary field reads. `Reflect.get` is the typed
360
- // bridge — returns `unknown`, narrowed below.
361
- const va = Reflect.get(a, orderByField);
362
- const vb = Reflect.get(b, orderByField);
363
- if (typeof va === 'number' && typeof vb === 'number')
364
- return va - vb;
365
- if (typeof va === 'string' && typeof vb === 'string')
366
- return va.localeCompare(vb);
367
- return 0;
368
- });
369
- }
370
- return results;
371
- },
372
- enumerable: true,
373
- configurable: true,
374
- });
375
- }
376
- }
377
- }
378
- registry.endBatch();
379
- }
380
- // ── JSON sub-property helpers ─────────────────────────────────────────────
381
- /**
382
- * Unwrap a Zod schema through .optional(), .nullable(), .default(),
383
- * .readonly() to find the innermost type. Needed to detect whether a
384
- * field.json() call wraps a ZodObject (has sub-properties) or a plain
385
- * type (ZodUnknown, ZodArray, etc.).
386
- *
387
- * Uses Zod's public `.unwrap()` API per wrapper type — no `_def`
388
- * digging. Bounded loop guards against pathological self-referential
389
- * wrappers.
390
- */
391
- function unwrapZodType(schema) {
392
- let current = schema;
393
- for (let i = 0; i < 10; i++) {
394
- if (current instanceof z.ZodOptional) {
395
- current = current.unwrap();
396
- continue;
397
- }
398
- if (current instanceof z.ZodNullable) {
399
- current = current.unwrap();
400
- continue;
401
- }
402
- if (current instanceof z.ZodDefault) {
403
- // v4 deprecates removeDefault in favor of unwrap, but the
404
- // installed @types declarations only expose removeDefault on
405
- // ZodDefault. Use it — it's the same runtime function.
406
- current = current.unwrap();
407
- continue;
408
- }
409
- if (current instanceof z.ZodReadonly) {
410
- current = current.unwrap();
411
- continue;
412
- }
413
- break;
414
- }
415
- return current;
416
- }
417
- /** Type guard: is this a ZodObject with a .shape property? */
418
- function isZodObject(schema) {
419
- return schema instanceof z.ZodObject;
420
- }
421
- /** Create a Model subclass for a schema-defined model */
422
- function createDynamicModelClass(modelName, jsonSubFields, fieldNames, computed, lazyObservable = false) {
423
- const ModelClass = class extends Model {
424
- _modelName = modelName;
425
- constructor(data) {
426
- super(data);
427
- // Gate `propertyChanged`-via-`observe` tracking during initial
428
- // hydration. M1 installs a MobX `observe()` listener per schema
429
- // property that forwards writes to `propertyChanged()` so direct
430
- // assignments like `layer.position = newPos` still round-trip
431
- // through the transaction queue. During construction we're writing
432
- // wire data, NOT user edits — flagging this as "constructing" lets
433
- // the listener early-return on those writes so `modifiedProperties`
434
- // doesn't get polluted with every field of every hydrated model.
435
- //
436
- // The listener is installed by `makeObservable()` below (inside
437
- // M1), so writes that happen BEFORE that line won't fire it; this
438
- // flag is defensive in case a subclass or call path reorders the
439
- // steps later.
440
- this._isConstructing = true;
441
- // MobX 6 requires fields to exist as own properties BEFORE makeObservable().
442
- // Model base only sets id/createdAt/updatedAt. Schema fields (title, userId, etc.)
443
- // must be initialized here so M1's annotations can find them.
444
- for (const field of fieldNames) {
445
- if (!(field in this)) {
446
- this[field] = data?.[field] ?? undefined;
447
- }
448
- }
449
- // Per-field MobX observability opt-in via `lazyObservable: true` on
450
- // the model definition. Defaults to plain objects — reactivity comes
451
- // from the QueryView "entry replaced" pattern, which is cheap for
452
- // read-only list UIs but invisible to in-place field mutations.
453
- //
454
- // Multiplayer editors need live field-level reactivity so remote
455
- // deltas AND local drag/resize/rename mutations surface through
456
- // `observer()` components without the whole pool entry being
457
- // replaced. Without observability, `layer.position.x = 500` emits
458
- // nothing and the UI lags until some unrelated state change triggers
459
- // a pass (toolbar close, deselect).
460
- //
461
- // Delegates to `Model.makeObservable()` (the inherited method) so
462
- // MobX annotations are derived from the same registry that M1 reads.
463
- // That means computed getters, reference collections, custom
464
- // getters/setters, and property-change tracking all integrate
465
- // correctly — reimplementing `makeObservable` inline here would miss
466
- // those seams.
467
- if (lazyObservable) {
468
- this.makeObservable();
469
- }
470
- this._isConstructing = false;
471
- }
472
- getModelName() {
473
- return this._modelName;
474
- }
475
- };
476
- // Generate ${field}Json getters for JSON fields with sub-properties.
477
- //
478
- // The getter reads the raw JSON string from the instance (set via
479
- // updateFromData), parses it, applies Zod defaults, and caches by
480
- // raw value. This replaces the hand-coded metadataObject + sub-property
481
- // getter pattern that 11+ Ablo models currently repeat.
482
- //
483
- // Example: field named 'metadata' with sub-schema { icon: z.string().default('presentation') }
484
- // → model.metadataJson returns { icon: 'presentation', ... } (typed, cached)
485
- for (const { fieldName, subSchema } of jsonSubFields) {
486
- const getterName = `${fieldName}Json`;
487
- const cacheKey = `__${fieldName}JsonCache`;
488
- Object.defineProperty(ModelClass.prototype, getterName, {
489
- get() {
490
- const raw = this[fieldName];
491
- // Cache check: same raw value → same parsed result
492
- const cache = this[cacheKey];
493
- if (cache && cache.raw === raw)
494
- return cache.parsed;
495
- // Parse: handle string (from DB/wire), object (already parsed), null/undefined
496
- let input;
497
- try {
498
- if (typeof raw === 'string') {
499
- input = JSON.parse(raw);
500
- }
501
- else if (raw && typeof raw === 'object') {
502
- input = raw;
503
- }
504
- else {
505
- input = {};
506
- }
507
- }
508
- catch {
509
- input = {};
510
- }
511
- // Apply Zod parse for type coercion + defaults. safeParse so
512
- // malformed metadata doesn't crash — falls back to all defaults.
513
- const result = subSchema.safeParse(input);
514
- const parsed = result.success ? result.data : subSchema.safeParse({}).data ?? {};
515
- this[cacheKey] = { raw, parsed };
516
- return parsed;
517
- },
518
- enumerable: true,
519
- configurable: true,
520
- });
521
- }
522
- // Install schema-declared computed getters on the prototype.
523
- // Each getter receives `this` (the model instance) and returns the computed value.
524
- if (computed) {
525
- for (const [name, fn] of Object.entries(computed)) {
526
- Object.defineProperty(ModelClass.prototype, name, {
527
- get() {
528
- return fn(this);
529
- },
530
- enumerable: true,
531
- configurable: true,
532
- });
533
- }
534
- }
535
- return ModelClass;
536
- }
537
- const LOG_LEVEL_RANK = { debug: 10, info: 20, warn: 30, error: 40, silent: 99 };
538
- /**
539
- * Resolve the effective level. Precedence: explicit `logLevel` option →
540
- * `debug: true` (⇒ debug) → `ABLO_LOG_LEVEL` env → default `warn`. `debug: false`
541
- * / omitted just means "don't raise the level" — it falls through to env/default
542
- * rather than force-silencing an ops-set env override.
543
- */
544
- function resolveLogLevel(opts) {
545
- if (opts?.logLevel && opts.logLevel in LOG_LEVEL_RANK)
546
- return opts.logLevel;
547
- if (opts?.debug === true)
548
- return 'debug';
549
- // `globalThis.process` guard keeps this safe in browser/edge runtimes that
550
- // have no `process` binding — there we fall through to the default.
551
- const raw = globalThis.process?.env?.ABLO_LOG_LEVEL;
552
- const normalized = raw?.toLowerCase();
553
- if (normalized && normalized in LOG_LEVEL_RANK)
554
- return normalized;
555
- return 'warn';
556
- }
557
- /**
558
- * Build the default logger, gated at `level` and prefixed `[Ablo]` so a creator
559
- * with a console full of other tools' logs can see at a glance what's ours.
560
- */
561
- function createConsoleLogger(level) {
562
- const threshold = LOG_LEVEL_RANK[level];
563
- const emit = (lvl, fn, args) => {
564
- if (typeof console === 'undefined' || LOG_LEVEL_RANK[lvl] < threshold)
565
- return;
566
- fn('[Ablo]', ...args);
567
- };
568
- return {
569
- debug: (...args) => emit('debug', console.debug, args),
570
- info: (...args) => emit('info', console.info, args),
571
- warn: (...args) => emit('warn', console.warn, args),
572
- error: (...args) => emit('error', console.error, args),
573
- };
574
- }
575
54
  // `readProcessEnv` lives in `./auth` alongside the other resolvers
576
55
  // that read it. Re-exported there for use elsewhere in the file.
577
- // ── Default mutation executor (wire: `commit` frame over WebSocket) ──────
578
- /**
579
- * Derive a stable `Idempotency-Key` from the batch's operation set.
580
- *
581
- * Retries of the same batch compute the same key — a reconnecting
582
- * client that rebuilds the identical mutations from its offline queue
583
- * sends the identical key, so the server's `mutation_log` replay path
584
- * returns the cached response instead of re-executing the mutators.
585
- *
586
- * Content-addressed: sort operations by (model, id, type) then sha256
587
- * the serialized form. Separator-safe — adjacent fields are delimited
588
- * by a character (`\x1e`, the ASCII record separator) that cannot
589
- * appear in a JSON string literal. Output length is 70 chars — safely
590
- * under Stripe's documented 255-char cap.
591
- *
592
- * Uses the Web Crypto API (cross-runtime: Node 20+ and browsers), same
593
- * primitive as the offline queue's AES-GCM encryption.
594
- *
595
- * @internal — exported as unexported file-local; callers go through
596
- * the executor's own `Idempotency-Key` plumbing.
597
- */
598
- async function deriveOperationsIdempotencyKey(operations) {
599
- const normalized = [...operations]
600
- .map((op) => ({
601
- type: op.type,
602
- model: op.model,
603
- id: op.id,
604
- input: op.input ?? null,
605
- }))
606
- .sort((a, b) => {
607
- if (a.model !== b.model)
608
- return a.model < b.model ? -1 : 1;
609
- if (a.id !== b.id)
610
- return a.id < b.id ? -1 : 1;
611
- return a.type < b.type ? -1 : a.type > b.type ? 1 : 0;
612
- });
613
- const encoded = new TextEncoder().encode(JSON.stringify(normalized));
614
- const digest = await crypto.subtle.digest('SHA-256', encoded);
615
- const bytes = new Uint8Array(digest);
616
- let hex = '';
617
- for (let i = 0; i < bytes.length; i++) {
618
- hex += bytes[i].toString(16).padStart(2, '0');
619
- }
620
- return `batch-${hex}`;
621
- }
622
- /**
623
- * Default mutation executor: sends `{ type: 'commit', payload: ... }` over
624
- * the sync engine's own WebSocket.
625
- *
626
- * Transport ownership follows the Zero / Liveblocks pattern — the engine
627
- * owns its socket end-to-end and the executor is internal. Apps pass URLs
628
- * and auth; they do NOT inject transport callbacks. That's why this
629
- * factory takes a `getWs` closure instead of a full SyncWebSocket: the WS
630
- * doesn't exist when the executor is constructed (it's created later in
631
- * `Ablo` during `BaseSyncedStore` init), so we resolve it
632
- * lazily at commit time. Same trick Zero uses internally — see
633
- * `packages/zero-client/src/client/zero.ts` where `Pusher`/`Puller` are
634
- * constructed before the socket then wired up at connect time.
635
- *
636
- * `options.idempotencyKey` becomes the wire-level `clientTxId` when set,
637
- * matching Stripe-style retry semantics. Otherwise the SDK generates one.
638
- */
639
- function createDefaultMutationExecutor(getWs) {
640
- async function commit(operations, options) {
641
- const ws = getWs();
642
- if (!ws?.sendCommit) {
643
- throw new AbloConnectionError('SyncWebSocket not ready for commit. The engine must finish bootstrap ' +
644
- 'before mutations can be sent.', { code: 'ws_not_ready' });
645
- }
646
- const clientTxId = options?.idempotencyKey ??
647
- (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'
648
- ? crypto.randomUUID()
649
- : `tx_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`);
650
- try {
651
- return await ws.sendCommit(operations, clientTxId, undefined, // use sendCommit's built-in 15s default; no per-call override
652
- options?.causedByTaskId, options?.reads);
653
- }
654
- catch (err) {
655
- // Wrap transport-level failures as connection errors so the
656
- // TransactionQueue's retry classifier treats them as transient
657
- // (matches the old HTTP path's network-error handling).
658
- if (err instanceof AbloError)
659
- throw err;
660
- if (err instanceof Error) {
661
- if (/not connected|timed out|connection|ECONN/i.test(err.message)) {
662
- const wrapped = new AbloConnectionError(err.message, { cause: err });
663
- // Preserve any `diagnostics` snapshot the underlying SyncWebSocket
664
- // attached to the rejection. Without this, the wrapped error
665
- // bottoms out at "AbloConnectionError: not connected" with no
666
- // attribution to which close code / heartbeat trip / session
667
- // error caused it. See SyncWebSocket.notConnectedError().
668
- if (err &&
669
- typeof err === 'object' &&
670
- 'diagnostics' in err &&
671
- err.diagnostics) {
672
- wrapped.diagnostics = err.diagnostics;
673
- }
674
- throw wrapped;
675
- }
676
- }
677
- throw err;
678
- }
679
- }
680
- return {
681
- commit,
682
- executeCreate: (model, id, input, _txId, options) => commit([{ type: 'CREATE', model: model.toLowerCase(), id, input }], options).then(() => { }),
683
- executeUpdate: (model, id, data, _txId, options) => commit([{ type: 'UPDATE', model: model.toLowerCase(), id, input: data }], options),
684
- executeDelete: (model, id, _txId, options) => commit([{ type: 'DELETE', model: model.toLowerCase(), id }], options).then(() => { }),
685
- executeArchive: (model, id, _txId, options) => commit([{ type: 'ARCHIVE', model: model.toLowerCase(), id }], options).then(() => { }),
686
- executeUnarchive: (model, id, _txId, options) => commit([{ type: 'UNARCHIVE', model: model.toLowerCase(), id }], options).then(() => { }),
687
- };
688
- }
689
- // ── Default mutation dispatcher (for offline flush) ───────────────────────
690
- function createDefaultMutationDispatcher(executor) {
691
- return {
692
- async dispatch(opName, variables) {
693
- const prefixes = ['Create', 'Update', 'Delete', 'Archive', 'Unarchive'];
694
- for (const prefix of prefixes) {
695
- if (opName.startsWith(prefix)) {
696
- const model = opName.slice(prefix.length);
697
- const v = variables;
698
- const input = (prefix === 'Create' || prefix === 'Update')
699
- ? v.input
700
- : undefined;
701
- await executor.commit([{
702
- type: prefix.toUpperCase(),
703
- model: model.toLowerCase(),
704
- id: v.id ?? '',
705
- input,
706
- }]);
707
- return;
708
- }
709
- }
710
- },
711
- };
712
- }
713
56
  // ── Auth normalization ─────────────────────────────────────────────────────
714
57
  /**
715
- * The one resolver the credential lifecycle needs: an async `() => token | null`,
716
- * or `null` when auth is static (a plain long-lived `apiKey` STRING with no
717
- * refresh the common case).
58
+ * The single resolver the credential lifecycle needs: an async
59
+ * `() => token | null`, or `null` when auth is static a plain long-lived
60
+ * `apiKey` string with no refresh, which is the common case.
718
61
  *
719
- * The short-lived per-user browser path passes a FUNCTION `apiKey` (an
720
- * `ApiKeySetter`): the SDK then drives the full credential lifecycle off it —
721
- * mint-before-connect, the proactive refresh timer + wake/online/focus re-mint,
722
- * and the reactive `credential_stale` re-mint. The resolver's contract is the
723
- * `ApiKeySetter` contract end-to-end: resolve a token, resolve `null` when the
724
- * login is gone (terminal `session_expired` → sign out), or THROW on a
725
- * transient failure ( back off, never sign out).
62
+ * The short-lived per-user browser path passes a function `apiKey` (an
63
+ * {@link ApiKeySetter}), and the SDK then drives the whole credential lifecycle
64
+ * from it: mint-before-connect, the proactive refresh timer with its
65
+ * wake/online/focus re-mint, and the reactive `credential_stale` re-mint. The
66
+ * resolver follows the `ApiKeySetter` contract end to end: resolve a token,
67
+ * resolve `null` when the login is gone (terminal surfaces `session_expired`
68
+ * and signs the user out), or throw on a transient failure (backs off, without
69
+ * signing out).
726
70
  */
727
71
  function resolveCredentialResolver(apiKey) {
728
72
  if (typeof apiKey === 'function')
@@ -743,12 +87,14 @@ export function Ablo(options) {
743
87
  const authInput = { options, env };
744
88
  const configuredApiKey = resolveApiKey(authInput);
745
89
  const configuredAuthToken = resolveAuthToken(authInput);
746
- // The client OWNS its credential lifecycle (not the React layer): this resolver
747
- // drives both the reactive re-mint (FSM `credential_stale`) and the proactive
748
- // refresh timer + wake/online/focus triggers. Null for the common static
749
- // `apiKey` path no refresh needed.
90
+ // The client owns its credential lifecycle (not the React layer): this resolver
91
+ // drives both the reactive re-mint (the connection's `credential_stale` state)
92
+ // and the proactive refresh timer with its wake/online/focus triggers. Null for
93
+ // the common static `apiKey` path, which needs no refresh.
750
94
  const credentialResolver = resolveCredentialResolver(configuredApiKey);
751
- const authCredentials = createAuthCredentialSource(internalOptions.capabilityToken ?? configuredAuthToken);
95
+ const authCredentials = createAuthCredentialSource(
96
+ // eslint-disable-next-line @typescript-eslint/no-deprecated -- load-bearing on the self-hosted path; server-internal cap-mint (Phase 3) not shipped
97
+ internalOptions.capabilityToken ?? configuredAuthToken);
752
98
  const configuredDatabaseUrl = resolveDatabaseUrl(authInput);
753
99
  assertBrowserSafety({
754
100
  apiKey: configuredApiKey,
@@ -761,9 +107,9 @@ export function Ablo(options) {
761
107
  createConsoleLogger(resolveLogLevel({ debug: options.debug, logLevel: options.logLevel }));
762
108
  // Nudge (once) if a stray DATABASE_URL is in the env but `databaseUrl` wasn't
763
109
  // passed — the env value is no longer auto-adopted (see resolveDatabaseUrl).
764
- warnIfDatabaseUrlEnvIgnored(authInput, (m) => logger.warn(m));
765
- warnIfDatabaseUrlDeprecated(authInput, (m) => logger.warn(m));
766
- void warnIfCliKeyMismatch(authInput, (m) => logger.warn(m));
110
+ warnIfDatabaseUrlEnvIgnored(authInput, (m) => { logger.warn(m); });
111
+ warnIfDatabaseUrlDeprecated(authInput, (m) => { logger.warn(m); });
112
+ void warnIfCliKeyMismatch(authInput, (m) => { logger.warn(m); });
767
113
  const schema = options.schema;
768
114
  const url = resolveBaseURL(authInput);
769
115
  // 1. Derive config from schema
@@ -773,24 +119,19 @@ export function Ablo(options) {
773
119
  ...deriveConfigFromSchema(schema),
774
120
  ...internalOptions.configOverrides,
775
121
  };
776
- // 2. Create the mutation executor + dispatcher.
122
+ // 2. Create the mutation executor and dispatcher.
777
123
  //
778
- // The default executor sends `{ type: 'commit', ... }` over the
779
- // engine's WebSocket. The WS doesn't exist yet at this point (it's
780
- // created later when `BaseSyncedStore` initializes), so the default
781
- // takes a lazy getter that resolves the live WS at commit time.
782
- // `storeForTransport` is captured by the closure and assigned below
783
- // once the store is built JS closures close over bindings, not
784
- // values, so by the time the first commit fires the store is live.
124
+ // The default executor sends `{ type: 'commit', ... }` over the engine's
125
+ // WebSocket. The socket doesn't exist yet at this point (it's created later
126
+ // when the store initializes), so the default takes a lazy getter that
127
+ // resolves the live socket at commit time. `storeHolder` is captured by the
128
+ // closure and assigned below once the store is built — JS closures close
129
+ // over bindings, not values, so by the time the first commit fires the store
130
+ // is live.
785
131
  //
786
- // Caller-supplied executors are still honored for advanced cases
787
- // (test mocks, alternative transports) but the public `<AbloProvider>`
788
- // surface will mark this option `@internal` — apps should almost
789
- // never need to override transport. See Zero's `ClientOptions`
790
- // (packages/zero-client/src/client/options.ts) and Liveblocks'
791
- // `ClientOptions` (packages/liveblocks-core/src/client.ts) for the
792
- // reference shape: URLs + auth + declarative mutators, never a
793
- // pluggable commit transport.
132
+ // Caller-supplied executors are still honored for advanced cases (test
133
+ // mocks, alternative transports), but apps should almost never need to
134
+ // override the transport.
794
135
  // Captured-by-reference binding — assigned below after BaseSyncedStore
795
136
  // is constructed. The default executor's `getWs` closure reads it
796
137
  // lazily at commit time.
@@ -802,7 +143,6 @@ export function Ablo(options) {
802
143
  const ws = storeHolder.store?.getSyncWebSocket() ?? null;
803
144
  return ws;
804
145
  });
805
- const dispatcher = internalOptions.mutationDispatcher ?? createDefaultMutationDispatcher(executor);
806
146
  // 3. Initialize SDK context (one call — hides all DI wiring).
807
147
  // Each provider can be overridden individually; the noop defaults
808
148
  // are preserved for the zero-config consumer path.
@@ -817,15 +157,13 @@ export function Ablo(options) {
817
157
  : browserOnlineStatus),
818
158
  config,
819
159
  mutationExecutor: executor,
820
- mutationDispatcher: dispatcher,
821
160
  });
822
161
  // 4. Create internal components (user never sees these). See
823
162
  // `./createInternalComponents.ts` for the construction order
824
163
  // and what each component does. Model registration happens
825
- // here because `registerModelsFromSchema` lives in this file —
826
- // the schema-to-Model-class translation depends on private
827
- // helpers (`createDynamicModelClass`, `unwrapZodType`, etc.)
828
- // that aren't worth pulling into the components module.
164
+ // here (via `registerModelsFromSchema`, in `./modelRegistration.ts`)
165
+ // because the schema-to-Model-class translation is client-construction
166
+ // wiring that isn't worth pulling into the components module.
829
167
  const { modelRegistry, objectPool, bootstrapHelper, database, syncClient, hydration, } = createInternalComponents({
830
168
  schema,
831
169
  url,
@@ -853,25 +191,46 @@ export function Ablo(options) {
853
191
  });
854
192
  // Hand the credential lifecycle to the client (refresher + proactive refresh
855
193
  // timer + wake/online/focus re-mint). Installed once here so refresh works for
856
- // ANY consumer of `Ablo({ auth })` not only those who render `<AbloProvider>`.
194
+ // any consumer of `Ablo({ auth })`, not only those who render `<AbloProvider>`.
857
195
  // The first mint happens in `ready()` so the first connection carries a token.
196
+ //
197
+ // Long-lived server clients also get the pre-roll timer on windowless hosts
198
+ // (`proactiveInNode`): their socket must renew its `rk_` or `ek_` before the
199
+ // server's keepalive reaper closes it (4001 `credential_expired`). Two signals
200
+ // qualify — an agent or system participant, and an absolute endpoint-string
201
+ // `apiKey` (a relative one can't be fetched in Node, so an absolute URL is
202
+ // unambiguously a deliberate server client). User-kind clients in Node (an
203
+ // SSR/RSC module evaluating scaffolded browser code) stay reactive-only.
858
204
  if (credentialResolver) {
859
- store.startCredentialLifecycle(credentialResolver);
205
+ const rawEndpoint = internalOptions.authEndpoint ?? internalOptions.apiKey;
206
+ const absoluteEndpoint = typeof rawEndpoint === 'string' && /^https?:\/\//i.test(rawEndpoint);
207
+ store.startCredentialLifecycle(credentialResolver, {
208
+ /* eslint-disable @typescript-eslint/no-deprecated -- `kind` gates the self-hosted proactive pre-roll; hosted path derives it from the apiKey scope */
209
+ proactiveInNode: internalOptions.kind === 'agent' ||
210
+ internalOptions.kind === 'system' ||
211
+ absoluteEndpoint,
212
+ /* eslint-enable @typescript-eslint/no-deprecated */
213
+ });
860
214
  }
215
+ // Put the lazy-query lane on the same auth-recovery path as the WebSocket probe
216
+ // and the proactive pre-roll: a 401 on `/sync/query` re-mints via the store's
217
+ // single-flight lifecycle and replays once, instead of silently returning empty
218
+ // rows against an expired `ek_` until the next proactive tick. Late-bound
219
+ // because the coordinator is constructed before the store exists.
220
+ hydration.setCredentialRecovery((recovery) => store.recoverFromAuthRejection(recovery));
861
221
  // Wire the store back into the default executor's lazy getter (see
862
222
  // `storeHolder` above). The executor was constructed before the store
863
223
  // existed; this late binding closes the loop so commits dispatch over
864
224
  // the engine's WebSocket once it opens.
865
225
  storeHolder.store = store;
866
- // Bind THIS executor to THIS Ablo's TransactionQueue. Without this,
867
- // the queue resolves `mutationExecutor` from the module-level
868
- // `getContext()`, which `initSyncEngine()` overwrites on every Ablo
869
- // construction. In multi-Ablo flows (e.g. agent-worker's worker +
870
- // per-job peer) the second `initSyncEngine()` call would silently
871
- // redirect the first Ablo's queue through the second Ablo's executor
872
- // closure and when the second Ablo disposes, its `storeHolder.store`
873
- // becomes null, so the first Ablo's commits start throwing
874
- // `ws_not_ready` forever (terminal AgentJob writes hang on retry).
226
+ // Bind this executor to this client's TransactionQueue. Without it, the queue
227
+ // resolves `mutationExecutor` from the module-level `getContext()`, which
228
+ // `initSyncEngine()` overwrites on every client construction. In multi-client
229
+ // flows (for example a worker plus a per-job peer) the second `initSyncEngine()`
230
+ // call would silently redirect the first client's queue through the second
231
+ // client's executor closure and when the second client disposes, its
232
+ // `storeHolder.store` becomes null, so the first client's commits start throwing
233
+ // `ws_not_ready` forever.
875
234
  syncClient.getTransactionQueue().setMutationExecutor(executor);
876
235
  // Presence + claim streams — built eagerly so `engine.presence`
877
236
  // and `engine.claims` return the same reference for the engine's
@@ -883,10 +242,13 @@ export function Ablo(options) {
883
242
  // The server stamps `isAgent` on outbound presence frames from the
884
243
  // connection's authenticated identity prefix, but the local `self`
885
244
  // entry uses the kind we know at construction.
886
- const participantId = (internalOptions.kind === 'agent' ? internalOptions.agentId : internalOptions.user?.id) ?? '';
245
+ const participantId =
246
+ // eslint-disable-next-line @typescript-eslint/no-deprecated -- self-hosted identity fallback; hosted path derives identity from the apiKey scope
247
+ (internalOptions.kind === 'agent' ? internalOptions.agentId : internalOptions.user?.id) ?? '';
887
248
  const presenceStream = createPresenceStream({
888
249
  participantId,
889
250
  syncGroups: internalOptions.syncGroups ?? [],
251
+ // eslint-disable-next-line @typescript-eslint/no-deprecated -- local self-entry kind; server re-stamps isAgent from the authenticated identity
890
252
  isAgent: internalOptions.kind === 'agent',
891
253
  });
892
254
  const claimStream = createClaimStream({ participantId });
@@ -900,6 +262,7 @@ export function Ablo(options) {
900
262
  // 6. Validate options up front — fail loudly on obviously wrong inputs so
901
263
  // strangers don't get silent empty results. Validation errors are written
902
264
  // into `store.syncStatus` (the single source of truth).
265
+ // eslint-disable-next-line @typescript-eslint/no-deprecated -- self-hosted default; hosted path ignores it (server derives kind from the apiKey scope)
903
266
  const kind = internalOptions.kind ?? 'user';
904
267
  const _validationError = validateAbloOptions({
905
268
  options: internalOptions,
@@ -918,6 +281,7 @@ export function Ablo(options) {
918
281
  // trusting them is the trap (you think you're an agent; the key says user).
919
282
  // Warn loudly rather than removing the fields — `agentId` is still load-bearing
920
283
  // on the self-hosted path (no apiKey; paired with `capabilityToken`).
284
+ // eslint-disable-next-line @typescript-eslint/no-deprecated -- reads the deprecated fields precisely to warn callers off them under a configured apiKey
921
285
  if (configuredApiKey && (internalOptions.kind || internalOptions.agentId)) {
922
286
  logger.warn('Ablo: `kind` / `agentId` are ignored when an `apiKey` is configured — ' +
923
287
  'the server derives participant identity from the key’s scope. Remove ' +
@@ -945,13 +309,13 @@ export function Ablo(options) {
945
309
  }
946
310
  _readyPromise = (async () => {
947
311
  try {
948
- // Mint the FIRST access credential before we connect, so the initial
949
- // WebSocket upgrade + bootstrap carry a valid bearer (no tokenless first
950
- // connect that has to self-heal). Only when a refreshing resolver is
951
- // wired AND no static credential is already present. Contract mirrors
952
- // the `apiKey` resolver: `null` the login is gone (terminal — fail ready so the
953
- // app shows sign-in); a THROW transient (rethrown; autoStart swallows
954
- // and the lifecycle's online/wake triggers retry).
312
+ // Mint the first access credential before we connect, so the initial
313
+ // WebSocket upgrade and bootstrap carry a valid bearer (no tokenless first
314
+ // connect that has to self-heal). Only when a refreshing resolver is wired
315
+ // and no static credential is already present. Follows the `apiKey`
316
+ // resolver contract: `null` means the login is gone (terminal — fail ready
317
+ // so the app shows sign-in); a throw means transient (rethrown; autoStart
318
+ // swallows it and the lifecycle's online/wake triggers retry).
955
319
  if (credentialResolver && !authCredentials.getAuthToken()) {
956
320
  const token = await credentialResolver();
957
321
  if (!token) {
@@ -959,7 +323,7 @@ export function Ablo(options) {
959
323
  }
960
324
  authCredentials.setAuthToken(token);
961
325
  }
962
- // Register the caller's own database for write-back BEFORE bootstrap, so
326
+ // Register the caller's own database for write-back before bootstrap, so
963
327
  // the server resolves this org's data plane to the customer's DB rather
964
328
  // than serving an empty/wrong store. The org is derived server-side from
965
329
  // the API key. Idempotent server-side (register-or-update). Skipped when
@@ -981,15 +345,15 @@ export function Ablo(options) {
981
345
  url,
982
346
  kind,
983
347
  configuredApiKey,
984
- // Resolve identity against the LIVE token, not the construction-time
985
- // `configuredAuthToken`. Consumers using a function `apiKey` (apps/web)
986
- // never pass `authToken` at construction — the lifecycle mints the
987
- // first `ek_`/`rk_` and calls `setAuthToken()` before
988
- // `ready()`, which updates the shared credential source. Reading the frozen
989
- // `configuredAuthToken` here made `/auth/identity` fire with no Bearer
990
- // (→ `no_matching_provider` / `session_expired`) even though the JWT
991
- // was present. Mirrors every other transport by reading the shared
992
- // credential source.
348
+ // Resolve identity against the live token, not the construction-time
349
+ // `configuredAuthToken`. Consumers using a function `apiKey` never pass
350
+ // `authToken` at construction — the lifecycle mints the first `ek_` or
351
+ // `rk_` and calls `setAuthToken()` before `ready()`, which updates the
352
+ // shared credential source. Reading the frozen `configuredAuthToken`
353
+ // here made `/auth/identity` fire with no bearer (returning
354
+ // `no_matching_provider` / `session_expired`) even though the token was
355
+ // present. This reads the shared credential source, like every other
356
+ // transport.
993
357
  configuredAuthToken: authCredentials.getAuthToken() ?? configuredAuthToken,
994
358
  bootstrapHelper,
995
359
  auth: authCredentials,
@@ -997,11 +361,11 @@ export function Ablo(options) {
997
361
  });
998
362
  const { userId, accountScope, teamIds, capabilityToken, syncGroups, participantKind, } = resolved;
999
363
  // Fail-loud guard: detect the degenerate "no real sync groups
1000
- // resolved" state before opening the WS. Same class of bug as
1001
- // the schema-drift `[commit] dropped stale field` warning —
364
+ // resolved" state before opening the socket. It is the same class of bug as
365
+ // a
1002
366
  // sensible-looking default that's functionally broken: the
1003
367
  // SDK ends up subscribing only to the server-side
1004
- // `['default']` fallback (bootstrap.ts:45, Hub.ts:480), no
368
+ // `['default']` fallback, no
1005
369
  // delta has that tag, live fan-out silently never delivers.
1006
370
  // For human users (kind:'user') this is almost certainly a
1007
371
  // misconfiguration upstream — either the caller didn't pass
@@ -1013,9 +377,9 @@ export function Ablo(options) {
1013
377
  if (participantKind === 'user' &&
1014
378
  (resolvedSyncGroups.length === 0 ||
1015
379
  (resolvedSyncGroups.length === 1 && resolvedSyncGroups[0] === 'default'))) {
1016
- // Actionable and NOT self-healing (no live updates until fixed):
1017
- // kept at warn, consumer register engine jargon and the internal
1018
- // file pointer stripped; forensic fields ride the debug companion.
380
+ // Actionable and not self-healing (no live updates until fixed):
381
+ // kept at warn level for consumers; the low-level diagnostic
382
+ // fields ride the debug log below.
1019
383
  logger.warn('This client was started without sync groups, so it will not receive ' +
1020
384
  'live updates. Pass `syncGroups` (for example ' +
1021
385
  '`["org:<id>", "user:<id>"]`) or check that your auth provider supplies them.');
@@ -1087,15 +451,15 @@ export function Ablo(options) {
1087
451
  httpStatus: error.httpStatus,
1088
452
  error: error.message,
1089
453
  });
1090
- // Clear the memo so a FUTURE `ready()` re-attempts bootstrap instead of
1091
- // replaying this rejection forever. Bootstrap failures here are
1092
- // transient by nature — offline, an IndexedDB open timeout, a bootstrap
1093
- // fetch hiccup — and used to brick the engine until a full page reload
1094
- // because line ~2013 (`if (_readyPromise) return _readyPromise`) handed
1095
- // every later caller this same dead promise. Nulling it lets the
1096
- // provider's online/wake/retry triggers drive a clean re-bootstrap.
1097
- // (The terminal `_validationError` branch above intentionally stays
1098
- // cached — config can't change without recreating the engine.)
454
+ // Clear the memo so a future `ready()` re-attempts bootstrap instead of
455
+ // replaying this rejection forever. Bootstrap failures here are transient
456
+ // by nature — offline, an IndexedDB open timeout, a bootstrap fetch
457
+ // hiccup — and the early `if (_readyPromise) return _readyPromise` guard
458
+ // would otherwise hand every later caller this same dead promise, bricking
459
+ // the engine until a full page reload. Nulling it lets the provider's
460
+ // online/wake/retry triggers drive a clean re-bootstrap. (The terminal
461
+ // `_validationError` branch above intentionally stays cached — config
462
+ // can't change without recreating the engine.)
1099
463
  _readyPromise = null;
1100
464
  throw error;
1101
465
  }
@@ -1202,27 +566,6 @@ export function Ablo(options) {
1202
566
  },
1203
567
  };
1204
568
  }
1205
- function modelClaimFromQueued(claim) {
1206
- return {
1207
- id: claim.id,
1208
- actor: claim.heldBy ?? "",
1209
- participantKind: claim.participantKind ?? "user",
1210
- reason: claim.reason,
1211
- ...(claim.description ? { description: claim.description } : {}),
1212
- field: claim.target.field,
1213
- status: 'queued',
1214
- position: claim.position,
1215
- expiresAt: claim.expiresAt ?? 0,
1216
- target: {
1217
- model: claim.target.type,
1218
- id: claim.target.id,
1219
- path: claim.target.path,
1220
- range: claim.target.range,
1221
- field: claim.target.field,
1222
- meta: claim.target.meta,
1223
- },
1224
- };
1225
- }
1226
569
  function targetMatchesModel(target, claim) {
1227
570
  if (target.model &&
1228
571
  claim.target.type.toLowerCase() !== target.model.toLowerCase()) {
@@ -1239,14 +582,6 @@ export function Ablo(options) {
1239
582
  .filter((claim) => (target ? targetMatchesModel(target, claim) : true))
1240
583
  .map(modelClaimFromActive);
1241
584
  }
1242
- function listModelClaimQueue(target) {
1243
- if (!target?.model || !target.id)
1244
- return [];
1245
- return publicClaims
1246
- .queueFor({ type: target.model, id: target.id })
1247
- .filter((claim) => (target.field ? claim.target.field === target.field : true))
1248
- .map(modelClaimFromQueued);
1249
- }
1250
585
  function waitForModelUnclaimed(target, options) {
1251
586
  if (listModelClaims(target).length === 0)
1252
587
  return Promise.resolve();
@@ -1274,10 +609,12 @@ export function Ablo(options) {
1274
609
  }
1275
610
  };
1276
611
  const onAbort = () => {
1277
- finish(() => reject(new AbloConnectionError('Claim wait aborted.', {
1278
- code: 'claim_wait_aborted',
1279
- cause: options?.signal?.reason,
1280
- })));
612
+ finish(() => {
613
+ reject(new AbloConnectionError('Claim wait aborted.', {
614
+ code: 'claim_wait_aborted',
615
+ cause: options?.signal?.reason,
616
+ }));
617
+ });
1281
618
  };
1282
619
  if (options?.signal?.aborted) {
1283
620
  onAbort();
@@ -1287,7 +624,9 @@ export function Ablo(options) {
1287
624
  options?.signal?.addEventListener('abort', onAbort, { once: true });
1288
625
  if (options?.timeout != null) {
1289
626
  timeoutId = setTimeout(() => {
1290
- finish(() => reject(claimedError(target, listModelClaims(target), 'model_claimed_timeout')));
627
+ finish(() => {
628
+ reject(claimedError(target, listModelClaims(target), 'model_claimed_timeout'));
629
+ });
1291
630
  }, options.timeout);
1292
631
  }
1293
632
  });
@@ -1314,6 +653,10 @@ export function Ablo(options) {
1314
653
  waited,
1315
654
  release,
1316
655
  revoke: claim.revoke,
656
+ // The lease-control members are forwarded explicitly — this wrapper
657
+ // rebuilds the handle field by field, so anything not named here is
658
+ // silently dropped from the public claim.
659
+ heartbeat: claim.heartbeat,
1317
660
  [Symbol.asyncDispose]: release,
1318
661
  };
1319
662
  }
@@ -1376,7 +719,7 @@ export function Ablo(options) {
1376
719
  createSnapshot: (modelKey, id) => createSnapshot({
1377
720
  pool: objectPool,
1378
721
  transport: store.getSyncWebSocket(),
1379
- // `position.readFloor` is THE value claims/snapshots stamp as
722
+ // `position.readFloor` is the value claims and snapshots stamp as
1380
723
  // `readAt` (max of the pool-applied cursor and the acked
1381
724
  // watermark for our own writes — see sync/syncPosition.ts).
1382
725
  // Stamping a bare stream cursor made a claim taken right after
@@ -1387,7 +730,7 @@ export function Ablo(options) {
1387
730
  entities: { [modelKey]: id },
1388
731
  }),
1389
732
  queue: (target) => publicClaims.queueFor({ type: target.model, id: target.id }),
1390
- reorder: (target, order) => publicClaims.reorder({ type: target.model, id: target.id }, order),
733
+ reorder: (target, order) => { publicClaims.reorder({ type: target.model, id: target.id }, order); },
1391
734
  state: (target) => {
1392
735
  // The live claim stream only tracks *open* (active) claims;
1393
736
  // terminal states (committed / expired / canceled) drop out of
@@ -1422,19 +765,18 @@ export function Ablo(options) {
1422
765
  selfParticipantKind: kind,
1423
766
  // Read-interest / write-intent enrolment for the typed surface.
1424
767
  // `enterScope`/`pinScope` resolve the `{ [schemaKey]: id }` scope
1425
- // through the SAME resolver the claim path uses, landing this client in
768
+ // through the same resolver the claim path uses, landing this client in
1426
769
  // the entity-scoped group the holder's claim presence fans out on.
1427
- // Return the store promise so the claim write path can AWAIT pinScope
1428
- // BEFORE acquiring the lease (closing the subscribe-vs-broadcast race);
770
+ // Returns the store promise so the claim write path can await pinScope
771
+ // before acquiring the lease (closing the subscribe-vs-broadcast race);
1429
772
  // read-interest callers (`retrieve`/`claim.state`) still `void` it and
1430
- // stay fire-and-forget. SOFT either way — the store swallows reconcile
1431
- // errors so read interest never makes a read reject or stall.
773
+ // stay fire-and-forget. It's soft either way — the store swallows
774
+ // reconcile errors so read interest never makes a read reject or stall.
1432
775
  enterScope: (scope) => store.enterScope(scope),
1433
776
  pinScope: (scope) => store.pinScope(scope),
1434
- // `ablo.<model>.watch(ids, { ttl })` a scoped participant join on
1435
- // this model's sync group(s). The model-scoped relocation of the old
1436
- // `ablo.participants.join({ scope: { <model>: ids } })`. WebSocket
1437
- // only — `join` throws AbloConnectionError if the socket isn't ready.
777
+ // `ablo.<model>.watch(ids, { ttl })` performs a scoped participant join
778
+ // on this model's sync group(s). WebSocket only `join` throws
779
+ // `AbloConnectionError` if the socket isn't ready.
1438
780
  createWatch: (modelKey, ids, options) => participantManager.join({
1439
781
  scope: { [modelKey]: ids },
1440
782
  ...(options?.ttl !== undefined ? { ttlSeconds: options.ttl } : {}),
@@ -1584,7 +926,7 @@ export function Ablo(options) {
1584
926
  const id = params.id ?? createModelId();
1585
927
  await applyClaimedPolicy({ model: name, id }, params);
1586
928
  // Confirm, then return the authoritative row (with framework defaults;
1587
- // the EXISTING row on an idempotent re-create) — mirrors the WS client.
929
+ // the existing row on an idempotent re-create) — mirrors the WebSocket client.
1588
930
  await commits.create({
1589
931
  claimRef: params.claimRef,
1590
932
  idempotencyKey: params.idempotencyKey,
@@ -1629,13 +971,13 @@ export function Ablo(options) {
1629
971
  };
1630
972
  }
1631
973
  /**
1632
- * The CONTROL-PLANE credential: always the original configured secret key.
974
+ * The control-plane credential: always the original configured secret key.
1633
975
  * Never reads `authCredentials` — that holds the exchanged sync credential
1634
976
  * (a wide-scope `rk_` on the hosted path), which control-plane routes
1635
977
  * rightly refuse (e.g. the user-session mint is sk_-gated). Counterpart to
1636
978
  * `getAuthToken()`, which resolves the sync-plane token.
1637
979
  *
1638
- * The sk_-only rule is enforced server-side; the credential KIND taxonomy
980
+ * The secret-key-only rule is enforced on the server; the credential-kind taxonomy
1639
981
  * (secret/restricted/ephemeral/publishable) lives in `auth/credentialPolicy`.
1640
982
  */
1641
983
  async function controlPlaneApiKey() {
@@ -1643,7 +985,7 @@ export function Ablo(options) {
1643
985
  }
1644
986
  /**
1645
987
  * Resolve the control-plane context a session/agent mint needs (sk_ +
1646
- * bootstrap base URL + the schema-key→typename map the Hub gates on).
988
+ * bootstrap base URL + the schema-key→typename map the server gates on).
1647
989
  * Shared by `sessions.create` and `agents.create` so the two mint doors
1648
990
  * can never drift on how a token is minted. Throws if no `sk_` is present —
1649
991
  * minting is a backend-only operation.
@@ -1660,12 +1002,12 @@ export function Ablo(options) {
1660
1002
  bootstrapBaseUrl: internalOptions.bootstrapBaseUrl,
1661
1003
  }),
1662
1004
  ...(internalOptions.fetch ? { fetch: internalOptions.fetch } : {}),
1663
- // Map every `can` schema-key to the wire typename the Hub gates on, so a
1005
+ // Map every `can` schema-key to the wire typename the server gates on, so a
1664
1006
  // typename override (`documents` → `Document`) doesn't mint a capability
1665
1007
  // the server then denies. See `MintSessionContext`.
1666
1008
  modelTypenames: Object.fromEntries(Object.entries(schema.models).map(([key, def]) => [
1667
1009
  key,
1668
- def.typename ?? key,
1010
+ (def).typename ?? key,
1669
1011
  ])),
1670
1012
  };
1671
1013
  }
@@ -1688,9 +1030,9 @@ export function Ablo(options) {
1688
1030
  // The live short-lived bearer (set via `setAuthToken` / `apiKey`-resolver refresh)
1689
1031
  // is the canonical credential; fall back to a configured API key.
1690
1032
  //
1691
- // This is the SYNC-PLANE token (bootstrap, WS, query HTTP). Control-plane
1033
+ // This is the sync-plane token (bootstrap, WebSocket, query HTTP). Control-plane
1692
1034
  // calls (sessions.create, datasource registration) never use it — they
1693
- // present the ORIGINAL secret key via `controlPlaneApiKey()` below. The
1035
+ // present the original secret key via `controlPlaneApiKey()` below. The
1694
1036
  // split matters: after the startup exchange this resolver returns the
1695
1037
  // derived wide-scope `rk_`, a credential the control-plane routes
1696
1038
  // correctly refuse (an agent token must never mint humans).
@@ -1702,9 +1044,8 @@ export function Ablo(options) {
1702
1044
  setCredentialRefresher(refresher) {
1703
1045
  store.setCredentialRefresher(refresher);
1704
1046
  },
1705
- // The org this client resolved to — null until `ready()` completes.
1706
- // Integrators previously had no programmatic way to learn it (the Pulse
1707
- // agent regex-scraped `ablo status` output); now it's a property.
1047
+ // The org this client resolved to — null until `ready()` completes. Exposed
1048
+ // as a property so integrators can read it programmatically.
1708
1049
  get organizationId() {
1709
1050
  return _resolvedOrganizationId;
1710
1051
  },
@@ -1712,33 +1053,33 @@ export function Ablo(options) {
1712
1053
  store.nudgeReconnect();
1713
1054
  },
1714
1055
  sessions: {
1715
- // Stripe `ephemeralKeys.create` shape: a BACKEND (holding `sk_`) mints a
1716
- // short-lived scoped token for one end user OR one agent.
1056
+ // A backend (holding `sk_`) mints a short-lived scoped token for one end
1057
+ // user or one agent.
1717
1058
  //
1718
- // CONTROL-PLANE CREDENTIAL RULE: both arms authenticate with the
1719
- // ORIGINAL secret key (`controlPlaneApiKey()`), never the wide-scope
1720
- // `rk_` the startup exchange installed as the sync credential. A derived
1721
- // agent credential silently replacing the secret key on control-plane
1722
- // calls is how humans get minted as agents — attribution is the product.
1059
+ // Both arms authenticate with the original secret key
1060
+ // (`controlPlaneApiKey()`), never the wide-scope `rk_` the startup exchange
1061
+ // installed as the sync credential. A derived agent credential silently
1062
+ // replacing the secret key on control-plane calls is how humans would get
1063
+ // minted as agents — and correct attribution is the point.
1723
1064
  async create(params) {
1724
- // Both mint doors (`{ user }` → /auth/ephemeral-keys → `ek_`,
1065
+ // Both mint paths (`{ user }` → /auth/ephemeral-keys → `ek_`,
1725
1066
  // `{ agent, can }` → /auth/capability → scoped `rk_`) resolve their
1726
1067
  // control-plane context through the shared `buildMintContext`, so this
1727
- // client, `agents.create`, and the stateless HTTP client can never drift
1728
- // on how a token is minted.
1068
+ // client, `agents.create`, and the stateless HTTP client can't drift on
1069
+ // how a token is minted.
1729
1070
  return mintSession(params, await buildMintContext('sessions.create'));
1730
1071
  },
1731
1072
  },
1732
- // Mint a scoped agent IDENTITY and hand back a connected client bound to it
1733
- // `sessions.create({ agent })` + `Ablo({ apiKey })` fused into one call,
1734
- // for agents that run in THIS (sk_-holding) process. Omitting `id` yields a
1735
- // fresh uuid per call, so concurrent agents are distinct participants that
1736
- // queue behind each other (even when they share a `name`). Humans don't get
1737
- // a server-built client — ship them a token via `sessions.create({ user })`.
1073
+ // Mint a scoped agent identity and hand back a connected client bound to it
1074
+ // `sessions.create({ agent })` plus `Ablo({ apiKey })` fused into one call,
1075
+ // for agents that run in this (secret-key-holding) process. Omitting `id`
1076
+ // yields a fresh uuid per call, so concurrent agents are distinct participants
1077
+ // that queue behind each other (even when they share a `name`). Humans don't
1078
+ // get a server-built client — ship them a token via `sessions.create({ user })`.
1738
1079
  agents: {
1739
1080
  async create(params) {
1740
1081
  // Distinct participant by default: omit `id` → a fresh uuid, so even two
1741
- // agents that share a `name` are INDEPENDENT participants and queue
1082
+ // agents that share a `name` are independent participants and queue
1742
1083
  // behind one another. `name` is display only (→ userMeta.name); it never
1743
1084
  // derives the id. Pass an explicit `id` only to re-attach an agent to
1744
1085
  // its own held claims.
@@ -1752,7 +1093,7 @@ export function Ablo(options) {
1752
1093
  ...(userMeta ? { userMeta } : {}),
1753
1094
  };
1754
1095
  // Re-mint the `rk_` on every resolver call so a long-lived agent client
1755
- // never hits token expiry; the `sk_` stays in THIS process — the child
1096
+ // never hits token expiry; the `sk_` stays in this process — the child
1756
1097
  // only ever sees its own short-lived `rk_`.
1757
1098
  const mintToken = async () => (await mintSession(sessionParams, await buildMintContext('agents.create')))
1758
1099
  .token;
@@ -1799,7 +1140,7 @@ export function Ablo(options) {
1799
1140
  * the session (WebSocket close code 1008/4001/4003 or a session_error
1800
1141
  * frame). Multiple subscribers supported; returns an unsubscribe
1801
1142
  * function. Consumers typically use this to trigger auth-failed UI
1802
- * flows (e.g., redirect to sign-in). Does NOT automatically purge the
1143
+ * flows (e.g., redirect to sign-in). Does not automatically purge the
1803
1144
  * IndexedDB — call `engine.purge()` from the listener if you need
1804
1145
  * that behavior (the SDK's `<AbloProvider>` does this by default).
1805
1146
  */
@@ -1826,7 +1167,7 @@ export function Ablo(options) {
1826
1167
  // the pool). Prefixed with _ to signal "internal but stable."
1827
1168
  /** The BaseSyncedStore — implements SyncStoreContract for SyncContext.Provider. */
1828
1169
  get _store() { return store; },
1829
- /** The ObjectPool — for demand loaders that need pool.createFromData(). */
1170
+ /** The InstanceCache — for demand loaders that need pool.createFromData(). */
1830
1171
  get _pool() { return objectPool; },
1831
1172
  /** The SyncWebSocket — for collaboration events (slide selection, cursors). */
1832
1173
  get _ws() { return store.getSyncWebSocket() ?? null; },