@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,741 @@
1
+ /**
2
+ * Keeps a per-scope history of reversible changes so a surface can offer undo
3
+ * and redo. Each mutator invocation records an ordered list of inverse
4
+ * operations; `undo()` pops the most recent group and replays those inverses
5
+ * without recording them, then moves the entry onto the redo stack.
6
+ *
7
+ * History is divided into named scopes, one per surface — a report editor, a
8
+ * ledger grid, and so on — reached through {@link UndoManager.getScope}. Undo in
9
+ * one surface never affects another.
10
+ *
11
+ * Two things to know about its reach. History lives in memory and does not
12
+ * persist across sessions. And if the server rejects a change after it was
13
+ * applied optimistically, the undo stack is not invalidated automatically; call
14
+ * {@link UndoScope.clear} on a sync error if you need strict correctness.
15
+ */
16
+
17
+ import type { Schema } from '@abloatai/transaction/schema/schema';
18
+ import { getContext } from '../context.js';
19
+ import type { SyncStoreContract, LocalMutation } from '../storeContract.js';
20
+ import { createTransaction, type Transaction } from './Transaction.js';
21
+ import { type InverseOp, type UndoEntry, parseUndoEntry } from './inverseOp.js';
22
+ import {
23
+ resolveOps,
24
+ DEFAULT_UNDO_CONFLICT_POLICY,
25
+ type UndoConflictPolicy,
26
+ } from './undoApply.js';
27
+
28
+ /** Normalize a registered model name to its lowercased alias form. */
29
+ const normalizeModelAlias = (modelName: string): string =>
30
+ modelName.replace('Model', '').toLowerCase();
31
+
32
+ // ── Inverse op model ──────────────────────────────────────────────────────
33
+ //
34
+ // The InverseOp and UndoEntry shapes and their validator are defined as Zod
35
+ // schemas in `./inverseOp.ts`, and re-exported here so they can be imported
36
+ // alongside the undo manager.
37
+ export type { InverseOp, UndoEntry };
38
+ export type { UndoConflictPolicy } from './undoApply.js';
39
+
40
+ // ── Scope ──────────────────────────────────────────────────────────────────
41
+
42
+ export interface UndoScopeOptions {
43
+ /** The maximum number of undo entries to keep. Older entries drop off the
44
+ * bottom. Defaults to 100. */
45
+ maxHistory?: number;
46
+ /**
47
+ * How undo and redo treat a field a collaborator changed after your own
48
+ * change. The default, `skip-stale`, reverts your change only where it still
49
+ * stands, so undo never overwrites a concurrent edit — undo is per user.
50
+ * `last-writer-wins` restores the older behavior of overwriting regardless. See
51
+ * {@link UndoConflictPolicy}.
52
+ */
53
+ conflictPolicy?: UndoConflictPolicy;
54
+ /**
55
+ * A predicate selecting which models this surface owns. The scope records only
56
+ * mutations whose resolved schema key passes it, so, for example, a ledger
57
+ * edit never lands on a report editor's undo stack. Omit it to track every model,
58
+ * which is fine for a single-surface app but wrong when two surfaces with
59
+ * independent undo share one store.
60
+ */
61
+ tracksModel?: (schemaKey: string) => boolean;
62
+ /**
63
+ * When `true`, the scope records undo entries by observing the stream of local
64
+ * mutations, so every write through the store is captured automatically. When
65
+ * `false`, the default, the scope records nothing on its own and relies on
66
+ * explicit {@link UndoScope.record} calls. Use one mode or the other for a given
67
+ * surface, not both, or shared writes are counted twice.
68
+ */
69
+ recordFromStream?: boolean;
70
+ }
71
+
72
+ /**
73
+ * A single undo stack for one surface, obtained from
74
+ * {@link UndoManager.getScope}. Call {@link UndoScope.record} after a mutator to
75
+ * add an entry, and {@link UndoScope.undo} / {@link UndoScope.redo} to move
76
+ * through the history.
77
+ */
78
+ /**
79
+ * How long a pending replay-echo marker stays armed before it is pruned. A real
80
+ * echo returns within a couple of local-store round-trips (tens of milliseconds);
81
+ * this is a generous ceiling so that an echo which never arrives — for instance,
82
+ * because the write was skipped while offline — cannot suppress a genuine later
83
+ * edit to the same row indefinitely.
84
+ */
85
+ const REPLAY_ECHO_TTL_MS = 5000;
86
+
87
+ export class UndoScope<S extends Schema> {
88
+ private undoStack: UndoEntry[] = [];
89
+ private redoStack: UndoEntry[] = [];
90
+ private readonly maxHistory: number;
91
+ private readonly conflictPolicy: UndoConflictPolicy;
92
+
93
+ /**
94
+ * Observers notified after each successful {@link UndoScope.record}. They see
95
+ * forward user actions only: undo and redo move entries between the stacks
96
+ * without calling `record`, so a listener never observes a reversal. It is a
97
+ * deliberately generic hook — analytics or audit code can watch the stream of
98
+ * committed mutations without the scope knowing about it. A listener that throws
99
+ * is isolated so it cannot break recording.
100
+ */
101
+ private readonly recordListeners = new Set<(entry: UndoEntry) => void>();
102
+
103
+ /**
104
+ * Observers notified after any stack change — record, undo, redo, or clear.
105
+ * Unlike {@link recordListeners}, which fires on forward actions only, this
106
+ * fires on reversals too, so a React consumer can keep `canUndo` and `canRedo`
107
+ * current. Because the stream-recording path adds entries without triggering a
108
+ * render, a component that read `canUndo` on its last render would otherwise go
109
+ * stale and a keyboard handler gated on it would quietly do nothing.
110
+ */
111
+ private readonly changeListeners = new Set<() => void>();
112
+
113
+ /**
114
+ * The serialization tail. Recording, undo, and redo all chain off this one
115
+ * promise, so they run strictly in the order they were invoked and never
116
+ * interleave. This matters for correctness, not just throughput, in two ways.
117
+ * Ordering: callers often fire writes without awaiting them, so without
118
+ * serialization an entry would land on the stack when its mutator resolves, and
119
+ * a fast second write could record before a slow first — replaying undo in the
120
+ * wrong order. Snapshot integrity: each recording reads and clears a model's
121
+ * modified-field markers, which form the undo baseline, so two recordings
122
+ * interleaving on the same model would corrupt each other's before-image.
123
+ * Serializing the whole scope closes both gaps at once.
124
+ */
125
+ private tail: Promise<unknown> = Promise.resolve();
126
+
127
+ /** Predicate selecting which models this surface records (see options). */
128
+ private readonly tracksModel?: (schemaKey: string) => boolean;
129
+ /** registered-name / alias → schema key, built once from the schema. */
130
+ private readonly schemaKeyByAlias = new Map<string, string>();
131
+ /** Unsubscribe from the local-mutation stream. */
132
+ private readonly unsubscribe: () => void;
133
+ /**
134
+ * True while undo or redo is replaying operations. A replay writes through the
135
+ * normal commit path and therefore re-emits on the local-mutation stream; this
136
+ * flag tells the scope's own listener to ignore those writes so they are not
137
+ * recorded again.
138
+ */
139
+ private replaying = false;
140
+ /** Operations collected during the current tick, flushed together as one entry. */
141
+ private batch: { forward: InverseOp; inverse: InverseOp | null }[] = [];
142
+ private flushScheduled = false;
143
+ /**
144
+ * An open grouping session. While set, stream operations accumulate here across
145
+ * ticks instead of flushing each tick, so a multi-tick action — a drag, or a
146
+ * whole streaming AI response — collapses into a single undo step.
147
+ * {@link UndoScope.endGroup} flushes it.
148
+ */
149
+ private group: { label?: string; ops: { forward: InverseOp; inverse: InverseOp | null }[] } | null =
150
+ null;
151
+ /**
152
+ * Suppression of a replay's asynchronous echo, keyed by `${modelKey}:${id}`.
153
+ *
154
+ * The synchronous {@link UndoScope.replaying} flag catches only echoes
155
+ * delivered inline while operations are applied. In practice the engine does not
156
+ * emit a replayed write's echo synchronously: the commit is deferred behind a
157
+ * local-store write, so the echo arrives on the stream after undo or redo has
158
+ * already reset `replaying` and pushed its entry. That late echo would be
159
+ * recorded as a new edit — and recording clears the redo stack, so every undo
160
+ * would quietly destroy its own redo. To prevent that, the row of each operation
161
+ * about to be replayed is marked here synchronously, before the write, and one
162
+ * mark is consumed when the matching mutation arrives, whenever that is. Marks
163
+ * carry a time-to-live so an echo that never arrives — because the write was
164
+ * skipped while offline — cannot linger and wrongly suppress a much later, real
165
+ * edit to the same row.
166
+ */
167
+ private readonly pendingReplayEchoes = new Map<string, { count: number; expiresAt: number }>();
168
+
169
+ constructor(
170
+ private readonly schema: S,
171
+ private readonly store: SyncStoreContract,
172
+ private readonly organizationId: string,
173
+ options: UndoScopeOptions = {},
174
+ ) {
175
+ this.maxHistory = options.maxHistory ?? 100;
176
+ this.conflictPolicy = options.conflictPolicy ?? DEFAULT_UNDO_CONFLICT_POLICY;
177
+ this.tracksModel = options.tracksModel;
178
+
179
+ // Build the map from registered name to schema key. The mutation stream
180
+ // reports a model's registered name (for example `'Block'`), but inverse
181
+ // operations and the replay transaction are keyed by the schema key (for
182
+ // example `'blocks'`), so map every reasonable spelling to the schema key.
183
+ for (const schemaKey of Object.keys(this.schema.models)) {
184
+ const def = (this.schema.models as Record<string, { typename?: string }>)[schemaKey];
185
+ const typename = def?.typename ?? schemaKey;
186
+ for (const alias of [schemaKey, typename]) {
187
+ this.schemaKeyByAlias.set(alias, schemaKey);
188
+ this.schemaKeyByAlias.set(alias.toLowerCase(), schemaKey);
189
+ this.schemaKeyByAlias.set(normalizeModelAlias(alias), schemaKey);
190
+ }
191
+ }
192
+
193
+ // Subscribe to the local-mutation stream only when this scope opts into
194
+ // stream recording. A scope using explicit `record()` calls instead keeps
195
+ // `recordFromStream` false so writes are not counted twice. The stream method
196
+ // on the store is optional, so a minimal test double can omit it, in which
197
+ // case undo records nothing.
198
+ this.unsubscribe =
199
+ options.recordFromStream && this.store.subscribeLocalMutations
200
+ ? this.store.subscribeLocalMutations((m) => { this.onLocalMutation(m); })
201
+ : () => {};
202
+ }
203
+
204
+ /**
205
+ * Opens a grouping session: every stream-recorded operation until
206
+ * {@link UndoScope.endGroup} collapses into one undo entry. Call it at the start
207
+ * of a gesture, such as a pointer-down, or at the start of an AI response. A
208
+ * second call closes the previous group first.
209
+ */
210
+ beginGroup(label?: string): void {
211
+ if (this.group) this.endGroup();
212
+ this.group = { label, ops: [] };
213
+ }
214
+
215
+ /** Close the grouping session and record the accumulated ops as one entry. */
216
+ endGroup(label?: string): void {
217
+ const g = this.group;
218
+ if (!g) return;
219
+ this.group = null;
220
+ const forwards = g.ops.map((c) => c.forward);
221
+ const inverses = g.ops
222
+ .map((c) => c.inverse)
223
+ .filter((i): i is InverseOp => i !== null)
224
+ .reverse();
225
+ if (forwards.length === 0 && inverses.length === 0) return;
226
+ this.record({ label: label ?? g.label, inverses, forwards });
227
+ }
228
+
229
+ /** Every `${modelKey}:${id}` a set of ops will touch (all op kinds). */
230
+ private *replayEchoKeys(ops: InverseOp[]): Iterable<string> {
231
+ for (const op of ops) {
232
+ switch (op.kind) {
233
+ case 'create': {
234
+ const id = op.data.id;
235
+ if (typeof id === 'string') yield `${op.modelKey}:${id}`;
236
+ break;
237
+ }
238
+ case 'update':
239
+ yield `${op.modelKey}:${op.patch.id}`;
240
+ break;
241
+ case 'delete':
242
+ yield `${op.modelKey}:${op.id}`;
243
+ break;
244
+ case 'createMany':
245
+ for (const d of op.data) {
246
+ const id = d.id;
247
+ if (typeof id === 'string') yield `${op.modelKey}:${id}`;
248
+ }
249
+ break;
250
+ case 'updateMany':
251
+ for (const p of op.patches) yield `${op.modelKey}:${p.id}`;
252
+ break;
253
+ case 'deleteMany':
254
+ for (const id of op.ids) yield `${op.modelKey}:${id}`;
255
+ break;
256
+ }
257
+ }
258
+ }
259
+
260
+ /**
261
+ * Arms echo suppression for the rows a replay is about to write. Called
262
+ * synchronously, before the writes, so the marks exist however long the engine
263
+ * takes to surface each echo on the stream. See {@link UndoScope.pendingReplayEchoes}.
264
+ */
265
+ private markReplayEchoes(ops: InverseOp[]): void {
266
+ const expiresAt = Date.now() + REPLAY_ECHO_TTL_MS;
267
+ for (const key of this.replayEchoKeys(ops)) {
268
+ const existing = this.pendingReplayEchoes.get(key);
269
+ if (existing) {
270
+ existing.count += 1;
271
+ existing.expiresAt = expiresAt;
272
+ } else {
273
+ this.pendingReplayEchoes.set(key, { count: 1, expiresAt });
274
+ }
275
+ }
276
+ }
277
+
278
+ /**
279
+ * If `${schemaKey}:${modelId}` has an armed mark, consume one and report that
280
+ * this mutation is the scope's own replay echo, so the caller drops it. Expired
281
+ * marks are pruned along the way, so an echo that never arrives cannot linger.
282
+ */
283
+ private consumeReplayEcho(schemaKey: string, modelId: string): boolean {
284
+ if (this.pendingReplayEchoes.size === 0) return false;
285
+ const now = Date.now();
286
+ for (const [k, v] of this.pendingReplayEchoes) {
287
+ if (v.expiresAt <= now) this.pendingReplayEchoes.delete(k);
288
+ }
289
+ const key = `${schemaKey}:${modelId}`;
290
+ const pending = this.pendingReplayEchoes.get(key);
291
+ if (!pending) return false;
292
+ pending.count -= 1;
293
+ if (pending.count <= 0) this.pendingReplayEchoes.delete(key);
294
+ return true;
295
+ }
296
+
297
+ /** Resolve a stream mutation's registered name to its schema key, or null. */
298
+ private resolveSchemaKey(modelName: string): string | null {
299
+ return (
300
+ this.schemaKeyByAlias.get(modelName) ??
301
+ this.schemaKeyByAlias.get(normalizeModelAlias(modelName)) ??
302
+ null
303
+ );
304
+ }
305
+
306
+ /**
307
+ * The stream listener, and the only place stream-recorded entries originate. It
308
+ * skips replay echoes and out-of-scope models, derives the forward and inverse
309
+ * operations from the mutation's `data` and `previousData`, and defers the stack
310
+ * push to a per-tick flush, so a burst of writes — aligning five blocks at once,
311
+ * say — becomes a single undo step.
312
+ */
313
+ private onLocalMutation(m: LocalMutation): void {
314
+ if (this.replaying) return;
315
+ const schemaKey = this.resolveSchemaKey(m.modelName);
316
+ if (!schemaKey) return;
317
+ // Drop the ASYNC echo of our own replayed writes. The engine surfaces a
318
+ // replay's `transaction:created` only after an IndexedDB-gated commit, i.e.
319
+ // after `replaying` has already reset — so the synchronous flag above misses
320
+ // it. The (modelKey,id) marks armed in `markReplayEchoes` catch it whenever
321
+ // it lands, which is what stops every undo from wiping its own redo stack.
322
+ if (this.consumeReplayEcho(schemaKey, m.modelId)) return;
323
+ if (this.tracksModel && !this.tracksModel(schemaKey)) return;
324
+
325
+ const ops = buildUndoOps(m, schemaKey);
326
+ if (!ops) return;
327
+
328
+ // Inside a grouping session, accumulate across ticks (flushed on
329
+ // endGroup); otherwise coalesce per-tick.
330
+ if (this.group) {
331
+ this.group.ops.push(ops);
332
+ return;
333
+ }
334
+ this.batch.push(ops);
335
+ this.scheduleFlush();
336
+ }
337
+
338
+ private scheduleFlush(): void {
339
+ if (this.flushScheduled) return;
340
+ this.flushScheduled = true;
341
+ const run = () => {
342
+ this.flushScheduled = false;
343
+ this.flushBatch();
344
+ };
345
+ if (typeof queueMicrotask === 'function') queueMicrotask(run);
346
+ else void Promise.resolve().then(run);
347
+ }
348
+
349
+ /** Coalesce the tick's collected ops into one entry and record it. */
350
+ private flushBatch(): void {
351
+ if (this.batch.length === 0) return;
352
+ const collected = this.batch;
353
+ this.batch = [];
354
+ const forwards = collected.map((c) => c.forward);
355
+ // Undo applies the inverses in reverse order of how the forwards ran.
356
+ const inverses = collected
357
+ .map((c) => c.inverse)
358
+ .filter((i): i is InverseOp => i !== null)
359
+ .reverse();
360
+ if (forwards.length === 0 && inverses.length === 0) return;
361
+ this.record({ inverses, forwards });
362
+ }
363
+
364
+ /**
365
+ * Run `work` after every previously-enqueued scope operation has settled,
366
+ * in invocation order. The internal `tail` always resolves (failures are
367
+ * swallowed *for the chain only*) so one rejected mutator can't wedge the
368
+ * queue; the original settlement is still surfaced to this call's caller.
369
+ */
370
+ private enqueue<T>(work: () => Promise<T>): Promise<T> {
371
+ const result = this.tail.then(work, work);
372
+ this.tail = result.then(
373
+ () => undefined,
374
+ () => undefined,
375
+ );
376
+ return result;
377
+ }
378
+
379
+ /**
380
+ * Runs a recording mutator by itself on the scope's serialization chain, so its
381
+ * snapshot, write, and {@link UndoScope.record} happen atomically with respect to
382
+ * undo and redo. This is used by the explicit-record path; the stream-recording
383
+ * path does not need it, since it derives entries from already-committed
384
+ * mutations.
385
+ */
386
+ runRecorded<T>(work: () => Promise<T>): Promise<T> {
387
+ return this.enqueue(work);
388
+ }
389
+
390
+ /**
391
+ * Records one entry onto the undo stack and clears the redo stack. It is fed
392
+ * both by the per-tick flush and grouping paths from the local-mutation stream
393
+ * and by direct callers using explicit recording. Entries are built internally
394
+ * and therefore trusted, so the schema check here runs only outside production:
395
+ * it catches recorder bugs early, rejecting a malformed operation at ingestion
396
+ * with a clear path rather than letting it fail later during replay, without
397
+ * paying a validation cost on every user action in production. The real
398
+ * validation boundary is {@link parseUndoEntry}, applied to entries loaded from
399
+ * persistence, which is untrusted input.
400
+ */
401
+ record(entry: UndoEntry): void {
402
+ if (typeof process !== 'undefined' && process.env?.NODE_ENV !== 'production') {
403
+ parseUndoEntry(entry);
404
+ }
405
+ this.undoStack.push(entry);
406
+ if (this.undoStack.length > this.maxHistory) this.undoStack.shift();
407
+ this.redoStack = [];
408
+ this.emitRecord(entry);
409
+ this.emitChange();
410
+ }
411
+
412
+ /**
413
+ * Subscribes to every recorded mutation. The listener fires synchronously at the
414
+ * end of each {@link UndoScope.record} call, once the entry is on the undo stack,
415
+ * and the returned function unsubscribes it. The listener receives the full
416
+ * {@link UndoEntry} — its `forwards` carry the `{ kind, modelKey, data }`
417
+ * operations — so a consumer can tell what changed without querying again.
418
+ */
419
+ onRecord(listener: (entry: UndoEntry) => void): () => void {
420
+ this.recordListeners.add(listener);
421
+ return () => {
422
+ this.recordListeners.delete(listener);
423
+ };
424
+ }
425
+
426
+ private emitRecord(entry: UndoEntry): void {
427
+ for (const listener of this.recordListeners) {
428
+ try {
429
+ listener(entry);
430
+ } catch (err) {
431
+ // A faulty observer must never break the recording path. The consumer's
432
+ // own onRecord callback is at fault, so log it as an actionable warning.
433
+ getContext().logger.warn('An undo/redo onRecord listener threw — your callback should not throw', err);
434
+ }
435
+ }
436
+ }
437
+
438
+ /**
439
+ * Subscribes to any stack change — record, undo, redo, or clear. The React
440
+ * `useUndoScope` hook uses this to re-render so `canUndo` and `canRedo` stay
441
+ * current for every consumer, not only the component that invoked undo or redo.
442
+ * The returned function unsubscribes.
443
+ */
444
+ onChange(listener: () => void): () => void {
445
+ this.changeListeners.add(listener);
446
+ return () => {
447
+ this.changeListeners.delete(listener);
448
+ };
449
+ }
450
+
451
+ private emitChange(): void {
452
+ for (const listener of this.changeListeners) {
453
+ try {
454
+ listener();
455
+ } catch (err) {
456
+ // The consumer's own onChange callback is at fault, so log it as an
457
+ // actionable warning.
458
+ getContext().logger.warn('An undo/redo onChange listener threw — your callback should not throw', err);
459
+ }
460
+ }
461
+ }
462
+
463
+ canUndo(): boolean {
464
+ return this.undoStack.length > 0;
465
+ }
466
+
467
+ canRedo(): boolean {
468
+ return this.redoStack.length > 0;
469
+ }
470
+
471
+ /**
472
+ * Pops the most recent entry, applies its inverse operations, and pushes it onto
473
+ * the redo stack. Under the default `skip-stale` policy the inverses are first
474
+ * filtered against the current state — paired with the entry's forwards, which
475
+ * record what this change set — so a field a collaborator changed afterward is
476
+ * left untouched, and undo reverts the change only where it still stands.
477
+ */
478
+ undo(): Promise<void> {
479
+ return this.enqueue(async () => {
480
+ const entry = this.undoStack.pop();
481
+ if (!entry) return;
482
+ const tx = createTransaction(this.schema, this.store, this.organizationId);
483
+ const ops = resolveOps(entry.inverses, entry.forwards, this.store, this.conflictPolicy);
484
+ // Suppress the scope's own stream listener so replayed writes are not
485
+ // recorded as new entries. `replaying` covers echoes delivered inline;
486
+ // `markReplayEchoes` covers the asynchronous echo that lands after this
487
+ // method returns. Cleared in `finally` even if a replay throws.
488
+ this.markReplayEchoes(ops);
489
+ this.replaying = true;
490
+ try {
491
+ await applyOps(tx, ops);
492
+ } catch (err) {
493
+ // The replay was rejected (for example, a server 409). Nothing changed,
494
+ // so restore the entry to the undo stack rather than dropping it, which
495
+ // would also strand it off the redo stack and lose the action entirely.
496
+ this.undoStack.push(entry);
497
+ this.emitChange();
498
+ throw err;
499
+ } finally {
500
+ this.replaying = false;
501
+ }
502
+ this.redoStack.push(entry);
503
+ if (this.redoStack.length > this.maxHistory) this.redoStack.shift();
504
+ this.emitChange();
505
+ });
506
+ }
507
+
508
+ /**
509
+ * Pops the most recently undone entry, re-applies its forward operations, and
510
+ * pushes it onto the undo stack. It mirrors {@link UndoScope.undo}: the forwards
511
+ * are filtered against the current state — paired with the entry's inverses,
512
+ * which record what undo restored — so redo re-asserts the change only where the
513
+ * undone value still stands.
514
+ */
515
+ redo(): Promise<void> {
516
+ return this.enqueue(async () => {
517
+ const entry = this.redoStack.pop();
518
+ if (!entry) return;
519
+ const tx = createTransaction(this.schema, this.store, this.organizationId);
520
+ const ops = resolveOps(entry.forwards, entry.inverses, this.store, this.conflictPolicy);
521
+ // See undo(): arm async-echo suppression before the replayed writes.
522
+ this.markReplayEchoes(ops);
523
+ this.replaying = true;
524
+ try {
525
+ await applyOps(tx, ops);
526
+ } catch (err) {
527
+ // Symmetric to undo: a rejected re-apply leaves state unchanged, so put
528
+ // the entry back on the redo stack instead of losing it.
529
+ this.redoStack.push(entry);
530
+ this.emitChange();
531
+ throw err;
532
+ } finally {
533
+ this.replaying = false;
534
+ }
535
+ this.undoStack.push(entry);
536
+ if (this.undoStack.length > this.maxHistory) this.undoStack.shift();
537
+ this.emitChange();
538
+ });
539
+ }
540
+
541
+ /** Drop all history. Use after bootstrap / sync group change / sync error. */
542
+ clear(): void {
543
+ this.undoStack = [];
544
+ this.redoStack = [];
545
+ this.batch = [];
546
+ this.pendingReplayEchoes.clear();
547
+ this.emitChange();
548
+ }
549
+
550
+ /** Introspection — for debug panels / e2e tests. */
551
+ size(): { undo: number; redo: number } {
552
+ return { undo: this.undoStack.length, redo: this.redoStack.length };
553
+ }
554
+
555
+ /**
556
+ * Detach from the local-mutation stream and drop listeners. Scopes are
557
+ * cached for the store's lifetime by `UndoManager`, so this is mainly for
558
+ * tests and explicit teardown.
559
+ */
560
+ dispose(): void {
561
+ this.unsubscribe();
562
+ this.recordListeners.clear();
563
+ this.changeListeners.clear();
564
+ this.batch = [];
565
+ this.pendingReplayEchoes.clear();
566
+ }
567
+ }
568
+
569
+ /**
570
+ * Derives the forward and inverse operation for a single local mutation. Returns
571
+ * null when the mutation cannot be reversed — for example, an update with no
572
+ * captured previous values — so the caller drops it rather than push a half-entry.
573
+ */
574
+ function buildUndoOps(
575
+ m: LocalMutation,
576
+ modelKey: string,
577
+ ): { forward: InverseOp; inverse: InverseOp | null } | null {
578
+ const id = m.modelId;
579
+ const stripId = (o?: Record<string, unknown> | null): Record<string, unknown> => {
580
+ const out = { ...(o ?? {}) };
581
+ delete out.id;
582
+ return out;
583
+ };
584
+
585
+ switch (m.type) {
586
+ case 'create':
587
+ return {
588
+ forward: { kind: 'create', modelKey, data: { ...stripId(m.data), id } },
589
+ inverse: { kind: 'delete', modelKey, id },
590
+ };
591
+ case 'update': {
592
+ const next = stripId(m.data);
593
+ const prev = stripId(m.previousData);
594
+ return {
595
+ forward: { kind: 'update', modelKey, patch: { id, ...next } },
596
+ // No previous values captured → not reversible; drop the inverse.
597
+ inverse:
598
+ Object.keys(prev).length > 0
599
+ ? { kind: 'update', modelKey, patch: { id, ...prev } }
600
+ : null,
601
+ };
602
+ }
603
+ case 'delete':
604
+ return {
605
+ forward: { kind: 'delete', modelKey, id },
606
+ inverse: { kind: 'create', modelKey, data: { ...stripId(m.previousData), id } },
607
+ };
608
+ case 'archive':
609
+ return {
610
+ forward: { kind: 'update', modelKey, patch: { id, archivedAt: new Date() } },
611
+ inverse: { kind: 'update', modelKey, patch: { id, archivedAt: null } },
612
+ };
613
+ case 'unarchive':
614
+ return {
615
+ forward: { kind: 'update', modelKey, patch: { id, archivedAt: null } },
616
+ inverse: { kind: 'update', modelKey, patch: { id, archivedAt: new Date() } },
617
+ };
618
+ default:
619
+ return null;
620
+ }
621
+ }
622
+
623
+ // ── Manager ────────────────────────────────────────────────────────────────
624
+
625
+ /**
626
+ * The registry of named undo scopes. One instance is created per application
627
+ * during engine setup, and each surface finds its scope by name through
628
+ * {@link UndoManager.getScope}.
629
+ */
630
+ export class UndoManager<S extends Schema> {
631
+ private readonly scopes = new Map<string, UndoScope<S>>();
632
+ /** The options each scope was constructed with, for the mismatch warning below. */
633
+ private readonly creationOptions = new Map<string, UndoScopeOptions | undefined>();
634
+
635
+ constructor(
636
+ private readonly schema: S,
637
+ private readonly store: SyncStoreContract,
638
+ private readonly organizationId: string,
639
+ ) {}
640
+
641
+ getScope(name: string, options?: UndoScopeOptions): UndoScope<S> {
642
+ let scope = this.scopes.get(name);
643
+ if (!scope) {
644
+ scope = new UndoScope(this.schema, this.store, this.organizationId, options);
645
+ this.scopes.set(name, scope);
646
+ this.creationOptions.set(name, options);
647
+ return scope;
648
+ }
649
+ // A scope keeps the options it was created with; later calls cannot change
650
+ // them. Requesting the shared scope with no options is the normal pattern
651
+ // and stays silent — but passing options that conflict with the creation
652
+ // values means one caller believes it configured a scope that another
653
+ // caller already configured differently, which is how a surface silently
654
+ // ends up with, say, no stream recording. Surface that instead of letting
655
+ // it pass.
656
+ if (options) {
657
+ const created = this.creationOptions.get(name);
658
+ const conflicts: string[] = [];
659
+ if (
660
+ options.recordFromStream !== undefined &&
661
+ options.recordFromStream !== (created?.recordFromStream ?? false)
662
+ ) {
663
+ conflicts.push('recordFromStream');
664
+ }
665
+ if (options.maxHistory !== undefined && options.maxHistory !== (created?.maxHistory ?? 100)) {
666
+ conflicts.push('maxHistory');
667
+ }
668
+ if (
669
+ options.conflictPolicy !== undefined &&
670
+ options.conflictPolicy !== (created?.conflictPolicy ?? DEFAULT_UNDO_CONFLICT_POLICY)
671
+ ) {
672
+ conflicts.push('conflictPolicy');
673
+ }
674
+ if (conflicts.length > 0) {
675
+ getContext().logger.warn(
676
+ `The undo scope "${name}" already exists with different options — ` +
677
+ `${conflicts.join(', ')} cannot be changed after creation and the requested ` +
678
+ `values are ignored. Create the scope with its full options before any ` +
679
+ `caller requests it without them, or use a differently named scope.`,
680
+ );
681
+ }
682
+ }
683
+ return scope;
684
+ }
685
+
686
+ clearAll(): void {
687
+ for (const scope of this.scopes.values()) scope.clear();
688
+ }
689
+ }
690
+
691
+ // ── Internal helpers ───────────────────────────────────────────────────────
692
+
693
+ /**
694
+ * Replays a list of operations through a {@link Transaction}. Used by both undo,
695
+ * which replays the captured inverses, and redo, which replays the captured
696
+ * forwards. Each operation is awaited in turn to preserve ordering.
697
+ */
698
+ async function applyOps<S extends Schema>(tx: Transaction<S>, ops: InverseOp[]): Promise<void> {
699
+ for (const op of ops) {
700
+ const mutations: object = tx.mutations;
701
+ const modelMutations = Reflect.get(mutations, op.modelKey);
702
+ if (!modelMutations || typeof modelMutations !== 'object') {
703
+ // A persisted inverse op references a model the schema no longer has;
704
+ // fail with a clear message rather than an opaque TypeError.
705
+ throw new Error(
706
+ `Cannot undo: model "${op.modelKey}" is not part of the current schema.`,
707
+ );
708
+ }
709
+
710
+ const invoke = async (method: 'create' | 'update' | 'delete', argument: unknown) => {
711
+ const mutation = Reflect.get(modelMutations, method);
712
+ if (typeof mutation !== 'function') {
713
+ throw new Error(
714
+ `Cannot undo: model "${op.modelKey}" has no "${method}" mutation.`,
715
+ );
716
+ }
717
+ await Reflect.apply(mutation, modelMutations, [argument]);
718
+ };
719
+
720
+ switch (op.kind) {
721
+ case 'create':
722
+ await invoke('create', op.data);
723
+ break;
724
+ case 'update':
725
+ await invoke('update', op.patch);
726
+ break;
727
+ case 'delete':
728
+ await invoke('delete', op.id);
729
+ break;
730
+ case 'createMany':
731
+ await invoke('create', op.data);
732
+ break;
733
+ case 'updateMany':
734
+ await invoke('update', op.patches);
735
+ break;
736
+ case 'deleteMany':
737
+ await invoke('delete', op.ids);
738
+ break;
739
+ }
740
+ }
741
+ }