@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,65 @@
1
+ /**
2
+ * An in-memory index of transactions by id, with a secondary index by status.
3
+ * The status index keeps the queue's hot paths — such as `getByStatus('pending')`
4
+ * on every batch and coalesce decision — proportional to the number of
5
+ * transactions in that status rather than the total across all statuses.
6
+ * {@link MutationQueue} owns an instance and routes every status change
7
+ * through {@link updateStatus}, which keeps the two indexes consistent.
8
+ */
9
+
10
+ import type { QueuedMutation } from './commitPayload.js';
11
+
12
+ export class MutationStore {
13
+ private transactions = new Map<string, QueuedMutation>();
14
+ private byStatus = new Map<string, Set<string>>();
15
+
16
+ add(transaction: QueuedMutation): void {
17
+ this.transactions.set(transaction.id, transaction);
18
+
19
+ if (!this.byStatus.has(transaction.status)) {
20
+ this.byStatus.set(transaction.status, new Set());
21
+ }
22
+ this.byStatus.get(transaction.status)!.add(transaction.id);
23
+ }
24
+
25
+ get(id: string): QueuedMutation | undefined {
26
+ return this.transactions.get(id);
27
+ }
28
+
29
+ updateStatus(id: string, newStatus: QueuedMutation['status']): void {
30
+ const tx = this.transactions.get(id);
31
+ if (!tx) return;
32
+
33
+ this.byStatus.get(tx.status)?.delete(id);
34
+ tx.status = newStatus;
35
+
36
+ if (!this.byStatus.has(newStatus)) {
37
+ this.byStatus.set(newStatus, new Set());
38
+ }
39
+ this.byStatus.get(newStatus)!.add(id);
40
+ }
41
+
42
+ getByStatus(status: QueuedMutation['status']): QueuedMutation[] {
43
+ const ids = this.byStatus.get(status) || new Set();
44
+ return Array.from(ids)
45
+ .map((id) => this.transactions.get(id)!)
46
+ .filter(Boolean);
47
+ }
48
+
49
+ remove(id: string): void {
50
+ const tx = this.transactions.get(id);
51
+ if (!tx) return;
52
+
53
+ this.transactions.delete(id);
54
+ this.byStatus.get(tx.status)?.delete(id);
55
+ }
56
+
57
+ clear(): void {
58
+ this.transactions.clear();
59
+ this.byStatus.clear();
60
+ }
61
+
62
+ getAll(): QueuedMutation[] {
63
+ return Array.from(this.transactions.values());
64
+ }
65
+ }
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Tracks the transactions this client has applied locally but the server has
3
+ * not yet confirmed, so their echoes can be recognized when they return over
4
+ * the sync stream. When a change is made optimistically, the client updates
5
+ * its own state at once; the server later broadcasts the same change as a sync
6
+ * delta stamped with the originating `transactionId`. This tracker lets the
7
+ * receive path recognize that delta as its own echo and skip re-applying it,
8
+ * since the optimistic write already reflects it. Without this discriminator
9
+ * the apply path would re-apply the confirmation on top of state that may have
10
+ * changed since, producing a visible flicker.
11
+ *
12
+ * An entry is added with {@link markPending} and drained either by
13
+ * {@link consumeEcho} when the matching delta arrives, or by
14
+ * {@link drainOnRollback} when the originating transaction is cancelled.
15
+ *
16
+ * The set is bounded by `maxSize` to guard against unbounded growth if
17
+ * transactions are somehow never confirmed and never rolled back. When the
18
+ * bound is reached the oldest id is evicted first, since the backing Map
19
+ * iterates in insertion order. Eviction carries no correctness risk for the
20
+ * client that made the write, whose local state already reflects the change;
21
+ * the only cost is that a later echo of the evicted transaction is applied as
22
+ * if it were a foreign change, at worst a single redundant re-set.
23
+ */
24
+
25
+ export interface UnconfirmedWritesOptions {
26
+ /**
27
+ * The maximum number of unconfirmed transaction ids to track. Once this many
28
+ * are held, adding another evicts the oldest. Defaults to 10,000, which
29
+ * leaves ample headroom even for a client with many tabs open during a large
30
+ * bulk edit.
31
+ */
32
+ maxSize?: number;
33
+ }
34
+
35
+ export interface UnconfirmedWritesMetrics {
36
+ /** Total ids currently tracked. */
37
+ size: number;
38
+ /** Cumulative ids ever added since construction. */
39
+ totalAdded: number;
40
+ /** Cumulative successful echo matches (delta arrived → drained). */
41
+ hits: number;
42
+ /** Cumulative explicit rollback drains (transaction never made it). */
43
+ rollbacks: number;
44
+ /** Cumulative ids evicted due to maxSize pressure. */
45
+ evictions: number;
46
+ }
47
+
48
+ const DEFAULT_MAX_SIZE = 10_000;
49
+
50
+ export class UnconfirmedWrites {
51
+ // Map (not Set) for O(1) FIFO eviction via insertion order.
52
+ // Value is unused; Map.keys() iterates in insertion order so
53
+ // `keys().next()` yields the oldest id.
54
+ private readonly ids = new Map<string, true>();
55
+ private readonly maxSize: number;
56
+
57
+ // Metrics — internal counters; exposed via `getMetrics()`. Kept
58
+ // numeric (not BigInt) since cumulative-since-page-load fits well
59
+ // under Number.MAX_SAFE_INTEGER for any realistic session.
60
+ private _totalAdded = 0;
61
+ private _hits = 0;
62
+ private _rollbacks = 0;
63
+ private _evictions = 0;
64
+
65
+ constructor(options: UnconfirmedWritesOptions = {}) {
66
+ this.maxSize = options.maxSize ?? DEFAULT_MAX_SIZE;
67
+ }
68
+
69
+ /**
70
+ * Mark a transaction as locally-applied. The next sync delta with a
71
+ * matching `transactionId` will be recognized as the server's
72
+ * confirmation of this same write. Idempotent — repeated calls with
73
+ * the same id are no-ops.
74
+ */
75
+ markPending(transactionId: string): void {
76
+ if (this.ids.has(transactionId)) return;
77
+ if (this.ids.size >= this.maxSize) {
78
+ const oldest = this.ids.keys().next().value;
79
+ if (oldest !== undefined) {
80
+ this.ids.delete(oldest);
81
+ this._evictions += 1;
82
+ }
83
+ }
84
+ this.ids.set(transactionId, true);
85
+ this._totalAdded += 1;
86
+ }
87
+
88
+ /**
89
+ * Reports whether an incoming delta is this client's own echo, draining the
90
+ * entry if so. Returns true when the id was being tracked, signalling the
91
+ * caller to skip re-applying the change because the optimistic write already
92
+ * reflects it; returns false for a change that originated elsewhere, which
93
+ * should be applied normally.
94
+ *
95
+ * The check and drain are combined into one call so the receive path cannot
96
+ * accidentally treat two deltas carrying the same id, arriving in one batch,
97
+ * as separate echoes.
98
+ */
99
+ consumeEcho(transactionId: string | null | undefined): boolean {
100
+ if (!transactionId) return false;
101
+ if (!this.ids.has(transactionId)) return false;
102
+ this.ids.delete(transactionId);
103
+ this._hits += 1;
104
+ return true;
105
+ }
106
+
107
+ /**
108
+ * Drains the entry for a transaction that was rolled back before the server
109
+ * confirmed it. No echo will ever arrive for a cancelled transaction, so
110
+ * without this the pending entry would leak. Rollbacks are counted separately
111
+ * from confirmed echoes, so a rise in rollbacks relative to hits can flag
112
+ * network or server trouble.
113
+ */
114
+ drainOnRollback(transactionId: string): void {
115
+ if (this.ids.delete(transactionId)) {
116
+ this._rollbacks += 1;
117
+ }
118
+ }
119
+
120
+ getMetrics(): Readonly<UnconfirmedWritesMetrics> {
121
+ return {
122
+ size: this.ids.size,
123
+ totalAdded: this._totalAdded,
124
+ hits: this._hits,
125
+ rollbacks: this._rollbacks,
126
+ evictions: this._evictions,
127
+ };
128
+ }
129
+
130
+ clear(): void {
131
+ this.ids.clear();
132
+ }
133
+ }
@@ -0,0 +1,469 @@
1
+ import type { RuntimeContext } from '../../RuntimeContext.js';
2
+ import type { MutationQueueConfig } from './MutationQueue.js';
3
+ import type { MutationInput, QueuedMutation } from './commitPayload.js';
4
+ import type { MutationStore } from './MutationStore.js';
5
+ import type { OptimisticUpdateEntry } from './localMutation.js';
6
+ import type { StaleNotification } from '@abloatai/transaction/coordination/schema';
7
+ import type { DeltaConfirmationTracker } from './deltaConfirmation.js';
8
+ import type { MutationCommitResult } from '@abloatai/transaction/wire/commit';
9
+ import type { DurableCommitEnvelope } from '@abloatai/transaction/transactions/settlement/commitEnvelope';
10
+ import { AbloError, AbloNotFoundError } from '@abloatai/transaction/errors';
11
+ import { applyWriteOptions, normalizeModelKey, TX_TYPE_TO_MUTATION_OP, type WriteOperationFields } from './commitPayload.js';
12
+ import type { MutationOperationType } from '@abloatai/transaction/types';
13
+
14
+ export interface BatchProcessingContext {
15
+ readonly runtime: RuntimeContext;
16
+ readonly config: MutationQueueConfig;
17
+ readonly durableReplayBlock: object | null;
18
+ readonly executionQueue: QueuedMutation[];
19
+ readonly isProcessing: boolean;
20
+ readonly setIsProcessing: (value: boolean) => void;
21
+ readonly takeNextExecutionBatch: () => QueuedMutation[];
22
+ readonly ensureDerivedFields: (transaction: QueuedMutation) => void;
23
+ readonly ensureCommitEnvelope: (batch: QueuedMutation[]) => string;
24
+ readonly executingCount: number;
25
+ readonly setExecutingCount: (value: number) => void;
26
+ readonly inFlightByModel: Set<string>;
27
+ readonly pendingMergeByModel: Map<string, { data: MutationInput; sourceMutationIds: string[] }>;
28
+ readonly generateId: () => string;
29
+ readonly computePriorityScore: (type: QueuedMutation['type'], modelName: string) => number;
30
+ readonly store: MutationStore;
31
+ readonly enqueue: (transaction: QueuedMutation) => void;
32
+ readonly optimisticUpdates: Map<string, OptimisticUpdateEntry>;
33
+ readonly commitNotifications: Map<string, StaleNotification[]>;
34
+ readonly commitMissingIds: Map<string, string[]>;
35
+ readonly sourceMutationIdsFor: (batch: readonly QueuedMutation[]) => string[];
36
+ readonly dispatchCommitBounded: (...args: Parameters<import('../../interfaces/index.js').MutationExecutor['commit']>) => ReturnType<import('../../interfaces/index.js').MutationExecutor['commit']>;
37
+ readonly parseMutationCommitResult: (value: Awaited<ReturnType<import('../../interfaces/index.js').MutationExecutor['commit']>>) => MutationCommitResult;
38
+ readonly persistDurableCommitAcceptance: (envelope: DurableCommitEnvelope, result: MutationCommitResult) => Promise<DurableCommitEnvelope>;
39
+ readonly removeDurableCommit: (idempotencyKey: string) => Promise<void>;
40
+ readonly assertEnvelopeInsideReplayWindow: (envelope: DurableCommitEnvelope) => void;
41
+ readonly sealDurableCommit: (input: Parameters<typeof import('./commitTransport.js').sealDurableCommit>[1]) => Promise<DurableCommitEnvelope>;
42
+ readonly noteAck: (syncId: number | undefined) => void;
43
+ readonly classifyReceiptNotifications: (
44
+ operations: readonly { model: string; id: string }[],
45
+ notifications: readonly StaleNotification[],
46
+ ) => { holdsEntireBatch: boolean; heldTargets: Set<string>; notificationsByTarget: Map<string, StaleNotification[]> };
47
+ readonly receiptTargetKey: (modelName: string, modelId: string) => string;
48
+ readonly scheduleReplicationLagTimeout: (transactionId: string, clientTxId?: string, correlationId?: string) => void;
49
+ readonly scheduleDeltaConfirmationTimeout: (transaction: QueuedMutation, timeoutMs: number) => void;
50
+ readonly clearReplicationLagState: (transactionId: string) => void;
51
+ readonly completeQueuedCommit: (transaction: import('./commitLane.js').CommitTransaction, syncId: number) => void;
52
+ readonly queuedCommitMatchesCorrelation: (transaction: import('./commitLane.js').CommitTransaction, correlationId: string) => boolean;
53
+ readonly recentDeltaCorrelations: Map<string, number>;
54
+ readonly lastSeenSyncId: number;
55
+ readonly scheduleProcessing: (immediate?: boolean) => void;
56
+ readonly handleFailure: (transaction: QueuedMutation, error: Error) => Promise<void>;
57
+ readonly isDefinitiveRejection: (error: Error) => boolean;
58
+ readonly emitCommitLifecycle: (event: string, payload: object) => void;
59
+ readonly emit: (event: string, payload: object) => boolean;
60
+ readonly rollbackOptimistic: (transaction: QueuedMutation, reason: string, error?: Error) => Promise<void>;
61
+ }
62
+
63
+ export async function processBatch(ctx: BatchProcessingContext): Promise<void> { if (ctx.durableReplayBlock) return;
64
+ const batchStart = typeof performance !== 'undefined' ? performance.now() : Date.now();
65
+
66
+ if (ctx.isProcessing || ctx.executionQueue.length === 0) {
67
+ return;
68
+ }
69
+
70
+ ctx.setIsProcessing(true);
71
+
72
+ // Declare batch outside try so it's accessible in finally for backpressure tracking
73
+ let batch: QueuedMutation[] = [];
74
+
75
+ await ctx.runtime.observability.startSpanAsync(
76
+ 'sync.batch',
77
+ 'sync.transaction.batch',
78
+ async () => {
79
+ try {
80
+ // Sort the execution queue by foreign-key priority before selecting a
81
+ // batch, so a parent row is always committed before its children, even
82
+ // across batch boundaries.
83
+ ctx.executionQueue.sort((a, b) => {
84
+ // Ensure derived fields exist (covers restored/persisted transactions)
85
+ ctx.ensureDerivedFields(a);
86
+ ctx.ensureDerivedFields(b);
87
+ if (a.modelName === b.modelName && a.modelId === b.modelId && a.type !== b.type) {
88
+ if (a.type === 'create') return -1;
89
+ if (b.type === 'create') return 1;
90
+ }
91
+ return a.priorityScore - b.priorityScore;
92
+ });
93
+
94
+ // Take a fresh batch or one complete retry envelope. Retry envelopes
95
+ // retain both their original membership and operation order.
96
+ batch = ctx.takeNextExecutionBatch();
97
+ if (batch.length === 0) return;
98
+ const commitIdempotencyKey = ctx.ensureCommitEnvelope(batch);
99
+
100
+ // Track executing count for backpressure
101
+ ctx.setExecutingCount(ctx.executingCount + batch.length);
102
+
103
+ // Mark all as executing
104
+ for (const tx of batch) {
105
+ const key = `${tx.modelName}:${tx.modelId}`;
106
+ if (tx.type === 'update') ctx.inFlightByModel.add(key);
107
+ ctx.store.updateStatus(tx.id, 'executing');
108
+ }
109
+
110
+ // Build every operation for one unified commit (a single round trip).
111
+ const batchOps: {
112
+ tx: QueuedMutation;
113
+ op: {
114
+ type: MutationOperationType;
115
+ model: string;
116
+ id: string;
117
+ input?: Record<string, unknown>;
118
+ transactionId: string;
119
+ } & WriteOperationFields;
120
+ }[] = [];
121
+
122
+ for (const tx of batch) {
123
+ // The per-operation `transactionId` carries the local transaction id
124
+ // over the wire so the server can stamp it on the resulting sync
125
+ // delta. The receive path matches it through
126
+ // {@link UnconfirmedWrites.consumeEcho} to avoid applying the
127
+ // client's own optimistic change twice. This is separate from the
128
+ // batch-level idempotency key recorded in `mutation_log`.
129
+ const op = applyWriteOptions({
130
+ type: TX_TYPE_TO_MUTATION_OP[tx.type],
131
+ model: tx.modelKey,
132
+ id: tx.modelId,
133
+ input: tx.type === 'create' || tx.type === 'update' ? tx.data || {} : undefined,
134
+ transactionId: tx.id,
135
+ }, tx);
136
+ batchOps.push({ tx, op });
137
+ }
138
+
139
+ // Execute the unified commit for every operation in one round trip.
140
+ if (batchOps.length > 0) {
141
+ let dispatchStarted = false;
142
+ try {
143
+ const durableEnvelope = await ctx.sealDurableCommit({
144
+ idempotencyKey: commitIdempotencyKey,
145
+ origin: 'model_batch',
146
+ operations: batchOps.map(({ op }) => op),
147
+ sourceMutationIds: ctx.sourceMutationIdsFor(batch),
148
+ createdAt: Math.min(...batch.map((transaction) => transaction.createdAt)),
149
+ sealedAt: batch[0]?.commitEnvelope?.sealedAt ?? Date.now(),
150
+ sequence: batch[0]?.commitEnvelope?.sequence,
151
+ });
152
+ const operations = durableEnvelope.operations;
153
+
154
+ // Capture lastSyncId from the server response for threshold-based
155
+ // confirmation.
156
+ //
157
+ // The queue owns request identity. A lost acknowledgement may be
158
+ // retried after a backoff or reconnect, so every retry must send
159
+ // the exact key assigned before the first transport attempt.
160
+ ctx.assertEnvelopeInsideReplayWindow(durableEnvelope);
161
+ dispatchStarted = true;
162
+ const result = ctx.parseMutationCommitResult(
163
+ await ctx.dispatchCommitBounded(operations, {
164
+ idempotencyKey: commitIdempotencyKey,
165
+ }),
166
+ );
167
+ await ctx.persistDurableCommitAcceptance(
168
+ durableEnvelope,
169
+ result,
170
+ );
171
+ const lastSyncId = result.lastSyncId;
172
+
173
+ const notifications = result.notifications;
174
+ const {
175
+ holdsEntireBatch,
176
+ heldTargets,
177
+ notificationsByTarget,
178
+ } = ctx.classifyReceiptNotifications(
179
+ batchOps.map(({ op }) => ({ model: op.model, id: op.id })),
180
+ notifications ?? [],
181
+ );
182
+ for (const { tx } of batchOps) {
183
+ const txTarget = ctx.receiptTargetKey(tx.modelKey, tx.modelId);
184
+ const txNotifs = holdsEntireBatch
185
+ ? notifications
186
+ : notificationsByTarget.get(txTarget);
187
+ if (txNotifs && txNotifs.length > 0) {
188
+ ctx.commitNotifications.set(tx.id, txNotifs);
189
+ }
190
+ }
191
+ const missingIds = new Set(result.missingIds ?? []);
192
+ const settlingBatchOps: typeof batchOps = [];
193
+ for (const entry of batchOps) {
194
+ const { tx } = entry;
195
+ if (missingIds.has(tx.modelId)) {
196
+ await ctx.handleFailure(
197
+ tx,
198
+ new AbloNotFoundError(
199
+ `${tx.modelName}/${tx.modelId} was not found or is outside this credential's scope.`,
200
+ [tx.modelId],
201
+ ),
202
+ );
203
+ continue;
204
+ }
205
+ if (
206
+ holdsEntireBatch ||
207
+ heldTargets.has(ctx.receiptTargetKey(tx.modelKey, tx.modelId))
208
+ ) {
209
+ await ctx.rollbackOptimistic(tx, 'conflict_server_wins');
210
+ ctx.store.updateStatus(tx.id, 'completed');
211
+ ctx.emit('transaction:completed', tx);
212
+ ctx.emit(`transaction:completed:${tx.id}`, tx);
213
+ ctx.optimisticUpdates.delete(tx.id);
214
+ continue;
215
+ }
216
+ settlingBatchOps.push(entry);
217
+ }
218
+
219
+ // A forwarded write has been accepted, but the source database
220
+ // remains authoritative. It must not enter any of the confirmed
221
+ // paths below: in particular, a queued DELETE with sync id 0 is
222
+ // not the hosted-path idempotent-delete shortcut, and a later
223
+ // unrelated delta cannot stand in for the source echo.
224
+ if (result.status === 'queued') {
225
+ // Keep the exact envelope crash-durable while any forwarded
226
+ // member is awaiting its WAL echo. If coordination/missing-row
227
+ // handling consumed the whole request, there is no echo left
228
+ // to await and the durable intent is definitive.
229
+ if (settlingBatchOps.length === 0) {
230
+ await ctx.removeDurableCommit(commitIdempotencyKey);
231
+ }
232
+ for (const { tx } of settlingBatchOps) {
233
+ tx.requiresCorrelatedDelta = true;
234
+ tx.syncIdNeededForCompletion = undefined;
235
+ tx.correlationId = result.correlationId;
236
+ const echoSyncId = result.correlationId
237
+ ? ctx.recentDeltaCorrelations.get(result.correlationId)
238
+ : undefined;
239
+ if (echoSyncId !== undefined) {
240
+ ctx.store.updateStatus(tx.id, 'completed');
241
+ ctx.emit('transaction:completed', tx);
242
+ ctx.emit(`transaction:completed:${tx.id}`, tx);
243
+ ctx.optimisticUpdates.delete(tx.id);
244
+ continue;
245
+ }
246
+ ctx.store.updateStatus(tx.id, 'awaiting_delta');
247
+ ctx.scheduleReplicationLagTimeout(
248
+ tx.id,
249
+ commitIdempotencyKey,
250
+ result.correlationId,
251
+ );
252
+ ctx.runtime.logger.debug('tx:awaiting_delta', {
253
+ txId: tx.id.slice(0, 8),
254
+ model: tx.modelName,
255
+ reason: 'queued_forward_waiting_for_correlated_echo',
256
+ });
257
+ ctx.scheduleDeltaConfirmationTimeout(
258
+ tx,
259
+ ctx.config.deltaConfirmationTimeout,
260
+ );
261
+ }
262
+ if (
263
+ settlingBatchOps.length > 0 &&
264
+ settlingBatchOps.every(({ tx }) => tx.status === 'completed')
265
+ ) {
266
+ await ctx.removeDurableCommit(commitIdempotencyKey);
267
+ }
268
+ } else {
269
+ await ctx.removeDurableCommit(commitIdempotencyKey);
270
+ ctx.noteAck(lastSyncId);
271
+
272
+ // A lastSyncId of 0 means the mutation succeeded but the server
273
+ // emitted no sync delta; record that anomaly for observability.
274
+ if (lastSyncId === 0) {
275
+ ctx.runtime.observability.captureCommitZeroSyncId({
276
+ operationCount: operations.length,
277
+ operations: operations.map(
278
+ (op) => `${op.type}:${op.model}:${op.id?.slice(0, 8) ?? '?'}`
279
+ ),
280
+ });
281
+ }
282
+
283
+ // Mark each remaining transaction with the sync-id threshold
284
+ // that will confirm it: any delta whose id is at least
285
+ // lastSyncId.
286
+ for (const { tx } of settlingBatchOps) {
287
+ tx.syncIdNeededForCompletion = lastSyncId;
288
+
289
+ // Safety net: a confirmed zero-sync DELETE is idempotent.
290
+ // This shortcut is intentionally unreachable for queued
291
+ // forwards, which must wait for their correlated echo.
292
+ if (lastSyncId === 0 && tx.type === 'delete') {
293
+ ctx.store.updateStatus(tx.id, 'completed');
294
+ ctx.emit('transaction:completed', tx);
295
+ ctx.emit(`transaction:completed:${tx.id}`, tx);
296
+ ctx.optimisticUpdates.delete(tx.id);
297
+ ctx.runtime.logger.debug('tx:confirm_delete_zero_syncid', {
298
+ txId: tx.id.slice(0, 8),
299
+ model: tx.modelName,
300
+ reason: 'delete_idempotent_no_delta',
301
+ });
302
+ continue;
303
+ }
304
+
305
+ // A real watermark is the established hosted-path
306
+ // confirmation. A zero watermark on a non-delete stays on
307
+ // the anomaly/reconciliation path.
308
+ if (lastSyncId > 0) {
309
+ ctx.store.updateStatus(tx.id, 'completed');
310
+ ctx.emit('transaction:completed', tx);
311
+ ctx.emit(`transaction:completed:${tx.id}`, tx);
312
+ ctx.optimisticUpdates.delete(tx.id);
313
+ ctx.runtime.logger.debug('tx:confirm_ack', {
314
+ txId: tx.id.slice(0, 8),
315
+ model: tx.modelName,
316
+ serverSyncId: lastSyncId,
317
+ lastSeenSyncId: ctx.lastSeenSyncId,
318
+ });
319
+ } else {
320
+ ctx.store.updateStatus(tx.id, 'awaiting_delta');
321
+ ctx.runtime.logger.debug('tx:awaiting_delta', {
322
+ txId: tx.id.slice(0, 8),
323
+ model: tx.modelName,
324
+ neededSyncId: lastSyncId,
325
+ lastSeenSyncId: ctx.lastSeenSyncId,
326
+ reason: 'zero_sync_id_anomaly',
327
+ });
328
+ ctx.scheduleDeltaConfirmationTimeout(
329
+ tx,
330
+ ctx.config.deltaConfirmationTimeout,
331
+ );
332
+ }
333
+ }
334
+ }
335
+ } catch (error) {
336
+ const errorMessage = (error as Error).message || '';
337
+ if (dispatchStarted && ctx.isDefinitiveRejection(error as Error)) {
338
+ await ctx.removeDurableCommit(commitIdempotencyKey);
339
+ }
340
+ // Surface the raw server rejection for the whole batch so a
341
+ // cascaded failure — for example a foreign-key violation that
342
+ // rolls back a multi-operation transaction — is attributable to a
343
+ // specific cause rather than showing as a generic permanent error
344
+ // on each operation.
345
+ const abloErr = error instanceof AbloError ? error : undefined;
346
+ // SyncWebSocket attaches a `diagnostics` snapshot to its
347
+ // "not connected" / "closed while in flight" rejections.
348
+ // Surface it here so the warn line attributes the drop to
349
+ // a specific cause (handshake reject, heartbeat zombie,
350
+ // session expiry, …) instead of just "AbloConnectionError".
351
+ const readDiagnostics = (e: unknown): unknown => {
352
+ let cur: unknown = e;
353
+ // Walk up to 3 wrap layers (current err → its cause → its
354
+ // cause's cause) so diagnostics survive AbloConnectionError
355
+ // wrapping in Ablo.commit() and any future wrappers.
356
+ for (let i = 0; i < 3 && cur && typeof cur === 'object'; i++) {
357
+ if ('diagnostics' in cur && (cur as { diagnostics?: unknown }).diagnostics) {
358
+ return (cur).diagnostics;
359
+ }
360
+ cur = (cur as { cause?: unknown }).cause;
361
+ }
362
+ return undefined;
363
+ };
364
+ const diagnostics = readDiagnostics(error);
365
+ // Mechanic-level breadcrumb. Every batch rejection — transient
366
+ // (reconnect retries it) or permanent (`handleFailure` logs the
367
+ // authoritative `warn` with the same typed cause) — passes
368
+ // through here. Logging it at `warn` made one rejected write
369
+ // surface three identical dumps; keep it at `debug`.
370
+ ctx.runtime.logger.debug('[MutationQueue] Batch commit rejected', {
371
+ batchSize: batchOps.length,
372
+ models: batchOps.map(({ op }) => `${op.type}:${op.model}`),
373
+ errorType: abloErr?.type ?? (error as Error)?.name,
374
+ errorCode: abloErr?.code,
375
+ httpStatus: abloErr?.httpStatus,
376
+ requestId: abloErr?.requestId,
377
+ message: errorMessage,
378
+ diagnostics,
379
+ });
380
+
381
+ // Handle "no rows in result set" gracefully: the row was already
382
+ // deleted, so for update and delete operations this is effectively
383
+ // success — the intended end state already holds — and the
384
+ // transaction is treated as completed.
385
+ if (errorMessage.includes('no rows in result set')) {
386
+ if (dispatchStarted) {
387
+ await ctx.removeDurableCommit(commitIdempotencyKey);
388
+ }
389
+ ctx.runtime.logger.info('[MutationQueue] Graceful handling: entity already deleted', {
390
+ batchSize: batchOps.length,
391
+ });
392
+
393
+ for (const { tx, op } of batchOps) {
394
+ if (op.type === 'UPDATE' || op.type === 'DELETE') {
395
+ // Row already gone: the intended state holds, mark completed.
396
+ ctx.store.updateStatus(tx.id, 'completed');
397
+ ctx.emit('transaction:completed', tx);
398
+
399
+ ctx.runtime.logger.debug('[MutationQueue] Orphaned transaction treated as success', {
400
+ txId: tx.id.slice(0, 12),
401
+ model: tx.modelName,
402
+ type: op.type,
403
+ });
404
+ } else {
405
+ // CREATE operations on non-existent parent are real failures
406
+ await ctx.handleFailure(tx, error as Error);
407
+ }
408
+ }
409
+ } else {
410
+ // Handle other batch failures - mark all as failed
411
+ for (const { tx } of batchOps) {
412
+ await ctx.handleFailure(tx, error as Error);
413
+ }
414
+ }
415
+ }
416
+ }
417
+
418
+ // Handle post-execution merge for updates
419
+ for (const tx of batch) {
420
+ const key = `${tx.modelName}:${tx.modelId}`;
421
+ if (tx.type === 'update') {
422
+ ctx.inFlightByModel.delete(key);
423
+ const pending = ctx.pendingMergeByModel.get(key);
424
+ if (pending && Object.keys(pending.data).length > 0) {
425
+ // Create a single merged follow-up transaction
426
+ const followUp: QueuedMutation = {
427
+ id: ctx.generateId(),
428
+ type: 'update',
429
+ modelName: tx.modelName,
430
+ modelId: tx.modelId,
431
+ modelKey: tx.modelKey ?? normalizeModelKey(tx.modelName),
432
+ data: pending.data,
433
+ previousData: undefined,
434
+ context: tx.context,
435
+ status: 'pending',
436
+ createdAt: Date.now(),
437
+ attempts: 0,
438
+ priority: 'normal',
439
+ priorityScore: ctx.computePriorityScore('update', tx.modelName),
440
+ sourceMutationIds: pending.sourceMutationIds,
441
+ };
442
+ ctx.pendingMergeByModel.delete(key);
443
+ ctx.store.add(followUp);
444
+ ctx.enqueue(followUp);
445
+ }
446
+ }
447
+ }
448
+ } finally {
449
+ ctx.setIsProcessing(false);
450
+
451
+ // Decrement executing count for backpressure tracking
452
+ ctx.setExecutingCount(ctx.executingCount - batch.length);
453
+
454
+ // Process next batch if needed
455
+ if (ctx.executionQueue.length > 0 && batch.length > 0) {
456
+ ctx.scheduleProcessing(true);
457
+ }
458
+
459
+ const batchEnd = typeof performance !== 'undefined' ? performance.now() : Date.now();
460
+ ctx.runtime.logger.debug('txn:batch', batchEnd - batchStart, {
461
+ maxBatchSize: ctx.config.maxBatchSize,
462
+ remaining: ctx.executionQueue.length,
463
+ executingCount: ctx.executingCount,
464
+ });
465
+ }
466
+ },
467
+ { batchSize: ctx.executionQueue.length + (batch?.length ?? 0) }
468
+ );
469
+ }