@abloatai/ablo 0.26.0 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (398) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +101 -85
  3. package/dist/BaseSyncedStore.d.ts +85 -88
  4. package/dist/BaseSyncedStore.js +131 -147
  5. package/dist/Database.d.ts +54 -68
  6. package/dist/Database.js +97 -113
  7. package/dist/{ObjectPool.d.ts → InstanceCache.d.ts} +10 -13
  8. package/dist/{ObjectPool.js → InstanceCache.js} +85 -83
  9. package/dist/LazyReferenceCollection.d.ts +11 -15
  10. package/dist/LazyReferenceCollection.js +12 -16
  11. package/dist/Model.d.ts +37 -52
  12. package/dist/Model.js +46 -61
  13. package/dist/ModelRegistry.d.ts +21 -19
  14. package/dist/ModelRegistry.js +23 -27
  15. package/dist/NetworkMonitor.d.ts +5 -6
  16. package/dist/NetworkMonitor.js +5 -6
  17. package/dist/SyncClient.d.ts +112 -112
  18. package/dist/SyncClient.js +165 -172
  19. package/dist/adapters/alwaysOnline.d.ts +6 -8
  20. package/dist/adapters/alwaysOnline.js +6 -8
  21. package/dist/adapters/inMemoryStorage.d.ts +9 -9
  22. package/dist/adapters/inMemoryStorage.js +9 -9
  23. package/dist/agent/Agent.d.ts +27 -32
  24. package/dist/agent/Agent.js +18 -19
  25. package/dist/agent/index.d.ts +4 -4
  26. package/dist/agent/index.js +5 -5
  27. package/dist/agent/session.d.ts +47 -44
  28. package/dist/agent/session.js +37 -48
  29. package/dist/agent/types.d.ts +26 -31
  30. package/dist/agent/types.js +6 -7
  31. package/dist/ai-sdk/coordinatedTool.d.ts +108 -0
  32. package/dist/ai-sdk/{coordinated-tool.js → coordinatedTool.js} +44 -38
  33. package/dist/ai-sdk/coordinationContext.d.ts +46 -0
  34. package/dist/ai-sdk/{coordination-context.js → coordinationContext.js} +26 -33
  35. package/dist/ai-sdk/index.d.ts +25 -22
  36. package/dist/ai-sdk/index.js +25 -22
  37. package/dist/ai-sdk/wrap.d.ts +6 -7
  38. package/dist/ai-sdk/wrap.js +1 -1
  39. package/dist/auth/credentialPolicy.d.ts +69 -74
  40. package/dist/auth/credentialPolicy.js +51 -56
  41. package/dist/auth/credentialSource.d.ts +6 -5
  42. package/dist/auth/credentialSource.js +9 -10
  43. package/dist/auth/index.d.ts +59 -58
  44. package/dist/auth/index.js +31 -37
  45. package/dist/auth/schemas.d.ts +5 -4
  46. package/dist/auth/schemas.js +5 -4
  47. package/dist/batching/index.d.ts +19 -21
  48. package/dist/batching/index.js +14 -17
  49. package/dist/cli.cjs +167 -119
  50. package/dist/client/Ablo.d.ts +73 -73
  51. package/dist/client/Ablo.js +125 -160
  52. package/dist/client/ApiClient.d.ts +30 -19
  53. package/dist/client/ApiClient.js +133 -38
  54. package/dist/client/auth.d.ts +47 -47
  55. package/dist/client/auth.js +108 -117
  56. package/dist/client/claimHeartbeatLoop.d.ts +50 -0
  57. package/dist/client/claimHeartbeatLoop.js +88 -0
  58. package/dist/client/consoleLogger.d.ts +5 -6
  59. package/dist/client/consoleLogger.js +5 -6
  60. package/dist/client/createInternalComponents.d.ts +14 -17
  61. package/dist/client/createInternalComponents.js +25 -30
  62. package/dist/client/createModelProxy.d.ts +130 -120
  63. package/dist/client/createModelProxy.js +152 -122
  64. package/dist/client/credentialEndpoint.d.ts +40 -42
  65. package/dist/client/credentialEndpoint.js +35 -36
  66. package/dist/client/functionalUpdate.d.ts +29 -27
  67. package/dist/client/functionalUpdate.js +21 -21
  68. package/dist/client/hostedEndpoints.d.ts +9 -12
  69. package/dist/client/hostedEndpoints.js +9 -12
  70. package/dist/client/httpClient.d.ts +57 -53
  71. package/dist/client/httpClient.js +29 -31
  72. package/dist/client/identity.d.ts +15 -20
  73. package/dist/client/identity.js +47 -58
  74. package/dist/client/modelRegistration.d.ts +5 -9
  75. package/dist/client/modelRegistration.js +67 -87
  76. package/dist/client/options.d.ts +134 -157
  77. package/dist/client/options.js +3 -7
  78. package/dist/client/registerDataSource.d.ts +9 -9
  79. package/dist/client/registerDataSource.js +15 -16
  80. package/dist/client/resourceTypes.d.ts +64 -75
  81. package/dist/client/resourceTypes.js +4 -10
  82. package/dist/client/schemaConfig.d.ts +31 -43
  83. package/dist/client/schemaConfig.js +38 -50
  84. package/dist/client/sessionMint.d.ts +16 -12
  85. package/dist/client/sessionMint.js +26 -31
  86. package/dist/client/validateAbloOptions.d.ts +12 -14
  87. package/dist/client/validateAbloOptions.js +8 -9
  88. package/dist/client/writeOptionsSchema.d.ts +18 -16
  89. package/dist/client/writeOptionsSchema.js +23 -20
  90. package/dist/client/wsMutationExecutor.d.ts +15 -20
  91. package/dist/client/wsMutationExecutor.js +17 -23
  92. package/dist/context.d.ts +6 -4
  93. package/dist/context.js +6 -4
  94. package/dist/coordination/index.d.ts +10 -8
  95. package/dist/coordination/index.js +14 -12
  96. package/dist/coordination/schema.d.ts +176 -128
  97. package/dist/coordination/schema.js +197 -133
  98. package/dist/coordination/trace.d.ts +9 -10
  99. package/dist/coordination/trace.js +13 -14
  100. package/dist/core/DatabaseManager.d.ts +5 -7
  101. package/dist/core/DatabaseManager.js +15 -19
  102. package/dist/core/QueryProcessor.d.ts +7 -9
  103. package/dist/core/QueryProcessor.js +22 -28
  104. package/dist/core/QueryView.d.ts +8 -8
  105. package/dist/core/QueryView.js +2 -2
  106. package/dist/core/StoreManager.d.ts +12 -14
  107. package/dist/core/StoreManager.js +21 -24
  108. package/dist/core/ViewRegistry.d.ts +5 -5
  109. package/dist/core/ViewRegistry.js +4 -4
  110. package/dist/core/index.d.ts +17 -12
  111. package/dist/core/index.js +32 -26
  112. package/dist/core/openIDBWithTimeout.d.ts +38 -36
  113. package/dist/core/openIDBWithTimeout.js +42 -43
  114. package/dist/core/queryUtils.d.ts +45 -0
  115. package/dist/core/queryUtils.js +69 -0
  116. package/dist/core/storeContract.d.ts +63 -61
  117. package/dist/core/storeContract.js +8 -12
  118. package/dist/environment.d.ts +28 -0
  119. package/dist/environment.js +21 -0
  120. package/dist/errorCodes.d.ts +107 -99
  121. package/dist/errorCodes.js +131 -132
  122. package/dist/errors.d.ts +160 -166
  123. package/dist/errors.js +155 -158
  124. package/dist/index.d.ts +30 -27
  125. package/dist/index.js +89 -86
  126. package/dist/interfaces/index.d.ts +102 -113
  127. package/dist/interfaces/index.js +5 -4
  128. package/dist/keys/index.d.ts +27 -29
  129. package/dist/keys/index.js +41 -40
  130. package/dist/mutators/RecordingTransaction.d.ts +16 -16
  131. package/dist/mutators/RecordingTransaction.js +31 -37
  132. package/dist/mutators/Transaction.d.ts +18 -26
  133. package/dist/mutators/Transaction.js +14 -20
  134. package/dist/mutators/UndoManager.d.ts +122 -131
  135. package/dist/mutators/UndoManager.js +145 -156
  136. package/dist/mutators/defineMutators.d.ts +23 -34
  137. package/dist/mutators/defineMutators.js +14 -20
  138. package/dist/mutators/inverseOp.d.ts +12 -15
  139. package/dist/mutators/inverseOp.js +12 -15
  140. package/dist/mutators/mutateActions.d.ts +10 -9
  141. package/dist/mutators/mutateActions.js +1 -1
  142. package/dist/mutators/readerActions.d.ts +9 -8
  143. package/dist/mutators/readerActions.js +2 -2
  144. package/dist/mutators/undoApply.d.ts +31 -27
  145. package/dist/mutators/undoApply.js +26 -24
  146. package/dist/policy/index.d.ts +5 -3
  147. package/dist/policy/index.js +5 -3
  148. package/dist/policy/types.d.ts +104 -100
  149. package/dist/policy/types.js +67 -66
  150. package/dist/query/client.d.ts +28 -23
  151. package/dist/query/client.js +45 -43
  152. package/dist/query/types.d.ts +37 -60
  153. package/dist/query/types.js +13 -33
  154. package/dist/react/AbloProvider.d.ts +1 -1
  155. package/dist/react/AbloProvider.js +2 -2
  156. package/dist/react/context.d.ts +25 -28
  157. package/dist/react/context.js +9 -10
  158. package/dist/react/index.d.ts +41 -42
  159. package/dist/react/index.js +37 -38
  160. package/dist/react/internalContext.d.ts +17 -19
  161. package/dist/react/useAblo.d.ts +23 -22
  162. package/dist/react/useAblo.js +16 -14
  163. package/dist/react/useCurrentUserId.d.ts +8 -7
  164. package/dist/react/useCurrentUserId.js +8 -7
  165. package/dist/react/useErrorListener.d.ts +7 -7
  166. package/dist/react/useErrorListener.js +10 -11
  167. package/dist/react/useMutationFailureListener.d.ts +8 -8
  168. package/dist/react/useMutationFailureListener.js +8 -8
  169. package/dist/react/useMutators.d.ts +11 -11
  170. package/dist/react/useMutators.js +3 -3
  171. package/dist/react/useReactive.js +2 -2
  172. package/dist/react/useSyncStatus.d.ts +4 -6
  173. package/dist/react/useUndoScope.d.ts +7 -9
  174. package/dist/react/useUndoScope.js +1 -1
  175. package/dist/schema/coordination.d.ts +21 -25
  176. package/dist/schema/coordination.js +21 -25
  177. package/dist/schema/ddl.d.ts +43 -39
  178. package/dist/schema/ddl.js +75 -68
  179. package/dist/schema/ddlLock.d.ts +20 -24
  180. package/dist/schema/ddlLock.js +18 -23
  181. package/dist/schema/diff.d.ts +99 -61
  182. package/dist/schema/diff.js +43 -34
  183. package/dist/schema/field.d.ts +37 -42
  184. package/dist/schema/field.js +35 -48
  185. package/dist/schema/generate.d.ts +12 -12
  186. package/dist/schema/generate.js +12 -12
  187. package/dist/schema/index.d.ts +2 -2
  188. package/dist/schema/index.js +21 -23
  189. package/dist/schema/model.d.ts +118 -143
  190. package/dist/schema/model.js +22 -33
  191. package/dist/schema/openapi.d.ts +10 -9
  192. package/dist/schema/openapi.js +5 -3
  193. package/dist/schema/queries.d.ts +29 -31
  194. package/dist/schema/queries.js +23 -25
  195. package/dist/schema/relation.d.ts +89 -99
  196. package/dist/schema/relation.js +13 -13
  197. package/dist/schema/residency.d.ts +16 -13
  198. package/dist/schema/residency.js +16 -13
  199. package/dist/schema/roles.d.ts +36 -43
  200. package/dist/schema/roles.js +31 -37
  201. package/dist/schema/schema.d.ts +33 -42
  202. package/dist/schema/schema.js +31 -32
  203. package/dist/schema/select.d.ts +13 -13
  204. package/dist/schema/select.js +13 -13
  205. package/dist/schema/serialize.d.ts +28 -31
  206. package/dist/schema/serialize.js +27 -31
  207. package/dist/schema/sugar.d.ts +17 -32
  208. package/dist/schema/sugar.js +14 -29
  209. package/dist/schema/{sync-delta-row.d.ts → syncDeltaRow.d.ts} +26 -49
  210. package/dist/schema/syncDeltaRow.js +89 -0
  211. package/dist/schema/tenancy.d.ts +44 -46
  212. package/dist/schema/tenancy.js +46 -48
  213. package/dist/server/adapter.d.ts +58 -58
  214. package/dist/server/adapter.js +13 -14
  215. package/dist/server/commit.d.ts +60 -64
  216. package/dist/server/index.d.ts +9 -10
  217. package/dist/server/index.js +1 -1
  218. package/dist/server/readConfig.d.ts +70 -0
  219. package/dist/server/readConfig.js +8 -0
  220. package/dist/server/storageMode.d.ts +23 -0
  221. package/dist/server/storageMode.js +17 -0
  222. package/dist/source/adapter.d.ts +30 -25
  223. package/dist/source/adapter.js +10 -10
  224. package/dist/source/adapters/drizzle.d.ts +28 -23
  225. package/dist/source/adapters/drizzle.js +30 -25
  226. package/dist/source/adapters/kysely.d.ts +27 -25
  227. package/dist/source/adapters/kysely.js +24 -23
  228. package/dist/source/adapters/memory.d.ts +8 -7
  229. package/dist/source/adapters/memory.js +9 -8
  230. package/dist/source/adapters/prisma.d.ts +13 -12
  231. package/dist/source/adapters/prisma.js +22 -25
  232. package/dist/source/conformance.d.ts +18 -11
  233. package/dist/source/conformance.js +17 -11
  234. package/dist/source/connector.d.ts +31 -32
  235. package/dist/source/connector.js +28 -28
  236. package/dist/source/connectorProtocol.d.ts +160 -0
  237. package/dist/source/connectorProtocol.js +162 -0
  238. package/dist/source/contract.d.ts +26 -27
  239. package/dist/source/contract.js +28 -29
  240. package/dist/source/factory.d.ts +46 -58
  241. package/dist/source/factory.js +22 -27
  242. package/dist/source/index.d.ts +7 -9
  243. package/dist/source/index.js +12 -14
  244. package/dist/source/migrations.d.ts +9 -9
  245. package/dist/source/migrations.js +9 -9
  246. package/dist/source/next.d.ts +9 -10
  247. package/dist/source/next.js +6 -7
  248. package/dist/source/pushQueue.d.ts +69 -47
  249. package/dist/source/pushQueue.js +32 -28
  250. package/dist/source/signing.d.ts +46 -17
  251. package/dist/source/signing.js +28 -11
  252. package/dist/source/types.d.ts +121 -104
  253. package/dist/source/types.js +13 -14
  254. package/dist/stores/ObjectStore.d.ts +10 -11
  255. package/dist/stores/ObjectStore.js +11 -12
  256. package/dist/stores/ObjectStoreContract.d.ts +12 -15
  257. package/dist/stores/SyncActionStore.d.ts +7 -11
  258. package/dist/stores/SyncActionStore.js +13 -17
  259. package/dist/surface.d.ts +27 -20
  260. package/dist/surface.js +27 -20
  261. package/dist/sync/{BootstrapHelper.d.ts → BootstrapFetcher.d.ts} +36 -42
  262. package/dist/sync/{BootstrapHelper.js → BootstrapFetcher.js} +76 -76
  263. package/dist/sync/ConnectionManager.d.ts +39 -50
  264. package/dist/sync/ConnectionManager.js +55 -66
  265. package/dist/sync/NetworkProbe.d.ts +24 -29
  266. package/dist/sync/NetworkProbe.js +63 -69
  267. package/dist/sync/{HydrationCoordinator.d.ts → OnDemandLoader.d.ts} +42 -41
  268. package/dist/sync/{HydrationCoordinator.js → OnDemandLoader.js} +59 -54
  269. package/dist/sync/{AreaOfInterestManager.d.ts → SubscriptionManager.d.ts} +43 -57
  270. package/dist/sync/{AreaOfInterestManager.js → SubscriptionManager.js} +43 -51
  271. package/dist/sync/SyncWebSocket.d.ts +139 -165
  272. package/dist/sync/SyncWebSocket.js +191 -223
  273. package/dist/sync/awaitClaimGrant.d.ts +18 -18
  274. package/dist/sync/awaitClaimGrant.js +11 -11
  275. package/dist/sync/bootstrapApply.d.ts +34 -24
  276. package/dist/sync/bootstrapApply.js +27 -19
  277. package/dist/sync/commitFrames.d.ts +21 -20
  278. package/dist/sync/commitFrames.js +18 -18
  279. package/dist/sync/createClaimStream.d.ts +23 -22
  280. package/dist/sync/createClaimStream.js +105 -23
  281. package/dist/sync/createPresenceStream.d.ts +19 -18
  282. package/dist/sync/createPresenceStream.js +25 -26
  283. package/dist/sync/createSnapshot.d.ts +12 -14
  284. package/dist/sync/createSnapshot.js +20 -26
  285. package/dist/sync/credentialLifecycle.d.ts +104 -104
  286. package/dist/sync/credentialLifecycle.js +140 -147
  287. package/dist/sync/deltaPipeline.d.ts +36 -34
  288. package/dist/sync/deltaPipeline.js +64 -65
  289. package/dist/sync/groupChange.d.ts +63 -61
  290. package/dist/sync/groupChange.js +74 -78
  291. package/dist/sync/heartbeat.d.ts +34 -33
  292. package/dist/sync/heartbeat.js +31 -31
  293. package/dist/sync/participants.d.ts +19 -19
  294. package/dist/sync/schemas.d.ts +3 -2
  295. package/dist/sync/schemas.js +14 -10
  296. package/dist/sync/syncCursor.d.ts +17 -21
  297. package/dist/sync/syncCursor.js +17 -21
  298. package/dist/sync/syncPlan.d.ts +28 -36
  299. package/dist/sync/syncPlan.js +18 -19
  300. package/dist/sync/syncPosition.d.ts +54 -49
  301. package/dist/sync/syncPosition.js +57 -52
  302. package/dist/sync/wsFrameHandlers.d.ts +35 -36
  303. package/dist/sync/wsFrameHandlers.js +63 -67
  304. package/dist/testing/fixtures/bootstrap.d.ts +12 -6
  305. package/dist/testing/fixtures/bootstrap.js +12 -6
  306. package/dist/testing/fixtures/deltas.d.ts +30 -33
  307. package/dist/testing/fixtures/deltas.js +30 -33
  308. package/dist/testing/fixtures/models.d.ts +11 -10
  309. package/dist/testing/fixtures/models.js +11 -10
  310. package/dist/testing/helpers/{react-wrapper.d.ts → reactWrapper.d.ts} +13 -10
  311. package/dist/testing/helpers/{react-wrapper.js → reactWrapper.js} +15 -12
  312. package/dist/testing/helpers/{sync-engine-harness.d.ts → syncEngineHarness.d.ts} +17 -15
  313. package/dist/testing/helpers/{sync-engine-harness.js → syncEngineHarness.js} +12 -10
  314. package/dist/testing/helpers/wait.d.ts +13 -8
  315. package/dist/testing/helpers/wait.js +13 -8
  316. package/dist/testing/index.d.ts +3 -3
  317. package/dist/testing/index.js +2 -2
  318. package/dist/testing/mocks/MockMutationExecutor.d.ts +18 -17
  319. package/dist/testing/mocks/MockMutationExecutor.js +15 -14
  320. package/dist/testing/mocks/MockNetworkMonitor.d.ts +8 -8
  321. package/dist/testing/mocks/MockNetworkMonitor.js +8 -8
  322. package/dist/testing/mocks/MockSyncContext.d.ts +20 -17
  323. package/dist/testing/mocks/MockSyncContext.js +15 -13
  324. package/dist/testing/mocks/MockSyncStore.d.ts +10 -10
  325. package/dist/testing/mocks/MockSyncStore.js +11 -11
  326. package/dist/testing/mocks/MockWebSocket.d.ts +26 -22
  327. package/dist/testing/mocks/MockWebSocket.js +22 -21
  328. package/dist/transactions/TransactionQueue.d.ts +181 -176
  329. package/dist/transactions/TransactionQueue.js +338 -350
  330. package/dist/transactions/TransactionStore.d.ts +6 -4
  331. package/dist/transactions/TransactionStore.js +6 -4
  332. package/dist/transactions/UnconfirmedWrites.d.ts +82 -0
  333. package/dist/transactions/UnconfirmedWrites.js +104 -0
  334. package/dist/transactions/coalesceRules.d.ts +41 -17
  335. package/dist/transactions/coalesceRules.js +40 -17
  336. package/dist/transactions/commitPayload.d.ts +48 -52
  337. package/dist/transactions/commitPayload.js +48 -57
  338. package/dist/transactions/deltaConfirmation.d.ts +20 -22
  339. package/dist/transactions/deltaConfirmation.js +37 -45
  340. package/dist/transactions/optimisticApply.d.ts +49 -0
  341. package/dist/transactions/optimisticApply.js +65 -0
  342. package/dist/transactions/{persistedReplay.d.ts → replayValidation.d.ts} +28 -22
  343. package/dist/transactions/{persistedReplay.js → replayValidation.js} +33 -27
  344. package/dist/types/global.d.ts +46 -41
  345. package/dist/types/global.js +20 -19
  346. package/dist/types/index.d.ts +71 -77
  347. package/dist/types/index.js +22 -22
  348. package/dist/types/modelData.d.ts +6 -8
  349. package/dist/types/modelData.js +5 -7
  350. package/dist/types/participant.d.ts +10 -11
  351. package/dist/types/participant.js +6 -8
  352. package/dist/types/streams.d.ts +208 -195
  353. package/dist/types/streams.js +7 -7
  354. package/dist/utils/asyncIterator.d.ts +25 -32
  355. package/dist/utils/asyncIterator.js +25 -32
  356. package/dist/utils/duration.d.ts +12 -15
  357. package/dist/utils/duration.js +12 -15
  358. package/dist/utils/mobxSetup.d.ts +53 -0
  359. package/dist/utils/{mobx-setup.js → mobxSetup.js} +42 -98
  360. package/dist/webhooks/events.d.ts +21 -16
  361. package/dist/webhooks/events.js +10 -8
  362. package/dist/webhooks/index.d.ts +5 -7
  363. package/dist/webhooks/index.js +5 -7
  364. package/dist/{schema/sync-delta-wire.d.ts → wire/delta.d.ts} +58 -41
  365. package/dist/wire/delta.js +114 -0
  366. package/dist/wire/errorEnvelope.d.ts +30 -31
  367. package/dist/wire/errorEnvelope.js +34 -40
  368. package/dist/wire/frames.d.ts +79 -86
  369. package/dist/wire/frames.js +26 -33
  370. package/dist/wire/index.d.ts +14 -12
  371. package/dist/wire/index.js +30 -26
  372. package/dist/wire/listEnvelope.d.ts +16 -23
  373. package/dist/wire/listEnvelope.js +7 -6
  374. package/dist/wire/protocol.d.ts +25 -32
  375. package/dist/wire/protocol.js +25 -32
  376. package/dist/wire/protocolVersion.d.ts +44 -40
  377. package/dist/wire/protocolVersion.js +44 -40
  378. package/docs/coordination.md +59 -0
  379. package/package.json +11 -10
  380. package/dist/ai-sdk/coordinated-tool.d.ts +0 -101
  381. package/dist/ai-sdk/coordination-context.d.ts +0 -52
  382. package/dist/core/query-utils.d.ts +0 -34
  383. package/dist/core/query-utils.js +0 -59
  384. package/dist/schema/sync-delta-row.js +0 -103
  385. package/dist/schema/sync-delta-wire.js +0 -102
  386. package/dist/server/read-config.d.ts +0 -67
  387. package/dist/server/read-config.js +0 -8
  388. package/dist/server/storage-mode.d.ts +0 -8
  389. package/dist/server/storage-mode.js +0 -28
  390. package/dist/source/connector-protocol.d.ts +0 -159
  391. package/dist/source/connector-protocol.js +0 -161
  392. package/dist/transactions/OptimisticEchoTracker.d.ts +0 -82
  393. package/dist/transactions/OptimisticEchoTracker.js +0 -104
  394. package/dist/transactions/mutation-error-handler.d.ts +0 -5
  395. package/dist/transactions/mutation-error-handler.js +0 -39
  396. package/dist/transactions/optimistic.d.ts +0 -24
  397. package/dist/transactions/optimistic.js +0 -45
  398. package/dist/utils/mobx-setup.d.ts +0 -42
@@ -1,11 +1,14 @@
1
1
  /**
2
- * Database - Simplified persistence layer
3
- * Fixed bootstrap triggering and data flow
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.
4
7
  */
5
8
  import { type DatabaseInfo, type WorkspaceMetadata } from './core/DatabaseManager.js';
6
9
  import { ModelRegistry } from './ModelRegistry.js';
7
10
  import { LoadStrategy } from './types/index.js';
8
- import type { BootstrapHelper, BootstrapData } from './sync/BootstrapHelper.js';
11
+ import type { BootstrapFetcher, BootstrapData } from './sync/BootstrapFetcher.js';
9
12
  import { InMemoryObjectStore } from './adapters/inMemoryStorage.js';
10
13
  /** Generic record type for model data */
11
14
  type ModelData = Record<string, unknown>;
@@ -43,19 +46,17 @@ interface PersistedTransaction {
43
46
  [key: string]: unknown;
44
47
  }
45
48
  /**
46
- * Bootstrap strategies (aligned with Linear's architecture):
49
+ * How a session establishes its baseline state at startup.
47
50
  *
48
- * 'full' - Full bootstrap from server
49
- * - Fetch complete snapshot from server
50
- * - Clear IndexedDB
51
- * - Load snapshot data
52
- * - Use snapshot's lastSyncId
51
+ * 'full' Fetch a complete snapshot from the server, clear the local store,
52
+ * load the snapshot, and adopt its `lastSyncId`.
53
53
  *
54
- * 'local' - Local-only bootstrap (skip server fetch)
55
- * - Use existing IndexedDB data
56
- * - Hydrate ObjectPool from IndexedDB
57
- * - Connect WebSocket with stored lastSyncId
58
- * - Receive deltas from lastSyncId+1 onwards
54
+ * 'partial' Fetch only the deltas since the stored `lastSyncId` and apply
55
+ * them on top of the existing local data.
56
+ *
57
+ * 'local' Skip the server entirely: hydrate the {@link InstanceCache} from the
58
+ * local store, connect the WebSocket with the stored `lastSyncId`, and
59
+ * receive deltas from there onward. Used when offline with valid local data.
59
60
  */
60
61
  export type BootstrapType = 'full' | 'partial' | 'local';
61
62
  export interface BootstrapRequirements {
@@ -67,7 +68,7 @@ export interface BootstrapRequirements {
67
68
  export interface BootstrapResult {
68
69
  modelsLoaded: number;
69
70
  modelsStored: number;
70
- /** The raw bootstrap response — callers can apply models directly to ObjectPool */
71
+ /** The raw bootstrap response — callers can apply models directly to InstanceCache */
71
72
  bootstrapData: BootstrapData;
72
73
  /**
73
74
  * Results of applying partial-bootstrap deltas to IDB. Present only when
@@ -90,13 +91,13 @@ export declare class Database {
90
91
  private modelRegistry;
91
92
  private bootstrapHelper;
92
93
  /** The pre-configured query helper for lazy-loading data from the sync server. */
93
- get helper(): BootstrapHelper;
94
+ get helper(): BootstrapFetcher;
94
95
  /**
95
- * PURE scoped snapshot fetch for hydrate-on-enter (P4). Returns the FULL
96
- * current rows of the given sync groups, with NO side effects — unlike
96
+ * Fetch the current rows of the given sync groups as a side-effect-free
97
+ * snapshot, used to hydrate a scope as the user enters it. Unlike
97
98
  * {@link bootstrapFromServer}, it does not persist to IndexedDB and does not
98
- * touch the connection's `subscribedSyncGroups` (which the shrinkage check
99
- * owns). The caller applies the result to the pool via the SCOPED apply path.
99
+ * change the connection's subscribed sync groups. The caller applies the
100
+ * result to the pool through the scoped apply path.
100
101
  */
101
102
  fetchScopedBootstrapData(syncGroups: readonly string[]): Promise<BootstrapData>;
102
103
  private currentDbInfo;
@@ -128,7 +129,7 @@ export declare class Database {
128
129
  private inMemoryStores;
129
130
  /** In-memory workspace metadata when inMemory=true. */
130
131
  private inMemoryMetadata;
131
- constructor(modelRegistry: ModelRegistry, bootstrapHelper: BootstrapHelper, options?: {
132
+ constructor(modelRegistry: ModelRegistry, bootstrapHelper: BootstrapFetcher, options?: {
132
133
  inMemory?: boolean;
133
134
  });
134
135
  /**
@@ -150,17 +151,14 @@ export declare class Database {
150
151
  private logPreservedFields;
151
152
  open(userId: string, organizationId: string, version?: number): Promise<void>;
152
153
  /**
153
- * Compact a record before persisting to IndexedDB
154
- * - Removes null/undefined fields
155
- * - Removes empty arrays and empty objects
156
- * - Drops redundant fields: __typename, __class, clientId, syncStatus
157
- *
158
- * ARCHITECTURE: By design, this method receives plain objects, not MobX observables:
159
- * - WebSocket deltas: Already JSON-parsed (SyncedStore.ts:889)
160
- * - Optimistic updates: Models call toJSON() which uses toJS() (SlideLayer.ts:224)
161
- * - Bootstrap data: Plain JSON from server
154
+ * Shrink a record before persisting it. Drops `undefined` fields, empty
155
+ * arrays, empty objects, and the redundant markers `__typename`, `__class`,
156
+ * `clientId`, and `syncStatus`. Explicit `null` values are preserved, since
157
+ * a null is a meaningful "clear this field" in a nullable column.
162
158
  *
163
- * Note: We do NOT drop required defaults; server provides them.
159
+ * By design this receives plain objects, never live observables: WebSocket
160
+ * deltas arrive already parsed, optimistic updates come through `toJSON()`,
161
+ * and bootstrap data is plain JSON from the server.
164
162
  */
165
163
  private compactRecord;
166
164
  /**
@@ -174,7 +172,9 @@ export declare class Database {
174
172
  */
175
173
  requiredBootstrap(): Promise<BootstrapRequirements>;
176
174
  /**
177
- * Bootstrap database with data from Go server
175
+ * Fetch a bootstrap snapshot (or delta batch) from the sync server and load
176
+ * it into the local store, then return a {@link BootstrapResult} the caller
177
+ * applies to the {@link InstanceCache}.
178
178
  */
179
179
  bootstrapFromServer(requirements: BootstrapRequirements,
180
180
  /** Full sync-group subscription list — what the WS subscribes to
@@ -183,17 +183,16 @@ export declare class Database {
183
183
  * team-derived groups. */
184
184
  syncGroups: readonly string[], onProgress?: (loaded: number) => void): Promise<BootstrapResult>;
185
185
  /**
186
- * Process incoming delta from WebSocket - simplified
186
+ * Apply a single inbound delta from the WebSocket to the local store.
187
187
  *
188
- * ⚠️ PERFORMANCE NOTE: This method is called for each individual delta.
189
- * For batch processing, use processDeltaBatch() instead to avoid
190
- * transaction overhead (2x transactions per delta = major bottleneck).
188
+ * This handles one delta at a time. To apply several, prefer
189
+ * {@link processDeltaBatch}, which commits them in one IndexedDB transaction
190
+ * rather than two transactions per delta.
191
191
  *
192
- * 📝 PARTIAL DELTA PATTERN:
193
- * - Server sends only changed fields: {id, position: {...}, updatedAt}
194
- * - UPDATE deltas are MERGED with existing records: {...existing, ...delta}
195
- * - This preserves fields not included in the delta (e.g., deckId, title)
196
- * - Explicit null values ARE preserved: {position: null} clears the field
192
+ * Update deltas carry only the changed fields, so they are merged onto the
193
+ * existing record rather than replacing it. That preserves fields the delta
194
+ * omits (such as deckId or title), and an explicit null is kept as a value,
195
+ * clearing that field.
197
196
  */
198
197
  processDelta(delta: {
199
198
  syncId?: number;
@@ -214,27 +213,20 @@ export declare class Database {
214
213
  data?: ModelData | null;
215
214
  }>;
216
215
  /**
217
- * PERFORMANCE FIX: Process multiple deltas in a single IndexedDB transaction
218
- *
219
- * This method dramatically improves sync performance by:
220
- * 1. Batch-reading all existing records for UPDATEs (outside transaction for speed)
221
- * 2. Opening a single transaction per store for all writes
222
- * 3. Merging UPDATE deltas with existing data to preserve unmodified fields
223
- * 4. Updating metadata only once at the end with highest syncId
224
- *
225
- * Performance impact: 186 deltas goes from ~372 transactions to just 1 transaction
216
+ * Apply many deltas to the local store in as few IndexedDB transactions as
217
+ * possible. Deltas are grouped by store, and each store's writes commit in a
218
+ * single transaction, so a batch of 186 deltas becomes roughly one
219
+ * transaction per store instead of two per delta.
226
220
  *
227
- * 📝 PARTIAL DELTA MERGE PATTERN:
228
- * - UPDATE deltas contain only changed fields
229
- * - We merge with existing: {...existing, ...delta}
230
- * - Preserves deckId, title, settings etc. when updating just position
231
- * - Handles explicit null: {field: null} clears the field correctly
221
+ * The method reads the existing records for update deltas up front, then
222
+ * merges each update onto its existing record so fields the delta omits are
223
+ * preserved and an explicit null still clears its field. It advances the
224
+ * persisted sync cursor once, to the highest committed sync id.
232
225
  *
233
- * 🔄 LINEAR-STYLE CONFLICT RESOLUTION:
234
- * - Builds a map of DELETE deltas with their syncIds
235
- * - Before processing UPDATE/INSERT, checks for DELETE with higher syncId
236
- * - Skips stale updates for entities that will be/were deleted
237
- * - Prevents 404 errors from fetching already-deleted entities
226
+ * Conflict resolution follows a delete-wins rule: it first indexes the
227
+ * delete deltas by entity, then skips any insert or update whose sync id is
228
+ * at or below a delete for the same entity. This avoids resurrecting a
229
+ * deleted entity and avoids fetching one that no longer exists.
238
230
  */
239
231
  processDeltaBatch(deltas: {
240
232
  syncId?: number;
@@ -268,7 +260,7 @@ export declare class Database {
268
260
  /**
269
261
  * Highest syncId whose IDB store transaction actually committed in this
270
262
  * batch. The runtime delta cursor (WS `lastSyncId`, server-side
271
- * `lastAckedSyncId`) must only advance to THIS value — not the input
263
+ * `lastAckedSyncId`) must only advance to this value — not the input
272
264
  * batch's range max — or it diverges from the persisted view and the
273
265
  * next catch-up request skips the un-persisted gap forever. Mirrors
274
266
  * the metadata-cursor invariant at `updateWorkspaceMetadata` below.
@@ -283,13 +275,7 @@ export declare class Database {
283
275
  /** Get data by index. `value` is an IDB key — string, number, Date,
284
276
  * BufferSource, or array thereof. */
285
277
  getDataByIndex(modelName: string, indexName: string, value: IDBValidKey): Promise<ModelData[]>;
286
- /**
287
- * Update workspace metadata
288
- */
289
- /**
290
- * Get the last sync ID from workspace metadata
291
- */
292
- /** Read workspace metadata from IDB (returns null if db not open). */
278
+ /** Read workspace metadata from IndexedDB. Returns null when the database is not open. */
293
279
  getWorkspaceMetadata(): Promise<WorkspaceMetadata | null>;
294
280
  getLastSyncId(): Promise<number>;
295
281
  updateWorkspaceMetadata(metadata: Partial<WorkspaceMetadata>): Promise<void>;
package/dist/Database.js CHANGED
@@ -1,6 +1,9 @@
1
1
  /**
2
- * Database - Simplified persistence layer
3
- * Fixed bootstrap triggering and data flow
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.
4
7
  */
5
8
  import { DatabaseManager } from './core/DatabaseManager.js';
6
9
  import { StoreManager } from './core/StoreManager.js';
@@ -22,11 +25,11 @@ export class Database {
22
25
  return this.bootstrapHelper;
23
26
  }
24
27
  /**
25
- * PURE scoped snapshot fetch for hydrate-on-enter (P4). Returns the FULL
26
- * current rows of the given sync groups, with NO side effects — unlike
28
+ * Fetch the current rows of the given sync groups as a side-effect-free
29
+ * snapshot, used to hydrate a scope as the user enters it. Unlike
27
30
  * {@link bootstrapFromServer}, it does not persist to IndexedDB and does not
28
- * touch the connection's `subscribedSyncGroups` (which the shrinkage check
29
- * owns). The caller applies the result to the pool via the SCOPED apply path.
31
+ * change the connection's subscribed sync groups. The caller applies the
32
+ * result to the pool through the scoped apply path.
30
33
  */
31
34
  async fetchScopedBootstrapData(syncGroups) {
32
35
  // No lastSyncId → a full snapshot of exactly these groups.
@@ -156,17 +159,14 @@ export class Database {
156
159
  getContext().logger.info(`Database opened: ${this.currentDbInfo.name} (${readiness.readyStores.length}/${readiness.totalStores} stores ready)`);
157
160
  }
158
161
  /**
159
- * Compact a record before persisting to IndexedDB
160
- * - Removes null/undefined fields
161
- * - Removes empty arrays and empty objects
162
- * - Drops redundant fields: __typename, __class, clientId, syncStatus
162
+ * Shrink a record before persisting it. Drops `undefined` fields, empty
163
+ * arrays, empty objects, and the redundant markers `__typename`, `__class`,
164
+ * `clientId`, and `syncStatus`. Explicit `null` values are preserved, since
165
+ * a null is a meaningful "clear this field" in a nullable column.
163
166
  *
164
- * ARCHITECTURE: By design, this method receives plain objects, not MobX observables:
165
- * - WebSocket deltas: Already JSON-parsed (SyncedStore.ts:889)
166
- * - Optimistic updates: Models call toJSON() which uses toJS() (SlideLayer.ts:224)
167
- * - Bootstrap data: Plain JSON from server
168
- *
169
- * Note: We do NOT drop required defaults; server provides them.
167
+ * By design this receives plain objects, never live observables: WebSocket
168
+ * deltas arrive already parsed, optimistic updates come through `toJSON()`,
169
+ * and bootstrap data is plain JSON from the server.
170
170
  */
171
171
  compactRecord(_modelName, data) {
172
172
  if (!data || typeof data !== 'object')
@@ -177,8 +177,8 @@ export class Database {
177
177
  if (key === '__typename' || key === '__class' || key === 'clientId' || key === 'syncStatus') {
178
178
  continue;
179
179
  }
180
- // FIXED: Only skip undefined, preserve explicit null values
181
- // Null is semantically meaningful in Prisma schemas (nullable fields)
180
+ // Skip only `undefined`; preserve explicit `null`, which is a
181
+ // meaningful value for a nullable column.
182
182
  if (value === undefined) {
183
183
  continue;
184
184
  }
@@ -272,26 +272,24 @@ export class Database {
272
272
  // point). Invalid → 0 → full bootstrap, the safe degradation.
273
273
  const metadataLastSyncId = syncPositionSchema.shape.persisted.safeParse(metadata?.lastSyncId).data ?? 0;
274
274
  const dataAge = metadata?.updatedAt ? Date.now() - metadata.updatedAt.getTime() : Infinity;
275
- // ── Zero-style cache-validity check ──────────────────────────
275
+ // ── Cache-validity check ─────────────────────────────────────
276
276
  //
277
277
  // The cursor (lastSyncId) is only valid if the data it refers to
278
- // actually exists in the stores. If IDB was cleared (or this is a
279
- // fresh in-memory session), the metadata's lastSyncId is stale —
280
- // sending it to the server would trigger a partial bootstrap that
281
- // returns zero deltas because the gap is 0, leaving the client
282
- // with an empty ObjectPool.
278
+ // actually exists in the stores. If the local store was cleared (or
279
+ // this is a fresh in-memory session), the metadata's lastSyncId is
280
+ // stale — sending it to the server would trigger a partial bootstrap
281
+ // that returns zero deltas because the gap is 0, leaving the client
282
+ // with an empty InstanceCache.
283
283
  //
284
- // Zero solves this by co-locating the cursor with the cached data:
285
- // if the data is gone, the cursor is gone. We achieve the same
286
- // property by sampling the actual stores — if they're empty, the
287
- // cursor is meaningless regardless of what metadata claims.
284
+ // The fix is to sample the actual stores: if they hold no rows, the
285
+ // cursor is meaningless regardless of what the metadata claims.
288
286
  const dataExists = this.inMemory
289
287
  ? false // In-memory mode: no persistent data across sessions
290
288
  : await this.storeManager.hasAnyData();
291
289
  // The effective lastSyncId: only trust the metadata cursor when
292
290
  // we've confirmed the data it refers to actually exists in the stores.
293
291
  const lastSyncId = dataExists ? metadataLastSyncId : 0;
294
- // 🔍 DIAGNOSTIC: Log database state
292
+ // Log the resolved database state for diagnostics.
295
293
  getContext().logger.debug('[Database.requiredBootstrap] State check', {
296
294
  readinessReady: readiness.ready,
297
295
  hasMetadata: !!metadata,
@@ -312,7 +310,8 @@ export class Database {
312
310
  getContext().logger.info('Offline detected with local data - using local bootstrap');
313
311
  }
314
312
  else {
315
- // SERVER-AUTHORITATIVE: Always use full bootstrap when online.
313
+ // The server is the source of truth: always use a full bootstrap
314
+ // when online.
316
315
  type = 'full';
317
316
  getContext().logger.info('Full bootstrap - server is source of truth', {
318
317
  reason: offline ? 'offline_no_data' : 'server_authoritative',
@@ -329,7 +328,9 @@ export class Database {
329
328
  };
330
329
  }
331
330
  /**
332
- * Bootstrap database with data from Go server
331
+ * Fetch a bootstrap snapshot (or delta batch) from the sync server and load
332
+ * it into the local store, then return a {@link BootstrapResult} the caller
333
+ * applies to the {@link InstanceCache}.
333
334
  */
334
335
  async bootstrapFromServer(requirements,
335
336
  /** Full sync-group subscription list — what the WS subscribes to
@@ -348,8 +349,8 @@ export class Database {
348
349
  modelsToLoad: requirements.modelsToLoad,
349
350
  });
350
351
  try {
351
- // FETCH FIRST (before any destructive operations)
352
- // This prevents data loss if the network request fails
352
+ // Fetch before any destructive operation, so a failed network
353
+ // request can't leave the local store empty.
353
354
  const startTime = typeof performance !== 'undefined' ? performance.now() : Date.now();
354
355
  getContext().logger.info('Fetching bootstrap data from server (before clearing local data)', {
355
356
  type: requirements.type,
@@ -363,8 +364,9 @@ export class Database {
363
364
  hasDeltas: !!bootstrapData.deltas,
364
365
  deltaCount: bootstrapData.deltaCount ?? 0,
365
366
  });
366
- // Only clear AFTER successful fetch (transactional safety)
367
- // IMPORTANT: Clear if the SERVER says it's a full snapshot, regardless of what we asked.
367
+ // Clear only after a successful fetch, for transactional safety.
368
+ // Clear when the server says the response is a full snapshot,
369
+ // regardless of what type was requested.
368
370
  if (bootstrapData.type === 'full') {
369
371
  await this.clear();
370
372
  }
@@ -379,7 +381,7 @@ export class Database {
379
381
  // Apply deltas to IndexedDB using processDeltaBatch for better performance.
380
382
  // Capture the return value so the pool can be updated by the caller —
381
383
  // without this, partial-bootstrap DELETEs persist to IDB but don't
382
- // evict entities from the in-memory ObjectPool, leaving ghost rows
384
+ // evict entities from the in-memory InstanceCache, leaving ghost rows
383
385
  // visible on the canvas until a full reload rebuilds the pool.
384
386
  let deltasApplied = 0;
385
387
  let deltaResults;
@@ -501,17 +503,16 @@ export class Database {
501
503
  }
502
504
  // bootstrapSpecificModels removed per request
503
505
  /**
504
- * Process incoming delta from WebSocket - simplified
506
+ * Apply a single inbound delta from the WebSocket to the local store.
505
507
  *
506
- * ⚠️ PERFORMANCE NOTE: This method is called for each individual delta.
507
- * For batch processing, use processDeltaBatch() instead to avoid
508
- * transaction overhead (2x transactions per delta = major bottleneck).
508
+ * This handles one delta at a time. To apply several, prefer
509
+ * {@link processDeltaBatch}, which commits them in one IndexedDB transaction
510
+ * rather than two transactions per delta.
509
511
  *
510
- * 📝 PARTIAL DELTA PATTERN:
511
- * - Server sends only changed fields: {id, position: {...}, updatedAt}
512
- * - UPDATE deltas are MERGED with existing records: {...existing, ...delta}
513
- * - This preserves fields not included in the delta (e.g., deckId, title)
514
- * - Explicit null values ARE preserved: {position: null} clears the field
512
+ * Update deltas carry only the changed fields, so they are merged onto the
513
+ * existing record rather than replacing it. That preserves fields the delta
514
+ * omits (such as deckId or title), and an explicit null is kept as a value,
515
+ * clearing that field.
515
516
  */
516
517
  async processDelta(delta) {
517
518
  const { actionType, modelName, modelId, data, syncId } = delta;
@@ -519,7 +520,7 @@ export class Database {
519
520
  if (!store) {
520
521
  return { action: 'verify', modelName, modelId };
521
522
  }
522
- // Best-practice gating: ignore already-applied deltas by comparing with persisted lastSyncId
523
+ // Idempotency gate: ignore already-applied deltas by comparing with the persisted lastSyncId
523
524
  try {
524
525
  const lastApplied = await this.getLastSyncId();
525
526
  const incomingId = typeof syncId === 'number' ? syncId : undefined;
@@ -572,11 +573,12 @@ export class Database {
572
573
  return { action: 'add', modelName, modelId, data: compacted };
573
574
  }
574
575
  case 'U': {
575
- // ✅ UPDATE: MUST merge with existing record (partial delta pattern)
576
- // Read existing record first
576
+ // Update: merge onto the existing record (partial-delta pattern).
577
+ // Read the existing record first.
577
578
  const existing = await store.get(modelId);
578
- // CRITICAL FIX: Skip UPDATE if there's no existing record to merge with
579
- // Creating a record from partial UPDATE data causes corruption (missing deckId, etc.)
579
+ // Skip the update when there's no existing record to merge with:
580
+ // building a record from partial update data would corrupt it
581
+ // (missing deckId, and so on).
580
582
  if (!existing) {
581
583
  getContext().observability.breadcrumb('Skipping UPDATE delta - no existing record to merge with', 'sync.database', 'warning', {
582
584
  modelName,
@@ -619,7 +621,7 @@ export class Database {
619
621
  getContext().observability.breadcrumb(`IndexedDB delete failed for ${modelName}:${modelId}`, 'sync.database', 'error', {
620
622
  error: err instanceof Error ? err.message : String(err),
621
623
  });
622
- // Surface failure so caller does not mutate ObjectPool inconsistently
624
+ // Surface failure so caller does not mutate InstanceCache inconsistently
623
625
  throw err;
624
626
  }
625
627
  return { action: 'remove', modelName, modelId };
@@ -659,27 +661,20 @@ export class Database {
659
661
  }
660
662
  }
661
663
  /**
662
- * PERFORMANCE FIX: Process multiple deltas in a single IndexedDB transaction
663
- *
664
- * This method dramatically improves sync performance by:
665
- * 1. Batch-reading all existing records for UPDATEs (outside transaction for speed)
666
- * 2. Opening a single transaction per store for all writes
667
- * 3. Merging UPDATE deltas with existing data to preserve unmodified fields
668
- * 4. Updating metadata only once at the end with highest syncId
664
+ * Apply many deltas to the local store in as few IndexedDB transactions as
665
+ * possible. Deltas are grouped by store, and each store's writes commit in a
666
+ * single transaction, so a batch of 186 deltas becomes roughly one
667
+ * transaction per store instead of two per delta.
669
668
  *
670
- * Performance impact: 186 deltas goes from ~372 transactions to just 1 transaction
669
+ * The method reads the existing records for update deltas up front, then
670
+ * merges each update onto its existing record so fields the delta omits are
671
+ * preserved and an explicit null still clears its field. It advances the
672
+ * persisted sync cursor once, to the highest committed sync id.
671
673
  *
672
- * 📝 PARTIAL DELTA MERGE PATTERN:
673
- * - UPDATE deltas contain only changed fields
674
- * - We merge with existing: {...existing, ...delta}
675
- * - Preserves deckId, title, settings etc. when updating just position
676
- * - Handles explicit null: {field: null} clears the field correctly
677
- *
678
- * 🔄 LINEAR-STYLE CONFLICT RESOLUTION:
679
- * - Builds a map of DELETE deltas with their syncIds
680
- * - Before processing UPDATE/INSERT, checks for DELETE with higher syncId
681
- * - Skips stale updates for entities that will be/were deleted
682
- * - Prevents 404 errors from fetching already-deleted entities
674
+ * Conflict resolution follows a delete-wins rule: it first indexes the
675
+ * delete deltas by entity, then skips any insert or update whose sync id is
676
+ * at or below a delete for the same entity. This avoids resurrecting a
677
+ * deleted entity and avoids fetching one that no longer exists.
683
678
  */
684
679
  async processDeltaBatch(deltas) {
685
680
  if ((!this.workspaceDb && !this.inMemory) || this.isClosing || deltas.length === 0) {
@@ -722,14 +717,11 @@ export class Database {
722
717
  }
723
718
  // Prepare results aligned with input order
724
719
  const results = new Array(deltas.length);
725
- // ========================================================================
726
- // LINEAR-STYLE CONFLICT RESOLUTION: Build DELETE syncId index
727
- // ========================================================================
728
- // Per Linear's architecture: "If the syncId of the deleting action is larger,
729
- // the model will not be created." This prevents processing stale UPDATE deltas
730
- // for entities that have been cascade-deleted (where DELETE delta exists).
731
- // ========================================================================
732
- const deleteSyncIds = new Map(); // key: "ModelName:modelId" -> DELETE syncId
720
+ // Build a delete index for conflict resolution. When a delete has a sync
721
+ // id at or above a later insert or update for the same entity, that entity
722
+ // is not (re)created — which drops stale updates for cascade-deleted
723
+ // entities.
724
+ const deleteSyncIds = new Map(); // key: "ModelName:modelId" -> delete syncId
733
725
  for (const delta of deltas) {
734
726
  if (delta.actionType === 'D' && delta.syncId) {
735
727
  const key = `${delta.modelName}:${delta.modelId}`;
@@ -749,18 +741,17 @@ export class Database {
749
741
  }
750
742
  // Group deltas by store for efficient transaction management.
751
743
  //
752
- // We intentionally track TWO highwater marks: `highestSyncId` for the
753
- // total range seen, and `highestPersistedSyncId` accumulated only from
754
- // deltas whose store transaction actually succeeded. The cursor
755
- // advance (at `updateWorkspaceMetadata`) uses ONLY the persisted one.
744
+ // The method tracks two high-water marks: `highestSyncId` for the total
745
+ // range seen, and `highestPersistedSyncId`, accumulated only from deltas
746
+ // whose store transaction actually committed. The cursor advance (at
747
+ // `updateWorkspaceMetadata`) uses only the persisted one.
756
748
  //
757
- // Without this split, a single store-level IDB failure (e.g. compact
758
- // record missing required field, validation abort) silently advances
759
- // the cursor past deltas that never wrote to IDB. Next partial
760
- // bootstrap asks "what's new since {advanced cursor}?" and the
761
- // skipped rows fall into the already-seen range forever the
762
- // observed "postgres has the deck, IDB doesn't, full reload can't
763
- // recover it" failure mode.
749
+ // Without this split, a single store-level failure (a compacted record
750
+ // missing a required field, a validation abort) would advance the cursor
751
+ // past deltas that never wrote to IndexedDB. The next partial bootstrap
752
+ // would ask "what's new since {advanced cursor}?", the skipped rows would
753
+ // fall into the already-seen range forever, and the local store would stay
754
+ // permanently behind the server with no way to recover on reload.
764
755
  const deltasByStore = new Map();
765
756
  let highestSyncId = 0;
766
757
  let highestPersistedSyncId = 0;
@@ -773,9 +764,8 @@ export class Database {
773
764
  if (typeof deltaSyncIdNum === 'number' && !isNaN(deltaSyncIdNum) && deltaSyncIdNum > highestSyncId) {
774
765
  highestSyncId = deltaSyncIdNum;
775
766
  }
776
- // ========================================================================
777
- // CONFLICT CHECK: Skip UPDATE/INSERT if DELETE exists with higher syncId
778
- // ========================================================================
767
+ // Conflict check: skip an insert or update when a delete for the same
768
+ // entity has an equal or higher sync id.
779
769
  if (delta.actionType === 'U' ||
780
770
  delta.actionType === 'I' ||
781
771
  delta.actionType === 'C' ||
@@ -823,8 +813,8 @@ export class Database {
823
813
  if (!store)
824
814
  continue;
825
815
  try {
826
- // ✅ BEST PRACTICE: Batch read-modify-write pattern
827
- // Step 1: Identify which deltas need existing data (UPDATEs)
816
+ // Batch read-modify-write.
817
+ // Step 1: Identify which deltas need existing data (updates)
828
818
  const updateDeltas = storeDeltas.filter(({ delta }) => delta.actionType === 'U');
829
819
  const updateIds = updateDeltas.map(({ delta }) => delta.modelId);
830
820
  // Step 2: Batch read all existing records in a SINGLE IDB transaction
@@ -849,8 +839,9 @@ export class Database {
849
839
  }
850
840
  }
851
841
  }
852
- // ✅ SELF-HEALING: Fetch missing records for UPDATE deltas
853
- // Track IDs that failed to fetch (404 = entity deleted, skip the delta)
842
+ // Self-heal by fetching missing records for update deltas.
843
+ // Track ids that failed to fetch (a 404 means the entity was deleted,
844
+ // so its delta is skipped).
854
845
  const failedToFetch = new Set();
855
846
  if (missingIds.size > 0) {
856
847
  getContext().logger.info(`[Database.processDeltaBatch] Found ${missingIds.size} missing records for ${modelName}, fetching from server...`);
@@ -924,13 +915,11 @@ export class Database {
924
915
  });
925
916
  break;
926
917
  case 'U': {
927
- // ✅ UPDATE: Merge delta with existing record (already fetched)
918
+ // Update: merge the delta onto the existing record (already fetched).
928
919
  const existing = existingRecords.get(modelId);
929
- // ========================================================================
930
- // SKIP STALE DELTAS: If entity doesn't exist locally AND failed to fetch
931
- // from server (404), this is a stale UPDATE for a deleted entity.
932
- // Per Linear's architecture, skip it instead of creating incomplete data.
933
- // ========================================================================
920
+ // Skip a stale update: if the entity is neither in the local
921
+ // store nor fetchable from the server (a 404), it was deleted,
922
+ // so skip it rather than create an incomplete record.
934
923
  if (!existing && failedToFetch.has(modelId)) {
935
924
  getContext().logger.debug('[Database.processDeltaBatch] Skipping UPDATE for deleted entity', {
936
925
  modelName,
@@ -939,8 +928,9 @@ export class Database {
939
928
  stagedResults.push({ action: 'verify', modelName, modelId, idx });
940
929
  break; // Skip this delta
941
930
  }
942
- // CRITICAL FIX: Skip UPDATE if there's no existing record to merge with
943
- // Creating a record from partial UPDATE data causes corruption (missing deckId, etc.)
931
+ // Skip the update when there's no existing record to merge with:
932
+ // building a record from partial update data would corrupt it
933
+ // (missing deckId, and so on).
944
934
  if (!existing) {
945
935
  getContext().observability.breadcrumb('Batch: Skipping UPDATE delta - no existing record', 'sync.database', 'warning', {
946
936
  modelName,
@@ -994,8 +984,8 @@ export class Database {
994
984
  tx.onerror = () => { reject(tx.error); };
995
985
  });
996
986
  // Only commit staged results to the global results if the transaction succeeded.
997
- // Also advance `highestPersistedSyncId` ONLY for deltas in this successful tx
998
- // so the cursor can't advance past rows that never wrote to IDB.
987
+ // Advance `highestPersistedSyncId` only for deltas in this successful
988
+ // transaction, so the cursor can't advance past rows that never wrote to IDB.
999
989
  for (const r of stagedResults) {
1000
990
  // Resolve the originating delta so we can carry its
1001
991
  // transactionId through to the result. Echo detection in
@@ -1104,13 +1094,7 @@ export class Database {
1104
1094
  const store = this.getRequiredStore(modelName);
1105
1095
  return await store.getAllFromIndex(indexName, value);
1106
1096
  }
1107
- /**
1108
- * Update workspace metadata
1109
- */
1110
- /**
1111
- * Get the last sync ID from workspace metadata
1112
- */
1113
- /** Read workspace metadata from IDB (returns null if db not open). */
1097
+ /** Read workspace metadata from IndexedDB. Returns null when the database is not open. */
1114
1098
  async getWorkspaceMetadata() {
1115
1099
  if (this.inMemory)
1116
1100
  return this.inMemoryMetadata;