@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,1124 @@
1
+ /**
2
+ * Model is the base class for the sync engine's domain models. A model is a
3
+ * plain domain object: it holds data and business logic, tracks its own
4
+ * property changes, and validates itself, and it returns the changes to apply
5
+ * rather than applying them. It does not reach into stores or singletons and
6
+ * does not perform side effects such as saving or sending notifications;
7
+ * persistence and sync are driven by the store that owns the model.
8
+ */
9
+
10
+ import { runInAction, isComputedProp } from 'mobx';
11
+ import { v4 as uuid } from 'uuid';
12
+ import { M1 } from './utils/mobxSetup.js';
13
+ import { getActiveRegistry, hasActiveRegistry } from './ModelRegistry.js';
14
+ import { getContext } from './context.js';
15
+ import { AbloValidationError } from '@abloatai/transaction/errors';
16
+ /** Store interface — methods that Model subclasses can call on the store */
17
+ interface SyncStoreRef {
18
+ getByForeignKey<T extends Model>(modelName: string, foreignKey: string, id: string): T[];
19
+ retrieve<T extends Model>(modelClass: abstract new (...args: never[]) => T, id: string): T | undefined;
20
+ /** Lookup a model by ID alone. Returns the pool entry regardless of type. */
21
+ getById(id: string): Model | undefined;
22
+ /** Persist a model (upsert). */
23
+ save(model: Model): Promise<void>;
24
+ /** Delete a model. */
25
+ delete(model: Model): Promise<void>;
26
+ /** Archive a model (soft delete). */
27
+ archive(model: Model): Promise<void>;
28
+ /** Unarchive a previously archived model. */
29
+ unarchive(model: Model): Promise<void>;
30
+ }
31
+ import type { PropertyMetadata } from '@abloatai/transaction/types';
32
+
33
+ /**
34
+ * Internal wire-hydration hint. Symbol-keyed and non-enumerable at the call
35
+ * site, so it can never enter persistence, JSON, a mutation payload, or a
36
+ * consumer's model fields.
37
+ */
38
+ export const DEFER_MODEL_OBSERVABILITY = Symbol('ablo.deferModelObservability');
39
+
40
+ // Type aliases for better type safety
41
+ /** Model data type - allows any object with string keys.
42
+ * Mirrors `ModelData` exported from BaseSyncedStore — kept local to
43
+ * break the import cycle between Model and BaseSyncedStore. */
44
+ type ModelData = Record<string, unknown>;
45
+
46
+ /** Represents a property value change with old and new values */
47
+ export interface PropertyChange {
48
+ old: unknown;
49
+ new: unknown;
50
+ }
51
+
52
+ /** Validation rule function that returns error string or null if valid */
53
+ type ValidationRule = (value: unknown) => string | null;
54
+
55
+ /** Interface for objects that can be disposed */
56
+ interface Disposable {
57
+ dispose(): void;
58
+ }
59
+
60
+ /** Field change information for activity tracking */
61
+ interface FieldChange {
62
+ field: string;
63
+ oldValue: unknown;
64
+ newValue: unknown;
65
+ fieldType: string;
66
+ }
67
+
68
+ /**
69
+ * Validation error for model validation failures
70
+ */
71
+ export class ValidationError extends Error {
72
+ constructor(public errors: string[]) {
73
+ super(`Validation failed: ${errors.join(', ')}`);
74
+ this.name = 'ValidationError';
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Model changes for external processing
80
+ */
81
+ export interface ModelChanges {
82
+ type: 'create' | 'update' | 'delete' | 'archive' | 'unarchive';
83
+ modelName: string;
84
+ modelId: string;
85
+ changes?: Map<string, PropertyChange>;
86
+ timestamp: Date;
87
+ }
88
+
89
+ /** Shared frozen default for {@link Model.getDerivedGetterNames}. */
90
+ const EMPTY_DERIVED_GETTERS: readonly string[] = Object.freeze([]);
91
+
92
+ /**
93
+ * The abstract base class every domain model extends. It holds the model's id
94
+ * and timestamps, tracks in-place property changes for change detection and
95
+ * undo, and serializes itself, while leaving persistence and sync to the store
96
+ * that owns it.
97
+ */
98
+ export abstract class Model {
99
+ /** Static reference to active SyncedStore for reactive queries */
100
+ private static store: SyncStoreRef | null = null;
101
+
102
+ /** Unique identifier - always permanent UUID */
103
+ id: string;
104
+
105
+ /** Client ID - always equals id, kept for compatibility */
106
+ clientId: string;
107
+
108
+ /** MobX observable properties storage */
109
+ _mobxProperties: ModelData = {};
110
+
111
+ /** Referenced models cache */
112
+ _referencedModels: Record<string, Model | null> = {};
113
+
114
+ /** Track property changes */
115
+ modifiedProperties = new Map<string, PropertyChange>();
116
+
117
+ /** Track if this is a new model */
118
+ private _isNew = true;
119
+
120
+ /** Original data snapshot */
121
+ private _originalData?: ModelData;
122
+
123
+ /** Sync status */
124
+ syncStatus: 'pending' | 'syncing' | 'synced' = 'pending';
125
+
126
+ /** Timestamps */
127
+ createdAt?: Date;
128
+ updatedAt?: Date;
129
+ archivedAt?: Date | null;
130
+
131
+ /** Validation rules */
132
+ protected validationRules: Record<string, ValidationRule[]> = {};
133
+
134
+ /** Lifecycle state */
135
+ private isDisposed = false;
136
+ private disposers: (() => void)[] = [];
137
+ /**
138
+ * Whether MobX field instrumentation has been installed. Wire hydration can
139
+ * deliberately defer this work until the cache hands the model to a
140
+ * consumer; optimistic/local models still install it in their constructor.
141
+ */
142
+ private _observabilityReady = false;
143
+ private _observabilityDeferred = false;
144
+
145
+ /**
146
+ * Track observed LazyReferenceCollections for GC prevention
147
+ * When any collection is being observed by React, the model should not be GC'd
148
+ * Following MobX best practice: https://mobx.js.org/lazy-observables.html
149
+ */
150
+ private _observedCollections = new Set<Disposable>();
151
+
152
+ constructor(data: Partial<Model> = {}) {
153
+ // Always generate permanent UUID on client
154
+ this.id = data.id ?? Model.generateId();
155
+ this.clientId = this.id; // No more temp IDs!
156
+
157
+ // Ensure dates are Date objects, not strings
158
+ this.createdAt = data.createdAt
159
+ ? data.createdAt instanceof Date
160
+ ? data.createdAt
161
+ : new Date(data.createdAt)
162
+ : new Date();
163
+ // A record that arrives WITH `createdAt` but WITHOUT `updatedAt` is
164
+ // server/IDB data whose update timestamp didn't survive the wire —
165
+ // falling back to "now" here fabricated an edit time for every such
166
+ // record on every bootstrap (the reports gallery sorted everything to
167
+ // "edited just now"). Fall back to createdAt instead; only a genuinely
168
+ // new local model (no dates at all) stamps the current time.
169
+ this.updatedAt = data.updatedAt
170
+ ? data.updatedAt instanceof Date
171
+ ? data.updatedAt
172
+ : new Date(data.updatedAt)
173
+ : data.createdAt
174
+ ? new Date(this.createdAt)
175
+ : new Date();
176
+ this.syncStatus = data.syncStatus ?? 'pending';
177
+ }
178
+
179
+ /**
180
+ * Generate unique ID
181
+ */
182
+ static generateId(): string {
183
+ return uuid();
184
+ }
185
+
186
+ /**
187
+ * Set the active SyncedStore reference for reactive queries.
188
+ * Called once at engine initialization.
189
+ */
190
+ static setStore(store: SyncStoreRef): void {
191
+ Model.store = store;
192
+ }
193
+
194
+ /**
195
+ * Get the active SyncedStore reference for reactive queries.
196
+ *
197
+ * Returns `null` if no store has been registered yet (e.g. during
198
+ * bootstrap before the engine is ready). Subclasses should use this
199
+ * instead of reaching into the private static field via bracket
200
+ * notation — the generic parameter lets app-side Model subclasses
201
+ * narrow the return to their concrete store type.
202
+ *
203
+ * @example
204
+ * // In a Section model getter
205
+ * const store = Section.getStore();
206
+ * if (!store) return [];
207
+ * return store.getByForeignKey<Block>('Block', 'sectionId', this.id);
208
+ */
209
+ static getStore<T extends SyncStoreRef = SyncStoreRef>(): T | null {
210
+ return Model.store as T | null;
211
+ }
212
+
213
+ /**
214
+ * Initialize MobX observability
215
+ */
216
+ public makeObservable(): void {
217
+ if (this._observabilityReady) return;
218
+ const modelName = this.getModelName();
219
+
220
+ // Get metadata from static ModelRegistry
221
+ const propertyMetadata = getActiveRegistry().getProperties(modelName);
222
+ const referenceMetadata = getActiveRegistry().getReferences(modelName);
223
+
224
+ // Use M1 for observability setup
225
+ M1(this, propertyMetadata, referenceMetadata);
226
+ this._observabilityReady = true;
227
+ this._observabilityDeferred = false;
228
+ }
229
+
230
+ /** Mark a schema model as intentionally cold until cache exposure. */
231
+ protected deferObservability(): void {
232
+ if (!this._observabilityReady) this._observabilityDeferred = true;
233
+ }
234
+
235
+ /**
236
+ * Activate a cold wire-hydrated model before it crosses a consumer-visible
237
+ * cache boundary. Idempotent, so ordinary eagerly observable models pay only
238
+ * the boolean check.
239
+ */
240
+ public ensureObservable(): void {
241
+ if (this._observabilityDeferred) this.makeObservable();
242
+ }
243
+
244
+ /**
245
+ * Track property changes
246
+ */
247
+ propertyChanged(propertyName: string, oldValue: unknown, newValue: unknown): void {
248
+ if (oldValue === newValue) return;
249
+
250
+ runInAction(() => {
251
+ // Preserve the earliest captured `old` for this field until the entry
252
+ // is cleared (by `clearChanges` on sync-ack or by a mutator consuming
253
+ // it). Consecutive in-place mutations between mutator invocations —
254
+ // e.g. a drag loop writing `block.position = ...` on every frame —
255
+ // would otherwise overwrite `.old` with each frame's predecessor,
256
+ // destroying the pre-session baseline that `RecordingMutation`
257
+ // relies on to record a correct undo inverse. `.new` always reflects
258
+ // the latest value so the transaction queue's `getChanges()` keeps
259
+ // sending the right payload to the server.
260
+ const existing = this.modifiedProperties.get(propertyName);
261
+ this.modifiedProperties.set(propertyName, {
262
+ old: existing ? existing.old : oldValue,
263
+ new: newValue,
264
+ });
265
+ this.updatedAt = new Date();
266
+ });
267
+ }
268
+
269
+ /**
270
+ * Get changes as object
271
+ */
272
+ getChanges(): ModelData {
273
+ const changes: ModelData = {};
274
+
275
+ for (const [propertyName, change] of this.modifiedProperties) {
276
+ changes[propertyName] = change.new;
277
+ }
278
+
279
+ return changes;
280
+ }
281
+
282
+ /**
283
+ * Check if model has changes
284
+ */
285
+ get hasChanges(): boolean {
286
+ return this.modifiedProperties.size > 0;
287
+ }
288
+
289
+ /**
290
+ * Mark model as persisted (not new)
291
+ */
292
+ markAsPersisted(): void {
293
+ this._isNew = false;
294
+ this._originalData = this.captureSnapshot();
295
+ }
296
+
297
+ /**
298
+ * Check if this is a new model
299
+ */
300
+ isNew(): boolean {
301
+ return this._isNew;
302
+ }
303
+
304
+ /**
305
+ * Return a read-only view of the snapshot taken at {@link markAsPersisted}
306
+ * or at load time. The undo machinery uses it to recover a field's pre-edit
307
+ * value when the field was written without first being tracked in
308
+ * `modifiedProperties`. The returned object is the live snapshot, so callers
309
+ * must not mutate it.
310
+ *
311
+ * This per-instance baseline is needed because application code can edit a
312
+ * model in two ways that coexist: a direct property write
313
+ * (`section.title = 'foo'`) and a recorded mutation. A design in which every
314
+ * write went through a single recorded path would not need it, since the
315
+ * last acknowledged state would already be the authoritative baseline.
316
+ */
317
+ getOriginalSnapshot(): Readonly<ModelData> | undefined {
318
+ return this._originalData;
319
+ }
320
+
321
+ /**
322
+ * Clear tracked changes
323
+ */
324
+ clearChanges(): void {
325
+ runInAction(() => {
326
+ this.modifiedProperties.clear();
327
+ this._originalData = this.captureSnapshot();
328
+ });
329
+ }
330
+
331
+ /**
332
+ * Capture a before-image for `keys` — the single source of truth for the
333
+ * "previous value" that undo inverses are built from. Both undo paths call
334
+ * this so they can never drift: the stream path
335
+ * (`MutationQueue.extractPreviousData`) and the manual-record path
336
+ * (`RecordingMutation.snapshotFields`).
337
+ *
338
+ * Resolution order per key:
339
+ * 1. `modifiedProperties.get(key).old` — first-old-wins pre-session
340
+ * baseline, set whenever the field was mutated in place before commit.
341
+ * 2. `getOriginalSnapshot()[key]` — the last loaded/acked row, the correct
342
+ * before-image for a key written without a prior in-place mutation
343
+ * (e.g. a `precomputedChanges` write).
344
+ * 3. `fallbackToLive` only — the current live value. The manual-record path
345
+ * wants this last resort; the stream path deliberately omits unresolved
346
+ * keys so `buildUndoOps` drops an un-revertible inverse rather than
347
+ * inventing one. The flag is the one intentional difference between the
348
+ * two callers — do not collapse it.
349
+ *
350
+ * `id` is always skipped. Values are read out per-key, so the
351
+ * `getOriginalSnapshot()` "callers must not mutate" contract is preserved.
352
+ *
353
+ * Invariant this relies on: a given undo scope is either stream-recorded
354
+ * (`recordFromStream: true`) or manual (`useMutators({ undoScope })`), never
355
+ * both — otherwise a write would be captured twice. No surface sets both.
356
+ */
357
+ capturePreviousValues(
358
+ keys: Iterable<string>,
359
+ opts?: { fallbackToLive?: boolean },
360
+ ): ModelData {
361
+ const out: ModelData = {};
362
+ const modified = this.modifiedProperties instanceof Map ? this.modifiedProperties : null;
363
+ const original = this.getOriginalSnapshot();
364
+ for (const key of keys) {
365
+ if (key === 'id') continue;
366
+ const mod = modified?.get(key);
367
+ if (mod) {
368
+ out[key] = mod.old;
369
+ } else if (original && key in original) {
370
+ out[key] = original[key];
371
+ } else if (opts?.fallbackToLive) {
372
+ out[key] = Reflect.get(this, key);
373
+ }
374
+ }
375
+ return out;
376
+ }
377
+
378
+ /**
379
+ * Drop the `modifiedProperties` entries for `keys` — re-baselines a field
380
+ * after its `.old` has been frozen into a committed transaction, so the next
381
+ * write to the same field starts from this commit's result rather than the
382
+ * stale pre-session `.old` that {@link propertyChanged}'s first-old-wins
383
+ * policy preserves. Safe because the committed transaction owns its own
384
+ * frozen `data`/`previousData`; neither re-reads `modifiedProperties`. `id`
385
+ * is never consumed. With no `keys`, consumes every tracked field.
386
+ */
387
+ consumeModifiedFields(keys?: Iterable<string>): void {
388
+ if (!(this.modifiedProperties instanceof Map) || this.modifiedProperties.size === 0) {
389
+ return;
390
+ }
391
+ const only = keys ? new Set(keys) : null;
392
+ for (const key of [...this.modifiedProperties.keys()]) {
393
+ if (key === 'id') continue;
394
+ if (only && !only.has(key)) continue;
395
+ this.modifiedProperties.delete(key);
396
+ }
397
+ }
398
+
399
+ /**
400
+ * Validate model
401
+ */
402
+ validate(): string[] {
403
+ if (this.isDisposed) {
404
+ throw new AbloValidationError('Cannot validate disposed model', {
405
+ code: 'model_disposed',
406
+ });
407
+ }
408
+
409
+ const errors: string[] = [];
410
+ const modelName = this.getModelName();
411
+ const properties = getActiveRegistry().getProperties(modelName);
412
+
413
+ if (properties) {
414
+ const json = this.toJSON();
415
+ for (const [propName, metadata] of properties) {
416
+ // Check required fields
417
+ if (!metadata.nullable && !metadata.optional) {
418
+ const value = json[propName];
419
+ if (value == null || value === '') {
420
+ errors.push(`${propName} is required`);
421
+ }
422
+ }
423
+
424
+ // Run custom validation rules
425
+ const rules = this.validationRules[propName];
426
+ if (rules) {
427
+ const value = json[propName];
428
+ for (const rule of rules) {
429
+ const error = rule(value);
430
+ if (error) errors.push(error);
431
+ }
432
+ }
433
+ }
434
+ }
435
+
436
+ // Run model-specific validation
437
+ const customErrors = this.customValidate();
438
+ errors.push(...customErrors);
439
+
440
+ return errors;
441
+ }
442
+
443
+ /**
444
+ * Override for custom validation
445
+ */
446
+ protected customValidate(): string[] {
447
+ return [];
448
+ }
449
+
450
+ /**
451
+ * Add validation rule
452
+ */
453
+ protected addValidationRule(propName: string, rule: ValidationRule): void {
454
+ this.validationRules[propName] ??= [];
455
+ this.validationRules[propName].push(rule);
456
+ }
457
+
458
+ /**
459
+ * Prepare save operation
460
+ * Returns the changes to be saved without side effects
461
+ */
462
+ prepareSave(): ModelChanges | null {
463
+ if (this.isDisposed) {
464
+ throw new AbloValidationError('Cannot prepare save for disposed model', {
465
+ code: 'model_disposed',
466
+ });
467
+ }
468
+
469
+ // Validate first
470
+ const errors = this.validate();
471
+ if (errors.length > 0) {
472
+ throw new ValidationError(errors);
473
+ }
474
+
475
+ if (this._isNew) {
476
+ // New model - return create operation
477
+ return {
478
+ type: 'create',
479
+ modelName: this.getModelName(), // the registered model name
480
+ modelId: this.id,
481
+ timestamp: new Date(),
482
+ };
483
+ } else if (this.hasChanges) {
484
+ // Existing model with changes - return update operation
485
+ return {
486
+ type: 'update',
487
+ modelName: this.getModelName(), // the registered model name
488
+ modelId: this.id,
489
+ changes: new Map(this.modifiedProperties),
490
+ timestamp: new Date(),
491
+ };
492
+ }
493
+
494
+ // No changes
495
+ return null;
496
+ }
497
+
498
+ /**
499
+ * Prepare delete operation
500
+ */
501
+ prepareDelete(): ModelChanges {
502
+ if (this.isDisposed) {
503
+ throw new AbloValidationError('Cannot prepare delete for disposed model', {
504
+ code: 'model_disposed',
505
+ });
506
+ }
507
+
508
+ this.willDelete();
509
+
510
+ return {
511
+ type: 'delete',
512
+ modelName: this.getModelName(), // the registered model name
513
+ modelId: this.id,
514
+ timestamp: new Date(),
515
+ };
516
+ }
517
+
518
+ /**
519
+ * Prepare archive operation
520
+ */
521
+ prepareArchive(): ModelChanges {
522
+ if (this.isDisposed) {
523
+ throw new AbloValidationError('Cannot prepare archive for disposed model', {
524
+ code: 'model_disposed',
525
+ });
526
+ }
527
+
528
+ this.archivedAt = new Date();
529
+
530
+ return {
531
+ type: 'archive',
532
+ modelName: this.getModelName(), // the registered model name
533
+ modelId: this.id,
534
+ timestamp: new Date(),
535
+ };
536
+ }
537
+
538
+ /**
539
+ * Prepare unarchive operation
540
+ */
541
+ prepareUnarchive(): ModelChanges {
542
+ if (this.isDisposed) {
543
+ throw new AbloValidationError('Cannot prepare unarchive for disposed model', {
544
+ code: 'model_disposed',
545
+ });
546
+ }
547
+
548
+ this.archivedAt = null;
549
+
550
+ return {
551
+ type: 'unarchive',
552
+ modelName: this.getModelName(), // the registered model name
553
+ modelId: this.id,
554
+ timestamp: new Date(),
555
+ };
556
+ }
557
+
558
+ /**
559
+ * Safely assign each field of `data` onto this instance, skipping `id`,
560
+ * unknown keys, MobX computed accessors, and getter-only (read-only)
561
+ * properties, and coercing date fields. Shared by `updateFromData`
562
+ * (hydration) and `applyChanges` (local user update).
563
+ *
564
+ * Change tracking is explicit: for every field actually
565
+ * written, `onWrite(key, oldValue, newValue)` is invoked with the value
566
+ * captured immediately before assignment. `applyChanges` passes a hook
567
+ * that records the change in `modifiedProperties`; `updateFromData`
568
+ * passes none (hydration must not generate outbound mutations).
569
+ *
570
+ * This is not the ONLY tracking path. `mobxSetup`'s `observe()` bridge is
571
+ * live and forwards a DIRECT assignment (`layer.position = next`) to
572
+ * `propertyChanged` as well — product code writes model fields directly in
573
+ * plenty of places (keyboard nudge, formatting, AI tools), and those writes
574
+ * have to reach the server and the undo stream too. The two paths compose:
575
+ * a direct write establishes the first-old-wins baseline, and a later
576
+ * `applyChanges` carrying the value the model already holds is a no-op that
577
+ * leaves that baseline intact.
578
+ */
579
+ private assignFieldsFromData(
580
+ data: ModelData,
581
+ onWrite?: (key: string, oldValue: unknown, newValue: unknown) => void,
582
+ ): void {
583
+ // Update properties with safety checks for read-only/computed accessors
584
+ for (const [key, raw] of Object.entries(data)) {
585
+ if (key === 'id') continue;
586
+
587
+ // Only attempt to set if the property exists on instance or prototype
588
+ if (!(this.hasOwnProperty(key) || key in this)) continue;
589
+
590
+ // Never assign to MobX computed properties (they may expose a setter that throws)
591
+ try {
592
+ if (isComputedProp(this, key)) {
593
+ continue;
594
+ }
595
+ } catch {
596
+ // If MobX internals are unavailable for some reason, fall back to descriptor checks below
597
+ }
598
+
599
+ // Resolve property descriptor from own or prototype chain
600
+ const ownDesc = Object.getOwnPropertyDescriptor(this, key);
601
+ let desc = ownDesc;
602
+ if (!desc) {
603
+ let proto = Object.getPrototypeOf(this) as object | null;
604
+ while (proto && proto !== Object.prototype && !desc) {
605
+ desc = Object.getOwnPropertyDescriptor(proto, key);
606
+ proto = Object.getPrototypeOf(proto) as object | null;
607
+ }
608
+ }
609
+
610
+ // Determine writability: allow if data descriptor writable, or accessor with setter
611
+ const writable = desc
612
+ ? ('writable' in desc && !!desc.writable) ||
613
+ ('set' in desc && typeof desc.set === 'function')
614
+ : true;
615
+ if (!writable) {
616
+ // Skip read-only accessor properties (getter-only)
617
+ continue;
618
+ }
619
+
620
+ // Handle date conversions
621
+ const value =
622
+ (key === 'createdAt' || key === 'updatedAt' || key === 'archivedAt') && raw
623
+ ? new Date(raw as string | number)
624
+ : raw;
625
+
626
+ // Capture the pre-write value BEFORE assignment so trackers
627
+ // (undo inverse, getChanges) see the true previous value.
628
+ const oldValue = onWrite ? (this as Record<string, unknown>)[key] : undefined;
629
+
630
+ // Dynamic property assignment - use indexed access
631
+ (this as Record<string, unknown>)[key] = value;
632
+
633
+ onWrite?.(key, oldValue, value);
634
+ }
635
+ }
636
+
637
+ /**
638
+ * Update from raw data (hydration)
639
+ *
640
+ * Used for inbound server deltas and pool upserts. Change tracking is
641
+ * deliberately suppressed: hydration writes must not land in
642
+ * `modifiedProperties`, otherwise applying a server delta would queue a
643
+ * brand-new outbound mutation and the record would echo forever. For a
644
+ * local user edit, use `applyChanges` instead.
645
+ *
646
+ * Suppression takes two forms and BOTH are load-bearing: we pass no
647
+ * `onWrite` hook, and we clear/restore `modifiedProperties` around the
648
+ * assignment. The second is what catches `mobxSetup`'s `observe()` bridge,
649
+ * which fires on the assignment itself and would otherwise record an
650
+ * inbound delta as a local edit — queueing an outbound mutation that echoes
651
+ * forever, and putting a collaborator's change on your undo stack. Do not
652
+ * remove it while that bridge exists (`mobxSetup.M1`).
653
+ */
654
+ updateFromData(data: ModelData): void {
655
+ if (this.isDisposed) {
656
+ throw new AbloValidationError('Cannot update disposed model', {
657
+ code: 'model_disposed',
658
+ });
659
+ }
660
+
661
+ runInAction(() => {
662
+ const originalTracking = this.modifiedProperties;
663
+ this.modifiedProperties = new Map();
664
+
665
+ // No `onWrite` → this call records nothing itself.
666
+ this.assignFieldsFromData(data);
667
+
668
+ this.modifiedProperties = originalTracking;
669
+ });
670
+
671
+ // Mark as persisted if updating existing model
672
+ if (!this._isNew) {
673
+ this._originalData = this.captureSnapshot();
674
+ }
675
+
676
+ this.didUpdate();
677
+ }
678
+
679
+ /**
680
+ * Apply a local, user-initiated update from a data object — the write
681
+ * path for `proxy.update({ id, data })`, which is the one and only way
682
+ * application code mutates synced fields.
683
+ *
684
+ * Unlike `updateFromData` (hydration, untracked), this records every
685
+ * written field in `modifiedProperties` via `propertyChanged`, so
686
+ * `getChanges()` and the transaction queue send the edited fields to the
687
+ * server and the undo system gets a correct pre-write baseline. Recording
688
+ * is explicit here, through the `onWrite` hook, and does not rely on any
689
+ * MobX `observe()` side channel.
690
+ *
691
+ * `_originalData` is intentionally not reset here: it stays as the
692
+ * last-persisted baseline until `clearChanges()` runs on sync-ack.
693
+ */
694
+ applyChanges(data: ModelData): void {
695
+ if (this.isDisposed) {
696
+ throw new AbloValidationError('Cannot update disposed model', {
697
+ code: 'model_disposed',
698
+ });
699
+ }
700
+
701
+ runInAction(() => {
702
+ this.assignFieldsFromData(data, (key, oldValue, newValue) => {
703
+ this.propertyChanged(key, oldValue, newValue);
704
+ });
705
+ });
706
+
707
+ this.didUpdate();
708
+ }
709
+
710
+ /**
711
+ * Serialize to JSON
712
+ * This method should not trigger MobX reactions since it's used for serialization
713
+ * Returns Record<string, any> to allow subclass specialization with more specific return types
714
+ */
715
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
716
+ toJSON(): Record<string, any> {
717
+ const modelName = this.getModelName();
718
+ const properties = getActiveRegistry().getProperties(modelName);
719
+ const result: ModelData = {
720
+ __class: this.getModelName(), // the registered model name for consistency
721
+ __typename: this.getModelName(), // __typename mirrors __class as the wire type discriminator
722
+ id: this.id,
723
+ createdAt: this.createdAt?.toISOString(),
724
+ updatedAt: this.updatedAt?.toISOString(),
725
+ clientId: this.clientId,
726
+ syncStatus: this.syncStatus,
727
+ };
728
+
729
+ if (this.archivedAt !== undefined) {
730
+ result.archivedAt = this.archivedAt?.toISOString() ?? null;
731
+ }
732
+
733
+ if (properties) {
734
+ const self = this as Record<string, unknown>;
735
+ for (const [propName, metadata] of properties) {
736
+ // Skip certain types
737
+ if (metadata.type === 'ephemeralProperty') continue;
738
+ if (metadata.type === 'referenceModel') continue;
739
+ if (metadata.type === 'referenceCollection') continue;
740
+
741
+ const value = self[propName];
742
+ if (value !== undefined) {
743
+ result[propName] = value;
744
+ }
745
+ }
746
+ }
747
+
748
+ return result;
749
+ }
750
+
751
+ /**
752
+ * Clone this model
753
+ */
754
+ clone(): this {
755
+ const Constructor = this.constructor as new () => this;
756
+ const clone = new Constructor();
757
+
758
+ const data = this.toJSON();
759
+ delete data.id; // New ID for clone
760
+ delete data.createdAt;
761
+ delete data.updatedAt;
762
+
763
+ clone.updateFromData(data);
764
+ return clone;
765
+ }
766
+
767
+ getModelName(): string {
768
+ const registeredName = getActiveRegistry().getModelNameFromConstructor(this.constructor);
769
+ if (registeredName) {
770
+ return registeredName;
771
+ }
772
+
773
+ const className = this.constructor.name;
774
+ // Use consumer-provided fallback map from config (replaces hardcoded Prisma name map)
775
+ const fallbackMap = getContext().config.classNameFallbackMap;
776
+ return fallbackMap[className] ?? className.replace(/Model$/, '');
777
+ }
778
+
779
+ /**
780
+ * Read a field value by name. Runtime-safe dynamic field access —
781
+ * schema-generated models store all declared fields as instance properties.
782
+ * Use this for generic code (sort comparators, filter predicates that work
783
+ * across model types) that reads fields by name string.
784
+ */
785
+ getField(name: string): unknown {
786
+ return Reflect.get(this, name);
787
+ }
788
+
789
+ /**
790
+ * Check equality
791
+ */
792
+ equals(other: Model): boolean {
793
+ return this.id === other.id && this.constructor === other.constructor;
794
+ }
795
+
796
+ /**
797
+ * String representation
798
+ */
799
+ toString(): string {
800
+ return `${this.constructor.name}[${this.id}]`;
801
+ }
802
+
803
+ // ==========================================
804
+ // MobX Observation Tracking (for GC prevention)
805
+ // ==========================================
806
+
807
+ /**
808
+ * Register a LazyReferenceCollection as being observed
809
+ * Called by LazyReferenceCollection when onBecomeObserved fires
810
+ */
811
+ _registerObservedCollection(collection: Disposable): void {
812
+ this._observedCollections.add(collection);
813
+ }
814
+
815
+ /**
816
+ * Unregister a LazyReferenceCollection that's no longer observed
817
+ * Called by LazyReferenceCollection when onBecomeUnobserved fires
818
+ */
819
+ _unregisterObservedCollection(collection: Disposable): void {
820
+ this._observedCollections.delete(collection);
821
+ }
822
+
823
+ /**
824
+ * Check if any collection on this model is currently being observed by React
825
+ * Used by InstanceCache GC to prevent disposing models in active use
826
+ */
827
+ hasObservedCollections(): boolean {
828
+ return this._observedCollections.size > 0;
829
+ }
830
+
831
+ /**
832
+ * Get count of observed collections (for debugging)
833
+ */
834
+ get observedCollectionCount(): number {
835
+ return this._observedCollections.size;
836
+ }
837
+
838
+ /**
839
+ * Dispose model
840
+ */
841
+ dispose(): void {
842
+ if (this.isDisposed) return;
843
+
844
+ // Clean up
845
+ for (const disposer of this.disposers) {
846
+ disposer();
847
+ }
848
+ this.disposers = [];
849
+
850
+ this._referencedModels = {};
851
+ this.modifiedProperties.clear();
852
+ this._observedCollections.clear();
853
+
854
+ // Dispose collections. Gracefully skip when no active registry
855
+ // exists — `dispose()` is a cleanup path and must not crash when a
856
+ // test (or a teardown during engine shutdown) calls it after the
857
+ // registry is gone. Production flows always have one set, so the
858
+ // collection-disposal branch still runs there.
859
+ if (hasActiveRegistry()) {
860
+ const modelName = this.getModelName();
861
+ const properties = getActiveRegistry().getProperties(modelName);
862
+
863
+ if (properties) {
864
+ const self = this as Record<string, unknown>;
865
+ for (const [propName, metadata] of properties) {
866
+ if (metadata.type === 'referenceCollection') {
867
+ const collection = self[propName] as Disposable | undefined;
868
+ if (collection?.dispose) {
869
+ collection.dispose();
870
+ }
871
+ }
872
+ }
873
+ }
874
+ }
875
+
876
+ this.isDisposed = true;
877
+ }
878
+
879
+ /**
880
+ * Check if disposed
881
+ */
882
+ get disposed(): boolean {
883
+ return this.isDisposed;
884
+ }
885
+
886
+ /**
887
+ * Lifecycle hooks - override in subclasses
888
+ */
889
+ protected didUpdate(): void {}
890
+ protected willDelete(): void {}
891
+
892
+ /**
893
+ * Capture snapshot for change detection
894
+ */
895
+ protected captureSnapshot(): ModelData {
896
+ const snapshot: ModelData = {};
897
+ const modelName = this.getModelName();
898
+ const properties = getActiveRegistry().getProperties(modelName);
899
+
900
+ if (properties) {
901
+ const json = this.toJSON();
902
+ for (const [propName] of properties) {
903
+ snapshot[propName] = json[propName];
904
+ }
905
+ }
906
+
907
+ return snapshot;
908
+ }
909
+
910
+ /**
911
+ * A fresh, plain row snapshot that READS every declared field through its
912
+ * MobX observable getter. This is the reactive read surface for
913
+ * `useReactive`/`useAblo`, and it relies on two MobX facts:
914
+ *
915
+ * 1. MobX tracks property *access*, not values — so reading each field here,
916
+ * inside the caller's tracked function, SUBSCRIBES the reaction to that
917
+ * field. A bare `return model` (the previous `modelAsRow`) dereferences
918
+ * nothing, so an in-place delta update fires no reaction and the UI never
919
+ * re-renders. (https://mobx.js.org/understanding-reactivity.html)
920
+ * 2. The returned object is a NEW identity each call, so the hook's equality
921
+ * check detects the change — the same mutated instance would compare equal
922
+ * and suppress the re-render even after tracking fired.
923
+ *
924
+ * Unlike `toJSON()`, values keep their runtime types (a `Date` stays a `Date`,
925
+ * a json field stays its parsed object) and wire-noise keys
926
+ * (`__class`/`clientId`/`syncStatus`) are omitted — this is exactly the row
927
+ * the schema's `T` describes. Computed relations (`referenceModel`/
928
+ * `referenceCollection`) and ephemeral fields are skipped, matching `toJSON`'s
929
+ * row projection; they're lazy/recursive and not part of the row's data.
930
+ *
931
+ * Schema-derived getters (`computed:` entries and `${field}Json` getters) ARE
932
+ * materialized, as non-enumerable own values. The schema's inferred row type
933
+ * includes them, so omitting them would make every snapshot read of a computed
934
+ * silently `undefined` — a type-level lie. They're evaluated here, inside the
935
+ * caller's tracked function, so the reaction subscribes to whatever fields the
936
+ * getter reads. Non-enumerable keeps write-path parity with model instances:
937
+ * an instance's getters sit on the prototype and never enter `{...model}`
938
+ * spreads or `JSON.stringify`, and materialized values must not either — a
939
+ * spread-into-update would otherwise send computed keys to the server.
940
+ */
941
+ toReactiveSnapshot<T = ModelData>(): T {
942
+ const snapshot: ModelData = {
943
+ id: this.id,
944
+ createdAt: this.createdAt,
945
+ updatedAt: this.updatedAt,
946
+ };
947
+ if (this.archivedAt !== undefined) snapshot.archivedAt = this.archivedAt;
948
+
949
+ const properties = getActiveRegistry().getProperties(this.getModelName());
950
+ const self = this as Record<string, unknown>;
951
+ if (properties) {
952
+ for (const [propName, metadata] of properties) {
953
+ if (
954
+ metadata.type === 'ephemeralProperty' ||
955
+ metadata.type === 'referenceModel' ||
956
+ metadata.type === 'referenceCollection'
957
+ ) {
958
+ continue;
959
+ }
960
+ // Reading through the observable getter is the point: it subscribes the
961
+ // enclosing MobX reaction to this field.
962
+ snapshot[propName] = self[propName];
963
+ }
964
+ }
965
+
966
+ for (const name of this.getDerivedGetterNames()) {
967
+ Object.defineProperty(snapshot, name, {
968
+ // Evaluated on the model instance so `${field}Json` caches stay on it
969
+ // and the enclosing reaction tracks the fields the getter reads.
970
+ value: self[name],
971
+ enumerable: false,
972
+ configurable: true,
973
+ writable: false,
974
+ });
975
+ }
976
+
977
+ return snapshot as T;
978
+ }
979
+
980
+ /**
981
+ * Names of schema-derived getters — `computed:` entries and `${field}Json`
982
+ * getters — that {@link toReactiveSnapshot} materializes onto snapshots.
983
+ * The dynamic model class built by `registerModelsFromSchema` overrides this;
984
+ * hand-written Model subclasses default to none.
985
+ */
986
+ getDerivedGetterNames(): readonly string[] {
987
+ return EMPTY_DERIVED_GETTERS;
988
+ }
989
+
990
+ /**
991
+ * Get field changes for activity tracking
992
+ */
993
+ getFieldChanges(): FieldChange[] {
994
+ const changes: FieldChange[] = [];
995
+
996
+ for (const [field, change] of this.modifiedProperties) {
997
+ changes.push({
998
+ field,
999
+ oldValue: change.old,
1000
+ newValue: change.new,
1001
+ fieldType: this.getFieldType(change.new),
1002
+ });
1003
+ }
1004
+
1005
+ return changes;
1006
+ }
1007
+
1008
+ private getFieldType(value: unknown): string {
1009
+ if (value === null || value === undefined) return 'string';
1010
+ if (typeof value === 'number') return 'number';
1011
+ if (value instanceof Date) return 'date';
1012
+ if (Array.isArray(value)) return 'array';
1013
+ if (typeof value === 'string' && /^[a-fA-F0-9-]{36}$/.test(value)) return 'reference';
1014
+ return 'string';
1015
+ }
1016
+
1017
+ /**
1018
+ * Create model from JSON
1019
+ */
1020
+ static fromJSON(
1021
+ data: ModelData & { __typename?: string; __class?: string; modelName?: string }
1022
+ ): Model {
1023
+ // Support both __class and __typename, and handle both old and new naming
1024
+ const modelIdentifier = data.__typename ?? data.__class ?? data.modelName;
1025
+
1026
+ if (!modelIdentifier) {
1027
+ throw new AbloValidationError(
1028
+ 'Model identifier (__typename, __class, or modelName) not found in data',
1029
+ { code: 'model_identifier_missing' },
1030
+ );
1031
+ }
1032
+
1033
+ // Try to get model class by identifier
1034
+ let ModelClass = getActiveRegistry().getModelByName(modelIdentifier);
1035
+
1036
+ // If not found by registered name, try mapping to the class name
1037
+ if (!ModelClass) {
1038
+ const classNameMap: Record<string, string> = {
1039
+ Task: 'TaskModel',
1040
+ Project: 'Project',
1041
+ Comment: 'CommentModel',
1042
+ User: 'UserModel',
1043
+ Organization: 'OrganizationModel',
1044
+ StatusGroup: 'StatusGroupModel',
1045
+ Team: 'TeamModel',
1046
+ Member: 'MemberModel',
1047
+ Role: 'RoleModel',
1048
+ };
1049
+
1050
+ const className = classNameMap[modelIdentifier];
1051
+ if (className) {
1052
+ ModelClass = getActiveRegistry().getModelByName(className);
1053
+ }
1054
+ }
1055
+
1056
+ if (!ModelClass) {
1057
+ throw new AbloValidationError(
1058
+ `Model class not found for: ${modelIdentifier}`,
1059
+ { code: 'model_class_not_registered' },
1060
+ );
1061
+ }
1062
+
1063
+ const instance = new ModelClass(data);
1064
+ instance.markAsPersisted();
1065
+ return instance;
1066
+ }
1067
+
1068
+ /**
1069
+ * Get sync status
1070
+ */
1071
+ getSyncStatus(): 'pending' | 'syncing' | 'synced' {
1072
+ return this.syncStatus;
1073
+ }
1074
+
1075
+ /**
1076
+ * Mark model as synced
1077
+ */
1078
+ markAsSynced(): void {
1079
+ this.syncStatus = 'synced';
1080
+ }
1081
+
1082
+ /**
1083
+ * Mark model as pending sync
1084
+ */
1085
+ markAsPending(): void {
1086
+ this.syncStatus = 'pending';
1087
+ }
1088
+ }
1089
+
1090
+ /**
1091
+ * Project a dynamic-class `Model` instance to the schema row shape `T`.
1092
+ *
1093
+ * The runtime invariant: `createDynamicModelClass(...)` attaches every
1094
+ * field of `T` directly onto the Model prototype/instance via
1095
+ * `Object.defineProperty` and the M1 observable bridge, so a Model
1096
+ * instance structurally satisfies `T` at runtime. The static type
1097
+ * system can't see this because `T` is a free generic — there's no
1098
+ * common ancestor between `Model` (base class) and the schema row
1099
+ * interface produced by `defineSchema`.
1100
+ *
1101
+ * This is a typed boundary, not a bypass: every call site is the
1102
+ * dynamic-class duality where Model-with-extras-and-T-fields is being
1103
+ * returned to a consumer that only sees `T`. Concentrating the cast
1104
+ * here means there's one place to look when the boundary changes.
1105
+ */
1106
+ export function modelAsRow<T>(model: Model): T {
1107
+ return model as Model & T;
1108
+ }
1109
+
1110
+ /**
1111
+ * Inverse of `modelAsRow`: accept a row-shaped value (schema-derived
1112
+ * `T` with at minimum an `id`) and surface it as a `Model`. Used by
1113
+ * `BaseSyncedStore.save / delete / archive / unarchive` so consumers
1114
+ * can pass either a typed schema row or a Model instance and the
1115
+ * SDK's persistence path sees a uniform Model surface.
1116
+ *
1117
+ * Same runtime invariant as `modelAsRow`: dynamic-class instances
1118
+ * carry both the row fields and the Model methods on the same object
1119
+ * — one structural identity, two static views. The helper does no
1120
+ * runtime conversion (no allocation, no copy) — it's a pure type cast.
1121
+ */
1122
+ export function rowAsModel<T extends { id: string }>(entity: T): Model {
1123
+ return entity as T & Model;
1124
+ }