@abloatai/ablo 0.34.1 → 0.36.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 (536) hide show
  1. package/AGENTS.md +2 -1
  2. package/CHANGELOG.md +758 -5
  3. package/README.md +56 -502
  4. package/bin/ablo.cjs +39 -0
  5. package/dist/BaseSyncedStore.d.ts +176 -48
  6. package/dist/BaseSyncedStore.js +346 -214
  7. package/dist/Database.d.ts +17 -44
  8. package/dist/Database.js +96 -79
  9. package/dist/InstanceCache.d.ts +31 -6
  10. package/dist/InstanceCache.js +65 -30
  11. package/dist/LazyReferenceCollection.d.ts +3 -3
  12. package/dist/LazyReferenceCollection.js +4 -4
  13. package/dist/Model.d.ts +23 -13
  14. package/dist/Model.js +27 -17
  15. package/dist/ModelRegistry.d.ts +8 -4
  16. package/dist/ModelRegistry.js +20 -18
  17. package/dist/NetworkMonitor.d.ts +3 -1
  18. package/dist/NetworkMonitor.js +7 -5
  19. package/dist/{SyncEngineContext.d.ts → RuntimeContext.d.ts} +20 -13
  20. package/dist/{SyncEngineContext.js → RuntimeContext.js} +11 -12
  21. package/dist/SyncClient.d.ts +47 -47
  22. package/dist/SyncClient.js +215 -156
  23. package/dist/ai-sdk/coordinatedTool.d.ts +2 -2
  24. package/dist/ai-sdk/coordinatedTool.js +1 -1
  25. package/dist/ai-sdk/coordinationContext.d.ts +2 -2
  26. package/dist/ai-sdk/coordinationContext.js +1 -1
  27. package/dist/ai-sdk/wrap.d.ts +3 -3
  28. package/dist/ai-sdk/wrap.js +2 -2
  29. package/dist/auth/index.d.ts +1 -156
  30. package/dist/auth/index.js +8 -301
  31. package/dist/client/Ablo.d.ts +42 -287
  32. package/dist/client/Ablo.js +129 -963
  33. package/dist/client/abloClient.d.ts +309 -0
  34. package/dist/client/abloClient.js +13 -0
  35. package/dist/client/clientPrelude.d.ts +52 -0
  36. package/dist/client/clientPrelude.js +60 -0
  37. package/dist/client/consoleLogger.d.ts +2 -2
  38. package/dist/client/coreClient.d.ts +60 -0
  39. package/dist/client/coreClient.js +118 -0
  40. package/dist/client/createInternalComponents.d.ts +8 -4
  41. package/dist/client/createInternalComponents.js +17 -10
  42. package/dist/client/createModelProxy.d.ts +98 -373
  43. package/dist/client/createModelProxy.js +233 -139
  44. package/dist/client/humans.d.ts +69 -0
  45. package/dist/client/humans.js +78 -0
  46. package/dist/client/modelRegistration.d.ts +1 -1
  47. package/dist/client/modelRegistration.js +9 -9
  48. package/dist/client/options.d.ts +73 -17
  49. package/dist/client/reactiveEngine.d.ts +53 -0
  50. package/dist/client/reactiveEngine.js +688 -0
  51. package/dist/client/resourceTypes.d.ts +9 -250
  52. package/dist/client/resourceTypes.js +8 -5
  53. package/dist/client/schemaConfig.d.ts +4 -4
  54. package/dist/client/schemaConfig.js +6 -2
  55. package/dist/client/storeCluster.d.ts +47 -0
  56. package/dist/client/storeCluster.js +118 -0
  57. package/dist/client/storeLifecycle.d.ts +61 -0
  58. package/dist/client/storeLifecycle.js +231 -0
  59. package/dist/client/validateAbloOptions.d.ts +3 -2
  60. package/dist/client/validateAbloOptions.js +1 -1
  61. package/dist/client/wsMutationExecutor.d.ts +3 -3
  62. package/dist/client/wsMutationExecutor.js +3 -3
  63. package/dist/context.d.ts +22 -9
  64. package/dist/context.js +33 -9
  65. package/dist/coordination/ClaimLog.d.ts +26 -0
  66. package/dist/coordination/ClaimLog.js +32 -0
  67. package/dist/coordination/index.d.ts +1 -15
  68. package/dist/coordination/index.js +8 -31
  69. package/dist/core/index.d.ts +3 -3
  70. package/dist/core/index.js +2 -2
  71. package/dist/docs/catalog.d.ts +72 -0
  72. package/dist/docs/catalog.js +230 -0
  73. package/dist/docs/index.d.ts +10 -0
  74. package/dist/docs/index.js +10 -0
  75. package/dist/environment.d.ts +1 -40
  76. package/dist/environment.js +8 -37
  77. package/dist/index.d.ts +44 -36
  78. package/dist/index.js +30 -22
  79. package/dist/interfaces/index.d.ts +44 -134
  80. package/dist/keys/index.d.ts +1 -77
  81. package/dist/keys/index.js +8 -190
  82. package/dist/mutators/{RecordingTransaction.d.ts → RecordingMutation.d.ts} +4 -4
  83. package/dist/mutators/{RecordingTransaction.js → RecordingMutation.js} +2 -2
  84. package/dist/mutators/Transaction.d.ts +1 -1
  85. package/dist/mutators/Transaction.js +1 -1
  86. package/dist/mutators/UndoManager.d.ts +6 -6
  87. package/dist/mutators/UndoManager.js +5 -5
  88. package/dist/mutators/defineMutators.d.ts +3 -3
  89. package/dist/mutators/defineMutators.js +1 -1
  90. package/dist/mutators/inverseOp.js +2 -2
  91. package/dist/mutators/mutateActions.d.ts +3 -3
  92. package/dist/mutators/mutateActions.js +1 -1
  93. package/dist/mutators/readerActions.d.ts +1 -1
  94. package/dist/mutators/undoApply.d.ts +1 -1
  95. package/dist/mutators/undoApply.js +1 -1
  96. package/dist/policy/index.d.ts +2 -2
  97. package/dist/policy/index.js +1 -1
  98. package/dist/query/client.d.ts +5 -2
  99. package/dist/query/client.js +10 -9
  100. package/dist/query/types.d.ts +6 -41
  101. package/dist/query/types.js +2 -2
  102. package/dist/react/AbloProvider.d.ts +18 -8
  103. package/dist/react/AbloProvider.js +10 -9
  104. package/dist/react/context.d.ts +3 -3
  105. package/dist/react/context.js +1 -1
  106. package/dist/react/createAbloReact.d.ts +56 -0
  107. package/dist/react/createAbloReact.js +51 -0
  108. package/dist/react/index.d.ts +6 -5
  109. package/dist/react/index.js +6 -3
  110. package/dist/react/internalContext.d.ts +1 -1
  111. package/dist/react/useAblo.d.ts +12 -5
  112. package/dist/react/useAblo.js +26 -8
  113. package/dist/react/useCurrentUserId.js +1 -1
  114. package/dist/react/useErrorListener.js +1 -1
  115. package/dist/react/useMutationFailureListener.d.ts +2 -2
  116. package/dist/react/useMutationFailureListener.js +1 -1
  117. package/dist/react/useMutators.d.ts +3 -3
  118. package/dist/react/useMutators.js +3 -3
  119. package/dist/react/useUndoScope.d.ts +5 -5
  120. package/dist/react/useUndoScope.js +1 -1
  121. package/dist/schema/coordination.d.ts +69 -10
  122. package/dist/schema/coordination.js +90 -9
  123. package/dist/schema/ddl.js +2 -2
  124. package/dist/schema/diff.d.ts +1 -1
  125. package/dist/schema/generate.js +1 -1
  126. package/dist/schema/index.d.ts +11 -10
  127. package/dist/schema/index.js +22 -18
  128. package/dist/schema/queries.d.ts +27 -27
  129. package/dist/schema/queries.js +23 -23
  130. package/dist/schema/select.d.ts +3 -3
  131. package/dist/schema/select.js +6 -3
  132. package/dist/schema/serialize.d.ts +15 -6
  133. package/dist/schema/serialize.js +20 -3
  134. package/dist/schema/sugar.d.ts +6 -7
  135. package/dist/schema/sugar.js +9 -12
  136. package/dist/schema/syncDeltaRow.d.ts +4 -152
  137. package/dist/schema/syncDeltaRow.js +4 -105
  138. package/dist/server/adapter.d.ts +18 -1
  139. package/dist/server/commit.d.ts +10 -16
  140. package/dist/server/index.d.ts +1 -1
  141. package/dist/server/index.js +1 -1
  142. package/dist/server/readConfig.d.ts +1 -1
  143. package/dist/source/adapter.d.ts +7 -5
  144. package/dist/source/adapter.js +7 -5
  145. package/dist/source/adapters/drizzle.d.ts +1 -1
  146. package/dist/source/adapters/drizzle.js +2 -2
  147. package/dist/source/adapters/kysely.d.ts +1 -1
  148. package/dist/source/adapters/kysely.js +1 -1
  149. package/dist/source/adapters/kyselyMutationCore.d.ts +1 -1
  150. package/dist/source/adapters/kyselyMutationCore.js +2 -2
  151. package/dist/source/adapters/memory.js +1 -1
  152. package/dist/source/adapters/prisma.d.ts +8 -3
  153. package/dist/source/adapters/prisma.js +1 -1
  154. package/dist/source/connector.js +1 -1
  155. package/dist/source/connectorProtocol.d.ts +2 -8
  156. package/dist/source/connectorProtocol.js +3 -2
  157. package/dist/source/contract.d.ts +29 -17
  158. package/dist/source/contract.js +27 -22
  159. package/dist/source/factory.d.ts +1 -1
  160. package/dist/source/idempotency.js +2 -2
  161. package/dist/source/index.d.ts +1 -0
  162. package/dist/source/index.js +3 -0
  163. package/dist/source/next.d.ts +1 -1
  164. package/dist/source/signing.d.ts +9 -2
  165. package/dist/source/signing.js +4 -1
  166. package/dist/source/types.d.ts +6 -4
  167. package/dist/source/types.js +1 -1
  168. package/dist/{core/storeContract.d.ts → storeContract.d.ts} +6 -6
  169. package/dist/{core → stores}/DatabaseManager.d.ts +3 -1
  170. package/dist/{core → stores}/DatabaseManager.js +14 -13
  171. package/dist/stores/ObjectStore.d.ts +1 -1
  172. package/dist/{core → stores}/StoreManager.d.ts +9 -26
  173. package/dist/{core → stores}/StoreManager.js +29 -77
  174. package/dist/stores/SyncActionStore.d.ts +4 -2
  175. package/dist/stores/SyncActionStore.js +11 -17
  176. package/dist/stores/syncAction.d.ts +26 -0
  177. package/dist/stores/syncAction.js +16 -0
  178. package/dist/surface.d.ts +3 -3
  179. package/dist/surface.js +6 -4
  180. package/dist/sync/BootstrapFetcher.d.ts +127 -6
  181. package/dist/sync/BootstrapFetcher.js +511 -83
  182. package/dist/sync/ConnectionManager.d.ts +6 -198
  183. package/dist/sync/ConnectionManager.js +6 -677
  184. package/dist/sync/OnDemandLoader.d.ts +5 -2
  185. package/dist/sync/OnDemandLoader.js +61 -21
  186. package/dist/sync/SubscriptionManager.d.ts +13 -2
  187. package/dist/sync/SubscriptionManager.js +23 -5
  188. package/dist/sync/SyncWebSocket.d.ts +27 -510
  189. package/dist/sync/SyncWebSocket.js +76 -954
  190. package/dist/sync/awaitClaimGrant.d.ts +4 -44
  191. package/dist/sync/awaitClaimGrant.js +4 -109
  192. package/dist/sync/bootstrapApply.d.ts +3 -0
  193. package/dist/sync/bootstrapApply.js +2 -2
  194. package/dist/sync/commitFrames.d.ts +6 -40
  195. package/dist/sync/commitFrames.js +6 -97
  196. package/dist/sync/contextPorts.d.ts +18 -0
  197. package/dist/sync/contextPorts.js +31 -0
  198. package/dist/sync/createClaimStream.d.ts +5 -49
  199. package/dist/sync/createClaimStream.js +5 -469
  200. package/dist/sync/createPresenceStream.d.ts +26 -4
  201. package/dist/sync/createPresenceStream.js +28 -20
  202. package/dist/sync/createSnapshot.d.ts +2 -2
  203. package/dist/sync/createSnapshot.js +1 -1
  204. package/dist/sync/credentialLifecycle.d.ts +5 -173
  205. package/dist/sync/credentialLifecycle.js +5 -320
  206. package/dist/sync/deltaPipeline.d.ts +13 -12
  207. package/dist/sync/deltaPipeline.js +21 -4
  208. package/dist/sync/groupChange.d.ts +3 -0
  209. package/dist/sync/groupChange.js +16 -14
  210. package/dist/sync/participants.d.ts +24 -6
  211. package/dist/sync/participants.js +32 -23
  212. package/dist/sync/schemaDrift.d.ts +55 -0
  213. package/dist/sync/schemaDrift.js +53 -0
  214. package/dist/sync/schemas.d.ts +23 -33
  215. package/dist/sync/schemas.js +29 -20
  216. package/dist/sync/syncPlan.d.ts +3 -3
  217. package/dist/sync/wsFrameHandlers.d.ts +6 -114
  218. package/dist/sync/wsFrameHandlers.js +6 -392
  219. package/dist/syncLog/contract.d.ts +20 -0
  220. package/dist/syncLog/contract.js +19 -0
  221. package/dist/syncLog/index.d.ts +1 -0
  222. package/dist/syncLog/index.js +1 -0
  223. package/dist/transaction/ablo.d.ts +88 -0
  224. package/dist/transaction/ablo.js +33 -0
  225. package/dist/{client/auth.d.ts → transaction/auth/apiKey.d.ts} +43 -8
  226. package/dist/{client/auth.js → transaction/auth/apiKey.js} +19 -0
  227. package/dist/transaction/auth/bootstrapScope.d.ts +15 -0
  228. package/dist/transaction/auth/bootstrapScope.js +1 -0
  229. package/dist/transaction/auth/capability.d.ts +212 -0
  230. package/dist/transaction/auth/capability.js +224 -0
  231. package/dist/{auth → transaction/auth}/credentialSource.d.ts +8 -1
  232. package/dist/{client → transaction/auth}/identity.d.ts +8 -7
  233. package/dist/{client → transaction/auth}/identity.js +1 -1
  234. package/dist/transaction/auth/index.d.ts +162 -0
  235. package/dist/transaction/auth/index.js +304 -0
  236. package/dist/{auth → transaction/auth}/schemas.d.ts +1 -1
  237. package/dist/{auth → transaction/auth}/schemas.js +13 -13
  238. package/dist/{client → transaction/auth}/sessionMint.d.ts +8 -8
  239. package/dist/{client → transaction/auth}/sessionMint.js +4 -7
  240. package/dist/transaction/coordination/awaitClaimGrant.d.ts +56 -0
  241. package/dist/transaction/coordination/awaitClaimGrant.js +124 -0
  242. package/dist/{client → transaction/coordination}/claimHeartbeatLoop.d.ts +34 -0
  243. package/dist/{client → transaction/coordination}/claimHeartbeatLoop.js +20 -0
  244. package/dist/transaction/coordination/claimMeta.d.ts +49 -0
  245. package/dist/transaction/coordination/claimMeta.js +52 -0
  246. package/dist/transaction/coordination/createClaimStream.d.ts +64 -0
  247. package/dist/transaction/coordination/createClaimStream.js +475 -0
  248. package/dist/transaction/coordination/events.d.ts +74 -0
  249. package/dist/transaction/coordination/events.js +7 -0
  250. package/dist/transaction/coordination/index.d.ts +19 -0
  251. package/dist/transaction/coordination/index.js +45 -0
  252. package/dist/transaction/coordination/locator.d.ts +104 -0
  253. package/dist/transaction/coordination/locator.js +102 -0
  254. package/dist/transaction/coordination/schema.d.ts +1536 -0
  255. package/dist/transaction/coordination/schema.js +1177 -0
  256. package/dist/transaction/coordination/targetConflict.d.ts +2 -0
  257. package/dist/transaction/coordination/targetConflict.js +107 -0
  258. package/dist/{coordination → transaction/coordination}/trace.d.ts +7 -18
  259. package/dist/{coordination → transaction/coordination}/trace.js +18 -25
  260. package/dist/transaction/durableWrites.d.ts +62 -0
  261. package/dist/{client → transaction}/durableWrites.js +28 -3
  262. package/dist/transaction/environment.d.ts +105 -0
  263. package/dist/transaction/environment.js +108 -0
  264. package/dist/{errorCodes.d.ts → transaction/errorCodes.d.ts} +12 -12
  265. package/dist/{errorCodes.js → transaction/errorCodes.js} +45 -18
  266. package/dist/{errors.d.ts → transaction/errors.d.ts} +37 -13
  267. package/dist/{errors.js → transaction/errors.js} +85 -16
  268. package/dist/transaction/footprint.d.ts +111 -0
  269. package/dist/transaction/footprint.js +0 -0
  270. package/dist/transaction/index.d.ts +20 -0
  271. package/dist/transaction/index.js +20 -0
  272. package/dist/transaction/keys/index.d.ts +87 -0
  273. package/dist/transaction/keys/index.js +207 -0
  274. package/dist/transaction/log/syncDeltaRow.d.ts +158 -0
  275. package/dist/transaction/log/syncDeltaRow.js +95 -0
  276. package/dist/{sync/syncPosition.d.ts → transaction/logPosition.d.ts} +22 -8
  277. package/dist/{sync/syncPosition.js → transaction/logPosition.js} +15 -6
  278. package/dist/transaction/logger.d.ts +16 -0
  279. package/dist/transaction/logger.js +7 -0
  280. package/dist/transaction/observability.d.ts +53 -0
  281. package/dist/transaction/observability.js +19 -0
  282. package/dist/transaction/plugin.d.ts +285 -0
  283. package/dist/transaction/plugin.js +106 -0
  284. package/dist/{policy → transaction/policy}/types.d.ts +3 -3
  285. package/dist/{policy → transaction/policy}/types.js +2 -0
  286. package/dist/transaction/resources/httpResources.d.ts +321 -0
  287. package/dist/transaction/resources/httpResources.js +7 -0
  288. package/dist/transaction/resources/modelOperations.d.ts +427 -0
  289. package/dist/transaction/resources/modelOperations.js +12 -0
  290. package/dist/transaction/resources/mutationOptions.d.ts +66 -0
  291. package/dist/transaction/resources/mutationOptions.js +9 -0
  292. package/dist/transaction/resources/where.d.ts +101 -0
  293. package/dist/transaction/resources/where.js +115 -0
  294. package/dist/{client → transaction/resources}/writeOptionsSchema.d.ts +2 -6
  295. package/dist/{client → transaction/resources}/writeOptionsSchema.js +9 -5
  296. package/dist/{schema → transaction/schema}/field.d.ts +17 -23
  297. package/dist/{schema → transaction/schema}/field.js +5 -5
  298. package/dist/transaction/schema/fieldRef.d.ts +38 -0
  299. package/dist/transaction/schema/fieldRef.js +11 -0
  300. package/dist/transaction/schema/loadStrategy.d.ts +45 -0
  301. package/dist/transaction/schema/loadStrategy.js +46 -0
  302. package/dist/{schema → transaction/schema}/model.d.ts +50 -35
  303. package/dist/{schema → transaction/schema}/model.js +30 -20
  304. package/dist/transaction/schema/openapi.d.ts +58 -0
  305. package/dist/transaction/schema/openapi.js +501 -0
  306. package/dist/{schema → transaction/schema}/relation.d.ts +21 -16
  307. package/dist/{schema → transaction/schema}/relation.js +7 -7
  308. package/dist/{schema → transaction/schema}/residency.d.ts +0 -9
  309. package/dist/{schema → transaction/schema}/residency.js +0 -5
  310. package/dist/{schema → transaction/schema}/roles.d.ts +5 -5
  311. package/dist/{schema → transaction/schema}/roles.js +5 -5
  312. package/dist/{schema → transaction/schema}/schema.d.ts +39 -10
  313. package/dist/{schema → transaction/schema}/schema.js +24 -3
  314. package/dist/{schema → transaction/schema}/tenancy.d.ts +1 -1
  315. package/dist/{schema → transaction/schema}/tenancy.js +7 -4
  316. package/dist/transaction/transactionLayer.d.ts +82 -0
  317. package/dist/transaction/transactionLayer.js +24 -0
  318. package/dist/{transactions → transaction/transactions/settlement}/commitEnvelope.d.ts +5 -6
  319. package/dist/{transactions → transaction/transactions/settlement}/commitEnvelope.js +9 -13
  320. package/dist/{transactions → transaction/transactions/settlement}/httpCommitEnvelope.d.ts +9 -2
  321. package/dist/{transactions → transaction/transactions/settlement}/httpCommitEnvelope.js +5 -9
  322. package/dist/{transactions/durableWriteStore.d.ts → transaction/transactions/settlement/pendingWrite.d.ts} +11 -37
  323. package/dist/transaction/transactions/settlement/pendingWrite.js +20 -0
  324. package/dist/transaction/transport/commitFrames.d.ts +90 -0
  325. package/dist/transaction/transport/commitFrames.js +134 -0
  326. package/dist/transaction/transport/connectionManager.d.ts +215 -0
  327. package/dist/transaction/transport/connectionManager.js +673 -0
  328. package/dist/transaction/transport/credentialLifecycle.d.ts +177 -0
  329. package/dist/transaction/transport/credentialLifecycle.js +324 -0
  330. package/dist/{sync → transaction/transport}/heartbeat.d.ts +3 -1
  331. package/dist/{sync → transaction/transport}/heartbeat.js +6 -4
  332. package/dist/transaction/transport/httpClient.d.ts +131 -0
  333. package/dist/{client → transaction/transport}/httpClient.js +6 -5
  334. package/dist/transaction/transport/httpOptions.d.ts +33 -0
  335. package/dist/transaction/transport/httpOptions.js +12 -0
  336. package/dist/{client → transaction/transport}/httpTransport.js +295 -97
  337. package/dist/{sync/NetworkProbe.d.ts → transaction/transport/networkProbe.d.ts} +7 -4
  338. package/dist/{sync/NetworkProbe.js → transaction/transport/networkProbe.js} +14 -13
  339. package/dist/transaction/transport/wsFrameHandlers.d.ts +128 -0
  340. package/dist/transaction/transport/wsFrameHandlers.js +429 -0
  341. package/dist/transaction/transport/wsTransport.d.ts +574 -0
  342. package/dist/transaction/transport/wsTransport.js +1023 -0
  343. package/dist/transaction/types/assertExact.d.ts +17 -0
  344. package/dist/transaction/types/assertExact.js +1 -0
  345. package/dist/{types → transaction/types}/global.d.ts +17 -2
  346. package/dist/{types → transaction/types}/global.js +2 -1
  347. package/dist/{types → transaction/types}/index.d.ts +14 -46
  348. package/dist/{types → transaction/types}/index.js +7 -16
  349. package/dist/{types → transaction/types}/streams.d.ts +73 -45
  350. package/dist/transaction/utils/duration.d.ts +50 -0
  351. package/dist/{utils → transaction/utils}/duration.js +32 -0
  352. package/dist/{utils → transaction/utils}/json.d.ts +18 -0
  353. package/dist/transaction/utils/json.js +276 -0
  354. package/dist/transaction/wire/accountResponses.d.ts +420 -0
  355. package/dist/transaction/wire/accountResponses.js +290 -0
  356. package/dist/transaction/wire/auth.d.ts +56 -0
  357. package/dist/transaction/wire/auth.js +63 -0
  358. package/dist/transaction/wire/claimEvent.d.ts +76 -0
  359. package/dist/transaction/wire/claimEvent.js +73 -0
  360. package/dist/transaction/wire/claims.d.ts +530 -0
  361. package/dist/transaction/wire/claims.js +327 -0
  362. package/dist/{wire → transaction/wire}/commit.d.ts +125 -193
  363. package/dist/{wire → transaction/wire}/commit.js +68 -47
  364. package/dist/{wire → transaction/wire}/delta.d.ts +66 -17
  365. package/dist/{wire → transaction/wire}/delta.js +37 -13
  366. package/dist/transaction/wire/errorEnvelope.d.ts +72 -0
  367. package/dist/{wire → transaction/wire}/errorEnvelope.js +36 -5
  368. package/dist/transaction/wire/feedCursor.d.ts +60 -0
  369. package/dist/transaction/wire/feedCursor.js +82 -0
  370. package/dist/transaction/wire/feedEvent.d.ts +204 -0
  371. package/dist/transaction/wire/feedEvent.js +65 -0
  372. package/dist/transaction/wire/frames.d.ts +194 -0
  373. package/dist/transaction/wire/frames.js +50 -0
  374. package/dist/transaction/wire/inboundFrames.d.ts +562 -0
  375. package/dist/transaction/wire/inboundFrames.js +116 -0
  376. package/dist/transaction/wire/index.d.ts +54 -0
  377. package/dist/transaction/wire/index.js +83 -0
  378. package/dist/{wire → transaction/wire}/listEnvelope.d.ts +13 -14
  379. package/dist/transaction/wire/listEnvelope.js +42 -0
  380. package/dist/transaction/wire/modelMutations.d.ts +31 -0
  381. package/dist/transaction/wire/modelMutations.js +52 -0
  382. package/dist/transaction/wire/modelResponses.d.ts +85 -0
  383. package/dist/transaction/wire/modelResponses.js +43 -0
  384. package/dist/transaction/wire/modelShape.d.ts +78 -0
  385. package/dist/transaction/wire/modelShape.js +74 -0
  386. package/dist/transactions/{TransactionQueue.d.ts → mutations/MutationQueue.d.ts} +85 -38
  387. package/dist/transactions/{TransactionQueue.js → mutations/MutationQueue.js} +141 -80
  388. package/dist/transactions/{TransactionStore.d.ts → mutations/MutationStore.d.ts} +8 -8
  389. package/dist/transactions/{TransactionStore.js → mutations/MutationStore.js} +2 -2
  390. package/dist/transactions/{coalesceRules.d.ts → mutations/coalesceRules.d.ts} +10 -10
  391. package/dist/transactions/{coalesceRules.js → mutations/coalesceRules.js} +1 -1
  392. package/dist/transactions/mutations/commitLatency.d.ts +52 -0
  393. package/dist/transactions/mutations/commitLatency.js +130 -0
  394. package/dist/transactions/{commitOutboxStore.d.ts → mutations/commitOutboxStore.d.ts} +1 -5
  395. package/dist/transactions/{commitOutboxStore.js → mutations/commitOutboxStore.js} +1 -1
  396. package/dist/transactions/{commitPayload.d.ts → mutations/commitPayload.d.ts} +18 -16
  397. package/dist/transactions/{commitPayload.js → mutations/commitPayload.js} +15 -15
  398. package/dist/transactions/{deltaConfirmation.d.ts → mutations/deltaConfirmation.d.ts} +15 -11
  399. package/dist/transactions/{deltaConfirmation.js → mutations/deltaConfirmation.js} +14 -12
  400. package/dist/transactions/mutations/durableWriteStore.d.ts +14 -0
  401. package/dist/transactions/mutations/durableWriteStore.js +12 -0
  402. package/dist/transactions/{optimisticApply.d.ts → mutations/optimisticApply.d.ts} +7 -7
  403. package/dist/transactions/{replayValidation.d.ts → mutations/replayValidation.d.ts} +4 -3
  404. package/dist/transactions/{replayValidation.js → mutations/replayValidation.js} +7 -5
  405. package/dist/utils/mobxSetup.d.ts +1 -1
  406. package/dist/utils/mobxSetup.js +5 -2
  407. package/dist/{core → views}/QueryView.d.ts +2 -2
  408. package/dist/{core → views}/QueryView.js +2 -2
  409. package/dist/{core → views}/ViewRegistry.d.ts +1 -1
  410. package/dist/{core/queryUtils.d.ts → views/incrementalView.d.ts} +6 -6
  411. package/dist/{core/queryUtils.js → views/incrementalView.js} +6 -6
  412. package/dist/webhooks/events.d.ts +2 -2
  413. package/dist/wire/index.d.ts +1 -34
  414. package/dist/wire/index.js +8 -49
  415. package/docs/agent-messaging.md +3 -3
  416. package/docs/agents.md +20 -13
  417. package/docs/api-keys.md +14 -10
  418. package/docs/api.md +27 -61
  419. package/docs/audit.md +6 -3
  420. package/docs/cli.md +41 -13
  421. package/docs/client-behavior.md +11 -9
  422. package/docs/concurrency-convention.md +49 -57
  423. package/docs/coordination.md +283 -121
  424. package/docs/data-sources.md +7 -5
  425. package/docs/debugging.md +39 -15
  426. package/docs/deployment.md +267 -0
  427. package/docs/examples/agent-human.md +49 -42
  428. package/docs/examples/ai-sdk-tool.md +69 -44
  429. package/docs/examples/existing-python-backend.md +8 -6
  430. package/docs/examples/nextjs.md +129 -47
  431. package/docs/examples/scoped-agent.md +46 -45
  432. package/docs/examples/server-agent.md +46 -26
  433. package/docs/groups.md +87 -30
  434. package/docs/guarantees.md +41 -12
  435. package/docs/how-it-works.md +38 -12
  436. package/docs/idempotency.md +126 -0
  437. package/docs/identity.md +77 -74
  438. package/docs/index.md +172 -86
  439. package/docs/integration-guide.md +31 -19
  440. package/docs/mcp.md +46 -21
  441. package/docs/migration.md +95 -18
  442. package/docs/operating-on-your-database.md +3 -1
  443. package/docs/projects.md +3 -1
  444. package/docs/quickstart.md +22 -5
  445. package/docs/react.md +31 -18
  446. package/docs/schema-contract.md +5 -3
  447. package/docs/session-settings.md +108 -0
  448. package/docs/sessions.md +4 -2
  449. package/docs/webhooks.md +12 -10
  450. package/llms.txt +48 -18
  451. package/package.json +21 -26
  452. package/dist/agent/Agent.d.ts +0 -366
  453. package/dist/agent/Agent.js +0 -514
  454. package/dist/agent/index.d.ts +0 -115
  455. package/dist/agent/index.js +0 -128
  456. package/dist/agent/session.d.ts +0 -93
  457. package/dist/agent/session.js +0 -149
  458. package/dist/agent/types.d.ts +0 -68
  459. package/dist/agent/types.js +0 -9
  460. package/dist/cli.cjs +0 -286329
  461. package/dist/client/durableWrites.d.ts +0 -21
  462. package/dist/client/httpClient.d.ts +0 -80
  463. package/dist/coordination/schema.d.ts +0 -722
  464. package/dist/coordination/schema.js +0 -578
  465. package/dist/schema/openapi.d.ts +0 -29
  466. package/dist/schema/openapi.js +0 -124
  467. package/dist/testing/fixtures/bootstrap.d.ts +0 -49
  468. package/dist/testing/fixtures/bootstrap.js +0 -59
  469. package/dist/testing/fixtures/deltas.d.ts +0 -83
  470. package/dist/testing/fixtures/deltas.js +0 -136
  471. package/dist/testing/fixtures/models.d.ts +0 -83
  472. package/dist/testing/fixtures/models.js +0 -272
  473. package/dist/testing/helpers/reactWrapper.d.ts +0 -69
  474. package/dist/testing/helpers/reactWrapper.js +0 -67
  475. package/dist/testing/helpers/syncEngineHarness.d.ts +0 -54
  476. package/dist/testing/helpers/syncEngineHarness.js +0 -73
  477. package/dist/testing/helpers/wait.d.ts +0 -30
  478. package/dist/testing/helpers/wait.js +0 -49
  479. package/dist/testing/index.d.ts +0 -23
  480. package/dist/testing/index.js +0 -33
  481. package/dist/testing/mocks/FakeDatabase.d.ts +0 -18
  482. package/dist/testing/mocks/FakeDatabase.js +0 -10
  483. package/dist/testing/mocks/MockMutationExecutor.d.ts +0 -87
  484. package/dist/testing/mocks/MockMutationExecutor.js +0 -192
  485. package/dist/testing/mocks/MockNetworkMonitor.d.ts +0 -20
  486. package/dist/testing/mocks/MockNetworkMonitor.js +0 -46
  487. package/dist/testing/mocks/MockSyncContext.d.ts +0 -51
  488. package/dist/testing/mocks/MockSyncContext.js +0 -71
  489. package/dist/testing/mocks/MockSyncStore.d.ts +0 -88
  490. package/dist/testing/mocks/MockSyncStore.js +0 -171
  491. package/dist/testing/mocks/MockWebSocket.d.ts +0 -71
  492. package/dist/testing/mocks/MockWebSocket.js +0 -118
  493. package/dist/transactions/durableWriteStore.js +0 -30
  494. package/dist/utils/duration.d.ts +0 -25
  495. package/dist/utils/json.js +0 -88
  496. package/dist/wire/errorEnvelope.d.ts +0 -55
  497. package/dist/wire/frames.d.ts +0 -197
  498. package/dist/wire/frames.js +0 -49
  499. package/dist/wire/listEnvelope.js +0 -18
  500. package/docs/interaction-model.md +0 -97
  501. /package/dist/{core → query}/QueryProcessor.d.ts +0 -0
  502. /package/dist/{core → query}/QueryProcessor.js +0 -0
  503. /package/dist/{core/storeContract.js → storeContract.js} +0 -0
  504. /package/dist/{core → stores}/openIDBWithTimeout.d.ts +0 -0
  505. /package/dist/{core → stores}/openIDBWithTimeout.js +0 -0
  506. /package/dist/{client → transaction/auth}/credentialEndpoint.d.ts +0 -0
  507. /package/dist/{client → transaction/auth}/credentialEndpoint.js +0 -0
  508. /package/dist/{auth → transaction/auth}/credentialPolicy.d.ts +0 -0
  509. /package/dist/{auth → transaction/auth}/credentialPolicy.js +0 -0
  510. /package/dist/{auth → transaction/auth}/credentialSource.js +0 -0
  511. /package/dist/{client → transaction/auth}/hostedEndpoints.d.ts +0 -0
  512. /package/dist/{client → transaction/auth}/hostedEndpoints.js +0 -0
  513. /package/dist/{client → transaction}/persistence.d.ts +0 -0
  514. /package/dist/{client → transaction}/persistence.js +0 -0
  515. /package/dist/{client → transaction/resources}/functionalUpdate.d.ts +0 -0
  516. /package/dist/{client → transaction/resources}/functionalUpdate.js +0 -0
  517. /package/dist/{transactions → transaction/transactions/settlement}/idempotencyKey.d.ts +0 -0
  518. /package/dist/{transactions → transaction/transactions/settlement}/idempotencyKey.js +0 -0
  519. /package/dist/{client → transaction/transport}/httpTransport.d.ts +0 -0
  520. /package/dist/{types → transaction/types}/modelData.d.ts +0 -0
  521. /package/dist/{types → transaction/types}/modelData.js +0 -0
  522. /package/dist/{types → transaction/types}/participant.d.ts +0 -0
  523. /package/dist/{types → transaction/types}/participant.js +0 -0
  524. /package/dist/{types → transaction/types}/streams.js +0 -0
  525. /package/dist/{utils → transaction/utils}/asyncIterator.d.ts +0 -0
  526. /package/dist/{utils → transaction/utils}/asyncIterator.js +0 -0
  527. /package/dist/{wire → transaction/wire}/bootstrapReason.d.ts +0 -0
  528. /package/dist/{wire → transaction/wire}/bootstrapReason.js +0 -0
  529. /package/dist/{wire → transaction/wire}/protocol.d.ts +0 -0
  530. /package/dist/{wire → transaction/wire}/protocol.js +0 -0
  531. /package/dist/{wire → transaction/wire}/protocolVersion.d.ts +0 -0
  532. /package/dist/{wire → transaction/wire}/protocolVersion.js +0 -0
  533. /package/dist/transactions/{UnconfirmedWrites.d.ts → mutations/UnconfirmedWrites.d.ts} +0 -0
  534. /package/dist/transactions/{UnconfirmedWrites.js → mutations/UnconfirmedWrites.js} +0 -0
  535. /package/dist/transactions/{optimisticApply.js → mutations/optimisticApply.js} +0 -0
  536. /package/dist/{core → views}/ViewRegistry.js +0 -0
@@ -6,16 +6,117 @@
6
6
  * cached snapshot when the device is offline. {@link BootstrapData} is the
7
7
  * shape it returns; {@link BootstrapOptions} configures it.
8
8
  */
9
- import { getContext } from '../context.js';
10
- import { SyncSessionError, AbloConnectionError, translateHttpError, toAbloError, isRetryableCode } from '../errors.js';
11
- import { withAuthHeaders } from '../auth/credentialSource.js';
12
- // SyncObservability replaced by getContext().observability
9
+ import { globalRuntime } 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 this.runtime.observability
13
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
+ }
14
68
  export class BootstrapFetcher {
15
69
  options;
16
- abortController = null;
70
+ runtime;
71
+ /**
72
+ * Every in-flight request's controller, tagged with the lane it belongs to. A
73
+ * registry rather than a single field because a chunked cold start runs
74
+ * several model fetches concurrently — aborting one request (its own
75
+ * TTFB/stall watchdog) must never take its siblings down, while
76
+ * {@link abort} takes down all of them.
77
+ */
78
+ activeControllers = new Map();
79
+ /**
80
+ * Non-scoped bootstraps currently running, keyed by request identity. A
81
+ * second call for the same snapshot joins the one already in flight rather
82
+ * than cancelling and restarting it — see {@link fetchBootstrap}.
83
+ */
84
+ flights = new Map();
17
85
  /** Warn about schema drift at most once per helper. */
18
86
  schemaDriftWarned = false;
87
+ /**
88
+ * Abort every in-flight request in `lane` — or in every lane when none is
89
+ * given — with an explicit reason.
90
+ *
91
+ * The reason is load-bearing, not decoration. `fetch` rejects with the exact
92
+ * value handed to `abort()`, so passing a typed error is what lets the retry
93
+ * loop below tell a deliberate cancellation apart from a dead connection. A
94
+ * bare `abort()` produces an `AbortError` indistinguishable from the one the
95
+ * browser's stop button produces, and a retry loop that cannot tell them
96
+ * apart re-issues the requests it just killed.
97
+ */
98
+ cancelActive(reason, lane) {
99
+ for (const [controller, controllerLane] of this.activeControllers) {
100
+ if (lane !== undefined && controllerLane !== lane)
101
+ continue;
102
+ controller.abort(reason);
103
+ this.activeControllers.delete(controller);
104
+ }
105
+ }
106
+ /**
107
+ * The longest a single bootstrap can run before every watchdog below has
108
+ * necessarily fired, derived from those watchdogs rather than guessed. A
109
+ * caller wanting an outer deadline reads this instead of picking a number,
110
+ * so it cannot set one shorter than the work it wraps. A cold start pages
111
+ * through its models {@link CHUNK_CONCURRENCY} at a time; each request may
112
+ * spend `fetchTimeout` waiting for response headers and `stallTimeout`
113
+ * waiting for the next body chunk, and may be retried `maxRetries` times.
114
+ */
115
+ get budgetMs() {
116
+ const models = Math.max(this.options.instantModels?.length ?? 1, 1);
117
+ const waves = Math.ceil(models / CHUNK_CONCURRENCY);
118
+ return (waves * (this.options.fetchTimeout + this.options.stallTimeout) * this.options.maxRetries);
119
+ }
19
120
  get baseUrl() {
20
121
  return this.options.baseUrl;
21
122
  }
@@ -39,24 +140,78 @@ export class BootstrapFetcher {
39
140
  warnOnSchemaDrift(serverHash) {
40
141
  if (this.schemaDriftWarned || !serverHash)
41
142
  return;
42
- const clientHash = getContext().config.expectedSchemaHash;
143
+ const clientHash = this.runtime.config.expectedSchemaHash;
43
144
  if (!clientHash || clientHash === serverHash)
44
145
  return;
45
146
  // A projection (`selectModels`/`omitModels`) hashes its subset, which never
46
147
  // equals the full schema a server runs — so it also carries the source
47
148
  // schema's hash. Matching that means the client is a faithful subset of the
48
149
  // deployed schema: current, not drifted. Only warn when neither matches.
49
- const sourceHash = getContext().config.expectedSourceSchemaHash;
150
+ const sourceHash = this.runtime.config.expectedSourceSchemaHash;
50
151
  if (sourceHash && sourceHash === serverHash)
51
152
  return;
52
153
  this.schemaDriftWarned = true;
53
154
  const org = this.options.organizationId;
54
155
  const where = org ? `${this.baseUrl} (org ${org})` : this.baseUrl;
156
+ // The whole-schema hashes differ — but that alone can't distinguish "the
157
+ // server gained models this build never touches" (fine, say nothing) from
158
+ // "a model this client uses moved" (name it). Resolve the semantic answer
159
+ // from the server's per-model surface before speaking; fall back to the
160
+ // hash message only when that surface is unavailable (older server,
161
+ // network hiccup). Fire-and-forget: never blocks or fails the bootstrap.
162
+ const clientModels = this.runtime.config.expectedModelHashes;
163
+ if (clientModels && Object.keys(clientModels).length > 0) {
164
+ void this.resolveSemanticDrift(clientModels, clientHash, serverHash, where);
165
+ return;
166
+ }
167
+ this.warnWholeHashDrift(clientHash, serverHash, where);
168
+ }
169
+ /** Fetch the server's per-model schema surface and warn precisely — or stay
170
+ * silent when every model this client declares matches (additive lead). */
171
+ async resolveSemanticDrift(clientModels, clientHash, serverHash, where) {
172
+ try {
173
+ const res = await fetch(`${this.options.baseUrl}/schema`, {
174
+ method: 'GET',
175
+ headers: withAuthHeaders(this.options.getAuthToken, {}, this.options.authToken),
176
+ });
177
+ if (!res.ok)
178
+ throw new Error(`schema read-back ${res.status}`);
179
+ const body = (await res.json());
180
+ const models = Array.isArray(body.models)
181
+ ? body.models.flatMap((m) => {
182
+ const entry = m;
183
+ return typeof entry.key === 'string'
184
+ ? [{ key: entry.key, ...(typeof entry.hash === 'string' ? { hash: entry.hash } : {}) }]
185
+ : [];
186
+ })
187
+ : [];
188
+ const finding = classifySchemaDrift(clientModels, models);
189
+ if (finding.kind === 'aligned')
190
+ return; // additive server lead — not this client's concern
191
+ if (finding.kind !== 'unknown') {
192
+ this.runtime.logger.warn(describeSchemaDrift(finding, where), {
193
+ clientSchemaHash: clientHash,
194
+ serverSchemaHash: serverHash,
195
+ serverUrl: this.baseUrl,
196
+ ...(finding.kind === 'unpushed'
197
+ ? { unpushedModels: finding.models }
198
+ : { changedModels: finding.models, unpushedModels: finding.unpushed }),
199
+ });
200
+ return;
201
+ }
202
+ }
203
+ catch {
204
+ /* surface unavailable — fall through to the hash message */
205
+ }
206
+ this.warnWholeHashDrift(clientHash, serverHash, where);
207
+ }
208
+ warnWholeHashDrift(clientHash, serverHash, where) {
209
+ const org = this.options.organizationId;
55
210
  // Self-brand the message ("Ablo:") rather than rely on the default logger's
56
211
  // `[Ablo]` namespace — consumers wiring their own logger (pino, etc.) lose
57
212
  // that prefix, and a drift warning that reads like the app's own log is
58
213
  // worse than none. The brand tells them at a glance who is talking.
59
- getContext().logger.warn(`Ablo: Schema drift — the schema this client was built with (${clientHash}) is not the ` +
214
+ this.runtime.logger.warn(`Ablo: Schema drift — the schema this client was built with (${clientHash}) is not the ` +
60
215
  `one active on the server it connected to (${serverHash} at ${where}). Until they match, ` +
61
216
  `operations that depend on the difference will fail later with an opaque database error. ` +
62
217
  `This is usually one of three things. The schema may have been pushed to a different ` +
@@ -73,6 +228,7 @@ export class BootstrapFetcher {
73
228
  });
74
229
  }
75
230
  constructor(options) {
231
+ this.runtime = options.runtime ?? globalRuntime;
76
232
  // Defaults are spread first; the explicit `baseUrl` then takes precedence,
77
233
  // resolved from `options.baseUrl` or the localhost fallback. Callers pass
78
234
  // the full base URL, including the `/api` prefix.
@@ -80,7 +236,11 @@ export class BootstrapFetcher {
80
236
  syncGroups: [],
81
237
  maxRetries: 3,
82
238
  retryDelay: 1000,
83
- fetchTimeout: 10_000, // 10 second timeout per request - fail fast for good UX
239
+ // Time-to-first-byte bound only. The server currently materializes the
240
+ // whole snapshot before sending headers, so a cold start on a large org
241
+ // legitimately needs more than a "fail fast" allowance here.
242
+ fetchTimeout: 20_000,
243
+ stallTimeout: 15_000,
84
244
  ...options,
85
245
  baseUrl: options.baseUrl ?? 'http://localhost:8080/api',
86
246
  // Reading the deprecated `organizationId` is deliberate: it preserves the
@@ -129,6 +289,56 @@ export class BootstrapFetcher {
129
289
  * request.
130
290
  */
131
291
  syncGroupsOverride) {
292
+ // A scoped hydrate answers a different question than the full bootstrap and
293
+ // runs in its own lane: it never joins one, and is never superseded by one.
294
+ if (syncGroupsOverride)
295
+ return this.runBootstrap(lastSyncId, syncGroupsOverride);
296
+ // Single-flight. Three callers reach this independently — first load,
297
+ // background refresh, and reconnect — and before this they raced: each new
298
+ // call cancelled whatever was running and started over, so a socket that
299
+ // reconnected mid-cold-start restarted the whole snapshot, repeatedly. The
300
+ // same request now joins the one in flight instead.
301
+ const key = this.flightKey(lastSyncId);
302
+ const joined = this.flights.get(key);
303
+ if (joined) {
304
+ this.runtime.logger.debug('Joining the bootstrap already in flight', { key });
305
+ return joined;
306
+ }
307
+ // A request for something else genuinely does supersede: the running one is
308
+ // not the answer being asked for. Retire it from the registry first, so a
309
+ // caller arriving in the same tick cannot join a flight that is dying.
310
+ if (this.flights.size > 0) {
311
+ this.flights.clear();
312
+ this.cancelActive(cancelled('Superseded by a newer bootstrap request'), 'bootstrap');
313
+ }
314
+ const flight = this.runBootstrap(lastSyncId);
315
+ this.flights.set(key, flight);
316
+ // The cleanup chain is terminated with `catch` so this derived promise can
317
+ // never surface as an unhandled rejection even when every caller handled the
318
+ // failure, and the delete is guarded by identity so a flight registered
319
+ // after a supersede is not evicted by its predecessor's cleanup.
320
+ void flight
321
+ .catch(() => undefined)
322
+ .finally(() => {
323
+ if (this.flights.get(key) === flight)
324
+ this.flights.delete(key);
325
+ });
326
+ return flight;
327
+ }
328
+ /**
329
+ * The identity of a bootstrap request: everything that determines its answer.
330
+ * Two calls with the same key are asking the same question, so the second can
331
+ * take the first's result.
332
+ */
333
+ flightKey(lastSyncId) {
334
+ return JSON.stringify({
335
+ lastSyncId: lastSyncId !== undefined && lastSyncId > 0 ? lastSyncId : 0,
336
+ syncGroups: [...this.options.syncGroups].sort(),
337
+ models: [...(this.options.instantModels ?? [])].sort(),
338
+ });
339
+ }
340
+ /** One bootstrap, start to finish. {@link fetchBootstrap} owns whether it runs. */
341
+ async runBootstrap(lastSyncId, syncGroupsOverride) {
132
342
  // organizationId omitted — server reads it from auth identity.
133
343
  // See `fetchBootstrapWithETag` for the full rationale.
134
344
  const params = new URLSearchParams();
@@ -164,39 +374,87 @@ export class BootstrapFetcher {
164
374
  ? this.loadCachedBootstrap(this.options.cacheScope)
165
375
  : null;
166
376
  if (cached) {
167
- getContext().logger.info('Using cached bootstrap (offline)');
377
+ this.runtime.logger.info('Using cached bootstrap (offline)');
168
378
  return cached;
169
379
  }
170
380
  throw new AbloConnectionError('Offline and no cached bootstrap available', {
171
381
  code: 'bootstrap_offline_no_cache',
172
382
  });
173
383
  }
174
- getContext().logger.info('Fetching fresh bootstrap data', { url });
175
- // Fetch with retries
384
+ this.runtime.logger.info('Fetching fresh bootstrap data', { url });
385
+ const lane = syncGroupsOverride ? 'scoped' : 'bootstrap';
386
+ // Chunk a COLD start by model: each instant model is its own request, so
387
+ // one giant model can't make the whole snapshot undeliverable, and a
388
+ // dropped connection costs one model, not everything. Each chunk is
389
+ // consistent at its own sync position; the merge anchors at the MINIMUM
390
+ // position, and the regular WS catch-up (`sync_request` → delta replay)
391
+ // closes the skew — full-row deltas make the overlapping re-apply
392
+ // convergent. Warm partials, scoped hydrates, and clients without a
393
+ // model list (server returns everything) stay on the single request.
394
+ const instantModels = this.options.instantModels ?? [];
395
+ const chunked = (lastSyncId === undefined || lastSyncId <= 0) &&
396
+ !syncGroupsOverride &&
397
+ instantModels.length > 1;
398
+ try {
399
+ const data = chunked
400
+ ? await this.fetchChunkedBootstrap(instantModels, this.options.syncGroups)
401
+ : await this.fetchWithRetries(url, lane);
402
+ this.runtime.logger.info('Bootstrap data fetched', {
403
+ type: data.type,
404
+ lastSyncId: data.lastSyncId,
405
+ chunked,
406
+ modelCount: data.models ? Object.keys(data.models).length : 0,
407
+ deltaCount: data.deltaCount ?? 0,
408
+ totalItems: data.models
409
+ ? Object.values(data.models).reduce((sum, arr) => sum + (Array.isArray(arr) ? arr.length : 0), 0)
410
+ : 0,
411
+ });
412
+ // Persist for offline fallback
413
+ if (this.options.cacheScope) {
414
+ this.saveCachedBootstrap(this.options.cacheScope, data);
415
+ }
416
+ return data;
417
+ }
418
+ catch (error) {
419
+ // Session and non-retryable errors already failed fast inside the
420
+ // retry loop; they must ALSO skip the cached fallback (a stale
421
+ // snapshot is not an answer to "your credential is invalid").
422
+ if (AbloSessionError.isSessionError(error)) {
423
+ throw error;
424
+ }
425
+ const ablo = toAbloError(error);
426
+ if (ablo.code && !isRetryableCode(ablo.code)) {
427
+ throw ablo;
428
+ }
429
+ // Transient failure after exhausting retries → cached fallback.
430
+ const cached = this.options.cacheScope
431
+ ? this.loadCachedBootstrap(this.options.cacheScope)
432
+ : null;
433
+ if (cached) {
434
+ this.runtime.observability.breadcrumb('Bootstrap cache fallback', 'sync.bootstrap', 'warning', {
435
+ error: ablo.message,
436
+ });
437
+ return cached;
438
+ }
439
+ throw ablo;
440
+ }
441
+ }
442
+ /**
443
+ * One bootstrap URL, fetched with backoff. Session errors and other
444
+ * non-retryable failures throw immediately; only transient failures
445
+ * (5xx, 429, timeouts, network blips) consume attempts. A cancellation is
446
+ * deliberate and therefore non-retryable — it leaves through the same gate.
447
+ */
448
+ async fetchWithRetries(url, lane) {
176
449
  let lastError = null;
177
450
  for (let attempt = 0; attempt < this.options.maxRetries; attempt++) {
178
451
  try {
179
- const data = await this.performFetch(url);
180
- getContext().logger.info('Bootstrap data fetched', {
181
- type: data.type,
182
- lastSyncId: data.lastSyncId,
183
- modelCount: data.models ? Object.keys(data.models).length : 0,
184
- deltaCount: data.deltaCount ?? 0,
185
- totalItems: data.models
186
- ? Object.values(data.models).reduce((sum, arr) => sum + (Array.isArray(arr) ? arr.length : 0), 0)
187
- : 0,
188
- });
189
- // Persist for offline fallback
190
- if (this.options.cacheScope) {
191
- this.saveCachedBootstrap(this.options.cacheScope, data);
192
- }
193
- return data;
452
+ return await this.fetchOnce(url, lane);
194
453
  }
195
454
  catch (error) {
196
455
  // SessionError should NOT be retried - the session is invalid and needs re-authentication
197
- // Also do NOT fallback to cache - the user must sign in again
198
- if (SyncSessionError.isSessionError(error)) {
199
- getContext().observability.breadcrumb('Bootstrap session error - redirecting to sign-in', 'sync.bootstrap', 'warning', {
456
+ if (AbloSessionError.isSessionError(error)) {
457
+ this.runtime.observability.breadcrumb('Bootstrap session error - redirecting to sign-in', 'sync.bootstrap', 'warning', {
200
458
  statusCode: (error).statusCode,
201
459
  });
202
460
  throw error;
@@ -209,11 +467,11 @@ export class BootstrapFetcher {
209
467
  // unclassified error with no code) flow through to the retry/backoff.
210
468
  const ablo = toAbloError(error);
211
469
  if (ablo.code && !isRetryableCode(ablo.code)) {
212
- getContext().observability.breadcrumb('Bootstrap non-retryable error — failing fast', 'sync.bootstrap', 'warning', { code: ablo.code, httpStatus: ablo.httpStatus });
470
+ this.runtime.observability.breadcrumb('Bootstrap non-retryable error — failing fast', 'sync.bootstrap', 'warning', { code: ablo.code, httpStatus: ablo.httpStatus });
213
471
  throw ablo;
214
472
  }
215
473
  lastError = error;
216
- getContext().observability.breadcrumb('Bootstrap fetch failed', 'sync.bootstrap', 'warning', {
474
+ this.runtime.observability.breadcrumb('Bootstrap fetch failed', 'sync.bootstrap', 'warning', {
217
475
  attempt: attempt + 1,
218
476
  });
219
477
  if (attempt < this.options.maxRetries - 1) {
@@ -221,22 +479,78 @@ export class BootstrapFetcher {
221
479
  }
222
480
  }
223
481
  }
224
- // On error, attempt cached fallback (but NOT for session errors - already handled above)
225
- const cached = this.options.cacheScope
226
- ? this.loadCachedBootstrap(this.options.cacheScope)
227
- : null;
228
- if (cached) {
229
- getContext().observability.breadcrumb('Bootstrap cache fallback', 'sync.bootstrap', 'warning', {
230
- error: lastError?.message,
231
- });
232
- return cached;
233
- }
234
482
  throw lastError
235
483
  ? toAbloError(lastError)
236
484
  : new AbloConnectionError('Failed to fetch bootstrap data', {
237
485
  code: 'bootstrap_fetch_timeout',
238
486
  });
239
487
  }
488
+ /**
489
+ * Cold-start bootstrap, one request per instant model with a small
490
+ * concurrency cap. Any chunk's terminal failure fails the whole
491
+ * bootstrap (a partial snapshot must never masquerade as a full one)
492
+ * and cancels its siblings.
493
+ */
494
+ async fetchChunkedBootstrap(models, syncGroups) {
495
+ this.runtime.logger.info('Bootstrap chunked by model', {
496
+ models: models.length,
497
+ });
498
+ const queue = [...models];
499
+ const chunks = [];
500
+ // Shared by the concurrent workers below, so it is deliberately re-read
501
+ // after `await` points where a sibling may have set it. Held on an object
502
+ // rather than in a `let`: the guard inside the worker narrows a plain
503
+ // binding to `null` for the rest of the loop body, and the compiler has no
504
+ // way to know a sibling can overwrite it mid-await.
505
+ const firstFailure = { error: null };
506
+ const worker = async () => {
507
+ for (;;) {
508
+ const model = queue.shift();
509
+ if (model === undefined || firstFailure.error !== null)
510
+ return;
511
+ try {
512
+ // Page through the model: each request is bounded to PAGE_LIMIT
513
+ // rows, so no single response grows with the model's size. A
514
+ // server without paging ignores `limit` and returns the whole
515
+ // model with no nextCursor — one page, previous behavior.
516
+ let cursor;
517
+ for (let pageNo = 0;; pageNo++) {
518
+ if (pageNo >= MAX_PAGES_PER_MODEL) {
519
+ throw new AbloConnectionError(`Bootstrap for model "${model}" exceeded ${MAX_PAGES_PER_MODEL} pages — the server keeps returning a next page`, { code: 'bootstrap_fetch_timeout' });
520
+ }
521
+ const params = new URLSearchParams();
522
+ syncGroups.forEach((group) => {
523
+ params.append('syncGroups', group);
524
+ });
525
+ params.append('models', model);
526
+ params.append('limit', String(PAGE_LIMIT));
527
+ if (cursor !== undefined)
528
+ params.append('cursor', cursor);
529
+ const url = `${this.options.baseUrl}/sync/bootstrap?${params.toString()}`;
530
+ const data = await this.fetchWithRetries(url, 'bootstrap');
531
+ chunks.push(data);
532
+ if (data.nextCursor === undefined)
533
+ break;
534
+ cursor = data.nextCursor;
535
+ }
536
+ }
537
+ catch (error) {
538
+ // First failure wins — a later sibling's error must not mask it.
539
+ firstFailure.error ??=
540
+ error instanceof Error ? error : new Error(String(error));
541
+ // The siblings are abandoned, not broken: the snapshot they belong to
542
+ // is already lost. Saying so in the abort reason is what keeps each
543
+ // of them from retrying a request nobody is waiting for any more.
544
+ this.cancelActive(cancelled(`Abandoned: the bootstrap chunk for "${model}" failed`), 'bootstrap');
545
+ return;
546
+ }
547
+ }
548
+ };
549
+ await Promise.all(Array.from({ length: Math.min(CHUNK_CONCURRENCY, models.length) }, worker));
550
+ if (firstFailure.error !== null)
551
+ throw firstFailure.error;
552
+ return mergeBootstrapChunks(chunks);
553
+ }
240
554
  /**
241
555
  * Fetch bootstrap with ETag, returning 304 hints
242
556
  */
@@ -257,16 +571,25 @@ export class BootstrapFetcher {
257
571
  // level where they own the cache-key namespace. The 304 branch below
258
572
  // remains defensively in place for when a caller enables revalidation.
259
573
  const headers = withAuthHeaders(this.options.getAuthToken, { 'Content-Type': 'application/json' }, this.options.authToken);
260
- this.abortController = new AbortController();
574
+ const controller = new AbortController();
575
+ this.activeControllers.set(controller, 'bootstrap');
576
+ try {
577
+ return await this.fetchWithETagUsing(url, headers, controller);
578
+ }
579
+ finally {
580
+ this.activeControllers.delete(controller);
581
+ }
582
+ }
583
+ async fetchWithETagUsing(url, headers, controller) {
261
584
  const res = await fetch(url, {
262
585
  method: 'GET',
263
586
  headers,
264
- signal: this.abortController.signal,
587
+ signal: controller.signal,
265
588
  });
266
589
  const etag = res.headers.get('ETag');
267
590
  if (res.status === 304) {
268
591
  // Log for telemetry
269
- getContext().logger.info('[Bootstrap] 304 Not Modified - using cached data');
592
+ this.runtime.logger.info('[Bootstrap] 304 Not Modified - using cached data');
270
593
  return { notModified: true, etag };
271
594
  }
272
595
  if (!res.ok) {
@@ -296,11 +619,11 @@ export class BootstrapFetcher {
296
619
  translated.code === 'jwt_expired' ||
297
620
  ((res.status === 401 || res.status === 403) &&
298
621
  translated.code === undefined)) {
299
- throw new SyncSessionError(translated.message, res.status);
622
+ throw new AbloSessionError(translated.message, res.status);
300
623
  }
301
624
  throw translated;
302
625
  }
303
- const data = parseBootstrapResponse(await res.json());
626
+ const data = parseBootstrapResponse(await this.readJsonWithStallGuard(res, controller), this.runtime);
304
627
  this.warnOnSchemaDrift(data.schemaHash);
305
628
  // Persist payload for offline
306
629
  try {
@@ -312,23 +635,100 @@ export class BootstrapFetcher {
312
635
  // Offline persistence is best-effort; a failed cache write must not
313
636
  // block returning the freshly fetched data.
314
637
  }
315
- getContext().logger.info('[Bootstrap] 200 OK - received new data');
638
+ this.runtime.logger.info('[Bootstrap] 200 OK - received new data');
316
639
  return { notModified: false, data, etag };
317
640
  }
318
641
  /**
319
- * Perform the actual fetch request with timeout protection
642
+ * Read a response body as a stream under a progress watchdog: the stall
643
+ * timer re-arms on every chunk, so only a silent stream is aborted — a
644
+ * slow-but-moving download is never killed for total duration. A cold-start
645
+ * snapshot can be tens of megabytes; bounding its total transfer time was
646
+ * what trapped large orgs in an endless full-bootstrap retry loop.
647
+ *
648
+ * Falls back to `response.json()` when the response exposes no readable
649
+ * stream (empty bodies, some test doubles).
320
650
  */
321
- async performFetch(url) {
322
- // Cancel any previous in-flight request
323
- if (this.abortController) {
324
- this.abortController.abort();
651
+ async readJsonWithStallGuard(response, controller) {
652
+ const body = response.body;
653
+ if (!body)
654
+ return response.json();
655
+ const reader = body.getReader();
656
+ const chunks = [];
657
+ let receivedBytes = 0;
658
+ let stallTimer;
659
+ // The watchdog must not depend on the stream being wired to the fetch
660
+ // signal (that plumbing is implementation-specific), so a stall races a
661
+ // rejection against each read instead of only aborting the controller.
662
+ let stallReject;
663
+ const stalled = new Promise((_, reject) => {
664
+ stallReject = reject;
665
+ });
666
+ // A stall can fire in the microtask gap between two read races; without a
667
+ // standing handler that would surface as an unhandled rejection.
668
+ stalled.catch(() => undefined);
669
+ const armStallTimer = () => {
670
+ clearTimeout(stallTimer);
671
+ stallTimer = setTimeout(() => {
672
+ this.runtime.observability.breadcrumb('Bootstrap download stalled', 'sync.bootstrap', 'warning', { receivedBytes, stallTimeoutMs: this.options.stallTimeout });
673
+ 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' });
674
+ stallReject?.(stallError);
675
+ // Then tear the transfer down: abort frees the socket under real
676
+ // fetch; cancel unblocks readers on streams not wired to the signal.
677
+ // Both carry the same error, so whichever path wins the race below
678
+ // reports one message rather than two descriptions of one stall.
679
+ controller.abort(stallError);
680
+ void reader.cancel().catch(() => undefined);
681
+ }, this.options.stallTimeout);
682
+ };
683
+ try {
684
+ armStallTimer();
685
+ for (;;) {
686
+ const { done, value } = await Promise.race([reader.read(), stalled]);
687
+ if (done)
688
+ break;
689
+ chunks.push(value);
690
+ receivedBytes += value.byteLength;
691
+ armStallTimer();
692
+ }
693
+ }
694
+ catch (error) {
695
+ throw classifyRequestFailure(error, controller, `Bootstrap download aborted after ${receivedBytes} bytes`);
696
+ }
697
+ finally {
698
+ clearTimeout(stallTimer);
325
699
  }
326
- this.abortController = new AbortController();
700
+ const bytes = new Uint8Array(receivedBytes);
701
+ let offset = 0;
702
+ for (const chunk of chunks) {
703
+ bytes.set(chunk, offset);
704
+ offset += chunk.byteLength;
705
+ }
706
+ return JSON.parse(new TextDecoder().decode(bytes));
707
+ }
708
+ /**
709
+ * Perform one fetch. The timeout here bounds time to response headers
710
+ * only; the body download is guarded by the stall watchdog in
711
+ * {@link readJsonWithStallGuard}. Superseding an older in-flight
712
+ * bootstrap is the caller's job ({@link fetchBootstrap} cancels the
713
+ * registry) — chunk requests run through here concurrently and must
714
+ * not cancel each other.
715
+ */
716
+ async fetchOnce(url, lane) {
717
+ const controller = new AbortController();
718
+ this.activeControllers.set(controller, lane);
719
+ try {
720
+ return await this.fetchOnceWith(url, controller);
721
+ }
722
+ finally {
723
+ this.activeControllers.delete(controller);
724
+ }
725
+ }
726
+ async fetchOnceWith(url, controller) {
327
727
  const timeoutId = setTimeout(() => {
328
- getContext().observability.breadcrumb('Bootstrap fetch timeout', 'sync.bootstrap', 'warning', {
728
+ this.runtime.observability.breadcrumb('Bootstrap fetch timeout', 'sync.bootstrap', 'warning', {
329
729
  timeoutMs: this.options.fetchTimeout,
330
730
  });
331
- this.abortController?.abort();
731
+ controller.abort(new AbloConnectionError(`Bootstrap fetch timed out after ${this.options.fetchTimeout}ms waiting for the server to respond`, { code: 'bootstrap_fetch_timeout' }));
332
732
  }, this.options.fetchTimeout);
333
733
  let response;
334
734
  try {
@@ -339,17 +739,13 @@ export class BootstrapFetcher {
339
739
  'Cache-Control': 'no-cache, no-store, must-revalidate',
340
740
  Pragma: 'no-cache',
341
741
  }, this.options.authToken),
342
- signal: this.abortController.signal,
742
+ signal: controller.signal,
343
743
  cache: 'no-store', // Force browser to not cache
344
744
  });
345
745
  }
346
746
  catch (error) {
347
747
  clearTimeout(timeoutId);
348
- // Convert abort to timeout error for better error messaging
349
- if (error instanceof Error && error.name === 'AbortError') {
350
- throw new AbloConnectionError(`Bootstrap fetch timed out after ${this.options.fetchTimeout}ms`, { code: 'bootstrap_fetch_timeout', cause: error });
351
- }
352
- throw error;
748
+ throw classifyRequestFailure(error, controller, 'The bootstrap request was aborted before the server responded');
353
749
  }
354
750
  clearTimeout(timeoutId);
355
751
  if (!response.ok) {
@@ -373,22 +769,14 @@ export class BootstrapFetcher {
373
769
  translated.code === 'jwt_expired' ||
374
770
  ((response.status === 401 || response.status === 403) &&
375
771
  translated.code === undefined)) {
376
- throw new SyncSessionError(translated.message, response.status);
772
+ throw new AbloSessionError(translated.message, response.status);
377
773
  }
378
774
  throw translated;
379
775
  }
380
- const data = parseBootstrapResponse(await response.json());
776
+ const data = parseBootstrapResponse(await this.readJsonWithStallGuard(response, controller), this.runtime);
381
777
  this.warnOnSchemaDrift(data.schemaHash);
382
- // Save a copy for offline
383
- try {
384
- if (this.options.cacheScope) {
385
- this.saveCachedBootstrap(this.options.cacheScope, data);
386
- }
387
- }
388
- catch {
389
- // Offline persistence is best-effort; a failed cache write must not
390
- // block returning the freshly fetched data.
391
- }
778
+ // Offline caching happens in `fetchBootstrap` on the assembled result —
779
+ // caching here would let a single-model chunk overwrite the full snapshot.
392
780
  return data;
393
781
  }
394
782
  /**
@@ -462,11 +850,11 @@ export class BootstrapFetcher {
462
850
  }
463
851
  keysToRemove.forEach((key) => {
464
852
  localStorage.removeItem(key);
465
- getContext().logger.debug('Cleared cache key', { key });
853
+ this.runtime.logger.debug('Cleared cache key', { key });
466
854
  });
467
855
  }
468
856
  catch (error) {
469
- getContext().logger.debug('Failed to clear cache', { error });
857
+ this.runtime.logger.debug('Failed to clear cache', { error });
470
858
  }
471
859
  }
472
860
  // Cache helpers for offline bootstrap
@@ -480,7 +868,7 @@ export class BootstrapFetcher {
480
868
  localStorage.setItem(this.getBootstrapCacheKey(orgId), JSON.stringify(data));
481
869
  }
482
870
  catch (e) {
483
- getContext().logger.debug('Failed to cache bootstrap payload', {
871
+ this.runtime.logger.debug('Failed to cache bootstrap payload', {
484
872
  error: e instanceof Error ? e.message : String(e),
485
873
  });
486
874
  }
@@ -499,13 +887,16 @@ export class BootstrapFetcher {
499
887
  }
500
888
  }
501
889
  /**
502
- * Abort ongoing fetch request
890
+ * Abort every ongoing bootstrap request (including all chunks of a
891
+ * chunked cold start). Entity self-heal fetches are unaffected.
892
+ *
893
+ * The flight registry is cleared first and synchronously, so a caller that
894
+ * bootstraps again in the same tick starts a fresh request rather than
895
+ * joining the one being torn down.
503
896
  */
504
897
  abort() {
505
- if (this.abortController) {
506
- this.abortController.abort();
507
- this.abortController = null;
508
- }
898
+ this.flights.clear();
899
+ this.cancelActive(cancelled('Bootstrap aborted by its caller'));
509
900
  }
510
901
  /**
511
902
  * Helper to delay execution
@@ -529,8 +920,45 @@ export class BootstrapFetcher {
529
920
  return body.status === 'healthy';
530
921
  }
531
922
  catch {
532
- getContext().observability.breadcrumb('Health check failed', 'sync.bootstrap', 'warning');
923
+ this.runtime.observability.breadcrumb('Health check failed', 'sync.bootstrap', 'warning');
533
924
  return false;
534
925
  }
535
926
  }
536
927
  }
928
+ /**
929
+ * Assemble per-model chunk responses into one full snapshot.
930
+ *
931
+ * Each chunk is internally consistent at its own sync position, and the
932
+ * positions differ (the chunks were served seconds apart). Anchoring the
933
+ * merged snapshot at the MINIMUM position turns that skew into an ordinary
934
+ * "briefly offline client": the WS catch-up replays every delta from the
935
+ * anchor, and since deltas carry full rows, re-applying one a later chunk
936
+ * already reflects converges to the same state. Anchoring at anything later
937
+ * would silently skip deltas for the earliest-fetched models.
938
+ */
939
+ export function mergeBootstrapChunks(chunks) {
940
+ const models = {};
941
+ const failedModels = [];
942
+ let lastSyncId = Number.POSITIVE_INFINITY;
943
+ let timestamp = 0;
944
+ let schemaHash;
945
+ for (const chunk of chunks) {
946
+ // Concatenate per model: pages of one model arrive as separate chunks.
947
+ for (const [name, rows] of Object.entries(chunk.models ?? {})) {
948
+ (models[name] ??= []).push(...rows);
949
+ }
950
+ if (chunk.failedModels)
951
+ failedModels.push(...chunk.failedModels);
952
+ lastSyncId = Math.min(lastSyncId, chunk.lastSyncId);
953
+ timestamp = Math.max(timestamp, chunk.timestamp);
954
+ schemaHash ??= chunk.schemaHash;
955
+ }
956
+ return {
957
+ type: 'full',
958
+ lastSyncId: Number.isFinite(lastSyncId) ? lastSyncId : 0,
959
+ models,
960
+ ...(failedModels.length > 0 ? { failedModels } : {}),
961
+ timestamp,
962
+ ...(schemaHash !== undefined ? { schemaHash } : {}),
963
+ };
964
+ }