@abloatai/ablo 0.35.0 → 0.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (622) hide show
  1. package/AGENTS.md +2 -2
  2. package/CHANGELOG.md +71 -1929
  3. package/NOTICE +2 -2
  4. package/README.md +23 -532
  5. package/assets/banner.png +0 -0
  6. package/dist/auth.d.ts +2 -0
  7. package/dist/auth.d.ts.map +1 -0
  8. package/dist/auth.js +2 -0
  9. package/dist/auth.js.map +1 -0
  10. package/dist/client.d.ts +3 -0
  11. package/dist/client.d.ts.map +1 -0
  12. package/dist/client.js +3 -0
  13. package/dist/client.js.map +1 -0
  14. package/dist/coordination.d.ts +2 -0
  15. package/dist/coordination.d.ts.map +1 -0
  16. package/dist/coordination.js +2 -0
  17. package/dist/coordination.js.map +1 -0
  18. package/dist/index.d.ts +3 -112
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +3 -161
  21. package/dist/index.js.map +1 -0
  22. package/dist/react.d.ts +4 -0
  23. package/dist/react.d.ts.map +1 -0
  24. package/dist/react.js +3 -0
  25. package/dist/react.js.map +1 -0
  26. package/dist/schema.d.ts +2 -0
  27. package/dist/schema.d.ts.map +1 -0
  28. package/dist/schema.js +2 -0
  29. package/dist/schema.js.map +1 -0
  30. package/dist/server.d.ts +2 -0
  31. package/dist/server.d.ts.map +1 -0
  32. package/dist/server.js +2 -0
  33. package/dist/server.js.map +1 -0
  34. package/dist/source-conformance.d.ts +2 -0
  35. package/dist/source-conformance.d.ts.map +1 -0
  36. package/dist/source-conformance.js +2 -0
  37. package/dist/source-conformance.js.map +1 -0
  38. package/dist/source-drizzle.d.ts +2 -0
  39. package/dist/source-drizzle.d.ts.map +1 -0
  40. package/dist/source-drizzle.js +2 -0
  41. package/dist/source-drizzle.js.map +1 -0
  42. package/dist/source-kysely.d.ts +2 -0
  43. package/dist/source-kysely.d.ts.map +1 -0
  44. package/dist/source-kysely.js +2 -0
  45. package/dist/source-kysely.js.map +1 -0
  46. package/dist/source-next.d.ts +2 -0
  47. package/dist/source-next.d.ts.map +1 -0
  48. package/dist/source-next.js +2 -0
  49. package/dist/source-next.js.map +1 -0
  50. package/dist/source.d.ts +2 -0
  51. package/dist/source.d.ts.map +1 -0
  52. package/dist/source.js +2 -0
  53. package/dist/source.js.map +1 -0
  54. package/dist/wire.d.ts +2 -0
  55. package/dist/wire.d.ts.map +1 -0
  56. package/dist/wire.js +2 -0
  57. package/dist/wire.js.map +1 -0
  58. package/docs/agents.md +2 -2
  59. package/docs/api-keys.md +13 -12
  60. package/docs/api.md +15 -53
  61. package/docs/audit.md +4 -3
  62. package/docs/cli.md +11 -11
  63. package/docs/client-behavior.md +9 -9
  64. package/docs/concurrency-convention.md +28 -42
  65. package/docs/coordination.md +228 -86
  66. package/docs/data-sources.md +5 -5
  67. package/docs/debugging.md +34 -12
  68. package/docs/deployment.md +8 -8
  69. package/docs/examples/agent-human.md +4 -4
  70. package/docs/examples/ai-sdk-tool.md +1 -1
  71. package/docs/examples/existing-python-backend.md +15 -4
  72. package/docs/examples/nextjs.md +27 -6
  73. package/docs/examples/scoped-agent.md +3 -3
  74. package/docs/examples/server-agent.md +2 -2
  75. package/docs/groups.md +57 -3
  76. package/docs/guarantees.md +37 -10
  77. package/docs/how-it-works.md +32 -8
  78. package/docs/idempotency.md +6 -6
  79. package/docs/identity.md +24 -24
  80. package/docs/index.md +8 -8
  81. package/docs/integration-guide.md +38 -16
  82. package/docs/internal/README.md +18 -0
  83. package/docs/internal/agent-fleet-coordination-design.md +171 -0
  84. package/docs/internal/agent-orchestration.md +58 -0
  85. package/docs/internal/commit-identifiers.md +91 -0
  86. package/docs/internal/concurrency-open-decisions.md +37 -0
  87. package/docs/internal/data-source-reverse-channel.md +150 -0
  88. package/docs/internal/per-field-conflict-detection.md +165 -0
  89. package/docs/internal/postgres-replication.md +64 -0
  90. package/docs/internal/serializable-schema.md +119 -0
  91. package/docs/internal/structure.md +32 -0
  92. package/docs/mcp.md +9 -9
  93. package/docs/migration.md +37 -18
  94. package/docs/projects.md +1 -1
  95. package/docs/quickstart.md +2 -2
  96. package/docs/react.md +24 -13
  97. package/docs/schema-contract.md +3 -3
  98. package/docs/sessions.md +91 -37
  99. package/docs/webhooks.md +9 -9
  100. package/examples/README.md +2 -2
  101. package/examples/data-source/README.md +1 -1
  102. package/examples/data-source/ablo-driver.ts +1 -1
  103. package/examples/data-source/customer-server.ts +1 -1
  104. package/examples/data-source/run.ts +1 -1
  105. package/examples/data-source/schema.ts +1 -1
  106. package/examples/quickstart.ts +2 -2
  107. package/llms.txt +12 -12
  108. package/package.json +64 -174
  109. package/dist/BaseSyncedStore.d.ts +0 -823
  110. package/dist/BaseSyncedStore.js +0 -1955
  111. package/dist/Database.d.ts +0 -335
  112. package/dist/Database.js +0 -1500
  113. package/dist/InstanceCache.d.ts +0 -233
  114. package/dist/InstanceCache.js +0 -1164
  115. package/dist/LazyReferenceCollection.d.ts +0 -177
  116. package/dist/LazyReferenceCollection.js +0 -461
  117. package/dist/Model.d.ts +0 -444
  118. package/dist/Model.js +0 -909
  119. package/dist/ModelRegistry.d.ts +0 -221
  120. package/dist/ModelRegistry.js +0 -537
  121. package/dist/NetworkMonitor.d.ts +0 -26
  122. package/dist/NetworkMonitor.js +0 -77
  123. package/dist/RuntimeContext.d.ts +0 -52
  124. package/dist/RuntimeContext.js +0 -80
  125. package/dist/SyncClient.d.ts +0 -551
  126. package/dist/SyncClient.js +0 -2199
  127. package/dist/adapters/alwaysOnline.d.ts +0 -14
  128. package/dist/adapters/alwaysOnline.js +0 -17
  129. package/dist/adapters/inMemoryStorage.d.ts +0 -31
  130. package/dist/adapters/inMemoryStorage.js +0 -110
  131. package/dist/ai-sdk/coordinatedTool.d.ts +0 -120
  132. package/dist/ai-sdk/coordinatedTool.js +0 -134
  133. package/dist/ai-sdk/coordinationContext.d.ts +0 -46
  134. package/dist/ai-sdk/coordinationContext.js +0 -106
  135. package/dist/ai-sdk/index.d.ts +0 -121
  136. package/dist/ai-sdk/index.js +0 -121
  137. package/dist/ai-sdk/wrap.d.ts +0 -65
  138. package/dist/ai-sdk/wrap.js +0 -39
  139. package/dist/auth/index.d.ts +0 -1
  140. package/dist/auth/index.js +0 -8
  141. package/dist/batching/index.d.ts +0 -55
  142. package/dist/batching/index.js +0 -147
  143. package/dist/cli.cjs +0 -288600
  144. package/dist/client/Ablo.d.ts +0 -231
  145. package/dist/client/Ablo.js +0 -149
  146. package/dist/client/abloClient.d.ts +0 -309
  147. package/dist/client/abloClient.js +0 -13
  148. package/dist/client/clientPrelude.d.ts +0 -52
  149. package/dist/client/clientPrelude.js +0 -60
  150. package/dist/client/consoleLogger.d.ts +0 -35
  151. package/dist/client/consoleLogger.js +0 -44
  152. package/dist/client/coreClient.d.ts +0 -60
  153. package/dist/client/coreClient.js +0 -118
  154. package/dist/client/createInternalComponents.d.ts +0 -46
  155. package/dist/client/createInternalComponents.js +0 -92
  156. package/dist/client/createModelProxy.d.ts +0 -228
  157. package/dist/client/createModelProxy.js +0 -818
  158. package/dist/client/humans.d.ts +0 -48
  159. package/dist/client/humans.js +0 -52
  160. package/dist/client/modelRegistration.d.ts +0 -10
  161. package/dist/client/modelRegistration.js +0 -312
  162. package/dist/client/options.d.ts +0 -461
  163. package/dist/client/options.js +0 -7
  164. package/dist/client/reactiveEngine.d.ts +0 -48
  165. package/dist/client/reactiveEngine.js +0 -910
  166. package/dist/client/resourceTypes.d.ts +0 -12
  167. package/dist/client/resourceTypes.js +0 -10
  168. package/dist/client/schemaConfig.d.ts +0 -44
  169. package/dist/client/schemaConfig.js +0 -185
  170. package/dist/client/validateAbloOptions.d.ts +0 -42
  171. package/dist/client/validateAbloOptions.js +0 -43
  172. package/dist/client/wsMutationExecutor.d.ts +0 -27
  173. package/dist/client/wsMutationExecutor.js +0 -72
  174. package/dist/context.d.ts +0 -29
  175. package/dist/context.js +0 -58
  176. package/dist/coordination/ClaimLog.d.ts +0 -26
  177. package/dist/coordination/ClaimLog.js +0 -32
  178. package/dist/coordination/index.d.ts +0 -1
  179. package/dist/coordination/index.js +0 -8
  180. package/dist/core/DatabaseManager.d.ts +0 -105
  181. package/dist/core/DatabaseManager.js +0 -387
  182. package/dist/core/QueryProcessor.d.ts +0 -75
  183. package/dist/core/QueryProcessor.js +0 -255
  184. package/dist/core/QueryView.d.ts +0 -79
  185. package/dist/core/QueryView.js +0 -218
  186. package/dist/core/StoreManager.d.ts +0 -112
  187. package/dist/core/StoreManager.js +0 -302
  188. package/dist/core/ViewRegistry.d.ts +0 -20
  189. package/dist/core/ViewRegistry.js +0 -55
  190. package/dist/core/index.d.ts +0 -33
  191. package/dist/core/index.js +0 -48
  192. package/dist/core/openIDBWithTimeout.d.ts +0 -65
  193. package/dist/core/openIDBWithTimeout.js +0 -153
  194. package/dist/core/queryUtils.d.ts +0 -45
  195. package/dist/core/queryUtils.js +0 -69
  196. package/dist/core/storeContract.d.ts +0 -145
  197. package/dist/core/storeContract.js +0 -12
  198. package/dist/docs/catalog.d.ts +0 -72
  199. package/dist/docs/catalog.js +0 -227
  200. package/dist/docs/index.d.ts +0 -10
  201. package/dist/docs/index.js +0 -10
  202. package/dist/environment.d.ts +0 -1
  203. package/dist/environment.js +0 -8
  204. package/dist/interfaces/index.d.ts +0 -311
  205. package/dist/interfaces/index.js +0 -9
  206. package/dist/keys/index.d.ts +0 -1
  207. package/dist/keys/index.js +0 -8
  208. package/dist/mutators/RecordingMutation.d.ts +0 -36
  209. package/dist/mutators/RecordingMutation.js +0 -182
  210. package/dist/mutators/Transaction.d.ts +0 -40
  211. package/dist/mutators/Transaction.js +0 -58
  212. package/dist/mutators/UndoManager.d.ts +0 -258
  213. package/dist/mutators/UndoManager.js +0 -658
  214. package/dist/mutators/defineMutators.d.ts +0 -60
  215. package/dist/mutators/defineMutators.js +0 -18
  216. package/dist/mutators/inverseOp.d.ts +0 -126
  217. package/dist/mutators/inverseOp.js +0 -71
  218. package/dist/mutators/mutateActions.d.ts +0 -45
  219. package/dist/mutators/mutateActions.js +0 -105
  220. package/dist/mutators/readerActions.d.ts +0 -33
  221. package/dist/mutators/readerActions.js +0 -57
  222. package/dist/mutators/undoApply.d.ts +0 -51
  223. package/dist/mutators/undoApply.js +0 -117
  224. package/dist/policy/index.d.ts +0 -21
  225. package/dist/policy/index.js +0 -20
  226. package/dist/query/client.d.ts +0 -61
  227. package/dist/query/client.js +0 -137
  228. package/dist/query/types.d.ts +0 -85
  229. package/dist/query/types.js +0 -16
  230. package/dist/react/AbloProvider.d.ts +0 -230
  231. package/dist/react/AbloProvider.js +0 -455
  232. package/dist/react/ClientSideSuspense.d.ts +0 -36
  233. package/dist/react/ClientSideSuspense.js +0 -17
  234. package/dist/react/DefaultFallback.d.ts +0 -24
  235. package/dist/react/DefaultFallback.js +0 -43
  236. package/dist/react/context.d.ts +0 -55
  237. package/dist/react/context.js +0 -29
  238. package/dist/react/index.d.ts +0 -61
  239. package/dist/react/index.js +0 -66
  240. package/dist/react/internalContext.d.ts +0 -33
  241. package/dist/react/internalContext.js +0 -3
  242. package/dist/react/useAblo.d.ts +0 -75
  243. package/dist/react/useAblo.js +0 -102
  244. package/dist/react/useCurrentUserId.d.ts +0 -22
  245. package/dist/react/useCurrentUserId.js +0 -34
  246. package/dist/react/useErrorListener.d.ts +0 -20
  247. package/dist/react/useErrorListener.js +0 -38
  248. package/dist/react/useMutationFailureListener.d.ts +0 -26
  249. package/dist/react/useMutationFailureListener.js +0 -38
  250. package/dist/react/useMutators.d.ts +0 -56
  251. package/dist/react/useMutators.js +0 -84
  252. package/dist/react/useReactive.d.ts +0 -35
  253. package/dist/react/useReactive.js +0 -123
  254. package/dist/react/useSyncStatus.d.ts +0 -59
  255. package/dist/react/useSyncStatus.js +0 -76
  256. package/dist/react/useUndoScope.d.ts +0 -34
  257. package/dist/react/useUndoScope.js +0 -81
  258. package/dist/schema/coordination.d.ts +0 -112
  259. package/dist/schema/coordination.js +0 -129
  260. package/dist/schema/ddl.d.ts +0 -97
  261. package/dist/schema/ddl.js +0 -491
  262. package/dist/schema/ddlLock.d.ts +0 -35
  263. package/dist/schema/ddlLock.js +0 -46
  264. package/dist/schema/diff.d.ts +0 -225
  265. package/dist/schema/diff.js +0 -289
  266. package/dist/schema/generate.d.ts +0 -19
  267. package/dist/schema/generate.js +0 -86
  268. package/dist/schema/index.d.ts +0 -41
  269. package/dist/schema/index.js +0 -76
  270. package/dist/schema/queries.d.ts +0 -201
  271. package/dist/schema/queries.js +0 -144
  272. package/dist/schema/select.d.ts +0 -40
  273. package/dist/schema/select.js +0 -87
  274. package/dist/schema/serialize.d.ts +0 -115
  275. package/dist/schema/serialize.js +0 -262
  276. package/dist/schema/sugar.d.ts +0 -109
  277. package/dist/schema/sugar.js +0 -83
  278. package/dist/schema/syncDeltaRow.d.ts +0 -6
  279. package/dist/schema/syncDeltaRow.js +0 -6
  280. package/dist/server/adapter.d.ts +0 -173
  281. package/dist/server/adapter.js +0 -18
  282. package/dist/server/commit.d.ts +0 -107
  283. package/dist/server/commit.js +0 -1
  284. package/dist/server/index.d.ts +0 -14
  285. package/dist/server/index.js +0 -2
  286. package/dist/server/readConfig.d.ts +0 -80
  287. package/dist/server/readConfig.js +0 -8
  288. package/dist/server/storageMode.d.ts +0 -23
  289. package/dist/server/storageMode.js +0 -17
  290. package/dist/source/adapter.d.ts +0 -81
  291. package/dist/source/adapter.js +0 -22
  292. package/dist/source/adapters/drizzle.d.ts +0 -48
  293. package/dist/source/adapters/drizzle.js +0 -219
  294. package/dist/source/adapters/kysely.d.ts +0 -42
  295. package/dist/source/adapters/kysely.js +0 -205
  296. package/dist/source/adapters/kyselyMutationCore.d.ts +0 -76
  297. package/dist/source/adapters/kyselyMutationCore.js +0 -125
  298. package/dist/source/adapters/memory.d.ts +0 -13
  299. package/dist/source/adapters/memory.js +0 -130
  300. package/dist/source/adapters/prisma.d.ts +0 -63
  301. package/dist/source/adapters/prisma.js +0 -202
  302. package/dist/source/conformance.d.ts +0 -37
  303. package/dist/source/conformance.js +0 -215
  304. package/dist/source/connector.d.ts +0 -95
  305. package/dist/source/connector.js +0 -266
  306. package/dist/source/connectorProtocol.d.ts +0 -154
  307. package/dist/source/connectorProtocol.js +0 -163
  308. package/dist/source/contract.d.ts +0 -195
  309. package/dist/source/contract.js +0 -164
  310. package/dist/source/factory.d.ts +0 -92
  311. package/dist/source/factory.js +0 -286
  312. package/dist/source/footprint.d.ts +0 -111
  313. package/dist/source/footprint.js +0 -0
  314. package/dist/source/idempotency.d.ts +0 -61
  315. package/dist/source/idempotency.js +0 -144
  316. package/dist/source/index.d.ts +0 -23
  317. package/dist/source/index.js +0 -30
  318. package/dist/source/migrations.d.ts +0 -21
  319. package/dist/source/migrations.js +0 -103
  320. package/dist/source/next.d.ts +0 -32
  321. package/dist/source/next.js +0 -25
  322. package/dist/source/pushQueue.d.ts +0 -134
  323. package/dist/source/pushQueue.js +0 -256
  324. package/dist/source/signing.d.ts +0 -92
  325. package/dist/source/signing.js +0 -162
  326. package/dist/source/types.d.ts +0 -401
  327. package/dist/source/types.js +0 -59
  328. package/dist/stores/ObjectStore.d.ts +0 -115
  329. package/dist/stores/ObjectStore.js +0 -393
  330. package/dist/stores/ObjectStoreContract.d.ts +0 -38
  331. package/dist/stores/ObjectStoreContract.js +0 -1
  332. package/dist/stores/SyncActionStore.d.ts +0 -97
  333. package/dist/stores/SyncActionStore.js +0 -504
  334. package/dist/stores/syncAction.d.ts +0 -26
  335. package/dist/stores/syncAction.js +0 -16
  336. package/dist/surface.d.ts +0 -36
  337. package/dist/surface.js +0 -75
  338. package/dist/sync/BootstrapFetcher.d.ts +0 -280
  339. package/dist/sync/BootstrapFetcher.js +0 -962
  340. package/dist/sync/ConnectionManager.d.ts +0 -8
  341. package/dist/sync/ConnectionManager.js +0 -8
  342. package/dist/sync/OnDemandLoader.d.ts +0 -228
  343. package/dist/sync/OnDemandLoader.js +0 -742
  344. package/dist/sync/SubscriptionManager.d.ts +0 -159
  345. package/dist/sync/SubscriptionManager.js +0 -243
  346. package/dist/sync/SyncWebSocket.d.ts +0 -173
  347. package/dist/sync/SyncWebSocket.js +0 -438
  348. package/dist/sync/awaitClaimGrant.d.ts +0 -6
  349. package/dist/sync/awaitClaimGrant.js +0 -6
  350. package/dist/sync/bootstrapApply.d.ts +0 -70
  351. package/dist/sync/bootstrapApply.js +0 -73
  352. package/dist/sync/commitFrames.d.ts +0 -8
  353. package/dist/sync/commitFrames.js +0 -8
  354. package/dist/sync/contextPorts.d.ts +0 -18
  355. package/dist/sync/contextPorts.js +0 -31
  356. package/dist/sync/createClaimStream.d.ts +0 -7
  357. package/dist/sync/createClaimStream.js +0 -7
  358. package/dist/sync/createPresenceStream.d.ts +0 -69
  359. package/dist/sync/createPresenceStream.js +0 -200
  360. package/dist/sync/createSnapshot.d.ts +0 -29
  361. package/dist/sync/createSnapshot.js +0 -118
  362. package/dist/sync/credentialLifecycle.d.ts +0 -7
  363. package/dist/sync/credentialLifecycle.js +0 -7
  364. package/dist/sync/deltaPipeline.d.ts +0 -113
  365. package/dist/sync/deltaPipeline.js +0 -261
  366. package/dist/sync/groupChange.d.ts +0 -113
  367. package/dist/sync/groupChange.js +0 -242
  368. package/dist/sync/participants.d.ts +0 -115
  369. package/dist/sync/participants.js +0 -344
  370. package/dist/sync/persistedPrefix.d.ts +0 -12
  371. package/dist/sync/persistedPrefix.js +0 -22
  372. package/dist/sync/schemaDrift.d.ts +0 -55
  373. package/dist/sync/schemaDrift.js +0 -53
  374. package/dist/sync/schemas.d.ts +0 -70
  375. package/dist/sync/schemas.js +0 -94
  376. package/dist/sync/syncCursor.d.ts +0 -40
  377. package/dist/sync/syncCursor.js +0 -55
  378. package/dist/sync/syncPlan.d.ts +0 -54
  379. package/dist/sync/syncPlan.js +0 -50
  380. package/dist/sync/wsFrameHandlers.d.ts +0 -8
  381. package/dist/sync/wsFrameHandlers.js +0 -8
  382. package/dist/testing/fixtures/bootstrap.d.ts +0 -49
  383. package/dist/testing/fixtures/bootstrap.js +0 -59
  384. package/dist/testing/fixtures/deltas.d.ts +0 -83
  385. package/dist/testing/fixtures/deltas.js +0 -136
  386. package/dist/testing/fixtures/httpResponses.d.ts +0 -70
  387. package/dist/testing/fixtures/httpResponses.js +0 -90
  388. package/dist/testing/fixtures/models.d.ts +0 -83
  389. package/dist/testing/fixtures/models.js +0 -272
  390. package/dist/testing/helpers/reactWrapper.d.ts +0 -69
  391. package/dist/testing/helpers/reactWrapper.js +0 -67
  392. package/dist/testing/helpers/syncEngineHarness.d.ts +0 -54
  393. package/dist/testing/helpers/syncEngineHarness.js +0 -73
  394. package/dist/testing/helpers/wait.d.ts +0 -30
  395. package/dist/testing/helpers/wait.js +0 -49
  396. package/dist/testing/index.d.ts +0 -23
  397. package/dist/testing/index.js +0 -33
  398. package/dist/testing/mocks/FakeDatabase.d.ts +0 -18
  399. package/dist/testing/mocks/FakeDatabase.js +0 -10
  400. package/dist/testing/mocks/MockMutationExecutor.d.ts +0 -87
  401. package/dist/testing/mocks/MockMutationExecutor.js +0 -186
  402. package/dist/testing/mocks/MockNetworkMonitor.d.ts +0 -20
  403. package/dist/testing/mocks/MockNetworkMonitor.js +0 -46
  404. package/dist/testing/mocks/MockSyncContext.d.ts +0 -51
  405. package/dist/testing/mocks/MockSyncContext.js +0 -72
  406. package/dist/testing/mocks/MockSyncStore.d.ts +0 -88
  407. package/dist/testing/mocks/MockSyncStore.js +0 -171
  408. package/dist/testing/mocks/MockWebSocket.d.ts +0 -71
  409. package/dist/testing/mocks/MockWebSocket.js +0 -118
  410. package/dist/transaction/ablo.d.ts +0 -88
  411. package/dist/transaction/ablo.js +0 -33
  412. package/dist/transaction/auth/apiKey.d.ts +0 -152
  413. package/dist/transaction/auth/apiKey.js +0 -419
  414. package/dist/transaction/auth/bootstrapScope.d.ts +0 -15
  415. package/dist/transaction/auth/bootstrapScope.js +0 -1
  416. package/dist/transaction/auth/capability.d.ts +0 -177
  417. package/dist/transaction/auth/capability.js +0 -199
  418. package/dist/transaction/auth/credentialEndpoint.d.ts +0 -61
  419. package/dist/transaction/auth/credentialEndpoint.js +0 -86
  420. package/dist/transaction/auth/credentialPolicy.d.ts +0 -148
  421. package/dist/transaction/auth/credentialPolicy.js +0 -125
  422. package/dist/transaction/auth/credentialSource.d.ts +0 -30
  423. package/dist/transaction/auth/credentialSource.js +0 -55
  424. package/dist/transaction/auth/hostedEndpoints.d.ts +0 -21
  425. package/dist/transaction/auth/hostedEndpoints.js +0 -21
  426. package/dist/transaction/auth/identity.d.ts +0 -55
  427. package/dist/transaction/auth/identity.js +0 -210
  428. package/dist/transaction/auth/index.d.ts +0 -162
  429. package/dist/transaction/auth/index.js +0 -304
  430. package/dist/transaction/auth/schemas.d.ts +0 -59
  431. package/dist/transaction/auth/schemas.js +0 -85
  432. package/dist/transaction/auth/sessionMint.d.ts +0 -28
  433. package/dist/transaction/auth/sessionMint.js +0 -85
  434. package/dist/transaction/coordination/awaitClaimGrant.d.ts +0 -49
  435. package/dist/transaction/coordination/awaitClaimGrant.js +0 -112
  436. package/dist/transaction/coordination/claimHeartbeatLoop.d.ts +0 -50
  437. package/dist/transaction/coordination/claimHeartbeatLoop.js +0 -88
  438. package/dist/transaction/coordination/claimMeta.d.ts +0 -49
  439. package/dist/transaction/coordination/claimMeta.js +0 -52
  440. package/dist/transaction/coordination/createClaimStream.d.ts +0 -64
  441. package/dist/transaction/coordination/createClaimStream.js +0 -475
  442. package/dist/transaction/coordination/events.d.ts +0 -74
  443. package/dist/transaction/coordination/events.js +0 -7
  444. package/dist/transaction/coordination/index.d.ts +0 -19
  445. package/dist/transaction/coordination/index.js +0 -44
  446. package/dist/transaction/coordination/locator.d.ts +0 -83
  447. package/dist/transaction/coordination/locator.js +0 -82
  448. package/dist/transaction/coordination/schema.d.ts +0 -1473
  449. package/dist/transaction/coordination/schema.js +0 -1013
  450. package/dist/transaction/coordination/targetConflict.d.ts +0 -2
  451. package/dist/transaction/coordination/targetConflict.js +0 -103
  452. package/dist/transaction/coordination/trace.d.ts +0 -78
  453. package/dist/transaction/coordination/trace.js +0 -138
  454. package/dist/transaction/durableWrites.d.ts +0 -62
  455. package/dist/transaction/durableWrites.js +0 -71
  456. package/dist/transaction/environment.d.ts +0 -105
  457. package/dist/transaction/environment.js +0 -108
  458. package/dist/transaction/errorCodes.d.ts +0 -403
  459. package/dist/transaction/errorCodes.js +0 -480
  460. package/dist/transaction/errors.d.ts +0 -428
  461. package/dist/transaction/errors.js +0 -686
  462. package/dist/transaction/index.d.ts +0 -20
  463. package/dist/transaction/index.js +0 -20
  464. package/dist/transaction/keys/index.d.ts +0 -87
  465. package/dist/transaction/keys/index.js +0 -207
  466. package/dist/transaction/log/syncDeltaRow.d.ts +0 -158
  467. package/dist/transaction/log/syncDeltaRow.js +0 -95
  468. package/dist/transaction/logPosition.d.ts +0 -97
  469. package/dist/transaction/logPosition.js +0 -125
  470. package/dist/transaction/logger.d.ts +0 -16
  471. package/dist/transaction/logger.js +0 -7
  472. package/dist/transaction/observability.d.ts +0 -53
  473. package/dist/transaction/observability.js +0 -19
  474. package/dist/transaction/persistence.d.ts +0 -12
  475. package/dist/transaction/persistence.js +0 -11
  476. package/dist/transaction/plugin.d.ts +0 -192
  477. package/dist/transaction/plugin.js +0 -87
  478. package/dist/transaction/policy/types.d.ts +0 -217
  479. package/dist/transaction/policy/types.js +0 -126
  480. package/dist/transaction/resources/functionalUpdate.d.ts +0 -79
  481. package/dist/transaction/resources/functionalUpdate.js +0 -87
  482. package/dist/transaction/resources/httpResources.d.ts +0 -266
  483. package/dist/transaction/resources/httpResources.js +0 -7
  484. package/dist/transaction/resources/modelOperations.d.ts +0 -319
  485. package/dist/transaction/resources/modelOperations.js +0 -12
  486. package/dist/transaction/resources/mutationOptions.d.ts +0 -66
  487. package/dist/transaction/resources/mutationOptions.js +0 -9
  488. package/dist/transaction/resources/where.d.ts +0 -85
  489. package/dist/transaction/resources/where.js +0 -70
  490. package/dist/transaction/resources/writeOptionsSchema.d.ts +0 -47
  491. package/dist/transaction/resources/writeOptionsSchema.js +0 -73
  492. package/dist/transaction/schema/field.d.ts +0 -126
  493. package/dist/transaction/schema/field.js +0 -265
  494. package/dist/transaction/schema/loadStrategy.d.ts +0 -45
  495. package/dist/transaction/schema/loadStrategy.js +0 -46
  496. package/dist/transaction/schema/model.d.ts +0 -379
  497. package/dist/transaction/schema/model.js +0 -123
  498. package/dist/transaction/schema/openapi.d.ts +0 -57
  499. package/dist/transaction/schema/openapi.js +0 -340
  500. package/dist/transaction/schema/relation.d.ts +0 -199
  501. package/dist/transaction/schema/relation.js +0 -104
  502. package/dist/transaction/schema/residency.d.ts +0 -29
  503. package/dist/transaction/schema/residency.js +0 -25
  504. package/dist/transaction/schema/roles.d.ts +0 -249
  505. package/dist/transaction/schema/roles.js +0 -230
  506. package/dist/transaction/schema/schema.d.ts +0 -324
  507. package/dist/transaction/schema/schema.js +0 -305
  508. package/dist/transaction/schema/tenancy.d.ts +0 -139
  509. package/dist/transaction/schema/tenancy.js +0 -190
  510. package/dist/transaction/transactionLayer.d.ts +0 -82
  511. package/dist/transaction/transactionLayer.js +0 -24
  512. package/dist/transaction/transactions/settlement/commitEnvelope.d.ts +0 -143
  513. package/dist/transaction/transactions/settlement/commitEnvelope.js +0 -161
  514. package/dist/transaction/transactions/settlement/httpCommitEnvelope.d.ts +0 -53
  515. package/dist/transaction/transactions/settlement/httpCommitEnvelope.js +0 -207
  516. package/dist/transaction/transactions/settlement/idempotencyKey.d.ts +0 -10
  517. package/dist/transaction/transactions/settlement/idempotencyKey.js +0 -9
  518. package/dist/transaction/transactions/settlement/pendingWrite.d.ts +0 -112
  519. package/dist/transaction/transactions/settlement/pendingWrite.js +0 -20
  520. package/dist/transaction/transport/commitFrames.d.ts +0 -90
  521. package/dist/transaction/transport/commitFrames.js +0 -134
  522. package/dist/transaction/transport/connectionManager.d.ts +0 -215
  523. package/dist/transaction/transport/connectionManager.js +0 -673
  524. package/dist/transaction/transport/credentialLifecycle.d.ts +0 -177
  525. package/dist/transaction/transport/credentialLifecycle.js +0 -324
  526. package/dist/transaction/transport/heartbeat.d.ts +0 -65
  527. package/dist/transaction/transport/heartbeat.js +0 -93
  528. package/dist/transaction/transport/httpClient.d.ts +0 -123
  529. package/dist/transaction/transport/httpClient.js +0 -145
  530. package/dist/transaction/transport/httpOptions.d.ts +0 -33
  531. package/dist/transaction/transport/httpOptions.js +0 -12
  532. package/dist/transaction/transport/httpTransport.d.ts +0 -8
  533. package/dist/transaction/transport/httpTransport.js +0 -1276
  534. package/dist/transaction/transport/networkProbe.d.ts +0 -84
  535. package/dist/transaction/transport/networkProbe.js +0 -207
  536. package/dist/transaction/transport/wsFrameHandlers.d.ts +0 -128
  537. package/dist/transaction/transport/wsFrameHandlers.js +0 -429
  538. package/dist/transaction/transport/wsTransport.d.ts +0 -576
  539. package/dist/transaction/transport/wsTransport.js +0 -1017
  540. package/dist/transaction/types/assertExact.d.ts +0 -17
  541. package/dist/transaction/types/assertExact.js +0 -1
  542. package/dist/transaction/types/global.d.ts +0 -107
  543. package/dist/transaction/types/global.js +0 -40
  544. package/dist/transaction/types/index.d.ts +0 -205
  545. package/dist/transaction/types/index.js +0 -56
  546. package/dist/transaction/types/modelData.d.ts +0 -10
  547. package/dist/transaction/types/modelData.js +0 -9
  548. package/dist/transaction/types/participant.d.ts +0 -20
  549. package/dist/transaction/types/participant.js +0 -10
  550. package/dist/transaction/types/streams.d.ts +0 -540
  551. package/dist/transaction/types/streams.js +0 -11
  552. package/dist/transaction/utils/asyncIterator.d.ts +0 -34
  553. package/dist/transaction/utils/asyncIterator.js +0 -135
  554. package/dist/transaction/utils/duration.d.ts +0 -25
  555. package/dist/transaction/utils/duration.js +0 -45
  556. package/dist/transaction/utils/json.d.ts +0 -57
  557. package/dist/transaction/utils/json.js +0 -276
  558. package/dist/transaction/wire/accountResponses.d.ts +0 -351
  559. package/dist/transaction/wire/accountResponses.js +0 -255
  560. package/dist/transaction/wire/auth.d.ts +0 -49
  561. package/dist/transaction/wire/auth.js +0 -57
  562. package/dist/transaction/wire/bootstrapReason.d.ts +0 -9
  563. package/dist/transaction/wire/bootstrapReason.js +0 -8
  564. package/dist/transaction/wire/claimEvent.d.ts +0 -76
  565. package/dist/transaction/wire/claimEvent.js +0 -73
  566. package/dist/transaction/wire/claims.d.ts +0 -463
  567. package/dist/transaction/wire/claims.js +0 -229
  568. package/dist/transaction/wire/commit.d.ts +0 -603
  569. package/dist/transaction/wire/commit.js +0 -321
  570. package/dist/transaction/wire/delta.d.ts +0 -250
  571. package/dist/transaction/wire/delta.js +0 -147
  572. package/dist/transaction/wire/errorEnvelope.d.ts +0 -72
  573. package/dist/transaction/wire/errorEnvelope.js +0 -123
  574. package/dist/transaction/wire/feedCursor.d.ts +0 -60
  575. package/dist/transaction/wire/feedCursor.js +0 -82
  576. package/dist/transaction/wire/feedEvent.d.ts +0 -177
  577. package/dist/transaction/wire/feedEvent.js +0 -39
  578. package/dist/transaction/wire/frames.d.ts +0 -194
  579. package/dist/transaction/wire/frames.js +0 -50
  580. package/dist/transaction/wire/inboundFrames.d.ts +0 -552
  581. package/dist/transaction/wire/inboundFrames.js +0 -116
  582. package/dist/transaction/wire/index.d.ts +0 -50
  583. package/dist/transaction/wire/index.js +0 -74
  584. package/dist/transaction/wire/listEnvelope.d.ts +0 -37
  585. package/dist/transaction/wire/listEnvelope.js +0 -42
  586. package/dist/transaction/wire/modelResponses.d.ts +0 -85
  587. package/dist/transaction/wire/modelResponses.js +0 -43
  588. package/dist/transaction/wire/protocol.d.ts +0 -38
  589. package/dist/transaction/wire/protocol.js +0 -38
  590. package/dist/transaction/wire/protocolVersion.d.ts +0 -73
  591. package/dist/transaction/wire/protocolVersion.js +0 -83
  592. package/dist/transactions/mutations/MutationQueue.d.ts +0 -655
  593. package/dist/transactions/mutations/MutationQueue.js +0 -2797
  594. package/dist/transactions/mutations/MutationStore.d.ts +0 -20
  595. package/dist/transactions/mutations/MutationStore.js +0 -53
  596. package/dist/transactions/mutations/UnconfirmedWrites.d.ts +0 -82
  597. package/dist/transactions/mutations/UnconfirmedWrites.js +0 -104
  598. package/dist/transactions/mutations/coalesceRules.d.ts +0 -58
  599. package/dist/transactions/mutations/coalesceRules.js +0 -140
  600. package/dist/transactions/mutations/commitLatency.d.ts +0 -52
  601. package/dist/transactions/mutations/commitLatency.js +0 -130
  602. package/dist/transactions/mutations/commitOutboxStore.d.ts +0 -28
  603. package/dist/transactions/mutations/commitOutboxStore.js +0 -26
  604. package/dist/transactions/mutations/commitPayload.d.ts +0 -164
  605. package/dist/transactions/mutations/commitPayload.js +0 -152
  606. package/dist/transactions/mutations/deltaConfirmation.d.ts +0 -59
  607. package/dist/transactions/mutations/deltaConfirmation.js +0 -233
  608. package/dist/transactions/mutations/durableWriteStore.d.ts +0 -14
  609. package/dist/transactions/mutations/durableWriteStore.js +0 -12
  610. package/dist/transactions/mutations/optimisticApply.d.ts +0 -49
  611. package/dist/transactions/mutations/optimisticApply.js +0 -65
  612. package/dist/transactions/mutations/replayValidation.d.ts +0 -186
  613. package/dist/transactions/mutations/replayValidation.js +0 -163
  614. package/dist/utils/mobxSetup.d.ts +0 -53
  615. package/dist/utils/mobxSetup.js +0 -330
  616. package/dist/webhooks/events.d.ts +0 -43
  617. package/dist/webhooks/events.js +0 -42
  618. package/dist/webhooks/index.d.ts +0 -8
  619. package/dist/webhooks/index.js +0 -8
  620. package/dist/wire/index.d.ts +0 -1
  621. package/dist/wire/index.js +0 -8
  622. package/docs/interaction-model.md +0 -99
@@ -1,962 +0,0 @@
1
- /**
2
- * Fetches the initial snapshot the sync engine needs before it can go live: the
3
- * current rows for the requested models plus the sync position from which to
4
- * resume live updates. It calls the sync server's `/sync/bootstrap` HTTP
5
- * endpoint, retries transient failures with backoff, and can fall back to a
6
- * cached snapshot when the device is offline. {@link BootstrapData} is the
7
- * shape it returns; {@link BootstrapOptions} configures it.
8
- */
9
- import { getContext } from '../context.js';
10
- import { AbloError, AbloSessionError, AbloConnectionError, translateHttpError, toAbloError, isRetryableCode } from '../transaction/errors.js';
11
- import { withAuthHeaders } from '../transaction/auth/credentialSource.js';
12
- import { classifySchemaDrift, describeSchemaDrift, } from './schemaDrift.js';
13
- // SyncObservability replaced by getContext().observability
14
- import { parseBootstrapResponse } from './schemas.js';
15
- /**
16
- * Rows per page for the chunked cold-start bootstrap. Matches the server's
17
- * hard cap on the `limit` query parameter — asking for more is silently
18
- * clamped, so this is the largest honest page.
19
- */
20
- const PAGE_LIMIT = 5000;
21
- /**
22
- * Runaway guard for the per-model paging loop: a server that keeps returning
23
- * a `nextCursor` past this many pages is looping, not paginating. At
24
- * {@link PAGE_LIMIT} rows per page this allows a million rows per model
25
- * before the loop is declared broken.
26
- */
27
- const MAX_PAGES_PER_MODEL = 200;
28
- /** How many model chunks a cold start fetches at once. */
29
- const CHUNK_CONCURRENCY = 3;
30
- /**
31
- * The reason handed to `abort()` when a request is stopped deliberately —
32
- * superseded by a newer bootstrap, or abandoned because the bootstrap it
33
- * belonged to had already failed elsewhere.
34
- */
35
- const cancelled = (why) => new AbloConnectionError(why, { code: 'bootstrap_cancelled' });
36
- /** Matches by `name` rather than `instanceof`: an abort that crosses a worker
37
- * boundary is structured-cloned, which drops the prototype. */
38
- const isAbortError = (value) => typeof value === 'object' &&
39
- value !== null &&
40
- 'name' in value &&
41
- value.name === 'AbortError';
42
- /**
43
- * What a failed request should report.
44
- *
45
- * A fetch aborted *with a reason* rejects with that exact reason object, so a
46
- * deliberate cancellation and a watchdog firing both arrive here already typed
47
- * and pass straight through — which is the whole point of passing one. Only a
48
- * bare abort needs translating: a signal aborted with no reason, the browser's
49
- * stop button, a closing tab. That case is the one that genuinely means the
50
- * transfer died, so it becomes a retryable timeout.
51
- *
52
- * The signal's reason is preferred over the thrown value because a pre-aborted
53
- * signal rejects before any request is made, and because interior code may have
54
- * wrapped the rejection on its way out.
55
- */
56
- function classifyRequestFailure(error, controller, diedMessage) {
57
- const reason = controller.signal.aborted ? controller.signal.reason : error;
58
- if (reason instanceof AbloError)
59
- return reason;
60
- if (isAbortError(reason)) {
61
- return new AbloConnectionError(diedMessage, {
62
- code: 'bootstrap_fetch_timeout',
63
- ...(reason instanceof Error ? { cause: reason } : {}),
64
- });
65
- }
66
- return error instanceof Error ? error : new Error(String(error));
67
- }
68
- export class BootstrapFetcher {
69
- options;
70
- /**
71
- * Every in-flight request's controller, tagged with the lane it belongs to. A
72
- * registry rather than a single field because a chunked cold start runs
73
- * several model fetches concurrently — aborting one request (its own
74
- * TTFB/stall watchdog) must never take its siblings down, while
75
- * {@link abort} takes down all of them.
76
- */
77
- activeControllers = new Map();
78
- /**
79
- * Non-scoped bootstraps currently running, keyed by request identity. A
80
- * second call for the same snapshot joins the one already in flight rather
81
- * than cancelling and restarting it — see {@link fetchBootstrap}.
82
- */
83
- flights = new Map();
84
- /** Warn about schema drift at most once per helper. */
85
- schemaDriftWarned = false;
86
- /**
87
- * Abort every in-flight request in `lane` — or in every lane when none is
88
- * given — with an explicit reason.
89
- *
90
- * The reason is load-bearing, not decoration. `fetch` rejects with the exact
91
- * value handed to `abort()`, so passing a typed error is what lets the retry
92
- * loop below tell a deliberate cancellation apart from a dead connection. A
93
- * bare `abort()` produces an `AbortError` indistinguishable from the one the
94
- * browser's stop button produces, and a retry loop that cannot tell them
95
- * apart re-issues the requests it just killed.
96
- */
97
- cancelActive(reason, lane) {
98
- for (const [controller, controllerLane] of this.activeControllers) {
99
- if (lane !== undefined && controllerLane !== lane)
100
- continue;
101
- controller.abort(reason);
102
- this.activeControllers.delete(controller);
103
- }
104
- }
105
- /**
106
- * The longest a single bootstrap can run before every watchdog below has
107
- * necessarily fired, derived from those watchdogs rather than guessed. A
108
- * caller wanting an outer deadline reads this instead of picking a number,
109
- * so it cannot set one shorter than the work it wraps. A cold start pages
110
- * through its models {@link CHUNK_CONCURRENCY} at a time; each request may
111
- * spend `fetchTimeout` waiting for response headers and `stallTimeout`
112
- * waiting for the next body chunk, and may be retried `maxRetries` times.
113
- */
114
- get budgetMs() {
115
- const models = Math.max(this.options.instantModels?.length ?? 1, 1);
116
- const waves = Math.ceil(models / CHUNK_CONCURRENCY);
117
- return (waves * (this.options.fetchTimeout + this.options.stallTimeout) * this.options.maxRetries);
118
- }
119
- get baseUrl() {
120
- return this.options.baseUrl;
121
- }
122
- /**
123
- * Advisory schema-drift check: compare the server's active schema hash (on the
124
- * bootstrap response) against the hash this client was built with. A mismatch
125
- * means the app's schema and the deployed schema have diverged — reads/writes
126
- * relying on undeployed changes will later fail with an opaque DB constraint
127
- * error. Warn once, actionably; never throws or blocks the bootstrap.
128
- *
129
- * The message names the SERVER it connected to, and spans all three real
130
- * causes rather than assuming "you forgot to push". Drift most often means the
131
- * schema was pushed to a different server, project, or environment than this
132
- * client points at (a bare `ablo push` targets the hosted default; a local app
133
- * usually reads a local server) — so the first, load-bearing pointer is `ablo
134
- * status`, which names the exact org/project/environment the key resolves to
135
- * and the deployed hash, turning "which of these is it?" into one glance. The
136
- * older "Run `ablo push`" copy sent everyone down one path and confused the
137
- * common wrong-target and version-skew cases.
138
- */
139
- warnOnSchemaDrift(serverHash) {
140
- if (this.schemaDriftWarned || !serverHash)
141
- return;
142
- const clientHash = getContext().config.expectedSchemaHash;
143
- if (!clientHash || clientHash === serverHash)
144
- return;
145
- // A projection (`selectModels`/`omitModels`) hashes its subset, which never
146
- // equals the full schema a server runs — so it also carries the source
147
- // schema's hash. Matching that means the client is a faithful subset of the
148
- // deployed schema: current, not drifted. Only warn when neither matches.
149
- const sourceHash = getContext().config.expectedSourceSchemaHash;
150
- if (sourceHash && sourceHash === serverHash)
151
- return;
152
- this.schemaDriftWarned = true;
153
- const org = this.options.organizationId;
154
- const where = org ? `${this.baseUrl} (org ${org})` : this.baseUrl;
155
- // The whole-schema hashes differ — but that alone can't distinguish "the
156
- // server gained models this build never touches" (fine, say nothing) from
157
- // "a model this client uses moved" (name it). Resolve the semantic answer
158
- // from the server's per-model surface before speaking; fall back to the
159
- // hash message only when that surface is unavailable (older server,
160
- // network hiccup). Fire-and-forget: never blocks or fails the bootstrap.
161
- const clientModels = getContext().config.expectedModelHashes;
162
- if (clientModels && Object.keys(clientModels).length > 0) {
163
- void this.resolveSemanticDrift(clientModels, clientHash, serverHash, where);
164
- return;
165
- }
166
- this.warnWholeHashDrift(clientHash, serverHash, where);
167
- }
168
- /** Fetch the server's per-model schema surface and warn precisely — or stay
169
- * silent when every model this client declares matches (additive lead). */
170
- async resolveSemanticDrift(clientModels, clientHash, serverHash, where) {
171
- try {
172
- const res = await fetch(`${this.options.baseUrl}/schema`, {
173
- method: 'GET',
174
- headers: withAuthHeaders(this.options.getAuthToken, {}, this.options.authToken),
175
- });
176
- if (!res.ok)
177
- throw new Error(`schema read-back ${res.status}`);
178
- const body = (await res.json());
179
- const models = Array.isArray(body.models)
180
- ? body.models.flatMap((m) => {
181
- const entry = m;
182
- return typeof entry.key === 'string'
183
- ? [{ key: entry.key, ...(typeof entry.hash === 'string' ? { hash: entry.hash } : {}) }]
184
- : [];
185
- })
186
- : [];
187
- const finding = classifySchemaDrift(clientModels, models);
188
- if (finding.kind === 'aligned')
189
- return; // additive server lead — not this client's concern
190
- if (finding.kind !== 'unknown') {
191
- getContext().logger.warn(describeSchemaDrift(finding, where), {
192
- clientSchemaHash: clientHash,
193
- serverSchemaHash: serverHash,
194
- serverUrl: this.baseUrl,
195
- ...(finding.kind === 'unpushed'
196
- ? { unpushedModels: finding.models }
197
- : { changedModels: finding.models, unpushedModels: finding.unpushed }),
198
- });
199
- return;
200
- }
201
- }
202
- catch {
203
- /* surface unavailable — fall through to the hash message */
204
- }
205
- this.warnWholeHashDrift(clientHash, serverHash, where);
206
- }
207
- warnWholeHashDrift(clientHash, serverHash, where) {
208
- const org = this.options.organizationId;
209
- // Self-brand the message ("Ablo:") rather than rely on the default logger's
210
- // `[Ablo]` namespace — consumers wiring their own logger (pino, etc.) lose
211
- // that prefix, and a drift warning that reads like the app's own log is
212
- // worse than none. The brand tells them at a glance who is talking.
213
- getContext().logger.warn(`Ablo: Schema drift — the schema this client was built with (${clientHash}) is not the ` +
214
- `one active on the server it connected to (${serverHash} at ${where}). Until they match, ` +
215
- `operations that depend on the difference will fail later with an opaque database error. ` +
216
- `This is usually one of three things. The schema may have been pushed to a different ` +
217
- `server, project, or environment than this client points at — run \`ablo status\` to see ` +
218
- `the exact org, project, and environment your key resolves to, alongside the deployed ` +
219
- `hash, and confirm they match here. Your local schema may simply not be pushed to this ` +
220
- `server yet — run \`ablo push\` against it. Or this client and the server may have been ` +
221
- `built with different Ablo versions, which can hash an identical schema differently — ` +
222
- `align the versions. This check is advisory and never blocks the connection.`, {
223
- clientSchemaHash: clientHash,
224
- serverSchemaHash: serverHash,
225
- serverUrl: this.baseUrl,
226
- ...(org ? { organizationId: org } : {}),
227
- });
228
- }
229
- constructor(options) {
230
- // Defaults are spread first; the explicit `baseUrl` then takes precedence,
231
- // resolved from `options.baseUrl` or the localhost fallback. Callers pass
232
- // the full base URL, including the `/api` prefix.
233
- this.options = {
234
- syncGroups: [],
235
- maxRetries: 3,
236
- retryDelay: 1000,
237
- // Time-to-first-byte bound only. The server currently materializes the
238
- // whole snapshot before sending headers, so a cold start on a large org
239
- // legitimately needs more than a "fail fast" allowance here.
240
- fetchTimeout: 20_000,
241
- stallTimeout: 15_000,
242
- ...options,
243
- baseUrl: options.baseUrl ?? 'http://localhost:8080/api',
244
- // Reading the deprecated `organizationId` is deliberate: it preserves the
245
- // cache namespace for callers that still construct BootstrapFetcher
246
- // directly with the old field instead of `cacheScope`.
247
- // eslint-disable-next-line @typescript-eslint/no-deprecated
248
- cacheScope: options.cacheScope ?? options.organizationId ?? null,
249
- };
250
- // Do not clear cache here; keep offline fallback available
251
- }
252
- /**
253
- * Update the offline-cache namespace once auth has resolved the server-side
254
- * account scope. This is intentionally not a public organizationId input.
255
- */
256
- setCacheScope(cacheScope) {
257
- if (cacheScope.trim().length === 0)
258
- return;
259
- this.options.cacheScope = cacheScope;
260
- }
261
- setSyncGroups(syncGroups) {
262
- this.options.syncGroups = [...(syncGroups ?? [])];
263
- }
264
- /**
265
- * Sets a fixed credential for callers that construct the helper directly.
266
- * The SDK instead supplies `getAuthToken` and never calls this.
267
- */
268
- setAuthToken(authToken) {
269
- if (!authToken) {
270
- delete this.options.authToken;
271
- return;
272
- }
273
- this.options.authToken = authToken;
274
- }
275
- /**
276
- * Fetch bootstrap data from sync engine with partial bootstrap support
277
- * @param lastSyncId - Optional: client's current lastSyncId for partial bootstrap
278
- * @returns Bootstrap data (either full snapshot or delta batch)
279
- */
280
- async fetchBootstrap(lastSyncId,
281
- /**
282
- * A per-call set of sync groups for a scoped hydrate-on-enter. When given,
283
- * the request uses these groups instead of the configured `syncGroups`, and
284
- * does so without mutating the shared options, so a concurrent full
285
- * bootstrap is unaffected. It also bypasses the offline snapshot cache,
286
- * which holds the full bootstrap and would be a wrong answer to a subset
287
- * request.
288
- */
289
- syncGroupsOverride) {
290
- // A scoped hydrate answers a different question than the full bootstrap and
291
- // runs in its own lane: it never joins one, and is never superseded by one.
292
- if (syncGroupsOverride)
293
- return this.runBootstrap(lastSyncId, syncGroupsOverride);
294
- // Single-flight. Three callers reach this independently — first load,
295
- // background refresh, and reconnect — and before this they raced: each new
296
- // call cancelled whatever was running and started over, so a socket that
297
- // reconnected mid-cold-start restarted the whole snapshot, repeatedly. The
298
- // same request now joins the one in flight instead.
299
- const key = this.flightKey(lastSyncId);
300
- const joined = this.flights.get(key);
301
- if (joined) {
302
- getContext().logger.debug('Joining the bootstrap already in flight', { key });
303
- return joined;
304
- }
305
- // A request for something else genuinely does supersede: the running one is
306
- // not the answer being asked for. Retire it from the registry first, so a
307
- // caller arriving in the same tick cannot join a flight that is dying.
308
- if (this.flights.size > 0) {
309
- this.flights.clear();
310
- this.cancelActive(cancelled('Superseded by a newer bootstrap request'), 'bootstrap');
311
- }
312
- const flight = this.runBootstrap(lastSyncId);
313
- this.flights.set(key, flight);
314
- // The cleanup chain is terminated with `catch` so this derived promise can
315
- // never surface as an unhandled rejection even when every caller handled the
316
- // failure, and the delete is guarded by identity so a flight registered
317
- // after a supersede is not evicted by its predecessor's cleanup.
318
- void flight
319
- .catch(() => undefined)
320
- .finally(() => {
321
- if (this.flights.get(key) === flight)
322
- this.flights.delete(key);
323
- });
324
- return flight;
325
- }
326
- /**
327
- * The identity of a bootstrap request: everything that determines its answer.
328
- * Two calls with the same key are asking the same question, so the second can
329
- * take the first's result.
330
- */
331
- flightKey(lastSyncId) {
332
- return JSON.stringify({
333
- lastSyncId: lastSyncId !== undefined && lastSyncId > 0 ? lastSyncId : 0,
334
- syncGroups: [...this.options.syncGroups].sort(),
335
- models: [...(this.options.instantModels ?? [])].sort(),
336
- });
337
- }
338
- /** One bootstrap, start to finish. {@link fetchBootstrap} owns whether it runs. */
339
- async runBootstrap(lastSyncId, syncGroupsOverride) {
340
- // organizationId omitted — server reads it from auth identity.
341
- // See `fetchBootstrapWithETag` for the full rationale.
342
- const params = new URLSearchParams();
343
- // Add lastSyncId for partial bootstrap support
344
- if (lastSyncId !== undefined && lastSyncId > 0) {
345
- params.append('lastSyncId', lastSyncId.toString());
346
- }
347
- // Add sync groups (per-call override wins over the configured set).
348
- (syncGroupsOverride ?? this.options.syncGroups).forEach((group) => {
349
- params.append('syncGroups', group);
350
- });
351
- // Selective bootstrap: only request instant-strategy models.
352
- // When present, the server skips all other models → smaller payload.
353
- // When absent, server returns all models (backward compat).
354
- if (this.options.instantModels && this.options.instantModels.length > 0) {
355
- params.append('models', this.options.instantModels.join(','));
356
- }
357
- const url = `${this.options.baseUrl}/sync/bootstrap?${params.toString()}`;
358
- // If offline, try the cached bootstrap. Skipped for a scoped override: the
359
- // cache holds the full snapshot, which is not a valid answer to a subset
360
- // request; a scoped hydrate just soft-fails offline and retries on re-enter.
361
- //
362
- // Only an explicit `false` means offline. `navigator.onLine` is *typed*
363
- // `boolean`, but at runtime it is `boolean | undefined`: Node 21+ exposes a
364
- // global `navigator` whose `onLine` is `undefined`. Reading `!navigator.onLine`
365
- // would treat that `undefined` as offline and falsely short-circuit to the
366
- // (empty, under `persistence: 'memory'`) cache — throwing instead of fetching.
367
- // Capturing it at its true runtime type keeps the `=== false` honest (and lets
368
- // the boolean-literal-compare lint rule see the nullable it really is).
369
- const navigatorOnline = typeof navigator !== 'undefined' ? navigator.onLine : undefined;
370
- if (!syncGroupsOverride && navigatorOnline === false) {
371
- const cached = this.options.cacheScope
372
- ? this.loadCachedBootstrap(this.options.cacheScope)
373
- : null;
374
- if (cached) {
375
- getContext().logger.info('Using cached bootstrap (offline)');
376
- return cached;
377
- }
378
- throw new AbloConnectionError('Offline and no cached bootstrap available', {
379
- code: 'bootstrap_offline_no_cache',
380
- });
381
- }
382
- getContext().logger.info('Fetching fresh bootstrap data', { url });
383
- const lane = syncGroupsOverride ? 'scoped' : 'bootstrap';
384
- // Chunk a COLD start by model: each instant model is its own request, so
385
- // one giant model can't make the whole snapshot undeliverable, and a
386
- // dropped connection costs one model, not everything. Each chunk is
387
- // consistent at its own sync position; the merge anchors at the MINIMUM
388
- // position, and the regular WS catch-up (`sync_request` → delta replay)
389
- // closes the skew — full-row deltas make the overlapping re-apply
390
- // convergent. Warm partials, scoped hydrates, and clients without a
391
- // model list (server returns everything) stay on the single request.
392
- const instantModels = this.options.instantModels ?? [];
393
- const chunked = (lastSyncId === undefined || lastSyncId <= 0) &&
394
- !syncGroupsOverride &&
395
- instantModels.length > 1;
396
- try {
397
- const data = chunked
398
- ? await this.fetchChunkedBootstrap(instantModels, this.options.syncGroups)
399
- : await this.fetchWithRetries(url, lane);
400
- getContext().logger.info('Bootstrap data fetched', {
401
- type: data.type,
402
- lastSyncId: data.lastSyncId,
403
- chunked,
404
- modelCount: data.models ? Object.keys(data.models).length : 0,
405
- deltaCount: data.deltaCount ?? 0,
406
- totalItems: data.models
407
- ? Object.values(data.models).reduce((sum, arr) => sum + (Array.isArray(arr) ? arr.length : 0), 0)
408
- : 0,
409
- });
410
- // Persist for offline fallback
411
- if (this.options.cacheScope) {
412
- this.saveCachedBootstrap(this.options.cacheScope, data);
413
- }
414
- return data;
415
- }
416
- catch (error) {
417
- // Session and non-retryable errors already failed fast inside the
418
- // retry loop; they must ALSO skip the cached fallback (a stale
419
- // snapshot is not an answer to "your credential is invalid").
420
- if (AbloSessionError.isSessionError(error)) {
421
- throw error;
422
- }
423
- const ablo = toAbloError(error);
424
- if (ablo.code && !isRetryableCode(ablo.code)) {
425
- throw ablo;
426
- }
427
- // Transient failure after exhausting retries → cached fallback.
428
- const cached = this.options.cacheScope
429
- ? this.loadCachedBootstrap(this.options.cacheScope)
430
- : null;
431
- if (cached) {
432
- getContext().observability.breadcrumb('Bootstrap cache fallback', 'sync.bootstrap', 'warning', {
433
- error: ablo.message,
434
- });
435
- return cached;
436
- }
437
- throw ablo;
438
- }
439
- }
440
- /**
441
- * One bootstrap URL, fetched with backoff. Session errors and other
442
- * non-retryable failures throw immediately; only transient failures
443
- * (5xx, 429, timeouts, network blips) consume attempts. A cancellation is
444
- * deliberate and therefore non-retryable — it leaves through the same gate.
445
- */
446
- async fetchWithRetries(url, lane) {
447
- let lastError = null;
448
- for (let attempt = 0; attempt < this.options.maxRetries; attempt++) {
449
- try {
450
- return await this.fetchOnce(url, lane);
451
- }
452
- catch (error) {
453
- // SessionError should NOT be retried - the session is invalid and needs re-authentication
454
- if (AbloSessionError.isSessionError(error)) {
455
- getContext().observability.breadcrumb('Bootstrap session error - redirecting to sign-in', 'sync.bootstrap', 'warning', {
456
- statusCode: (error).statusCode,
457
- });
458
- throw error;
459
- }
460
- // Don't retry NON-retryable errors. A 401/403/4xx auth or client error
461
- // (api_key_required, jwt_issuer_untrusted, …) will NOT succeed by
462
- // repeating the same request with the same credential — retrying just
463
- // hammers the server and floods the console with doomed requests. Only
464
- // transient failures (5xx, 429, timeouts, network blips, or an
465
- // unclassified error with no code) flow through to the retry/backoff.
466
- const ablo = toAbloError(error);
467
- if (ablo.code && !isRetryableCode(ablo.code)) {
468
- getContext().observability.breadcrumb('Bootstrap non-retryable error — failing fast', 'sync.bootstrap', 'warning', { code: ablo.code, httpStatus: ablo.httpStatus });
469
- throw ablo;
470
- }
471
- lastError = error;
472
- getContext().observability.breadcrumb('Bootstrap fetch failed', 'sync.bootstrap', 'warning', {
473
- attempt: attempt + 1,
474
- });
475
- if (attempt < this.options.maxRetries - 1) {
476
- await this.delay(this.options.retryDelay * Math.pow(2, attempt));
477
- }
478
- }
479
- }
480
- throw lastError
481
- ? toAbloError(lastError)
482
- : new AbloConnectionError('Failed to fetch bootstrap data', {
483
- code: 'bootstrap_fetch_timeout',
484
- });
485
- }
486
- /**
487
- * Cold-start bootstrap, one request per instant model with a small
488
- * concurrency cap. Any chunk's terminal failure fails the whole
489
- * bootstrap (a partial snapshot must never masquerade as a full one)
490
- * and cancels its siblings.
491
- */
492
- async fetchChunkedBootstrap(models, syncGroups) {
493
- getContext().logger.info('Bootstrap chunked by model', {
494
- models: models.length,
495
- });
496
- const queue = [...models];
497
- const chunks = [];
498
- // Shared by the concurrent workers below, so it is deliberately re-read
499
- // after `await` points where a sibling may have set it. Held on an object
500
- // rather than in a `let`: the guard inside the worker narrows a plain
501
- // binding to `null` for the rest of the loop body, and the compiler has no
502
- // way to know a sibling can overwrite it mid-await.
503
- const firstFailure = { error: null };
504
- const worker = async () => {
505
- for (;;) {
506
- const model = queue.shift();
507
- if (model === undefined || firstFailure.error !== null)
508
- return;
509
- try {
510
- // Page through the model: each request is bounded to PAGE_LIMIT
511
- // rows, so no single response grows with the model's size. A
512
- // server without paging ignores `limit` and returns the whole
513
- // model with no nextCursor — one page, previous behavior.
514
- let cursor;
515
- for (let pageNo = 0;; pageNo++) {
516
- if (pageNo >= MAX_PAGES_PER_MODEL) {
517
- throw new AbloConnectionError(`Bootstrap for model "${model}" exceeded ${MAX_PAGES_PER_MODEL} pages — the server keeps returning a next page`, { code: 'bootstrap_fetch_timeout' });
518
- }
519
- const params = new URLSearchParams();
520
- syncGroups.forEach((group) => {
521
- params.append('syncGroups', group);
522
- });
523
- params.append('models', model);
524
- params.append('limit', String(PAGE_LIMIT));
525
- if (cursor !== undefined)
526
- params.append('cursor', cursor);
527
- const url = `${this.options.baseUrl}/sync/bootstrap?${params.toString()}`;
528
- const data = await this.fetchWithRetries(url, 'bootstrap');
529
- chunks.push(data);
530
- if (data.nextCursor === undefined)
531
- break;
532
- cursor = data.nextCursor;
533
- }
534
- }
535
- catch (error) {
536
- // First failure wins — a later sibling's error must not mask it.
537
- firstFailure.error ??=
538
- error instanceof Error ? error : new Error(String(error));
539
- // The siblings are abandoned, not broken: the snapshot they belong to
540
- // is already lost. Saying so in the abort reason is what keeps each
541
- // of them from retrying a request nobody is waiting for any more.
542
- this.cancelActive(cancelled(`Abandoned: the bootstrap chunk for "${model}" failed`), 'bootstrap');
543
- return;
544
- }
545
- }
546
- };
547
- await Promise.all(Array.from({ length: Math.min(CHUNK_CONCURRENCY, models.length) }, worker));
548
- if (firstFailure.error !== null)
549
- throw firstFailure.error;
550
- return mergeBootstrapChunks(chunks);
551
- }
552
- /**
553
- * Fetch bootstrap with ETag, returning 304 hints
554
- */
555
- async fetchBootstrapWithETag() {
556
- // The organization id is intentionally not sent. The server resolves it
557
- // from the authenticated identity, so the client cannot select or spoof an
558
- // organization it is not scoped to.
559
- const params = new URLSearchParams();
560
- this.options.syncGroups.forEach((g) => { params.append('syncGroups', g); });
561
- if (this.options.instantModels && this.options.instantModels.length > 0) {
562
- params.append('models', this.options.instantModels.join(','));
563
- }
564
- const url = `${this.options.baseUrl}/sync/bootstrap?${params.toString()}`;
565
- // Note: ETag caching is deliberately app-side, not SDK-side. The server
566
- // still returns an ETag on responses, which is captured below and
567
- // forwarded to callers via BootstrapFetchResult.etag — apps that want
568
- // conditional revalidation (If-None-Match) implement it at their own
569
- // level where they own the cache-key namespace. The 304 branch below
570
- // remains defensively in place for when a caller enables revalidation.
571
- const headers = withAuthHeaders(this.options.getAuthToken, { 'Content-Type': 'application/json' }, this.options.authToken);
572
- const controller = new AbortController();
573
- this.activeControllers.set(controller, 'bootstrap');
574
- try {
575
- return await this.fetchWithETagUsing(url, headers, controller);
576
- }
577
- finally {
578
- this.activeControllers.delete(controller);
579
- }
580
- }
581
- async fetchWithETagUsing(url, headers, controller) {
582
- const res = await fetch(url, {
583
- method: 'GET',
584
- headers,
585
- signal: controller.signal,
586
- });
587
- const etag = res.headers.get('ETag');
588
- if (res.status === 304) {
589
- // Log for telemetry
590
- getContext().logger.info('[Bootstrap] 304 Not Modified - using cached data');
591
- return { notModified: true, etag };
592
- }
593
- if (!res.ok) {
594
- const bodyText = await res.text().catch(() => '');
595
- // Map an empty body to undefined so the `??` below falls through to the
596
- // synthetic message — translateHttpError renders an empty string body as
597
- // an empty error message, which is useless to the caller.
598
- let parsed = bodyText || undefined;
599
- if (bodyText) {
600
- try {
601
- parsed = JSON.parse(bodyText);
602
- }
603
- catch {
604
- // Keep as string.
605
- }
606
- }
607
- // Translate the canonical envelope first so the server's specific code
608
- // and message survive (for example `api_key_required` or
609
- // `jwt_issuer_untrusted`).
610
- const translated = translateHttpError(res.status, parsed ?? `Bootstrap fetch failed: ${res.status} ${res.statusText}`, res.headers.get('x-request-id') ?? undefined);
611
- // Only a genuine session or JWT expiry — or a bare auth failure carrying
612
- // no structured code — should drive the sign-in redirect. A specific auth
613
- // code like `api_key_required` is not an expired session: signing in again
614
- // mints the same credential and loops. Surface it as its real typed error
615
- // instead of a `session_expired` wrapping the stringified body.
616
- if (translated.code === 'session_expired' ||
617
- translated.code === 'jwt_expired' ||
618
- ((res.status === 401 || res.status === 403) &&
619
- translated.code === undefined)) {
620
- throw new AbloSessionError(translated.message, res.status);
621
- }
622
- throw translated;
623
- }
624
- const data = parseBootstrapResponse(await this.readJsonWithStallGuard(res, controller));
625
- this.warnOnSchemaDrift(data.schemaHash);
626
- // Persist payload for offline
627
- try {
628
- if (this.options.cacheScope) {
629
- this.saveCachedBootstrap(this.options.cacheScope, data);
630
- }
631
- }
632
- catch {
633
- // Offline persistence is best-effort; a failed cache write must not
634
- // block returning the freshly fetched data.
635
- }
636
- getContext().logger.info('[Bootstrap] 200 OK - received new data');
637
- return { notModified: false, data, etag };
638
- }
639
- /**
640
- * Read a response body as a stream under a progress watchdog: the stall
641
- * timer re-arms on every chunk, so only a silent stream is aborted — a
642
- * slow-but-moving download is never killed for total duration. A cold-start
643
- * snapshot can be tens of megabytes; bounding its total transfer time was
644
- * what trapped large orgs in an endless full-bootstrap retry loop.
645
- *
646
- * Falls back to `response.json()` when the response exposes no readable
647
- * stream (empty bodies, some test doubles).
648
- */
649
- async readJsonWithStallGuard(response, controller) {
650
- const body = response.body;
651
- if (!body)
652
- return response.json();
653
- const reader = body.getReader();
654
- const chunks = [];
655
- let receivedBytes = 0;
656
- let stallTimer;
657
- // The watchdog must not depend on the stream being wired to the fetch
658
- // signal (that plumbing is implementation-specific), so a stall races a
659
- // rejection against each read instead of only aborting the controller.
660
- let stallReject;
661
- const stalled = new Promise((_, reject) => {
662
- stallReject = reject;
663
- });
664
- // A stall can fire in the microtask gap between two read races; without a
665
- // standing handler that would surface as an unhandled rejection.
666
- stalled.catch(() => undefined);
667
- const armStallTimer = () => {
668
- clearTimeout(stallTimer);
669
- stallTimer = setTimeout(() => {
670
- getContext().observability.breadcrumb('Bootstrap download stalled', 'sync.bootstrap', 'warning', { receivedBytes, stallTimeoutMs: this.options.stallTimeout });
671
- const stallError = new AbloConnectionError(`Bootstrap download stalled: no data received for ${this.options.stallTimeout}ms (${receivedBytes} bytes arrived before the stream went quiet)`, { code: 'bootstrap_fetch_timeout' });
672
- stallReject?.(stallError);
673
- // Then tear the transfer down: abort frees the socket under real
674
- // fetch; cancel unblocks readers on streams not wired to the signal.
675
- // Both carry the same error, so whichever path wins the race below
676
- // reports one message rather than two descriptions of one stall.
677
- controller.abort(stallError);
678
- void reader.cancel().catch(() => undefined);
679
- }, this.options.stallTimeout);
680
- };
681
- try {
682
- armStallTimer();
683
- for (;;) {
684
- const { done, value } = await Promise.race([reader.read(), stalled]);
685
- if (done)
686
- break;
687
- chunks.push(value);
688
- receivedBytes += value.byteLength;
689
- armStallTimer();
690
- }
691
- }
692
- catch (error) {
693
- throw classifyRequestFailure(error, controller, `Bootstrap download aborted after ${receivedBytes} bytes`);
694
- }
695
- finally {
696
- clearTimeout(stallTimer);
697
- }
698
- const bytes = new Uint8Array(receivedBytes);
699
- let offset = 0;
700
- for (const chunk of chunks) {
701
- bytes.set(chunk, offset);
702
- offset += chunk.byteLength;
703
- }
704
- return JSON.parse(new TextDecoder().decode(bytes));
705
- }
706
- /**
707
- * Perform one fetch. The timeout here bounds time to response headers
708
- * only; the body download is guarded by the stall watchdog in
709
- * {@link readJsonWithStallGuard}. Superseding an older in-flight
710
- * bootstrap is the caller's job ({@link fetchBootstrap} cancels the
711
- * registry) — chunk requests run through here concurrently and must
712
- * not cancel each other.
713
- */
714
- async fetchOnce(url, lane) {
715
- const controller = new AbortController();
716
- this.activeControllers.set(controller, lane);
717
- try {
718
- return await this.fetchOnceWith(url, controller);
719
- }
720
- finally {
721
- this.activeControllers.delete(controller);
722
- }
723
- }
724
- async fetchOnceWith(url, controller) {
725
- const timeoutId = setTimeout(() => {
726
- getContext().observability.breadcrumb('Bootstrap fetch timeout', 'sync.bootstrap', 'warning', {
727
- timeoutMs: this.options.fetchTimeout,
728
- });
729
- controller.abort(new AbloConnectionError(`Bootstrap fetch timed out after ${this.options.fetchTimeout}ms waiting for the server to respond`, { code: 'bootstrap_fetch_timeout' }));
730
- }, this.options.fetchTimeout);
731
- let response;
732
- try {
733
- response = await fetch(url, {
734
- method: 'GET',
735
- headers: withAuthHeaders(this.options.getAuthToken, {
736
- 'Content-Type': 'application/json',
737
- 'Cache-Control': 'no-cache, no-store, must-revalidate',
738
- Pragma: 'no-cache',
739
- }, this.options.authToken),
740
- signal: controller.signal,
741
- cache: 'no-store', // Force browser to not cache
742
- });
743
- }
744
- catch (error) {
745
- clearTimeout(timeoutId);
746
- throw classifyRequestFailure(error, controller, 'The bootstrap request was aborted before the server responded');
747
- }
748
- clearTimeout(timeoutId);
749
- if (!response.ok) {
750
- const bodyText = await response.text().catch(() => '');
751
- // Map an empty body to undefined so the `??` below falls through to the
752
- // synthetic message (see the note on the primary fetch path).
753
- let parsed = bodyText || undefined;
754
- if (bodyText) {
755
- try {
756
- parsed = JSON.parse(bodyText);
757
- }
758
- catch {
759
- // Keep as string.
760
- }
761
- }
762
- // Same code-aware handling as the primary bootstrap fetch: preserve the
763
- // server's specific code/message; only a genuine expiry (or a bare,
764
- // code-less auth failure) drives the sign-in redirect.
765
- const translated = translateHttpError(response.status, parsed ?? `Bootstrap fetch failed: ${response.status} ${response.statusText}`, response.headers.get('x-request-id') ?? undefined);
766
- if (translated.code === 'session_expired' ||
767
- translated.code === 'jwt_expired' ||
768
- ((response.status === 401 || response.status === 403) &&
769
- translated.code === undefined)) {
770
- throw new AbloSessionError(translated.message, response.status);
771
- }
772
- throw translated;
773
- }
774
- const data = parseBootstrapResponse(await this.readJsonWithStallGuard(response, controller));
775
- this.warnOnSchemaDrift(data.schemaHash);
776
- // Offline caching happens in `fetchBootstrap` on the assembled result —
777
- // caching here would let a single-model chunk overwrite the full snapshot.
778
- return data;
779
- }
780
- /**
781
- * Fetch a single entity by ID (on-demand self-healing).
782
- * Returns `null` for 404 (entity deleted) — this is an expected state, not an error.
783
- * Throws for unexpected HTTP errors (5xx, network failures).
784
- */
785
- async fetchEntity(modelName, id) {
786
- const url = `${this.options.baseUrl}/sync/entity/${modelName}/${id}`;
787
- // Uses the same `fetchTimeout` deadline as `performFetch`. A local
788
- // AbortController, rather than the shared `this.abortController`, means an
789
- // entity self-heal never cancels a concurrent bootstrap fetch.
790
- const controller = new AbortController();
791
- const timeoutId = setTimeout(() => { controller.abort(); }, this.options.fetchTimeout);
792
- let response;
793
- try {
794
- response = await fetch(url, {
795
- method: 'GET',
796
- headers: withAuthHeaders(this.options.getAuthToken, {
797
- 'Content-Type': 'application/json',
798
- }, this.options.authToken),
799
- signal: controller.signal,
800
- });
801
- }
802
- catch (error) {
803
- // Convert abort to the existing typed timeout error (same code as the
804
- // bootstrap fetch path) so callers get a retryable connection error.
805
- if (error instanceof Error && error.name === 'AbortError') {
806
- throw new AbloConnectionError(`Entity fetch timed out after ${this.options.fetchTimeout}ms`, { code: 'bootstrap_fetch_timeout', cause: error });
807
- }
808
- throw error;
809
- }
810
- finally {
811
- clearTimeout(timeoutId);
812
- }
813
- if (response.status === 404) {
814
- return null;
815
- }
816
- if (!response.ok) {
817
- const bodyText = await response.text().catch(() => '');
818
- // Map an empty body to undefined so the `??` below falls through to the
819
- // synthetic message (see the note on the primary fetch path).
820
- let parsed = bodyText || undefined;
821
- if (bodyText) {
822
- try {
823
- parsed = JSON.parse(bodyText);
824
- }
825
- catch {
826
- // Keep as string.
827
- }
828
- }
829
- throw translateHttpError(response.status, parsed ?? `Entity fetch failed: ${response.status} ${response.statusText}`, response.headers.get('x-request-id') ?? undefined);
830
- }
831
- return (await response.json());
832
- }
833
- // ─────────────────────────────────────────────────────────────────────
834
- /**
835
- * Clear all cached bootstrap data
836
- */
837
- clearCache() {
838
- if (typeof window === 'undefined')
839
- return;
840
- try {
841
- // Clear all bootstrap cache keys
842
- const keysToRemove = [];
843
- for (let i = 0; i < localStorage.length; i++) {
844
- const key = localStorage.key(i);
845
- if (key?.includes('sync-bootstrap')) {
846
- keysToRemove.push(key);
847
- }
848
- }
849
- keysToRemove.forEach((key) => {
850
- localStorage.removeItem(key);
851
- getContext().logger.debug('Cleared cache key', { key });
852
- });
853
- }
854
- catch (error) {
855
- getContext().logger.debug('Failed to clear cache', { error });
856
- }
857
- }
858
- // Cache helpers for offline bootstrap
859
- getBootstrapCacheKey(orgId) {
860
- return `ablo:bootstrap:${orgId}`;
861
- }
862
- saveCachedBootstrap(orgId, data) {
863
- if (typeof window === 'undefined')
864
- return;
865
- try {
866
- localStorage.setItem(this.getBootstrapCacheKey(orgId), JSON.stringify(data));
867
- }
868
- catch (e) {
869
- getContext().logger.debug('Failed to cache bootstrap payload', {
870
- error: e instanceof Error ? e.message : String(e),
871
- });
872
- }
873
- }
874
- loadCachedBootstrap(orgId) {
875
- if (typeof window === 'undefined')
876
- return null;
877
- try {
878
- const raw = localStorage.getItem(this.getBootstrapCacheKey(orgId));
879
- if (!raw)
880
- return null;
881
- return JSON.parse(raw);
882
- }
883
- catch {
884
- return null;
885
- }
886
- }
887
- /**
888
- * Abort every ongoing bootstrap request (including all chunks of a
889
- * chunked cold start). Entity self-heal fetches are unaffected.
890
- *
891
- * The flight registry is cleared first and synchronously, so a caller that
892
- * bootstraps again in the same tick starts a fresh request rather than
893
- * joining the one being torn down.
894
- */
895
- abort() {
896
- this.flights.clear();
897
- this.cancelActive(cancelled('Bootstrap aborted by its caller'));
898
- }
899
- /**
900
- * Helper to delay execution
901
- */
902
- delay(ms) {
903
- return new Promise((resolve) => setTimeout(resolve, ms));
904
- }
905
- /**
906
- * Get health status of sync engine
907
- */
908
- async checkHealth() {
909
- try {
910
- const response = await fetch(`${this.options.baseUrl}/health`, {
911
- method: 'GET',
912
- signal: AbortSignal.timeout(5000),
913
- cache: 'no-store',
914
- });
915
- if (!response.ok)
916
- return false;
917
- const body = (await response.json());
918
- return body.status === 'healthy';
919
- }
920
- catch {
921
- getContext().observability.breadcrumb('Health check failed', 'sync.bootstrap', 'warning');
922
- return false;
923
- }
924
- }
925
- }
926
- /**
927
- * Assemble per-model chunk responses into one full snapshot.
928
- *
929
- * Each chunk is internally consistent at its own sync position, and the
930
- * positions differ (the chunks were served seconds apart). Anchoring the
931
- * merged snapshot at the MINIMUM position turns that skew into an ordinary
932
- * "briefly offline client": the WS catch-up replays every delta from the
933
- * anchor, and since deltas carry full rows, re-applying one a later chunk
934
- * already reflects converges to the same state. Anchoring at anything later
935
- * would silently skip deltas for the earliest-fetched models.
936
- */
937
- export function mergeBootstrapChunks(chunks) {
938
- const models = {};
939
- const failedModels = [];
940
- let lastSyncId = Number.POSITIVE_INFINITY;
941
- let timestamp = 0;
942
- let schemaHash;
943
- for (const chunk of chunks) {
944
- // Concatenate per model: pages of one model arrive as separate chunks.
945
- for (const [name, rows] of Object.entries(chunk.models ?? {})) {
946
- (models[name] ??= []).push(...rows);
947
- }
948
- if (chunk.failedModels)
949
- failedModels.push(...chunk.failedModels);
950
- lastSyncId = Math.min(lastSyncId, chunk.lastSyncId);
951
- timestamp = Math.max(timestamp, chunk.timestamp);
952
- schemaHash ??= chunk.schemaHash;
953
- }
954
- return {
955
- type: 'full',
956
- lastSyncId: Number.isFinite(lastSyncId) ? lastSyncId : 0,
957
- models,
958
- ...(failedModels.length > 0 ? { failedModels } : {}),
959
- timestamp,
960
- ...(schemaHash !== undefined ? { schemaHash } : {}),
961
- };
962
- }