@abloatai/ablo 0.36.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 (597) hide show
  1. package/AGENTS.md +2 -2
  2. package/CHANGELOG.md +71 -2013
  3. package/NOTICE +2 -2
  4. package/README.md +25 -71
  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 -114
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +3 -163
  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 +1 -1
  59. package/docs/api-keys.md +7 -6
  60. package/docs/api.md +10 -10
  61. package/docs/client-behavior.md +5 -5
  62. package/docs/coordination.md +52 -62
  63. package/docs/data-sources.md +1 -1
  64. package/docs/examples/agent-human.md +4 -4
  65. package/docs/examples/ai-sdk-tool.md +1 -1
  66. package/docs/examples/existing-python-backend.md +15 -4
  67. package/docs/examples/nextjs.md +27 -6
  68. package/docs/examples/server-agent.md +2 -2
  69. package/docs/how-it-works.md +4 -4
  70. package/docs/identity.md +2 -1
  71. package/docs/integration-guide.md +24 -13
  72. package/docs/internal/README.md +18 -0
  73. package/docs/internal/agent-fleet-coordination-design.md +171 -0
  74. package/docs/internal/agent-orchestration.md +58 -0
  75. package/docs/internal/commit-identifiers.md +91 -0
  76. package/docs/internal/concurrency-open-decisions.md +37 -0
  77. package/docs/internal/data-source-reverse-channel.md +150 -0
  78. package/docs/internal/per-field-conflict-detection.md +165 -0
  79. package/docs/internal/postgres-replication.md +64 -0
  80. package/docs/internal/serializable-schema.md +119 -0
  81. package/docs/internal/structure.md +32 -0
  82. package/docs/mcp.md +2 -2
  83. package/docs/migration.md +3 -3
  84. package/docs/quickstart.md +2 -2
  85. package/docs/react.md +5 -5
  86. package/docs/schema-contract.md +3 -3
  87. package/docs/sessions.md +91 -37
  88. package/examples/README.md +2 -2
  89. package/examples/data-source/README.md +1 -1
  90. package/examples/data-source/ablo-driver.ts +1 -1
  91. package/examples/data-source/customer-server.ts +1 -1
  92. package/examples/data-source/run.ts +1 -1
  93. package/examples/data-source/schema.ts +1 -1
  94. package/examples/quickstart.ts +2 -2
  95. package/llms.txt +8 -8
  96. package/package.json +63 -166
  97. package/bin/ablo.cjs +0 -39
  98. package/dist/BaseSyncedStore.d.ts +0 -843
  99. package/dist/BaseSyncedStore.js +0 -1971
  100. package/dist/Database.d.ts +0 -323
  101. package/dist/Database.js +0 -1502
  102. package/dist/InstanceCache.d.ts +0 -237
  103. package/dist/InstanceCache.js +0 -1166
  104. package/dist/LazyReferenceCollection.d.ts +0 -177
  105. package/dist/LazyReferenceCollection.js +0 -461
  106. package/dist/Model.d.ts +0 -454
  107. package/dist/Model.js +0 -919
  108. package/dist/ModelRegistry.d.ts +0 -225
  109. package/dist/ModelRegistry.js +0 -539
  110. package/dist/NetworkMonitor.d.ts +0 -28
  111. package/dist/NetworkMonitor.js +0 -79
  112. package/dist/RuntimeContext.d.ts +0 -52
  113. package/dist/RuntimeContext.js +0 -80
  114. package/dist/SyncClient.d.ts +0 -541
  115. package/dist/SyncClient.js +0 -2202
  116. package/dist/adapters/alwaysOnline.d.ts +0 -14
  117. package/dist/adapters/alwaysOnline.js +0 -17
  118. package/dist/adapters/inMemoryStorage.d.ts +0 -31
  119. package/dist/adapters/inMemoryStorage.js +0 -110
  120. package/dist/ai-sdk/coordinatedTool.d.ts +0 -120
  121. package/dist/ai-sdk/coordinatedTool.js +0 -134
  122. package/dist/ai-sdk/coordinationContext.d.ts +0 -46
  123. package/dist/ai-sdk/coordinationContext.js +0 -106
  124. package/dist/ai-sdk/index.d.ts +0 -121
  125. package/dist/ai-sdk/index.js +0 -121
  126. package/dist/ai-sdk/wrap.d.ts +0 -65
  127. package/dist/ai-sdk/wrap.js +0 -39
  128. package/dist/auth/index.d.ts +0 -1
  129. package/dist/auth/index.js +0 -8
  130. package/dist/batching/index.d.ts +0 -55
  131. package/dist/batching/index.js +0 -147
  132. package/dist/client/Ablo.d.ts +0 -231
  133. package/dist/client/Ablo.js +0 -160
  134. package/dist/client/abloClient.d.ts +0 -309
  135. package/dist/client/abloClient.js +0 -13
  136. package/dist/client/clientPrelude.d.ts +0 -52
  137. package/dist/client/clientPrelude.js +0 -60
  138. package/dist/client/consoleLogger.d.ts +0 -35
  139. package/dist/client/consoleLogger.js +0 -44
  140. package/dist/client/coreClient.d.ts +0 -60
  141. package/dist/client/coreClient.js +0 -118
  142. package/dist/client/createInternalComponents.d.ts +0 -50
  143. package/dist/client/createInternalComponents.js +0 -98
  144. package/dist/client/createModelProxy.d.ts +0 -248
  145. package/dist/client/createModelProxy.js +0 -884
  146. package/dist/client/humans.d.ts +0 -69
  147. package/dist/client/humans.js +0 -78
  148. package/dist/client/modelRegistration.d.ts +0 -10
  149. package/dist/client/modelRegistration.js +0 -312
  150. package/dist/client/options.d.ts +0 -461
  151. package/dist/client/options.js +0 -7
  152. package/dist/client/reactiveEngine.d.ts +0 -53
  153. package/dist/client/reactiveEngine.js +0 -688
  154. package/dist/client/resourceTypes.d.ts +0 -12
  155. package/dist/client/resourceTypes.js +0 -10
  156. package/dist/client/schemaConfig.d.ts +0 -44
  157. package/dist/client/schemaConfig.js +0 -185
  158. package/dist/client/storeCluster.d.ts +0 -47
  159. package/dist/client/storeCluster.js +0 -118
  160. package/dist/client/storeLifecycle.d.ts +0 -61
  161. package/dist/client/storeLifecycle.js +0 -231
  162. package/dist/client/validateAbloOptions.d.ts +0 -42
  163. package/dist/client/validateAbloOptions.js +0 -43
  164. package/dist/client/wsMutationExecutor.d.ts +0 -27
  165. package/dist/client/wsMutationExecutor.js +0 -72
  166. package/dist/context.d.ts +0 -42
  167. package/dist/context.js +0 -81
  168. package/dist/coordination/ClaimLog.d.ts +0 -26
  169. package/dist/coordination/ClaimLog.js +0 -32
  170. package/dist/coordination/index.d.ts +0 -1
  171. package/dist/coordination/index.js +0 -8
  172. package/dist/core/index.d.ts +0 -33
  173. package/dist/core/index.js +0 -48
  174. package/dist/docs/catalog.d.ts +0 -72
  175. package/dist/docs/catalog.js +0 -230
  176. package/dist/docs/index.d.ts +0 -10
  177. package/dist/docs/index.js +0 -10
  178. package/dist/environment.d.ts +0 -1
  179. package/dist/environment.js +0 -8
  180. package/dist/interfaces/index.d.ts +0 -311
  181. package/dist/interfaces/index.js +0 -9
  182. package/dist/keys/index.d.ts +0 -1
  183. package/dist/keys/index.js +0 -8
  184. package/dist/mutators/RecordingMutation.d.ts +0 -36
  185. package/dist/mutators/RecordingMutation.js +0 -182
  186. package/dist/mutators/Transaction.d.ts +0 -40
  187. package/dist/mutators/Transaction.js +0 -58
  188. package/dist/mutators/UndoManager.d.ts +0 -258
  189. package/dist/mutators/UndoManager.js +0 -658
  190. package/dist/mutators/defineMutators.d.ts +0 -60
  191. package/dist/mutators/defineMutators.js +0 -18
  192. package/dist/mutators/inverseOp.d.ts +0 -126
  193. package/dist/mutators/inverseOp.js +0 -71
  194. package/dist/mutators/mutateActions.d.ts +0 -45
  195. package/dist/mutators/mutateActions.js +0 -105
  196. package/dist/mutators/readerActions.d.ts +0 -33
  197. package/dist/mutators/readerActions.js +0 -57
  198. package/dist/mutators/undoApply.d.ts +0 -51
  199. package/dist/mutators/undoApply.js +0 -117
  200. package/dist/policy/index.d.ts +0 -21
  201. package/dist/policy/index.js +0 -20
  202. package/dist/query/QueryProcessor.d.ts +0 -75
  203. package/dist/query/QueryProcessor.js +0 -255
  204. package/dist/query/client.d.ts +0 -64
  205. package/dist/query/client.js +0 -138
  206. package/dist/query/types.d.ts +0 -85
  207. package/dist/query/types.js +0 -16
  208. package/dist/react/AbloProvider.d.ts +0 -242
  209. package/dist/react/AbloProvider.js +0 -458
  210. package/dist/react/ClientSideSuspense.d.ts +0 -36
  211. package/dist/react/ClientSideSuspense.js +0 -17
  212. package/dist/react/DefaultFallback.d.ts +0 -24
  213. package/dist/react/DefaultFallback.js +0 -43
  214. package/dist/react/context.d.ts +0 -55
  215. package/dist/react/context.js +0 -29
  216. package/dist/react/createAbloReact.d.ts +0 -56
  217. package/dist/react/createAbloReact.js +0 -51
  218. package/dist/react/index.d.ts +0 -62
  219. package/dist/react/index.js +0 -69
  220. package/dist/react/internalContext.d.ts +0 -33
  221. package/dist/react/internalContext.js +0 -3
  222. package/dist/react/useAblo.d.ts +0 -82
  223. package/dist/react/useAblo.js +0 -120
  224. package/dist/react/useCurrentUserId.d.ts +0 -22
  225. package/dist/react/useCurrentUserId.js +0 -34
  226. package/dist/react/useErrorListener.d.ts +0 -20
  227. package/dist/react/useErrorListener.js +0 -38
  228. package/dist/react/useMutationFailureListener.d.ts +0 -26
  229. package/dist/react/useMutationFailureListener.js +0 -38
  230. package/dist/react/useMutators.d.ts +0 -56
  231. package/dist/react/useMutators.js +0 -84
  232. package/dist/react/useReactive.d.ts +0 -35
  233. package/dist/react/useReactive.js +0 -123
  234. package/dist/react/useSyncStatus.d.ts +0 -59
  235. package/dist/react/useSyncStatus.js +0 -76
  236. package/dist/react/useUndoScope.d.ts +0 -34
  237. package/dist/react/useUndoScope.js +0 -81
  238. package/dist/schema/coordination.d.ts +0 -112
  239. package/dist/schema/coordination.js +0 -133
  240. package/dist/schema/ddl.d.ts +0 -97
  241. package/dist/schema/ddl.js +0 -491
  242. package/dist/schema/ddlLock.d.ts +0 -35
  243. package/dist/schema/ddlLock.js +0 -46
  244. package/dist/schema/diff.d.ts +0 -225
  245. package/dist/schema/diff.js +0 -289
  246. package/dist/schema/generate.d.ts +0 -19
  247. package/dist/schema/generate.js +0 -86
  248. package/dist/schema/index.d.ts +0 -42
  249. package/dist/schema/index.js +0 -80
  250. package/dist/schema/queries.d.ts +0 -201
  251. package/dist/schema/queries.js +0 -144
  252. package/dist/schema/select.d.ts +0 -40
  253. package/dist/schema/select.js +0 -90
  254. package/dist/schema/serialize.d.ts +0 -115
  255. package/dist/schema/serialize.js +0 -265
  256. package/dist/schema/sugar.d.ts +0 -109
  257. package/dist/schema/sugar.js +0 -83
  258. package/dist/schema/syncDeltaRow.d.ts +0 -6
  259. package/dist/schema/syncDeltaRow.js +0 -6
  260. package/dist/server/adapter.d.ts +0 -173
  261. package/dist/server/adapter.js +0 -18
  262. package/dist/server/commit.d.ts +0 -107
  263. package/dist/server/commit.js +0 -1
  264. package/dist/server/index.d.ts +0 -14
  265. package/dist/server/index.js +0 -2
  266. package/dist/server/readConfig.d.ts +0 -80
  267. package/dist/server/readConfig.js +0 -8
  268. package/dist/server/storageMode.d.ts +0 -23
  269. package/dist/server/storageMode.js +0 -17
  270. package/dist/source/adapter.d.ts +0 -83
  271. package/dist/source/adapter.js +0 -24
  272. package/dist/source/adapters/drizzle.d.ts +0 -48
  273. package/dist/source/adapters/drizzle.js +0 -219
  274. package/dist/source/adapters/kysely.d.ts +0 -42
  275. package/dist/source/adapters/kysely.js +0 -205
  276. package/dist/source/adapters/kyselyMutationCore.d.ts +0 -76
  277. package/dist/source/adapters/kyselyMutationCore.js +0 -125
  278. package/dist/source/adapters/memory.d.ts +0 -13
  279. package/dist/source/adapters/memory.js +0 -130
  280. package/dist/source/adapters/prisma.d.ts +0 -63
  281. package/dist/source/adapters/prisma.js +0 -202
  282. package/dist/source/conformance.d.ts +0 -37
  283. package/dist/source/conformance.js +0 -215
  284. package/dist/source/connector.d.ts +0 -95
  285. package/dist/source/connector.js +0 -266
  286. package/dist/source/connectorProtocol.d.ts +0 -154
  287. package/dist/source/connectorProtocol.js +0 -163
  288. package/dist/source/contract.d.ts +0 -195
  289. package/dist/source/contract.js +0 -164
  290. package/dist/source/factory.d.ts +0 -92
  291. package/dist/source/factory.js +0 -286
  292. package/dist/source/idempotency.d.ts +0 -61
  293. package/dist/source/idempotency.js +0 -144
  294. package/dist/source/index.d.ts +0 -23
  295. package/dist/source/index.js +0 -30
  296. package/dist/source/migrations.d.ts +0 -21
  297. package/dist/source/migrations.js +0 -103
  298. package/dist/source/next.d.ts +0 -32
  299. package/dist/source/next.js +0 -25
  300. package/dist/source/pushQueue.d.ts +0 -134
  301. package/dist/source/pushQueue.js +0 -256
  302. package/dist/source/signing.d.ts +0 -92
  303. package/dist/source/signing.js +0 -162
  304. package/dist/source/types.d.ts +0 -401
  305. package/dist/source/types.js +0 -59
  306. package/dist/storeContract.d.ts +0 -145
  307. package/dist/storeContract.js +0 -12
  308. package/dist/stores/DatabaseManager.d.ts +0 -107
  309. package/dist/stores/DatabaseManager.js +0 -388
  310. package/dist/stores/ObjectStore.d.ts +0 -115
  311. package/dist/stores/ObjectStore.js +0 -393
  312. package/dist/stores/ObjectStoreContract.d.ts +0 -38
  313. package/dist/stores/ObjectStoreContract.js +0 -1
  314. package/dist/stores/StoreManager.d.ts +0 -114
  315. package/dist/stores/StoreManager.js +0 -304
  316. package/dist/stores/SyncActionStore.d.ts +0 -99
  317. package/dist/stores/SyncActionStore.js +0 -506
  318. package/dist/stores/openIDBWithTimeout.d.ts +0 -65
  319. package/dist/stores/openIDBWithTimeout.js +0 -153
  320. package/dist/stores/syncAction.d.ts +0 -26
  321. package/dist/stores/syncAction.js +0 -16
  322. package/dist/surface.d.ts +0 -36
  323. package/dist/surface.js +0 -75
  324. package/dist/sync/BootstrapFetcher.d.ts +0 -284
  325. package/dist/sync/BootstrapFetcher.js +0 -964
  326. package/dist/sync/ConnectionManager.d.ts +0 -8
  327. package/dist/sync/ConnectionManager.js +0 -8
  328. package/dist/sync/OnDemandLoader.d.ts +0 -231
  329. package/dist/sync/OnDemandLoader.js +0 -743
  330. package/dist/sync/SubscriptionManager.d.ts +0 -159
  331. package/dist/sync/SubscriptionManager.js +0 -243
  332. package/dist/sync/SyncWebSocket.d.ts +0 -173
  333. package/dist/sync/SyncWebSocket.js +0 -438
  334. package/dist/sync/awaitClaimGrant.d.ts +0 -6
  335. package/dist/sync/awaitClaimGrant.js +0 -6
  336. package/dist/sync/bootstrapApply.d.ts +0 -73
  337. package/dist/sync/bootstrapApply.js +0 -73
  338. package/dist/sync/commitFrames.d.ts +0 -8
  339. package/dist/sync/commitFrames.js +0 -8
  340. package/dist/sync/contextPorts.d.ts +0 -18
  341. package/dist/sync/contextPorts.js +0 -31
  342. package/dist/sync/createClaimStream.d.ts +0 -7
  343. package/dist/sync/createClaimStream.js +0 -7
  344. package/dist/sync/createPresenceStream.d.ts +0 -69
  345. package/dist/sync/createPresenceStream.js +0 -200
  346. package/dist/sync/createSnapshot.d.ts +0 -29
  347. package/dist/sync/createSnapshot.js +0 -118
  348. package/dist/sync/credentialLifecycle.d.ts +0 -7
  349. package/dist/sync/credentialLifecycle.js +0 -7
  350. package/dist/sync/deltaPipeline.d.ts +0 -114
  351. package/dist/sync/deltaPipeline.js +0 -278
  352. package/dist/sync/groupChange.d.ts +0 -116
  353. package/dist/sync/groupChange.js +0 -244
  354. package/dist/sync/participants.d.ts +0 -132
  355. package/dist/sync/participants.js +0 -346
  356. package/dist/sync/persistedPrefix.d.ts +0 -12
  357. package/dist/sync/persistedPrefix.js +0 -22
  358. package/dist/sync/schemaDrift.d.ts +0 -55
  359. package/dist/sync/schemaDrift.js +0 -53
  360. package/dist/sync/schemas.d.ts +0 -71
  361. package/dist/sync/schemas.js +0 -94
  362. package/dist/sync/syncCursor.d.ts +0 -40
  363. package/dist/sync/syncCursor.js +0 -55
  364. package/dist/sync/syncPlan.d.ts +0 -54
  365. package/dist/sync/syncPlan.js +0 -50
  366. package/dist/sync/wsFrameHandlers.d.ts +0 -8
  367. package/dist/sync/wsFrameHandlers.js +0 -8
  368. package/dist/syncLog/contract.d.ts +0 -20
  369. package/dist/syncLog/contract.js +0 -19
  370. package/dist/syncLog/index.d.ts +0 -1
  371. package/dist/syncLog/index.js +0 -1
  372. package/dist/transaction/ablo.d.ts +0 -88
  373. package/dist/transaction/ablo.js +0 -33
  374. package/dist/transaction/auth/apiKey.d.ts +0 -152
  375. package/dist/transaction/auth/apiKey.js +0 -419
  376. package/dist/transaction/auth/bootstrapScope.d.ts +0 -15
  377. package/dist/transaction/auth/bootstrapScope.js +0 -1
  378. package/dist/transaction/auth/capability.d.ts +0 -212
  379. package/dist/transaction/auth/capability.js +0 -224
  380. package/dist/transaction/auth/credentialEndpoint.d.ts +0 -61
  381. package/dist/transaction/auth/credentialEndpoint.js +0 -86
  382. package/dist/transaction/auth/credentialPolicy.d.ts +0 -148
  383. package/dist/transaction/auth/credentialPolicy.js +0 -125
  384. package/dist/transaction/auth/credentialSource.d.ts +0 -30
  385. package/dist/transaction/auth/credentialSource.js +0 -55
  386. package/dist/transaction/auth/hostedEndpoints.d.ts +0 -21
  387. package/dist/transaction/auth/hostedEndpoints.js +0 -21
  388. package/dist/transaction/auth/identity.d.ts +0 -55
  389. package/dist/transaction/auth/identity.js +0 -210
  390. package/dist/transaction/auth/index.d.ts +0 -162
  391. package/dist/transaction/auth/index.js +0 -304
  392. package/dist/transaction/auth/schemas.d.ts +0 -59
  393. package/dist/transaction/auth/schemas.js +0 -85
  394. package/dist/transaction/auth/sessionMint.d.ts +0 -28
  395. package/dist/transaction/auth/sessionMint.js +0 -85
  396. package/dist/transaction/coordination/awaitClaimGrant.d.ts +0 -56
  397. package/dist/transaction/coordination/awaitClaimGrant.js +0 -124
  398. package/dist/transaction/coordination/claimHeartbeatLoop.d.ts +0 -84
  399. package/dist/transaction/coordination/claimHeartbeatLoop.js +0 -108
  400. package/dist/transaction/coordination/claimMeta.d.ts +0 -49
  401. package/dist/transaction/coordination/claimMeta.js +0 -52
  402. package/dist/transaction/coordination/createClaimStream.d.ts +0 -64
  403. package/dist/transaction/coordination/createClaimStream.js +0 -475
  404. package/dist/transaction/coordination/events.d.ts +0 -74
  405. package/dist/transaction/coordination/events.js +0 -7
  406. package/dist/transaction/coordination/index.d.ts +0 -19
  407. package/dist/transaction/coordination/index.js +0 -45
  408. package/dist/transaction/coordination/locator.d.ts +0 -104
  409. package/dist/transaction/coordination/locator.js +0 -102
  410. package/dist/transaction/coordination/schema.d.ts +0 -1536
  411. package/dist/transaction/coordination/schema.js +0 -1177
  412. package/dist/transaction/coordination/targetConflict.d.ts +0 -2
  413. package/dist/transaction/coordination/targetConflict.js +0 -107
  414. package/dist/transaction/coordination/trace.d.ts +0 -78
  415. package/dist/transaction/coordination/trace.js +0 -138
  416. package/dist/transaction/durableWrites.d.ts +0 -62
  417. package/dist/transaction/durableWrites.js +0 -71
  418. package/dist/transaction/environment.d.ts +0 -105
  419. package/dist/transaction/environment.js +0 -108
  420. package/dist/transaction/errorCodes.d.ts +0 -403
  421. package/dist/transaction/errorCodes.js +0 -484
  422. package/dist/transaction/errors.d.ts +0 -428
  423. package/dist/transaction/errors.js +0 -686
  424. package/dist/transaction/footprint.d.ts +0 -111
  425. package/dist/transaction/footprint.js +0 -0
  426. package/dist/transaction/index.d.ts +0 -20
  427. package/dist/transaction/index.js +0 -20
  428. package/dist/transaction/keys/index.d.ts +0 -87
  429. package/dist/transaction/keys/index.js +0 -207
  430. package/dist/transaction/log/syncDeltaRow.d.ts +0 -158
  431. package/dist/transaction/log/syncDeltaRow.js +0 -95
  432. package/dist/transaction/logPosition.d.ts +0 -97
  433. package/dist/transaction/logPosition.js +0 -125
  434. package/dist/transaction/logger.d.ts +0 -16
  435. package/dist/transaction/logger.js +0 -7
  436. package/dist/transaction/observability.d.ts +0 -53
  437. package/dist/transaction/observability.js +0 -19
  438. package/dist/transaction/persistence.d.ts +0 -12
  439. package/dist/transaction/persistence.js +0 -11
  440. package/dist/transaction/plugin.d.ts +0 -285
  441. package/dist/transaction/plugin.js +0 -106
  442. package/dist/transaction/policy/types.d.ts +0 -217
  443. package/dist/transaction/policy/types.js +0 -126
  444. package/dist/transaction/resources/functionalUpdate.d.ts +0 -79
  445. package/dist/transaction/resources/functionalUpdate.js +0 -87
  446. package/dist/transaction/resources/httpResources.d.ts +0 -321
  447. package/dist/transaction/resources/httpResources.js +0 -7
  448. package/dist/transaction/resources/modelOperations.d.ts +0 -427
  449. package/dist/transaction/resources/modelOperations.js +0 -12
  450. package/dist/transaction/resources/mutationOptions.d.ts +0 -66
  451. package/dist/transaction/resources/mutationOptions.js +0 -9
  452. package/dist/transaction/resources/where.d.ts +0 -101
  453. package/dist/transaction/resources/where.js +0 -115
  454. package/dist/transaction/resources/writeOptionsSchema.d.ts +0 -47
  455. package/dist/transaction/resources/writeOptionsSchema.js +0 -73
  456. package/dist/transaction/schema/field.d.ts +0 -120
  457. package/dist/transaction/schema/field.js +0 -265
  458. package/dist/transaction/schema/fieldRef.d.ts +0 -38
  459. package/dist/transaction/schema/fieldRef.js +0 -11
  460. package/dist/transaction/schema/loadStrategy.d.ts +0 -45
  461. package/dist/transaction/schema/loadStrategy.js +0 -46
  462. package/dist/transaction/schema/model.d.ts +0 -379
  463. package/dist/transaction/schema/model.js +0 -123
  464. package/dist/transaction/schema/openapi.d.ts +0 -58
  465. package/dist/transaction/schema/openapi.js +0 -501
  466. package/dist/transaction/schema/relation.d.ts +0 -204
  467. package/dist/transaction/schema/relation.js +0 -104
  468. package/dist/transaction/schema/residency.d.ts +0 -29
  469. package/dist/transaction/schema/residency.js +0 -25
  470. package/dist/transaction/schema/roles.d.ts +0 -249
  471. package/dist/transaction/schema/roles.js +0 -230
  472. package/dist/transaction/schema/schema.d.ts +0 -351
  473. package/dist/transaction/schema/schema.js +0 -325
  474. package/dist/transaction/schema/tenancy.d.ts +0 -139
  475. package/dist/transaction/schema/tenancy.js +0 -190
  476. package/dist/transaction/transactionLayer.d.ts +0 -82
  477. package/dist/transaction/transactionLayer.js +0 -24
  478. package/dist/transaction/transactions/settlement/commitEnvelope.d.ts +0 -143
  479. package/dist/transaction/transactions/settlement/commitEnvelope.js +0 -161
  480. package/dist/transaction/transactions/settlement/httpCommitEnvelope.d.ts +0 -53
  481. package/dist/transaction/transactions/settlement/httpCommitEnvelope.js +0 -207
  482. package/dist/transaction/transactions/settlement/idempotencyKey.d.ts +0 -10
  483. package/dist/transaction/transactions/settlement/idempotencyKey.js +0 -9
  484. package/dist/transaction/transactions/settlement/pendingWrite.d.ts +0 -112
  485. package/dist/transaction/transactions/settlement/pendingWrite.js +0 -20
  486. package/dist/transaction/transport/commitFrames.d.ts +0 -90
  487. package/dist/transaction/transport/commitFrames.js +0 -134
  488. package/dist/transaction/transport/connectionManager.d.ts +0 -215
  489. package/dist/transaction/transport/connectionManager.js +0 -673
  490. package/dist/transaction/transport/credentialLifecycle.d.ts +0 -177
  491. package/dist/transaction/transport/credentialLifecycle.js +0 -324
  492. package/dist/transaction/transport/heartbeat.d.ts +0 -65
  493. package/dist/transaction/transport/heartbeat.js +0 -93
  494. package/dist/transaction/transport/httpClient.d.ts +0 -131
  495. package/dist/transaction/transport/httpClient.js +0 -146
  496. package/dist/transaction/transport/httpOptions.d.ts +0 -33
  497. package/dist/transaction/transport/httpOptions.js +0 -12
  498. package/dist/transaction/transport/httpTransport.d.ts +0 -8
  499. package/dist/transaction/transport/httpTransport.js +0 -1388
  500. package/dist/transaction/transport/networkProbe.d.ts +0 -84
  501. package/dist/transaction/transport/networkProbe.js +0 -207
  502. package/dist/transaction/transport/wsFrameHandlers.d.ts +0 -128
  503. package/dist/transaction/transport/wsFrameHandlers.js +0 -429
  504. package/dist/transaction/transport/wsTransport.d.ts +0 -574
  505. package/dist/transaction/transport/wsTransport.js +0 -1023
  506. package/dist/transaction/types/assertExact.d.ts +0 -17
  507. package/dist/transaction/types/assertExact.js +0 -1
  508. package/dist/transaction/types/global.d.ts +0 -107
  509. package/dist/transaction/types/global.js +0 -40
  510. package/dist/transaction/types/index.d.ts +0 -205
  511. package/dist/transaction/types/index.js +0 -56
  512. package/dist/transaction/types/modelData.d.ts +0 -10
  513. package/dist/transaction/types/modelData.js +0 -9
  514. package/dist/transaction/types/participant.d.ts +0 -20
  515. package/dist/transaction/types/participant.js +0 -10
  516. package/dist/transaction/types/streams.d.ts +0 -550
  517. package/dist/transaction/types/streams.js +0 -11
  518. package/dist/transaction/utils/asyncIterator.d.ts +0 -34
  519. package/dist/transaction/utils/asyncIterator.js +0 -135
  520. package/dist/transaction/utils/duration.d.ts +0 -50
  521. package/dist/transaction/utils/duration.js +0 -77
  522. package/dist/transaction/utils/json.d.ts +0 -57
  523. package/dist/transaction/utils/json.js +0 -276
  524. package/dist/transaction/wire/accountResponses.d.ts +0 -420
  525. package/dist/transaction/wire/accountResponses.js +0 -290
  526. package/dist/transaction/wire/auth.d.ts +0 -56
  527. package/dist/transaction/wire/auth.js +0 -63
  528. package/dist/transaction/wire/bootstrapReason.d.ts +0 -9
  529. package/dist/transaction/wire/bootstrapReason.js +0 -8
  530. package/dist/transaction/wire/claimEvent.d.ts +0 -76
  531. package/dist/transaction/wire/claimEvent.js +0 -73
  532. package/dist/transaction/wire/claims.d.ts +0 -530
  533. package/dist/transaction/wire/claims.js +0 -327
  534. package/dist/transaction/wire/commit.d.ts +0 -603
  535. package/dist/transaction/wire/commit.js +0 -321
  536. package/dist/transaction/wire/delta.d.ts +0 -250
  537. package/dist/transaction/wire/delta.js +0 -147
  538. package/dist/transaction/wire/errorEnvelope.d.ts +0 -72
  539. package/dist/transaction/wire/errorEnvelope.js +0 -123
  540. package/dist/transaction/wire/feedCursor.d.ts +0 -60
  541. package/dist/transaction/wire/feedCursor.js +0 -82
  542. package/dist/transaction/wire/feedEvent.d.ts +0 -204
  543. package/dist/transaction/wire/feedEvent.js +0 -65
  544. package/dist/transaction/wire/frames.d.ts +0 -194
  545. package/dist/transaction/wire/frames.js +0 -50
  546. package/dist/transaction/wire/inboundFrames.d.ts +0 -562
  547. package/dist/transaction/wire/inboundFrames.js +0 -116
  548. package/dist/transaction/wire/index.d.ts +0 -54
  549. package/dist/transaction/wire/index.js +0 -83
  550. package/dist/transaction/wire/listEnvelope.d.ts +0 -37
  551. package/dist/transaction/wire/listEnvelope.js +0 -42
  552. package/dist/transaction/wire/modelMutations.d.ts +0 -31
  553. package/dist/transaction/wire/modelMutations.js +0 -52
  554. package/dist/transaction/wire/modelResponses.d.ts +0 -85
  555. package/dist/transaction/wire/modelResponses.js +0 -43
  556. package/dist/transaction/wire/modelShape.d.ts +0 -78
  557. package/dist/transaction/wire/modelShape.js +0 -74
  558. package/dist/transaction/wire/protocol.d.ts +0 -38
  559. package/dist/transaction/wire/protocol.js +0 -38
  560. package/dist/transaction/wire/protocolVersion.d.ts +0 -73
  561. package/dist/transaction/wire/protocolVersion.js +0 -83
  562. package/dist/transactions/mutations/MutationQueue.d.ts +0 -661
  563. package/dist/transactions/mutations/MutationQueue.js +0 -2807
  564. package/dist/transactions/mutations/MutationStore.d.ts +0 -20
  565. package/dist/transactions/mutations/MutationStore.js +0 -53
  566. package/dist/transactions/mutations/UnconfirmedWrites.d.ts +0 -82
  567. package/dist/transactions/mutations/UnconfirmedWrites.js +0 -104
  568. package/dist/transactions/mutations/coalesceRules.d.ts +0 -58
  569. package/dist/transactions/mutations/coalesceRules.js +0 -140
  570. package/dist/transactions/mutations/commitLatency.d.ts +0 -52
  571. package/dist/transactions/mutations/commitLatency.js +0 -130
  572. package/dist/transactions/mutations/commitOutboxStore.d.ts +0 -28
  573. package/dist/transactions/mutations/commitOutboxStore.js +0 -26
  574. package/dist/transactions/mutations/commitPayload.d.ts +0 -165
  575. package/dist/transactions/mutations/commitPayload.js +0 -152
  576. package/dist/transactions/mutations/deltaConfirmation.d.ts +0 -63
  577. package/dist/transactions/mutations/deltaConfirmation.js +0 -235
  578. package/dist/transactions/mutations/durableWriteStore.d.ts +0 -14
  579. package/dist/transactions/mutations/durableWriteStore.js +0 -12
  580. package/dist/transactions/mutations/optimisticApply.d.ts +0 -49
  581. package/dist/transactions/mutations/optimisticApply.js +0 -65
  582. package/dist/transactions/mutations/replayValidation.d.ts +0 -187
  583. package/dist/transactions/mutations/replayValidation.js +0 -164
  584. package/dist/utils/mobxSetup.d.ts +0 -53
  585. package/dist/utils/mobxSetup.js +0 -330
  586. package/dist/views/QueryView.d.ts +0 -79
  587. package/dist/views/QueryView.js +0 -218
  588. package/dist/views/ViewRegistry.d.ts +0 -20
  589. package/dist/views/ViewRegistry.js +0 -55
  590. package/dist/views/incrementalView.d.ts +0 -45
  591. package/dist/views/incrementalView.js +0 -69
  592. package/dist/webhooks/events.d.ts +0 -43
  593. package/dist/webhooks/events.js +0 -42
  594. package/dist/webhooks/index.d.ts +0 -8
  595. package/dist/webhooks/index.js +0 -8
  596. package/dist/wire/index.d.ts +0 -1
  597. package/dist/wire/index.js +0 -8
package/dist/Database.js DELETED
@@ -1,1502 +0,0 @@
1
- /**
2
- * The local persistence layer for synced models. It stores rows in the
3
- * browser's IndexedDB (or in-memory maps when run headlessly), applies inbound
4
- * deltas to that store, and fetches the bootstrap snapshot from your sync
5
- * server. {@link BaseSyncedStore} drives it, and {@link InstanceCache} holds the
6
- * in-memory mirror of what this class persists.
7
- */
8
- import { DatabaseManager } from './stores/DatabaseManager.js';
9
- import { StoreManager } from './stores/StoreManager.js';
10
- import { ModelRegistry } from './ModelRegistry.js';
11
- import { LoadStrategy } from './transaction/types/index.js';
12
- import { globalRuntime } from './context.js';
13
- import { AbloConnectionError, AbloValidationError } from './transaction/errors.js';
14
- import { InMemoryObjectStore } from './adapters/inMemoryStorage.js';
15
- import { logPositionSchema } from './transaction/logPosition.js';
16
- import { highestPersistedPrefixSyncId } from './sync/persistedPrefix.js';
17
- /**
18
- * Request identity excludes local timing metadata for re-entrant seals: a
19
- * retry rebuilds its envelope with a fresh `sequence`/seal clock, so comparing
20
- * those volatile fields would reject every legitimate same-request re-seal as
21
- * an idempotency conflict. Only the fields that define the wire request count.
22
- */
23
- function isSameOutboxRecord(existing, candidate) {
24
- if (existing.type === 'http_commit_envelope' &&
25
- candidate.type === 'http_commit_envelope') {
26
- const identity = (record) => ({
27
- id: record.id,
28
- type: record.type,
29
- storageVersion: record.storageVersion,
30
- idempotencyKey: record.idempotencyKey,
31
- // HTTP outbox rows written before protocol versioning are v1. Normalize
32
- // them so a same-request re-seal remains idempotent after an upgrade.
33
- protocolVersion: record.protocolVersion ?? 1,
34
- request: record.request,
35
- scopeNamespace: record.scopeNamespace,
36
- });
37
- if (existing.correlationId !== undefined &&
38
- candidate.correlationId !== undefined &&
39
- existing.correlationId !== candidate.correlationId) {
40
- return false;
41
- }
42
- return JSON.stringify(identity(existing)) === JSON.stringify(identity(candidate));
43
- }
44
- if (existing.type === 'commit_envelope' &&
45
- candidate.type === 'commit_envelope') {
46
- const identity = (record) => ({
47
- id: record.id,
48
- type: record.type,
49
- storageVersion: record.storageVersion,
50
- origin: record.origin,
51
- idempotencyKey: record.idempotencyKey,
52
- operations: record.operations,
53
- sourceMutationIds: record.sourceMutationIds,
54
- commitOptions: record.commitOptions,
55
- scope: record.scope,
56
- });
57
- if (existing.correlationId !== undefined &&
58
- candidate.correlationId !== undefined &&
59
- existing.correlationId !== candidate.correlationId) {
60
- return false;
61
- }
62
- return JSON.stringify(identity(existing)) === JSON.stringify(identity(candidate));
63
- }
64
- return JSON.stringify(existing) === JSON.stringify(candidate);
65
- }
66
- function isAcceptedOutboxPromotion(existing, candidate) {
67
- return (existing !== undefined &&
68
- (existing.type === 'commit_envelope' ||
69
- existing.type === 'http_commit_envelope') &&
70
- existing.type === candidate.type &&
71
- existing.acceptedAt === undefined &&
72
- candidate.acceptedAt !== undefined);
73
- }
74
- export class Database {
75
- // Core database components
76
- databaseManager;
77
- storeManager;
78
- // Injected dependencies
79
- modelRegistry;
80
- bootstrapHelper;
81
- /** The pre-configured query helper for lazy-loading data from the sync server. */
82
- get helper() {
83
- return this.bootstrapHelper;
84
- }
85
- /**
86
- * Fetch the current rows of the given sync groups as a side-effect-free
87
- * snapshot, used to hydrate a scope as the user enters it. Unlike
88
- * {@link bootstrapFromServer}, it does not persist to IndexedDB and does not
89
- * change the connection's subscribed sync groups. The caller applies the
90
- * result to the pool through the scoped apply path.
91
- */
92
- async fetchScopedBootstrapData(syncGroups) {
93
- // No lastSyncId → a full snapshot of exactly these groups.
94
- return this.bootstrapHelper.fetchBootstrap(undefined, syncGroups);
95
- }
96
- // Database state
97
- currentDbInfo = null;
98
- workspaceDb = null;
99
- /**
100
- * Flag to track if database is closing/closed.
101
- * Used for graceful degradation when operations are attempted during shutdown.
102
- */
103
- isClosing = false;
104
- /**
105
- * When set, forces the next requiredBootstrap() call to return 'full' even if offline.
106
- * Used when a sync group change delta is received — we must re-bootstrap to purge
107
- * revoked data, even if the device is currently offline (it will bootstrap when online).
108
- */
109
- _forceFullBootstrap = false;
110
- /** Essential fields that must be preserved during partial UPDATE merges.
111
- * Sourced from SyncEngineConfig.essentialFields — consumers define their own. */
112
- get essentialFields() {
113
- return this.runtime.config.essentialFields;
114
- }
115
- /**
116
- * When true, all IndexedDB operations are replaced with in-memory Maps.
117
- * Enables the SDK to run headlessly in Node.js / agent workers / tests
118
- * without requiring a browser environment.
119
- *
120
- * Set via createSyncEngine({ storage: inMemoryStorage() }) or directly:
121
- * new Database(registry, bootstrap, { inMemory: true })
122
- */
123
- inMemory;
124
- runtime;
125
- /** In-memory stores used when inMemory=true. Keyed by model name. */
126
- inMemoryStores = new Map();
127
- /** In-memory workspace metadata when inMemory=true. */
128
- inMemoryMetadata = null;
129
- constructor(modelRegistry, bootstrapHelper, options) {
130
- this.runtime = options?.runtime ?? globalRuntime;
131
- this.databaseManager = new DatabaseManager(this.runtime);
132
- this.storeManager = new StoreManager(modelRegistry, this.runtime);
133
- this.modelRegistry = modelRegistry;
134
- this.bootstrapHelper = bootstrapHelper;
135
- this.inMemory = options?.inMemory ?? false;
136
- }
137
- /**
138
- * Get store for a model, or `undefined` if no store exists.
139
- *
140
- * Routes to `inMemoryStores` in inMemory mode and `storeManager`
141
- * otherwise. Both implementations satisfy `ObjectStoreContract`, so
142
- * callers don't branch on which one they got back.
143
- *
144
- * Pass `context` to emit an observability breadcrumb when the store
145
- * is missing — useful for hot paths (bootstrap, delta apply, hydrate)
146
- * where a missing store points to silent data loss. Callers that
147
- * already expect optional behavior (e.g. lazy lookups) can omit it.
148
- */
149
- getStore(modelName, context) {
150
- const store = this.inMemory
151
- ? this.inMemoryStores.get(modelName)
152
- : this.storeManager.getStore(modelName);
153
- if (!store && context) {
154
- this.runtime.observability.breadcrumb(`Store not found for model: ${modelName}`, 'sync.database', 'warning', { context });
155
- }
156
- return store;
157
- }
158
- /** Get store or throw if not found (for operations that require the store). */
159
- getRequiredStore(modelName) {
160
- const store = this.getStore(modelName);
161
- if (!store) {
162
- throw new AbloValidationError(`Store not found: ${modelName}`, {
163
- code: 'db_store_not_found',
164
- });
165
- }
166
- return store; // TypeScript narrows to non-undefined after the throw
167
- }
168
- /** Log preserved fields during partial UPDATE merge (debug helper) */
169
- logPreservedFields(modelName, modelId, existing, delta) {
170
- if (modelName === 'Activity')
171
- return;
172
- const requiredFields = this.essentialFields[modelName] ?? [];
173
- const preserved = requiredFields.filter((field) => existing[field] !== undefined && delta[field] === undefined);
174
- if (preserved.length > 0) {
175
- this.runtime.logger.debug('[Database] UPDATE merged - preserved fields', {
176
- modelName,
177
- modelId: modelId.slice(0, 12),
178
- deltaFields: Object.keys(delta),
179
- preservedFields: preserved,
180
- });
181
- }
182
- }
183
- async open(userId, organizationId, version = 1) {
184
- // Reset closing flag when opening (in case of reopen)
185
- this.isClosing = false;
186
- if (this.workspaceDb && this.currentDbInfo) {
187
- return;
188
- }
189
- // ── In-memory mode: skip IndexedDB entirely ──────────────────
190
- // Creates InMemoryObjectStore instances for all registered models.
191
- // Bootstrap via HTTP still works; only local persistence is skipped.
192
- if (this.inMemory) {
193
- this.runtime.logger.debug('Opening in-memory database (headless mode)');
194
- const allModels = this.modelRegistry.getRegisteredModelNames();
195
- for (const modelName of allModels) {
196
- const storeName = `store_${modelName.toLowerCase()}`;
197
- this.inMemoryStores.set(modelName, new InMemoryObjectStore(modelName, storeName));
198
- }
199
- // Create a __transactions store for the offline queue
200
- this.inMemoryStores.set('__transactions', new InMemoryObjectStore('__transactions', '__transactions'));
201
- this.runtime.logger.info(`In-memory database opened: ${this.inMemoryStores.size} stores`);
202
- return;
203
- }
204
- // ── Browser mode: IndexedDB (existing behavior, unchanged) ───
205
- this.runtime.logger.debug('Opening IndexedDB database');
206
- // Initialize meta database
207
- await this.databaseManager.initializeMetaDatabase();
208
- // Calculate database info
209
- this.currentDbInfo = await this.databaseManager.calculateDatabaseInfo(userId, organizationId, version);
210
- // Register database
211
- await this.databaseManager.registerDatabase(this.currentDbInfo);
212
- // Open workspace database
213
- this.workspaceDb = await this.databaseManager.openWorkspaceDatabase(this.currentDbInfo, async (db) => {
214
- await this.storeManager.createStores(db);
215
- });
216
- // Initialize stores
217
- await this.storeManager.initializeStores(this.workspaceDb);
218
- const readiness = await this.storeManager.checkReadinessOfStores();
219
- this.runtime.logger.info(`Database opened: ${this.currentDbInfo.name} (${readiness.readyStores.length}/${readiness.totalStores} stores ready)`);
220
- }
221
- /**
222
- * Shrink a record before persisting it. Drops `undefined` fields, empty
223
- * arrays, empty objects, and the redundant markers `__typename`, `__class`,
224
- * `clientId`, and `syncStatus`. Explicit `null` values are preserved, since
225
- * a null is a meaningful "clear this field" in a nullable column.
226
- *
227
- * By design this receives plain objects, never live observables: WebSocket
228
- * deltas arrive already parsed, optimistic updates come through `toJSON()`,
229
- * and bootstrap data is plain JSON from the server.
230
- */
231
- compactRecord(_modelName, data) {
232
- if (!data || typeof data !== 'object')
233
- return data;
234
- const out = {};
235
- for (const [key, value] of Object.entries(data)) {
236
- // Drop redundant or ephemeral markers
237
- if (key === '__typename' || key === '__class' || key === 'clientId' || key === 'syncStatus') {
238
- continue;
239
- }
240
- // Skip only `undefined`; preserve explicit `null`, which is a
241
- // meaningful value for a nullable column.
242
- if (value === undefined) {
243
- continue;
244
- }
245
- if (Array.isArray(value)) {
246
- if (value.length === 0)
247
- continue;
248
- out[key] = value;
249
- continue;
250
- }
251
- if (typeof value === 'object') {
252
- // Preserve explicit null values
253
- if (value === null) {
254
- out[key] = null;
255
- continue;
256
- }
257
- // Preserve Date objects (IndexedDB can clone these)
258
- if (value instanceof Date) {
259
- out[key] = value;
260
- continue;
261
- }
262
- // For plain objects, drop if empty
263
- if (Object.keys(value).length === 0)
264
- continue;
265
- out[key] = value;
266
- continue;
267
- }
268
- out[key] = value;
269
- }
270
- // Always ensure id is present
271
- if (!out.id && data.id)
272
- out.id = data.id;
273
- return out;
274
- }
275
- /**
276
- * Mark that the next bootstrap must be a full bootstrap.
277
- * Called when a sync group change ("G" delta) is received — the client must
278
- * re-fetch all data from the server to purge models from revoked sync groups.
279
- */
280
- markRequiresFullBootstrap() {
281
- this._forceFullBootstrap = true;
282
- this.runtime.logger.info('[Database] Marked for forced full bootstrap (sync group change)');
283
- }
284
- /**
285
- * Smart bootstrap requirements based on data freshness
286
- */
287
- async requiredBootstrap() {
288
- // In-memory mode (server-side agents, headless workers): there's
289
- // no `workspaceDb` by design — `open()` returns early after
290
- // initializing `inMemoryStores`. Persistent data never exists
291
- // across sessions, so the right answer is always a full bootstrap
292
- // from the server. Mirrors the `inMemory` short-circuit in
293
- // `setModelPersisted` / `isModelPersisted` / `getMetadata`.
294
- if (this.inMemory) {
295
- const instantModels = this.modelRegistry.getModelsByLoadStrategy(LoadStrategy.instant);
296
- const lazyModels = this.modelRegistry.getModelsByLoadStrategy(LoadStrategy.lazy);
297
- return {
298
- type: 'full',
299
- modelsToLoad: [...instantModels, ...lazyModels],
300
- lastSyncId: 0,
301
- syncGroups: [],
302
- };
303
- }
304
- if (!this.workspaceDb) {
305
- throw new AbloConnectionError('Database not opened', {
306
- code: 'db_not_opened',
307
- });
308
- }
309
- // Sync group change requires full re-bootstrap to purge revoked data
310
- if (this._forceFullBootstrap) {
311
- this._forceFullBootstrap = false;
312
- const instantModels = this.modelRegistry.getModelsByLoadStrategy(LoadStrategy.instant);
313
- const lazyModels = this.modelRegistry.getModelsByLoadStrategy(LoadStrategy.lazy);
314
- this.runtime.logger.info('[Database.requiredBootstrap] Forced FULL bootstrap (sync group change)');
315
- return {
316
- type: 'full',
317
- modelsToLoad: [...instantModels, ...lazyModels],
318
- lastSyncId: 0,
319
- syncGroups: [],
320
- };
321
- }
322
- const readiness = await this.storeManager.checkReadinessOfStores();
323
- const metadata = await this.databaseManager.getWorkspaceMetadata(this.workspaceDb);
324
- // Get models from registry
325
- const instantModels = this.modelRegistry.getModelsByLoadStrategy(LoadStrategy.instant);
326
- const lazyModels = this.modelRegistry.getModelsByLoadStrategy(LoadStrategy.lazy);
327
- const modelsToLoad = [...instantModels, ...lazyModels];
328
- // Gate the PERSISTED cursor through the sync-position schema field —
329
- // the one trust boundary for resume state. IDB can hand back anything
330
- // (a corrupted negative/float cursor would previously pass `|| 0`,
331
- // which only catches falsy, and get sent to the server as the resume
332
- // point). Invalid → 0 → full bootstrap, the safe degradation.
333
- const metadataLastSyncId = logPositionSchema.shape.persisted.safeParse(metadata?.lastSyncId).data ?? 0;
334
- const dataAge = metadata?.updatedAt ? Date.now() - metadata.updatedAt.getTime() : Infinity;
335
- // ── Cache-validity check ─────────────────────────────────────
336
- //
337
- // The cursor (lastSyncId) is only valid if the data it refers to
338
- // actually exists in the stores. If the local store was cleared (or
339
- // this is a fresh in-memory session), the metadata's lastSyncId is
340
- // stale — sending it to the server would trigger a partial bootstrap
341
- // that returns zero deltas because the gap is 0, leaving the client
342
- // with an empty InstanceCache.
343
- //
344
- // The fix is to sample the actual stores: if they hold no rows, the
345
- // cursor is meaningless regardless of what the metadata claims.
346
- const dataExists = this.inMemory
347
- ? false // In-memory mode: no persistent data across sessions
348
- : await this.storeManager.hasAnyData();
349
- // The effective lastSyncId: only trust the metadata cursor when
350
- // we've confirmed the data it refers to actually exists in the stores.
351
- const lastSyncId = dataExists ? metadataLastSyncId : 0;
352
- // Log the resolved database state for diagnostics.
353
- this.runtime.logger.debug('[Database.requiredBootstrap] State check', {
354
- readinessReady: readiness.ready,
355
- hasMetadata: !!metadata,
356
- metadataLastSyncId,
357
- effectiveLastSyncId: lastSyncId,
358
- dataExists,
359
- dataAge: metadata?.updatedAt ? Math.round(dataAge / 1000) + 's' : 'N/A',
360
- navigatorOnline: typeof navigator !== 'undefined' ? navigator.onLine : 'N/A',
361
- });
362
- // Determine bootstrap type based on connectivity and data state
363
- const offline = typeof navigator !== 'undefined' && navigator && navigator.onLine === false;
364
- let type;
365
- // hasLocalData: stores actually have records AND we have a valid cursor
366
- const hasLocalData = readiness.ready && dataExists && lastSyncId > 0;
367
- if (offline && hasLocalData) {
368
- // Offline with data - use local bootstrap (only option when offline)
369
- type = 'local';
370
- this.runtime.logger.info('Offline detected with local data - using local bootstrap');
371
- }
372
- else {
373
- // The server is the source of truth: always use a full bootstrap
374
- // when online.
375
- type = 'full';
376
- this.runtime.logger.info('Full bootstrap - server is source of truth', {
377
- reason: offline ? 'offline_no_data' : 'server_authoritative',
378
- hasLocalData,
379
- lastSyncId,
380
- dataExists,
381
- });
382
- }
383
- return {
384
- type,
385
- modelsToLoad,
386
- lastSyncId,
387
- syncGroups: metadata?.syncGroups ?? [],
388
- };
389
- }
390
- /**
391
- * Fetch a bootstrap snapshot (or delta batch) from the sync server and load
392
- * it into the local store, then return a {@link BootstrapResult} the caller
393
- * applies to the {@link InstanceCache}.
394
- */
395
- async bootstrapFromServer(requirements,
396
- /** Full sync-group subscription list — what the WS subscribes to
397
- * AND what gets persisted as `subscribedSyncGroups` for the
398
- * shrinkage check. Caller supplies the complete list, not just
399
- * team-derived groups. */
400
- syncGroups, onProgress) {
401
- this.runtime.logger.debug('Starting bootstrap fetch', {
402
- type: requirements.type,
403
- lastSyncId: requirements.lastSyncId,
404
- modelsToLoad: requirements.modelsToLoad,
405
- });
406
- this.runtime.logger.info('Database: Starting bootstrap from Go server', {
407
- type: requirements.type,
408
- syncGroups,
409
- modelsToLoad: requirements.modelsToLoad,
410
- });
411
- try {
412
- // Fetch before any destructive operation, so a failed network
413
- // request can't leave the local store empty.
414
- const startTime = typeof performance !== 'undefined' ? performance.now() : Date.now();
415
- this.runtime.logger.info('Fetching bootstrap data from server (before clearing local data)', {
416
- type: requirements.type,
417
- lastSyncId: requirements.lastSyncId,
418
- });
419
- const bootstrapData = await this.bootstrapHelper.fetchBootstrap(requirements.lastSyncId);
420
- this.runtime.logger.debug('Received bootstrap response', {
421
- type: bootstrapData.type,
422
- lastSyncId: bootstrapData.lastSyncId,
423
- hasModels: !!bootstrapData.models,
424
- hasDeltas: !!bootstrapData.deltas,
425
- deltaCount: bootstrapData.deltaCount ?? 0,
426
- });
427
- // Clear only after a successful fetch, for transactional safety.
428
- // Clear when the server says the response is a full snapshot,
429
- // regardless of what type was requested.
430
- if (bootstrapData.type === 'full') {
431
- await this.clear();
432
- }
433
- // Handle partial bootstrap (delta batch)
434
- if (bootstrapData.type === 'partial') {
435
- const deltas = bootstrapData.deltas ?? [];
436
- this.runtime.logger.info('Processing partial bootstrap with delta batch', {
437
- deltaCount: deltas.length,
438
- fromSyncId: requirements.lastSyncId,
439
- toSyncId: bootstrapData.lastSyncId,
440
- });
441
- // Apply deltas to IndexedDB using processDeltaBatch for better performance.
442
- // Capture the return value so the pool can be updated by the caller —
443
- // without this, partial-bootstrap DELETEs persist to IDB but don't
444
- // evict entities from the in-memory InstanceCache, leaving ghost rows
445
- // visible on the canvas until a full reload rebuilds the pool.
446
- let deltasApplied = 0;
447
- let deltaResults;
448
- if (deltas.length > 0) {
449
- // Narrow the wire delta to what processDelta reads. The field names
450
- // are the wire's own — the only change is `id` becoming `syncId`.
451
- // A group-change frame carries its payload as a JSON string, decoded
452
- // here exactly as the live delta path does in BaseSyncedStore.
453
- const formattedDeltas = deltas.map((delta) => ({
454
- syncId: delta.id,
455
- actionType: delta.actionType,
456
- modelName: delta.modelName,
457
- modelId: delta.modelId,
458
- data: typeof delta.data === 'string'
459
- ? JSON.parse(delta.data)
460
- : delta.data,
461
- }));
462
- // Use batch processing for better performance
463
- const batch = await this.processDeltaBatch(formattedDeltas);
464
- deltaResults = batch.results;
465
- deltasApplied = formattedDeltas.length;
466
- onProgress?.(deltasApplied);
467
- }
468
- // Update workspace metadata with new lastSyncId (critical even when 0 deltas)
469
- await this.updateWorkspaceMetadata({
470
- lastSyncId: bootstrapData.lastSyncId,
471
- schemaHash: this.modelRegistry.getSchemaHash(),
472
- syncGroups: [...syncGroups],
473
- updatedAt: new Date(),
474
- });
475
- const elapsed = (typeof performance !== 'undefined' ? performance.now() : Date.now()) - startTime;
476
- this.runtime.logger.info(`Partial bootstrap complete in ${elapsed.toFixed(2)}ms`, {
477
- deltasApplied,
478
- lastSyncId: bootstrapData.lastSyncId,
479
- });
480
- return { modelsLoaded: 0, modelsStored: deltasApplied, bootstrapData, deltaResults };
481
- }
482
- // Full bootstrap: Process model data
483
- if (!bootstrapData.models) {
484
- throw new AbloValidationError('Full bootstrap response missing models data', {
485
- code: 'bootstrap_response_invalid',
486
- });
487
- }
488
- let modelsLoaded = 0;
489
- let modelsStored = 0;
490
- for (const [modelName, modelData] of Object.entries(bootstrapData.models)) {
491
- // Handle null, undefined, or non-array data
492
- if (!modelData) {
493
- this.runtime.observability.breadcrumb(`No data received for ${modelName}`, 'sync.bootstrap', 'warning');
494
- continue;
495
- }
496
- if (!Array.isArray(modelData)) {
497
- this.runtime.observability.breadcrumb(`Skipping non-array data for ${modelName}`, 'sync.bootstrap', 'warning');
498
- continue;
499
- }
500
- // Skip empty arrays silently (expected for some models)
501
- if (modelData.length === 0) {
502
- this.runtime.logger.debug(`No ${modelName} items to store (empty array)`);
503
- continue;
504
- }
505
- const store = this.getStore(modelName, 'bootstrap');
506
- if (!store) {
507
- this.runtime.logger.debug(`[Bootstrap] NO IDB STORE for ${modelName} — ${modelData.length} items DROPPED`);
508
- continue;
509
- }
510
- let writeErrors = 0;
511
- // Store all items to IndexedDB (compacted)
512
- for (const item of modelData) {
513
- try {
514
- const compacted = this.compactRecord(modelName, item);
515
- await store.put(compacted);
516
- modelsStored++;
517
- modelsLoaded++;
518
- // Report progress every 10 items
519
- if (modelsLoaded % 10 === 0) {
520
- onProgress?.(modelsLoaded);
521
- }
522
- }
523
- catch (error) {
524
- writeErrors++;
525
- this.runtime.observability.breadcrumb(`Failed to store ${modelName} item`, 'sync.database', 'error', {
526
- error: error instanceof Error ? error.message : String(error),
527
- });
528
- }
529
- }
530
- // The model is marked persisted below whether or not every item landed,
531
- // because a partial store is still what the next sync reconciles
532
- // against. Counted and surfaced here so a partial does not read as a
533
- // clean bootstrap.
534
- if (writeErrors > 0) {
535
- this.runtime.observability.breadcrumb(`Stored ${modelName} with ${writeErrors} of ${modelData.length} items dropped`, 'sync.database', 'warning');
536
- }
537
- // Mark model as persisted after successful write
538
- try {
539
- await this.setModelPersisted(modelName, true);
540
- }
541
- catch { }
542
- }
543
- // Update workspace metadata with bootstrap snapshot's lastSyncId
544
- // Note: This method is only called for 'full' bootstrap (not 'local')
545
- // For 'partial' bootstrap (future): would need intelligent merge logic here
546
- await this.updateWorkspaceMetadata({
547
- lastSyncId: bootstrapData.lastSyncId,
548
- schemaHash: this.modelRegistry.getSchemaHash(),
549
- syncGroups: [...syncGroups],
550
- updatedAt: new Date(),
551
- });
552
- const elapsed = (typeof performance !== 'undefined' ? performance.now() : Date.now()) - startTime;
553
- this.runtime.logger.info(`Bootstrap complete: ${modelsLoaded} items loaded, ${modelsStored} stored to IndexedDB in ${elapsed.toFixed(2)}ms`);
554
- this.runtime.analytics?.capture('bootstrap_success', {
555
- responseTime: elapsed,
556
- modelsLoaded,
557
- });
558
- return { modelsLoaded, modelsStored, bootstrapData };
559
- }
560
- catch (error) {
561
- // Comprehensive error logging for bootstrap failures
562
- this.runtime.observability.captureBootstrapFailure(error, {
563
- type: requirements.type,
564
- navigatorOnline: typeof navigator !== 'undefined' ? navigator.onLine : undefined,
565
- });
566
- // Track bootstrap failure telemetry
567
- this.runtime.analytics?.capture('bootstrap_failed', {
568
- bootstrapType: requirements.type,
569
- lastSyncId: requirements.lastSyncId,
570
- errorMessage: error instanceof Error ? error.message : String(error),
571
- errorName: error instanceof Error ? error.name : 'UnknownError',
572
- });
573
- throw error;
574
- }
575
- }
576
- // bootstrapSpecificModels removed per request
577
- /**
578
- * Apply a single inbound delta from the WebSocket to the local store.
579
- *
580
- * This handles one delta at a time. To apply several, prefer
581
- * {@link processDeltaBatch}, which commits them in one IndexedDB transaction
582
- * rather than two transactions per delta.
583
- *
584
- * Update deltas carry only the changed fields, so they are merged onto the
585
- * existing record rather than replacing it. That preserves fields the delta
586
- * omits (such as reportId or title), and an explicit null is kept as a value,
587
- * clearing that field.
588
- */
589
- async processDelta(delta) {
590
- const { actionType, modelName, modelId, data, syncId } = delta;
591
- const store = this.getStore(modelName, 'processDelta');
592
- if (!store) {
593
- return { action: 'verify', modelName, modelId };
594
- }
595
- // Idempotency gate: ignore already-applied deltas by comparing with the persisted lastSyncId
596
- try {
597
- const lastApplied = await this.getLastSyncId();
598
- const incomingId = typeof syncId === 'number' ? syncId : undefined;
599
- if (typeof incomingId === 'number' && incomingId <= lastApplied) {
600
- return { action: 'verify', modelName, modelId };
601
- }
602
- }
603
- catch { }
604
- // Compact data before persistence; do not store redundant type markers.
605
- // Inject `id` from the envelope — server deltas frequently strip it
606
- // from the `data` payload, but IDB object stores use keyPath='id'
607
- // and require it on the record itself. See `processDeltaBatch` for
608
- // the same rationale on the batch path.
609
- const dataWithId = data && typeof data === 'object'
610
- ? { id: modelId, ...data }
611
- : data;
612
- const compacted = dataWithId && typeof dataWithId === 'object'
613
- ? this.compactRecord(modelName, dataWithId)
614
- : dataWithId;
615
- switch (actionType) {
616
- // 'C' (Covering) — client gained permission to see an existing entity.
617
- // End state in the local store is identical to an insert: the row is
618
- // present. The semantic difference is purely observability — it wasn't
619
- // newly created, it was newly visible. We fall through to the 'I' case
620
- // after a debug trace so the two can be disambiguated in logs.
621
- case 'C':
622
- this.runtime.observability.breadcrumb('Applying covering delta (gained permission)', 'sync.database', 'info', { modelName, modelId: modelId.slice(0, 12) });
623
- // falls through
624
- case 'I': {
625
- // Skip when the delta payload was empty/null. IDB rejects
626
- // non-record `put` arguments at runtime; the previous `any`
627
- // typing on `ObjectStore.put` was silently letting that
628
- // through. Real I-deltas always carry a row body.
629
- if (!compacted || typeof compacted !== 'object') {
630
- return { action: 'add', modelName, modelId, data: null };
631
- }
632
- // Insert synchronously for durable ack-after-apply semantics
633
- try {
634
- await store.put(compacted);
635
- if (typeof syncId === 'number') {
636
- await this.updateWorkspaceMetadata({ lastSyncId: syncId });
637
- }
638
- }
639
- catch (err) {
640
- this.runtime.observability.breadcrumb(`IndexedDB put failed for ${modelName}:${modelId}`, 'sync.database', 'error', {
641
- error: err instanceof Error ? err.message : String(err),
642
- });
643
- throw err; // Re-throw to see the actual error
644
- }
645
- return { action: 'add', modelName, modelId, data: compacted };
646
- }
647
- case 'U': {
648
- // Update: merge onto the existing record (partial-delta pattern).
649
- // Read the existing record first.
650
- const existing = await store.get(modelId);
651
- // Skip the update when there's no existing record to merge with:
652
- // building a record from partial update data would corrupt it
653
- // (missing reportId, and so on).
654
- if (!existing) {
655
- this.runtime.observability.breadcrumb('Skipping UPDATE delta - no existing record to merge with', 'sync.database', 'warning', {
656
- modelName,
657
- modelId: modelId.slice(0, 12),
658
- });
659
- // Return verify action to signal no changes were made
660
- return { action: 'verify', modelName, modelId, data: null };
661
- }
662
- // Shallow merge: delta overrides existing fields (safe - existing is guaranteed)
663
- const merged = { ...existing, ...compacted };
664
- // Log preserved fields for debugging partial updates
665
- if (existing && compacted) {
666
- this.logPreservedFields(modelName, modelId, existing, compacted);
667
- }
668
- // Persist merged record
669
- try {
670
- await store.put(merged);
671
- if (typeof syncId === 'number') {
672
- await this.updateWorkspaceMetadata({ lastSyncId: syncId });
673
- }
674
- }
675
- catch (err) {
676
- this.runtime.observability.breadcrumb(`IndexedDB put failed for ${modelName}:${modelId}`, 'sync.database', 'error', {
677
- error: err instanceof Error ? err.message : String(err),
678
- });
679
- throw err;
680
- }
681
- // Return merged data (not just delta) to preserve essential fields like organizationId
682
- return { action: 'update', modelName, modelId, data: merged };
683
- }
684
- case 'D': {
685
- // Delete synchronously
686
- try {
687
- await store.delete(modelId);
688
- if (typeof syncId === 'number') {
689
- await this.updateWorkspaceMetadata({ lastSyncId: syncId });
690
- }
691
- }
692
- catch (err) {
693
- this.runtime.observability.breadcrumb(`IndexedDB delete failed for ${modelName}:${modelId}`, 'sync.database', 'error', {
694
- error: err instanceof Error ? err.message : String(err),
695
- });
696
- // Surface failure so caller does not mutate InstanceCache inconsistently
697
- throw err;
698
- }
699
- return { action: 'remove', modelName, modelId };
700
- }
701
- case 'A': {
702
- // Archive
703
- const archivedData = this.compactRecord(modelName, { ...data, archivedAt: new Date() });
704
- try {
705
- await store.put(archivedData);
706
- if (typeof syncId === 'number') {
707
- await this.updateWorkspaceMetadata({ lastSyncId: syncId });
708
- }
709
- }
710
- catch (err) {
711
- this.runtime.observability.breadcrumb(`IndexedDB archive put failed for ${modelName}:${modelId}`, 'sync.database', 'error', {
712
- error: err instanceof Error ? err.message : String(err),
713
- });
714
- throw err;
715
- }
716
- return { action: 'archive', modelName, modelId, data: archivedData };
717
- }
718
- case 'V': // Verify
719
- return { action: 'verify', modelName, modelId, data };
720
- // 'G' (GroupAdded) and 'S' (GroupRemoved) are sync-group membership
721
- // signals, not entity mutations. They are routed upstream in
722
- // BaseSyncedStore.processDeltaWithBatching and should never reach
723
- // processDelta. If one slips through (e.g. replayed from the bootstrap
724
- // queue), we return a no-op verify rather than crashing the engine.
725
- case 'G':
726
- case 'S':
727
- this.runtime.observability.breadcrumb(`Group membership delta (${actionType}) reached processDelta — should be handled upstream`, 'sync.database', 'warning', { modelName, modelId: modelId.slice(0, 12), actionType });
728
- return { action: 'verify', modelName, modelId, data: null };
729
- default: {
730
- // The switch above is exhaustive over the declared action types, so
731
- // this branch is only reachable when a value escapes the type — hence
732
- // stringifying whatever actually arrived rather than the `never`.
733
- const _exhaustive = actionType;
734
- void _exhaustive;
735
- throw new AbloValidationError(`Unknown action type: ${JSON.stringify(actionType)}`, { code: 'db_unknown_action_type' });
736
- }
737
- }
738
- }
739
- /**
740
- * Apply many deltas to the local store in as few IndexedDB transactions as
741
- * possible. Deltas are grouped by store, and each store's writes commit in a
742
- * single transaction, so a batch of 186 deltas becomes roughly one
743
- * transaction per store instead of two per delta.
744
- *
745
- * The method reads the existing records for update deltas up front, then
746
- * merges each update onto its existing record so fields the delta omits are
747
- * preserved and an explicit null still clears its field. It advances the
748
- * persisted sync cursor once, to the highest committed sync id.
749
- *
750
- * Conflict resolution follows a delete-wins rule: it first indexes the
751
- * delete deltas by entity, then skips any insert or update whose sync id is
752
- * at or below a delete for the same entity. This avoids resurrecting a
753
- * deleted entity and avoids fetching one that no longer exists.
754
- */
755
- async processDeltaBatch(deltas) {
756
- if ((!this.workspaceDb && !this.inMemory) || this.isClosing || deltas.length === 0) {
757
- return { results: [], persistedSyncId: 0 };
758
- }
759
- // ── inMemory short-circuit ───────────────────────────────────────
760
- //
761
- // The batched IDB transaction path below assumes `this.storeManager`
762
- // and `workspaceDb`. In inMemory mode (agent-worker, tests) those
763
- // don't exist. Without this branch, every live delta arriving over
764
- // the WebSocket is silently dropped — the local pool never updates,
765
- // `subscribe()` autoruns never re-fire, lazy-model dispatchers
766
- // never claim incoming work.
767
- //
768
- // Fall through to the single-delta path (`processDelta`), which
769
- // uses `getStore` and is inMemory-compatible. Same return
770
- // shape, sequential apply per delta — fine since inMemory mode
771
- // doesn't need IDB transaction batching for performance.
772
- if (this.inMemory) {
773
- const inMemResults = [];
774
- let inMemPersistedSyncId = 0;
775
- for (const delta of deltas) {
776
- const single = await this.processDelta({
777
- syncId: delta.syncId,
778
- actionType: delta.actionType,
779
- modelName: delta.modelName,
780
- modelId: delta.modelId,
781
- data: delta.data,
782
- });
783
- inMemResults.push({ ...single, transactionId: delta.transactionId });
784
- // inMemory has no IDB tx that can fail — every non-'verify'
785
- // single result is durable in the in-memory store. Advance the
786
- // persisted-cursor watermark to the input delta's syncId so the
787
- // ack path can move forward.
788
- if (single.action !== 'verify' && typeof delta.syncId === 'number' && delta.syncId > inMemPersistedSyncId) {
789
- inMemPersistedSyncId = delta.syncId;
790
- }
791
- }
792
- return { results: inMemResults, persistedSyncId: inMemPersistedSyncId };
793
- }
794
- // Prepare results aligned with input order
795
- const results = new Array(deltas.length);
796
- // Build a delete index for conflict resolution. When a delete has a sync
797
- // id at or above a later insert or update for the same entity, that entity
798
- // is not (re)created — which drops stale updates for cascade-deleted
799
- // entities.
800
- const deleteSyncIds = new Map(); // key: "ModelName:modelId" -> delete syncId
801
- for (const delta of deltas) {
802
- if (delta.actionType === 'D' && delta.syncId) {
803
- const key = `${delta.modelName}:${delta.modelId}`;
804
- const existing = deleteSyncIds.get(key);
805
- // Normalize to number — postgres sends bigint as string on the wire.
806
- const n = typeof delta.syncId === 'string' ? Number(delta.syncId) : delta.syncId;
807
- if (typeof n === 'number' && !isNaN(n) && (!existing || n > existing)) {
808
- deleteSyncIds.set(key, n);
809
- }
810
- }
811
- }
812
- if (deleteSyncIds.size > 0) {
813
- this.runtime.logger.debug('[Database.processDeltaBatch] Built DELETE index for conflict resolution', {
814
- deleteCount: deleteSyncIds.size,
815
- totalDeltas: deltas.length,
816
- });
817
- }
818
- // Group deltas by store for efficient transaction management.
819
- //
820
- // The method tracks the total range seen plus the exact input indexes whose
821
- // store transaction committed. The cursor is derived from their ordered
822
- // prefix after every store finishes; a maximum alone is unsafe because a
823
- // later store can succeed after an earlier store failed.
824
- //
825
- // Without this split, a single store-level failure (a compacted record
826
- // missing a required field, a validation abort) would advance the cursor
827
- // past deltas that never wrote to IndexedDB. The next partial bootstrap
828
- // would ask "what's new since {advanced cursor}?", the skipped rows would
829
- // fall into the already-seen range forever, and the local store would stay
830
- // permanently behind the server with no way to recover on reload.
831
- const deltasByStore = new Map();
832
- let highestSyncId = 0;
833
- const persistedIndexes = new Set();
834
- let skippedDueToConflict = 0;
835
- deltas.forEach((delta, idx) => {
836
- // Normalize to number — postgres sends bigint syncIds as strings.
837
- const deltaSyncIdNum = typeof delta.syncId === 'string'
838
- ? Number(delta.syncId)
839
- : delta.syncId;
840
- if (typeof deltaSyncIdNum === 'number' && !isNaN(deltaSyncIdNum) && deltaSyncIdNum > highestSyncId) {
841
- highestSyncId = deltaSyncIdNum;
842
- }
843
- // Conflict check: skip an insert or update when a delete for the same
844
- // entity has an equal or higher sync id.
845
- if (delta.actionType === 'U' ||
846
- delta.actionType === 'I' ||
847
- delta.actionType === 'C') {
848
- const key = `${delta.modelName}:${delta.modelId}`;
849
- const deleteSyncId = deleteSyncIds.get(key);
850
- if (deleteSyncId !== undefined) {
851
- // DELETE exists for this entity
852
- const deltaSyncId = delta.syncId ?? 0;
853
- if (deleteSyncId >= deltaSyncId) {
854
- // DELETE has equal or higher syncId - skip this UPDATE/INSERT
855
- this.runtime.logger.debug('[Database.processDeltaBatch] Skipping stale delta (DELETE wins)', {
856
- modelName: delta.modelName,
857
- modelId: delta.modelId.slice(0, 12),
858
- actionType: delta.actionType,
859
- deltaSyncId,
860
- deleteSyncId,
861
- });
862
- results[idx] = { action: 'verify', modelName: delta.modelName, modelId: delta.modelId };
863
- // The later delete in this same ordered frame supersedes this
864
- // value, so the stale predecessor requires no separate write.
865
- persistedIndexes.add(idx);
866
- skippedDueToConflict++;
867
- return; // Skip this delta
868
- }
869
- }
870
- }
871
- const store = this.getStore(delta.modelName, 'processDeltaBatch');
872
- if (!store) {
873
- results[idx] = { action: 'verify', modelName: delta.modelName, modelId: delta.modelId };
874
- return;
875
- }
876
- if (!deltasByStore.has(delta.modelName)) {
877
- deltasByStore.set(delta.modelName, []);
878
- }
879
- deltasByStore.get(delta.modelName).push({ idx, delta });
880
- });
881
- if (skippedDueToConflict > 0) {
882
- this.runtime.logger.info('[Database.processDeltaBatch] Conflict resolution summary', {
883
- skippedDueToConflict,
884
- totalDeltas: deltas.length,
885
- deleteCount: deleteSyncIds.size,
886
- });
887
- }
888
- // Process each store's deltas in a single transaction
889
- for (const [modelName, storeDeltas] of deltasByStore.entries()) {
890
- const store = this.storeManager.getStore(modelName);
891
- if (!store)
892
- continue;
893
- try {
894
- // Batch read-modify-write.
895
- // Step 1: Identify which deltas need existing data (updates)
896
- const updateDeltas = storeDeltas.filter(({ delta }) => delta.actionType === 'U');
897
- const updateIds = updateDeltas.map(({ delta }) => delta.modelId);
898
- // Step 2: Batch read all existing records in a SINGLE IDB transaction
899
- // This replaces N sequential get() calls with 1 transaction containing N gets
900
- let existingRecords = new Map();
901
- const missingIds = new Set();
902
- if (updateIds.length > 0) {
903
- try {
904
- existingRecords = await store.getMany(updateIds);
905
- // Identify missing IDs for self-healing
906
- for (const id of updateIds) {
907
- if (!existingRecords.has(id)) {
908
- missingIds.add(id);
909
- }
910
- }
911
- }
912
- catch {
913
- this.runtime.observability.breadcrumb(`Batch read failed for ${modelName}, falling back to individual reads`, 'sync.database', 'warning');
914
- // Fallback: mark all as missing for self-healing
915
- for (const id of updateIds) {
916
- missingIds.add(id);
917
- }
918
- }
919
- }
920
- // Self-heal by fetching missing records for update deltas.
921
- // Track ids that failed to fetch (a 404 means the entity was deleted,
922
- // so its delta is skipped).
923
- const failedToFetch = new Set();
924
- if (missingIds.size > 0) {
925
- this.runtime.logger.info(`[Database.processDeltaBatch] Found ${missingIds.size} missing records for ${modelName}, fetching from server...`);
926
- // Fetch sequentially to avoid overwhelming server
927
- for (const id of missingIds) {
928
- try {
929
- const fetchedRecord = await this.bootstrapHelper.fetchEntity(modelName, id);
930
- if (fetchedRecord) {
931
- const compacted = this.compactRecord(modelName, fetchedRecord);
932
- existingRecords.set(id, compacted);
933
- this.runtime.logger.debug(`[Database.processDeltaBatch] Successfully fetched missing record: ${modelName}:${id}`);
934
- }
935
- else {
936
- // fetchEntity returns null for 404 — entity was deleted, skip the delta
937
- failedToFetch.add(id);
938
- this.runtime.logger.debug(`[Database.processDeltaBatch] Entity not found (deleted): ${modelName}:${id}`);
939
- }
940
- }
941
- catch (error) {
942
- // Unexpected error (5xx, network failure) — mark for skipping and report
943
- failedToFetch.add(id);
944
- this.runtime.observability.breadcrumb(`Failed to fetch missing record ${modelName}:${id}`, 'sync.database', 'warning', {
945
- error: error instanceof Error ? error.message : String(error),
946
- });
947
- }
948
- }
949
- if (failedToFetch.size > 0) {
950
- this.runtime.logger.info(`[Database.processDeltaBatch] Skipping ${failedToFetch.size} stale UPDATE deltas for deleted entities`, {
951
- modelName,
952
- failedCount: failedToFetch.size,
953
- totalMissing: missingIds.size,
954
- });
955
- }
956
- }
957
- // Re-check after entity fetch loop: close() may have run during network I/O
958
- if (!this.workspaceDb || this.isClosing) {
959
- for (const { idx, delta } of storeDeltas) {
960
- results[idx] = { action: 'verify', modelName, modelId: delta.modelId };
961
- }
962
- continue;
963
- }
964
- // Step 3: Start a single readwrite transaction for this store
965
- const tx = this.workspaceDb.transaction([modelName], 'readwrite');
966
- const objectStore = tx.objectStore(modelName);
967
- // Stage results for this store; only commit to global results when tx completes successfully
968
- const stagedResults = [];
969
- // Step 4: Process all deltas synchronously within transaction (no await!)
970
- for (const { idx, delta } of storeDeltas) {
971
- const { actionType, modelId, data } = delta;
972
- // Server deltas carry `id` in the envelope (modelId) but often
973
- // strip it from the `data` payload as redundant. IDB object
974
- // stores use keyPath='id' on the record itself, so the record
975
- // MUST have `id` set. Inject it before `compactRecord` so the
976
- // record is self-describing.
977
- const dataWithId = data && typeof data === 'object'
978
- ? { id: modelId, ...data }
979
- : data;
980
- const compacted = dataWithId && typeof dataWithId === 'object'
981
- ? this.compactRecord(modelName, dataWithId)
982
- : dataWithId;
983
- switch (actionType) {
984
- case 'C': // Create
985
- case 'I': // Insert
986
- objectStore.put(compacted);
987
- stagedResults.push({
988
- action: 'add',
989
- modelName,
990
- modelId,
991
- data: compacted,
992
- idx,
993
- });
994
- break;
995
- case 'U': {
996
- // Update: merge the delta onto the existing record (already fetched).
997
- const existing = existingRecords.get(modelId);
998
- // Skip a stale update: if the entity is neither in the local
999
- // store nor fetchable from the server (a 404), it was deleted,
1000
- // so skip it rather than create an incomplete record.
1001
- if (!existing && failedToFetch.has(modelId)) {
1002
- this.runtime.logger.debug('[Database.processDeltaBatch] Skipping UPDATE for deleted entity', {
1003
- modelName,
1004
- modelId: modelId.slice(0, 12),
1005
- });
1006
- stagedResults.push({ action: 'verify', modelName, modelId, idx });
1007
- break; // Skip this delta
1008
- }
1009
- // Skip the update when there's no existing record to merge with:
1010
- // building a record from partial update data would corrupt it
1011
- // (missing reportId, and so on).
1012
- if (!existing) {
1013
- this.runtime.observability.breadcrumb('Batch: Skipping UPDATE delta - no existing record', 'sync.database', 'warning', {
1014
- modelName,
1015
- modelId: modelId.slice(0, 12),
1016
- });
1017
- stagedResults.push({ action: 'verify', modelName, modelId, idx });
1018
- break; // Skip this delta
1019
- }
1020
- // Safe to merge - existing record is guaranteed
1021
- const merged = { ...existing, ...compacted };
1022
- // Log preserved fields for debugging partial updates
1023
- if (existing && compacted) {
1024
- this.logPreservedFields(modelName, modelId, existing, compacted);
1025
- }
1026
- objectStore.put(merged);
1027
- stagedResults.push({
1028
- action: 'update',
1029
- modelName,
1030
- modelId,
1031
- data: merged, // Return merged data, not just delta
1032
- idx,
1033
- });
1034
- break;
1035
- }
1036
- case 'D': // Delete
1037
- objectStore.delete(modelId);
1038
- stagedResults.push({ action: 'remove', modelName, modelId, idx });
1039
- break;
1040
- case 'A': // Archive
1041
- const archivedData = this.compactRecord(modelName, {
1042
- ...data,
1043
- archivedAt: new Date(),
1044
- });
1045
- objectStore.put(archivedData);
1046
- stagedResults.push({
1047
- action: 'archive',
1048
- modelName,
1049
- modelId,
1050
- data: archivedData,
1051
- idx,
1052
- });
1053
- break;
1054
- case 'V': // Verify
1055
- stagedResults.push({ action: 'verify', modelName, modelId, data, idx });
1056
- break;
1057
- }
1058
- }
1059
- // Wait for transaction to complete
1060
- await new Promise((resolve, reject) => {
1061
- tx.oncomplete = () => { resolve(); };
1062
- tx.onerror = () => { reject(tx.error); };
1063
- });
1064
- // Only commit staged results to the global results if the transaction
1065
- // succeeded. Record input indexes rather than a maximum sync id; the
1066
- // durable cursor is the prefix through these indexes.
1067
- for (const r of stagedResults) {
1068
- // Resolve the originating delta so we can carry its
1069
- // transactionId through to the result. Echo detection in
1070
- // `SyncClient.applyDeltaBatchToPool` reads it.
1071
- const sourceDelta = deltas[r.idx];
1072
- results[r.idx] = {
1073
- action: r.action,
1074
- modelName: r.modelName,
1075
- modelId: r.modelId,
1076
- data: r.data,
1077
- transactionId: sourceDelta?.transactionId,
1078
- };
1079
- persistedIndexes.add(r.idx);
1080
- }
1081
- }
1082
- catch (err) {
1083
- // Surface the IDB error directly — `captureMutationFailure`
1084
- // routes to Sentry, but during interactive debugging the console
1085
- // needs to show the specific failure (e.g. `ConstraintError`,
1086
- // `DataError`, `AbortError`) so we can find what's wrong with
1087
- // the `compacted` payload shape or store schema.
1088
- const idbErr = err instanceof Error ? err : new Error(String(err));
1089
- this.runtime.logger.debug('[Database.processDeltaBatch] store tx FAILED', {
1090
- modelName,
1091
- storeDeltasCount: storeDeltas.length,
1092
- errorName: idbErr.name,
1093
- message: idbErr.message,
1094
- sampleDeltas: storeDeltas.slice(0, 3).map(({ delta }) => ({
1095
- action: delta.actionType,
1096
- id: delta.modelId.slice(0, 12),
1097
- dataKeys: delta.data && typeof delta.data === 'object'
1098
- ? Object.keys(delta.data).slice(0, 8)
1099
- : typeof delta.data,
1100
- })),
1101
- });
1102
- this.runtime.observability.captureMutationFailure({
1103
- context: 'batch-indexeddb-operation',
1104
- modelName,
1105
- error: idbErr,
1106
- });
1107
- // Mark all store deltas as verify in their original positions
1108
- for (const { idx, delta } of storeDeltas) {
1109
- results[idx] = { action: 'verify', modelName, modelId: delta.modelId };
1110
- }
1111
- }
1112
- }
1113
- // Advance only through the durable INPUT PREFIX. IDs need not be
1114
- // numerically contiguous because other tenants and filtered sync groups
1115
- // occupy gaps; the server-delivered order is the relevant sequence.
1116
- const highestPersistedSyncId = highestPersistedPrefixSyncId(deltas, persistedIndexes);
1117
- // Using `highestSyncId` (the range-seen max) would advance past an earlier
1118
- // failed store transaction and permanently skip its delta.
1119
- //
1120
- // If `highestPersistedSyncId === 0` (every store tx failed), we leave
1121
- // the metadata alone. Next partial bootstrap will re-deliver the
1122
- // deltas at the original cursor position.
1123
- if (highestPersistedSyncId > 0) {
1124
- try {
1125
- await this.updateWorkspaceMetadata({ lastSyncId: highestPersistedSyncId });
1126
- }
1127
- catch (err) {
1128
- this.runtime.observability.breadcrumb('Failed to update metadata after batch', 'sync.database', 'error', {
1129
- error: err instanceof Error ? err.message : String(err),
1130
- });
1131
- }
1132
- }
1133
- if (highestPersistedSyncId < highestSyncId) {
1134
- // Staging-visibility probe: makes the "some deltas seen but not
1135
- // persisted" signal loud when it actually happens. If this fires
1136
- // repeatedly on the same sync IDs, a specific row is un-writable
1137
- // (validation? compact issue?) and needs fixing at that layer.
1138
- this.runtime.logger.debug('[Database.processDeltaBatch] cursor withheld due to failed store tx', {
1139
- seen: highestSyncId,
1140
- persisted: highestPersistedSyncId,
1141
- gap: highestSyncId - highestPersistedSyncId,
1142
- });
1143
- }
1144
- return { results, persistedSyncId: highestPersistedSyncId };
1145
- }
1146
- /** Get raw data for hydration */
1147
- async hydrateModels(modelName) {
1148
- const store = this.getStore(modelName, 'hydrate');
1149
- if (!store) {
1150
- return [];
1151
- }
1152
- return store.getAll();
1153
- }
1154
- /** Put a single record to IndexedDB (for self-healing corrupted records) */
1155
- async putRecord(modelName, id, data) {
1156
- const store = this.getStore(modelName, 'putRecord');
1157
- if (!store) {
1158
- this.runtime.observability.breadcrumb(`Store not found for putRecord: ${modelName}`, 'sync.database', 'warning');
1159
- return;
1160
- }
1161
- const compacted = this.compactRecord(modelName, data);
1162
- await store.put(compacted);
1163
- }
1164
- /** Get data by index. `value` is an IDB key — string, number, Date,
1165
- * BufferSource, or array thereof. */
1166
- async getDataByIndex(modelName, indexName, value) {
1167
- const store = this.getRequiredStore(modelName);
1168
- return await store.getAllFromIndex(indexName, value);
1169
- }
1170
- /** Read workspace metadata from IndexedDB. Returns null when the database is not open. */
1171
- async getWorkspaceMetadata() {
1172
- if (this.inMemory)
1173
- return this.inMemoryMetadata;
1174
- if (!this.workspaceDb)
1175
- return null;
1176
- return this.databaseManager.getWorkspaceMetadata(this.workspaceDb);
1177
- }
1178
- async getLastSyncId() {
1179
- if (!this.workspaceDb) {
1180
- return 0;
1181
- }
1182
- const metadata = await this.databaseManager.getWorkspaceMetadata(this.workspaceDb);
1183
- return metadata?.lastSyncId ?? 0;
1184
- }
1185
- async updateWorkspaceMetadata(metadata) {
1186
- // In-memory mode: store in local variable
1187
- if (this.inMemory) {
1188
- this.inMemoryMetadata = {
1189
- ...(this.inMemoryMetadata ?? {
1190
- lastSyncId: 0, firstSyncId: 0, backendDatabaseVersion: 0,
1191
- subscribedSyncGroups: [], updatedAt: new Date(),
1192
- }),
1193
- ...metadata,
1194
- updatedAt: new Date(),
1195
- };
1196
- return;
1197
- }
1198
- // Graceful degradation: skip if database is closing or not open
1199
- // This prevents "Database not opened" errors during React Strict Mode cleanup
1200
- if (!this.workspaceDb || this.isClosing) {
1201
- this.runtime.observability.breadcrumb('updateWorkspaceMetadata: Database not open or closing', 'sync.database', 'warning', {
1202
- hasDb: !!this.workspaceDb,
1203
- isClosing: this.isClosing,
1204
- });
1205
- return;
1206
- }
1207
- const current = await this.databaseManager.getWorkspaceMetadata(this.workspaceDb);
1208
- // Re-check after await: close() may have been called during getWorkspaceMetadata,
1209
- // or the browser may have closed the IDB connection (tab background, navigation).
1210
- // Without this, setWorkspaceMetadata would hit "The database connection is closing".
1211
- if (!this.workspaceDb || this.isClosing) {
1212
- return;
1213
- }
1214
- const updated = {
1215
- ...current,
1216
- ...metadata,
1217
- updatedAt: new Date(),
1218
- };
1219
- await this.databaseManager.setWorkspaceMetadata(this.workspaceDb, updated);
1220
- }
1221
- /** Transaction persistence for offline/retry support.
1222
- * Returns either the IDB-backed ObjectStore or its in-memory twin
1223
- * (`InMemoryObjectStore`) — both expose the same async put/get/
1224
- * delete/getAll/getAllFromIndex surface, so callers don't need to
1225
- * branch on which one they got back. */
1226
- get transactionStore() {
1227
- return this.getRequiredStore('__transactions');
1228
- }
1229
- async saveTransaction(transaction) {
1230
- await this.transactionStore.put(transaction);
1231
- }
1232
- /** Persist one burst of journal rows in a single strict durability group. */
1233
- async saveTransactions(transactions) {
1234
- if (transactions.length === 0)
1235
- return;
1236
- if (this.inMemory) {
1237
- await Promise.all(transactions.map((transaction) => this.transactionStore.put(transaction)));
1238
- return;
1239
- }
1240
- const db = this.workspaceDb;
1241
- if (!db || this.isClosing) {
1242
- throw new AbloConnectionError('Database not opened for mutation journal', {
1243
- code: 'db_not_opened',
1244
- });
1245
- }
1246
- await new Promise((resolve, reject) => {
1247
- try {
1248
- const tx = db.transaction(['__transactions'], 'readwrite', {
1249
- durability: 'strict',
1250
- });
1251
- const store = tx.objectStore('__transactions');
1252
- for (const transaction of transactions)
1253
- store.put(transaction);
1254
- tx.oncomplete = () => { resolve(); };
1255
- tx.onabort = () => {
1256
- reject(tx.error ?? new Error('Mutation journal transaction aborted'));
1257
- };
1258
- tx.onerror = () => {
1259
- // onabort owns rejection.
1260
- };
1261
- }
1262
- catch (error) {
1263
- reject(error instanceof Error ? error : new Error(String(error)));
1264
- }
1265
- });
1266
- }
1267
- async removeTransaction(id) {
1268
- await this.transactionStore.delete(id);
1269
- }
1270
- async getPersistedTransactions() {
1271
- const rows = await this.transactionStore.getAll();
1272
- // Storage layer returns the centralized `Record<string, unknown>`
1273
- // shape from `ObjectStoreContract`. PersistedTransaction adds an
1274
- // index signature so each row already structurally satisfies the
1275
- // narrower type — runtime invariant: only saveTransaction writes
1276
- // here, and it only accepts PersistedTransaction.
1277
- return rows;
1278
- }
1279
- async getPersistedTransaction(id) {
1280
- return (await this.transactionStore.get(id));
1281
- }
1282
- /**
1283
- * Atomically seal one exact commit request and consume the staged mutation
1284
- * records it replaces. The read, optional add, and deletes share one strict
1285
- * IndexedDB transaction, so a crash can expose the staged records or the
1286
- * sealed envelope, never a missing handoff. Returns the pre-existing record
1287
- * when the envelope id was already sealed (retry/re-entrant call).
1288
- */
1289
- async sealTransactionRecord(record, consumedRecordIds) {
1290
- const recordId = record.id;
1291
- if (!recordId) {
1292
- throw new AbloValidationError('A sealed transaction record must carry an id', {
1293
- code: 'invalid_body',
1294
- });
1295
- }
1296
- if (this.inMemory) {
1297
- const store = this.transactionStore;
1298
- const existing = (await store.get(recordId));
1299
- if (existing && !isSameOutboxRecord(existing, record)) {
1300
- throw new AbloValidationError('Pending-write key already identifies a different request', {
1301
- code: 'idempotency_conflict',
1302
- });
1303
- }
1304
- if (isAcceptedOutboxPromotion(existing, record)) {
1305
- await store.put(record);
1306
- }
1307
- if (!existing) {
1308
- const sources = await Promise.all(consumedRecordIds.map((id) => store.get(id)));
1309
- if (sources.some((source) => source === undefined)) {
1310
- throw new AbloValidationError('Pending-write source mutations were already claimed by another write', { code: 'idempotency_conflict' });
1311
- }
1312
- await store.add(record);
1313
- }
1314
- for (const id of consumedRecordIds) {
1315
- if (id !== recordId)
1316
- await store.delete(id);
1317
- }
1318
- return existing;
1319
- }
1320
- const db = this.workspaceDb;
1321
- if (!db || this.isClosing) {
1322
- throw new AbloConnectionError('Database not opened for durable writes', {
1323
- code: 'db_not_opened',
1324
- });
1325
- }
1326
- return new Promise((resolve, reject) => {
1327
- try {
1328
- const tx = db.transaction(['__transactions'], 'readwrite', {
1329
- durability: 'strict',
1330
- });
1331
- const store = tx.objectStore('__transactions');
1332
- const getRequest = store.get(recordId);
1333
- const sourceIds = [...new Set(consumedRecordIds)].filter((id) => id !== recordId);
1334
- const sourceRequests = sourceIds.map((id) => store.get(id));
1335
- const sourceExists = new Array(sourceRequests.length).fill(false);
1336
- let existing;
1337
- let collisionError;
1338
- let envelopeRead = false;
1339
- let sourcesRead = 0;
1340
- let promotionStarted = false;
1341
- const promote = () => {
1342
- if (promotionStarted ||
1343
- !envelopeRead ||
1344
- sourcesRead !== sourceRequests.length)
1345
- return;
1346
- promotionStarted = true;
1347
- if (existing && !isSameOutboxRecord(existing, record)) {
1348
- collisionError = new AbloValidationError('Pending-write key already identifies a different request', { code: 'idempotency_conflict' });
1349
- tx.abort();
1350
- return;
1351
- }
1352
- // A new envelope owns promotion only while every source row still
1353
- // exists. This is the fleet/tab execution claim: a second tab that
1354
- // restored the same journal entries under another key loses here and
1355
- // cannot dispatch. An identical existing envelope is an idempotent
1356
- // retry, so its already-consumed sources may be absent.
1357
- if (!existing && sourceExists.some((exists) => !exists)) {
1358
- collisionError = new AbloValidationError('Pending-write source mutations were already claimed by another write', { code: 'idempotency_conflict' });
1359
- tx.abort();
1360
- return;
1361
- }
1362
- if (!existing) {
1363
- store.add(record);
1364
- }
1365
- else if (isAcceptedOutboxPromotion(existing, record)) {
1366
- store.put(record);
1367
- }
1368
- for (const id of sourceIds)
1369
- store.delete(id);
1370
- };
1371
- getRequest.onsuccess = () => {
1372
- existing = getRequest.result;
1373
- envelopeRead = true;
1374
- promote();
1375
- };
1376
- getRequest.onerror = () => {
1377
- tx.abort();
1378
- };
1379
- sourceRequests.forEach((request, index) => {
1380
- request.onsuccess = () => {
1381
- sourceExists[index] = request.result !== undefined;
1382
- sourcesRead += 1;
1383
- promote();
1384
- };
1385
- request.onerror = () => {
1386
- tx.abort();
1387
- };
1388
- });
1389
- tx.oncomplete = () => { resolve(existing); };
1390
- tx.onabort = () => {
1391
- reject(collisionError ??
1392
- tx.error ??
1393
- getRequest.error ??
1394
- new Error('Durable-write transaction aborted'));
1395
- };
1396
- tx.onerror = () => {
1397
- // onabort owns rejection so the promise settles exactly once.
1398
- };
1399
- }
1400
- catch (error) {
1401
- reject(error instanceof Error ? error : new Error(String(error)));
1402
- }
1403
- });
1404
- }
1405
- async cleanupOldTransactions(maxAge) {
1406
- const store = this.transactionStore;
1407
- const rows = (await store.getAll());
1408
- const cutoff = Date.now() - maxAge;
1409
- let cleaned = 0;
1410
- for (const tx of rows) {
1411
- // Live write intent has no safe age-based expiry. In particular, server
1412
- // idempotency retention may already have elapsed, so silently deleting or
1413
- // blindly replaying an old envelope would both be unsafe. Restoration
1414
- // owns quarantine/reconciliation for these records.
1415
- if (tx.type === 'commit_envelope' ||
1416
- tx.type === 'http_commit_envelope' ||
1417
- tx.type === 'pending_mutation') {
1418
- continue;
1419
- }
1420
- if (typeof tx.timestamp === 'number' && tx.timestamp < cutoff) {
1421
- await store.delete(tx.id);
1422
- cleaned++;
1423
- }
1424
- }
1425
- return cleaned;
1426
- }
1427
- /**
1428
- * Store management
1429
- *
1430
- * `getStore(modelName, context?)` is defined near the top of this
1431
- * class — single accessor for both inMemory and IDB modes.
1432
- */
1433
- getAllStores() {
1434
- if (this.inMemory) {
1435
- return this.inMemoryStores;
1436
- }
1437
- return this.storeManager.getAllStores();
1438
- }
1439
- /**
1440
- * Model persistence tracking
1441
- */
1442
- async setModelPersisted(modelName, persisted) {
1443
- if (this.inMemory)
1444
- return; // No persistence tracking in memory mode
1445
- if (!this.workspaceDb) {
1446
- throw new AbloConnectionError('Database not opened', {
1447
- code: 'db_not_opened',
1448
- });
1449
- }
1450
- await this.databaseManager.setModelPersisted(this.workspaceDb, modelName, persisted);
1451
- }
1452
- async isModelPersisted(modelName) {
1453
- if (this.inMemory)
1454
- return false; // In-memory = nothing persisted
1455
- if (!this.workspaceDb) {
1456
- throw new AbloConnectionError('Database not opened', {
1457
- code: 'db_not_opened',
1458
- });
1459
- }
1460
- return await this.databaseManager.isModelPersisted(this.workspaceDb, modelName);
1461
- }
1462
- /**
1463
- * Statistics
1464
- */
1465
- async getStats() {
1466
- const storeStats = await this.storeManager.getComprehensiveStats();
1467
- return {
1468
- database: this.currentDbInfo,
1469
- stores: storeStats,
1470
- metadata: this.workspaceDb
1471
- ? await this.databaseManager.getWorkspaceMetadata(this.workspaceDb)
1472
- : null,
1473
- };
1474
- }
1475
- /**
1476
- * Lifecycle
1477
- */
1478
- isOpen() {
1479
- return this.workspaceDb !== null;
1480
- }
1481
- async close() {
1482
- // Mark database as closing FIRST to enable graceful degradation
1483
- // This allows in-flight operations to bail out gracefully
1484
- this.isClosing = true;
1485
- // Mark all stores as closing to prevent new operations
1486
- this.storeManager.markAllStoresAsClosing();
1487
- if (this.workspaceDb) {
1488
- this.workspaceDb.close();
1489
- this.workspaceDb = null;
1490
- }
1491
- await this.databaseManager.close();
1492
- this.currentDbInfo = null;
1493
- this.runtime.logger.debug('Database closed');
1494
- }
1495
- async clear(options = {}) {
1496
- await this.storeManager.clearAllStores();
1497
- if (options.includeWriteJournal) {
1498
- await this.transactionStore.clear();
1499
- }
1500
- this.runtime.logger.info('All stores cleared');
1501
- }
1502
- }