@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,927 @@
1
+ /**
2
+ * Loads model rows on demand — the lazy-load path of the sync engine. When
3
+ * something needs an entity that the initial bootstrap did not fetch,
4
+ * {@link OnDemandLoader.fetch | fetch} finds it and populates the
5
+ * in-memory {@link InstanceCache} so the rest of the engine can read it normally.
6
+ *
7
+ * A fetch resolves against three tiers in order, stopping at the first that can
8
+ * answer:
9
+ * 1. The object pool — if rows already in memory match the query, return them.
10
+ * 2. Local storage — if matching rows exist there, hydrate the pool and return.
11
+ * 3. The network — post the query to `/sync/query`, then hydrate both the pool
12
+ * and local storage.
13
+ *
14
+ * Concurrent calls with the same query key share one in-flight promise, so a
15
+ * burst of components mounting and asking for the same data on first paint
16
+ * triggers a single fetch rather than one each.
17
+ *
18
+ * The coordinator does not replace the bootstrap (which fully syncs instantly
19
+ * loaded models) or the live delta stream (pushed over the WebSocket). It only
20
+ * fills the gap for lazily loaded models read by id or filter after the engine
21
+ * is ready.
22
+ */
23
+
24
+ import type { InstanceCache } from '../InstanceCache.js';
25
+ import { ModelScope } from '../InstanceCache.js';
26
+ import { AbloValidationError } from '@abloatai/transaction/errors';
27
+ import type { Database } from '../Database.js';
28
+ import type { Model } from '../Model.js';
29
+ import type { ModelRegistry, RegisteredModelClass } from '../ModelRegistry.js';
30
+ import type { RuntimeContext } from '../RuntimeContext.js';
31
+ import { postQuery } from '../query/client.js';
32
+ import type { RecoveryClass } from '@abloatai/transaction/errorCodes';
33
+ import type { LoadWhere, Query, WhereClause, WhereOp, WherePrimitive } from '../query/types.js';
34
+ import type { Schema } from '@abloatai/transaction/schema/schema';
35
+
36
+ export interface OnDemandLoaderOptions {
37
+ readonly objectPool: InstanceCache;
38
+ readonly database: Database;
39
+ readonly registry: ModelRegistry;
40
+ readonly schema: Schema;
41
+ /** Bootstrap base URL (without trailing slash), e.g. `https://api.example.com/api`. */
42
+ readonly baseUrl: string;
43
+ /**
44
+ * Lazy getter for the active bearer token. Resolved per request so refreshes
45
+ * propagate without re-instantiating the coordinator.
46
+ */
47
+ readonly getAuthToken?: () => string | null;
48
+ /** @deprecated Use `getAuthToken`. */
49
+ readonly getCapabilityToken?: () => string | null;
50
+ /** The owning client's runtime. Defaults to the module-global bridge. */
51
+ readonly runtime?: RuntimeContext;
52
+ }
53
+
54
+ export interface FetchOptions<T> {
55
+ /**
56
+ * Filter clauses for the lookup. Accepts either the equality-object form
57
+ * (`{ id: 'abc' }` becomes `WHERE id = 'abc'`, and an array value becomes an
58
+ * `IN`) or the explicit tuple form (`[['name', 'ILIKE', '%Acme%']]`), which
59
+ * mirrors the wire `WhereClause[]` exactly. Multiple entries combine with AND.
60
+ * See {@link LoadWhere} for the full shape.
61
+ */
62
+ readonly where?: LoadWhere<T>;
63
+ readonly orderBy?: { [K in keyof T]?: 'asc' | 'desc' };
64
+ readonly limit?: number;
65
+ /**
66
+ * Freshness mode. When omitted, the default is derived from the model's
67
+ * load strategy: `lazy` models default to `'unknown'` (local-first), while
68
+ * `instant`/`partial` models default to `'complete'`.
69
+ *
70
+ * `'complete'`: wait for the network round-trip even if local data exists,
71
+ * so the caller observes server-confirmed state (read-after-write).
72
+ * `'unknown'`: return whatever's in the pool/IDB immediately and fire the
73
+ * network refresh in the background (stale-while-revalidate).
74
+ */
75
+ readonly type?: 'complete' | 'unknown';
76
+ /**
77
+ * Schema-declared relation names to hydrate alongside the primary
78
+ * rows. Each related entity is hydrated into its own typed pool
79
+ * via the same path as the primary fetch (network → pool + IDB).
80
+ */
81
+ readonly expand?: readonly string[];
82
+ }
83
+
84
+
85
+ /**
86
+ * The slice of a schema model definition the coordinator reads: the wire
87
+ * typename and the relation map (relation `type`/`target`/`foreignKey`).
88
+ * Mirrors the runtime shape produced by `defineSchema` without pulling in the
89
+ * full builder generics.
90
+ */
91
+ interface SchemaModelDef {
92
+ readonly typename?: string;
93
+ readonly relations?: Record<
94
+ string,
95
+ { readonly type?: string; readonly target?: string; readonly foreignKey?: string }
96
+ >;
97
+ }
98
+
99
+ function timestampMs(value: unknown): number | undefined {
100
+ if (value instanceof Date) {
101
+ const timestamp = value.getTime();
102
+ return Number.isFinite(timestamp) ? timestamp : undefined;
103
+ }
104
+ if (typeof value === 'number' && Number.isFinite(value)) return value;
105
+ if (typeof value !== 'string') return undefined;
106
+ const parsed = Date.parse(value);
107
+ return Number.isNaN(parsed) ? undefined : parsed;
108
+ }
109
+
110
+ /**
111
+ * A query response is a snapshot, not an ordered delta. It may have started
112
+ * before a local optimistic write and completed after it. Only treat the row
113
+ * as authoritative over an existing model when its server timestamp is newer;
114
+ * missing timestamps retain the legacy merge behavior for custom sources that
115
+ * do not expose `updatedAt`.
116
+ */
117
+ function snapshotDoesNotAdvanceModel(data: Record<string, unknown>, model: Model): boolean {
118
+ const incoming = timestampMs(data.updatedAt);
119
+ const resident = timestampMs(model.updatedAt);
120
+ return incoming !== undefined && resident !== undefined && incoming <= resident;
121
+ }
122
+
123
+ export class OnDemandLoader {
124
+ private readonly inFlight = new Map<string, Promise<Model[]>>();
125
+ /**
126
+ * Query keys with a background confirm currently in flight. Distinct from
127
+ * {@link inFlight} (which dedupes *blocking* callers awaiting the same
128
+ * fetch): this set dedupes the fire-and-forget network confirm kicked off
129
+ * after a local-first read returns cached data, so a burst of mounts that
130
+ * all hit the warm pool/IDB don't each spawn their own redundant fetch.
131
+ */
132
+ private readonly revalidating = new Set<string>();
133
+ /**
134
+ * Query keys that have been satisfied from the server at least once this
135
+ * session. Once a key is here, repeat reads serve purely from the pool with
136
+ * no network round-trip: the WebSocket delta stream keeps those pool rows
137
+ * fresh, so re-running the HTTP query would be redundant polling. This ledger
138
+ * is what stops an already-open view from re-querying on every navigation.
139
+ *
140
+ * Cleared on reconnect (see {@link invalidate}) so that, after a connection
141
+ * drop where deltas may have been missed, the next read re-confirms once.
142
+ */
143
+ private readonly hydratedKeys = new Set<string>();
144
+ private authTokenProvider: (() => string | null) | null = null;
145
+ /**
146
+ * The credential-recovery hook (the store's `recoverFromAuthRejection`),
147
+ * late-bound like {@link setAuthTokenProvider} because the store does not
148
+ * exist yet when the coordinator is constructed. Handed to `postQuery` so a
149
+ * 401 on the lazy lane re-mints through the same single-flight path the
150
+ * WebSocket probe uses, then replays the query once — instead of silently
151
+ * returning empty rows against an expired key.
152
+ */
153
+ private credentialRecovery:
154
+ | ((recovery: RecoveryClass) => Promise<'retry' | 'stop'>)
155
+ | null = null;
156
+
157
+ constructor(private readonly opts: OnDemandLoaderOptions) {
158
+ // Reading the deprecated `getCapabilityToken` is deliberate: it's the
159
+ // back-compat shim that keeps older callers who still pass it working
160
+ // until they migrate to `getAuthToken`.
161
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
162
+ this.authTokenProvider = opts.getAuthToken ?? opts.getCapabilityToken ?? null;
163
+ }
164
+
165
+ /**
166
+ * Late-bind the auth token getter. Browser cookie consumers can omit this;
167
+ * bearer consumers need it so lazy HTTP queries use the same credential as
168
+ * bootstrap and the WebSocket.
169
+ */
170
+ setAuthTokenProvider(provider: () => string | null): void {
171
+ this.authTokenProvider = provider;
172
+ }
173
+
174
+ /** Late-bind the auth-recovery backbone. See {@link credentialRecovery}. */
175
+ setCredentialRecovery(
176
+ recover: (recovery: RecoveryClass) => Promise<'retry' | 'stop'>,
177
+ ): void {
178
+ this.credentialRecovery = recover;
179
+ }
180
+
181
+ /** @deprecated Use `setAuthTokenProvider`. */
182
+ setCapabilityTokenProvider(provider: () => string | null): void {
183
+ this.setAuthTokenProvider(provider);
184
+ }
185
+
186
+ /**
187
+ * Fetch matching rows for a model, hydrating the pool from IDB or
188
+ * network if not already present. Idempotent and single-flight
189
+ * deduped on the (modelName, where, orderBy, limit) tuple.
190
+ */
191
+ async fetch<T>(
192
+ modelName: string,
193
+ options?: FetchOptions<T>,
194
+ ): Promise<Model[]> {
195
+ const typename = this.resolveTypename(modelName);
196
+ const ModelClass = this.opts.registry.getModelByName(typename)
197
+ ?? this.opts.registry.getModelByName(modelName);
198
+ if (!ModelClass) {
199
+ throw new AbloValidationError(
200
+ `OnDemandLoader.fetch: unknown model "${modelName}" — ` +
201
+ `not registered in the schema.`,
202
+ { code: 'model_not_registered' },
203
+ );
204
+ }
205
+
206
+ const clauses = normalizeWhere(options?.where);
207
+ const queryKey = stableKey(modelName, clauses, options?.orderBy, options?.limit, options?.expand);
208
+
209
+ // Single-flight: an identical hydration is already in flight.
210
+ const inFlight = this.inFlight.get(queryKey);
211
+ if (inFlight) return inFlight;
212
+
213
+ const work = this.runFetch(modelName, typename, ModelClass, clauses, options, queryKey);
214
+ this.inFlight.set(queryKey, work);
215
+ // The rejection (if any) reaches callers via the returned `work`; this
216
+ // side-chain only clears the single-flight slot. Without the trailing
217
+ // catch, `.finally()` mirrors the rejection into a second, unhandled
218
+ // promise even when every caller handles theirs.
219
+ void work
220
+ .finally(() => {
221
+ this.inFlight.delete(queryKey);
222
+ })
223
+ .catch(() => undefined);
224
+ return work;
225
+ }
226
+
227
+ private async runFetch(
228
+ modelName: string,
229
+ typename: string,
230
+ ModelClass: RegisteredModelClass,
231
+ clauses: readonly WhereClause[],
232
+ options: FetchOptions<unknown> | undefined,
233
+ queryKey: string,
234
+ ): Promise<Model[]> {
235
+ // `{ type: 'complete' }` is the only way to force a server round-trip:
236
+ // read-after-write certainty. Every other read is local-first.
237
+ const explicitComplete = options?.type === 'complete';
238
+ const expand = options?.expand;
239
+ const hasExpand = !!(expand && expand.length > 0);
240
+
241
+ // Fast path — this exact query was already satisfied from the server this
242
+ // session. The WebSocket delta stream has kept the pool fresh since, so a
243
+ // repeat read needs no network: serve straight from local. This is what
244
+ // stops an already-open view from re-querying on every navigation when no
245
+ // new deltas have arrived.
246
+ if (!explicitComplete && this.hydratedKeys.has(queryKey)) {
247
+ return applyLimit(
248
+ await this.readLocal(modelName, typename, ModelClass, clauses, hasExpand, expand),
249
+ options?.limit,
250
+ );
251
+ }
252
+
253
+ // Not yet hydrated (or an explicit complete read). For a non-complete read
254
+ // without expand, if there is anything local to show (a warm pool, or local
255
+ // storage after a reload), hand it back immediately and confirm with the
256
+ // server once in the background — then mark the key hydrated so subsequent
257
+ // reads are purely local. First paint never blocks on the network.
258
+ //
259
+ // Expand queries are deliberately excluded here: the presence of a primary
260
+ // row says nothing about whether its relations are loaded. Returning the
261
+ // parent now would surface it with empty children, letting a readiness flag
262
+ // flip before the children exist. So an un-hydrated expand query falls
263
+ // through to the blocking fetch that brings parent and children together;
264
+ // the second open is served by the fast path.
265
+ if (!explicitComplete && !hasExpand) {
266
+ const local = await this.readLocal(modelName, typename, ModelClass, clauses, hasExpand, expand);
267
+ if (local.length > 0) {
268
+ this.scheduleHydratingFetch(queryKey, modelName, typename, clauses, options);
269
+ return applyLimit(local, options?.limit);
270
+ }
271
+ }
272
+
273
+ // Cold cache, or caller demanded server-confirmed state: block on the
274
+ // network, then mark this query hydrated so future reads serve local.
275
+ const networkModels = await this.fetchFromNetwork(modelName, typename, clauses, options);
276
+ this.hydratedKeys.add(queryKey);
277
+ if (networkModels.length > 0) return applyLimit(networkModels, options?.limit);
278
+
279
+ // Network returned nothing — fall back to whatever's local (e.g. a
280
+ // complete read whose server result was empty but IDB still holds rows).
281
+ return applyLimit(
282
+ await this.readLocal(modelName, typename, ModelClass, clauses, hasExpand, expand),
283
+ options?.limit,
284
+ );
285
+ }
286
+
287
+ /**
288
+ * Read a query's rows from local storage only — pool first, then IndexedDB
289
+ * on a pool miss (cold start after reload, or LRU eviction), hydrating the
290
+ * pool from IDB as a side effect. Resolves requested `expand` relations from
291
+ * their own local stores too. Never touches the network.
292
+ */
293
+ private async readLocal(
294
+ modelName: string,
295
+ typename: string,
296
+ ModelClass: RegisteredModelClass,
297
+ clauses: readonly WhereClause[],
298
+ hasExpand: boolean,
299
+ expand: readonly string[] | undefined,
300
+ ): Promise<Model[]> {
301
+ let local = scanPool(this.opts.objectPool, ModelClass, clauses);
302
+ if (local.length === 0) {
303
+ const fromIdb = await scanIdb(this.opts.database, typename, clauses);
304
+ const idbModels = fromIdb
305
+ .map((raw) => this.hydrateOne(raw, typename))
306
+ .filter((m): m is Model => m !== null);
307
+ if (idbModels.length > 0) {
308
+ this.opts.objectPool.addBatch(idbModels, ModelScope.live);
309
+ local = idbModels;
310
+ }
311
+ }
312
+ if (hasExpand && expand && local.length > 0) {
313
+ await this.hydrateExpandedFromLocal(modelName, local.map((m) => m.id), expand);
314
+ }
315
+ return local;
316
+ }
317
+
318
+ /**
319
+ * Drop the hydration ledger so the next read of each query re-confirms with
320
+ * the server. Called on reconnect — after a connection drop, deltas may have
321
+ * been missed, so the "WS keeps the pool fresh" assumption no longer holds
322
+ * until a fresh fetch (or the engine's delta catch-up) reconciles.
323
+ */
324
+ invalidate(): void {
325
+ this.hydratedKeys.clear();
326
+ }
327
+
328
+ /**
329
+ * Run the network leg of a fetch: query the server, hydrate primary rows
330
+ * (and any expanded relations) into the pool, and persist them to IDB.
331
+ * Shared by the blocking path (`runFetch` step 3) and the background
332
+ * revalidation kicked off after an `'unknown'` local hit.
333
+ */
334
+ private async fetchFromNetwork(
335
+ modelName: string,
336
+ typename: string,
337
+ clauses: readonly WhereClause[],
338
+ options: FetchOptions<unknown> | undefined,
339
+ ): Promise<Model[]> {
340
+ const networkRows = await this.queryNetwork(modelName, clauses, options);
341
+ const networkModels = networkRows
342
+ // Strict: a row the server returned whose type name this client never
343
+ // registered is a genuine schema collision (the pushed schema differs
344
+ // from the local one). Throw here, naming the cause, rather than silently
345
+ // dropping the row and failing downstream as `entity_not_found`.
346
+ .map((raw) => this.hydrateOne(raw, typename, { strict: true }))
347
+ .filter((m): m is Model => m !== null);
348
+
349
+ if (networkModels.length > 0) {
350
+ this.opts.objectPool.addBatch(networkModels, ModelScope.live);
351
+ // Background IDB write — don't block the caller. Expanded children are
352
+ // persisted to their own stores inside `queryNetwork`/`hydrateExpanded`.
353
+ void this.persistToIdb(modelName, networkRows);
354
+ }
355
+
356
+ return networkModels;
357
+ }
358
+
359
+ /**
360
+ * Fires the single background confirm for a query that was just served from
361
+ * local cache but is not hydrated yet. On success the key is marked hydrated,
362
+ * so every later read serves purely from local with no network until a
363
+ * reconnect invalidates the ledger. Deduped per query key so a render burst
364
+ * does not stampede. Errors are swallowed — the caller already has a usable
365
+ * local snapshot, and a failed confirm leaves the key un-hydrated so the next
366
+ * read simply tries again.
367
+ */
368
+ private scheduleHydratingFetch(
369
+ queryKey: string,
370
+ modelName: string,
371
+ typename: string,
372
+ clauses: readonly WhereClause[],
373
+ options: FetchOptions<unknown> | undefined,
374
+ ): void {
375
+ if (this.revalidating.has(queryKey)) return;
376
+ this.revalidating.add(queryKey);
377
+ void this.fetchFromNetwork(modelName, typename, clauses, options)
378
+ .then(() => {
379
+ this.hydratedKeys.add(queryKey);
380
+ })
381
+ .catch(() => undefined)
382
+ .finally(() => {
383
+ this.revalidating.delete(queryKey);
384
+ });
385
+ }
386
+
387
+ /**
388
+ * Hydrate a parent's `hasMany`/`hasOne` relations from their OWN local
389
+ * stores (pool first, then IndexedDB by the FK secondary index) into the
390
+ * pool. The mirror of {@link hydrateExpanded} for the local read path:
391
+ * `hydrateExpanded` walks server-JOINed nested rows, this walks the child
392
+ * model's own store keyed by the relation's foreign key.
393
+ *
394
+ * Fully schema-driven via the relation's `target` + `foreignKey` — no
395
+ * per-model special-casing. `belongsTo` relations are skipped: those point
396
+ * at a single parent (the inverse direction), already covered by the
397
+ * primary scan when that parent is itself the fetched model.
398
+ */
399
+ private async hydrateExpandedFromLocal(
400
+ parentModelName: string,
401
+ parentIds: readonly string[],
402
+ relationNames: readonly string[],
403
+ ): Promise<void> {
404
+ if (parentIds.length === 0) return;
405
+ const parentDef = this.getModelDef(parentModelName);
406
+ if (!parentDef?.relations) return;
407
+
408
+ for (const rel of relationNames) {
409
+ const relDef = parentDef.relations[rel];
410
+ if (!relDef) continue;
411
+ if (relDef.type !== 'hasMany' && relDef.type !== 'hasOne') continue;
412
+ const targetKey = relDef.target;
413
+ const foreignKey = relDef.foreignKey;
414
+ if (!targetKey || !foreignKey) continue;
415
+ const targetTypename = this.resolveTypename(targetKey);
416
+
417
+ // Skip parents whose children are already pool-resident (O(1) when the
418
+ // FK is indexed). Falls through to a local read for the rest.
419
+ const missing = parentIds.filter(
420
+ (pid) =>
421
+ this.opts.objectPool.getByForeignKey(targetTypename, foreignKey, pid).length === 0,
422
+ );
423
+ if (missing.length === 0) continue;
424
+
425
+ const rows = await this.readChildrenLocal(targetTypename, foreignKey, missing);
426
+ const models = rows
427
+ .map((raw) => this.hydrateOne(this.stampTypename(raw, targetTypename), targetTypename))
428
+ .filter((m): m is Model => m !== null);
429
+ if (models.length > 0) {
430
+ this.opts.objectPool.addBatch(models, ModelScope.live);
431
+ }
432
+ }
433
+ }
434
+
435
+ /**
436
+ * Read a child model's rows from local storage by foreign key.
437
+ *
438
+ * Uses the FK secondary index (O(matches) per parent) only when the schema
439
+ * declares one — `getAllFromIndex` resolves `[]` for a missing index rather
440
+ * than throwing, so the decision is made up front from the registry, not by
441
+ * catching. Unindexed FKs — and in-memory stores, which carry no secondary
442
+ * indexes at all — fall back to a single full-store scan filtered in JS.
443
+ */
444
+ private async readChildrenLocal(
445
+ childTypename: string,
446
+ foreignKey: string,
447
+ parentIds: readonly string[],
448
+ ): Promise<unknown[]> {
449
+ const store = this.opts.database.getStore(childTypename);
450
+ if (!store) return [];
451
+
452
+ const isIndexed = this.opts.registry.getIndexedProperties(childTypename).includes(foreignKey);
453
+ if (isIndexed) {
454
+ const collected: unknown[] = [];
455
+ for (const pid of parentIds) {
456
+ const rows = await store.getAllFromIndex(foreignKey, pid);
457
+ if (Array.isArray(rows)) collected.push(...rows);
458
+ }
459
+ // A non-empty result means the index is live (browser IDB). Empty can
460
+ // mean "no children" OR "no physical index" (in-memory) — fall through
461
+ // to the scan so the in-memory/SSR path stays correct.
462
+ if (collected.length > 0) return collected;
463
+ }
464
+
465
+ try {
466
+ const all = await store.getAll();
467
+ if (!Array.isArray(all)) return [];
468
+ const idSet = new Set(parentIds);
469
+ return all.filter((r) => idSet.has((r)[foreignKey] as string));
470
+ } catch {
471
+ return [];
472
+ }
473
+ }
474
+
475
+ /** Typed accessor for a model's schema definition (typename + relations). */
476
+ private getModelDef(modelName: string): SchemaModelDef | undefined {
477
+ return (this.opts.schema as { models?: Record<string, SchemaModelDef> }).models?.[modelName];
478
+ }
479
+
480
+ private hydrateOne(
481
+ raw: unknown,
482
+ typename?: string,
483
+ opts?: { strict?: boolean },
484
+ ): Model | null {
485
+ if (!raw || typeof raw !== 'object') return null;
486
+ const obj = raw as Record<string, unknown>;
487
+ if (typeof obj.id !== 'string') return null;
488
+ if (this.opts.objectPool.has(obj.id)) {
489
+ // Keep the existing instance alive when a query refreshes it. A query
490
+ // can carry fresher server state after a missed delta, but unlike the
491
+ // ordered delta stream it can also finish late with an older snapshot;
492
+ // the reconciliation below distinguishes those cases before applying.
493
+ const existing = this.opts.objectPool.get(obj.id);
494
+ if (existing) {
495
+ const stamped = this.stampTypename(obj, typename) as Record<string, unknown>;
496
+ // Network queries are unordered snapshots. A request that began before
497
+ // an optimistic resize can return afterward with the old row; applying
498
+ // it here would visibly snap the live model back, and the matching
499
+ // authoritative delta cannot repair it because own echoes are
500
+ // intentionally suppressed. Keep a newer resident row intact.
501
+ if (snapshotDoesNotAdvanceModel(stamped, existing)) return existing;
502
+
503
+ // If the source has no comparable timestamp, retain pending local
504
+ // fields while accepting unrelated server fields. This is the same
505
+ // local-first merge contract used by SyncClient's delta resolver.
506
+ const localChanges = existing.getChanges();
507
+ existing.updateFromData(
508
+ Object.keys(localChanges).length > 0
509
+ ? { ...stamped, ...localChanges, updatedAt: existing.updatedAt }
510
+ : stamped,
511
+ );
512
+ return existing;
513
+ }
514
+ return null;
515
+ }
516
+ // Stamp the known relation typename onto the row when the source
517
+ // (IndexedDB rows, sometimes network rows) didn't carry one. Without
518
+ // this, InstanceCache.createFromData falls through to the 'Unknown'
519
+ // model-name branch and emits the
520
+ // "InstanceCache.createFromData: No model identifier found" warning,
521
+ // failing to hydrate the entity from cache (network path then has to
522
+ // re-populate it). The typename comes from the schema relation
523
+ // (`'Block'`, `'Section'`, etc.) so no guessing involved.
524
+ const stamped = this.stampTypename(obj, typename) as Record<string, unknown>;
525
+ return this.opts.objectPool.createFromData(stamped, undefined, opts);
526
+ }
527
+
528
+ /**
529
+ * Stamp `__typename` onto a row when it's known (from the schema's
530
+ * relation target). Strips the mangled `_Typename` key the
531
+ * `postgres.camel` driver leaves behind when the server's SQL
532
+ * bakes `__typename` into a JSONB literal — the driver's
533
+ * snake↔camel transform misreads `__typename` as `_typename` with
534
+ * a leading underscore and produces `_Typename`. InstanceCache only
535
+ * recognises `__typename`, so without this step nested rows fall
536
+ * through to the 'Unknown' branch and never instantiate.
537
+ */
538
+ private stampTypename(item: unknown, typename: string | undefined): unknown {
539
+ if (!item || typeof item !== 'object' || !typename) return item;
540
+ const obj = item as Record<string, unknown>;
541
+ if (obj.__typename === typename) return obj;
542
+ // Drop the driver-mangled `_Typename` AND any row-carried `__typename`
543
+ // that disagrees with the schema's: these rows were returned FOR this
544
+ // model's query, so the schema typename is correct by construction — and
545
+ // without stripping it, the spread would put the row's variant (a server
546
+ // echoing the schema KEY `tasks` instead of the typename `Task`) back on
547
+ // top of the stamp, sending hydration to the strict unknown-model error.
548
+ const { _Typename: _dropMangled, __typename: _dropRowVariant, ...rest } = obj as Record<
549
+ string,
550
+ unknown
551
+ > & { _Typename?: unknown; __typename?: unknown };
552
+ void _dropMangled;
553
+ void _dropRowVariant;
554
+ return { __typename: typename, ...rest };
555
+ }
556
+
557
+ private async queryNetwork(
558
+ modelName: string,
559
+ clauses: readonly WhereClause[],
560
+ options: FetchOptions<unknown> | undefined,
561
+ ): Promise<unknown[]> {
562
+ const typename = this.resolveTypename(modelName);
563
+ const orderEntries = options?.orderBy ? Object.entries(options.orderBy) : [];
564
+ const firstOrder = orderEntries[0];
565
+ const query: Query = {
566
+ model: typename,
567
+ where: clauses.map((c) => this.columnizeClause(modelName, c)),
568
+ ...(firstOrder
569
+ ? {
570
+ orderBy: this.columnizeField(modelName, firstOrder[0]),
571
+ order: (firstOrder[1] as 'asc' | 'desc' | undefined) ?? 'asc',
572
+ }
573
+ : {}),
574
+ ...(options?.limit ? { limit: options.limit } : {}),
575
+ ...(options?.expand && options.expand.length > 0
576
+ ? { related: options.expand }
577
+ : {}),
578
+ };
579
+ const result = await postQuery(
580
+ {
581
+ baseUrl: this.opts.baseUrl,
582
+ getAuthToken: this.authTokenProvider ?? undefined,
583
+ recoverCredential: this.credentialRecovery ?? undefined,
584
+ runtime: this.opts.runtime,
585
+ },
586
+ { queries: [query] },
587
+ );
588
+ const rows: unknown[] = Array.isArray(result.results[0]) ? result.results[0] : [];
589
+ // Normalize: wire rows lack `__typename` when the server elides it.
590
+ const normalized = rows.map((row) => {
591
+ if (row && typeof row === 'object' && !('__typename' in row)) {
592
+ return { __typename: typename, ...row };
593
+ }
594
+ return row;
595
+ });
596
+
597
+ // Expand: server returns related entities nested under each row
598
+ // (`row.blocks = [{...}, ...]`). Walk the nested shape, stamp the
599
+ // typename from the schema's relation metadata (the server bakes
600
+ // `__typename` into the JSONB but the postgres.camel driver
601
+ // mangles it to `_Typename` mid-flight, so client-side stamping
602
+ // is the only reliable path), hydrate each related row into its
603
+ // own typed pool, then leave the nested arrays in place on the
604
+ // primary row.
605
+ if (options?.expand && options.expand.length > 0) {
606
+ this.hydrateExpanded(modelName, normalized, options.expand);
607
+ }
608
+ return normalized;
609
+ }
610
+
611
+ /**
612
+ * Hydrate nested expanded rows. Resolves each relation's target
613
+ * typename via the schema and stamps `__typename` on every nested
614
+ * row before passing to `hydrateOne` — the server's JSONB
615
+ * `__typename` field gets mangled by `postgres.camel` (`__typename`
616
+ * → `_Typename`), so the SDK can't trust whatever string lands.
617
+ */
618
+ private hydrateExpanded(
619
+ parentModelName: string,
620
+ rows: unknown[],
621
+ relationNames: readonly string[],
622
+ ): void {
623
+ const parentDef = this.getModelDef(parentModelName);
624
+
625
+ for (const row of rows) {
626
+ if (!row || typeof row !== 'object') continue;
627
+ const obj = row as Record<string, unknown>;
628
+ for (const rel of relationNames) {
629
+ const nested = obj[rel];
630
+ if (!nested) continue;
631
+ // Resolve target typename via parent's relations map.
632
+ const relDef = parentDef?.relations?.[rel];
633
+ const targetKey = relDef?.target;
634
+ const targetTypename = targetKey ? this.resolveTypename(targetKey) : undefined;
635
+ const items = Array.isArray(nested) ? nested : [nested];
636
+ const models: Model[] = [];
637
+ const stampedItems: unknown[] = [];
638
+ for (const item of items) {
639
+ const stamped = this.stampTypename(item, targetTypename);
640
+ stampedItems.push(stamped);
641
+ const m = this.hydrateOne(stamped);
642
+ if (m) models.push(m);
643
+ }
644
+ if (models.length > 0) {
645
+ this.opts.objectPool.addBatch(models, ModelScope.live);
646
+ }
647
+ // Persist expanded children to their OWN typed store so they survive
648
+ // reload and can be re-served by `hydrateExpandedFromLocal` — without
649
+ // this, expand-fetched relations live only inside the parent's row
650
+ // and are lost to a lazy child query after a cold start.
651
+ if (stampedItems.length > 0 && targetKey) {
652
+ void this.persistToIdb(targetKey, stampedItems);
653
+ }
654
+ }
655
+ }
656
+ }
657
+
658
+ private async persistToIdb(modelName: string, rows: unknown[]): Promise<void> {
659
+ const store = this.opts.database.getStore(this.resolveTypename(modelName));
660
+ if (!store) return;
661
+ for (const row of rows) {
662
+ try {
663
+ await store.put(row as Record<string, unknown>);
664
+ } catch {
665
+ // IDB writes are best-effort — a transient quota/transaction
666
+ // failure shouldn't break the hydration's primary purpose.
667
+ }
668
+ }
669
+ }
670
+
671
+ private resolveTypename(modelName: string): string {
672
+ // Schema is the source of truth for wire typenames. The model proxy
673
+ // is keyed by camelCase plural (`blocks`) but the wire query +
674
+ // InstanceCache typeIndex use the typename (`Block`).
675
+ const def = (this.opts.schema as { models?: Record<string, { typename?: string }> })
676
+ .models?.[modelName];
677
+ return def?.typename ?? modelName;
678
+ }
679
+
680
+ private columnizeField(modelName: string, field: string): string {
681
+ const fields = (this.opts.schema as {
682
+ models?: Record<string, { fields?: Record<string, { column?: string }> }>;
683
+ }).models?.[modelName]?.fields;
684
+ if (fields) {
685
+ const direct = fields[field]?.column;
686
+ if (direct) return direct;
687
+ for (const [fieldName, meta] of Object.entries(fields)) {
688
+ const conventional = columnize(fieldName);
689
+ if (field === fieldName || field === conventional || field === meta.column) {
690
+ return meta.column ?? conventional;
691
+ }
692
+ }
693
+ }
694
+ return /[A-Z]/.test(field) ? columnize(field) : field;
695
+ }
696
+
697
+ private columnizeClause(modelName: string, clause: WhereClause): WhereClause {
698
+ const finalCol = this.columnizeField(modelName, clause[0]);
699
+ if (clause.length === 2) return [finalCol, clause[1]] as WhereClause;
700
+ return [finalCol, clause[1], clause[2]] as WhereClause;
701
+ }
702
+ }
703
+
704
+ // ── Helpers ────────────────────────────────────────────────────────────
705
+
706
+ function stableKey(
707
+ modelName: string,
708
+ clauses: readonly WhereClause[],
709
+ orderBy: Record<string, unknown> | undefined,
710
+ limit: number | undefined,
711
+ expand: readonly string[] | undefined,
712
+ ): string {
713
+ // Sort clauses by their stringified form so caller order doesn't
714
+ // produce different dedup keys for semantically identical queries.
715
+ const sorted = [...clauses].map((c) => [...c]).sort((a, b) => {
716
+ const ka = JSON.stringify(a);
717
+ const kb = JSON.stringify(b);
718
+ return ka < kb ? -1 : ka > kb ? 1 : 0;
719
+ });
720
+ // Expand is part of the query identity: `sections where report=r1` and the same
721
+ // with `expand:['blocks']` hydrate different data, so they must not share a
722
+ // ledger/dedup key. Sorted so relation order doesn't fork the key.
723
+ const expandKey = expand && expand.length > 0 ? [...expand].sort() : undefined;
724
+ return JSON.stringify({ modelName, where: sorted, orderBy, limit, expand: expandKey });
725
+ }
726
+
727
+ function applyLimit<T>(arr: T[], limit: number | undefined): T[] {
728
+ return typeof limit === 'number' ? arr.slice(0, limit) : arr;
729
+ }
730
+
731
+ function scanPool(
732
+ pool: InstanceCache,
733
+ ModelClass: RegisteredModelClass,
734
+ clauses: readonly WhereClause[],
735
+ ): Model[] {
736
+ const all = pool.getByType(ModelClass);
737
+ if (clauses.length === 0) return all;
738
+ return all.filter((entity) => matchesClauses(entity.toJSON(), clauses));
739
+ }
740
+
741
+ async function scanIdb(
742
+ database: Database,
743
+ modelName: string,
744
+ clauses: readonly WhereClause[],
745
+ ): Promise<unknown[]> {
746
+ const store = database.getStore(modelName);
747
+ if (!store) return [];
748
+
749
+ // Fast path: a single equality `id` lookup hits the primary key.
750
+ const eqClauses = extractEqClauses(clauses);
751
+ if (clauses.length === 1 && eqClauses.id !== undefined && typeof eqClauses.id === 'string') {
752
+ try {
753
+ const row = await store.get(eqClauses.id);
754
+ return row ? [row] : [];
755
+ } catch {
756
+ return [];
757
+ }
758
+ }
759
+
760
+ // Index-aware path: when every clause is equality and exactly one
761
+ // non-id string column is constrained, hit that column's index for
762
+ // an O(matches) read. Anything involving LIKE/ILIKE/ranges falls
763
+ // through to full-scan + filter.
764
+ if (clausesAreAllEquality(clauses)) {
765
+ const indexedKeys = Object.keys(eqClauses).filter(
766
+ (k) => k !== 'id' && typeof eqClauses[k] === 'string',
767
+ );
768
+ const idxKey = indexedKeys.length === 1 ? indexedKeys[0] : undefined;
769
+ if (idxKey !== undefined) {
770
+ try {
771
+ const rows = await store.getAllFromIndex(idxKey, eqClauses[idxKey] as string);
772
+ if (Array.isArray(rows)) {
773
+ return rows.filter((r) => matchesClauses(r, clauses));
774
+ }
775
+ } catch {
776
+ // index doesn't exist — fall through to full-scan path.
777
+ }
778
+ }
779
+ }
780
+
781
+ try {
782
+ const rows = await store.getAll();
783
+ return Array.isArray(rows)
784
+ ? rows.filter((r) => matchesClauses(r, clauses))
785
+ : [];
786
+ } catch {
787
+ return [];
788
+ }
789
+ }
790
+
791
+ /**
792
+ * Normalize `LoadWhere<T>` input to the canonical `readonly WhereClause[]`
793
+ * tuple form used throughout `runFetch`. Tuple inputs pass through; object
794
+ * inputs become one `['col', '=', val]` or `['col', 'IN', vals]` per key.
795
+ *
796
+ * Detection: an array whose first element is itself an array is treated
797
+ * as tuple form. Object form is the fallback.
798
+ *
799
+ * Exported so callers can pre-normalize (e.g., for tests, or to inspect
800
+ * the canonical clauses before passing them to `load`/`subscribe`).
801
+ */
802
+ export function normalizeWhere(where: unknown): readonly WhereClause[] {
803
+ if (where == null) return [];
804
+ if (Array.isArray(where)) {
805
+ // Tuple form — assumed to already use server-side column names.
806
+ return where as readonly WhereClause[];
807
+ }
808
+ if (typeof where === 'object') {
809
+ const obj = where as Record<string, unknown>;
810
+ return Object.entries(obj).map(([key, value]) => {
811
+ if (Array.isArray(value)) {
812
+ return [key, 'IN', value as readonly WherePrimitive[]] as WhereClause;
813
+ }
814
+ return [key, value as WherePrimitive] as WhereClause;
815
+ });
816
+ }
817
+ return [];
818
+ }
819
+
820
+ /** Equality-only subset of clauses, keyed by column. Used by IDB fast paths. */
821
+ function extractEqClauses(clauses: readonly WhereClause[]): Record<string, unknown> {
822
+ const out: Record<string, unknown> = {};
823
+ for (const c of clauses) {
824
+ if (c.length === 2) {
825
+ out[c[0]] = c[1];
826
+ } else if (c[1] === '=') {
827
+ out[c[0]] = c[2];
828
+ }
829
+ }
830
+ return out;
831
+ }
832
+
833
+ function clausesAreAllEquality(clauses: readonly WhereClause[]): boolean {
834
+ return clauses.every((c) => c.length === 2 || c[1] === '=');
835
+ }
836
+
837
+ /**
838
+ * Operator-aware predicate. Mirrors the server's WhereOp semantics for
839
+ * local matching against pool/IDB rows. LIKE/ILIKE use SQL wildcards
840
+ * (`%` = any chars, `_` = one char) translated to a JS regex.
841
+ *
842
+ * Exported so callers can apply the same predicate to in-memory
843
+ * collections (tests, batch operations) using the canonical clauses.
844
+ */
845
+ export function matchesClauses(entity: Record<string, unknown>, clauses: readonly WhereClause[]): boolean {
846
+ for (const clause of clauses) {
847
+ const col = clause[0];
848
+ const op: WhereOp = clause.length === 2 ? '=' : clause[1];
849
+ const expected = clause.length === 2 ? clause[1] : clause[2];
850
+ const v = entity[col];
851
+ if (!matchOp(v, op, expected)) return false;
852
+ }
853
+ return true;
854
+ }
855
+
856
+ function matchOp(actual: unknown, op: WhereOp, expected: unknown): boolean {
857
+ switch (op) {
858
+ case '=':
859
+ return actual === expected;
860
+ case '!=':
861
+ return actual !== expected;
862
+ case '<':
863
+ return compareOrdered(actual, expected, (a, b) => a < b);
864
+ case '<=':
865
+ return compareOrdered(actual, expected, (a, b) => a <= b);
866
+ case '>':
867
+ return compareOrdered(actual, expected, (a, b) => a > b);
868
+ case '>=':
869
+ return compareOrdered(actual, expected, (a, b) => a >= b);
870
+ case 'IN':
871
+ return Array.isArray(expected) && (expected as readonly unknown[]).some((alt) => alt === actual);
872
+ case 'NOT IN':
873
+ return Array.isArray(expected) && !(expected as readonly unknown[]).some((alt) => alt === actual);
874
+ case 'IS':
875
+ // SQL `IS` is null-equality; the only meaningful right-hand side here is null.
876
+ return actual === expected;
877
+ case 'IS NOT':
878
+ return actual !== expected;
879
+ case 'LIKE':
880
+ return typeof actual === 'string' && typeof expected === 'string' && likeRegex(expected, false).test(actual);
881
+ case 'NOT LIKE':
882
+ return typeof actual === 'string' && typeof expected === 'string' && !likeRegex(expected, false).test(actual);
883
+ case 'ILIKE':
884
+ return typeof actual === 'string' && typeof expected === 'string' && likeRegex(expected, true).test(actual);
885
+ case 'NOT ILIKE':
886
+ return typeof actual === 'string' && typeof expected === 'string' && !likeRegex(expected, true).test(actual);
887
+ }
888
+ }
889
+
890
+ /**
891
+ * Ordered comparison helper. Both operands must be non-null and the same
892
+ * comparable primitive (string-vs-string or number-vs-number). Mixed
893
+ * types fall back to JS's loose ordering, which would be confusing — so
894
+ * we reject early to match SQL semantics (a NULL operand yields false).
895
+ */
896
+ function compareOrdered(
897
+ actual: unknown,
898
+ expected: unknown,
899
+ cmp: (a: string | number, b: string | number) => boolean,
900
+ ): boolean {
901
+ if (actual == null || expected == null) return false;
902
+ if (typeof actual === 'number' && typeof expected === 'number') {
903
+ return cmp(actual, expected);
904
+ }
905
+ if (typeof actual === 'string' && typeof expected === 'string') {
906
+ return cmp(actual, expected);
907
+ }
908
+ return false;
909
+ }
910
+
911
+ /** Translate a SQL LIKE/ILIKE pattern to a JS regex (`%` → `.*`, `_` → `.`). */
912
+ function likeRegex(pattern: string, insensitive: boolean): RegExp {
913
+ // Escape regex specials *except* `%` and `_`, then translate those.
914
+ const escaped = pattern.replace(/[\\^$.*+?()[\]{}|]/g, '\\$&');
915
+ const body = escaped.replace(/%/g, '.*').replace(/_/g, '.');
916
+ return new RegExp(`^${body}$`, insensitive ? 'i' : '');
917
+ }
918
+
919
+ /**
920
+ * Schema fields are camelCase (`sectionId`); the wire query expects
921
+ * the server-side column name. The query server's input resolver
922
+ * casing-folds, but we send snake_case to match the convention used
923
+ * by the existing loaders' postQuery calls (`'section_id'` etc.).
924
+ */
925
+ function columnize(field: string): string {
926
+ return field.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`);
927
+ }