@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,160 @@
1
+ /**
2
+ * Captures a local {@link Snapshot} of a chosen set of entities, along with a
3
+ * watermark, so a caller can detect when that state has gone stale. This is
4
+ * what an LLM caller threads into a prompt: `stamp` flows into later writes as
5
+ * `readAt`, so the server rejects a mutation premised on data that has since
6
+ * changed; `signal` is an `AbortSignal` that fires as soon as any captured
7
+ * entity receives a delta, so a mid-generation invalidation can abort the token
8
+ * stream instead of producing output against stale context.
9
+ *
10
+ * It reads the current entity state from the in-memory pool, reads the engine's
11
+ * current `lastSyncId` as the watermark, and subscribes to delta frames on the
12
+ * existing sync connection — no second connection.
13
+ */
14
+
15
+ import type { InstanceCache } from '../InstanceCache.js';
16
+ import type { Schema } from '@abloatai/transaction/schema/schema';
17
+ import type { SyncDelta, SyncWebSocket } from './SyncWebSocket.js';
18
+ import type {
19
+ ContextChange,
20
+ Snapshot,
21
+ } from '@abloatai/transaction/types/streams';
22
+ import { AbloValidationError } from '@abloatai/transaction/errors';
23
+ import { modelAsRow } from '../Model.js';
24
+
25
+ /**
26
+ * The snapshot result exposes `stamp`, `signal`, and `onChange` at its top
27
+ * level, alongside one bucket per model. If a schema declares a model with one
28
+ * of these names, the two would collide, so snapshot creation throws instead.
29
+ */
30
+ const RESERVED_SNAPSHOT_KEYS: ReadonlySet<string> = new Set([
31
+ 'stamp',
32
+ 'signal',
33
+ 'onChange',
34
+ ]);
35
+
36
+ export interface CreateSnapshotArgs<
37
+ TSchema extends Schema = Schema,
38
+ K extends keyof TSchema['models'] & string = keyof TSchema['models'] & string,
39
+ > {
40
+ pool: InstanceCache;
41
+ /** Live transport for delta subscriptions. May be null if the engine
42
+ * hasn't connected yet — the snapshot still resolves with current
43
+ * pool state, but `signal` won't fire until reconnect. */
44
+ transport: SyncWebSocket | null;
45
+ /** Returns the engine's current `lastSyncId`. Read at snapshot time
46
+ * to stamp the watermark; not re-read after. */
47
+ getLastSyncId: () => number;
48
+ entities: Readonly<Record<K, string | readonly string[]>>;
49
+ }
50
+
51
+ export function createSnapshot<
52
+ TSchema extends Schema,
53
+ K extends keyof TSchema['models'] & string,
54
+ >(args: CreateSnapshotArgs<TSchema, K>): Snapshot<TSchema, K> {
55
+ const { pool, transport, getLastSyncId, entities } = args;
56
+
57
+ // ── Validate keys ────────────────────────────────────────────────
58
+ for (const key of Object.keys(entities)) {
59
+ if (RESERVED_SNAPSHOT_KEYS.has(key)) {
60
+ throw new AbloValidationError(
61
+ `engine.snapshot: model key "${key}" collides with a reserved ` +
62
+ `snapshot field (stamp / signal / onChange). Rename the model ` +
63
+ 'in your schema.',
64
+ { code: 'snapshot_reserved_key' },
65
+ );
66
+ }
67
+ }
68
+
69
+ // ── Watermark ────────────────────────────────────────────────────
70
+ const stamp = getLastSyncId();
71
+
72
+ // ── Capture data + watched set ───────────────────────────────────
73
+ const watched = new Set<string>(); // `${type}:${id}`
74
+ const data: Record<string, Record<string, unknown>> = {};
75
+
76
+ for (const [type, idOrIds] of Object.entries(entities)) {
77
+ const ids = Array.isArray(idOrIds)
78
+ ? (idOrIds as readonly string[])
79
+ : [idOrIds as string];
80
+ const bucket: Record<string, unknown> = {};
81
+ for (const id of ids) {
82
+ const m = pool.get(id);
83
+ // Only include if the model actually has the requested type —
84
+ // pool keys models globally by id, so `pool.get(id)` could
85
+ // return a different model that happens to share the id (rare,
86
+ // but type guards keep the surface honest).
87
+ if (
88
+ m &&
89
+ typeof (m as { getModelName?: unknown }).getModelName === 'function' &&
90
+ m.getModelName() === type
91
+ ) {
92
+ bucket[id] = modelAsRow(m);
93
+ }
94
+ watched.add(`${type}:${id}`);
95
+ }
96
+ data[type] = bucket;
97
+ }
98
+
99
+ // ── Invalidation wiring ──────────────────────────────────────────
100
+ const listeners = new Set<(change: ContextChange) => void>();
101
+ const controller = new AbortController();
102
+
103
+ const fireChange = (change: ContextChange) => {
104
+ if (!controller.signal.aborted) {
105
+ controller.abort(
106
+ new Error(
107
+ 'snapshot invalidated — underlying entity received a delta',
108
+ ),
109
+ );
110
+ }
111
+ for (const l of listeners) {
112
+ try {
113
+ l(change);
114
+ } catch {
115
+ /* listener errors don't break siblings */
116
+ }
117
+ }
118
+ };
119
+
120
+ let unsubDelta: (() => void) | null = null;
121
+ if (transport) {
122
+ unsubDelta = transport.subscribe('delta', (delta: SyncDelta) => {
123
+ const key = `${delta.modelName}:${delta.modelId}`;
124
+ if (!watched.has(key)) return;
125
+ // Every delta to a captured entity is reported as 'semantic' severity.
126
+ fireChange({
127
+ model: delta.modelName,
128
+ id: delta.modelId,
129
+ severity: 'semantic',
130
+ });
131
+ });
132
+ }
133
+
134
+ // ── Build the flat result ────────────────────────────────────────
135
+ const result: Record<string, unknown> = {
136
+ stamp,
137
+ signal: controller.signal,
138
+ onChange: (listener: (change: ContextChange) => void) => {
139
+ listeners.add(listener);
140
+ // The caller unsubscribes its own listener via the returned function.
141
+ // The underlying delta subscription lives for the snapshot's lifetime;
142
+ // there is no explicit dispose because a snapshot is short-lived (one
143
+ // LLM call's worth) and the subscription is cheap.
144
+ return () => {
145
+ listeners.delete(listener);
146
+ // Once the last listener is gone and the abort has fired, drop the
147
+ // delta subscription too — nothing is listening anymore.
148
+ if (listeners.size === 0 && controller.signal.aborted && unsubDelta) {
149
+ unsubDelta();
150
+ unsubDelta = null;
151
+ }
152
+ };
153
+ },
154
+ };
155
+ for (const [modelName, bucket] of Object.entries(data)) {
156
+ result[modelName] = bucket;
157
+ }
158
+
159
+ return result as Snapshot<TSchema, K>;
160
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Moved to the settlement core with the duplex transport (ADR 0016): keeping
3
+ * a long-lived socket's credential fresh is connection plumbing an agent needs
4
+ * as much as a browser does. This path re-exports it so existing importers
5
+ * stay unchanged.
6
+ */
7
+
8
+ export {
9
+ DEFAULT_PREROLL_INTERVAL_MS,
10
+ MIN_PREROLL_DELAY_MS,
11
+ computePrerollDelayMs,
12
+ CredentialLifecycle,
13
+ type CredentialRefreshOutcome,
14
+ type CredentialRecoveryOutcome,
15
+ type CredentialRefreshResult,
16
+ type CredentialRefresher,
17
+ type CredentialLifecycleContext,
18
+ } from '@abloatai/transaction/transport/credentialLifecycle';
@@ -0,0 +1,473 @@
1
+ /**
2
+ * The pipeline that takes incoming deltas from the server and lands them in
3
+ * local state. It has four stages: deduplicating deltas by state signature,
4
+ * per-delta bookkeeping and enqueue, a debounce for live traffic, and a flush
5
+ * that writes to the local store and then the in-memory pool. Every call it
6
+ * makes back into the surrounding store — resolving state fields, identifying
7
+ * custom entities, deduplicating, flushing, handling sync-group changes —
8
+ * routes through the {@link DeltaPipelineContext} interface, so a subclass that
9
+ * overrides any of those hooks still takes effect. The atomic frame-apply entry
10
+ * point lives on the store and drives this pipeline through {@link enqueueDelta}
11
+ * and {@link flushPendingDeltas}.
12
+ */
13
+
14
+ import { runInAction } from 'mobx';
15
+ import { globalRuntime } from '../context.js';
16
+ import type { RuntimeContext } from '../RuntimeContext.js';
17
+ import { ModelScope } from '../InstanceCache.js';
18
+ import type { Model } from '../Model.js';
19
+ import type { SyncDelta } from './SyncWebSocket.js';
20
+ import type { ModelData } from '@abloatai/transaction/types/modelData';
21
+ import {
22
+ runStage,
23
+ pluginsForStage,
24
+ type AbloPlugin,
25
+ type AppliedChange,
26
+ } from '../../plugin.js';
27
+
28
+ /**
29
+ * What the pipeline needs back from the surrounding store: the shared mutable
30
+ * pipeline state (backed by the store's own fields through accessors), narrow
31
+ * persistence and pool facades, and the store's overridable hooks, so a
32
+ * subclass's overrides continue to take effect.
33
+ */
34
+ export interface DeltaPipelineContext {
35
+ /** The owning client's runtime. Defaults to the module-global bridge. */
36
+ readonly runtime?: RuntimeContext;
37
+ /**
38
+ * The installed plugins, whose declared stage handlers this pipeline
39
+ * dispatches at each boundary. Empty (or absent) on directly-constructed
40
+ * stores, where the store's own apply is the whole pipeline.
41
+ */
42
+ readonly stagePlugins?: readonly AbloPlugin[];
43
+ // ── Shared pipeline state (host fields behind accessors) ──
44
+ pendingDeltas: SyncDelta[];
45
+ batchTimer: ReturnType<typeof setTimeout> | null;
46
+ /** Queue for deltas arriving during an active bootstrap; null when none. */
47
+ readonly bootstrapDeltaQueue: SyncDelta[] | null;
48
+ readonly smartSyncOptions: { readonly batchingDelay: number; readonly maxBatchSize: number };
49
+ /** Pool-applied cursor (`syncClient.position.applied`). */
50
+ readonly highestProcessedSyncId: number;
51
+ /** Resume/ack cursor (`syncClient.position.persisted`). */
52
+ readonly lastAckedId: number;
53
+
54
+ // ── SyncClient position/transaction bookkeeping ──
55
+ onDeltaReceived(
56
+ syncId: number,
57
+ transactionId?: string,
58
+ correlationId?: string,
59
+ ): void;
60
+ advanceApplied(syncId: number): void;
61
+ advancePersisted(syncId: number): void;
62
+
63
+ // ── Persistence + pool writes ──
64
+ processDeltaBatch(
65
+ deltas: {
66
+ syncId?: number;
67
+ actionType: SyncDelta['actionType'];
68
+ modelName: string;
69
+ modelId: string;
70
+ data: ModelData | null;
71
+ transactionId?: string;
72
+ }[],
73
+ ): Promise<{ results: AppliedChange[]; persistedSyncId: number }>;
74
+ /** Applies persisted delta results to the in-memory pool, with the host's relation enrichment bound. */
75
+ applyDeltaBatchToPool(results: AppliedChange[]): void;
76
+ /** Acknowledges a sync id back to the server; a no-op when the socket is down. */
77
+ acknowledge(syncId: number): void;
78
+
79
+ // ── Custom-entity pool ops (deltas that skip the local store) ──
80
+ readonly objectPool: {
81
+ get(id: string): Model | undefined;
82
+ add(model: Model, scope: ModelScope): void;
83
+ remove(id: string): boolean;
84
+ /** Full in-memory clear — the revocation-failure fallback (see
85
+ * {@link handleGroupHandlerFailure}). */
86
+ clear(): void;
87
+ };
88
+
89
+ // ── Dynamic-dispatch hooks back into the store (protected override points) ──
90
+ getStateFields(modelName: string): string[];
91
+ isCustomEntity(modelName: string): boolean;
92
+ createCustomEntity(modelName: string, modelId: string, data: Record<string, unknown>): Model | null;
93
+ deduplicateDeltas(deltas: SyncDelta[]): SyncDelta[];
94
+ flushPendingDeltas(): Promise<void>;
95
+ handleFlushError(error: unknown): void;
96
+ handleSyncGroupChange(delta: SyncDelta): Promise<void>;
97
+ handleGroupRemoved(delta: SyncDelta): Promise<void>;
98
+ /** Host's re-bootstrap trigger — the revocation-failure fallback. */
99
+ forceFullRebootstrap(): void;
100
+ cascadeCancelTransactionsForDeletedParent(parentModelName: string, parentId: string): void;
101
+ }
102
+
103
+ /**
104
+ * One drain per store. Incoming WebSocket frames may arrive while persistence
105
+ * and pool application are awaiting. Without a single-flight guard every
106
+ * frame starts another flush over the same mutable queue, duplicating work and
107
+ * allowing acknowledgements to race.
108
+ *
109
+ * The context object is memoized by BaseSyncedStore, so a WeakMap keeps this
110
+ * coordination private to the pipeline without adding lifecycle state to the
111
+ * public store surface.
112
+ */
113
+ const activeFlushes = new WeakMap<DeltaPipelineContext, Promise<void>>();
114
+
115
+ /**
116
+ * Handles a sync-group ('G' or 'S') delta whose handler rejected after the
117
+ * applied watermark had already advanced. That delta will never be redelivered,
118
+ * so a failed security clear — revoked data that must not stay cached — would
119
+ * otherwise fail silently. The safe fallback is blunt: drop the whole in-memory
120
+ * pool and force a full re-bootstrap, rebuilding local state from the server.
121
+ * It never throws, because it runs inside the pipeline's fire-and-forget path.
122
+ */
123
+ export function handleGroupHandlerFailure(
124
+ ctx: DeltaPipelineContext,
125
+ delta: SyncDelta,
126
+ error: unknown,
127
+ ): void {
128
+ (ctx.runtime ?? globalRuntime).logger.error(
129
+ 'Your access changed but cached data could not be cleared — resetting local data.',
130
+ {
131
+ syncId: delta.id,
132
+ error: error instanceof Error ? error.message : String(error),
133
+ },
134
+ );
135
+ try {
136
+ ctx.objectPool.clear();
137
+ } catch {
138
+ // In-memory clear must never mask the re-bootstrap below.
139
+ }
140
+ try {
141
+ ctx.forceFullRebootstrap();
142
+ } catch {
143
+ // Best-effort: the reconnect/bootstrap cycle self-heals on next connect.
144
+ }
145
+ }
146
+
147
+ /** Builds a small signature of a delta's state fields, used to detect no-op duplicate deltas. */
148
+ function extractStateSignature(
149
+ ctx: DeltaPipelineContext,
150
+ delta: SyncDelta,
151
+ ): Record<string, unknown> | null {
152
+ if (!delta.data || typeof delta.data !== 'object') return null;
153
+
154
+ const data = typeof delta.data === 'string'
155
+ ? (JSON.parse(delta.data) as Record<string, unknown>)
156
+ : (delta.data);
157
+
158
+ // Generic state fields — subclasses can override getStateFields() for model-specific fields
159
+ const fieldsToCheck = ctx.getStateFields(delta.modelName);
160
+ const signature: Record<string, unknown> = {
161
+ actionType: delta.actionType,
162
+ modelName: delta.modelName,
163
+ };
164
+
165
+ for (const field of fieldsToCheck) {
166
+ if (field in data) signature[field] = data[field];
167
+ }
168
+
169
+ return signature;
170
+ }
171
+
172
+ function isSameState(a: Record<string, unknown> | null, b: Record<string, unknown> | null): boolean {
173
+ if (!a || !b) return false;
174
+ const keys = Object.keys(a);
175
+ if (keys.length !== Object.keys(b).length) return false;
176
+ return keys.every((k) => a[k] === b[k]);
177
+ }
178
+
179
+ /** Deduplicate deltas to the same entity — keep meaningful state transitions only */
180
+ export function deduplicateDeltas(ctx: DeltaPipelineContext, deltas: SyncDelta[]): SyncDelta[] {
181
+ const byEntity = new Map<string, SyncDelta[]>();
182
+ for (const d of deltas) {
183
+ const key = `${d.modelName}:${d.modelId}`;
184
+ if (!byEntity.has(key)) byEntity.set(key, []);
185
+ byEntity.get(key)!.push(d);
186
+ }
187
+
188
+ const result: SyncDelta[] = [];
189
+ for (const entityDeltas of byEntity.values()) {
190
+ const sorted = entityDeltas.sort((a, b) => a.id - b.id);
191
+
192
+ // DELETE wins — it's the final state
193
+ const del = sorted.find((d) => d.actionType === 'D');
194
+ if (del) { result.push(del); continue; }
195
+
196
+ // Keep deltas that represent different states
197
+ const unique: SyncDelta[] = [];
198
+ let prev: Record<string, unknown> | null = null;
199
+ for (const d of sorted) {
200
+ const sig = extractStateSignature(ctx, d);
201
+ if (!isSameState(prev, sig)) { unique.push(d); prev = sig; }
202
+ }
203
+
204
+ if (unique.length > 0) {
205
+ result.push(...unique);
206
+ } else {
207
+ // `sorted` is never empty (every byEntity bucket gets at least one
208
+ // delta pushed) — the guard only narrows the indexed access.
209
+ const last = sorted.at(-1);
210
+ if (last) result.push(last);
211
+ }
212
+ }
213
+
214
+ return result.sort((a, b) => a.id - b.id);
215
+ }
216
+
217
+ /**
218
+ * Performs per-delta bookkeeping and enqueues the delta. Returns `true` when
219
+ * the delta was pushed onto `pendingDeltas` — a regular batchable insert,
220
+ * update, covering, or delete that a later flush must drain — and `false` when
221
+ * it was skipped as a duplicate, deferred into the bootstrap queue, or handled
222
+ * immediately (a 'G'/'S' sync-group change). It does not schedule the flush
223
+ * itself; the caller chooses whether to debounce live traffic or flush a
224
+ * catch-up frame atomically.
225
+ */
226
+ export function enqueueDelta(
227
+ ctx: DeltaPipelineContext,
228
+ delta: SyncDelta,
229
+ options: { authoritative?: boolean } = {},
230
+ ): boolean {
231
+ // Dedup guard — skip already-processed deltas. The `applied` watermark is a
232
+ // valid skip threshold only for in-order live traffic; an authoritative
233
+ // catch-up frame bypasses it, so an out-of-order live delta that advanced the
234
+ // watermark can't cause the frame's lower ids to be dropped silently.
235
+ if (!options.authoritative && delta.id > 0 && delta.id <= ctx.highestProcessedSyncId) {
236
+ return false;
237
+ }
238
+
239
+ // Confirm awaiting transactions via sync ID threshold (before batching)
240
+ ctx.onDeltaReceived(delta.id, delta.transactionId, delta.correlationId);
241
+
242
+ // Queue during active bootstrap
243
+ if (ctx.bootstrapDeltaQueue !== null) {
244
+ ctx.bootstrapDeltaQueue.push(delta);
245
+ return false;
246
+ }
247
+
248
+ // Advance watermark
249
+ ctx.advanceApplied(delta.id);
250
+
251
+ // Sync group added — handle immediately. Accepts both the batched
252
+ // (addedGroups/removedGroups) and incremental (group/userId) payloads. This
253
+ // is deliberately not fire-and-forget: the watermark has already advanced, so
254
+ // a rejected handler (a failed security clear) must trigger the fallback
255
+ // rather than vanish.
256
+ if (delta.actionType === 'G') {
257
+ void ctx.handleSyncGroupChange(delta).catch((error: unknown) => {
258
+ handleGroupHandlerFailure(ctx, delta, error);
259
+ });
260
+ return false;
261
+ }
262
+
263
+ // Sync group removed — handle immediately. Clears affected local state
264
+ // and forces re-bootstrap with the updated group list. Same fallback:
265
+ // a failed revocation clear escalates instead of leaving revoked rows.
266
+ if (delta.actionType === 'S') {
267
+ void ctx.handleGroupRemoved(delta).catch((error: unknown) => {
268
+ handleGroupHandlerFailure(ctx, delta, error);
269
+ });
270
+ return false;
271
+ }
272
+
273
+ // Delete — run the cascade cancel immediately (O(1) through the foreign-key
274
+ // index; it must run before any later update on the same model lands, so
275
+ // pending update transactions for soon-deleted children don't race their
276
+ // parent's delete). The persistence and pool write still goes through the
277
+ // same batched path as updates: flushing each delete on its own produced one
278
+ // store write, one pool mutation, and one `models:changed` event per row, so
279
+ // deleting a parent with many children fanned out into many of each, whereas
280
+ // the batched path collapses them into one per flush. Deduplication in
281
+ // `flushPendingDeltas` handles an update-then-delete on the same model by
282
+ // replaying in arrival order.
283
+ if (delta.actionType === 'D') {
284
+ ctx.cascadeCancelTransactionsForDeletedParent(delta.modelName, delta.modelId);
285
+ }
286
+
287
+ // The delta is accepted and queued — the `receive` stage boundary.
288
+ runStage(ctx.stagePlugins ?? [], 'receive', { delta });
289
+ ctx.pendingDeltas.push(delta);
290
+ return true;
291
+ }
292
+
293
+ /** Debounce a flush for live single-delta traffic. */
294
+ export function scheduleDeltaFlush(ctx: DeltaPipelineContext): void {
295
+ if (ctx.batchTimer) clearTimeout(ctx.batchTimer);
296
+
297
+ if (ctx.pendingDeltas.length >= ctx.smartSyncOptions.maxBatchSize) {
298
+ void ctx.flushPendingDeltas().catch(ctx.handleFlushError);
299
+ } else {
300
+ ctx.batchTimer = setTimeout(() => {
301
+ void ctx.flushPendingDeltas().catch(ctx.handleFlushError);
302
+ }, ctx.smartSyncOptions.batchingDelay);
303
+ }
304
+ }
305
+
306
+ /** Apply an authoritative delta frame as one atomic flush. */
307
+ export function applyDeltaFrame(ctx: DeltaPipelineContext, deltas: SyncDelta[]): void {
308
+ let enqueuedAny = false;
309
+ for (const delta of deltas) {
310
+ if (enqueueDelta(ctx, delta, { authoritative: true })) enqueuedAny = true;
311
+ }
312
+ if (!enqueuedAny) return;
313
+ if (ctx.batchTimer) {
314
+ clearTimeout(ctx.batchTimer);
315
+ ctx.batchTimer = null;
316
+ }
317
+ void ctx.flushPendingDeltas().catch(ctx.handleFlushError);
318
+ }
319
+
320
+ /**
321
+ * Flushes the queued deltas: deduplicates them, applies custom-entity deltas
322
+ * straight to the pool, writes the rest to the local store and then the pool,
323
+ * and advances the acknowledgement cursor once the store write has committed.
324
+ */
325
+ export async function flushPendingDeltas(ctx: DeltaPipelineContext): Promise<void> {
326
+ const activeFlush = activeFlushes.get(ctx);
327
+ if (activeFlush) return activeFlush;
328
+
329
+ const flush = drainPendingDeltas(ctx);
330
+ activeFlushes.set(ctx, flush);
331
+ try {
332
+ await flush;
333
+ } finally {
334
+ if (activeFlushes.get(ctx) === flush) activeFlushes.delete(ctx);
335
+ }
336
+ }
337
+
338
+ /**
339
+ * Detaches each batch before its first await, then keeps draining anything
340
+ * that arrived in the meantime. Detaching is the critical ownership transfer:
341
+ * a later frame appends to a fresh queue instead of observing and reprocessing
342
+ * the batch currently being persisted.
343
+ */
344
+ async function drainPendingDeltas(ctx: DeltaPipelineContext): Promise<void> {
345
+ while (ctx.pendingDeltas.length > 0) {
346
+ if (ctx.batchTimer) {
347
+ clearTimeout(ctx.batchTimer);
348
+ ctx.batchTimer = null;
349
+ }
350
+
351
+ const queuedDeltas = ctx.pendingDeltas;
352
+ ctx.pendingDeltas = [];
353
+ try {
354
+ await flushDeltaBatch(ctx, queuedDeltas);
355
+ } catch (error) {
356
+ // Preserve the pre-existing retry contract. The failed detached batch
357
+ // goes back ahead of deltas received while it was in flight.
358
+ ctx.pendingDeltas = [...queuedDeltas, ...ctx.pendingDeltas];
359
+ throw error;
360
+ }
361
+
362
+ if (ctx.pendingDeltas.length > 0) {
363
+ // A sustained stream can refill the detached queue before every
364
+ // persistence promise settles. Promise-only looping then forms an
365
+ // unbounded microtask chain that starves WebSocket reads, timers and
366
+ // replication keepalives. Give the host one macrotask turn between
367
+ // owned batches; Node has setImmediate, browsers fall back to a timer.
368
+ await yieldToHost();
369
+ }
370
+ }
371
+
372
+ if (ctx.batchTimer) {
373
+ clearTimeout(ctx.batchTimer);
374
+ ctx.batchTimer = null;
375
+ }
376
+ }
377
+
378
+ function yieldToHost(): Promise<void> {
379
+ const immediate = (
380
+ globalThis as {
381
+ setImmediate?: (callback: () => void) => unknown;
382
+ }
383
+ ).setImmediate;
384
+ return new Promise((resolve) => {
385
+ if (immediate) immediate(resolve);
386
+ else setTimeout(resolve, 0);
387
+ });
388
+ }
389
+
390
+ async function flushDeltaBatch(
391
+ ctx: DeltaPipelineContext,
392
+ queuedDeltas: SyncDelta[],
393
+ ): Promise<void> {
394
+ const stagePlugins = ctx.stagePlugins ?? [];
395
+ const deduplicatedDeltas = ctx.deduplicateDeltas(queuedDeltas);
396
+ runStage(stagePlugins, 'dedupe', { deltas: deduplicatedDeltas });
397
+
398
+ // Custom entities → apply straight to the pool, skipping the local store.
399
+ const customDeltas = deduplicatedDeltas.filter((d) => ctx.isCustomEntity(d.modelName));
400
+ if (customDeltas.length > 0) {
401
+ runInAction(() => {
402
+ for (const delta of customDeltas) {
403
+ const data = typeof delta.data === 'string'
404
+ ? (JSON.parse(delta.data) as Record<string, unknown>)
405
+ : (delta.data!);
406
+
407
+ // 'C' (Covering) is treated identically to 'I' here — the client
408
+ // gained permission to see the entity, so we insert it into the
409
+ // pool as if newly created.
410
+ if (delta.actionType === 'I' || delta.actionType === 'U' || delta.actionType === 'C') {
411
+ const existing = ctx.objectPool.get(delta.modelId);
412
+ if (existing) {
413
+ existing.updateFromData(data);
414
+ } else {
415
+ const model = ctx.createCustomEntity(delta.modelName, delta.modelId, data);
416
+ if (model) { model.markAsPersisted(); ctx.objectPool.add(model, ModelScope.live); }
417
+ }
418
+ } else if (delta.actionType === 'D') {
419
+ ctx.objectPool.remove(delta.modelId);
420
+ }
421
+ }
422
+ });
423
+ }
424
+
425
+ // Regular deltas → the local store, then the pool.
426
+ // 'G' and 'S' deltas are handled earlier (handleSyncGroupChange /
427
+ // handleGroupRemoved) and never reach here, though the persistence
428
+ // signature accepts them defensively.
429
+ const regularDeltas = deduplicatedDeltas.filter((d) => !ctx.isCustomEntity(d.modelName));
430
+ const batch = await ctx.processDeltaBatch(
431
+ regularDeltas.map((d) => ({
432
+ syncId: d.id,
433
+ actionType: d.actionType,
434
+ modelName: d.modelName,
435
+ modelId: d.modelId,
436
+ data: typeof d.data === 'string' ? JSON.parse(d.data) : d.data,
437
+ // Thread `transactionId` through so the receive layer can recognize
438
+ // echoes of locally-applied transactions and skip the pool mutation.
439
+ transactionId: d.transactionId,
440
+ }))
441
+ );
442
+ const dbResults = batch.results;
443
+ runStage(stagePlugins, 'persist', { deltas: regularDeltas });
444
+
445
+ // Apply the batch results to the in-memory graph. When a plugin has
446
+ // declared the `apply` stage, its handlers ARE the apply — the
447
+ // materialiser attached where it said it would. The direct call is the
448
+ // bridge for stores constructed without plugins (subclasses, tests),
449
+ // whose own apply is the whole pipeline.
450
+ if (pluginsForStage(stagePlugins, 'apply').length > 0) {
451
+ runStage(stagePlugins, 'apply', { changes: dbResults });
452
+ } else {
453
+ ctx.applyDeltaBatchToPool(dbResults);
454
+ }
455
+
456
+ // Acknowledge and advance the sync cursor, gated on persistence.
457
+ //
458
+ // We must acknowledge `persistedSyncId` — the high-water mark of deltas whose
459
+ // store transaction actually committed — not the input batch's last delta id.
460
+ // Acknowledging the input range would advance the server's view past deltas
461
+ // that never persisted; the next catch-up would then send the advanced cursor,
462
+ // the server would answer "you're up to date", and the unpersisted delta would
463
+ // be lost. The cursor and the persisted state must move together.
464
+ const persistedSyncId = batch.persistedSyncId;
465
+ if (persistedSyncId > ctx.lastAckedId) {
466
+ ctx.acknowledge(persistedSyncId);
467
+ ctx.advancePersisted(persistedSyncId);
468
+ runStage(stagePlugins, 'acknowledge', { syncId: persistedSyncId });
469
+ }
470
+
471
+ // Cache invalidation happens automatically via the 'models:changed' event.
472
+ runStage(stagePlugins, 'notify', { changes: dbResults });
473
+ }