@playfast/reform 1.2.1 → 1.4.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 (416) hide show
  1. package/README.md +15 -6
  2. package/dist/boundary/boundary.d.ts +64 -0
  3. package/dist/boundary/boundary.d.ts.map +1 -0
  4. package/dist/boundary/boundary.js +102 -0
  5. package/dist/boundary/boundary.js.map +1 -0
  6. package/dist/calc/asyncCalc.d.ts +23 -0
  7. package/dist/calc/asyncCalc.d.ts.map +1 -0
  8. package/dist/calc/asyncCalc.js +100 -0
  9. package/dist/calc/asyncCalc.js.map +1 -0
  10. package/dist/calc/asyncCalcTypes.d.ts +80 -0
  11. package/dist/calc/asyncCalcTypes.d.ts.map +1 -0
  12. package/dist/calc/asyncCalcTypes.js +2 -0
  13. package/dist/calc/asyncCalcTypes.js.map +1 -0
  14. package/dist/calc/asyncData.d.ts +29 -0
  15. package/dist/calc/asyncData.d.ts.map +1 -0
  16. package/dist/calc/asyncData.js +21 -0
  17. package/dist/calc/asyncData.js.map +1 -0
  18. package/dist/calc/calc.d.ts +39 -0
  19. package/dist/calc/calc.d.ts.map +1 -0
  20. package/dist/calc/calc.js +48 -0
  21. package/dist/calc/calc.js.map +1 -0
  22. package/dist/calc/calcFamily.d.ts +40 -0
  23. package/dist/calc/calcFamily.d.ts.map +1 -0
  24. package/dist/calc/calcFamily.js +156 -0
  25. package/dist/calc/calcFamily.js.map +1 -0
  26. package/dist/calc/compose.d.ts +5 -0
  27. package/dist/calc/compose.d.ts.map +1 -0
  28. package/dist/calc/compose.js +5 -0
  29. package/dist/calc/compose.js.map +1 -0
  30. package/dist/calc/queryState.d.ts +14 -0
  31. package/dist/calc/queryState.d.ts.map +1 -0
  32. package/dist/calc/queryState.js +39 -0
  33. package/dist/calc/queryState.js.map +1 -0
  34. package/dist/channel/channel.d.ts +51 -0
  35. package/dist/channel/channel.d.ts.map +1 -0
  36. package/dist/channel/channel.js +91 -0
  37. package/dist/channel/channel.js.map +1 -0
  38. package/dist/channel/procedures.d.ts +24 -0
  39. package/dist/channel/procedures.d.ts.map +1 -0
  40. package/dist/channel/procedures.js +82 -0
  41. package/dist/channel/procedures.js.map +1 -0
  42. package/dist/compose/composition.d.ts +12 -0
  43. package/dist/compose/composition.d.ts.map +1 -0
  44. package/dist/compose/composition.js +62 -0
  45. package/dist/compose/composition.js.map +1 -0
  46. package/dist/compose/compositionTypes.d.ts +127 -0
  47. package/dist/compose/compositionTypes.d.ts.map +1 -0
  48. package/dist/compose/compositionTypes.js +2 -0
  49. package/dist/compose/compositionTypes.js.map +1 -0
  50. package/dist/compose/host.d.ts +6 -0
  51. package/dist/compose/host.d.ts.map +1 -0
  52. package/dist/compose/host.js +2 -0
  53. package/dist/compose/host.js.map +1 -0
  54. package/dist/compose/props.d.ts +10 -0
  55. package/dist/compose/props.d.ts.map +1 -0
  56. package/dist/compose/props.js +6 -0
  57. package/dist/compose/props.js.map +1 -0
  58. package/dist/compose/provide.d.ts +18 -0
  59. package/dist/compose/provide.d.ts.map +1 -0
  60. package/dist/compose/provide.js +49 -0
  61. package/dist/compose/provide.js.map +1 -0
  62. package/dist/compose/slot.d.ts +70 -0
  63. package/dist/compose/slot.d.ts.map +1 -0
  64. package/dist/compose/slot.js +26 -0
  65. package/dist/compose/slot.js.map +1 -0
  66. package/dist/compose/structure.d.ts +51 -0
  67. package/dist/compose/structure.d.ts.map +1 -0
  68. package/dist/compose/structure.js +117 -0
  69. package/dist/compose/structure.js.map +1 -0
  70. package/dist/compose/ui.d.ts +112 -0
  71. package/dist/compose/ui.d.ts.map +1 -0
  72. package/dist/compose/ui.js +58 -0
  73. package/dist/compose/ui.js.map +1 -0
  74. package/dist/definition/definition.d.ts +11 -0
  75. package/dist/definition/definition.d.ts.map +1 -0
  76. package/dist/definition/definition.js +35 -0
  77. package/dist/definition/definition.js.map +1 -0
  78. package/dist/event/event.d.ts +52 -0
  79. package/dist/event/event.d.ts.map +1 -0
  80. package/dist/event/event.js +88 -0
  81. package/dist/event/event.js.map +1 -0
  82. package/dist/event/eventGroup.d.ts +8 -0
  83. package/dist/event/eventGroup.d.ts.map +1 -0
  84. package/dist/event/eventGroup.js +3 -0
  85. package/dist/event/eventGroup.js.map +1 -0
  86. package/dist/feature/feature.d.ts +27 -0
  87. package/dist/feature/feature.d.ts.map +1 -0
  88. package/dist/feature/feature.js +20 -0
  89. package/dist/feature/feature.js.map +1 -0
  90. package/dist/feature/featureBinding.d.ts +85 -0
  91. package/dist/feature/featureBinding.d.ts.map +1 -0
  92. package/dist/feature/featureBinding.js +10 -0
  93. package/dist/feature/featureBinding.js.map +1 -0
  94. package/dist/feature/featureClass.d.ts +10 -0
  95. package/dist/feature/featureClass.d.ts.map +1 -0
  96. package/dist/feature/featureClass.js +2 -0
  97. package/dist/feature/featureClass.js.map +1 -0
  98. package/dist/feature/featureConfig.d.ts +86 -0
  99. package/dist/feature/featureConfig.d.ts.map +1 -0
  100. package/dist/feature/featureConfig.js +13 -0
  101. package/dist/feature/featureConfig.js.map +1 -0
  102. package/dist/feature/featureFactory.d.ts +15 -0
  103. package/dist/feature/featureFactory.d.ts.map +1 -0
  104. package/dist/feature/featureFactory.js +59 -0
  105. package/dist/feature/featureFactory.js.map +1 -0
  106. package/dist/feature/featureModule.d.ts +28 -0
  107. package/dist/feature/featureModule.d.ts.map +1 -0
  108. package/dist/feature/featureModule.js +33 -0
  109. package/dist/feature/featureModule.js.map +1 -0
  110. package/dist/feature/featureNativeTree.d.ts +26 -0
  111. package/dist/feature/featureNativeTree.d.ts.map +1 -0
  112. package/dist/feature/featureNativeTree.js +2 -0
  113. package/dist/feature/featureNativeTree.js.map +1 -0
  114. package/dist/feature/featureRequirement.d.ts +43 -0
  115. package/dist/feature/featureRequirement.d.ts.map +1 -0
  116. package/dist/feature/featureRequirement.js +6 -0
  117. package/dist/feature/featureRequirement.js.map +1 -0
  118. package/dist/feature/featureVariants.d.ts +9 -0
  119. package/dist/feature/featureVariants.d.ts.map +1 -0
  120. package/dist/feature/featureVariants.js +59 -0
  121. package/dist/feature/featureVariants.js.map +1 -0
  122. package/dist/feature/mountFeature.d.ts +10 -0
  123. package/dist/feature/mountFeature.d.ts.map +1 -0
  124. package/dist/feature/mountFeature.js +14 -0
  125. package/dist/feature/mountFeature.js.map +1 -0
  126. package/dist/graph/closure.d.ts +77 -0
  127. package/dist/graph/closure.d.ts.map +1 -0
  128. package/dist/graph/closure.js +4 -0
  129. package/dist/graph/closure.js.map +1 -0
  130. package/dist/graph/services.d.ts +30 -0
  131. package/dist/graph/services.d.ts.map +1 -0
  132. package/dist/graph/services.js +2 -0
  133. package/dist/graph/services.js.map +1 -0
  134. package/dist/graph/summary.d.ts +60 -0
  135. package/dist/graph/summary.d.ts.map +1 -0
  136. package/dist/graph/summary.js +7 -0
  137. package/dist/graph/summary.js.map +1 -0
  138. package/dist/index.d.ts +51 -0
  139. package/dist/index.d.ts.map +1 -0
  140. package/dist/index.js +34 -0
  141. package/dist/index.js.map +1 -0
  142. package/dist/internal/bucketCache.d.ts +19 -0
  143. package/dist/internal/bucketCache.d.ts.map +1 -0
  144. package/dist/internal/bucketCache.js +20 -0
  145. package/dist/internal/bucketCache.js.map +1 -0
  146. package/dist/internal/capture.d.ts +17 -0
  147. package/dist/internal/capture.d.ts.map +1 -0
  148. package/dist/internal/capture.js +5 -0
  149. package/dist/internal/capture.js.map +1 -0
  150. package/dist/internal/env.d.ts +2 -0
  151. package/dist/internal/env.d.ts.map +1 -0
  152. package/dist/internal/env.js +5 -0
  153. package/dist/internal/env.js.map +1 -0
  154. package/dist/internal/errors.d.ts +48 -0
  155. package/dist/internal/errors.d.ts.map +1 -0
  156. package/dist/internal/errors.js +69 -0
  157. package/dist/internal/errors.js.map +1 -0
  158. package/dist/internal/familyKeys.d.ts +16 -0
  159. package/dist/internal/familyKeys.d.ts.map +1 -0
  160. package/dist/internal/familyKeys.js +35 -0
  161. package/dist/internal/familyKeys.js.map +1 -0
  162. package/dist/internal/inspect.d.ts +5 -0
  163. package/dist/internal/inspect.d.ts.map +1 -0
  164. package/dist/internal/inspect.js +20 -0
  165. package/dist/internal/inspect.js.map +1 -0
  166. package/dist/internal/platform.d.ts +43 -0
  167. package/dist/internal/platform.d.ts.map +1 -0
  168. package/dist/internal/platform.js +68 -0
  169. package/dist/internal/platform.js.map +1 -0
  170. package/dist/internal/queryDriver.d.ts +7 -0
  171. package/dist/internal/queryDriver.d.ts.map +1 -0
  172. package/dist/internal/queryDriver.js +222 -0
  173. package/dist/internal/queryDriver.js.map +1 -0
  174. package/dist/internal/queryDriverConsume.d.ts +9 -0
  175. package/dist/internal/queryDriverConsume.d.ts.map +1 -0
  176. package/dist/internal/queryDriverConsume.js +16 -0
  177. package/dist/internal/queryDriverConsume.js.map +1 -0
  178. package/dist/internal/queryDriverHydrate.d.ts +17 -0
  179. package/dist/internal/queryDriverHydrate.d.ts.map +1 -0
  180. package/dist/internal/queryDriverHydrate.js +25 -0
  181. package/dist/internal/queryDriverHydrate.js.map +1 -0
  182. package/dist/internal/queryDriverPersist.d.ts +16 -0
  183. package/dist/internal/queryDriverPersist.d.ts.map +1 -0
  184. package/dist/internal/queryDriverPersist.js +15 -0
  185. package/dist/internal/queryDriverPersist.js.map +1 -0
  186. package/dist/internal/queryDriverPersistWarn.d.ts +12 -0
  187. package/dist/internal/queryDriverPersistWarn.d.ts.map +1 -0
  188. package/dist/internal/queryDriverPersistWarn.js +31 -0
  189. package/dist/internal/queryDriverPersistWarn.js.map +1 -0
  190. package/dist/internal/queryDriverSettle.d.ts +23 -0
  191. package/dist/internal/queryDriverSettle.d.ts.map +1 -0
  192. package/dist/internal/queryDriverSettle.js +52 -0
  193. package/dist/internal/queryDriverSettle.js.map +1 -0
  194. package/dist/internal/queryDriverStore.d.ts +28 -0
  195. package/dist/internal/queryDriverStore.d.ts.map +1 -0
  196. package/dist/internal/queryDriverStore.js +53 -0
  197. package/dist/internal/queryDriverStore.js.map +1 -0
  198. package/dist/internal/queryDriverTypes.d.ts +44 -0
  199. package/dist/internal/queryDriverTypes.d.ts.map +1 -0
  200. package/dist/internal/queryDriverTypes.js +9 -0
  201. package/dist/internal/queryDriverTypes.js.map +1 -0
  202. package/dist/internal/queryEvents.d.ts +12 -0
  203. package/dist/internal/queryEvents.d.ts.map +1 -0
  204. package/dist/internal/queryEvents.js +11 -0
  205. package/dist/internal/queryEvents.js.map +1 -0
  206. package/dist/internal/queryStore.d.ts +13 -0
  207. package/dist/internal/queryStore.d.ts.map +1 -0
  208. package/dist/internal/queryStore.js +11 -0
  209. package/dist/internal/queryStore.js.map +1 -0
  210. package/dist/internal/reuse.d.ts +2 -0
  211. package/dist/internal/reuse.d.ts.map +1 -0
  212. package/dist/internal/reuse.js +55 -0
  213. package/dist/internal/reuse.js.map +1 -0
  214. package/dist/internal/scheduler.d.ts +14 -0
  215. package/dist/internal/scheduler.d.ts.map +1 -0
  216. package/dist/internal/scheduler.js +101 -0
  217. package/dist/internal/scheduler.js.map +1 -0
  218. package/dist/internal/seeds.d.ts +3 -0
  219. package/dist/internal/seeds.d.ts.map +1 -0
  220. package/dist/internal/seeds.js +3 -0
  221. package/dist/internal/seeds.js.map +1 -0
  222. package/dist/internal/sources.d.ts +20 -0
  223. package/dist/internal/sources.d.ts.map +1 -0
  224. package/dist/internal/sources.js +39 -0
  225. package/dist/internal/sources.js.map +1 -0
  226. package/dist/internal/stateRegistry.d.ts +2 -0
  227. package/dist/internal/stateRegistry.d.ts.map +1 -0
  228. package/dist/internal/stateRegistry.js +15 -0
  229. package/dist/internal/stateRegistry.js.map +1 -0
  230. package/dist/internal/store.d.ts +15 -0
  231. package/dist/internal/store.d.ts.map +1 -0
  232. package/dist/internal/store.js +61 -0
  233. package/dist/internal/store.js.map +1 -0
  234. package/dist/internal/track.d.ts +15 -0
  235. package/dist/internal/track.d.ts.map +1 -0
  236. package/dist/internal/track.js +11 -0
  237. package/dist/internal/track.js.map +1 -0
  238. package/dist/internal/variance.d.ts +4 -0
  239. package/dist/internal/variance.d.ts.map +1 -0
  240. package/dist/internal/variance.js +2 -0
  241. package/dist/internal/variance.js.map +1 -0
  242. package/dist/internal.d.ts +43 -0
  243. package/dist/internal.d.ts.map +1 -0
  244. package/dist/internal.js +59 -0
  245. package/dist/internal.js.map +1 -0
  246. package/dist/namespace/namespace.d.ts +24 -0
  247. package/dist/namespace/namespace.d.ts.map +1 -0
  248. package/dist/namespace/namespace.js +20 -0
  249. package/dist/namespace/namespace.js.map +1 -0
  250. package/dist/procedure/procedure.d.ts +32 -0
  251. package/dist/procedure/procedure.d.ts.map +1 -0
  252. package/dist/procedure/procedure.js +34 -0
  253. package/dist/procedure/procedure.js.map +1 -0
  254. package/dist/reducer/reducer.d.ts +35 -0
  255. package/dist/reducer/reducer.d.ts.map +1 -0
  256. package/dist/reducer/reducer.js +73 -0
  257. package/dist/reducer/reducer.js.map +1 -0
  258. package/dist/remote/pendingQueue.d.ts +22 -0
  259. package/dist/remote/pendingQueue.d.ts.map +1 -0
  260. package/dist/remote/pendingQueue.js +129 -0
  261. package/dist/remote/pendingQueue.js.map +1 -0
  262. package/dist/remote/remoteState.d.ts +13 -0
  263. package/dist/remote/remoteState.d.ts.map +1 -0
  264. package/dist/remote/remoteState.js +189 -0
  265. package/dist/remote/remoteState.js.map +1 -0
  266. package/dist/remote/remoteStateMake.d.ts +8 -0
  267. package/dist/remote/remoteStateMake.d.ts.map +1 -0
  268. package/dist/remote/remoteStateMake.js +46 -0
  269. package/dist/remote/remoteStateMake.js.map +1 -0
  270. package/dist/remote/remoteStateSend.d.ts +43 -0
  271. package/dist/remote/remoteStateSend.d.ts.map +1 -0
  272. package/dist/remote/remoteStateSend.js +83 -0
  273. package/dist/remote/remoteStateSend.js.map +1 -0
  274. package/dist/remote/remoteStateTypes.d.ts +106 -0
  275. package/dist/remote/remoteStateTypes.d.ts.map +1 -0
  276. package/dist/remote/remoteStateTypes.js +4 -0
  277. package/dist/remote/remoteStateTypes.js.map +1 -0
  278. package/dist/runtime/appRuntime.d.ts +10 -0
  279. package/dist/runtime/appRuntime.d.ts.map +1 -0
  280. package/dist/runtime/appRuntime.js +6 -0
  281. package/dist/runtime/appRuntime.js.map +1 -0
  282. package/dist/runtime/bus.d.ts +22 -0
  283. package/dist/runtime/bus.d.ts.map +1 -0
  284. package/dist/runtime/bus.js +12 -0
  285. package/dist/runtime/bus.js.map +1 -0
  286. package/dist/runtime/capturedAppRuntime.d.ts +7 -0
  287. package/dist/runtime/capturedAppRuntime.d.ts.map +1 -0
  288. package/dist/runtime/capturedAppRuntime.js +137 -0
  289. package/dist/runtime/capturedAppRuntime.js.map +1 -0
  290. package/dist/runtime/eventBudget.d.ts +32 -0
  291. package/dist/runtime/eventBudget.d.ts.map +1 -0
  292. package/dist/runtime/eventBudget.js +68 -0
  293. package/dist/runtime/eventBudget.js.map +1 -0
  294. package/dist/runtime/featureMount.d.ts +48 -0
  295. package/dist/runtime/featureMount.d.ts.map +1 -0
  296. package/dist/runtime/featureMount.js +118 -0
  297. package/dist/runtime/featureMount.js.map +1 -0
  298. package/dist/runtime/instrumentation.d.ts +18 -0
  299. package/dist/runtime/instrumentation.d.ts.map +1 -0
  300. package/dist/runtime/instrumentation.js +16 -0
  301. package/dist/runtime/instrumentation.js.map +1 -0
  302. package/dist/runtime/loop.d.ts +44 -0
  303. package/dist/runtime/loop.d.ts.map +1 -0
  304. package/dist/runtime/loop.js +131 -0
  305. package/dist/runtime/loop.js.map +1 -0
  306. package/dist/runtime/queries.d.ts +20 -0
  307. package/dist/runtime/queries.d.ts.map +1 -0
  308. package/dist/runtime/queries.js +21 -0
  309. package/dist/runtime/queries.js.map +1 -0
  310. package/dist/runtime/runtimeHandle.d.ts +56 -0
  311. package/dist/runtime/runtimeHandle.d.ts.map +1 -0
  312. package/dist/runtime/runtimeHandle.js +2 -0
  313. package/dist/runtime/runtimeHandle.js.map +1 -0
  314. package/dist/scene/scene.d.ts +55 -0
  315. package/dist/scene/scene.d.ts.map +1 -0
  316. package/dist/scene/scene.js +74 -0
  317. package/dist/scene/scene.js.map +1 -0
  318. package/dist/state/state.d.ts +54 -0
  319. package/dist/state/state.d.ts.map +1 -0
  320. package/dist/state/state.js +41 -0
  321. package/dist/state/state.js.map +1 -0
  322. package/dist/state/stateFamily.d.ts +59 -0
  323. package/dist/state/stateFamily.d.ts.map +1 -0
  324. package/dist/state/stateFamily.js +139 -0
  325. package/dist/state/stateFamily.js.map +1 -0
  326. package/dist/state/stateGroup.d.ts +36 -0
  327. package/dist/state/stateGroup.d.ts.map +1 -0
  328. package/dist/state/stateGroup.js +33 -0
  329. package/dist/state/stateGroup.js.map +1 -0
  330. package/dist/state/token.d.ts +35 -0
  331. package/dist/state/token.d.ts.map +1 -0
  332. package/dist/state/token.js +18 -0
  333. package/dist/state/token.js.map +1 -0
  334. package/dist/synced/syncedStore.d.ts +44 -0
  335. package/dist/synced/syncedStore.d.ts.map +1 -0
  336. package/dist/synced/syncedStore.js +32 -0
  337. package/dist/synced/syncedStore.js.map +1 -0
  338. package/dist/testkit/flight.testkit.d.ts +18 -0
  339. package/dist/testkit/flight.testkit.d.ts.map +1 -0
  340. package/dist/testkit/flight.testkit.js +46 -0
  341. package/dist/testkit/flight.testkit.js.map +1 -0
  342. package/dist/ui/node.d.ts +3 -0
  343. package/dist/ui/node.d.ts.map +1 -0
  344. package/dist/ui/node.js +2 -0
  345. package/dist/ui/node.js.map +1 -0
  346. package/dist/ui/trigger.d.ts +2 -0
  347. package/dist/ui/trigger.d.ts.map +1 -0
  348. package/dist/ui/trigger.js +2 -0
  349. package/dist/ui/trigger.js.map +1 -0
  350. package/dist/wire/tree.d.ts +32 -0
  351. package/dist/wire/tree.d.ts.map +1 -0
  352. package/dist/wire/tree.js +64 -0
  353. package/dist/wire/tree.js.map +1 -0
  354. package/dist/wire/triggers.d.ts +25 -0
  355. package/dist/wire/triggers.d.ts.map +1 -0
  356. package/dist/wire/triggers.js +31 -0
  357. package/dist/wire/triggers.js.map +1 -0
  358. package/package.json +17 -5
  359. package/src/calc/asyncCalc-key-attribution.test.ts +182 -0
  360. package/src/calc/asyncCalc-registry-name.test.ts +82 -0
  361. package/src/calc/asyncCalc.superseded-commit.test.ts +162 -0
  362. package/src/calc/asyncCalc.test.ts +35 -7
  363. package/src/calc/asyncCalc.ts +33 -10
  364. package/src/calc/asyncCalcTypes.test.ts +64 -0
  365. package/src/calc/asyncCalcTypes.ts +8 -10
  366. package/src/calc/calcFamily.ts +81 -34
  367. package/src/calc/family-identity.test.ts +231 -0
  368. package/src/calc/queryState.ts +36 -5
  369. package/src/channel/channel-throttle-frames.test.ts +163 -0
  370. package/src/channel/channel.ts +5 -0
  371. package/src/compose/compose-identity.test.ts +175 -0
  372. package/src/compose/composition.ts +8 -4
  373. package/src/compose/structure.ts +45 -13
  374. package/src/compose/ui.ts +22 -5
  375. package/src/event/event.ts +31 -2
  376. package/src/event/syncTrigger.test.ts +66 -0
  377. package/src/internal/familyKeys.ts +41 -0
  378. package/src/internal/platform.test.ts +55 -0
  379. package/src/internal/platform.ts +72 -0
  380. package/src/internal/queryDriver-defect-attribution.test.ts +107 -0
  381. package/src/internal/queryDriver-persist-key.test.ts +118 -0
  382. package/src/internal/queryDriver.persistence-staleness.test.ts +209 -0
  383. package/src/internal/queryDriver.ts +92 -80
  384. package/src/internal/queryDriverConsume.ts +42 -0
  385. package/src/internal/queryDriverHydrate.ts +9 -3
  386. package/src/internal/queryDriverPersist.ts +42 -0
  387. package/src/internal/queryDriverPersistWarn.ts +61 -0
  388. package/src/internal/queryDriverSettle.ts +85 -0
  389. package/src/internal/queryDriverStore.ts +9 -74
  390. package/src/internal/queryDriverTypes.ts +10 -0
  391. package/src/internal/queryState-and-scheduler-audit.test.ts +170 -0
  392. package/src/internal/scheduler-multi-source-wake.test.ts +111 -0
  393. package/src/internal/scheduler.ts +80 -6
  394. package/src/internal.ts +1 -0
  395. package/src/reducer/reducer.ts +3 -3
  396. package/src/remote/pendingQueue.ts +35 -5
  397. package/src/remote/remoteState.intent-key-stamp.test.ts +207 -0
  398. package/src/remote/remoteState.key-attribution.test.ts +174 -0
  399. package/src/remote/remoteState.ts +27 -15
  400. package/src/remote/remoteStateSend.ts +29 -8
  401. package/src/remote/remoteStateTypes.ts +7 -0
  402. package/src/runtime/bus.ts +6 -1
  403. package/src/runtime/capturedAppRuntime.ts +134 -110
  404. package/src/runtime/eventBudget.ts +2 -1
  405. package/src/runtime/feature-mount-ownership.test.ts +539 -0
  406. package/src/runtime/featureMount.ts +110 -3
  407. package/src/runtime/loop.ts +29 -6
  408. package/src/runtime/mount-inflight-ownership.test.ts +361 -0
  409. package/src/runtime/mount-scope-hardening.test.ts +440 -0
  410. package/src/runtime/runtime.schema-identity.test.ts +188 -0
  411. package/src/state/state.ts +8 -4
  412. package/src/state/stateFamily.ts +86 -36
  413. package/src/state/stateGroup.ts +24 -6
  414. package/src/testkit/flight.testkit.ts +2 -1
  415. package/src/wire/tree-key-presence.test.ts +46 -0
  416. package/src/wire/tree.ts +9 -1
@@ -1,22 +1,24 @@
1
- import { Cause, Chunk, Effect, MutableRef, Option, Queue, Runtime, type Scope, Stream } from 'effect'
1
+ import { Cause, Effect, MutableRef, Option, Queue, Runtime, type Scope } from 'effect'
2
2
  import { empty, type QueryState } from '../calc/queryState'
3
3
  import { Queries, type QueryHandle } from '../runtime/queries'
4
- import { reuse } from './reuse'
5
4
  import { resolveInstrumentation } from '../runtime/instrumentation'
6
5
  import { resolveQueryStore } from './queryStore'
6
+ import { makePersistWrite, persistCacheKey } from './queryDriverPersist'
7
+ import { makeSettlers } from './queryDriverSettle'
7
8
  import { hydratePersistedQuery } from './queryDriverHydrate'
8
9
  import {
9
10
  incrementGeneration,
10
11
  makeQueryProjection,
11
12
  subscribeQuerySources,
12
- settleQueryError,
13
13
  withReaderTracking,
14
14
  } from './queryDriverStore'
15
15
  import type { QueryDriver, QueryDriverOptions } from './queryDriverTypes'
16
+ import { consumeRequests } from './queryDriverConsume'
16
17
  import { resolveScheduler } from './scheduler'
17
18
  import { type InputsObject, type InputStores, sameKey, wireSources } from './sources'
18
19
  import { makeStore } from './store'
19
20
  import { type AnySource } from '../state/token'
21
+ import { microtask } from './platform'
20
22
 
21
23
  export {
22
24
  bumpRevision,
@@ -44,19 +46,23 @@ export const makeQueryDriver = <Inputs extends ReadonlyArray<AnySource>, A, E, R
44
46
 
45
47
  const keyOf = (args: InputsObject<Inputs>): ReadonlyArray<unknown> =>
46
48
  extraKey === undefined ? sources.keyOf(args) : [...sources.keyOf(args), extraKey.read()]
47
- const persistKey = (
48
- key: string | ((inputs: InputsObject<Inputs>) => string),
49
- args: InputsObject<Inputs>,
50
- ): string => (typeof key === 'function' ? key(args) : key)
51
49
  const disabledNow = (args: InputsObject<Inputs> = sources.snapshot()): boolean =>
52
50
  options.gated && options.disabled !== undefined ? options.disabled(args) : false
53
51
 
54
- const idle: QueryState<A, E> = empty(false)
55
- const stateStore = makeStore<QueryState<A, E>>(disabledNow() ? idle : empty(true), scheduler)
52
+ const keepPreviousData = options.keepPreviousData === true
53
+ const idle: QueryState<A, E> = empty(false, keepPreviousData)
54
+ const stateStore = makeStore<QueryState<A, E>>(
55
+ disabledNow() ? idle : empty(true, keepPreviousData),
56
+ scheduler,
57
+ )
56
58
 
57
59
  const readers = MutableRef.make(0)
58
60
 
59
61
  const lastKey = MutableRef.make<ReadonlyArray<unknown> | undefined>(undefined)
62
+ // Attribution tracks the *source* key, not `keyOf`: `extraKey` carries the
63
+ // invalidateOn revision, and a revision bump revalidates the same question
64
+ // rather than asking a new one.
65
+ const lastSourceKey = MutableRef.make<ReadonlyArray<unknown> | undefined>(undefined)
60
66
  // Monotonic source epoch for the one startup hydration attempt. Comparing
61
67
  // only the current key is insufficient: A → B → A would otherwise let the
62
68
  // original A cache land over the later A request. Same-key refetches do not
@@ -68,50 +74,31 @@ export const makeQueryDriver = <Inputs extends ReadonlyArray<AnySource>, A, E, R
68
74
  // captures this before the startup kick, while invalidation captures it when
69
75
  // the value is marked stale.
70
76
  const staleAtGeneration = MutableRef.make(0)
71
- const markFetching = () => {
72
- const state = stateStore.get()
73
- if (!state.isFetching) {
74
- stateStore.set({ ...state, isFetching: true })
75
- }
76
- }
77
- const settleSuccess = (produced: A): A => {
78
- const prev = stateStore.get()
79
- const shared =
80
- options.reuse === true && Option.isSome(prev.data)
81
- ? reuse(prev.data.value, produced)
82
- : produced
83
- stateStore.set({
84
- data: Option.some(shared),
85
- error: Option.none(),
86
- isFetching: false,
87
- isStale: false,
88
- })
89
- return shared
90
- }
91
- const settleError = (error: E, failedGeneration: number) => {
92
- // Preserve the stale cause after failure without duplicating a newer request.
93
- MutableRef.update(staleAtGeneration, (current) => Math.max(current, failedGeneration))
94
- settleQueryError(stateStore, error)
95
- }
77
+ const { markFetching, settleSuccess, settleError, settleDefect } = makeSettlers<A, E>({
78
+ stateStore,
79
+ lastSourceKey,
80
+ generation,
81
+ staleAtGeneration,
82
+ reuse: options.reuse === true,
83
+ })
96
84
 
97
- const persistWriteTo = (
98
- target: NonNullable<typeof persist>,
99
- produced: A,
100
- args: InputsObject<Inputs>,
101
- ): Effect.Effect<void> =>
102
- target.codec.encode(produced).pipe(
103
- Effect.flatMap((encoded) => queryStore.set(persistKey(target.key, args), encoded)),
104
- // Persistence is best-effort and must not block the visible write.
105
- Effect.ignore,
106
- )
107
- const persistWrite = (produced: A, args: InputsObject<Inputs>): Effect.Effect<void> =>
108
- persist === undefined ? Effect.void : persistWriteTo(persist, produced, args)
85
+ const persistWrite = makePersistWrite({
86
+ queryStore,
87
+ persist,
88
+ label: options.label,
89
+ name: options.name,
90
+ })
109
91
 
110
- const derived = makeQueryProjection({ gated: options.gated, scheduler, stateStore })
92
+ const derived = makeQueryProjection({
93
+ gated: options.gated,
94
+ scheduler,
95
+ stateStore,
96
+ })
111
97
  yield* Effect.addFinalizer(() => Effect.sync(derived.unsubscribe))
112
98
 
113
99
  interface Request {
114
100
  readonly args: InputsObject<Inputs>
101
+ readonly sourceKey: ReadonlyArray<unknown>
115
102
  readonly generation: number
116
103
  }
117
104
 
@@ -120,7 +107,7 @@ export const makeQueryDriver = <Inputs extends ReadonlyArray<AnySource>, A, E, R
120
107
  return Effect.void
121
108
  }
122
109
  return Effect.suspend(() => {
123
- const shared = settleSuccess(produced)
110
+ const shared = settleSuccess(produced, request)
124
111
  options.onSettled?.(request.generation)
125
112
  return persistWrite(shared, request.args)
126
113
  })
@@ -137,9 +124,11 @@ export const makeQueryDriver = <Inputs extends ReadonlyArray<AnySource>, A, E, R
137
124
  Effect.matchCause({
138
125
  onSuccess: (produced): Option.Option<A> => Option.some(produced),
139
126
  onFailure: (cause): Option.Option<A> => {
140
- const failure = Cause.failureOption(cause)
141
- if (Option.isSome(failure) && !disabledNow()) {
142
- settleError(failure.value, request.generation)
127
+ if (!disabledNow()) {
128
+ Option.match(Cause.failureOption(cause), {
129
+ onSome: (error) => settleError(error, request),
130
+ onNone: () => settleDefect(request),
131
+ })
143
132
  }
144
133
  return Option.none()
145
134
  },
@@ -152,27 +141,15 @@ export const makeQueryDriver = <Inputs extends ReadonlyArray<AnySource>, A, E, R
152
141
  ),
153
142
  )
154
143
 
155
- const trailing = options.coalesce === 'trailing'
156
144
  const requests = yield* Queue.unbounded<Request>()
157
- const trailingConsumer = Effect.forever(
158
- Effect.gen(function* () {
159
- const first = yield* Queue.take(requests)
160
- const queued = yield* Queue.takeAll(requests)
161
- const request = Option.getOrElse(Chunk.last(queued), () => first)
162
- if (!disabledNow()) {
163
- yield* runQuery(request)
164
- }
165
- const pending = yield* Queue.size(requests)
166
- if (pending > 0 && !disabledNow()) {
167
- yield* Effect.sync(markFetching)
168
- }
145
+ yield* Effect.forkScoped(
146
+ consumeRequests(options.coalesce === 'trailing', {
147
+ requests,
148
+ runQuery,
149
+ disabledNow: () => disabledNow(),
150
+ markFetching,
169
151
  }),
170
152
  )
171
- const switchConsumer = Stream.fromQueue(requests).pipe(
172
- Stream.flatMap((request) => Stream.fromEffect(runQuery(request)), { switch: true }),
173
- Stream.runDrain,
174
- )
175
- yield* Effect.forkScoped(trailing ? trailingConsumer : switchConsumer)
176
153
 
177
154
  const enqueue = (force: boolean) => {
178
155
  const args = sources.snapshot()
@@ -181,6 +158,7 @@ export const makeQueryDriver = <Inputs extends ReadonlyArray<AnySource>, A, E, R
181
158
  MutableRef.update(sourceEpoch, (current) => current + 1)
182
159
  }
183
160
  MutableRef.set(lastKey, undefined)
161
+ MutableRef.set(lastSourceKey, undefined)
184
162
  stateStore.set(idle)
185
163
  return
186
164
  }
@@ -189,12 +167,16 @@ export const makeQueryDriver = <Inputs extends ReadonlyArray<AnySource>, A, E, R
189
167
  if (!force && previous !== undefined && sameKey(key, previous)) {
190
168
  return
191
169
  }
170
+ const sourceKey = sources.keyOf(args)
171
+ const previousSourceKey = MutableRef.get(lastSourceKey)
172
+ const changedKey = previousSourceKey !== undefined && !sameKey(sourceKey, previousSourceKey)
192
173
  if (previous !== undefined && !sameKey(key, previous)) {
193
174
  MutableRef.update(sourceEpoch, (current) => current + 1)
194
175
  }
195
176
  MutableRef.set(lastKey, key)
196
- markFetching()
197
- Queue.unsafeOffer(requests, { args, generation: nextGeneration() })
177
+ MutableRef.set(lastSourceKey, sourceKey)
178
+ markFetching(changedKey)
179
+ Queue.unsafeOffer(requests, { args, sourceKey, generation: nextGeneration() })
198
180
  }
199
181
  const trigger = () => enqueue(false)
200
182
  const refetch = () => enqueue(true)
@@ -205,14 +187,32 @@ export const makeQueryDriver = <Inputs extends ReadonlyArray<AnySource>, A, E, R
205
187
  refetch()
206
188
  }
207
189
  }
208
- const invalidate = () => {
209
- const state = stateStore.get()
210
- if (state.isStale) {
190
+ // Every invalidation in a frame is one signal, not N: the mark is raised
191
+ // synchronously but the response is coalesced onto a microtask, so a storm
192
+ // (a focus burst, several mutations acking together) issues one request. A
193
+ // later frame arms it again — that invalidation is genuinely uncovered by a
194
+ // request issued before it.
195
+ const refetchArmed = MutableRef.make(false)
196
+ const scheduleAutoRefetch = () => {
197
+ if (MutableRef.get(refetchArmed)) {
211
198
  return
212
199
  }
200
+ MutableRef.set(refetchArmed, true)
201
+ microtask(() => {
202
+ MutableRef.set(refetchArmed, false)
203
+ maybeAutoRefetch()
204
+ })
205
+ }
206
+ const invalidate = () => {
207
+ const state = stateStore.get()
208
+ // Always advance the mark: an invalidation raised while an earlier one's
209
+ // revalidation is in flight is not covered by that flight, so returning early
210
+ // on `isStale` would drop it whole (the refetch-on-focus path).
213
211
  MutableRef.set(staleAtGeneration, MutableRef.get(generation))
214
- stateStore.set({ ...state, isStale: true })
215
- maybeAutoRefetch()
212
+ if (!state.isStale) {
213
+ stateStore.set({ ...state, isStale: true })
214
+ }
215
+ scheduleAutoRefetch()
216
216
  }
217
217
 
218
218
  const unsubscribe = subscribeQuerySources(sources, extraKey, trigger)
@@ -225,7 +225,7 @@ export const makeQueryDriver = <Inputs extends ReadonlyArray<AnySource>, A, E, R
225
225
  const hydrateSourceEpoch = MutableRef.get(sourceEpoch)
226
226
  const hydrate = hydratePersistedQuery({
227
227
  queryStore,
228
- cacheKey: persistKey(persist.key, hydrateArgs),
228
+ cacheKey: persistCacheKey(persist.key, hydrateArgs),
229
229
  codec: persist.codec,
230
230
  stateStore,
231
231
  staleAtGeneration,
@@ -242,9 +242,16 @@ export const makeQueryDriver = <Inputs extends ReadonlyArray<AnySource>, A, E, R
242
242
 
243
243
  const initial = sources.snapshot()
244
244
  if (!disabledNow(initial)) {
245
- MutableRef.set(lastKey, keyOf(initial))
245
+ const initialKey = keyOf(initial)
246
+ const initialSourceKey = sources.keyOf(initial)
247
+ MutableRef.set(lastKey, initialKey)
248
+ MutableRef.set(lastSourceKey, initialSourceKey)
246
249
  markFetching()
247
- Queue.unsafeOffer(requests, { args: initial, generation: nextGeneration() })
250
+ Queue.unsafeOffer(requests, {
251
+ args: initial,
252
+ sourceKey: initialSourceKey,
253
+ generation: nextGeneration(),
254
+ })
248
255
  }
249
256
 
250
257
  const store = withReaderTracking({
@@ -271,5 +278,10 @@ export const makeQueryDriver = <Inputs extends ReadonlyArray<AnySource>, A, E, R
271
278
  yield* Effect.addFinalizer(() => Effect.sync(() => queries.unregister(handle)))
272
279
  }
273
280
 
274
- return { store, requested: () => MutableRef.get(generation) }
281
+ return {
282
+ store,
283
+ requested: () => MutableRef.get(generation),
284
+ key: () => sources.keyOf(sources.snapshot()),
285
+ subscribeKey: sources.subscribe,
286
+ }
275
287
  })
@@ -0,0 +1,42 @@
1
+ import { Chunk, Effect, Option, type Queue, Stream } from 'effect'
2
+
3
+ // How a driver drains its request queue. `switch` keeps only the newest run alive;
4
+ // `trailing` runs one at a time and collapses everything that piled up behind it,
5
+ // re-raising the fetching mark when more is still waiting.
6
+ export interface ConsumeOptions<Request, R> {
7
+ readonly requests: Queue.Queue<Request>
8
+ readonly runQuery: (request: Request) => Effect.Effect<void, never, R>
9
+ readonly disabledNow: () => boolean
10
+ readonly markFetching: () => void
11
+ }
12
+
13
+ const trailingConsumer = <Request, R>(
14
+ options: ConsumeOptions<Request, R>,
15
+ ): Effect.Effect<never, never, R> =>
16
+ Effect.forever(
17
+ Effect.gen(function* () {
18
+ const first = yield* options.requests.take
19
+ const queued = yield* options.requests.takeAll
20
+ const request = Option.getOrElse(Chunk.last(queued), () => first)
21
+ if (!options.disabledNow()) {
22
+ yield* options.runQuery(request)
23
+ }
24
+ const pending = yield* options.requests.size
25
+ if (pending > 0 && !options.disabledNow()) {
26
+ yield* Effect.sync(options.markFetching)
27
+ }
28
+ }),
29
+ )
30
+
31
+ const switchConsumer = <Request, R>(
32
+ options: ConsumeOptions<Request, R>,
33
+ ): Effect.Effect<void, never, R> =>
34
+ Stream.fromQueue(options.requests).pipe(
35
+ Stream.flatMap((request) => Stream.fromEffect(options.runQuery(request)), { switch: true }),
36
+ Stream.runDrain,
37
+ )
38
+
39
+ export const consumeRequests = <Request, R>(
40
+ trailing: boolean,
41
+ options: ConsumeOptions<Request, R>,
42
+ ): Effect.Effect<void, never, R> => (trailing ? trailingConsumer(options) : switchConsumer(options))
@@ -23,7 +23,12 @@ const paint = <A, E>(options: HydratePersistedOptions<A, E>, decoded: A): void =
23
23
  MutableRef.update(options.staleAtGeneration, (current) =>
24
24
  Math.max(current, options.capturedGeneration),
25
25
  )
26
- options.stateStore.set({ ...state, data: Option.some(decoded), isStale: true })
26
+ options.stateStore.set({
27
+ ...state,
28
+ data: Option.some(decoded),
29
+ isStale: true,
30
+ isPreviousKey: false,
31
+ })
27
32
  }
28
33
 
29
34
  // Epoch, key, and settled-state guards prevent a late cache from replacing newer truth.
@@ -40,6 +45,7 @@ export const hydratePersistedQuery = <A, E>(
40
45
  .pipe(Effect.tap((decoded) => Effect.sync(() => paint(options, decoded)))),
41
46
  }),
42
47
  ),
43
- // Corrupt/incompatible cache: ignore and fetch fresh.
44
- Effect.ignore,
48
+ // Corrupt/incompatible cache: ignore and fetch fresh. Catches defects too — a
49
+ // store backed by localStorage throws rather than failing typed.
50
+ Effect.catchAllCause(() => Effect.void),
45
51
  )
@@ -0,0 +1,42 @@
1
+ import { Effect } from 'effect'
2
+ import type { QueryStoreApi } from './queryStore'
3
+ import type { QueryCodec } from './queryDriverTypes'
4
+
5
+ export interface PersistTarget<Args, A> {
6
+ readonly key: string | ((inputs: Args) => string)
7
+ readonly codec: QueryCodec<A>
8
+ }
9
+
10
+ export const persistCacheKey = <Args>(
11
+ key: string | ((inputs: Args) => string),
12
+ args: Args,
13
+ ): string => (typeof key === 'function' ? key(args) : key)
14
+
15
+ export interface PersistWriterOptions<Args, A> {
16
+ readonly queryStore: QueryStoreApi
17
+ readonly persist: PersistTarget<Args, A> | undefined
18
+ readonly label: string
19
+ readonly name: string
20
+ }
21
+
22
+ export const makePersistWrite = <Args, A>(
23
+ options: PersistWriterOptions<Args, A>,
24
+ ): ((produced: A, args: Args) => Effect.Effect<void>) => {
25
+ const target = options.persist
26
+ if (target === undefined) {
27
+ return () => Effect.void
28
+ }
29
+ return (produced, args) =>
30
+ target.codec.encode(produced).pipe(
31
+ Effect.flatMap((encoded) =>
32
+ options.queryStore.set(persistCacheKey(target.key, args), encoded),
33
+ ),
34
+ // Persistence is best-effort and must not block the visible write. This
35
+ // catches defects, not just typed failures: a store backed by localStorage
36
+ // *throws* when quota is exhausted, and `Effect.ignore` would let that escape
37
+ // and kill the request consumer for the rest of the session.
38
+ Effect.catchAllCause((cause) =>
39
+ Effect.logWarning(`reform: ${options.label} '${options.name}' persist failed`, cause),
40
+ ),
41
+ )
42
+ }
@@ -0,0 +1,61 @@
1
+ import { Effect, Predicate } from 'effect'
2
+
3
+ // Mirrors the public `persist` config, whose shape is the author's to write.
4
+ export type PersistOptionExternalApi<Args> =
5
+ | boolean
6
+ | undefined
7
+ | { readonly key?: string | ((inputs: Args) => string) }
8
+
9
+ export interface PersistSlotInput<Args> {
10
+ readonly label: string
11
+ readonly name: string
12
+ readonly inputs: number
13
+ readonly option: PersistOptionExternalApi<Args>
14
+ }
15
+
16
+ interface SharedPersistSlot {
17
+ readonly label: string
18
+ readonly name: string
19
+ readonly inputs: number
20
+ readonly key: string | ((inputs: never) => string)
21
+ readonly enabled: boolean
22
+ }
23
+
24
+ // The cache slot a query persists into, plus the warning if that slot is shared.
25
+ export const resolvePersistSlot = <Args>(
26
+ input: PersistSlotInput<Args>,
27
+ ): Effect.Effect<string | ((inputs: Args) => string)> => {
28
+ const option = input.option
29
+ const key =
30
+ option === true || option === undefined || option === false
31
+ ? input.name
32
+ : (option.key ?? input.name)
33
+ return Effect.as(
34
+ warnSharedPersistSlot({
35
+ label: input.label,
36
+ name: input.name,
37
+ inputs: input.inputs,
38
+ key,
39
+ enabled: option !== undefined && option !== false,
40
+ }),
41
+ key,
42
+ )
43
+ }
44
+
45
+ // A constant `persist` key gives every input key ONE cache slot: the last key to
46
+ // settle overwrites the rest, and on the next boot that payload is hydrated as the
47
+ // answer to whichever key is being asked. Sharing a slot on purpose is a legitimate
48
+ // choice — a "last seen" cache — so this warns rather than refuses, but it must not
49
+ // be silent: it is the same misattribution `isPreviousKey` exists to stop, arriving
50
+ // across a reload where no epoch or key guard can see it.
51
+ const warnSharedPersistSlot = (slot: SharedPersistSlot): Effect.Effect<void> => {
52
+ if (!slot.enabled || slot.inputs === 0 || !Predicate.isString(slot.key)) {
53
+ return Effect.void
54
+ }
55
+ return Effect.logWarning(
56
+ `reform: ${slot.label} '${slot.name}' persists ${slot.inputs} input(s) into the single cache slot ` +
57
+ `'${slot.key}'. Every input key overwrites the same entry, and a reload hydrates it as the answer ` +
58
+ `to whichever key is being asked. Pass persist: { key: (inputs) => ... } for a slot per key, or ` +
59
+ `keep the shared slot deliberately.`,
60
+ )
61
+ }
@@ -0,0 +1,85 @@
1
+ import { MutableRef, Option } from 'effect'
2
+ import type { QueryState } from '../calc/queryState'
3
+ import { settleQueryError } from './queryDriverStore'
4
+ import { reuse } from './reuse'
5
+ import { sameKey } from './sources'
6
+ import type { Store } from './store'
7
+
8
+ // What a request is an answer to. `sourceKey` is the *input* key, excluding
9
+ // `extraKey`: a revision bump revalidates the same question rather than asking a
10
+ // new one, so it must not read as a key change.
11
+ export interface SettleRequest {
12
+ readonly sourceKey: ReadonlyArray<unknown>
13
+ readonly generation: number
14
+ }
15
+
16
+ export interface SettlersOptions<A, E> {
17
+ readonly stateStore: Store<QueryState<A, E>>
18
+ readonly lastSourceKey: MutableRef.MutableRef<ReadonlyArray<unknown> | undefined>
19
+ readonly generation: MutableRef.MutableRef<number>
20
+ readonly staleAtGeneration: MutableRef.MutableRef<number>
21
+ readonly reuse: boolean
22
+ }
23
+
24
+ export interface Settlers<A, E> {
25
+ readonly supersededKey: (requestSourceKey: ReadonlyArray<unknown>) => boolean
26
+ readonly markFetching: (superseded?: boolean) => void
27
+ readonly settleSuccess: (produced: A, request: SettleRequest) => A
28
+ readonly settleError: (error: E, request: SettleRequest) => void
29
+ readonly settleDefect: (request: SettleRequest) => void
30
+ }
31
+
32
+ export const makeSettlers = <A, E>(options: SettlersOptions<A, E>): Settlers<A, E> => {
33
+ const { stateStore } = options
34
+
35
+ // A settling request answers the key it was issued for. If the sources have
36
+ // moved on, whatever it writes is about a question nobody is asking any more.
37
+ const supersededKey = (requestSourceKey: ReadonlyArray<unknown>): boolean => {
38
+ const current = MutableRef.get(options.lastSourceKey)
39
+ return current === undefined || !sameKey(requestSourceKey, current)
40
+ }
41
+
42
+ const markFetching = (superseded = false): void => {
43
+ const state = stateStore.get()
44
+ const isPreviousKey = state.isPreviousKey || superseded
45
+ if (!state.isFetching || isPreviousKey !== state.isPreviousKey) {
46
+ stateStore.set({ ...state, isFetching: true, isPreviousKey })
47
+ }
48
+ }
49
+
50
+ const settleSuccess = (produced: A, request: SettleRequest): A => {
51
+ const prev = stateStore.get()
52
+ const shared =
53
+ options.reuse && Option.isSome(prev.data) ? reuse(prev.data.value, produced) : produced
54
+ stateStore.set({
55
+ ...prev,
56
+ data: Option.some(shared),
57
+ error: Option.none(),
58
+ isFetching: false,
59
+ // An answer requested before an invalidation cannot discharge it: the mark
60
+ // clears only for a strictly newer generation, matching `maybeAutoRefetch`.
61
+ isStale: request.generation <= MutableRef.get(options.staleAtGeneration),
62
+ isPreviousKey: supersededKey(request.sourceKey),
63
+ })
64
+ return shared
65
+ }
66
+
67
+ const settleError = (error: E, request: SettleRequest): void => {
68
+ // Preserve the stale cause after failure without duplicating a newer request.
69
+ MutableRef.update(options.staleAtGeneration, (current) => Math.max(current, request.generation))
70
+ settleQueryError(stateStore, error, supersededKey(request.sourceKey))
71
+ }
72
+
73
+ // A run that dies is over; leaving `isFetching` raised advertises a flight that
74
+ // no longer exists. Only the newest run may retract it — a superseded run's
75
+ // death says nothing about its successor.
76
+ const settleDefect = (request: SettleRequest): void => {
77
+ if (MutableRef.get(options.generation) !== request.generation) {
78
+ return
79
+ }
80
+ const state = stateStore.get()
81
+ stateStore.set({ ...state, isFetching: false })
82
+ }
83
+
84
+ return { supersededKey, markFetching, settleSuccess, settleError, settleDefect }
85
+ }
@@ -1,78 +1,8 @@
1
- import { Effect, MutableRef, Option, Schema } from 'effect'
1
+ import { MutableRef, Option } from 'effect'
2
2
  import type { AnyAsyncData } from '../calc/asyncData'
3
3
  import { type QueryState, toAsyncData } from '../calc/queryState'
4
- import type { QueryStoreApi } from './queryStore'
5
4
  import type { Scheduler } from './scheduler'
6
5
  import { makeDerivedStore, type Store } from './store'
7
- import type { AnySchema } from './variance'
8
- import type { InputsObject } from './sources'
9
- import type { AnySource } from '../state/token'
10
-
11
- interface HydrateQueryOptions<A, E> {
12
- readonly cacheKey: string
13
- readonly queryStore: QueryStoreApi
14
- readonly schema: AnySchema<A>
15
- readonly stateStore: Store<QueryState<A, E>>
16
- }
17
-
18
- export const hydrateQuery = <A, E>(options: HydrateQueryOptions<A, E>): Effect.Effect<void> =>
19
- Effect.flatMap(options.queryStore.get(options.cacheKey), (cached) =>
20
- Option.match(cached, {
21
- onNone: () => Effect.void,
22
- onSome: (raw) =>
23
- Schema.decodeUnknown(options.schema)(raw).pipe(
24
- Effect.match({
25
- onFailure: () => undefined,
26
- onSuccess: (decoded) =>
27
- options.stateStore.set({
28
- data: Option.some(decoded),
29
- error: Option.none(),
30
- isFetching: false,
31
- isStale: true,
32
- }),
33
- }),
34
- ),
35
- }),
36
- )
37
-
38
- interface PersistQueryValueOptions<A> {
39
- readonly cacheKey: string
40
- readonly produced: A
41
- readonly queryStore: QueryStoreApi
42
- readonly schema: AnySchema<A>
43
- }
44
-
45
- export const persistQueryValue = <A>(options: PersistQueryValueOptions<A>): Effect.Effect<void> =>
46
- Schema.encode(options.schema)(options.produced).pipe(
47
- Effect.flatMap((encoded) => options.queryStore.set(options.cacheKey, encoded)),
48
- Effect.ignore,
49
- )
50
-
51
- interface PersistWriterOptions<Inputs extends ReadonlyArray<AnySource>, A> {
52
- readonly persist:
53
- | {
54
- readonly key: string | ((inputs: InputsObject<Inputs>) => string)
55
- readonly schema: AnySchema<A>
56
- }
57
- | undefined
58
- readonly queryStore: QueryStoreApi
59
- }
60
-
61
- export const makePersistWriter = <Inputs extends ReadonlyArray<AnySource>, A>(
62
- options: PersistWriterOptions<Inputs, A>,
63
- ): ((produced: A, inputs: InputsObject<Inputs>) => Effect.Effect<void>) => {
64
- if (options.persist === undefined) {
65
- return () => Effect.void
66
- }
67
- const persist = options.persist
68
- return (produced, inputs) =>
69
- persistQueryValue({
70
- cacheKey: typeof persist.key === 'function' ? persist.key(inputs) : persist.key,
71
- produced,
72
- queryStore: options.queryStore,
73
- schema: persist.schema,
74
- })
75
- }
76
6
 
77
7
  interface SubscribableSource {
78
8
  readonly subscribe: (listener: () => void) => () => void
@@ -91,9 +21,13 @@ export const subscribeQuerySources = (
91
21
  }
92
22
  }
93
23
 
94
- export const settleQueryError = <A, E>(stateStore: Store<QueryState<A, E>>, error: E): void => {
24
+ export const settleQueryError = <A, E>(
25
+ stateStore: Store<QueryState<A, E>>,
26
+ error: E,
27
+ isPreviousKey: boolean,
28
+ ): void => {
95
29
  const previous = stateStore.get()
96
- stateStore.set({ ...previous, error: Option.some(error), isFetching: false })
30
+ stateStore.set({ ...previous, error: Option.some(error), isFetching: false, isPreviousKey })
97
31
  }
98
32
 
99
33
  export const incrementGeneration = (generation: MutableRef.MutableRef<number>): number => {
@@ -120,7 +54,8 @@ export const makeQueryProjection = <A, E>(
120
54
  previous !== undefined &&
121
55
  previous.state.data === state.data &&
122
56
  previous.state.error === state.error &&
123
- previous.state.isFetching === state.isFetching
57
+ previous.state.isFetching === state.isFetching &&
58
+ previous.state.isPreviousKey === state.isPreviousKey
124
59
  ) {
125
60
  return previous.view
126
61
  }
@@ -36,6 +36,9 @@ export interface QueryDriverOptions<Inputs extends ReadonlyArray<AnySource>, A,
36
36
  readonly disabled?: ((inputs: InputsObject<Inputs>) => boolean) | undefined
37
37
  readonly coalesce?: 'switch' | 'trailing' | undefined
38
38
  readonly reuse?: boolean | undefined
39
+ // Show the previous key's result while the new key is in flight. Off by default:
40
+ // a retained payload is not an answer to the key being asked.
41
+ readonly keepPreviousData?: boolean | undefined
39
42
  readonly extraKey?:
40
43
  | {
41
44
  readonly read: () => unknown
@@ -54,4 +57,11 @@ export interface QueryDriverOptions<Inputs extends ReadonlyArray<AnySource>, A,
54
57
  export interface QueryDriver<A, E> {
55
58
  readonly store: Store<AnyAsyncData<A, E>>
56
59
  readonly requested: () => number
60
+ // The current *input* key, excluding `extraKey`: what a result or an optimistic
61
+ // intent is about. A revision bump revalidates the same question.
62
+ readonly key: () => ReadonlyArray<unknown>
63
+ // Wake on a key move. A reader that derives from the key as well as the payload
64
+ // cannot see one through the store: a key change that settles nothing writes
65
+ // nothing, so the payload it reads is unchanged while its meaning is not.
66
+ readonly subscribeKey: (listener: () => void) => () => void
57
67
  }