@abloatai/humans 0.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (404) hide show
  1. package/README.md +52 -0
  2. package/dist/Ablo.d.ts +208 -0
  3. package/dist/Ablo.js +120 -0
  4. package/dist/client.d.ts +317 -0
  5. package/dist/client.js +13 -0
  6. package/dist/core.d.ts +35 -0
  7. package/dist/core.js +48 -0
  8. package/dist/humans.d.ts +28 -0
  9. package/dist/humans.js +34 -0
  10. package/dist/index.d.ts +10 -0
  11. package/dist/index.js +6 -0
  12. package/dist/local/BaseSyncedStore.d.ts +807 -0
  13. package/dist/local/BaseSyncedStore.js +1516 -0
  14. package/dist/local/Database.d.ts +322 -0
  15. package/dist/local/Database.js +1589 -0
  16. package/dist/local/InstanceCache.d.ts +255 -0
  17. package/dist/local/InstanceCache.js +1263 -0
  18. package/dist/local/LazyReferenceCollection.d.ts +177 -0
  19. package/dist/local/LazyReferenceCollection.js +461 -0
  20. package/dist/local/Model.d.ts +475 -0
  21. package/dist/local/Model.js +950 -0
  22. package/dist/local/ModelRegistry.d.ts +225 -0
  23. package/dist/local/ModelRegistry.js +539 -0
  24. package/dist/local/NetworkMonitor.d.ts +28 -0
  25. package/dist/local/NetworkMonitor.js +79 -0
  26. package/dist/local/RuntimeContext.d.ts +52 -0
  27. package/dist/local/RuntimeContext.js +80 -0
  28. package/dist/local/SyncClient.d.ts +516 -0
  29. package/dist/local/SyncClient.js +1754 -0
  30. package/dist/local/adapters/alwaysOnline.d.ts +14 -0
  31. package/dist/local/adapters/alwaysOnline.js +17 -0
  32. package/dist/local/adapters/inMemoryStorage.d.ts +37 -0
  33. package/dist/local/adapters/inMemoryStorage.js +122 -0
  34. package/dist/local/client/clientPrelude.d.ts +52 -0
  35. package/dist/local/client/clientPrelude.js +60 -0
  36. package/dist/local/client/consoleLogger.d.ts +35 -0
  37. package/dist/local/client/consoleLogger.js +44 -0
  38. package/dist/local/client/createInternalComponents.d.ts +50 -0
  39. package/dist/local/client/createInternalComponents.js +98 -0
  40. package/dist/local/client/createModelProxy.d.ts +248 -0
  41. package/dist/local/client/createModelProxy.js +880 -0
  42. package/dist/local/client/modelRegistration.d.ts +10 -0
  43. package/dist/local/client/modelRegistration.js +316 -0
  44. package/dist/local/client/options.d.ts +440 -0
  45. package/dist/local/client/options.js +7 -0
  46. package/dist/local/client/reactiveEngine.d.ts +53 -0
  47. package/dist/local/client/reactiveEngine.js +705 -0
  48. package/dist/local/client/resourceTypes.d.ts +12 -0
  49. package/dist/local/client/resourceTypes.js +10 -0
  50. package/dist/local/client/schemaConfig.d.ts +44 -0
  51. package/dist/local/client/schemaConfig.js +185 -0
  52. package/dist/local/client/storeCluster.d.ts +46 -0
  53. package/dist/local/client/storeCluster.js +133 -0
  54. package/dist/local/client/storeLifecycle.d.ts +61 -0
  55. package/dist/local/client/storeLifecycle.js +236 -0
  56. package/dist/local/client/validateAbloOptions.d.ts +42 -0
  57. package/dist/local/client/validateAbloOptions.js +43 -0
  58. package/dist/local/client/wsMutationExecutor.d.ts +27 -0
  59. package/dist/local/client/wsMutationExecutor.js +72 -0
  60. package/dist/local/context.d.ts +42 -0
  61. package/dist/local/context.js +81 -0
  62. package/dist/local/coordination/ClaimLog.d.ts +26 -0
  63. package/dist/local/coordination/ClaimLog.js +32 -0
  64. package/dist/local/interfaces/index.d.ts +311 -0
  65. package/dist/local/interfaces/index.js +9 -0
  66. package/dist/local/localModelContract.d.ts +14 -0
  67. package/dist/local/localModelContract.js +1 -0
  68. package/dist/local/logPosition.d.ts +31 -0
  69. package/dist/local/logPosition.js +53 -0
  70. package/dist/local/mutationPersistence.d.ts +6 -0
  71. package/dist/local/mutationPersistence.js +1 -0
  72. package/dist/local/mutators/RecordingMutation.d.ts +36 -0
  73. package/dist/local/mutators/RecordingMutation.js +182 -0
  74. package/dist/local/mutators/Transaction.d.ts +40 -0
  75. package/dist/local/mutators/Transaction.js +58 -0
  76. package/dist/local/mutators/UndoManager.d.ts +258 -0
  77. package/dist/local/mutators/UndoManager.js +665 -0
  78. package/dist/local/mutators/defineMutators.d.ts +60 -0
  79. package/dist/local/mutators/defineMutators.js +18 -0
  80. package/dist/local/mutators/inverseOp.d.ts +126 -0
  81. package/dist/local/mutators/inverseOp.js +71 -0
  82. package/dist/local/mutators/mutateActions.d.ts +45 -0
  83. package/dist/local/mutators/mutateActions.js +105 -0
  84. package/dist/local/mutators/readerActions.d.ts +33 -0
  85. package/dist/local/mutators/readerActions.js +57 -0
  86. package/dist/local/mutators/undoApply.d.ts +51 -0
  87. package/dist/local/mutators/undoApply.js +117 -0
  88. package/dist/local/persistence.d.ts +7 -0
  89. package/dist/local/persistence.js +9 -0
  90. package/dist/local/query/QueryProcessor.d.ts +75 -0
  91. package/dist/local/query/QueryProcessor.js +255 -0
  92. package/dist/local/query/client.d.ts +64 -0
  93. package/dist/local/query/client.js +138 -0
  94. package/dist/local/query/types.d.ts +85 -0
  95. package/dist/local/query/types.js +16 -0
  96. package/dist/local/schema/serialize.d.ts +1 -0
  97. package/dist/local/schema/serialize.js +1 -0
  98. package/dist/local/store/queryApi.d.ts +13 -0
  99. package/dist/local/store/queryApi.js +35 -0
  100. package/dist/local/storeContract.d.ts +145 -0
  101. package/dist/local/storeContract.js +12 -0
  102. package/dist/local/stores/DatabaseManager.d.ts +112 -0
  103. package/dist/local/stores/DatabaseManager.js +400 -0
  104. package/dist/local/stores/ObjectStore.d.ts +115 -0
  105. package/dist/local/stores/ObjectStore.js +393 -0
  106. package/dist/local/stores/ObjectStoreContract.d.ts +38 -0
  107. package/dist/local/stores/ObjectStoreContract.js +1 -0
  108. package/dist/local/stores/StoreManager.d.ts +114 -0
  109. package/dist/local/stores/StoreManager.js +304 -0
  110. package/dist/local/stores/SyncActionStore.d.ts +99 -0
  111. package/dist/local/stores/SyncActionStore.js +506 -0
  112. package/dist/local/stores/openIDBWithTimeout.d.ts +65 -0
  113. package/dist/local/stores/openIDBWithTimeout.js +153 -0
  114. package/dist/local/stores/persistenceCleanup.d.ts +7 -0
  115. package/dist/local/stores/persistenceCleanup.js +26 -0
  116. package/dist/local/stores/persistenceIdentity.d.ts +27 -0
  117. package/dist/local/stores/persistenceIdentity.js +38 -0
  118. package/dist/local/stores/syncAction.d.ts +26 -0
  119. package/dist/local/stores/syncAction.js +16 -0
  120. package/dist/local/stores/v1PersistenceDeletion.d.ts +8 -0
  121. package/dist/local/stores/v1PersistenceDeletion.js +16 -0
  122. package/dist/local/sync/BootstrapFetcher.d.ts +284 -0
  123. package/dist/local/sync/BootstrapFetcher.js +964 -0
  124. package/dist/local/sync/ConnectionManager.d.ts +8 -0
  125. package/dist/local/sync/ConnectionManager.js +8 -0
  126. package/dist/local/sync/OnDemandLoader.d.ts +231 -0
  127. package/dist/local/sync/OnDemandLoader.js +743 -0
  128. package/dist/local/sync/SubscriptionManager.d.ts +159 -0
  129. package/dist/local/sync/SubscriptionManager.js +243 -0
  130. package/dist/local/sync/SyncWebSocket.d.ts +173 -0
  131. package/dist/local/sync/SyncWebSocket.js +438 -0
  132. package/dist/local/sync/bootstrapApply.d.ts +73 -0
  133. package/dist/local/sync/bootstrapApply.js +73 -0
  134. package/dist/local/sync/commitFrames.d.ts +8 -0
  135. package/dist/local/sync/commitFrames.js +8 -0
  136. package/dist/local/sync/connectionManagerLifecycle.d.ts +23 -0
  137. package/dist/local/sync/connectionManagerLifecycle.js +126 -0
  138. package/dist/local/sync/contextPorts.d.ts +18 -0
  139. package/dist/local/sync/contextPorts.js +31 -0
  140. package/dist/local/sync/createClaimStream.d.ts +64 -0
  141. package/dist/local/sync/createClaimStream.js +475 -0
  142. package/dist/local/sync/createSnapshot.d.ts +29 -0
  143. package/dist/local/sync/createSnapshot.js +116 -0
  144. package/dist/local/sync/credentialLifecycle.d.ts +7 -0
  145. package/dist/local/sync/credentialLifecycle.js +7 -0
  146. package/dist/local/sync/deltaPipeline.d.ts +116 -0
  147. package/dist/local/sync/deltaPipeline.js +357 -0
  148. package/dist/local/sync/groupChange.d.ts +116 -0
  149. package/dist/local/sync/groupChange.js +244 -0
  150. package/dist/local/sync/initialize.d.ts +27 -0
  151. package/dist/local/sync/initialize.js +137 -0
  152. package/dist/local/sync/participants.d.ts +132 -0
  153. package/dist/local/sync/participants.js +342 -0
  154. package/dist/local/sync/persistedPrefix.d.ts +12 -0
  155. package/dist/local/sync/persistedPrefix.js +22 -0
  156. package/dist/local/sync/reconnect.d.ts +23 -0
  157. package/dist/local/sync/reconnect.js +55 -0
  158. package/dist/local/sync/schemaDrift.d.ts +55 -0
  159. package/dist/local/sync/schemaDrift.js +53 -0
  160. package/dist/local/sync/schemas.d.ts +71 -0
  161. package/dist/local/sync/schemas.js +94 -0
  162. package/dist/local/sync/socketEventWiring.d.ts +31 -0
  163. package/dist/local/sync/socketEventWiring.js +130 -0
  164. package/dist/local/sync/syncCursor.d.ts +40 -0
  165. package/dist/local/sync/syncCursor.js +55 -0
  166. package/dist/local/sync/syncPlan.d.ts +54 -0
  167. package/dist/local/sync/syncPlan.js +50 -0
  168. package/dist/local/sync/terminalSessionLifecycle.d.ts +20 -0
  169. package/dist/local/sync/terminalSessionLifecycle.js +50 -0
  170. package/dist/local/sync/wsFrameHandlers.d.ts +8 -0
  171. package/dist/local/sync/wsFrameHandlers.js +8 -0
  172. package/dist/local/transactions/databaseCommitOutbox.d.ts +15 -0
  173. package/dist/local/transactions/databaseCommitOutbox.js +16 -0
  174. package/dist/local/transactions/localMutation.d.ts +10 -0
  175. package/dist/local/transactions/localMutation.js +37 -0
  176. package/dist/local/transactions/mutations/MutationQueue.d.ts +511 -0
  177. package/dist/local/transactions/mutations/MutationQueue.js +1498 -0
  178. package/dist/local/transactions/mutations/MutationStore.d.ts +20 -0
  179. package/dist/local/transactions/mutations/MutationStore.js +53 -0
  180. package/dist/local/transactions/mutations/UnconfirmedWrites.d.ts +82 -0
  181. package/dist/local/transactions/mutations/UnconfirmedWrites.js +104 -0
  182. package/dist/local/transactions/mutations/batchProcessing.d.ts +64 -0
  183. package/dist/local/transactions/mutations/batchProcessing.js +349 -0
  184. package/dist/local/transactions/mutations/coalesceRules.d.ts +58 -0
  185. package/dist/local/transactions/mutations/coalesceRules.js +140 -0
  186. package/dist/local/transactions/mutations/commitApi.d.ts +19 -0
  187. package/dist/local/transactions/mutations/commitApi.js +74 -0
  188. package/dist/local/transactions/mutations/commitLane.d.ts +70 -0
  189. package/dist/local/transactions/mutations/commitLane.js +140 -0
  190. package/dist/local/transactions/mutations/commitLatency.d.ts +52 -0
  191. package/dist/local/transactions/mutations/commitLatency.js +130 -0
  192. package/dist/local/transactions/mutations/commitPayload.d.ts +165 -0
  193. package/dist/local/transactions/mutations/commitPayload.js +152 -0
  194. package/dist/local/transactions/mutations/commitTransport.d.ts +36 -0
  195. package/dist/local/transactions/mutations/commitTransport.js +104 -0
  196. package/dist/local/transactions/mutations/deltaConfirmation.d.ts +63 -0
  197. package/dist/local/transactions/mutations/deltaConfirmation.js +235 -0
  198. package/dist/local/transactions/mutations/durableCommitRestore.d.ts +17 -0
  199. package/dist/local/transactions/mutations/durableCommitRestore.js +95 -0
  200. package/dist/local/transactions/mutations/durableWriteStore.d.ts +14 -0
  201. package/dist/local/transactions/mutations/durableWriteStore.js +12 -0
  202. package/dist/local/transactions/mutations/executionSelection.d.ts +6 -0
  203. package/dist/local/transactions/mutations/executionSelection.js +42 -0
  204. package/dist/local/transactions/mutations/failureHandling.d.ts +16 -0
  205. package/dist/local/transactions/mutations/failureHandling.js +130 -0
  206. package/dist/local/transactions/mutations/failurePolicy.d.ts +13 -0
  207. package/dist/local/transactions/mutations/failurePolicy.js +48 -0
  208. package/dist/local/transactions/mutations/localMutation.d.ts +58 -0
  209. package/dist/local/transactions/mutations/localMutation.js +75 -0
  210. package/dist/local/transactions/mutations/modelOperations.d.ts +44 -0
  211. package/dist/local/transactions/mutations/modelOperations.js +144 -0
  212. package/dist/local/transactions/mutations/mutationPersistence.d.ts +25 -0
  213. package/dist/local/transactions/mutations/mutationPersistence.js +188 -0
  214. package/dist/local/transactions/mutations/pendingDrain.d.ts +33 -0
  215. package/dist/local/transactions/mutations/pendingDrain.js +112 -0
  216. package/dist/local/transactions/mutations/processingScheduler.d.ts +14 -0
  217. package/dist/local/transactions/mutations/processingScheduler.js +24 -0
  218. package/dist/local/transactions/mutations/queueCoalescing.d.ts +13 -0
  219. package/dist/local/transactions/mutations/queueCoalescing.js +35 -0
  220. package/dist/local/transactions/mutations/replayValidation.d.ts +187 -0
  221. package/dist/local/transactions/mutations/replayValidation.js +164 -0
  222. package/dist/local/transactions/reconnectDrain.d.ts +11 -0
  223. package/dist/local/transactions/reconnectDrain.js +13 -0
  224. package/dist/local/utils/mobxSetup.d.ts +53 -0
  225. package/dist/local/utils/mobxSetup.js +330 -0
  226. package/dist/local/views/QueryView.d.ts +79 -0
  227. package/dist/local/views/QueryView.js +218 -0
  228. package/dist/local/views/ViewRegistry.d.ts +20 -0
  229. package/dist/local/views/ViewRegistry.js +57 -0
  230. package/dist/local/views/incrementalView.d.ts +45 -0
  231. package/dist/local/views/incrementalView.js +69 -0
  232. package/dist/plugin.d.ts +285 -0
  233. package/dist/plugin.js +106 -0
  234. package/dist/presenceStream.d.ts +69 -0
  235. package/dist/presenceStream.js +200 -0
  236. package/dist/react/AbloProvider.d.ts +242 -0
  237. package/dist/react/AbloProvider.js +456 -0
  238. package/dist/react/ClientSideSuspense.d.ts +36 -0
  239. package/dist/react/ClientSideSuspense.js +17 -0
  240. package/dist/react/DefaultFallback.d.ts +24 -0
  241. package/dist/react/DefaultFallback.js +43 -0
  242. package/dist/react/context.d.ts +55 -0
  243. package/dist/react/context.js +29 -0
  244. package/dist/react/createAbloReact.d.ts +50 -0
  245. package/dist/react/createAbloReact.js +48 -0
  246. package/dist/react/internalContext.d.ts +33 -0
  247. package/dist/react/internalContext.js +3 -0
  248. package/dist/react/useAblo.d.ts +96 -0
  249. package/dist/react/useAblo.js +120 -0
  250. package/dist/react/useCurrentUserId.d.ts +2 -0
  251. package/dist/react/useCurrentUserId.js +12 -0
  252. package/dist/react/useErrorListener.d.ts +2 -0
  253. package/dist/react/useErrorListener.js +14 -0
  254. package/dist/react/useMutationFailureListener.d.ts +8 -0
  255. package/dist/react/useMutationFailureListener.js +19 -0
  256. package/dist/react/useMutators.d.ts +56 -0
  257. package/dist/react/useMutators.js +84 -0
  258. package/dist/react/useSyncStatus.d.ts +19 -0
  259. package/dist/react/useSyncStatus.js +37 -0
  260. package/dist/react/useUndoScope.d.ts +34 -0
  261. package/dist/react/useUndoScope.js +73 -0
  262. package/dist/react.d.ts +18 -0
  263. package/dist/react.js +14 -0
  264. package/dist/reactRuntime.d.ts +4 -0
  265. package/dist/reactRuntime.js +2 -0
  266. package/dist/surface.d.ts +36 -0
  267. package/dist/surface.js +77 -0
  268. package/dist/useReactive.d.ts +6 -0
  269. package/dist/useReactive.js +43 -0
  270. package/package.json +119 -0
  271. package/src/Ablo.ts +456 -0
  272. package/src/client.ts +374 -0
  273. package/src/core.ts +104 -0
  274. package/src/humans.ts +61 -0
  275. package/src/index.ts +40 -0
  276. package/src/local/BaseSyncedStore.ts +1991 -0
  277. package/src/local/Database.ts +2052 -0
  278. package/src/local/InstanceCache.ts +1503 -0
  279. package/src/local/LazyReferenceCollection.ts +563 -0
  280. package/src/local/Model.ts +1124 -0
  281. package/src/local/ModelRegistry.ts +762 -0
  282. package/src/local/NetworkMonitor.ts +88 -0
  283. package/src/local/RuntimeContext.ts +141 -0
  284. package/src/local/SyncClient.ts +2131 -0
  285. package/src/local/adapters/alwaysOnline.ts +20 -0
  286. package/src/local/adapters/inMemoryStorage.ts +141 -0
  287. package/src/local/client/clientPrelude.ts +112 -0
  288. package/src/local/client/consoleLogger.ts +60 -0
  289. package/src/local/client/createInternalComponents.ts +163 -0
  290. package/src/local/client/createModelProxy.ts +1390 -0
  291. package/src/local/client/modelRegistration.ts +350 -0
  292. package/src/local/client/options.ts +526 -0
  293. package/src/local/client/reactiveEngine.ts +935 -0
  294. package/src/local/client/resourceTypes.ts +37 -0
  295. package/src/local/client/schemaConfig.ts +194 -0
  296. package/src/local/client/storeCluster.ts +172 -0
  297. package/src/local/client/storeLifecycle.ts +343 -0
  298. package/src/local/client/validateAbloOptions.ts +95 -0
  299. package/src/local/client/wsMutationExecutor.ts +110 -0
  300. package/src/local/context.ts +96 -0
  301. package/src/local/coordination/ClaimLog.ts +39 -0
  302. package/src/local/interfaces/index.ts +468 -0
  303. package/src/local/localModelContract.ts +15 -0
  304. package/src/local/logPosition.ts +84 -0
  305. package/src/local/mutationPersistence.ts +7 -0
  306. package/src/local/mutators/RecordingMutation.ts +222 -0
  307. package/src/local/mutators/Transaction.ts +97 -0
  308. package/src/local/mutators/UndoManager.ts +741 -0
  309. package/src/local/mutators/defineMutators.ts +76 -0
  310. package/src/local/mutators/inverseOp.ts +83 -0
  311. package/src/local/mutators/mutateActions.ts +167 -0
  312. package/src/local/mutators/readerActions.ts +99 -0
  313. package/src/local/mutators/undoApply.ts +141 -0
  314. package/src/local/persistence.ts +16 -0
  315. package/src/local/query/QueryProcessor.ts +347 -0
  316. package/src/local/query/client.ts +197 -0
  317. package/src/local/query/types.ts +102 -0
  318. package/src/local/schema/serialize.ts +1 -0
  319. package/src/local/store/queryApi.ts +56 -0
  320. package/src/local/storeContract.ts +146 -0
  321. package/src/local/stores/DatabaseManager.ts +507 -0
  322. package/src/local/stores/ObjectStore.ts +449 -0
  323. package/src/local/stores/ObjectStoreContract.ts +48 -0
  324. package/src/local/stores/StoreManager.ts +388 -0
  325. package/src/local/stores/SyncActionStore.ts +579 -0
  326. package/src/local/stores/openIDBWithTimeout.ts +195 -0
  327. package/src/local/stores/persistenceCleanup.ts +43 -0
  328. package/src/local/stores/persistenceIdentity.ts +83 -0
  329. package/src/local/stores/syncAction.ts +21 -0
  330. package/src/local/stores/v1PersistenceDeletion.ts +21 -0
  331. package/src/local/sync/BootstrapFetcher.ts +1224 -0
  332. package/src/local/sync/ConnectionManager.ts +15 -0
  333. package/src/local/sync/OnDemandLoader.ts +927 -0
  334. package/src/local/sync/SubscriptionManager.ts +300 -0
  335. package/src/local/sync/SyncWebSocket.ts +584 -0
  336. package/src/local/sync/bootstrapApply.ts +130 -0
  337. package/src/local/sync/commitFrames.ts +16 -0
  338. package/src/local/sync/connectionManagerLifecycle.ts +158 -0
  339. package/src/local/sync/contextPorts.ts +37 -0
  340. package/src/local/sync/createClaimStream.ts +668 -0
  341. package/src/local/sync/createSnapshot.ts +160 -0
  342. package/src/local/sync/credentialLifecycle.ts +18 -0
  343. package/src/local/sync/deltaPipeline.ts +473 -0
  344. package/src/local/sync/groupChange.ts +343 -0
  345. package/src/local/sync/initialize.ts +205 -0
  346. package/src/local/sync/participants.ts +564 -0
  347. package/src/local/sync/persistedPrefix.ts +27 -0
  348. package/src/local/sync/reconnect.ts +88 -0
  349. package/src/local/sync/schemaDrift.ts +86 -0
  350. package/src/local/sync/schemas.ts +118 -0
  351. package/src/local/sync/socketEventWiring.ts +196 -0
  352. package/src/local/sync/syncCursor.ts +62 -0
  353. package/src/local/sync/syncPlan.ts +89 -0
  354. package/src/local/sync/terminalSessionLifecycle.ts +66 -0
  355. package/src/local/sync/wsFrameHandlers.ts +20 -0
  356. package/src/local/transactions/databaseCommitOutbox.ts +33 -0
  357. package/src/local/transactions/localMutation.ts +58 -0
  358. package/src/local/transactions/mutations/MutationQueue.ts +1998 -0
  359. package/src/local/transactions/mutations/MutationStore.ts +65 -0
  360. package/src/local/transactions/mutations/UnconfirmedWrites.ts +133 -0
  361. package/src/local/transactions/mutations/batchProcessing.ts +469 -0
  362. package/src/local/transactions/mutations/coalesceRules.ts +192 -0
  363. package/src/local/transactions/mutations/commitApi.ts +97 -0
  364. package/src/local/transactions/mutations/commitLane.ts +191 -0
  365. package/src/local/transactions/mutations/commitLatency.ts +164 -0
  366. package/src/local/transactions/mutations/commitPayload.ts +281 -0
  367. package/src/local/transactions/mutations/commitTransport.ts +174 -0
  368. package/src/local/transactions/mutations/deltaConfirmation.ts +298 -0
  369. package/src/local/transactions/mutations/durableCommitRestore.ts +128 -0
  370. package/src/local/transactions/mutations/durableWriteStore.ts +21 -0
  371. package/src/local/transactions/mutations/executionSelection.ts +42 -0
  372. package/src/local/transactions/mutations/failureHandling.ts +154 -0
  373. package/src/local/transactions/mutations/failurePolicy.ts +61 -0
  374. package/src/local/transactions/mutations/localMutation.ts +135 -0
  375. package/src/local/transactions/mutations/modelOperations.ts +210 -0
  376. package/src/local/transactions/mutations/mutationPersistence.ts +231 -0
  377. package/src/local/transactions/mutations/pendingDrain.ts +160 -0
  378. package/src/local/transactions/mutations/processingScheduler.ts +35 -0
  379. package/src/local/transactions/mutations/queueCoalescing.ts +45 -0
  380. package/src/local/transactions/mutations/replayValidation.ts +192 -0
  381. package/src/local/transactions/reconnectDrain.ts +24 -0
  382. package/src/local/utils/mobxSetup.ts +388 -0
  383. package/src/local/views/QueryView.ts +311 -0
  384. package/src/local/views/ViewRegistry.ts +61 -0
  385. package/src/local/views/incrementalView.ts +92 -0
  386. package/src/plugin.ts +396 -0
  387. package/src/presenceStream.ts +279 -0
  388. package/src/react/AbloProvider.tsx +744 -0
  389. package/src/react/ClientSideSuspense.tsx +57 -0
  390. package/src/react/DefaultFallback.tsx +60 -0
  391. package/src/react/context.ts +89 -0
  392. package/src/react/createAbloReact.ts +116 -0
  393. package/src/react/internalContext.ts +38 -0
  394. package/src/react/useAblo.ts +280 -0
  395. package/src/react/useCurrentUserId.ts +17 -0
  396. package/src/react/useErrorListener.ts +22 -0
  397. package/src/react/useMutationFailureListener.ts +34 -0
  398. package/src/react/useMutators.ts +184 -0
  399. package/src/react/useSyncStatus.ts +42 -0
  400. package/src/react/useUndoScope.ts +143 -0
  401. package/src/react.ts +69 -0
  402. package/src/reactRuntime.ts +10 -0
  403. package/src/surface.ts +106 -0
  404. package/src/useReactive.ts +51 -0
@@ -0,0 +1,1498 @@
1
+ /**
2
+ * MutationQueue 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.
6
+ *
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.
13
+ */
14
+ import { EventEmitter } from 'events';
15
+ import { v4 as uuid } from 'uuid';
16
+ import { globalRuntime } from '../../context.js';
17
+ import { AbloError, AbloConnectionError, AbloIdempotencyError, AbloNotFoundError, AbloValidationError, errorCodeSpec, } from '@abloatai/transaction/errors';
18
+ import { LogPosition, } from '../../logPosition.js';
19
+ import { mutationCommitResultSchema, } from '@abloatai/transaction/wire/commit';
20
+ import { projectCommitPayload, computePriorityScore, normalizeModelKey,
21
+ // Includes stale guards as well as request identity/audit barriers.
22
+ hasCommitCoalescingBarrier, applyWriteOptions, asTransportError, extractStatusCode, TX_TYPE_TO_MUTATION_OP, } from './commitPayload.js';
23
+ import { MutationStore } from './MutationStore.js';
24
+ import { entityKey, mergeUpdateData, takeUnsentCreateForModel, findCreateBarrierForDelete, deferDeleteUntilCreateSettles, releaseDeferredDeletesForCreate, } from './coalesceRules.js';
25
+ import { DeltaConfirmationTracker } from './deltaConfirmation.js';
26
+ import { deserializePersistedTransaction, isNonReplayablePersistedRow, pendingMutationRecordId, legacyPendingMutationRecordSchema, pendingMutationRecordSchema, persistedMutationSchema, } from './replayValidation.js';
27
+ import { deserializeLegacyPendingMutation, loadPersistedTransactions, persistQueuedTransaction, removePersistedTransaction, settlePersistedFailure, } from './mutationPersistence.js';
28
+ import { createCommitEnvelopeMember, createDurableCommitEnvelope, commitEnvelopeRecordId, durableCommitEnvelopeSchema, } from '@abloatai/transaction/transactions/settlement/commitEnvelope';
29
+ import { stableStringify } from '@abloatai/transaction/utils/json';
30
+ import { createLocalMutationPort, } from './localMutation.js';
31
+ import { dispatchCommitBounded, parseMutationCommitResult, persistDurableCommitAcceptance, removeDurableCommit, sealDurableCommit, } from './commitTransport.js';
32
+ import { processCommitLane, waitForCommitReceipt, } from './commitLane.js';
33
+ import { enqueueCommit } from './commitApi.js';
34
+ import { archive as archiveModel, create as createModel, remove as deleteModel, unarchive as unarchiveModel, update as updateModel, } from './modelOperations.js';
35
+ import { enqueueTransaction } from './queueCoalescing.js';
36
+ import { processBatch } from './batchProcessing.js';
37
+ import { handleFailure } from './failureHandling.js';
38
+ import { handleConflict as resolveConflict, isPermanentError as classifyPermanentError, isDefinitiveRejection as classifyDefinitiveRejection } from './failurePolicy.js';
39
+ import { takeNextExecutionBatch as selectExecutionBatch, takePendingDrainBatch as selectPendingDrainBatch } from './executionSelection.js';
40
+ import { scheduleProcessing as scheduleProcessingExternal } from './processingScheduler.js';
41
+ import { drainPendingSettlements, } from './pendingDrain.js';
42
+ import { restoreDurableCommits as restoreDurableCommitsExternal } from './durableCommitRestore.js';
43
+ export class MutationQueue extends EventEmitter {
44
+ // Keep one hour of clock/network margin inside the server's 24-hour ledger.
45
+ static DURABLE_REPLAY_WINDOW_MS = 23 * 60 * 60 * 1000;
46
+ store = new MutationStore();
47
+ // Signature of the last permanent-error we logged at `warn`. A `create`
48
+ // whose id already exists (`unique_violation`) is a permanent rejection
49
+ // that a pending-work drain re-drives after reconnect/bootstrap — without
50
+ // this, the identical cause prints on a loop. We log the first occurrence
51
+ // and demote exact repeats to `debug`.
52
+ lastPermanentErrorSig;
53
+ // The executor bound to this queue instance, set by `setMutationExecutor(...)`
54
+ // just after construction. When unset it falls back to the ambient executor
55
+ // from `getContext()`.
56
+ //
57
+ // The binding matters because the ambient executor is a module-level
58
+ // singleton: constructing a second client instance overwrites the first
59
+ // instance's executor. Without a per-instance binding, commits on one
60
+ // instance would dispatch through another instance's executor closure; once
61
+ // that other instance disposed its store, the closure would resolve no live
62
+ // connection and every commit here would fail with `ws_not_ready`, which the
63
+ // queue treats as transient and retries endlessly.
64
+ _mutationExecutor = null;
65
+ get mutationExecutor() {
66
+ return this._mutationExecutor ?? this.runtime.mutationExecutor;
67
+ }
68
+ runtime;
69
+ /** Durable transaction journal owned by this queue, before commit sealing. */
70
+ persistence = null;
71
+ deferredMutations = [];
72
+ pendingPersistenceStages = [];
73
+ persistenceStageScheduled = false;
74
+ pendingDrainPromise = null;
75
+ executionQueue = [];
76
+ isProcessing = false;
77
+ processTimer;
78
+ processScheduled = false;
79
+ // Staging area for transactions created in the same event-loop tick. Each one
80
+ // lands here first, then a microtask commits them together.
81
+ createdTransactions = [];
82
+ commitScheduled = false;
83
+ // Per-model in-flight tracking and merge buffer
84
+ inFlightByModel = new Set();
85
+ pendingMergeByModel = new Map();
86
+ deferredDeletesByCreate = new Map();
87
+ // Commit lane: pre-built atomic multi-op envelopes from `ablo.commits.create()`.
88
+ // Drained serially (one envelope at a time) since each is atomic; no
89
+ // coalescing with model-proxy transactions.
90
+ commitLane = [];
91
+ commitStore = new Map();
92
+ /**
93
+ * Small race buffer for authoritative echoes that arrive before the queued
94
+ * mutation receipt. The forward and WAL stream are independent channels, so
95
+ * either can win without changing the settlement result.
96
+ */
97
+ recentDeltaCorrelations = new Map();
98
+ /**
99
+ * Client-facing confirmation deadlines for source-forwarded writes. These
100
+ * timers settle only the caller waiting for `confirmed`; the accepted write
101
+ * remains in `awaiting_delta`, with its durable envelope intact, until the
102
+ * authoritative WAL echo arrives.
103
+ */
104
+ replicationLagTimeouts = new Map();
105
+ replicationLagErrors = new Map();
106
+ commitProcessing = false;
107
+ lastCommitSequence = 0;
108
+ durableReplayBlock = null;
109
+ /** Browser-backed strict outbox; absent for standalone/in-memory consumers. */
110
+ commitOutbox = null;
111
+ commitOutboxScope = null;
112
+ get commitTransportContext() {
113
+ return {
114
+ runtime: this.runtime,
115
+ config: {
116
+ enablePersistence: this.config.enablePersistence,
117
+ commitDispatchTimeoutMs: this.config.commitDispatchTimeoutMs,
118
+ },
119
+ commitOutbox: this.commitOutbox,
120
+ commitOutboxScope: this.commitOutboxScope,
121
+ mutationExecutor: this.mutationExecutor,
122
+ emitCommitLifecycle: (event, payload) => this.emitCommitLifecycle(event, payload),
123
+ };
124
+ }
125
+ get commitLaneContext() {
126
+ return {
127
+ runtime: this.runtime,
128
+ config: { maxRetries: this.config.maxRetries },
129
+ commitLane: this.commitLane,
130
+ commitNotifications: this.commitNotifications,
131
+ commitMissingIds: this.commitMissingIds,
132
+ commitProcessing: this.commitProcessing,
133
+ setCommitProcessing: (value) => { this.commitProcessing = value; },
134
+ durableReplayBlock: this.durableReplayBlock,
135
+ sealDurableCommit: (input) => this.sealDurableCommit(input),
136
+ assertEnvelopeInsideReplayWindow: (envelope) => this.assertEnvelopeInsideReplayWindow(envelope),
137
+ dispatchCommit: async (envelope) => this.parseMutationCommitResult(await this.dispatchCommitBounded(envelope.operations, {
138
+ idempotencyKey: envelope.idempotencyKey,
139
+ ...(envelope.commitOptions.reads ? { reads: envelope.commitOptions.reads } : {}),
140
+ ...(envelope.commitOptions.track ? { track: envelope.commitOptions.track } : {}),
141
+ })),
142
+ persistDurableCommitAcceptance: (envelope, result) => this.persistDurableCommitAcceptance(envelope, result),
143
+ removeDurableCommit: (idempotencyKey) => this.removeDurableCommit(idempotencyKey),
144
+ queuedCommitEchoSyncId: (transaction) => this.queuedCommitEchoSyncId(transaction),
145
+ completeQueuedCommit: (transaction, syncId) => this.completeQueuedCommit(transaction, syncId),
146
+ scheduleReplicationLagTimeout: (transactionId, clientTxId, correlationId) => this.scheduleReplicationLagTimeout(transactionId, clientTxId, correlationId),
147
+ noteAck: (syncId) => this.noteAck(syncId),
148
+ isDefinitiveRejection: (error) => this.isDefinitiveRejection(error),
149
+ isPermanentError: (error) => this.isPermanentError(error),
150
+ emitCommitLifecycle: (event, payload) => this.emitCommitLifecycle(event, payload),
151
+ };
152
+ }
153
+ get commitReceiptContext() {
154
+ return {
155
+ commitStore: this.commitStore,
156
+ commitNotifications: this.commitNotifications,
157
+ commitMissingIds: this.commitMissingIds,
158
+ replicationLagErrors: this.replicationLagErrors,
159
+ on: (event, listener) => this.on(event, listener),
160
+ off: (event, listener) => this.off(event, listener),
161
+ };
162
+ }
163
+ get commitApiContext() {
164
+ return {
165
+ assertDurableReplayOpen: () => this.assertDurableReplayOpen(),
166
+ commitStore: this.commitStore,
167
+ commitLane: this.commitLane,
168
+ replicationLagErrors: this.replicationLagErrors,
169
+ clearReplicationLagState: (transactionId) => this.clearReplicationLagState(transactionId),
170
+ nextCommitSequence: () => this.nextCommitSequence(),
171
+ sealDurableCommit: (input) => this.sealDurableCommit(input),
172
+ processCommitLane: () => this.processCommitLane(),
173
+ emitCommitLifecycle: (event, payload) => this.emitCommitLifecycle(event, payload),
174
+ };
175
+ }
176
+ get modelMutationContext() {
177
+ return {
178
+ enableOptimistic: this.config.enableOptimistic,
179
+ persistenceReady: !!this.persistence && !!this.commitOutboxScope,
180
+ assertDurableReplayOpen: () => this.assertDurableReplayOpen(),
181
+ generateId: () => this.generateId(),
182
+ normalizeModelKey,
183
+ computePriorityScore: (type, modelName) => this.computePriorityScore(type, modelName),
184
+ extractCreateData: (model) => this.extractCreateData(model),
185
+ extractUpdateData: (model) => this.extractUpdateData(model),
186
+ extractPreviousData: (model, input) => this.extractPreviousData(model, input),
187
+ mapChangesToInput: (modelName, changes) => this.mapChangesToInput(modelName, changes),
188
+ isReorderPayload: (input) => this.isReorderPayload(input),
189
+ attachConfirmation: (transaction) => this.attachConfirmation(transaction),
190
+ add: (transaction) => this.store.add(transaction),
191
+ applyOptimisticCreate: (model, transaction) => this.applyOptimisticCreate(model, transaction),
192
+ applyOptimisticUpdate: (model, transaction) => this.applyOptimisticUpdate(model, transaction),
193
+ applyOptimisticDelete: (model, transaction) => this.applyOptimisticDelete(model, transaction),
194
+ takeUnsentCreateForModel: (modelName, modelId) => this.takeUnsentCreateForModel(modelName, modelId),
195
+ cancelUnsentCreateForDelete: (transaction) => this.cancelUnsentCreateForDelete(transaction),
196
+ completeLocalDelete: (model, context, writeOptions, sourceMutationIds) => this.completeLocalDelete(model, context, writeOptions, sourceMutationIds),
197
+ cancelTransactionsForModel: (modelId, type) => this.cancelTransactionsForModel(modelId, type),
198
+ pendingMergeByModel: this.pendingMergeByModel,
199
+ inFlightByModel: this.inFlightByModel,
200
+ findCreateBarrierForDelete: (modelName, modelId) => this.findCreateBarrierForDelete(modelName, modelId),
201
+ deferDeleteUntilCreateSettles: (create, transaction) => this.deferDeleteUntilCreateSettles(create, transaction),
202
+ logger: this.runtime.logger,
203
+ persistAndStage: (transaction, modelData) => this.persistAndStage(transaction, modelData),
204
+ persistQueuedTransaction: (transaction, modelData) => this.persistQueuedTransaction(transaction, modelData),
205
+ stageTransaction: (transaction) => this.stageTransaction(transaction),
206
+ emit: (event, payload) => this.emit(event, payload),
207
+ };
208
+ }
209
+ get queueCoalescingContext() {
210
+ return {
211
+ executionQueue: this.executionQueue,
212
+ inFlightByModel: this.inFlightByModel,
213
+ pendingMergeByModel: this.pendingMergeByModel,
214
+ ensureDerivedFields: (transaction) => this.ensureDerivedFields(transaction),
215
+ scheduleProcessing: (immediate) => this.scheduleProcessing(immediate),
216
+ storeRemove: (transactionId) => this.store.remove(transactionId),
217
+ };
218
+ }
219
+ get batchProcessingContext() {
220
+ return {
221
+ runtime: this.runtime,
222
+ config: this.config,
223
+ durableReplayBlock: this.durableReplayBlock,
224
+ executionQueue: this.executionQueue,
225
+ isProcessing: this.isProcessing,
226
+ setIsProcessing: (value) => { this.isProcessing = value; },
227
+ takeNextExecutionBatch: () => this.takeNextExecutionBatch(),
228
+ ensureDerivedFields: (transaction) => this.ensureDerivedFields(transaction),
229
+ ensureCommitEnvelope: (batch) => this.ensureCommitEnvelope([...batch]),
230
+ executingCount: this.executingCount,
231
+ setExecutingCount: (value) => { this.executingCount = value; },
232
+ inFlightByModel: this.inFlightByModel,
233
+ pendingMergeByModel: this.pendingMergeByModel,
234
+ generateId: () => this.generateId(),
235
+ computePriorityScore: (type, modelName) => this.computePriorityScore(type, modelName),
236
+ store: this.store,
237
+ enqueue: (transaction) => this.enqueue(transaction),
238
+ optimisticUpdates: this.localMutationPort.updates,
239
+ commitNotifications: this.commitNotifications,
240
+ commitMissingIds: this.commitMissingIds,
241
+ sourceMutationIdsFor: (batch) => this.sourceMutationIdsFor(batch),
242
+ dispatchCommitBounded: (...args) => this.dispatchCommitBounded(...args),
243
+ parseMutationCommitResult: (value) => this.parseMutationCommitResult(value),
244
+ persistDurableCommitAcceptance: (envelope, result) => this.persistDurableCommitAcceptance(envelope, result),
245
+ removeDurableCommit: (idempotencyKey) => this.removeDurableCommit(idempotencyKey),
246
+ assertEnvelopeInsideReplayWindow: (envelope) => this.assertEnvelopeInsideReplayWindow(envelope),
247
+ sealDurableCommit: (input) => this.sealDurableCommit(input),
248
+ noteAck: (syncId) => this.noteAck(syncId),
249
+ classifyReceiptNotifications: (operations, notifications) => this.classifyReceiptNotifications(operations, notifications),
250
+ receiptTargetKey: (modelName, modelId) => this.receiptTargetKey(modelName, modelId),
251
+ scheduleReplicationLagTimeout: (transactionId, clientTxId, correlationId) => this.scheduleReplicationLagTimeout(transactionId, clientTxId, correlationId),
252
+ scheduleDeltaConfirmationTimeout: (transaction, timeoutMs) => this.scheduleDeltaConfirmationTimeout(transaction, timeoutMs),
253
+ clearReplicationLagState: (transactionId) => this.clearReplicationLagState(transactionId),
254
+ completeQueuedCommit: (transaction, syncId) => this.completeQueuedCommit(transaction, syncId),
255
+ queuedCommitMatchesCorrelation: (transaction, correlationId) => this.queuedCommitMatchesCorrelation(transaction, correlationId),
256
+ recentDeltaCorrelations: this.recentDeltaCorrelations,
257
+ lastSeenSyncId: this.lastSeenSyncId,
258
+ scheduleProcessing: (immediate) => this.scheduleProcessing(immediate),
259
+ handleFailure: (transaction, error) => this.handleFailure(transaction, error),
260
+ isDefinitiveRejection: (error) => this.isDefinitiveRejection(error),
261
+ emitCommitLifecycle: (event, payload) => this.emitCommitLifecycle(event, payload),
262
+ emit: (event, payload) => this.emit(event, payload),
263
+ rollbackOptimistic: (transaction, reason, error) => this.rollbackOptimistic(transaction, reason, error),
264
+ };
265
+ }
266
+ get failureHandlingContext() {
267
+ return {
268
+ runtime: this.runtime,
269
+ config: this.config,
270
+ store: this.store,
271
+ isPermanentError: (error) => this.isPermanentError(error),
272
+ rollbackOptimistic: (transaction, reason, error) => this.rollbackOptimistic(transaction, reason, error),
273
+ enqueue: (transaction) => this.enqueue(transaction),
274
+ getLastPermanentErrorSignature: () => this.lastPermanentErrorSig,
275
+ setLastPermanentErrorSignature: (signature) => { this.lastPermanentErrorSig = signature; },
276
+ emit: (event, payload) => this.emit(event, payload),
277
+ };
278
+ }
279
+ get conflictPolicyContext() {
280
+ return {
281
+ config: this.config,
282
+ store: this.store,
283
+ rollbackOptimistic: (transaction, reason) => this.rollbackOptimistic(transaction, reason),
284
+ mergeData: (local, remote) => this.mergeData(local, remote),
285
+ enqueue: (transaction) => this.enqueue(transaction),
286
+ };
287
+ }
288
+ get processingSchedulerContext() {
289
+ return {
290
+ processScheduled: this.processScheduled,
291
+ setProcessScheduled: (value) => { this.processScheduled = value; },
292
+ processTimer: this.processTimer,
293
+ setProcessTimer: (timer) => { this.processTimer = timer; },
294
+ executingCount: this.executingCount,
295
+ maxExecutingTransactions: this.config.maxExecutingTransactions,
296
+ batchDelay: this.config.batchDelay,
297
+ processBatch: () => { void this.processBatch(); },
298
+ logger: this.runtime.logger,
299
+ };
300
+ }
301
+ get pendingDrainContext() {
302
+ return {
303
+ runtime: this.runtime,
304
+ config: { deltaConfirmationTimeout: this.config.deltaConfirmationTimeout },
305
+ store: this.store,
306
+ executionQueue: this.executionQueue,
307
+ optimisticUpdates: this.localMutationPort.updates,
308
+ assertDurableReplayOpen: () => this.assertDurableReplayOpen(),
309
+ processCommitLane: () => this.processCommitLane(),
310
+ takePendingDrainBatch: (pending) => this.takePendingDrainBatch(pending),
311
+ ensureCommitEnvelope: (batch) => this.ensureCommitEnvelope(batch),
312
+ ensureDerivedFields: (transaction) => this.ensureDerivedFields(transaction),
313
+ sourceMutationIdsFor: (batch) => this.sourceMutationIdsFor(batch),
314
+ sealDurableCommit: (input) => this.sealDurableCommit(input),
315
+ assertEnvelopeInsideReplayWindow: (envelope) => this.assertEnvelopeInsideReplayWindow(envelope),
316
+ parseMutationCommitResult: (value) => this.parseMutationCommitResult(value),
317
+ dispatchCommitBounded: (...args) => this.dispatchCommitBounded(...args),
318
+ persistDurableCommitAcceptance: (envelope, result) => this.persistDurableCommitAcceptance(envelope, result),
319
+ removeDurableCommit: (idempotencyKey) => this.removeDurableCommit(idempotencyKey),
320
+ scheduleReplicationLagTimeout: (transactionId, clientTxId, correlationId) => this.scheduleReplicationLagTimeout(transactionId, clientTxId, correlationId),
321
+ scheduleDeltaConfirmationTimeout: (transaction, timeoutMs) => this.scheduleDeltaConfirmationTimeout(transaction, timeoutMs),
322
+ enqueue: (transaction) => this.enqueue(transaction),
323
+ recentDeltaCorrelations: this.recentDeltaCorrelations,
324
+ emit: (event, payload) => this.emit(event, payload),
325
+ };
326
+ }
327
+ get durableCommitRestoreContext() {
328
+ return {
329
+ config: this.config,
330
+ commitOutbox: this.commitOutbox,
331
+ commitOutboxScope: this.commitOutboxScope,
332
+ commitStore: this.commitStore,
333
+ commitLane: this.commitLane,
334
+ runtime: this.runtime,
335
+ processCommitLane: () => this.processCommitLane(),
336
+ durableReplayWindowMs: MutationQueue.DURABLE_REPLAY_WINDOW_MS,
337
+ };
338
+ }
339
+ get persistenceContext() {
340
+ return {
341
+ runtime: this.runtime,
342
+ persistence: this.persistence,
343
+ commitOutboxScope: this.commitOutboxScope,
344
+ config: this.config,
345
+ store: this.store,
346
+ enqueue: (transaction) => this.enqueue(transaction),
347
+ computePriorityScore: (type, modelName) => this.computePriorityScore(type, modelName),
348
+ deserializeTransaction: (data) => this.deserializeTransaction(data),
349
+ };
350
+ }
351
+ nextCommitSequence() {
352
+ const wallSequence = Date.now() * 1_000;
353
+ this.lastCommitSequence = Math.max(wallSequence, this.lastCommitSequence + 1);
354
+ return this.lastCommitSequence;
355
+ }
356
+ emitCommitLifecycle(event, payload) {
357
+ try {
358
+ this.emit(event, payload);
359
+ }
360
+ catch (error) {
361
+ this.runtime.observability.captureMutationFailure({
362
+ context: `commit-lifecycle-listener:${event}`,
363
+ error: error instanceof Error ? error : String(error),
364
+ });
365
+ }
366
+ }
367
+ assertDurableReplayOpen() {
368
+ if (this.durableReplayBlock)
369
+ throw this.durableReplayBlock;
370
+ }
371
+ assertEnvelopeInsideReplayWindow(envelope) {
372
+ this.assertDurableReplayOpen();
373
+ if (envelope.acceptedAt === undefined &&
374
+ Date.now() - envelope.sealedAt >=
375
+ MutationQueue.DURABLE_REPLAY_WINDOW_MS) {
376
+ this.durableReplayBlock = new AbloIdempotencyError('A pending commit is older than the server idempotency window; newer writes are blocked until it is reviewed.', { code: 'idempotency_conflict' });
377
+ // This gate stops EVERY subsequent write on this client, and each of
378
+ // those rejections is captured to observability rather than surfaced to
379
+ // the caller — without this line the session degrades into "nothing
380
+ // saves and nothing errors". One loud line at the moment the block
381
+ // engages is the only visible trace.
382
+ this.runtime.logger.warn('sync paused: a saved write from an earlier session is older than the server replay window, so newer writes are held until it is reviewed', { sealedAt: envelope.sealedAt });
383
+ throw this.durableReplayBlock;
384
+ }
385
+ }
386
+ computePriorityScore(type, modelName) {
387
+ return computePriorityScore(type, modelName, this.runtime);
388
+ }
389
+ ensureDerivedFields(transaction) {
390
+ if (!transaction.modelKey) {
391
+ transaction.modelKey = normalizeModelKey(transaction.modelName);
392
+ }
393
+ if (transaction.priorityScore === undefined) {
394
+ transaction.priorityScore = this.computePriorityScore(transaction.type, transaction.modelName);
395
+ }
396
+ }
397
+ entityKey(modelName, modelId) {
398
+ return entityKey(modelName, modelId);
399
+ }
400
+ /** Collision-safe receipt target identity across models sharing a row id. */
401
+ receiptTargetKey(modelName, modelId) {
402
+ return stableStringify([normalizeModelKey(modelName), modelId]);
403
+ }
404
+ /**
405
+ * Relates stale notifications back to write targets without assuming the
406
+ * server's canonical model name uses the same spelling as the public schema
407
+ * key (`Task` versus `tasks`). Exact `(model,id)` wins; a globally unique id
408
+ * is the compatibility fallback. An ambiguous same-id cross-model mismatch
409
+ * is deliberately left unclassified, so it cannot falsely settle a queued
410
+ * write. A notification with no write-target id (or an explicit group) is a
411
+ * declared-read conflict and holds the whole batch.
412
+ */
413
+ classifyReceiptNotifications(operations, notifications) {
414
+ const targets = operations.map((operation) => ({
415
+ id: operation.id,
416
+ key: this.receiptTargetKey(operation.model, operation.id),
417
+ }));
418
+ const heldTargets = new Set();
419
+ const notificationsByTarget = new Map();
420
+ let holdsEntireBatch = false;
421
+ for (const notification of notifications) {
422
+ const candidates = targets.filter((target) => target.id === notification.id);
423
+ const notificationKey = this.receiptTargetKey(notification.model, notification.id);
424
+ const exactTargets = candidates.filter((target) => target.key === notificationKey);
425
+ const candidateKeys = new Set((exactTargets.length > 0 ? exactTargets : candidates).map((target) => target.key));
426
+ if (notification.group || candidates.length === 0) {
427
+ holdsEntireBatch = true;
428
+ continue;
429
+ }
430
+ if (candidateKeys.size !== 1) {
431
+ // Same id across multiple differently named models with no exact match:
432
+ // the id-only compatibility fallback is ambiguous. Await the echo.
433
+ continue;
434
+ }
435
+ const [targetKey] = candidateKeys;
436
+ if (!targetKey)
437
+ continue;
438
+ heldTargets.add(targetKey);
439
+ const targetNotifications = notificationsByTarget.get(targetKey) ?? [];
440
+ targetNotifications.push(notification);
441
+ notificationsByTarget.set(targetKey, targetNotifications);
442
+ }
443
+ return { holdsEntireBatch, heldTargets, notificationsByTarget };
444
+ }
445
+ resolveConfirmation(transaction) {
446
+ const resolver = this.confirmationResolvers.get(transaction.id);
447
+ if (!resolver)
448
+ return;
449
+ this.confirmationResolvers.delete(transaction.id);
450
+ resolver.resolve();
451
+ }
452
+ takeUnsentCreateForModel(modelName, modelId) {
453
+ return takeUnsentCreateForModel(this.createdTransactions, this.executionQueue, this.store, modelName, modelId);
454
+ }
455
+ async cancelUnsentCreateForDelete(transaction) {
456
+ this.store.updateStatus(transaction.id, 'rolled_back');
457
+ if (this.config.enableOptimistic) {
458
+ await this.rollbackOptimistic(transaction, 'model_cancelled');
459
+ }
460
+ this.resolveConfirmation(transaction);
461
+ }
462
+ findCreateBarrierForDelete(modelName, modelId) {
463
+ return findCreateBarrierForDelete(this.store, modelName, modelId);
464
+ }
465
+ completeLocalDelete(model, context, writeOptions, sourceMutationIds = []) {
466
+ const actualModelName = model.getModelName();
467
+ const modelKey = normalizeModelKey(actualModelName);
468
+ const transaction = {
469
+ id: this.generateId(),
470
+ type: 'delete',
471
+ modelName: actualModelName,
472
+ modelId: model.id,
473
+ modelKey,
474
+ priorityScore: this.computePriorityScore('delete', actualModelName),
475
+ previousData: model.toJSON ? model.toJSON() : { ...model },
476
+ context,
477
+ status: 'completed',
478
+ createdAt: Date.now(),
479
+ attempts: 0,
480
+ priority: 'high',
481
+ writeOptions,
482
+ ...(sourceMutationIds.length > 0
483
+ ? { sourceMutationIds: [...new Set(sourceMutationIds)] }
484
+ : {}),
485
+ localOnly: true,
486
+ };
487
+ this.attachConfirmation(transaction);
488
+ this.store.add(transaction);
489
+ if (this.config.enableOptimistic) {
490
+ this.applyOptimisticDelete(model, transaction);
491
+ }
492
+ this.emit('transaction:created', transaction);
493
+ this.emit('transaction:completed', transaction);
494
+ this.emit(`transaction:completed:${transaction.id}`, transaction);
495
+ this.localMutationPort.updates.delete(transaction.id);
496
+ return transaction;
497
+ }
498
+ deferDeleteUntilCreateSettles(createTransaction, deleteTransaction) {
499
+ deferDeleteUntilCreateSettles(this.deferredDeletesByCreate, createTransaction, deleteTransaction);
500
+ }
501
+ releaseDeferredDeletesForCreate(createTransaction) {
502
+ releaseDeferredDeletesForCreate(this.deferredDeletesByCreate, this.store, (tx) => { this.enqueue(tx); }, createTransaction);
503
+ }
504
+ // Default configuration, tuned so more operations coalesce into a single
505
+ // commit: a larger batch size and delay give rapid operations more time to
506
+ // merge before the batch is sent.
507
+ config = {
508
+ maxBatchSize: 50, // send up to this many operations per commit
509
+ batchDelay: 150, // milliseconds to wait for more operations before sending
510
+ maxRetries: 3,
511
+ conflictResolution: {
512
+ strategy: 'last-write-wins',
513
+ },
514
+ enablePersistence: true,
515
+ enableOptimistic: true,
516
+ maxExecutingTransactions: 100,
517
+ deltaConfirmationTimeout: 30000,
518
+ retryBackoff: { baseMs: 200, capMs: 1500 },
519
+ commitOfflineGraceMs: 30_000,
520
+ commitDispatchTimeoutMs: 30_000,
521
+ };
522
+ // Track executing transactions for backpressure
523
+ executingCount = 0;
524
+ localMutationPort;
525
+ // Stale-context notifications, keyed by transaction id. When the server
526
+ // accepts a commit but reports that an operation's premise had moved,
527
+ // the notification lands here from the commit acknowledgement and is drained
528
+ // by `waitForCommitReceipt`, so the receipt can carry it back to the caller.
529
+ commitNotifications = new Map();
530
+ /** Zero-row targets returned on a successful atomic commit receipt. */
531
+ commitMissingIds = new Map();
532
+ // Delta-confirmation tracking (ack watermark advance + the awaiting_delta
533
+ // timeout/retry maps) lives in `./deltaConfirmation.js`. Constructed in the
534
+ // constructor once `position` is bound.
535
+ deltaConfirmation;
536
+ // Connection-state check, supplied by the client, used to hold off rollbacks
537
+ // while disconnected.
538
+ isConnectedFn = () => true;
539
+ // Grace timer that, when fired, fails any commit-lane transaction
540
+ // still awaiting an ack. Started on `setConnectionState('disconnected')`,
541
+ // cleared on `'connected'`. The reconnect-retry behavior of the queue
542
+ // is preserved for brief blips; this only catches persistent disconnects.
543
+ commitOfflineGraceTimer = null;
544
+ /**
545
+ * This client's place in the global order of sync deltas. The instance is
546
+ * shared: the client injects one, and a standalone queue creates its own. The
547
+ * queue advances the `acked` cursor as commit responses arrive, the store
548
+ * advances `applied` and `persisted`, and snapshots and claims read
549
+ * `readFloor`. See `../logPosition.js` for the full contract.
550
+ */
551
+ position;
552
+ /** Applied-cursor alias, kept so the many internal read sites stay legible. */
553
+ get lastSeenSyncId() {
554
+ return this.position.applied;
555
+ }
556
+ noteAck(lastSyncId) {
557
+ this.deltaConfirmation.noteAck(lastSyncId);
558
+ }
559
+ // Batch management
560
+ batchIndex = 0;
561
+ /** Mints the request identity once; retry paths only read the stored value. */
562
+ generateCommitIdempotencyKey() {
563
+ return `commit_${uuid()}`;
564
+ }
565
+ /**
566
+ * Binds an ordered transaction batch to one wire-level idempotency key.
567
+ * Existing envelopes are validated and restored to their original order;
568
+ * they are never extended with newly queued work.
569
+ */
570
+ ensureCommitEnvelope(batch) {
571
+ const firstTransaction = batch[0];
572
+ if (!firstTransaction) {
573
+ throw new Error('Cannot create an idempotency envelope for an empty commit');
574
+ }
575
+ const existingKeys = new Set(batch
576
+ .map((tx) => tx.commitEnvelope?.idempotencyKey)
577
+ .filter((key) => key !== undefined));
578
+ if (existingKeys.size > 1) {
579
+ throw new Error('Cannot combine transactions from different commit envelopes');
580
+ }
581
+ const existingKey = existingKeys.values().next().value;
582
+ if (existingKey) {
583
+ const expectedCount = firstTransaction.commitEnvelope?.operationCount;
584
+ const indexes = new Set(batch.map((tx) => tx.commitEnvelope?.operationIndex));
585
+ const isCompleteEnvelope = expectedCount === batch.length &&
586
+ indexes.size === batch.length &&
587
+ batch.every((tx) => tx.commitEnvelope?.idempotencyKey === existingKey &&
588
+ tx.commitEnvelope.operationCount === expectedCount &&
589
+ tx.commitEnvelope.operationIndex < expectedCount);
590
+ if (!isCompleteEnvelope) {
591
+ throw new Error('Cannot replay a partial or malformed commit envelope');
592
+ }
593
+ const persistedSealTimes = new Set(batch
594
+ .map((transaction) => transaction.commitEnvelope?.sealedAt)
595
+ .filter((sealedAt) => sealedAt !== undefined));
596
+ if (persistedSealTimes.size > 1) {
597
+ throw new Error('Cannot replay a commit envelope with inconsistent seal times');
598
+ }
599
+ const sealedAt = persistedSealTimes.values().next().value ?? Date.now();
600
+ for (const transaction of batch) {
601
+ if (transaction.commitEnvelope)
602
+ transaction.commitEnvelope.sealedAt = sealedAt;
603
+ }
604
+ batch.sort((a, b) => (a.commitEnvelope?.operationIndex ?? 0) -
605
+ (b.commitEnvelope?.operationIndex ?? 0));
606
+ return existingKey;
607
+ }
608
+ // An explicit public key owns its request. Transactions carrying one are
609
+ // selected as solo batches by takeNextExecutionBatch().
610
+ const explicitKey = batch.length === 1
611
+ ? firstTransaction.writeOptions?.idempotencyKey
612
+ : undefined;
613
+ const idempotencyKey = typeof explicitKey === 'string' && explicitKey.length > 0
614
+ ? explicitKey
615
+ : this.generateCommitIdempotencyKey();
616
+ const sealedAt = Date.now();
617
+ const sequence = this.nextCommitSequence();
618
+ batch.forEach((tx, operationIndex) => {
619
+ tx.commitEnvelope = createCommitEnvelopeMember({
620
+ idempotencyKey,
621
+ operationIndex,
622
+ operationCount: batch.length,
623
+ sealedAt,
624
+ sequence,
625
+ });
626
+ });
627
+ return idempotencyKey;
628
+ }
629
+ /** Bind the strict local outbox used before any mutation reaches the wire. */
630
+ setCommitOutbox(outbox) {
631
+ this.commitOutbox = outbox;
632
+ }
633
+ async setCommitOutboxScope(scope) {
634
+ this.commitOutboxScope = scope;
635
+ const deferred = this.deferredMutations;
636
+ this.deferredMutations = [];
637
+ await Promise.all(deferred.map((mutation) => this.enqueueModelMutation(mutation.type, mutation.model, { userId: scope.participantId, organizationId: scope.organizationId }, mutation.capturedChanges, mutation.writeOptions)));
638
+ }
639
+ deferMutation(type, model, capturedChanges, writeOptions) {
640
+ this.runtime.logger.debug('[MutationQueue] Deferring mutation until identity is resolved', { type, model: model.getModelName(), modelId: model.id });
641
+ this.deferredMutations.push({ type, model, capturedChanges, writeOptions });
642
+ }
643
+ async enqueueModelMutation(type, model, context, capturedChanges, writeOptions) {
644
+ switch (type) {
645
+ case 'create': return this.create(model, context, writeOptions);
646
+ case 'update': return this.update(model, context, capturedChanges, writeOptions);
647
+ case 'delete': return this.delete(model, context, writeOptions);
648
+ case 'archive': return this.archive(model, context, writeOptions);
649
+ }
650
+ }
651
+ /** Attach the durable journal supplied by the local/materializer layer. */
652
+ setPersistence(persistence) {
653
+ this.persistence = persistence;
654
+ }
655
+ async persistQueuedTransaction(transaction, modelData) {
656
+ await persistQueuedTransaction(this.persistenceContext, transaction, modelData);
657
+ }
658
+ persistAndStage(transaction, modelData) {
659
+ if (!this.persistence || !this.commitOutboxScope) {
660
+ this.stageTransaction(transaction);
661
+ return Promise.resolve();
662
+ }
663
+ return new Promise((resolve, reject) => {
664
+ this.pendingPersistenceStages.push({ transaction, modelData, resolve, reject });
665
+ if (this.persistenceStageScheduled)
666
+ return;
667
+ this.persistenceStageScheduled = true;
668
+ queueMicrotask(async () => {
669
+ this.persistenceStageScheduled = false;
670
+ const batch = this.pendingPersistenceStages;
671
+ this.pendingPersistenceStages = [];
672
+ const results = await Promise.allSettled(batch.map(({ transaction: queued, modelData: data }) => this.persistQueuedTransaction(queued, data)));
673
+ for (const [index, result] of results.entries()) {
674
+ const item = batch[index];
675
+ if (!item)
676
+ continue;
677
+ if (result.status === 'fulfilled') {
678
+ this.stageTransaction(item.transaction);
679
+ item.resolve();
680
+ }
681
+ else {
682
+ item.reject(result.reason instanceof Error ? result.reason : new Error(String(result.reason)));
683
+ }
684
+ }
685
+ });
686
+ });
687
+ }
688
+ removePersistedTransaction(transactionId) {
689
+ removePersistedTransaction(this.persistenceContext, transactionId);
690
+ }
691
+ settlePersistedFailure(transaction) {
692
+ settlePersistedFailure(this.persistenceContext, transaction);
693
+ }
694
+ sourceMutationIdsFor(batch) {
695
+ return [...new Set(batch.flatMap((transaction) => transaction.sourceMutationIds ?? []))];
696
+ }
697
+ /** Atomically seals a commit before it is allowed onto the dispatch lane. */
698
+ async sealDurableCommit(input) {
699
+ return sealDurableCommit(this.commitTransportContext, input, pendingMutationRecordId);
700
+ }
701
+ async removeDurableCommit(idempotencyKey) {
702
+ return removeDurableCommit(this.commitTransportContext, idempotencyKey);
703
+ }
704
+ async persistDurableCommitAcceptance(envelope, result) {
705
+ return persistDurableCommitAcceptance(this.commitTransportContext, envelope, result);
706
+ }
707
+ parseMutationCommitResult(value) {
708
+ return parseMutationCommitResult(value);
709
+ }
710
+ dispatchCommitBounded(...args) {
711
+ return dispatchCommitBounded(this.commitTransportContext, ...args);
712
+ }
713
+ clearReplicationLagState(transactionId) {
714
+ const timeout = this.replicationLagTimeouts.get(transactionId);
715
+ if (timeout)
716
+ clearTimeout(timeout);
717
+ this.replicationLagTimeouts.delete(transactionId);
718
+ this.replicationLagErrors.delete(transactionId);
719
+ }
720
+ /**
721
+ * Bounds the public `wait: 'confirmed'` promise without changing the
722
+ * accepted write's lifecycle. A lag timeout is not a rejection from the
723
+ * source database, so it must never emit `transaction:failed`, roll back
724
+ * optimistic state, or remove the durable replay envelope.
725
+ */
726
+ scheduleReplicationLagTimeout(transactionId, clientTxId = transactionId, correlationId) {
727
+ const previous = this.replicationLagTimeouts.get(transactionId);
728
+ if (previous)
729
+ clearTimeout(previous);
730
+ this.replicationLagErrors.delete(transactionId);
731
+ const timeoutMs = this.config.deltaConfirmationTimeout;
732
+ const timeout = setTimeout(() => {
733
+ this.replicationLagTimeouts.delete(transactionId);
734
+ const modelTx = this.store.get(transactionId);
735
+ const commitTx = this.commitStore.get(transactionId);
736
+ if (modelTx?.status !== 'awaiting_delta' &&
737
+ commitTx?.status !== 'awaiting_delta')
738
+ return;
739
+ const error = new AbloConnectionError(`The source accepted commit ${clientTxId}, but its replication echo did not arrive within ${timeoutMs}ms.`, {
740
+ code: 'replication_lag_timeout',
741
+ httpStatus: 504,
742
+ details: {
743
+ clientTxId,
744
+ ...(correlationId ? { correlationId } : {}),
745
+ timeoutMs,
746
+ accepted: true,
747
+ },
748
+ });
749
+ this.replicationLagErrors.set(transactionId, error);
750
+ // Model-proxy writes expose their waiter through the resolver table.
751
+ // Reject that promise without moving the transaction out of
752
+ // `awaiting_delta`; the eventual echo still completes it normally.
753
+ const resolver = this.confirmationResolvers.get(transactionId);
754
+ if (resolver) {
755
+ this.confirmationResolvers.delete(transactionId);
756
+ resolver.reject(error);
757
+ }
758
+ this.emitCommitLifecycle('transaction:confirmation_lagged', {
759
+ transactionId,
760
+ error,
761
+ });
762
+ this.emitCommitLifecycle(`transaction:confirmation_lagged:${transactionId}`, {
763
+ error,
764
+ });
765
+ if (commitTx) {
766
+ const firstOperation = commitTx.operations[0];
767
+ this.emitCommitLifecycle('reconciliation:needed', {
768
+ reason: 'replication_lag_timeout',
769
+ txId: transactionId,
770
+ model: firstOperation?.model ?? 'commit',
771
+ modelId: firstOperation?.id ?? transactionId,
772
+ lastSeenSyncId: this.lastSeenSyncId,
773
+ retryCount: 1,
774
+ });
775
+ }
776
+ }, timeoutMs);
777
+ this.replicationLagTimeouts.set(transactionId, timeout);
778
+ }
779
+ takeNextExecutionBatch() {
780
+ const selected = selectExecutionBatch(this.executionQueue, this.config.maxBatchSize);
781
+ this.executionQueue = selected.remaining;
782
+ return selected.batch;
783
+ }
784
+ takePendingDrainBatch(pending) {
785
+ return selectPendingDrainBatch(pending, this.config.maxBatchSize);
786
+ }
787
+ /**
788
+ * Resolvers for per-transaction `confirmation` promises. Populated in
789
+ * `attachConfirmation` at staging time, consumed by the constructor-time
790
+ * listeners on `transaction:completed` / `transaction:failed`. Kept off
791
+ * the QueuedMutation row so the store's iteration order stays plain-data
792
+ * and serialization-friendly.
793
+ */
794
+ confirmationResolvers = new Map();
795
+ constructor(config) {
796
+ super();
797
+ this.runtime = config?.runtime ?? globalRuntime;
798
+ this.position = config?.position ?? new LogPosition();
799
+ this.localMutationPort = config?.localMutationPort ?? createLocalMutationPort(this);
800
+ // Bind the confirmation tracker to this queue's store/ledger/events.
801
+ // `isConnected` closes over `isConnectedFn` so `setConnectionChecker`
802
+ // swaps stay visible to in-flight timeouts.
803
+ this.deltaConfirmation = new DeltaConfirmationTracker({
804
+ store: this.store,
805
+ optimisticUpdates: this.localMutationPort.updates,
806
+ emit: (event, payload) => {
807
+ this.emit(event, payload);
808
+ },
809
+ isConnected: () => this.isConnectedFn(),
810
+ position: this.position,
811
+ runtime: this.runtime,
812
+ });
813
+ if (config) {
814
+ this.config = { ...this.config, ...config };
815
+ }
816
+ // Centralized fan-in for `tx.confirmation`. Completion/failure are
817
+ // emitted from ~10 sites (delta confirm, immediate confirm, batch
818
+ // success, permanent error, max_retries_exhausted, …). Subscribing
819
+ // once here keeps every emit site intact and guarantees the call-site
820
+ // promise always settles, regardless of which path produced the
821
+ // terminal state.
822
+ this.on('transaction:completed', (tx) => {
823
+ // Any successful write clears the permanent-error dedup, so a genuine
824
+ // recurrence after recovery warns again instead of staying demoted.
825
+ this.lastPermanentErrorSig = undefined;
826
+ this.clearReplicationLagState(tx.id);
827
+ const r = this.confirmationResolvers.get(tx.id);
828
+ if (r) {
829
+ this.confirmationResolvers.delete(tx.id);
830
+ r.resolve();
831
+ }
832
+ if (tx.type === 'create') {
833
+ this.releaseDeferredDeletesForCreate(tx);
834
+ }
835
+ this.removePersistedTransaction(tx.id);
836
+ });
837
+ this.on('transaction:failed', ({ transaction, error }) => {
838
+ const r = this.confirmationResolvers.get(transaction.id);
839
+ if (r) {
840
+ this.confirmationResolvers.delete(transaction.id);
841
+ r.reject(error);
842
+ }
843
+ if (transaction.type === 'create') {
844
+ this.releaseDeferredDeletesForCreate(transaction);
845
+ }
846
+ this.settlePersistedFailure(transaction);
847
+ });
848
+ }
849
+ /**
850
+ * Returns the in-flight confirmation promise for a given model and id. When
851
+ * several transactions match, it returns the most recent one's promise; when
852
+ * none is open it resolves immediately, which covers both "already confirmed"
853
+ * and "never staged".
854
+ *
855
+ * It considers the three non-terminal statuses in which the write can still
856
+ * be rolled back — `pending`, `executing`, and `awaiting_delta` — and ignores
857
+ * `completed` (already settled) and `failed`/`rolled_back` (already
858
+ * rejected). This complements the `confirmation` promise carried on a known
859
+ * {@link QueuedMutation}: use this method at call sites that hold a model
860
+ * returned by `ablo.<model>.create()` but never see the underlying
861
+ * transaction.
862
+ */
863
+ confirmationFor(modelName, modelId) {
864
+ const candidates = [
865
+ ...this.store.getByStatus('pending'),
866
+ ...this.store.getByStatus('executing'),
867
+ ...this.store.getByStatus('awaiting_delta'),
868
+ ].filter((tx) => tx.modelName === modelName && tx.modelId === modelId);
869
+ if (candidates.length === 0)
870
+ return Promise.resolve();
871
+ const latest = candidates.sort((a, b) => b.createdAt - a.createdAt)[0];
872
+ if (!latest)
873
+ return Promise.resolve();
874
+ return latest.confirmation ?? Promise.resolve();
875
+ }
876
+ /**
877
+ * Attaches a `confirmation` promise to a newly created transaction. Call this
878
+ * before the transaction is staged so a caller can `await tx.confirmation`
879
+ * immediately after a create, update, or delete returns. It is idempotent and
880
+ * returns early if one is already attached.
881
+ *
882
+ * It also attaches a no-op rejection handler. Most callers never await the
883
+ * confirmation, and without this the runtime would report an unhandled
884
+ * rejection when a write fails. Callers that do want to observe failure simply
885
+ * attach their own `.then`/`.catch`.
886
+ */
887
+ attachConfirmation(tx) {
888
+ if (tx.confirmation)
889
+ return;
890
+ tx.confirmation = new Promise((resolve, reject) => {
891
+ this.confirmationResolvers.set(tx.id, { resolve, reject });
892
+ });
893
+ tx.confirmation.catch(() => {
894
+ // Swallow unhandled rejections; callers that care attach their own handler.
895
+ });
896
+ }
897
+ /**
898
+ * Registers a predicate the queue uses to check whether it is connected.
899
+ * While disconnected, confirmation timeouts re-schedule themselves instead of
900
+ * escalating, so a transaction is never rolled back merely because the client
901
+ * was briefly offline.
902
+ */
903
+ setConnectionChecker(fn) {
904
+ this.isConnectedFn = fn;
905
+ }
906
+ /**
907
+ * Drives the offline-grace timer for in-flight commit-lane transactions.
908
+ *
909
+ * On `'disconnected'` it starts a one-shot timer of
910
+ * `config.commitOfflineGraceMs`. If that timer fires — meaning the disconnect
911
+ * outlasted the grace window — every commit-lane transaction still `pending`
912
+ * or `executing` is failed with an {@link AbloConnectionError}, so
913
+ * {@link waitForCommitReceipt} rejects within seconds instead of hanging.
914
+ *
915
+ * On `'connected'` it clears any pending grace timer. Brief disconnects are
916
+ * absorbed transparently; {@link processCommitLane} and
917
+ * {@link drainPending} resumes the work when the owner decides to drain.
918
+ */
919
+ setConnectionState(state) {
920
+ if (state === 'connected') {
921
+ if (this.commitOfflineGraceTimer !== null) {
922
+ clearTimeout(this.commitOfflineGraceTimer);
923
+ this.commitOfflineGraceTimer = null;
924
+ }
925
+ return;
926
+ }
927
+ // state === 'disconnected'
928
+ if (this.commitOfflineGraceTimer !== null)
929
+ return; // already armed
930
+ const graceMs = this.config.commitOfflineGraceMs;
931
+ this.commitOfflineGraceTimer = setTimeout(() => {
932
+ this.commitOfflineGraceTimer = null;
933
+ this.failInFlightCommitsOnOffline(graceMs);
934
+ }, graceMs);
935
+ }
936
+ failInFlightCommitsOnOffline(graceMs) {
937
+ const inFlight = [];
938
+ for (const [id, tx] of this.commitStore.entries()) {
939
+ if (tx.status === 'pending' || tx.status === 'executing') {
940
+ inFlight.push(id);
941
+ }
942
+ }
943
+ if (inFlight.length === 0)
944
+ return;
945
+ // Each failed commit reaches the consumer through its own rejection path,
946
+ // so this aggregate line is forensic and logged at debug rather than warn.
947
+ this.runtime.logger.debug(`[MutationQueue] WS disconnected > ${graceMs}ms; failing ${inFlight.length} in-flight commit(s) with AbloConnectionError`, { inFlightIds: inFlight.map((id) => id.slice(0, 8)) });
948
+ for (const id of inFlight) {
949
+ const tx = this.commitStore.get(id);
950
+ if (!tx)
951
+ continue;
952
+ const err = new AbloConnectionError(`commit ack abandoned after ${graceMs}ms offline`, { code: 'commit_offline_grace_expired' });
953
+ tx.status = 'failed';
954
+ tx.error = err;
955
+ this.emit(`transaction:failed:${id}`, { error: err });
956
+ }
957
+ }
958
+ /**
959
+ * Binds the mutation executor for this queue instance. The owning client
960
+ * calls this right after construction, so commits made here always dispatch
961
+ * through this instance's connection even when several client instances exist
962
+ * in the same process.
963
+ */
964
+ setMutationExecutor(executor) {
965
+ this._mutationExecutor = executor;
966
+ }
967
+ // ============================================================================
968
+ // Microtask-based transaction staging
969
+ // ============================================================================
970
+ //
971
+ // Every transaction lands in the `createdTransactions` staging area first.
972
+ // A microtask then commits them together under one batch index, so a bulk
973
+ // operation such as importing a hundred rows is sent efficiently.
974
+ //
975
+ // Flow:
976
+ // 1. create()/update()/delete() calls stageTransaction().
977
+ // 2. stageTransaction() adds to createdTransactions and schedules a microtask.
978
+ // 3. The microtask runs commitCreatedTransactions() once the current
979
+ // synchronous code finishes.
980
+ // 4. All staged transactions share one batch index and move to the execution
981
+ // queue.
982
+ // ============================================================================
983
+ /**
984
+ * Stages a transaction for commit. Transactions staged within the same
985
+ * event-loop tick are committed together.
986
+ */
987
+ stageTransaction(transaction) {
988
+ this.createdTransactions.push(transaction);
989
+ this.scheduleCommit();
990
+ }
991
+ /**
992
+ * Schedules the staged transactions to commit on a microtask, so all
993
+ * transactions created synchronously within one tick are batched together.
994
+ */
995
+ scheduleCommit() {
996
+ if (this.commitScheduled)
997
+ return;
998
+ this.commitScheduled = true;
999
+ // Use queueMicrotask to run after current sync code completes
1000
+ // All transactions created in same event loop will be committed together
1001
+ const schedule = typeof queueMicrotask === 'function'
1002
+ ? queueMicrotask
1003
+ : (cb) => Promise.resolve().then(cb);
1004
+ schedule(() => {
1005
+ this.commitCreatedTransactions();
1006
+ });
1007
+ }
1008
+ /**
1009
+ * Moves all staged transactions onto the execution queue, assigning them a
1010
+ * single shared batch index so they commit together.
1011
+ */
1012
+ commitCreatedTransactions() {
1013
+ this.commitScheduled = false;
1014
+ if (this.createdTransactions.length === 0)
1015
+ return;
1016
+ // Increment batch index - all transactions in this commit share it
1017
+ this.batchIndex++;
1018
+ const currentBatchIndex = this.batchIndex;
1019
+ // Log batch commit for performance monitoring
1020
+ this.runtime.logger.debug('[MutationQueue] commitCreatedTransactions', {
1021
+ count: this.createdTransactions.length,
1022
+ batchIndex: currentBatchIndex,
1023
+ types: this.createdTransactions.map((t) => `${t.type}:${t.modelName}`),
1024
+ });
1025
+ // Move all staged transactions to execution queue
1026
+ const staged = this.createdTransactions;
1027
+ this.createdTransactions = [];
1028
+ for (const transaction of staged) {
1029
+ // Assign batch ID based on current batch index
1030
+ transaction.batchId = `batch_${currentBatchIndex}`;
1031
+ this.enqueue(transaction);
1032
+ }
1033
+ }
1034
+ /**
1035
+ * Flushes every pending transaction in one commit, the fast path taken on
1036
+ * reconnect. If transport fails, the transactions retain this exact commit
1037
+ * envelope when they fall back to normal queue processing.
1038
+ */
1039
+ async drainPending() {
1040
+ if (this.pendingDrainPromise)
1041
+ return this.pendingDrainPromise;
1042
+ const drain = this.drainPendingInternal();
1043
+ this.pendingDrainPromise = drain.finally(() => {
1044
+ this.pendingDrainPromise = null;
1045
+ });
1046
+ return this.pendingDrainPromise;
1047
+ }
1048
+ async drainPendingInternal() {
1049
+ await drainPendingSettlements(this.pendingDrainContext);
1050
+ }
1051
+ async create(model, context, writeOptions, sourceMutationId) {
1052
+ return createModel(this.modelMutationContext, model, context, writeOptions, sourceMutationId);
1053
+ }
1054
+ async update(model, context, precomputedChanges, writeOptions, sourceMutationId) {
1055
+ return updateModel(this.modelMutationContext, model, context, precomputedChanges, writeOptions, sourceMutationId);
1056
+ }
1057
+ async delete(model, context, writeOptions, sourceMutationId) {
1058
+ return deleteModel(this.modelMutationContext, model, context, writeOptions, sourceMutationId);
1059
+ }
1060
+ async uploadAttachment(_file, options, _context // eslint-disable-line @typescript-eslint/no-unused-vars -- reserved executor context
1061
+ ) {
1062
+ return this.mutationExecutor.uploadAttachment?.(options.id, options) ?? null;
1063
+ }
1064
+ async batchUploadAttachments(_files, items, _context // eslint-disable-line @typescript-eslint/no-unused-vars -- reserved executor context
1065
+ ) {
1066
+ return this.mutationExecutor.batchUploadAttachments?.(items.map(i => ({ id: i.id, input: i }))) ?? [];
1067
+ }
1068
+ async archive(model, context, writeOptions, sourceMutationId) {
1069
+ return archiveModel(this.modelMutationContext, model, context, writeOptions, sourceMutationId);
1070
+ }
1071
+ async unarchive(model, context) {
1072
+ return unarchiveModel(this.modelMutationContext, model, context);
1073
+ }
1074
+ enqueue(transaction) {
1075
+ enqueueTransaction(this.queueCoalescingContext, transaction);
1076
+ }
1077
+ scheduleProcessing(immediate = false) {
1078
+ scheduleProcessingExternal(this.processingSchedulerContext, immediate);
1079
+ }
1080
+ async processBatch() {
1081
+ await processBatch(this.batchProcessingContext);
1082
+ }
1083
+ rememberDeltaCorrelation(correlationId, syncId) {
1084
+ // Refresh insertion order when a replay repeats the same correlation id.
1085
+ this.recentDeltaCorrelations.delete(correlationId);
1086
+ this.recentDeltaCorrelations.set(correlationId, syncId);
1087
+ if (this.recentDeltaCorrelations.size <= 2_048)
1088
+ return;
1089
+ const oldest = this.recentDeltaCorrelations.keys().next().value;
1090
+ if (typeof oldest === 'string')
1091
+ this.recentDeltaCorrelations.delete(oldest);
1092
+ }
1093
+ queuedCommitEchoSyncId(tx) {
1094
+ return tx.correlationId
1095
+ ? this.recentDeltaCorrelations.get(tx.correlationId)
1096
+ : undefined;
1097
+ }
1098
+ queuedCommitMatchesCorrelation(tx, correlationId) {
1099
+ return tx.correlationId !== undefined && tx.correlationId === correlationId;
1100
+ }
1101
+ completeQueuedCommit(tx, syncId) {
1102
+ if (tx.status !== 'awaiting_delta')
1103
+ return;
1104
+ this.clearReplicationLagState(tx.id);
1105
+ tx.lastSyncId = syncId;
1106
+ tx.status = 'completed';
1107
+ // The queued receipt was only acceptance. The correlated source echo is
1108
+ // the first definitive success and therefore the point where the durable
1109
+ // replay envelope may be removed.
1110
+ void this.removeDurableCommit(tx.id);
1111
+ this.emitCommitLifecycle('transaction:completed', tx);
1112
+ this.emitCommitLifecycle(`transaction:completed:${tx.id}`, tx);
1113
+ }
1114
+ /**
1115
+ * Confirms awaiting writes. Hosted/anomaly receipts keep their sync-id
1116
+ * threshold semantics; queued forwards require the exact server correlation.
1117
+ */
1118
+ onDeltaReceived(syncId, _transactionId, correlationId) {
1119
+ if (correlationId)
1120
+ this.rememberDeltaCorrelation(correlationId, syncId);
1121
+ const correlatedModelTransactions = correlationId
1122
+ ? this.store.getByStatus('awaiting_delta').filter((tx) => tx.requiresCorrelatedDelta === true &&
1123
+ tx.correlationId !== undefined &&
1124
+ tx.correlationId === correlationId)
1125
+ : [];
1126
+ this.deltaConfirmation.onDeltaReceived(syncId, correlationId);
1127
+ if (correlatedModelTransactions.length > 0) {
1128
+ const envelopeIds = new Set();
1129
+ for (const tx of correlatedModelTransactions) {
1130
+ this.clearReplicationLagState(tx.id);
1131
+ if (tx.commitEnvelope)
1132
+ envelopeIds.add(tx.commitEnvelope.idempotencyKey);
1133
+ }
1134
+ for (const envelopeId of envelopeIds) {
1135
+ const envelopeStillAwaiting = this.store
1136
+ .getByStatus('awaiting_delta')
1137
+ .some((tx) => tx.commitEnvelope?.idempotencyKey === envelopeId);
1138
+ if (!envelopeStillAwaiting)
1139
+ void this.removeDurableCommit(envelopeId);
1140
+ }
1141
+ }
1142
+ if (!correlationId)
1143
+ return;
1144
+ for (const tx of this.commitStore.values()) {
1145
+ if (tx.status !== 'awaiting_delta')
1146
+ continue;
1147
+ if (this.queuedCommitMatchesCorrelation(tx, correlationId)) {
1148
+ this.completeQueuedCommit(tx, syncId);
1149
+ }
1150
+ }
1151
+ }
1152
+ // Schedule the retry-and-reconciliation wait for a transaction's confirming
1153
+ // delta; see {@link DeltaConfirmationTracker} in `./deltaConfirmation.js`.
1154
+ scheduleDeltaConfirmationTimeout(tx, timeoutMs) {
1155
+ this.deltaConfirmation.scheduleDeltaConfirmationTimeout(tx, timeoutMs);
1156
+ }
1157
+ /**
1158
+ * Resolves once the given transaction is confirmed and rejects if it fails.
1159
+ * The confirming delta's timeout is handled by
1160
+ * {@link scheduleDeltaConfirmationTimeout}.
1161
+ */
1162
+ waitForConfirmation(transactionId) {
1163
+ return new Promise((resolve, reject) => {
1164
+ // Check if already completed
1165
+ const tx = this.store.get(transactionId);
1166
+ if (tx?.status === 'completed') {
1167
+ resolve();
1168
+ return;
1169
+ }
1170
+ const lagError = this.replicationLagErrors.get(transactionId);
1171
+ if (lagError) {
1172
+ reject(lagError);
1173
+ return;
1174
+ }
1175
+ const onCompleted = () => {
1176
+ cleanup();
1177
+ resolve();
1178
+ };
1179
+ const onFailed = ({ error }) => {
1180
+ cleanup();
1181
+ reject(error);
1182
+ };
1183
+ const onLagged = ({ error }) => {
1184
+ cleanup();
1185
+ reject(error);
1186
+ };
1187
+ const cleanup = () => {
1188
+ this.off(`transaction:completed:${transactionId}`, onCompleted);
1189
+ this.off(`transaction:failed:${transactionId}`, onFailed);
1190
+ this.off(`transaction:confirmation_lagged:${transactionId}`, onLagged);
1191
+ };
1192
+ // Listen to existing events (timeout already handled by scheduleDeltaConfirmationTimeout)
1193
+ this.on(`transaction:completed:${transactionId}`, onCompleted);
1194
+ this.on(`transaction:failed:${transactionId}`, onFailed);
1195
+ this.on(`transaction:confirmation_lagged:${transactionId}`, onLagged);
1196
+ });
1197
+ }
1198
+ // Reports whether a client mutation id is known to this queue, which helps
1199
+ // identify a delta as this client's own echo.
1200
+ hasClientMutationId(id) {
1201
+ return !!this.store.get(id) || this.commitStore.has(id);
1202
+ }
1203
+ /** Enqueues a pre-built atomic commit through the commit API coordinator. */
1204
+ async enqueueCommit(clientTxId, operations, options = {}) {
1205
+ return enqueueCommit(this.commitApiContext, clientTxId, operations, options);
1206
+ }
1207
+ async processCommitLane() {
1208
+ await processCommitLane(this.commitLaneContext);
1209
+ }
1210
+ waitForCommitReceipt(clientTxId) {
1211
+ return waitForCommitReceipt(this.commitReceiptContext, clientTxId);
1212
+ }
1213
+ isReorderPayload(data) {
1214
+ if (!data || typeof data !== 'object')
1215
+ return false;
1216
+ return 'order' in data || 'orderKey' in data || 'position' in data;
1217
+ }
1218
+ isPermanentError(error) {
1219
+ return classifyPermanentError(error);
1220
+ }
1221
+ isDefinitiveRejection(error) {
1222
+ return classifyDefinitiveRejection(error);
1223
+ }
1224
+ async handleFailure(transaction, error) {
1225
+ await handleFailure(this.failureHandlingContext, transaction, error);
1226
+ }
1227
+ async handleConflict(transaction, serverData) {
1228
+ await resolveConflict(this.conflictPolicyContext, transaction, serverData);
1229
+ }
1230
+ applyOptimisticCreate(model, transaction) {
1231
+ this.localMutationPort.applyCreate(model, transaction);
1232
+ }
1233
+ applyOptimisticUpdate(model, transaction) {
1234
+ this.localMutationPort.applyUpdate(model, transaction);
1235
+ }
1236
+ applyOptimisticDelete(model, transaction) {
1237
+ this.localMutationPort.applyDelete(model, transaction);
1238
+ }
1239
+ async rollbackOptimistic(transaction, reason, error) {
1240
+ await this.localMutationPort.rollback(transaction, reason, error);
1241
+ }
1242
+ deserializeLegacyPendingMutation(row, fallbackMutationId) {
1243
+ return deserializeLegacyPendingMutation(this.persistenceContext, row, fallbackMutationId);
1244
+ }
1245
+ async loadPersistedTransactions(persistence, sealedMutationIds = new Set()) {
1246
+ await loadPersistedTransactions(this.persistenceContext, persistence, sealedMutationIds);
1247
+ }
1248
+ async restoreDurableCommits() {
1249
+ return restoreDurableCommitsExternal(this.durableCommitRestoreContext);
1250
+ }
1251
+ deserializeTransaction(data) {
1252
+ if (isNonReplayablePersistedRow(data))
1253
+ return null;
1254
+ const transaction = deserializePersistedTransaction(data, this.runtime);
1255
+ if (!transaction) {
1256
+ const rowId = typeof data === 'object' && data !== null && typeof data.id === 'string'
1257
+ ? data.id
1258
+ : undefined;
1259
+ this.runtime.logger.debug('[MutationQueue] Dropping malformed persisted transaction', {
1260
+ rowId,
1261
+ });
1262
+ this.runtime.observability.captureMutationFailure({
1263
+ context: 'deserialize-persisted-transaction',
1264
+ error: `Persisted transaction failed schema validation${rowId ? ` (id: ${rowId})` : ''}`,
1265
+ });
1266
+ return null;
1267
+ }
1268
+ return transaction;
1269
+ }
1270
+ cancelTransactionsForModel(modelId, transactionType) {
1271
+ const cancelledTransactions = [];
1272
+ const allTransactions = [
1273
+ ...this.store.getByStatus('pending'),
1274
+ ...this.store.getByStatus('executing'),
1275
+ ];
1276
+ for (const transaction of allTransactions) {
1277
+ if (transaction.modelId === modelId) {
1278
+ if (!transactionType || transaction.type === transactionType) {
1279
+ cancelledTransactions.push(transaction);
1280
+ this.store.updateStatus(transaction.id, 'rolled_back');
1281
+ // Sync caller: a rejected rollback (throwing optimistic:rollback
1282
+ // listener) must surface, not vanish — the status flip above is
1283
+ // already committed either way.
1284
+ void this.rollbackOptimistic(transaction, 'model_cancelled').catch((error) => {
1285
+ this.runtime.observability.captureMutationFailure({
1286
+ context: 'rollback-model-cancelled',
1287
+ error: error instanceof Error ? error : String(error),
1288
+ });
1289
+ });
1290
+ }
1291
+ }
1292
+ }
1293
+ return cancelledTransactions;
1294
+ }
1295
+ /**
1296
+ * Cancels pending transactions for child rows that reference a deleted parent,
1297
+ * used to cascade a parent deletion. The caller supplies the foreign-key
1298
+ * relationship; this method performs the cancellation.
1299
+ *
1300
+ * @param childModelName - The child model type (for example 'Block').
1301
+ * @param foreignKey - The foreign-key property name (for example 'sectionId').
1302
+ * @param parentId - The deleted parent's id.
1303
+ * @returns The number of transactions cancelled.
1304
+ */
1305
+ cancelTransactionsByForeignKey(childModelName, foreignKey, parentId) {
1306
+ let cancelled = 0;
1307
+ const allTransactions = [
1308
+ ...this.store.getByStatus('pending'),
1309
+ ...this.store.getByStatus('executing'),
1310
+ ...this.store.getByStatus('awaiting_delta'),
1311
+ ];
1312
+ for (const transaction of allTransactions) {
1313
+ if (transaction.modelName === childModelName) {
1314
+ // Check if this transaction's data contains the parent FK
1315
+ const fkValue = transaction.data?.[foreignKey];
1316
+ if (fkValue === parentId) {
1317
+ this.store.updateStatus(transaction.id, 'rolled_back');
1318
+ void this.rollbackOptimistic(transaction, 'cascade_parent_deleted').catch((error) => {
1319
+ this.runtime.observability.captureMutationFailure({
1320
+ context: 'rollback-cascade-parent-deleted',
1321
+ error: error instanceof Error ? error : String(error),
1322
+ });
1323
+ });
1324
+ cancelled++;
1325
+ this.runtime.logger.debug('[MutationQueue] Cascade cancelled orphaned transaction', {
1326
+ txId: transaction.id.slice(0, 12),
1327
+ model: childModelName,
1328
+ foreignKey,
1329
+ parentId: parentId.slice(0, 12),
1330
+ });
1331
+ }
1332
+ }
1333
+ }
1334
+ return cancelled;
1335
+ }
1336
+ /**
1337
+ * Returns the number of transactions still pending or executing.
1338
+ */
1339
+ getOutstandingTransactionCount() {
1340
+ return this.deferredMutations.length +
1341
+ this.store.getByStatus('pending').length +
1342
+ this.store.getByStatus('executing').length;
1343
+ }
1344
+ getOutstandingTransactions() {
1345
+ return [
1346
+ ...this.store.getByStatus('pending'),
1347
+ ...this.store.getByStatus('executing'),
1348
+ ];
1349
+ }
1350
+ /** Generates a unique local transaction id. */
1351
+ generateId() {
1352
+ return `tx_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
1353
+ }
1354
+ mergeData(local, remote) {
1355
+ return { ...(remote || {}), ...(local || {}) };
1356
+ }
1357
+ extractCreateData(model) {
1358
+ return projectCommitPayload(model.getModelName(), model.toJSON(), { dropUndefined: false }, this.runtime);
1359
+ }
1360
+ mapChangesToInput(modelName, changes) {
1361
+ return projectCommitPayload(modelName, changes, { dropUndefined: true }, this.runtime);
1362
+ }
1363
+ extractUpdateData(model) {
1364
+ return projectCommitPayload(model.getModelName(), model.getChanges(), { dropUndefined: true }, this.runtime);
1365
+ }
1366
+ // Derive previous values for changed fields to support accurate rollback.
1367
+ // Model-specific special cases do not belong here; a model that needs to
1368
+ // surface previous state beyond `modifiedProperties` should expose a typed
1369
+ // `getPreviousData()` accessor for this method to call.
1370
+ extractPreviousData(model, updateInput) {
1371
+ // When the update's written keys are known, capture a before-image for
1372
+ // exactly those keys, so the recorded undo inverse reverts them and nothing
1373
+ // else — a full-row inverse would clobber concurrent edits to unrelated
1374
+ // fields. `fallbackToLive: false` makes `Model.capturePreviousValues` omit
1375
+ // any key it cannot resolve, and `buildUndoOps` then drops an un-revertible
1376
+ // inverse rather than inventing one. With no `updateInput` (a full extract)
1377
+ // it falls back to every tracked field. `Model.capturePreviousValues` is the
1378
+ // single before-image source, shared with
1379
+ // `RecordingMutation.snapshotFields`.
1380
+ const keys = updateInput
1381
+ ? Object.keys(updateInput)
1382
+ : [...(model.modifiedProperties instanceof Map ? model.modifiedProperties.keys() : [])];
1383
+ return { id: model.id, ...model.capturePreviousValues(keys, { fallbackToLive: false }) };
1384
+ }
1385
+ /** Returns a snapshot of queue counts and the current configuration. */
1386
+ getStats() {
1387
+ return {
1388
+ pending: this.store.getByStatus('pending').length,
1389
+ executing: this.store.getByStatus('executing').length,
1390
+ completed: this.store.getByStatus('completed').length,
1391
+ failed: this.store.getByStatus('failed').length,
1392
+ optimistic: this.localMutationPort.updates.size,
1393
+ totalTransactions: this.store.getAll().length,
1394
+ batchIndex: this.batchIndex,
1395
+ config: { ...this.config },
1396
+ };
1397
+ }
1398
+ /**
1399
+ * Returns detailed internal state — pending, executing, and awaiting-delta
1400
+ * transactions — to help diagnose delta-confirmation issues.
1401
+ */
1402
+ getDebugInfo() {
1403
+ const awaitingDelta = this.store.getByStatus('awaiting_delta');
1404
+ return {
1405
+ lastSeenSyncId: this.lastSeenSyncId,
1406
+ awaitingDeltaCount: awaitingDelta.length,
1407
+ awaitingDeltaTransactions: awaitingDelta.map((tx) => ({
1408
+ id: tx.id.slice(0, 8),
1409
+ type: tx.type,
1410
+ modelName: tx.modelName,
1411
+ modelId: tx.modelId.slice(0, 8),
1412
+ syncIdNeeded: tx.syncIdNeededForCompletion,
1413
+ createdAt: tx.createdAt,
1414
+ age: Date.now() - tx.createdAt,
1415
+ })),
1416
+ pendingTransactions: this.store.getByStatus('pending').map((tx) => ({
1417
+ id: tx.id.slice(0, 8),
1418
+ type: tx.type,
1419
+ modelName: tx.modelName,
1420
+ modelId: tx.modelId.slice(0, 8),
1421
+ })),
1422
+ executingTransactions: this.store.getByStatus('executing').map((tx) => ({
1423
+ id: tx.id.slice(0, 8),
1424
+ type: tx.type,
1425
+ modelName: tx.modelName,
1426
+ modelId: tx.modelId.slice(0, 8),
1427
+ })),
1428
+ };
1429
+ }
1430
+ /** Merges the given options into the queue's configuration. */
1431
+ setConfig(config) {
1432
+ this.config = { ...this.config, ...config };
1433
+ }
1434
+ /**
1435
+ * Re-emits an incoming sync delta on the `sync:delta` event for the store to
1436
+ * apply. Because rows use stable ids, no id reconciliation is needed here.
1437
+ */
1438
+ handleSyncDelta(delta) {
1439
+ // Row ids are stable, so no reconciliation is needed; re-emit the delta for
1440
+ // the store to apply directly.
1441
+ this.emit('sync:delta', {
1442
+ id: delta.id,
1443
+ modelName: delta.modelName,
1444
+ action: delta.action,
1445
+ data: delta.data,
1446
+ });
1447
+ return true;
1448
+ }
1449
+ /**
1450
+ * Releases the queue's resources: rolls back outstanding optimistic updates,
1451
+ * clears all timers and stored transactions, and removes event listeners.
1452
+ */
1453
+ dispose() {
1454
+ // Cancel all active optimistic updates
1455
+ for (const [, optimistic] of this.localMutationPort.updates) {
1456
+ this.emit('optimistic:rollback', {
1457
+ model: optimistic.model,
1458
+ previousState: optimistic.previousState,
1459
+ transaction: optimistic.transaction,
1460
+ reason: 'dispose',
1461
+ });
1462
+ }
1463
+ // Clear processing
1464
+ if (this.processTimer) {
1465
+ clearTimeout(this.processTimer);
1466
+ }
1467
+ // Clear every armed delta-confirmation timer (one per in-flight tx,
1468
+ // 30–120s each) — a disposed queue must not keep the process alive or
1469
+ // fire confirmation callbacks against the cleared store below.
1470
+ this.deltaConfirmation.dispose();
1471
+ for (const timeout of this.replicationLagTimeouts.values()) {
1472
+ clearTimeout(timeout);
1473
+ }
1474
+ this.replicationLagTimeouts.clear();
1475
+ this.replicationLagErrors.clear();
1476
+ // Clear the offline-grace timer armed by setConnectionState('disconnected').
1477
+ if (this.commitOfflineGraceTimer !== null) {
1478
+ clearTimeout(this.commitOfflineGraceTimer);
1479
+ this.commitOfflineGraceTimer = null;
1480
+ }
1481
+ // Clear store
1482
+ this.store.clear();
1483
+ this.localMutationPort.updates.clear();
1484
+ this.executionQueue = [];
1485
+ this.createdTransactions = [];
1486
+ this.deferredDeletesByCreate.clear();
1487
+ this.recentDeltaCorrelations.clear();
1488
+ this.commitLane = [];
1489
+ this.commitStore.clear();
1490
+ this.commitNotifications.clear();
1491
+ this.commitMissingIds.clear();
1492
+ // Clear event listeners
1493
+ this.removeAllListeners();
1494
+ // Reset state
1495
+ this.isProcessing = false;
1496
+ this.batchIndex = 0;
1497
+ }
1498
+ }