@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,298 @@
1
+ /**
2
+ * Tracks whether the confirming delta for a write has arrived. It holds the
3
+ * acknowledgement watermark (through the shared {@link LogPosition}), the
4
+ * per-transaction confirmation timeouts, and the retry-with-backoff and
5
+ * reconciliation policy for transactions in the `awaiting_delta` status. It
6
+ * reaches back to {@link MutationQueue} only through the small
7
+ * {@link DeltaConfirmationContext} interface, not the queue class itself, so it
8
+ * has no cyclic dependency and can be tested on its own.
9
+ */
10
+
11
+ import { globalRuntime } from '../../context.js';
12
+ import type { RuntimeContext } from '../../RuntimeContext.js';
13
+ import type { LogPositionPort } from '../../logPosition.js';
14
+ import type { QueuedMutation } from './commitPayload.js';
15
+
16
+ /**
17
+ * The subset of {@link MutationQueue} that the confirmation tracker needs:
18
+ * store lookups and status changes, removing optimistic entries once a write
19
+ * confirms, the queue's event emitter (`transaction:completed`,
20
+ * `reconciliation:needed`, and so on), a connection check (so timeouts
21
+ * re-schedule instead of escalating while offline), and the shared client
22
+ * position (`noteAck` advances the acknowledgement cursor; diagnostics read the
23
+ * applied cursor).
24
+ */
25
+ export interface DeltaConfirmationContext {
26
+ store: {
27
+ get(id: string): QueuedMutation | undefined;
28
+ getByStatus(status: QueuedMutation['status']): QueuedMutation[];
29
+ updateStatus(id: string, status: QueuedMutation['status']): void;
30
+ };
31
+ optimisticUpdates: { delete(id: string): boolean };
32
+ emit(event: string, payload?: unknown): void;
33
+ isConnected(): boolean;
34
+ position: LogPositionPort;
35
+ /** The owning client's runtime. Defaults to the module-global bridge. */
36
+ runtime?: RuntimeContext;
37
+ }
38
+
39
+ export class DeltaConfirmationTracker {
40
+ // Retry configuration for delta confirmation, using exponential backoff.
41
+ // Maximum retries before requesting a full reconciliation.
42
+ private static readonly DELTA_MAX_RETRIES = 5;
43
+ // Upper bound on the backoff timeout.
44
+ private static readonly DELTA_MAX_TIMEOUT_MS = 120_000;
45
+
46
+ // Pending confirmation timeouts for transactions awaiting their delta. On
47
+ // timeout the tracker retries with backoff rather than rolling back.
48
+ private deltaConfirmationTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
49
+
50
+ // Track retry attempts per transaction for exponential backoff
51
+ private deltaConfirmationRetries = new Map<string, number>();
52
+
53
+ private readonly runtime: RuntimeContext;
54
+
55
+ constructor(private readonly ctx: DeltaConfirmationContext) {
56
+ this.runtime = ctx.runtime ?? globalRuntime;
57
+ }
58
+
59
+ /** Applied-cursor alias, kept so the read sites below stay legible. */
60
+ private get lastSeenSyncId(): number {
61
+ return this.ctx.position.applied;
62
+ }
63
+
64
+ private matchesSourceEcho(
65
+ tx: QueuedMutation,
66
+ correlationId: string | undefined,
67
+ ): boolean {
68
+ return (
69
+ correlationId !== undefined &&
70
+ tx.correlationId !== undefined &&
71
+ correlationId === tx.correlationId
72
+ );
73
+ }
74
+
75
+ noteAck(lastSyncId: number | undefined): void {
76
+ this.ctx.position.noteAck(lastSyncId);
77
+ }
78
+
79
+ /**
80
+ * Confirms every awaiting transaction whose sync-id threshold this delta
81
+ * meets or exceeds.
82
+ * @param syncId - The sync id of the received delta.
83
+ */
84
+ onDeltaReceived(syncId: number, correlationId?: string): void {
85
+ // The cursor advances where the delta is applied (the store calls
86
+ // position.advanceApplied / advancePersisted); this hook only resolves
87
+ // confirmation thresholds against the incoming id.
88
+
89
+ const awaitingTxs = this.ctx.store.getByStatus('awaiting_delta');
90
+ const executingTxs = this.ctx.store.getByStatus('executing');
91
+
92
+ // Debug: Show state when delta arrives
93
+ if (awaitingTxs.length > 0 || executingTxs.length > 0) {
94
+ this.runtime.logger.debug('tx:delta_received', {
95
+ syncId,
96
+ lastSeenSyncId: this.lastSeenSyncId,
97
+ awaitingCount: awaitingTxs.length,
98
+ executingCount: executingTxs.length,
99
+ awaitingThresholds: awaitingTxs.map((tx) => ({
100
+ txId: tx.id.slice(0, 8),
101
+ model: tx.modelName,
102
+ needed: tx.syncIdNeededForCompletion,
103
+ requiresCorrelatedDelta: tx.requiresCorrelatedDelta === true,
104
+ willConfirm:
105
+ tx.requiresCorrelatedDelta === true
106
+ ? this.matchesSourceEcho(tx, correlationId)
107
+ : tx.syncIdNeededForCompletion !== undefined &&
108
+ syncId >= tx.syncIdNeededForCompletion,
109
+ })),
110
+ });
111
+ }
112
+
113
+ // Fast path: no awaiting transactions
114
+ if (awaitingTxs.length === 0) return;
115
+
116
+ let confirmedCount = 0;
117
+
118
+ for (const tx of awaitingTxs) {
119
+ // Queued forward receipts deliberately have no watermark. They confirm
120
+ // only when the authoritative source echoes the receipt's opaque,
121
+ // authenticated-scope batch identity; the legacy anomaly path continues
122
+ // to use its sync-id threshold.
123
+ const confirmedByCorrelation =
124
+ tx.requiresCorrelatedDelta === true &&
125
+ this.matchesSourceEcho(tx, correlationId);
126
+ const confirmedByThreshold =
127
+ tx.requiresCorrelatedDelta !== true &&
128
+ tx.syncIdNeededForCompletion !== undefined &&
129
+ syncId >= tx.syncIdNeededForCompletion;
130
+ if (confirmedByCorrelation || confirmedByThreshold) {
131
+ this.cancelDeltaConfirmationTimeout(tx.id);
132
+ this.ctx.store.updateStatus(tx.id, 'completed');
133
+ this.ctx.emit('transaction:completed', tx);
134
+ this.ctx.emit(`transaction:completed:${tx.id}`, tx);
135
+ this.ctx.optimisticUpdates.delete(tx.id);
136
+ confirmedCount++;
137
+
138
+ this.runtime.logger.debug('tx:confirm_via_delta', {
139
+ txId: tx.id.slice(0, 8),
140
+ model: tx.modelName,
141
+ neededSyncId: tx.syncIdNeededForCompletion,
142
+ receivedSyncId: syncId,
143
+ confirmation: confirmedByCorrelation ? 'source_correlation' : 'sync_id',
144
+ });
145
+ }
146
+ }
147
+
148
+ // Log batch summary only if we confirmed something
149
+ if (confirmedCount > 0) {
150
+ // Leave a breadcrumb when transactions confirm.
151
+ this.runtime.observability.breadcrumb('Transactions confirmed via delta', 'sync.transaction', 'info', {
152
+ count: confirmedCount,
153
+ syncId,
154
+ remainingAwaiting: awaitingTxs.length - confirmedCount,
155
+ });
156
+ }
157
+ }
158
+
159
+ // Schedule the confirmation wait for a transaction. On timeout the tracker
160
+ // retries with exponential backoff and requests reconciliation to catch up on
161
+ // missed deltas, rather than rolling back, which would discard state the
162
+ // server has already confirmed. A rollback happens only on an explicit server
163
+ // rejection, never on a timeout.
164
+ scheduleDeltaConfirmationTimeout(tx: QueuedMutation, timeoutMs: number): void {
165
+ // Cancel any existing timeout for this transaction
166
+ this.cancelDeltaConfirmationTimeout(tx.id);
167
+
168
+ // Deliberately not an async callback: the body is fully synchronous, and
169
+ // `setTimeout(async …)` would turn any throw into an unhandled promise
170
+ // rejection instead of a catchable synchronous error.
171
+ const timeoutHandle = setTimeout(() => {
172
+ const currentTx = this.ctx.store.get(tx.id);
173
+ if (currentTx?.status !== 'awaiting_delta') {
174
+ this.deltaConfirmationRetries.delete(tx.id);
175
+ return; // Already confirmed or failed
176
+ }
177
+
178
+ // If disconnected, re-schedule with same timeout (no backoff while offline)
179
+ if (!this.ctx.isConnected()) {
180
+ // Self-healing: re-schedule the confirmation wait while offline, no
181
+ // consumer action needed → debug.
182
+ this.runtime.logger.debug('[MutationQueue] Timeout fired while disconnected - re-scheduling', {
183
+ txId: tx.id.slice(0, 8),
184
+ model: tx.modelName,
185
+ });
186
+ this.deltaConfirmationTimeouts.delete(tx.id);
187
+ this.scheduleDeltaConfirmationTimeout(tx, timeoutMs);
188
+ return;
189
+ }
190
+
191
+ const retryCount = this.deltaConfirmationRetries.get(tx.id) ?? 0;
192
+
193
+ this.runtime.observability.captureReconciliation({
194
+ reason: 'delta_timeout',
195
+ model: tx.modelName,
196
+ modelId: tx.modelId,
197
+ syncIdNeeded: currentTx.syncIdNeededForCompletion,
198
+ lastSeenSyncId: this.lastSeenSyncId,
199
+ retryCount,
200
+ connectionState: this.ctx.isConnected() ? 'connected' : 'disconnected',
201
+ });
202
+
203
+ if (retryCount < DeltaConfirmationTracker.DELTA_MAX_RETRIES) {
204
+ // Retry: request reconciliation and re-schedule with exponential
205
+ // backoff. The server has already committed the mutation; only the
206
+ // delta is outstanding.
207
+ this.deltaConfirmationRetries.set(tx.id, retryCount + 1);
208
+ this.deltaConfirmationTimeouts.delete(tx.id);
209
+
210
+ // Exponential backoff: 30s → 60s → 120s → 120s → 120s (capped)
211
+ const nextTimeout = Math.min(timeoutMs * 2, DeltaConfirmationTracker.DELTA_MAX_TIMEOUT_MS);
212
+
213
+ // Request reconciliation so the client can cycle the connection and
214
+ // catch up on missed deltas from the server.
215
+ this.ctx.emit('reconciliation:needed', {
216
+ reason: 'delta_confirmation_timeout',
217
+ txId: tx.id,
218
+ model: tx.modelName,
219
+ modelId: tx.modelId,
220
+ syncIdNeeded: currentTx.syncIdNeededForCompletion,
221
+ lastSeenSyncId: this.lastSeenSyncId,
222
+ retryCount: retryCount + 1,
223
+ });
224
+
225
+ // Self-healing retry with backoff — the server already committed; we're
226
+ // just waiting on the delta. No consumer action → debug.
227
+ this.runtime.logger.debug('[MutationQueue] Re-scheduling with backoff', {
228
+ txId: tx.id.slice(0, 8),
229
+ model: tx.modelName,
230
+ nextTimeoutMs: nextTimeout,
231
+ retry: retryCount + 1,
232
+ });
233
+
234
+ this.scheduleDeltaConfirmationTimeout(tx, nextTimeout);
235
+ } else {
236
+ // Retries exhausted: persist the awaiting state instead of rolling back.
237
+ // The commit succeeded on the server, so the data exists there. Saving
238
+ // the awaiting state lets it survive the page closing; on the next
239
+ // session, reconnecting and catching up on deltas will confirm it.
240
+ this.deltaConfirmationRetries.delete(tx.id);
241
+ this.deltaConfirmationTimeouts.delete(tx.id);
242
+
243
+ this.runtime.observability.captureDeltaRetryExhausted({
244
+ txId: tx.id,
245
+ model: tx.modelName,
246
+ modelId: tx.modelId,
247
+ retryCount: DeltaConfirmationTracker.DELTA_MAX_RETRIES,
248
+ syncIdNeeded: currentTx.syncIdNeededForCompletion,
249
+ });
250
+
251
+ // Emit the persist event; the client performs the write to local storage.
252
+ this.ctx.emit('transaction:persist_awaiting', {
253
+ txId: tx.id,
254
+ model: tx.modelName,
255
+ modelId: tx.modelId,
256
+ operationType: tx.type,
257
+ syncIdNeeded: currentTx.syncIdNeededForCompletion,
258
+ });
259
+
260
+ // Also request one final reconciliation cycle
261
+ this.ctx.emit('reconciliation:needed', {
262
+ reason: 'delta_retries_exhausted',
263
+ txId: tx.id,
264
+ model: tx.modelName,
265
+ modelId: tx.modelId,
266
+ syncIdNeeded: currentTx.syncIdNeededForCompletion,
267
+ lastSeenSyncId: this.lastSeenSyncId,
268
+ retryCount: DeltaConfirmationTracker.DELTA_MAX_RETRIES,
269
+ });
270
+ }
271
+ }, timeoutMs);
272
+
273
+ this.deltaConfirmationTimeouts.set(tx.id, timeoutHandle);
274
+ }
275
+
276
+ // Cancel a pending delta confirmation timeout and clean up retry tracking
277
+ private cancelDeltaConfirmationTimeout(id: string): void {
278
+ const timeoutHandle = this.deltaConfirmationTimeouts.get(id);
279
+ if (timeoutHandle) {
280
+ clearTimeout(timeoutHandle);
281
+ this.deltaConfirmationTimeouts.delete(id);
282
+ }
283
+ this.deltaConfirmationRetries.delete(id);
284
+ }
285
+
286
+ /**
287
+ * Clears every armed confirmation timer, one per in-flight transaction.
288
+ * {@link MutationQueue.dispose} calls this; without it a disposed queue
289
+ * would keep the process alive and fire callbacks against a cleared store.
290
+ */
291
+ dispose(): void {
292
+ for (const timeoutHandle of this.deltaConfirmationTimeouts.values()) {
293
+ clearTimeout(timeoutHandle);
294
+ }
295
+ this.deltaConfirmationTimeouts.clear();
296
+ this.deltaConfirmationRetries.clear();
297
+ }
298
+ }
@@ -0,0 +1,128 @@
1
+ import { AbloIdempotencyError, AbloValidationError } from '@abloatai/transaction/errors';
2
+ import type { RuntimeContext } from '../../RuntimeContext.js';
3
+ import type { CommitTransaction } from './commitLane.js';
4
+ import type { DurableCommitEnvelope, CommitOutboxScope } from '@abloatai/transaction/transactions/settlement/commitEnvelope';
5
+ import type { DurableWriteStore } from './durableWriteStore.js';
6
+ import { durableCommitEnvelopeSchema } from '@abloatai/transaction/transactions/settlement/commitEnvelope';
7
+
8
+ export interface DurableCommitRestoreContext {
9
+ readonly config: { enablePersistence: boolean };
10
+ readonly commitOutbox: DurableWriteStore | null;
11
+ readonly commitOutboxScope: CommitOutboxScope | null;
12
+ readonly commitStore: Map<string, CommitTransaction>;
13
+ readonly commitLane: CommitTransaction[];
14
+ readonly runtime: RuntimeContext;
15
+ readonly processCommitLane: () => Promise<void>;
16
+ readonly durableReplayWindowMs: number;
17
+ }
18
+
19
+ export async function restoreDurableCommits(ctx: DurableCommitRestoreContext): Promise<Set<string>> {
20
+ if (!ctx.config.enablePersistence) return new Set();
21
+
22
+ const sourceMutationIds = new Set<string>();
23
+ try {
24
+ if (!ctx.commitOutbox) return sourceMutationIds;
25
+ const rows = await ctx.commitOutbox.list();
26
+ const envelopes: DurableCommitEnvelope[] = [];
27
+ for (const row of rows) {
28
+ if (
29
+ typeof row !== 'object' ||
30
+ row === null ||
31
+ (row as { type?: unknown }).type !== 'commit_envelope'
32
+ ) continue;
33
+ const parsed = durableCommitEnvelopeSchema.safeParse(row);
34
+ if (parsed.success) {
35
+ envelopes.push(parsed.data);
36
+ } else {
37
+ ctx.runtime.logger.warn('A saved local write is unreadable and was held for review.');
38
+ ctx.runtime.observability.captureMutationFailure({
39
+ context: 'restore-commit-envelope',
40
+ error: parsed.error,
41
+ });
42
+ throw new AbloValidationError(
43
+ 'A saved commit envelope is unreadable; replay stopped before newer writes were sent.',
44
+ { code: 'write_options_invalid', cause: parsed.error },
45
+ );
46
+ }
47
+ }
48
+ envelopes.sort(
49
+ (a, b) =>
50
+ (a.sequence ?? a.sealedAt * 1_000) -
51
+ (b.sequence ?? b.sealedAt * 1_000) ||
52
+ a.id.localeCompare(b.id),
53
+ );
54
+
55
+ for (const envelope of envelopes) {
56
+ for (const mutationId of envelope.sourceMutationIds) {
57
+ sourceMutationIds.add(mutationId);
58
+ }
59
+ if (
60
+ envelope.acceptedAt === undefined &&
61
+ Date.now() - envelope.sealedAt >=
62
+ ctx.durableReplayWindowMs
63
+ ) {
64
+ ctx.runtime.logger.warn(
65
+ 'A saved local write is too old to retry safely and was held for review.',
66
+ );
67
+ ctx.runtime.observability.captureMutationFailure({
68
+ context: 'quarantine-expired-commit-envelope',
69
+ error: `Envelope ${envelope.idempotencyKey} is too old to replay safely`,
70
+ });
71
+ throw new AbloIdempotencyError(
72
+ 'A saved commit is older than the server idempotency window and cannot be replayed safely.',
73
+ { code: 'idempotency_conflict' },
74
+ );
75
+ }
76
+ if (
77
+ ctx.commitOutboxScope &&
78
+ (
79
+ !envelope.scope || // eslint-disable-line @typescript-eslint/prefer-optional-chain -- missing scope must quarantine
80
+ envelope.scope.organizationId !== ctx.commitOutboxScope.organizationId ||
81
+ envelope.scope.participantId !== ctx.commitOutboxScope.participantId ||
82
+ envelope.scope.namespace !== ctx.commitOutboxScope.namespace
83
+ )
84
+ ) {
85
+ ctx.runtime.logger.warn(
86
+ 'A saved local write belongs to a different account or server and was held for review.',
87
+ );
88
+ continue;
89
+ }
90
+ if (ctx.commitStore.has(envelope.idempotencyKey)) continue;
91
+ const transaction: CommitTransaction = {
92
+ id: envelope.idempotencyKey,
93
+ kind: 'commit',
94
+ operations: envelope.operations.map((operation) => ({ ...operation })),
95
+ ...(envelope.commitOptions.reads
96
+ ? { reads: [...envelope.commitOptions.reads] }
97
+ : {}),
98
+ ...(envelope.commitOptions.track
99
+ ? { track: [...envelope.commitOptions.track] }
100
+ : {}),
101
+ status: 'pending',
102
+ createdAt: envelope.createdAt,
103
+ sealedAt: envelope.sealedAt,
104
+ sequence: envelope.sequence ?? envelope.sealedAt * 1_000,
105
+ attempts: 0,
106
+ ...(envelope.correlationId
107
+ ? { correlationId: envelope.correlationId }
108
+ : {}),
109
+ sourceMutationIds: [...envelope.sourceMutationIds],
110
+ durableEnvelope: envelope,
111
+ };
112
+ ctx.commitStore.set(transaction.id, transaction);
113
+ ctx.commitLane.push(transaction);
114
+ }
115
+
116
+ if (ctx.commitLane.length > 0) void ctx.processCommitLane();
117
+ } catch (error) {
118
+ ctx.runtime.logger.debug('[MutationQueue] Failed to restore durable writes', {
119
+ error: error instanceof Error ? error.message : String(error),
120
+ });
121
+ ctx.runtime.observability.captureMutationFailure({
122
+ context: 'restore-commit-envelopes',
123
+ error: error instanceof Error ? error : String(error),
124
+ });
125
+ throw error;
126
+ }
127
+ return sourceMutationIds;
128
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * The durable-write port moved to the settlement core (ADR 0016): it is a
3
+ * contract over commit envelopes and holds no local rows. Re-exported here so
4
+ * the existing `transactions/mutations/durableWriteStore` import path keeps
5
+ * resolving for the queue, the outbox, and the client options.
6
+ *
7
+ * The port and its config live in the core's `durableWrites` module (a behavior
8
+ * contract, not a persisted shape); the records that cross it are owned by
9
+ * `transactions/settlement/pendingWrite`.
10
+ */
11
+
12
+ export {
13
+ durableWriteStoreSchema,
14
+ durableWritesConfigSchema,
15
+ } from '@abloatai/transaction/durableWrites';
16
+ export type {
17
+ DurableWriteStore,
18
+ DurableWritesConfig,
19
+ } from '@abloatai/transaction/durableWrites';
20
+ export { pendingWriteSchema } from '@abloatai/transaction/transactions/settlement/pendingWrite';
21
+ export type { PendingWrite } from '@abloatai/transaction/transactions/settlement/pendingWrite';
@@ -0,0 +1,42 @@
1
+ import type { QueuedMutation } from './commitPayload.js';
2
+
3
+ export function takeNextExecutionBatch(
4
+ executionQueue: QueuedMutation[],
5
+ maxBatchSize: number,
6
+ ): { batch: QueuedMutation[]; remaining: QueuedMutation[] } {
7
+ const retryGroups = new Map<string, Map<string, QueuedMutation>>();
8
+ for (const tx of executionQueue) {
9
+ const envelope = tx.commitEnvelope;
10
+ if (!envelope) continue;
11
+ const group = retryGroups.get(envelope.idempotencyKey) ?? new Map<string, QueuedMutation>();
12
+ group.set(tx.id, tx);
13
+ retryGroups.set(envelope.idempotencyKey, group);
14
+ }
15
+ for (const [idempotencyKey, byId] of retryGroups) {
16
+ const members = [...byId.values()];
17
+ const expectedCount = members[0]?.commitEnvelope?.operationCount;
18
+ if (expectedCount === undefined || members.length !== expectedCount) continue;
19
+ const remaining = executionQueue.filter((tx) => tx.commitEnvelope?.idempotencyKey !== idempotencyKey);
20
+ members.sort((a, b) => (a.commitEnvelope?.operationIndex ?? 0) - (b.commitEnvelope?.operationIndex ?? 0));
21
+ return { batch: members, remaining };
22
+ }
23
+ const fresh = executionQueue.filter((tx) => !tx.commitEnvelope);
24
+ const firstFresh = fresh[0];
25
+ if (!firstFresh) return { batch: [], remaining: executionQueue };
26
+ const explicitIndex = fresh.findIndex((tx) => typeof tx.writeOptions?.idempotencyKey === 'string');
27
+ const selected = explicitIndex === 0
28
+ ? [firstFresh]
29
+ : fresh.slice(0, Math.min(maxBatchSize, explicitIndex > 0 ? explicitIndex : fresh.length));
30
+ const selectedIds = new Set(selected.map((tx) => tx.id));
31
+ return { batch: selected, remaining: executionQueue.filter((tx) => !selectedIds.has(tx.id)) };
32
+ }
33
+
34
+ export function takePendingDrainBatch(pending: QueuedMutation[], maxBatchSize: number): QueuedMutation[] {
35
+ const first = pending[0];
36
+ if (!first) return [];
37
+ const envelope = first.commitEnvelope;
38
+ if (envelope) return pending.filter((tx) => tx.commitEnvelope?.idempotencyKey === envelope.idempotencyKey);
39
+ if (typeof first.writeOptions?.idempotencyKey === 'string') return [first];
40
+ const explicitIndex = pending.findIndex((tx) => typeof tx.writeOptions?.idempotencyKey === 'string');
41
+ return pending.slice(0, Math.min(maxBatchSize, explicitIndex > 0 ? explicitIndex : pending.length));
42
+ }
@@ -0,0 +1,154 @@
1
+ import type { RuntimeContext } from '../../RuntimeContext.js';
2
+ import type { MutationQueueConfig } from './MutationQueue.js';
3
+ import type { QueuedMutation } from './commitPayload.js';
4
+ import type { MutationStore } from './MutationStore.js';
5
+ import { AbloError } from '@abloatai/transaction/errors';
6
+ import { extractStatusCode } from './commitPayload.js';
7
+
8
+ export interface FailureHandlingContext {
9
+ readonly runtime: RuntimeContext;
10
+ readonly config: Pick<MutationQueueConfig, 'enableOptimistic' | 'maxRetries' | 'retryBackoff'>;
11
+ readonly store: MutationStore;
12
+ readonly isPermanentError: (error: Error) => boolean;
13
+ readonly rollbackOptimistic: (transaction: QueuedMutation, reason: string, error?: Error) => Promise<void>;
14
+ readonly enqueue: (transaction: QueuedMutation) => void;
15
+ readonly getLastPermanentErrorSignature: () => string | undefined;
16
+ readonly setLastPermanentErrorSignature: (signature: string) => void;
17
+ readonly emit: (event: string, payload: object) => boolean;
18
+ }
19
+
20
+ export async function handleFailure(ctx: FailureHandlingContext, transaction: QueuedMutation, error: Error): Promise<void> {
21
+ transaction.attempts++;
22
+
23
+ // Check whether this is a permanent error that should not be retried.
24
+ if (ctx.isPermanentError(error)) {
25
+ // Logged at warn: a permanent error means the server rejected the write,
26
+ // so the developer should see the reason in the console. The typed
27
+ // AbloError fields (`type`, `code`, `httpStatus`) are included so the
28
+ // cause is visible — for example a foreign-key violation
29
+ // (AbloValidationError) versus expired authentication
30
+ // (AbloAuthenticationError).
31
+ try {
32
+ const abloErr = error instanceof AbloError ? error : undefined;
33
+ const details = {
34
+ txId: transaction.id.slice(0, 8),
35
+ type: transaction.type,
36
+ model: transaction.modelName,
37
+ modelId: transaction.modelId.slice(0, 12),
38
+ errorType: abloErr?.type ?? error?.name,
39
+ errorCode: abloErr?.code,
40
+ httpStatus: abloErr?.httpStatus,
41
+ requestId: abloErr?.requestId,
42
+ message: error?.message,
43
+ inputKeys: transaction.data ? Object.keys(transaction.data) : undefined,
44
+ };
45
+
46
+ // A `create` whose id already exists is the benign idempotency case:
47
+ // "this row is already there." It's the least alarming permanent
48
+ // error, so it doesn't warrant a `warn` — `info` keeps it visible
49
+ // without crying wolf. Everything else (FK violation, auth expiry,
50
+ // server 500) stays at `warn`.
51
+ const isBenignIdempotent =
52
+ transaction.type === 'create' &&
53
+ (abloErr?.code === 'unique_violation' ||
54
+ abloErr?.type === 'AbloIdempotencyError');
55
+
56
+ // Demote exact repeats (same write rejected for the same reason on
57
+ // each reconnect replay) to `debug` so the loop logs once.
58
+ const sig = `${details.type}:${details.model}:${details.modelId}:${details.errorCode ?? details.errorType}`;
59
+ const isRepeat = sig === ctx.getLastPermanentErrorSignature();
60
+ ctx.setLastPermanentErrorSignature(sig);
61
+
62
+ const logger = ctx.runtime.logger;
63
+
64
+ // Two registers from one call site, split by log level (the default
65
+ // logger is gated at `warn`, so `debug` stays hidden unless
66
+ // ABLO_LOG_LEVEL=debug is set to inspect the engine):
67
+ // - the default-visible line speaks the application developer's
68
+ // language: their verb (such as `update`), their model, the typed
69
+ // error's own message, and the wire `code` for searching. It uses
70
+ // no engine jargon and prints no JSON dump, which would alarm
71
+ // without helping.
72
+ // - the forensic `details` ride a companion `debug` line for anyone
73
+ // debugging the engine internals.
74
+ const revertNote = ctx.config.enableOptimistic
75
+ ? ' The local change was reverted.'
76
+ : '';
77
+ const reason = abloErr?.message ? ` — ${abloErr.message}` : '';
78
+ const code = abloErr?.code ? ` (code: ${abloErr.code})` : '';
79
+ // An optimistic write resolves before the server answers, so a later
80
+ // rejection has no caller left to return to and this log is the only
81
+ // place it appears. That reads to an application developer as their own
82
+ // save silently failing — the write showed, then vanished — and sends
83
+ // them into their editor instead of here. Name the subscription that
84
+ // hands them the same typed error, so the application can say what
85
+ // happened rather than only the console.
86
+ const channelNote = ctx.config.enableOptimistic
87
+ ? ' To surface this in your app, subscribe with `ablo.onMutationFailure(…)`.'
88
+ : '';
89
+ const headline = `Your ${transaction.type} to "${transaction.modelName}" was not saved${reason}${code}.${revertNote}${channelNote}`;
90
+
91
+ if (isRepeat) {
92
+ // Same write rejected for the same reason on each reconnect replay —
93
+ // log the forensics once, stay quiet after.
94
+ logger.debug('write rejected again (same reason)', details);
95
+ } else if (isBenignIdempotent) {
96
+ // Already-exists on a `create` is expected on replay, not a problem.
97
+ logger.info(`Your ${transaction.type} to "${transaction.modelName}" was skipped — this row already exists.`);
98
+ logger.debug('idempotent skip — details', details);
99
+ } else {
100
+ logger.warn(headline);
101
+ logger.debug('write rejection — details', details);
102
+ }
103
+ } catch {}
104
+
105
+ // Mark as failed immediately and rollback
106
+ ctx.store.updateStatus(transaction.id, 'failed');
107
+
108
+ if (ctx.config.enableOptimistic) {
109
+ await ctx.rollbackOptimistic(transaction, 'permanent_error', error);
110
+ }
111
+
112
+ ctx.emit('transaction:failed', { transaction, error, permanent: true });
113
+ // The id-suffixed event is what `waitForConfirmation` (the
114
+ // `wait:'confirmed'` path) listens on — without it a permanently
115
+ // rejected write left the caller's promise hanging forever.
116
+ ctx.emit(`transaction:failed:${transaction.id}`, { error });
117
+ return;
118
+ }
119
+
120
+ if (transaction.attempts < ctx.config.maxRetries) {
121
+ // Exponential backoff with full jitter on every transient retry:
122
+ // `sleep = random(0, min(cap, base * 2^attempt))`. Throttling responses
123
+ // (429/503) use a longer base than other transient errors. The re-enqueue
124
+ // is scheduled rather than awaited, so one backing-off transaction cannot
125
+ // stall unrelated commits.
126
+ const { baseMs, capMs } = ctx.config.retryBackoff;
127
+ let base = baseMs;
128
+ try {
129
+ const status = extractStatusCode(error);
130
+ if (status === 429 || status === 503) base = Math.max(baseMs, 1_000);
131
+ } catch {}
132
+ const ceiling = Math.min(capMs, base * Math.pow(2, transaction.attempts - 1));
133
+ const delay = Math.floor(Math.random() * ceiling);
134
+
135
+ ctx.store.updateStatus(transaction.id, 'pending');
136
+ setTimeout(() => {
137
+ // The queue may have shut down or the tx may have been settled
138
+ // (e.g. delta-confirmed) while we backed off.
139
+ if (ctx.store.get(transaction.id)?.status !== 'pending') return;
140
+ ctx.enqueue(transaction);
141
+ }, delay);
142
+ } else {
143
+ // Mark as failed and rollback
144
+ ctx.store.updateStatus(transaction.id, 'failed');
145
+
146
+ if (ctx.config.enableOptimistic) {
147
+ await ctx.rollbackOptimistic(transaction, 'max_retries_exhausted', error);
148
+ }
149
+
150
+ ctx.emit('transaction:failed', { transaction, error });
151
+ // Settle `waitForConfirmation` waiters (see the permanent branch above).
152
+ ctx.emit(`transaction:failed:${transaction.id}`, { error });
153
+ }
154
+ }