@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,24 +1,27 @@
1
1
  /**
2
- * TransactionQueue - Production-ready transaction management
2
+ * TransactionQueue manages the lifecycle of local writes on their way to the
3
+ * server: it applies each change optimistically, batches the writes made in one
4
+ * event-loop tick into a single commit, retries transient failures, and rolls
5
+ * back on permanent rejection.
3
6
  *
4
- * Key features:
5
- * - Optimistic updates with rollback
6
- * - Conflict resolution strategies
7
- * - LINEAR-style microtask batching (transactions in same event loop share batchId)
8
- * - Proper dependency injection (no singleton)
7
+ * Key behaviours:
8
+ * - Optimistic updates with rollback on failure.
9
+ * - Configurable conflict resolution.
10
+ * - Microtask batching: transactions created in the same event-loop tick share
11
+ * a batch id and commit together in one round trip.
12
+ * - A dependency-injected executor, so several queues can coexist.
9
13
  */
10
14
  import { EventEmitter } from 'events';
11
15
  import { Model } from '../Model.js';
12
16
  import { getContext } from '../context.js';
13
- import { handleMutationError } from './mutation-error-handler.js';
14
17
  import { AbloError, AbloConnectionError, errorCodeSpec } from '../errors.js';
15
18
  import { SyncPosition } from '../sync/syncPosition.js';
16
- import { projectCommitPayload, computePriorityScore, normalizeModelKey, stripModelSuffix, hasStaleWriteOptions, applyWriteOptions, asTransportError, extractStatusCode, TX_TYPE_TO_MUTATION_OP, } from './commitPayload.js';
19
+ import { projectCommitPayload, computePriorityScore, normalizeModelKey, hasStaleWriteOptions, applyWriteOptions, asTransportError, extractStatusCode, TX_TYPE_TO_MUTATION_OP, } from './commitPayload.js';
17
20
  import { TransactionStore } from './TransactionStore.js';
18
21
  import { entityKey, mergeUpdateData, takeUnsentCreateForModel, findCreateBarrierForDelete, deferDeleteUntilCreateSettles, releaseDeferredDeletesForCreate, } from './coalesceRules.js';
19
22
  import { DeltaConfirmationTracker } from './deltaConfirmation.js';
20
- import { deserializePersistedTransaction, isNonReplayablePersistedRow, } from './persistedReplay.js';
21
- import { applyOptimisticCreate, applyOptimisticUpdate, applyOptimisticDelete, rollbackOptimistic, } from './optimistic.js';
23
+ import { deserializePersistedTransaction, isNonReplayablePersistedRow, } from './replayValidation.js';
24
+ import { applyOptimisticCreate, applyOptimisticUpdate, applyOptimisticDelete, rollbackOptimistic, } from './optimisticApply.js';
22
25
  export class TransactionQueue extends EventEmitter {
23
26
  store = new TransactionStore();
24
27
  // Signature of the last permanent-error we logged at `warn`. A `create`
@@ -27,17 +30,17 @@ export class TransactionQueue extends EventEmitter {
27
30
  // this, the identical cause prints on a loop. We log the first occurrence
28
31
  // and demote exact repeats to `debug`.
29
32
  lastPermanentErrorSig;
30
- // Per-instance executor binding. Set by `setMutationExecutor(...)` from the
31
- // owning Ablo right after construction. Falls back to `getContext()` only
32
- // when unset (preserves legacy tests / SDK consumers that haven't migrated).
33
+ // The executor bound to this queue instance, set by `setMutationExecutor(...)`
34
+ // just after construction. When unset it falls back to the ambient executor
35
+ // from `getContext()`.
33
36
  //
34
- // Why this exists: `initSyncEngine()` writes a *module-level* singleton.
35
- // Constructing a second Ablo (e.g. worker + per-job peer in agent-worker)
36
- // overwrites the first instance's executor. Without an instance binding,
37
- // queue commits on Ablo A would dispatch through Ablo B's executor closure,
38
- // which captures B's `storeHolder.store` and once B disposes its store,
39
- // that closure returns `null` for `getWs()` and every commit on A throws
40
- // `ws_not_ready` forever (queue classifies it as transient retry loop).
37
+ // The binding matters because the ambient executor is a module-level
38
+ // singleton: constructing a second client instance overwrites the first
39
+ // instance's executor. Without a per-instance binding, commits on one
40
+ // instance would dispatch through another instance's executor closure; once
41
+ // that other instance disposed its store, the closure would resolve no live
42
+ // connection and every commit here would fail with `ws_not_ready`, which the
43
+ // queue treats as transient and retries endlessly.
41
44
  _mutationExecutor = null;
42
45
  get mutationExecutor() {
43
46
  return this._mutationExecutor ?? getContext().mutationExecutor;
@@ -46,8 +49,8 @@ export class TransactionQueue extends EventEmitter {
46
49
  isProcessing = false;
47
50
  processTimer;
48
51
  processScheduled = false;
49
- // LINEAR PATTERN: Staging area for transactions created in same event loop tick
50
- // All transactions go here first, then get committed together via microtask
52
+ // Staging area for transactions created in the same event-loop tick. Each one
53
+ // lands here first, then a microtask commits them together.
51
54
  createdTransactions = [];
52
55
  commitScheduled = false;
53
56
  // Per-model in-flight tracking and merge buffer
@@ -130,11 +133,12 @@ export class TransactionQueue extends EventEmitter {
130
133
  releaseDeferredDeletesForCreate(createTransaction) {
131
134
  releaseDeferredDeletesForCreate(this.deferredDeletesByCreate, this.store, (tx) => { this.enqueue(tx); }, createTransaction);
132
135
  }
133
- // Configuration - tuned for LINEAR-style batching
134
- // Higher batch size and delay allows more operations to coalesce into single HTTP call
136
+ // Default configuration, tuned so more operations coalesce into a single
137
+ // commit: a larger batch size and delay give rapid operations more time to
138
+ // merge before the batch is sent.
135
139
  config = {
136
- maxBatchSize: 50, // Increased from 10 - matches Linear's batch size
137
- batchDelay: 150, // Increased from 50ms - more time to coalesce rapid operations
140
+ maxBatchSize: 50, // send up to this many operations per commit
141
+ batchDelay: 150, // milliseconds to wait for more operations before sending
138
142
  maxRetries: 3,
139
143
  conflictResolution: {
140
144
  strategy: 'last-write-wins',
@@ -151,19 +155,21 @@ export class TransactionQueue extends EventEmitter {
151
155
  };
152
156
  // Track executing transactions for backpressure
153
157
  executingCount = 0;
154
- // Optimistic update tracking. The entry shape + apply/rollback rules live
155
- // in `./optimistic.js`; the ledger itself stays host state because the
158
+ // Optimistic update tracking. The entry shape and apply/rollback rules live
159
+ // in `./optimisticApply.js`; the map itself stays on the queue because the
156
160
  // completion paths, `getStats`, and `dispose` all read it.
157
161
  optimisticUpdates = new Map();
158
- // Stale-context notifications (CoAgent/MTPO notify-instead-of-abort) keyed by
159
- // transaction id, populated from the commit ack and drained by
160
- // `waitForCommitReceipt` so the receipt carries the self-heal signal.
162
+ // Stale-context notifications, keyed by transaction id. When the server
163
+ // accepts a commit but reports that an operation's read premise had moved,
164
+ // the notification lands here from the commit acknowledgement and is drained
165
+ // by `waitForCommitReceipt`, so the receipt can carry it back to the caller.
161
166
  commitNotifications = new Map();
162
167
  // Delta-confirmation tracking (ack watermark advance + the awaiting_delta
163
168
  // timeout/retry maps) lives in `./deltaConfirmation.js`. Constructed in the
164
169
  // constructor once `position` is bound.
165
170
  deltaConfirmation;
166
- // Connection state check - set by SyncClient to prevent rollbacks during disconnection
171
+ // Connection-state check, supplied by the client, used to hold off rollbacks
172
+ // while disconnected.
167
173
  isConnectedFn = () => true;
168
174
  // Grace timer that, when fired, fails any commit-lane transaction
169
175
  // still awaiting an ack. Started on `setConnectionState('disconnected')`,
@@ -171,11 +177,11 @@ export class TransactionQueue extends EventEmitter {
171
177
  // is preserved for brief blips; this only catches persistent disconnects.
172
178
  commitOfflineGraceTimer = null;
173
179
  /**
174
- * THE client's place in the global delta order the SHARED instance
175
- * (injected by SyncClient; standalone construction gets its own). The
176
- * queue advances `acked` on commit responses; the store advances
177
- * `applied`/`persisted`; snapshots/claims read `readFloor`. Contract +
178
- * rationale live in `sync/syncPosition.ts`.
180
+ * This client's place in the global order of sync deltas. The instance is
181
+ * shared: the client injects one, and a standalone queue creates its own. The
182
+ * queue advances the `acked` cursor as commit responses arrive, the store
183
+ * advances `applied` and `persisted`, and snapshots and claims read
184
+ * `readFloor`. See `../sync/syncPosition.js` for the full contract.
179
185
  */
180
186
  position;
181
187
  /** Applied-cursor alias, kept so the many internal read sites stay legible. */
@@ -244,22 +250,18 @@ export class TransactionQueue extends EventEmitter {
244
250
  });
245
251
  }
246
252
  /**
247
- * Look up the in-flight `confirmation` promise for a (model, id) pair.
248
- * Returns the promise from the most-recent live transaction matching
249
- * the given model+id, or `Promise.resolve()` if none is open (which
250
- * means either "already confirmed" or "never staged" — both safe
251
- * outcomes for the routing-helper grace-window use case).
253
+ * Returns the in-flight confirmation promise for a given model and id. When
254
+ * several transactions match, it returns the most recent one's promise; when
255
+ * none is open it resolves immediately, which covers both "already confirmed"
256
+ * and "never staged".
252
257
  *
253
- * Looks across `pending`, `executing`, and `awaiting_delta` these
254
- * are the three non-terminal statuses where rollback is still
255
- * possible. Skips `completed` (already settled) and `failed` /
256
- * `rolled_back` (already rejected; the call site missed the
257
- * `confirmation` window and should rely on `onMutationFailure` toast
258
- * instead).
259
- *
260
- * Distinct from `tx.confirmation` on a known transaction — used by
261
- * call sites that hold a Model reference (returned by
262
- * `ablo.<model>.create()`) but never see the underlying transaction.
258
+ * It considers the three non-terminal statuses in which the write can still
259
+ * be rolled back `pending`, `executing`, and `awaiting_delta` — and ignores
260
+ * `completed` (already settled) and `failed`/`rolled_back` (already
261
+ * rejected). This complements the `confirmation` promise carried on a known
262
+ * {@link Transaction}: use this method at call sites that hold a model
263
+ * returned by `ablo.<model>.create()` but never see the underlying
264
+ * transaction.
263
265
  */
264
266
  confirmationFor(modelName, modelId) {
265
267
  const candidates = [
@@ -275,15 +277,15 @@ export class TransactionQueue extends EventEmitter {
275
277
  return latest.confirmation ?? Promise.resolve();
276
278
  }
277
279
  /**
278
- * Attach a hot `confirmation` promise to a freshly created transaction.
279
- * Must be called BEFORE the transaction is staged so the call site can
280
- * `await tx.confirmation` synchronously after the create/update/delete
281
- * call returns. Idempotent: returns early if the tx already has one.
280
+ * Attaches a `confirmation` promise to a newly created transaction. Call this
281
+ * before the transaction is staged so a caller can `await tx.confirmation`
282
+ * immediately after a create, update, or delete returns. It is idempotent and
283
+ * returns early if one is already attached.
282
284
  *
283
- * The unhandled-rejection trap is mandatory most call sites won't
284
- * `await confirmation`, and Node/browser would otherwise crash on the
285
- * rejection. Consumers who *do* want failure visibility just attach a
286
- * `.then`/`.catch` and the trap becomes a no-op.
285
+ * It also attaches a no-op rejection handler. Most callers never await the
286
+ * confirmation, and without this the runtime would report an unhandled
287
+ * rejection when a write fails. Callers that do want to observe failure simply
288
+ * attach their own `.then`/`.catch`.
287
289
  */
288
290
  attachConfirmation(tx) {
289
291
  if (tx.confirmation)
@@ -292,34 +294,30 @@ export class TransactionQueue extends EventEmitter {
292
294
  this.confirmationResolvers.set(tx.id, { resolve, reject });
293
295
  });
294
296
  tx.confirmation.catch(() => {
295
- // Swallow unhandled rejections explicit consumers attach their own
296
- // handler; silent failure is the leak we're already fixing elsewhere.
297
+ // Swallow unhandled rejections; callers that care attach their own handler.
297
298
  });
298
299
  }
299
300
  /**
300
- * Set connection state checker - prevents rollbacks during disconnection.
301
- * When disconnected, timeouts re-schedule instead of rolling back.
301
+ * Registers a predicate the queue uses to check whether it is connected.
302
+ * While disconnected, confirmation timeouts re-schedule themselves instead of
303
+ * escalating, so a transaction is never rolled back merely because the client
304
+ * was briefly offline.
302
305
  */
303
306
  setConnectionChecker(fn) {
304
307
  this.isConnectedFn = fn;
305
308
  }
306
309
  /**
307
- * Drive the offline-grace timer for in-flight commit-lane transactions.
308
- *
309
- * On `'disconnected'`: start a one-shot timer of
310
- * `config.commitOfflineGraceMs`. If the timer fires (disconnect
311
- * persisted past grace), iterate every commit-lane transaction with
312
- * `status ∈ {'pending', 'executing'}` and emit
313
- * `transaction:failed:${id}` with an `AbloConnectionError`. That
314
- * lets `waitForCommitReceipt` reject in seconds instead of hanging
315
- * forever — which is what wedged the 2026-05-15 subagent run.
310
+ * Drives the offline-grace timer for in-flight commit-lane transactions.
316
311
  *
317
- * On `'connected'`: clear any pending grace timer. Brief blips are
318
- * absorbed transparently; the existing reconnect-retry path in
319
- * `processCommitLane` / `flushOfflineQueue` handles the resumption.
312
+ * On `'disconnected'` it starts a one-shot timer of
313
+ * `config.commitOfflineGraceMs`. If that timer fires meaning the disconnect
314
+ * outlasted the grace window every commit-lane transaction still `pending`
315
+ * or `executing` is failed with an {@link AbloConnectionError}, so
316
+ * {@link waitForCommitReceipt} rejects within seconds instead of hanging.
320
317
  *
321
- * Called from SyncClient's `setConnectionState` after the
322
- * `'connection:disconnected'` / `'connection:established'` events.
318
+ * On `'connected'` it clears any pending grace timer. Brief disconnects are
319
+ * absorbed transparently; {@link processCommitLane} and
320
+ * {@link flushOfflineQueue} resume the work on reconnect.
323
321
  */
324
322
  setConnectionState(state) {
325
323
  if (state === 'connected') {
@@ -347,8 +345,8 @@ export class TransactionQueue extends EventEmitter {
347
345
  }
348
346
  if (inFlight.length === 0)
349
347
  return;
350
- // Internal mechanic: each failed commit surfaces to the consumer via its own
351
- // rejection path, so this aggregate is forensic debug, not warn.
348
+ // Each failed commit reaches the consumer through its own rejection path,
349
+ // so this aggregate line is forensic and logged at debug rather than warn.
352
350
  getContext().logger.debug(`[TransactionQueue] WS disconnected > ${graceMs}ms; failing ${inFlight.length} in-flight commit(s) with AbloConnectionError`, { inFlightIds: inFlight.map((id) => id.slice(0, 8)) });
353
351
  for (const id of inFlight) {
354
352
  const tx = this.commitStore.get(id);
@@ -361,39 +359,41 @@ export class TransactionQueue extends EventEmitter {
361
359
  }
362
360
  }
363
361
  /**
364
- * Bind the executor for this queue instance. Called by the owning Ablo
365
- * right after `BaseSyncedStore` is constructed so the executor's
366
- * `storeHolder.store` closure resolves to *this* Ablo's WS not whichever
367
- * Ablo most recently called `initSyncEngine()`.
362
+ * Binds the mutation executor for this queue instance. The owning client
363
+ * calls this right after construction, so commits made here always dispatch
364
+ * through this instance's connection even when several client instances exist
365
+ * in the same process.
368
366
  */
369
367
  setMutationExecutor(executor) {
370
368
  this._mutationExecutor = executor;
371
369
  }
372
370
  // ============================================================================
373
- // LINEAR PATTERN: Microtask-based Transaction Staging
371
+ // Microtask-based transaction staging
374
372
  // ============================================================================
375
373
  //
376
- // All transactions first go to `createdTransactions` staging area.
377
- // A microtask commits them all together with the same batchIndex.
378
- // This ensures that bulk operations (like importing 100 layers) are batched efficiently.
374
+ // Every transaction lands in the `createdTransactions` staging area first.
375
+ // A microtask then commits them together under one batch index, so a bulk
376
+ // operation such as importing a hundred rows is sent efficiently.
379
377
  //
380
378
  // Flow:
381
- // 1. create()/update()/delete() calls stageTransaction()
382
- // 2. stageTransaction() adds to createdTransactions and schedules microtask
383
- // 3. Microtask runs commitCreatedTransactions() after current sync code completes
384
- // 4. All staged transactions get same batchIndex and move to executionQueue
379
+ // 1. create()/update()/delete() calls stageTransaction().
380
+ // 2. stageTransaction() adds to createdTransactions and schedules a microtask.
381
+ // 3. The microtask runs commitCreatedTransactions() once the current
382
+ // synchronous code finishes.
383
+ // 4. All staged transactions share one batch index and move to the execution
384
+ // queue.
385
385
  // ============================================================================
386
386
  /**
387
- * Stage a transaction for commit (Linear pattern)
388
- * Transactions staged in the same event loop tick will be committed together
387
+ * Stages a transaction for commit. Transactions staged within the same
388
+ * event-loop tick are committed together.
389
389
  */
390
390
  stageTransaction(transaction) {
391
391
  this.createdTransactions.push(transaction);
392
392
  this.scheduleCommit();
393
393
  }
394
394
  /**
395
- * Schedule commit of staged transactions via microtask
396
- * This ensures all synchronous transaction creates are batched together
395
+ * Schedules the staged transactions to commit on a microtask, so all
396
+ * transactions created synchronously within one tick are batched together.
397
397
  */
398
398
  scheduleCommit() {
399
399
  if (this.commitScheduled)
@@ -409,8 +409,8 @@ export class TransactionQueue extends EventEmitter {
409
409
  });
410
410
  }
411
411
  /**
412
- * Commit all staged transactions to the execution queue (Linear pattern)
413
- * All transactions get the same batchIndex for efficient batching
412
+ * Moves all staged transactions onto the execution queue, assigning them a
413
+ * single shared batch index so they commit together.
414
414
  */
415
415
  commitCreatedTransactions() {
416
416
  this.commitScheduled = false;
@@ -434,11 +434,15 @@ export class TransactionQueue extends EventEmitter {
434
434
  this.enqueue(transaction);
435
435
  }
436
436
  }
437
- // Batch flush all pending transactions via commit (fast path on reconnect)
437
+ /**
438
+ * Flushes every pending transaction in one commit, the fast path taken on
439
+ * reconnect. If the batch fails, each transaction falls back to normal,
440
+ * one-by-one processing.
441
+ */
438
442
  async flushOfflineQueue() {
439
- // Kick the commit lane too — pending atomic envelopes from
440
- // `commits.create()` were left at the head of the lane while the WS
441
- // was down. Fire-and-forget; processCommitLane self-serializes.
443
+ // Kick the commit lane too: atomic envelopes from `commits.create()` may
444
+ // have been left at the head of the lane while the connection was down.
445
+ // Fire-and-forget; processCommitLane serializes itself.
442
446
  void this.processCommitLane();
443
447
  // Collect pending transactions in created order
444
448
  const pending = this.store.getByStatus('pending').sort((a, b) => a.createdAt - b.createdAt);
@@ -489,7 +493,9 @@ export class TransactionQueue extends EventEmitter {
489
493
  }
490
494
  }
491
495
  /**
492
- * Create operation with optimistic update
496
+ * Records a create and applies it optimistically, then stages it for the next
497
+ * batched commit. Returns the {@link Transaction}, whose `confirmation`
498
+ * promise settles once the server confirms the write.
493
499
  */
494
500
  async create(model, context, writeOptions) {
495
501
  const actualModelName = model.getModelName();
@@ -501,8 +507,8 @@ export class TransactionQueue extends EventEmitter {
501
507
  modelKey: normalizeModelKey(actualModelName),
502
508
  priorityScore: this.computePriorityScore('create', actualModelName),
503
509
  data: this.extractCreateData(model),
504
- // CREATE rollback removes the row there is no prior state to
505
- // restore, so allocating a `toJSON()` snapshot here was waste.
510
+ // Rolling back a create removes the row, so there is no prior state to
511
+ // restore and no snapshot is captured here.
506
512
  previousData: null,
507
513
  context,
508
514
  status: 'pending',
@@ -516,16 +522,18 @@ export class TransactionQueue extends EventEmitter {
516
522
  if (this.config.enableOptimistic) {
517
523
  this.applyOptimisticCreate(model, transaction);
518
524
  }
519
- // Microtask coalescer (`scheduleCommit`) collapses all creates in
520
- // this tick into one wire commit with one `batchIndex` — see
521
- // `commitCreatedTransactions`. No batch API needed at the call site.
525
+ // The microtask coalescer (`scheduleCommit`) collapses all creates in this
526
+ // tick into one commit under a single `batchIndex` — see
527
+ // `commitCreatedTransactions`. No batch call is needed at the call site.
522
528
  this.stageTransaction(transaction);
523
529
  this.emit('transaction:created', transaction);
524
530
  return transaction;
525
531
  }
526
532
  /**
527
- * Update operation with conflict detection
528
- * @param precomputedChanges - Optional pre-captured changes (avoids re-reading from model)
533
+ * Records an update and applies it optimistically, then stages it for the next
534
+ * batched commit. Rapid updates to the same entity coalesce into a single wire
535
+ * operation.
536
+ * @param precomputedChanges - Optional pre-captured changes, used instead of re-reading them from the model.
529
537
  */
530
538
  async update(model, context, precomputedChanges, writeOptions) {
531
539
  const actualModelName = model.getModelName();
@@ -534,12 +542,11 @@ export class TransactionQueue extends EventEmitter {
534
542
  ? this.mapChangesToInput(actualModelName, precomputedChanges)
535
543
  : this.extractUpdateData(model);
536
544
  const previousData = this.extractPreviousData(model, updateInput);
537
- // Advance the per-field baseline for the keys we just froze into this
538
- // transaction. `Model.propertyChanged` is first-old-wins and only cleared on
539
- // sync-ack, so without this a SECOND update to the same field before the
540
- // first acks would re-capture the original `.old` (the pre-session value)
541
- // instead of THIS update's result corrupting the stream-recorded undo
542
- // inverse (the second move's "before" would point all the way back). The
545
+ // Advance the per-field baseline for the keys just frozen into this
546
+ // transaction. The model records the first old value per field and clears it
547
+ // only on sync acknowledgement, so a second update to the same field before
548
+ // the first is acknowledged would otherwise re-capture the original value
549
+ // instead of this update's result, corrupting the recorded undo inverse. The
543
550
  // wire payload is already frozen in `transaction.data`, so dropping the
544
551
  // consumed entries is safe.
545
552
  model.consumeModifiedFields(Object.keys(updateInput));
@@ -567,18 +574,22 @@ export class TransactionQueue extends EventEmitter {
567
574
  if (this.config.enableOptimistic) {
568
575
  this.applyOptimisticUpdate(model, transaction);
569
576
  }
570
- // LINEAR PATTERN: Stage transaction for microtask commit
571
- // Multiple updates in same event loop will be batched together
572
- // enqueue() will still apply its coalescing logic for same-entity updates
577
+ // Stage the transaction for the microtask commit; updates made in the same
578
+ // tick are batched together, and enqueue() still coalesces same-entity
579
+ // updates.
573
580
  this.stageTransaction(transaction);
574
581
  this.emit('transaction:created', transaction);
575
582
  return transaction;
576
583
  }
577
584
  /**
578
- * Delete operation with cascade handling
585
+ * Records a delete and applies it optimistically. If the row's own create has
586
+ * not yet been sent, both are cancelled locally rather than sending a create
587
+ * followed by a delete; if the create is already in flight, the delete waits
588
+ * until it settles so the server never sees a delete before the create.
579
589
  */
580
590
  async delete(model, context, writeOptions) {
581
- // 🔧 FIXED: Use getModelName() instead of constructor.name (production-safe)
591
+ // Use getModelName() rather than constructor.name, which is unreliable once
592
+ // class names are minified.
582
593
  const actualModelName = model.getModelName();
583
594
  // Skip Activity delete transactions - activities are permanent audit records
584
595
  if (actualModelName === 'Activity') {
@@ -651,30 +662,32 @@ export class TransactionQueue extends EventEmitter {
651
662
  this.deferDeleteUntilCreateSettles(createBarrier, transaction);
652
663
  }
653
664
  else {
654
- // LINEAR PATTERN: Stage transaction for microtask commit
655
- // All deletes in same event loop will be batched together
665
+ // Stage the transaction for the microtask commit; deletes in the same
666
+ // tick are batched together.
656
667
  this.stageTransaction(transaction);
657
668
  }
658
669
  this.emit('transaction:created', transaction);
659
670
  return transaction;
660
671
  }
661
672
  /**
662
- * Upload attachment delegates to attachment-uploader.ts
673
+ * Uploads a single attachment, delegating to the mutation executor.
663
674
  */
664
675
  async uploadAttachment(_file, options, _context) {
665
676
  return this.mutationExecutor.uploadAttachment?.(options.id, options) ?? null;
666
677
  }
667
678
  /**
668
- * Batch upload attachments delegates to MutationExecutor
679
+ * Uploads several attachments in one call, delegating to the mutation executor.
669
680
  */
670
681
  async batchUploadAttachments(_files, items, _context) {
671
682
  return this.mutationExecutor.batchUploadAttachments?.(items.map(i => ({ id: i.id, input: i }))) ?? [];
672
683
  }
673
684
  /**
674
- * Archive operation
685
+ * Records an archive and applies it optimistically, then stages it for the
686
+ * next batched commit.
675
687
  */
676
688
  async archive(model, context, writeOptions) {
677
- // 🔧 FIXED: Use getModelName() instead of constructor.name (production-safe)
689
+ // Use getModelName() rather than constructor.name, which is unreliable once
690
+ // class names are minified.
678
691
  const actualModelName = model.getModelName();
679
692
  const modelKey = normalizeModelKey(actualModelName);
680
693
  const priorityScore = this.computePriorityScore('archive', actualModelName);
@@ -695,16 +708,18 @@ export class TransactionQueue extends EventEmitter {
695
708
  };
696
709
  this.attachConfirmation(transaction);
697
710
  this.store.add(transaction);
698
- // LINEAR PATTERN: Stage transaction for microtask commit
711
+ // Stage the transaction for the microtask commit.
699
712
  this.stageTransaction(transaction);
700
713
  this.emit('transaction:created', transaction);
701
714
  return transaction;
702
715
  }
703
716
  /**
704
- * Unarchive operation
717
+ * Records an unarchive and applies it optimistically, then stages it for the
718
+ * next batched commit.
705
719
  */
706
720
  async unarchive(model, context) {
707
- // 🔧 FIXED: Use getModelName() instead of constructor.name (production-safe)
721
+ // Use getModelName() rather than constructor.name, which is unreliable once
722
+ // class names are minified.
708
723
  const actualModelName = model.getModelName();
709
724
  const modelKey = normalizeModelKey(actualModelName);
710
725
  const priorityScore = this.computePriorityScore('unarchive', actualModelName);
@@ -724,30 +739,29 @@ export class TransactionQueue extends EventEmitter {
724
739
  };
725
740
  this.attachConfirmation(transaction);
726
741
  this.store.add(transaction);
727
- // LINEAR PATTERN: Stage transaction for microtask commit
742
+ // Stage the transaction for the microtask commit.
728
743
  this.stageTransaction(transaction);
729
744
  this.emit('transaction:created', transaction);
730
745
  return transaction;
731
746
  }
732
747
  /**
733
- * Enqueue transaction for execution
748
+ * Places a transaction on the execution queue, coalescing it into an existing
749
+ * same-entity update where possible so redundant writes collapse.
734
750
  */
735
751
  enqueue(transaction) {
736
752
  this.ensureDerivedFields(transaction);
737
753
  const modelKey = `${transaction.modelName}:${transaction.modelId}`;
738
- // LINEAR PATTERN: Simplified coalescing for updates
739
- // Staging already batches all transactions in same event loop tick
740
- // We only need to handle: (1) in-flight merging, (2) same-entity merging
754
+ // Coalescing for updates. Staging already batches everything created in one
755
+ // event-loop tick, so only two cases remain here: merging into an in-flight
756
+ // update, and merging into a pending same-entity update.
741
757
  //
742
- // RETRIES (attempts > 0) must NEVER take either merge path. A retry is
743
- // re-enqueued from `handleFailure` while its own modelKey is still in
744
- // `inFlightByModel` (the post-batch cleanup runs after the catch), so the
745
- // in-flight merge mistook the retry for a concurrent user edit: it moved
746
- // the data into `pendingMergeByModel`, REMOVED the transaction, and the
747
- // post-batch block minted a fresh follow-up with `attempts: 0`. The
748
- // attempt counter never accumulated and `maxRetries` never tripped — an
749
- // infinite ~`batchDelay` resend storm of self-laundering transaction
750
- // clones (found by the claims journey, 2026-06-10).
758
+ // Retries (attempts > 0) must not take either merge path. A retry is
759
+ // re-enqueued from `handleFailure` while its own model key is still marked
760
+ // in `inFlightByModel`, so treating it as a concurrent edit would move its
761
+ // data into `pendingMergeByModel`, remove the transaction, and then mint a
762
+ // fresh follow-up with the attempt counter reset to zero. The counter would
763
+ // never accumulate, `maxRetries` would never trip, and the write would
764
+ // resend in an endless loop.
751
765
  if (transaction.type === 'update' && transaction.attempts === 0) {
752
766
  const preserveWatermark = hasStaleWriteOptions(transaction.writeOptions);
753
767
  // If there is an in-flight update for this model, merge into post-flight buffer
@@ -782,8 +796,9 @@ export class TransactionQueue extends EventEmitter {
782
796
  scheduleProcessing(immediate = false) {
783
797
  if (this.processScheduled)
784
798
  return;
785
- // BACKPRESSURE: Don't schedule if too many transactions are already executing
786
- // This prevents overwhelming the server with concurrent requests (Linear pattern)
799
+ // Backpressure: don't schedule another batch while too many transactions
800
+ // are already executing, so the server is not flooded with concurrent
801
+ // requests.
787
802
  if (this.executingCount >= this.config.maxExecutingTransactions) {
788
803
  getContext().logger.debug('[TransactionQueue] Backpressure: delaying batch, too many executing', {
789
804
  executingCount: this.executingCount,
@@ -810,13 +825,11 @@ export class TransactionQueue extends EventEmitter {
810
825
  }, delay);
811
826
  }
812
827
  /**
813
- * Process batch of transactions using LINEAR-style unified batch execution.
814
- *
815
- * Key optimization: Instead of making separate calls per operation type/model,
816
- * we collect ALL batchable operations and send them in a SINGLE commit call.
817
- * The sync-server handles mixed types atomically inside one transaction.
818
- *
819
- * This reduces N round-trips to 1, dramatically improving batch latency.
828
+ * Processes one batch of transactions in a single commit. Rather than calling
829
+ * the server once per operation type or model, it collects every batchable
830
+ * operation and sends them together; the server applies the mixed operations
831
+ * atomically within one transaction. This turns many round trips into one and
832
+ * greatly reduces batch latency.
820
833
  */
821
834
  async processBatch() {
822
835
  const batchStart = typeof performance !== 'undefined' ? performance.now() : Date.now();
@@ -828,9 +841,9 @@ export class TransactionQueue extends EventEmitter {
828
841
  let batch = [];
829
842
  await getContext().observability.startSpanAsync('sync.batch', 'sync.transaction.batch', async () => {
830
843
  try {
831
- // Sort executionQueue by FK priority before batch selection
832
- // This ensures parent entities (Layout, SlideLayout) are always processed
833
- // before their children (SlideLayoutLayer) across batch boundaries
844
+ // Sort the execution queue by foreign-key priority before selecting a
845
+ // batch, so a parent row is always committed before its children, even
846
+ // across batch boundaries.
834
847
  this.executionQueue.sort((a, b) => {
835
848
  // Ensure derived fields exist (covers restored/persisted transactions)
836
849
  this.ensureDerivedFields(a);
@@ -854,15 +867,15 @@ export class TransactionQueue extends EventEmitter {
854
867
  this.inFlightByModel.add(key);
855
868
  this.store.updateStatus(tx.id, 'executing');
856
869
  }
857
- // Build ALL operations for unified commit (SINGLE WS round-trip)
870
+ // Build every operation for one unified commit (a single round trip).
858
871
  const batchOps = [];
859
872
  for (const tx of batch) {
860
- // Per-op `transactionId` carries the local tx UUID through
861
- // the wire so the server can stamp it on the resulting
862
- // sync delta. The receive path (`SyncClient.applyDeltaBatchToPool`)
863
- // matches it via `OptimisticEchoTracker.consumeEcho` to suppress
864
- // double-applying optimistic mutations. Distinct from the
865
- // batch-level idempotency key in mutation_log.
873
+ // The per-operation `transactionId` carries the local transaction id
874
+ // over the wire so the server can stamp it on the resulting sync
875
+ // delta. The receive path matches it through
876
+ // {@link UnconfirmedWrites.consumeEcho} to avoid applying the
877
+ // client's own optimistic change twice. This is separate from the
878
+ // batch-level idempotency key recorded in `mutation_log`.
866
879
  const op = applyWriteOptions({
867
880
  type: TX_TYPE_TO_MUTATION_OP[tx.type],
868
881
  model: tx.modelKey,
@@ -872,28 +885,27 @@ export class TransactionQueue extends EventEmitter {
872
885
  }, tx);
873
886
  batchOps.push({ tx, op });
874
887
  }
875
- // Execute unified commit for ALL operations (SINGLE WS round-trip)
888
+ // Execute the unified commit for every operation in one round trip.
876
889
  if (batchOps.length > 0) {
877
890
  const operations = batchOps.map(({ op }) => op);
878
891
  try {
879
- // LINEAR PATTERN: Capture lastSyncId from server response for threshold-based confirmation
892
+ // Capture lastSyncId from the server response for threshold-based
893
+ // confirmation.
880
894
  //
881
- // Idempotency note: the default HTTP executor derives a
882
- // stable `Idempotency-Key` from the operations array
883
- // itself (sorted sha256), so retries of the SAME batch
884
- // hit the server's `mutation_log` replay path without
885
- // requiring us to thread a key through the microtask
886
- // boundary here. Keeping this path await-free preserves
887
- // the coalescing test's tight bound on batch count.
895
+ // Idempotency: the default HTTP executor derives a stable
896
+ // idempotency key from the operations array itself (a sorted
897
+ // SHA-256), so retrying the same batch reaches the server's
898
+ // `mutation_log` replay path without threading a key through the
899
+ // microtask boundary here.
888
900
  const result = await this.mutationExecutor.commit(operations);
889
901
  const lastSyncId = result?.lastSyncId ?? 0;
890
902
  this.noteAck(lastSyncId);
891
- // Notify-instead-of-abort: the server returned stale-context
892
- // notifications for `onStale: 'notify'` ops whose premise moved.
893
- // Every notified op was HELD (not written) its optimistic state
894
- // must be rolled back here; no delta will ever confirm it. We also
895
- // stamp the signal so `waitForCommitReceipt` carries it onto the
896
- // receipt.
903
+ // The server returned stale-context notifications for operations
904
+ // marked `onStale: 'notify'` whose read premise had moved. Each
905
+ // notified operation was held rather than written, so its
906
+ // optimistic state must be rolled back here no delta will ever
907
+ // confirm it and the signal is stamped so
908
+ // {@link waitForCommitReceipt} can carry it onto the receipt.
897
909
  const notifications = result?.notifications;
898
910
  const heldIds = new Set((notifications ?? []).map((n) => n.id));
899
911
  for (const { tx } of batchOps) {
@@ -902,15 +914,16 @@ export class TransactionQueue extends EventEmitter {
902
914
  this.commitNotifications.set(tx.id, txNotifs);
903
915
  }
904
916
  }
905
- // Detect server bug: lastSyncId 0 means mutation succeeded but no sync delta was emitted
917
+ // A lastSyncId of 0 means the mutation succeeded but the server
918
+ // emitted no sync delta; record that anomaly for observability.
906
919
  if (lastSyncId === 0) {
907
920
  getContext().observability.captureCommitZeroSyncId({
908
921
  operationCount: operations.length,
909
922
  operations: operations.map((op) => `${op.type}:${op.model}:${op.id?.slice(0, 8) ?? '?'}`),
910
923
  });
911
924
  }
912
- // LINEAR PATTERN: Mark as awaiting_delta with syncId threshold
913
- // Transactions will be confirmed when any delta with id >= lastSyncId arrives
925
+ // Mark each transaction with the sync-id threshold that will
926
+ // confirm it: any delta whose id is at least lastSyncId.
914
927
  for (const { tx } of batchOps) {
915
928
  // Held op ('notify'): the server withheld the write, so no delta
916
929
  // will confirm it. Roll back the optimistic update (server
@@ -942,17 +955,17 @@ export class TransactionQueue extends EventEmitter {
942
955
  });
943
956
  continue;
944
957
  }
945
- // ACK-BASED CONFIRMATION. A successful commit response with a
946
- // real watermark means the server durably applied the write —
947
- // that IS the confirmation (the documented `wait: 'confirmed'`
948
- // contract, and how Replicache/Zero treat the push response's
949
- // lastMutationID). The delta echo is NOT an acknowledgement
950
- // channel: it's replication for OTHER clients, and this
951
- // client's own echo is suppressed by the OptimisticEchoTracker
952
- // anyway. Gating confirmation on the echo coupled "did my
953
- // write land" to subscription-stream health a bare-Node
954
- // client with no live delta stream hung forever in
955
- // `awaiting_delta` on a write the server had already applied.
958
+ // Acknowledgement-based confirmation. A successful commit
959
+ // response carrying a real watermark means the server durably
960
+ // applied the write, and that is the confirmation the
961
+ // `wait: 'confirmed'` contract promises. The delta echo is not
962
+ // an acknowledgement channel: it exists to replicate the change
963
+ // to other clients, and this client's own echo is suppressed by
964
+ // the {@link UnconfirmedWrites} regardless. Waiting on the
965
+ // echo would tie "did my write land?" to the health of the
966
+ // subscription stream, so a client with no live delta stream
967
+ // would wait forever in `awaiting_delta` for a write the server
968
+ // had already applied.
956
969
  if (lastSyncId > 0) {
957
970
  this.store.updateStatus(tx.id, 'completed');
958
971
  this.emit('transaction:completed', tx);
@@ -966,12 +979,11 @@ export class TransactionQueue extends EventEmitter {
966
979
  });
967
980
  }
968
981
  else {
969
- // lastSyncId === 0 on a non-DELETE: the server accepted the
970
- // commit but emitted no delta a server-side anomaly
971
- // (already captured via captureCommitZeroSyncId above). Keep
972
- // the delta-wait + reconciliation timeout for THIS case
973
- // only, so the anomaly surfaces instead of silently
974
- // confirming a write with no watermark.
982
+ // A lastSyncId of 0 on a non-delete: the server accepted the
983
+ // commit but emitted no delta, which is an anomaly (already
984
+ // captured above). Keep the delta wait and reconciliation
985
+ // timeout only for this case, so the anomaly surfaces instead
986
+ // of silently confirming a write that carries no watermark.
975
987
  this.store.updateStatus(tx.id, 'awaiting_delta');
976
988
  getContext().logger.debug('tx:awaiting_delta', {
977
989
  txId: tx.id.slice(0, 8),
@@ -986,11 +998,11 @@ export class TransactionQueue extends EventEmitter {
986
998
  }
987
999
  catch (error) {
988
1000
  const errorMessage = error.message || '';
989
- // Surface the raw server rejection for the whole batch so
990
- // cascaded failures (e.g. Layout FK violation that rolls
991
- // back a 6-op transaction) are attributable to a specific
992
- // cause instead of each op showing as a generic permanent
993
- // error downstream.
1001
+ // Surface the raw server rejection for the whole batch so a
1002
+ // cascaded failure for example a foreign-key violation that
1003
+ // rolls back a multi-operation transaction is attributable to a
1004
+ // specific cause rather than showing as a generic permanent error
1005
+ // on each operation.
994
1006
  const abloErr = error instanceof AbloError ? error : undefined;
995
1007
  // SyncWebSocket attaches a `diagnostics` snapshot to its
996
1008
  // "not connected" / "closed while in flight" rejections.
@@ -1026,16 +1038,17 @@ export class TransactionQueue extends EventEmitter {
1026
1038
  message: errorMessage,
1027
1039
  diagnostics,
1028
1040
  });
1029
- // LINEAR PATTERN: Handle "no rows in result set" gracefully
1030
- // This error means the entity was already deleted - for UPDATE/DELETE ops, this is success
1031
- // The claim was achieved (the data doesn't exist), so treat as completed
1041
+ // Handle "no rows in result set" gracefully: the row was already
1042
+ // deleted, so for update and delete operations this is effectively
1043
+ // success the intended end state already holds and the
1044
+ // transaction is treated as completed.
1032
1045
  if (errorMessage.includes('no rows in result set')) {
1033
1046
  getContext().logger.info('[TransactionQueue] Graceful handling: entity already deleted', {
1034
1047
  batchSize: batchOps.length,
1035
1048
  });
1036
1049
  for (const { tx, op } of batchOps) {
1037
1050
  if (op.type === 'UPDATE' || op.type === 'DELETE') {
1038
- // Entity gone = claim achieved, mark as completed
1051
+ // Row already gone: the intended state holds, mark completed.
1039
1052
  this.store.updateStatus(tx.id, 'completed');
1040
1053
  this.emit('transaction:completed', tx);
1041
1054
  getContext().logger.debug('[TransactionQueue] Orphaned transaction treated as success', {
@@ -1106,22 +1119,23 @@ export class TransactionQueue extends EventEmitter {
1106
1119
  }, { batchSize: this.executionQueue.length + (batch?.length ?? 0) });
1107
1120
  }
1108
1121
  /**
1109
- * LINEAR PATTERN: Confirm all awaiting transactions when delta with syncId >= threshold arrives.
1110
- * This replaces clientMutationId echoing - transactions are confirmed by sync ID threshold.
1111
- * Policy + timeout maps live in the `./deltaConfirmation.js` leaf.
1112
- * @param syncId - The sync ID of the received delta
1122
+ * Confirms every awaiting transaction whose sync-id threshold this delta meets
1123
+ * or exceeds. The confirmation policy and timeout tracking live in
1124
+ * {@link DeltaConfirmationTracker} (`./deltaConfirmation.js`).
1125
+ * @param syncId - The sync id of the received delta.
1113
1126
  */
1114
1127
  onDeltaReceived(syncId) {
1115
1128
  this.deltaConfirmation.onDeltaReceived(syncId);
1116
1129
  }
1117
- // REPLICACHE/POWERSYNC PATTERN: Schedule delta confirmation with retry +
1118
- // reconciliation see DeltaConfirmationTracker in `./deltaConfirmation.js`.
1130
+ // Schedule the retry-and-reconciliation wait for a transaction's confirming
1131
+ // delta; see {@link DeltaConfirmationTracker} in `./deltaConfirmation.js`.
1119
1132
  scheduleDeltaConfirmationTimeout(tx, timeoutMs) {
1120
1133
  this.deltaConfirmation.scheduleDeltaConfirmationTimeout(tx, timeoutMs);
1121
1134
  }
1122
1135
  /**
1123
- * Wait for a transaction to be confirmed via delta echo (Linear pattern)
1124
- * Reuses existing timeout mechanism from scheduleDeltaConfirmationTimeout
1136
+ * Resolves once the given transaction is confirmed and rejects if it fails.
1137
+ * The confirming delta's timeout is handled by
1138
+ * {@link scheduleDeltaConfirmationTimeout}.
1125
1139
  */
1126
1140
  waitForConfirmation(transactionId) {
1127
1141
  return new Promise((resolve, reject) => {
@@ -1148,17 +1162,18 @@ export class TransactionQueue extends EventEmitter {
1148
1162
  this.on(`transaction:failed:${transactionId}`, onFailed);
1149
1163
  });
1150
1164
  }
1151
- // Public: check if a clientMutationId exists in this queue (helps identify self-echo deltas)
1165
+ // Reports whether a client mutation id is known to this queue, which helps
1166
+ // identify a delta as this client's own echo.
1152
1167
  hasClientMutationId(id) {
1153
1168
  return !!this.store.get(id) || this.commitStore.has(id);
1154
1169
  }
1155
1170
  /**
1156
- * Enqueue a raw multi-op atomic commit envelope (the `ablo.commits.create`
1157
- * path). Operations are pre-built by the caller; the queue's job is
1158
- * retry-on-reconnect + idempotent dedup, NOT optimistic apply or FK
1159
- * ordering. Same idempotency key (clientTxId) is dropped on the floor
1160
- * if already in flight server-side `mutation_log` handles cross-session
1161
- * dedup; this guard handles same-session double-enqueue.
1171
+ * Enqueues a pre-built, multi-operation atomic commit the
1172
+ * `ablo.commits.create()` path. The caller supplies the operations; the queue
1173
+ * only retries on reconnect and de-duplicates, and does not apply the change
1174
+ * optimistically or reorder for foreign keys. A duplicate `clientTxId`
1175
+ * already in flight is ignored: the server's `mutation_log` de-duplicates
1176
+ * across sessions, and this guard covers a double-enqueue within one session.
1162
1177
  */
1163
1178
  enqueueCommit(clientTxId, operations, options = {}) {
1164
1179
  if (this.commitStore.has(clientTxId))
@@ -1175,19 +1190,19 @@ export class TransactionQueue extends EventEmitter {
1175
1190
  };
1176
1191
  this.commitStore.set(clientTxId, tx);
1177
1192
  this.commitLane.push(tx);
1178
- // Surface the envelope on its OWN event so the undo stream can record
1179
- // commit-lane writes too (`SyncClient.onLocalTransaction` enriches each
1180
- // operation with pool-captured previous state). Deliberately NOT
1181
- // `transaction:created` that event also feeds the optimistic-echo
1182
- // tracker, and commit-lane ops have no optimistic pool apply to echo.
1193
+ // Emit the envelope on its own event so the undo stream can record
1194
+ // commit-lane writes as well. This deliberately avoids `transaction:created`,
1195
+ // which also feeds the optimistic-echo tracker: commit-lane operations apply
1196
+ // nothing optimistically and so have no echo to suppress.
1183
1197
  this.emit('commit:created', { clientTxId, operations: tx.operations });
1184
1198
  void this.processCommitLane();
1185
1199
  }
1186
1200
  /**
1187
- * Drain pending commit-lane envelopes serially. Transient failures
1188
- * (network, ws_not_ready) leave the head-of-queue tx in `pending` and
1189
- * break reconnect handler re-kicks via `flushOfflineQueue`.
1190
- * Permanent failures emit `transaction:failed:<id>` and drop the tx.
1201
+ * Drains the pending commit-lane envelopes one at a time. A transient
1202
+ * failure, such as a network error, leaves the envelope at the head of the
1203
+ * lane in `pending` and stops; reconnect re-kicks it through
1204
+ * {@link flushOfflineQueue}. A permanent failure emits
1205
+ * `transaction:failed:<id>` and drops the envelope.
1191
1206
  */
1192
1207
  async processCommitLane() {
1193
1208
  if (this.commitProcessing)
@@ -1220,8 +1235,9 @@ export class TransactionQueue extends EventEmitter {
1220
1235
  catch (err) {
1221
1236
  const error = err instanceof Error ? err : new Error(String(err));
1222
1237
  if (!this.isPermanentError(error)) {
1223
- // Transient leave at head, retry on next kick (reconnect or
1224
- // next enqueueCommit). Don't tight-loop while WS is down.
1238
+ // Transient: leave it at the head and retry on the next kick
1239
+ // (reconnect or the next enqueueCommit) rather than tight-looping
1240
+ // while the connection is down.
1225
1241
  tx.status = 'pending';
1226
1242
  getContext().logger.debug('[TransactionQueue] commit lane transient', {
1227
1243
  txId: tx.id.slice(0, 12),
@@ -1233,8 +1249,9 @@ export class TransactionQueue extends EventEmitter {
1233
1249
  tx.status = 'failed';
1234
1250
  tx.error = error;
1235
1251
  this.commitLane.shift();
1236
- // Internal bookkeeping the consumer-facing rejection is emitted via
1237
- // 'transaction:failed' and surfaced by the permanent-error headline → debug.
1252
+ // Internal bookkeeping; the consumer-facing rejection is emitted on
1253
+ // 'transaction:failed' and surfaced by the permanent-error headline,
1254
+ // so this line stays at debug.
1238
1255
  getContext().logger.debug('[TransactionQueue] commit lane permanent error', {
1239
1256
  txId: tx.id.slice(0, 12),
1240
1257
  attempts: tx.attempts,
@@ -1250,10 +1267,10 @@ export class TransactionQueue extends EventEmitter {
1250
1267
  }
1251
1268
  }
1252
1269
  /**
1253
- * Promise-based confirmation for a commit-lane transaction. Resolves
1254
- * with the server-side `lastSyncId` once `mutation_result` lands;
1255
- * rejects on permanent failure. Backs the `wait: 'confirmed'` semantics
1256
- * of `ablo.commits.create()`.
1270
+ * Resolves once a commit-lane transaction is confirmed, returning the server's
1271
+ * `lastSyncId` and any stale-context notifications; rejects on permanent
1272
+ * failure. This backs the `wait: 'confirmed'` semantics of
1273
+ * `ablo.commits.create()`.
1257
1274
  */
1258
1275
  waitForCommitReceipt(clientTxId) {
1259
1276
  // Drain any stale-context notifications stamped for this tx on the ack.
@@ -1296,20 +1313,20 @@ export class TransactionQueue extends EventEmitter {
1296
1313
  return 'order' in data || 'orderKey' in data || 'position' in data;
1297
1314
  }
1298
1315
  /**
1299
- * Determine if an error is transient (retryable) vs permanent (non-retryable).
1316
+ * Classifies an error as transient (worth retrying) or permanent. The
1317
+ * approach is deliberately conservative: only known-transient errors are
1318
+ * retried, and anything unrecognized is treated as permanent so a failing
1319
+ * write cannot loop forever.
1300
1320
  *
1301
- * IMPORTANT: Uses a BLOCKLIST approach for safety - only retry on known transient errors.
1302
- * Any unknown error type defaults to permanent (don't retry) to prevent infinite loops.
1321
+ * Transient (retried):
1322
+ * - Network failures, connection errors, and timeouts.
1323
+ * - Server errors (HTTP 5xx).
1324
+ * - Rate limiting (HTTP 429).
1303
1325
  *
1304
- * Transient errors (will retry):
1305
- * - Network failures, connection errors, timeouts
1306
- * - Server errors (5xx status codes)
1307
- * - Rate limiting (429)
1308
- *
1309
- * Permanent errors (won't retry - includes but not limited to):
1310
- * - Validation errors, constraint violations
1311
- * - Not found, unauthorized, forbidden
1312
- * - Any other business logic error from the server
1326
+ * Permanent (not retried), among others:
1327
+ * - Validation errors and constraint violations.
1328
+ * - Not found, unauthorized, and forbidden.
1329
+ * - Any other business-logic error from the server.
1313
1330
  */
1314
1331
  isPermanentError(error) {
1315
1332
  // Typed connection error (e.g. ws_not_ready, transport timeout) is
@@ -1367,18 +1384,20 @@ export class TransactionQueue extends EventEmitter {
1367
1384
  return true;
1368
1385
  }
1369
1386
  /**
1370
- * Handle transaction failure
1387
+ * Handles a failed transaction: retries transient failures with backoff and
1388
+ * rolls back permanent ones, settling the transaction's confirmation promise
1389
+ * either way.
1371
1390
  */
1372
1391
  async handleFailure(transaction, error) {
1373
1392
  transaction.attempts++;
1374
- // Check if this is a permanent error that should NOT be retried
1393
+ // Check whether this is a permanent error that should not be retried.
1375
1394
  if (this.isPermanentError(error)) {
1376
- // Elevated to warn permanent errors mean user writes were rejected
1377
- // by the server, so the user should be able to see WHY in the
1378
- // console (not just via Sentry). Include the typed AbloError fields
1379
- // so the cause is visible: `type`/`code`/`httpStatus` are what
1380
- // distinguish e.g. FK-violation (AbloValidationError) from auth
1381
- // expiry (AbloAuthenticationError).
1395
+ // Logged at warn: a permanent error means the server rejected the write,
1396
+ // so the developer should see the reason in the console. The typed
1397
+ // AbloError fields (`type`, `code`, `httpStatus`) are included so the
1398
+ // cause is visible for example a foreign-key violation
1399
+ // (AbloValidationError) versus expired authentication
1400
+ // (AbloAuthenticationError).
1382
1401
  try {
1383
1402
  const abloErr = error instanceof AbloError ? error : undefined;
1384
1403
  const details = {
@@ -1407,17 +1426,16 @@ export class TransactionQueue extends EventEmitter {
1407
1426
  const isRepeat = sig === this.lastPermanentErrorSig;
1408
1427
  this.lastPermanentErrorSig = sig;
1409
1428
  const logger = getContext().logger;
1410
- // Two registers, one call site, split by log level (the default
1411
- // consumer logger is gated at `warn`, so `debug` is invisible unless
1412
- // someone sets ABLO_LOG_LEVEL=debug to debug the engine itself):
1413
- // the default-visible line speaks the APP DEVELOPER's language —
1414
- // their verb (`update`), their model, the typed error's own human
1415
- // message, and the wire `code` for grep the way AI SDK / Next.js
1416
- // surface errors. No engine nouns ("TransactionQueue", "permanent",
1417
- // "rolling back") and no JSON dump on this line: those alarm and
1418
- // don't help someone who just installed @abloatai/ablo.
1419
- // the forensic `details` ride a companion `debug` line for whoever
1420
- // is debugging the engine internals.
1429
+ // Two registers from one call site, split by log level (the default
1430
+ // logger is gated at `warn`, so `debug` stays hidden unless
1431
+ // ABLO_LOG_LEVEL=debug is set to inspect the engine):
1432
+ // - the default-visible line speaks the application developer's
1433
+ // language: their verb (such as `update`), their model, the typed
1434
+ // error's own message, and the wire `code` for searching. It uses
1435
+ // no engine jargon and prints no JSON dump, which would alarm
1436
+ // without helping.
1437
+ // - the forensic `details` ride a companion `debug` line for anyone
1438
+ // debugging the engine internals.
1421
1439
  const revertNote = this.config.enableOptimistic
1422
1440
  ? ' The local change was reverted.'
1423
1441
  : '';
@@ -1453,15 +1471,11 @@ export class TransactionQueue extends EventEmitter {
1453
1471
  return;
1454
1472
  }
1455
1473
  if (transaction.attempts < this.config.maxRetries) {
1456
- // Exponential backoff with FULL jitter for EVERY transient retry
1457
- // (AWS-standard shape: `sleep = random(0, min(cap, base × 2^attempt))`
1458
- // see the Exponential Backoff And Jitter analysis). Throttling
1459
- // signals (429/503) get a longer base, mirroring the AWS SDK's
1460
- // transient-vs-throttle split. Previously only 429/503 backed off
1461
- // AND the sleep blocked the whole batch loop — every other failure
1462
- // retried at raw `batchDelay` cadence. The re-enqueue is scheduled
1463
- // (never awaited) so one backing-off transaction can't stall
1464
- // unrelated commits.
1474
+ // Exponential backoff with full jitter on every transient retry:
1475
+ // `sleep = random(0, min(cap, base * 2^attempt))`. Throttling responses
1476
+ // (429/503) use a longer base than other transient errors. The re-enqueue
1477
+ // is scheduled rather than awaited, so one backing-off transaction cannot
1478
+ // stall unrelated commits.
1465
1479
  const { baseMs, capMs } = this.config.retryBackoff;
1466
1480
  let base = baseMs;
1467
1481
  try {
@@ -1493,7 +1507,9 @@ export class TransactionQueue extends EventEmitter {
1493
1507
  }
1494
1508
  }
1495
1509
  /**
1496
- * Conflict resolution
1510
+ * Resolves a conflict against server data using the configured strategy:
1511
+ * last-write-wins rolls the local change back, merge and reject re-enqueue it,
1512
+ * and custom applies the caller's resolver.
1497
1513
  */
1498
1514
  async handleConflict(transaction, serverData) {
1499
1515
  const { strategy, resolver } = this.config.conflictResolution;
@@ -1523,8 +1539,8 @@ export class TransactionQueue extends EventEmitter {
1523
1539
  }
1524
1540
  }
1525
1541
  /**
1526
- * Optimistic updates apply/rollback rules live in `./optimistic.js`;
1527
- * these delegates bind the host-owned ledger + event emitter.
1542
+ * Optimistic updates. The apply and rollback rules live in `./optimisticApply.js`;
1543
+ * these methods bind them to the queue's own tracking map and event emitter.
1528
1544
  */
1529
1545
  applyOptimisticCreate(model, transaction) {
1530
1546
  applyOptimisticCreate(this.optimisticUpdates, this, model, transaction);
@@ -1539,25 +1555,9 @@ export class TransactionQueue extends EventEmitter {
1539
1555
  await rollbackOptimistic(this.optimisticUpdates, this, transaction, reason, error);
1540
1556
  }
1541
1557
  /**
1542
- * Execute individual transaction via the unified commit path
1543
- */
1544
- async executeTransaction(transaction) {
1545
- const { type, modelName, modelId, data } = transaction;
1546
- const schemaName = stripModelSuffix(modelName);
1547
- const mutationType = TX_TYPE_TO_MUTATION_OP[type];
1548
- const model = normalizeModelKey(modelName);
1549
- const input = (type === 'create' || type === 'update') ? data : undefined;
1550
- try {
1551
- await this.mutationExecutor.commit([
1552
- applyWriteOptions({ type: mutationType, model, id: modelId, input }, transaction),
1553
- ]);
1554
- }
1555
- catch (error) {
1556
- handleMutationError(error, `${type}-mutation`, schemaName, modelId);
1557
- }
1558
- }
1559
- /**
1560
- * Persistence
1558
+ * Loads transactions persisted from a previous session and re-enqueues them,
1559
+ * so writes made while offline survive a restart. Does nothing when
1560
+ * persistence is disabled.
1561
1561
  */
1562
1562
  async loadPersistedTransactions(database) {
1563
1563
  if (!this.config.enablePersistence)
@@ -1580,11 +1580,10 @@ export class TransactionQueue extends EventEmitter {
1580
1580
  }
1581
1581
  }
1582
1582
  /**
1583
- * Validate + rehydrate one persisted row (the T1.8 IDB replay boundary).
1584
- * Rows written by other subsystems into the same store (`'queue'`,
1585
- * `'awaiting_delta'`) are skipped silently; rows that fail the
1586
- * persisted-transaction schema (older SDK versions, corruption) are
1587
- * DROPPED with an observability capture instead of replayed as commits.
1583
+ * Validates and rehydrates one persisted row. Rows written to the same store
1584
+ * by other subsystems are skipped, and rows that fail the persisted
1585
+ * transaction schema from an older version or corruption — are dropped and
1586
+ * reported rather than replayed as commits.
1588
1587
  */
1589
1588
  deserializeTransaction(data) {
1590
1589
  if (isNonReplayablePersistedRow(data))
@@ -1606,7 +1605,9 @@ export class TransactionQueue extends EventEmitter {
1606
1605
  return transaction;
1607
1606
  }
1608
1607
  /**
1609
- * Cancel transactions for a specific model
1608
+ * Cancels every pending or executing transaction for a given model id,
1609
+ * optionally limited to one operation type, rolling back their optimistic
1610
+ * state. Returns the cancelled transactions.
1610
1611
  */
1611
1612
  cancelTransactionsForModel(modelId, transactionType) {
1612
1613
  const cancelledTransactions = [];
@@ -1634,16 +1635,14 @@ export class TransactionQueue extends EventEmitter {
1634
1635
  return cancelledTransactions;
1635
1636
  }
1636
1637
  /**
1637
- * LINEAR PATTERN: Cancel transactions for child entities by foreign key
1638
- *
1639
- * Used by SyncedStore for cascade cancellation when a parent is deleted.
1640
- * This keeps FK relationship knowledge in ModelRegistry/SyncedStore,
1641
- * while TransactionQueue just handles the cancellation mechanics.
1638
+ * Cancels pending transactions for child rows that reference a deleted parent,
1639
+ * used to cascade a parent deletion. The caller supplies the foreign-key
1640
+ * relationship; this method performs the cancellation.
1642
1641
  *
1643
- * @param childModelName - The child model type (e.g., 'SlideLayer')
1644
- * @param foreignKey - The FK property name (e.g., 'slideId')
1645
- * @param parentId - The deleted parent's ID
1646
- * @returns Number of transactions cancelled
1642
+ * @param childModelName - The child model type (for example 'SlideLayer').
1643
+ * @param foreignKey - The foreign-key property name (for example 'slideId').
1644
+ * @param parentId - The deleted parent's id.
1645
+ * @returns The number of transactions cancelled.
1647
1646
  */
1648
1647
  cancelTransactionsByForeignKey(childModelName, foreignKey, parentId) {
1649
1648
  let cancelled = 0;
@@ -1677,14 +1676,12 @@ export class TransactionQueue extends EventEmitter {
1677
1676
  return cancelled;
1678
1677
  }
1679
1678
  /**
1680
- * Get count of outstanding transactions
1679
+ * Returns the number of transactions still pending or executing.
1681
1680
  */
1682
1681
  getOutstandingTransactionCount() {
1683
1682
  return this.store.getByStatus('pending').length + this.store.getByStatus('executing').length;
1684
1683
  }
1685
- /**
1686
- * Utilities
1687
- */
1684
+ /** Generates a unique local transaction id. */
1688
1685
  generateId() {
1689
1686
  return `tx_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
1690
1687
  }
@@ -1700,35 +1697,26 @@ export class TransactionQueue extends EventEmitter {
1700
1697
  extractUpdateData(model) {
1701
1698
  return projectCommitPayload(model.getModelName(), model.getChanges(), { dropUndefined: true });
1702
1699
  }
1703
- buildUpdateInput(modelName, changes) {
1704
- return projectCommitPayload(modelName, changes, { dropUndefined: true });
1705
- }
1706
1700
  // Derive previous values for changed fields to support accurate rollback.
1707
- //
1708
- // The previous Slide-specific branch reaching into `_data` was removed:
1709
- // the field name in the comment (`_localChanges`) didn't match the
1710
- // code (`_data`), no `Slide` class still defines either field, and
1711
- // hardcoded model-name checks don't belong in a generic queue. If a
1712
- // model ever needs to surface previous-state outside `modifiedProperties`,
1713
- // expose a typed `getPreviousData()` accessor on Model and call that.
1701
+ // Model-specific special cases do not belong here; a model that needs to
1702
+ // surface previous state beyond `modifiedProperties` should expose a typed
1703
+ // `getPreviousData()` accessor for this method to call.
1714
1704
  extractPreviousData(model, updateInput) {
1715
1705
  // When the update's written keys are known, capture a before-image for
1716
- // EXACTLY those keys so the recorded undo inverse can revert them and only
1717
- // them (a full-row inverse would clobber concurrent edits to unrelated
1718
- // fields). `fallbackToLive: false` makes `Model.capturePreviousValues` OMIT
1719
- // any key it can't resolve from `modifiedProperties.old` / the original
1720
- // snapshot `buildUndoOps` then drops an un-revertible inverse rather than
1721
- // inventing one. With no `updateInput` (full extract) fall back to every
1722
- // tracked field. `Model.capturePreviousValues` is the single before-image
1723
- // source shared with `RecordingTransaction.snapshotFields`.
1706
+ // exactly those keys, so the recorded undo inverse reverts them and nothing
1707
+ // else a full-row inverse would clobber concurrent edits to unrelated
1708
+ // fields. `fallbackToLive: false` makes `Model.capturePreviousValues` omit
1709
+ // any key it cannot resolve, and `buildUndoOps` then drops an un-revertible
1710
+ // inverse rather than inventing one. With no `updateInput` (a full extract)
1711
+ // it falls back to every tracked field. `Model.capturePreviousValues` is the
1712
+ // single before-image source, shared with
1713
+ // `RecordingTransaction.snapshotFields`.
1724
1714
  const keys = updateInput
1725
1715
  ? Object.keys(updateInput)
1726
1716
  : [...(model.modifiedProperties instanceof Map ? model.modifiedProperties.keys() : [])];
1727
1717
  return { id: model.id, ...model.capturePreviousValues(keys, { fallbackToLive: false }) };
1728
1718
  }
1729
- /**
1730
- * Public API
1731
- */
1719
+ /** Returns a snapshot of queue counts and the current configuration. */
1732
1720
  getStats() {
1733
1721
  return {
1734
1722
  pending: this.store.getByStatus('pending').length,
@@ -1742,8 +1730,8 @@ export class TransactionQueue extends EventEmitter {
1742
1730
  };
1743
1731
  }
1744
1732
  /**
1745
- * Get detailed debug info for the sync debug page
1746
- * Exposes internal state that helps diagnose delta confirmation issues
1733
+ * Returns detailed internal state pending, executing, and awaiting-delta
1734
+ * transactions to help diagnose delta-confirmation issues.
1747
1735
  */
1748
1736
  getDebugInfo() {
1749
1737
  const awaitingDelta = this.store.getByStatus('awaiting_delta');
@@ -1773,18 +1761,17 @@ export class TransactionQueue extends EventEmitter {
1773
1761
  })),
1774
1762
  };
1775
1763
  }
1776
- /**
1777
- * Set configuration
1778
- */
1764
+ /** Merges the given options into the queue's configuration. */
1779
1765
  setConfig(config) {
1780
1766
  this.config = { ...this.config, ...config };
1781
1767
  }
1782
1768
  /**
1783
- * Handle incoming sync delta - simplified for permanent IDs
1769
+ * Re-emits an incoming sync delta on the `sync:delta` event for the store to
1770
+ * apply. Because rows use stable ids, no id reconciliation is needed here.
1784
1771
  */
1785
1772
  handleSyncDelta(delta) {
1786
- // With permanent IDs, no reconciliation needed!
1787
- // Just emit the delta for ObjectPool to handle directly
1773
+ // Row ids are stable, so no reconciliation is needed; re-emit the delta for
1774
+ // the store to apply directly.
1788
1775
  this.emit('sync:delta', {
1789
1776
  id: delta.id,
1790
1777
  modelName: delta.modelName,
@@ -1794,7 +1781,8 @@ export class TransactionQueue extends EventEmitter {
1794
1781
  return true;
1795
1782
  }
1796
1783
  /**
1797
- * Cleanup and dispose resources
1784
+ * Releases the queue's resources: rolls back outstanding optimistic updates,
1785
+ * clears all timers and stored transactions, and removes event listeners.
1798
1786
  */
1799
1787
  dispose() {
1800
1788
  // Cancel all active optimistic updates