@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
@@ -0,0 +1,170 @@
1
+ import { expect, it } from '@effect/vitest'
2
+ import * as Arr from 'effect/Array'
3
+ import { Data, Effect, Layer, Match, Option, Schema as S } from 'effect'
4
+ import { AsyncCalc, State, StateGroup } from '../index'
5
+ import { mount, structureEquals } from '../compose/structure'
6
+ import type { UiContract } from '../compose/ui'
7
+ import { isLoading, type QueryState, toAsyncData } from '../calc/queryState'
8
+ import { makeScheduler } from './scheduler'
9
+ import { makeFlightGate, until } from '../testkit/flight.testkit'
10
+
11
+ /**
12
+ * Adversarial audit of the key-attribution / identity fix set. Every assertion
13
+ * below states the behaviour the fixed code is *supposed* to have; each test
14
+ * fails on the current working tree.
15
+ */
16
+
17
+ // ---------------------------------------------------------------------------
18
+ // 1. calc/queryState.ts — a previous key's ERROR is still republished.
19
+ //
20
+ // `toAsyncData` withholds a superseded result unless `keepPreviousData` AND a
21
+ // payload is present. That payload check is what is supposed to guarantee "a
22
+ // previous key's error is never republished" — but a query holding *both* (an
23
+ // older key's payload and a newer key's failure) passes the guard, and past the
24
+ // guard the error branch is read before the data branch. The failure of a key
25
+ // nobody is asking about then gates the key now in flight.
26
+ // ---------------------------------------------------------------------------
27
+
28
+ it('toAsyncData: a superseded error is withheld even when a previous payload is kept', () => {
29
+ // Reachable state: key A settled Success; key B settled Error (settleQueryError
30
+ // spreads the previous state, so `data` survives the failure); key C is now in
31
+ // flight and `markFetching(changedKey)` has raised `isPreviousKey`.
32
+ const carried: QueryState<string, string> = {
33
+ data: Option.some('payload-answering-key-A'),
34
+ error: Option.some('failure-answering-key-B'),
35
+ isFetching: true,
36
+ isStale: false,
37
+ isPreviousKey: true,
38
+ keepPreviousData: true,
39
+ }
40
+ // Control: `isLoading` already reports "there is something to show" here, so
41
+ // the two readers only agree if `toAsyncData` shows the kept *payload* — never
42
+ // the superseded failure.
43
+ expect(isLoading(carried)).toBe(false)
44
+ expect(toAsyncData(carried, true)._tag).not.toBe('Error')
45
+ })
46
+
47
+ class Boom extends Data.TaggedError('AuditBoom')<{ readonly chatId: string }> {}
48
+
49
+ const Chat = S.Struct({ chatId: S.String, canWrite: S.Boolean })
50
+
51
+ class AuditChatId extends State.make('audit2ChatId', S.String) {}
52
+ class AuditInputs extends StateGroup.make(AuditChatId) {}
53
+ const auditChatId = StateGroup.select(AuditInputs, 'audit2ChatId')
54
+ const seededAtA = StateGroup.live(AuditInputs, { audit2ChatId: 'A' })
55
+
56
+ it.live('keepPreviousData: a previous key’s error must not gate the key now in flight', () => {
57
+ class History extends AsyncCalc.make('AuditKeptHistory', {
58
+ inputs: [auditChatId],
59
+ output: Chat,
60
+ error: S.instanceOf(Boom),
61
+ alwaysOn: true,
62
+ }) {}
63
+ const gate = makeFlightGate()
64
+ const answer = (id: string): Effect.Effect<typeof Chat.Type, Boom> =>
65
+ Match.value(id).pipe(
66
+ Match.when('A', () => Effect.succeed({ chatId: 'A', canWrite: true })),
67
+ Match.when('B', () => Effect.fail(new Boom({ chatId: 'B' }))),
68
+ Match.orElse(() => gate.through(Effect.succeed({ chatId: id, canWrite: false }))),
69
+ )
70
+ const HistoryLive = AsyncCalc.live(History, {
71
+ query: ({ audit2ChatId: id }) => answer(id),
72
+ keepPreviousData: true,
73
+ })
74
+ const TestLayer = HistoryLive.pipe(Layer.provideMerge(seededAtA))
75
+
76
+ return Effect.gen(function* () {
77
+ const source = yield* auditChatId.store
78
+ const store = yield* History.store
79
+
80
+ // A answers, so there is a payload for `keepPreviousData` to keep.
81
+ yield* until(
82
+ () => store.get(),
83
+ (view) => view._tag === 'Success',
84
+ )
85
+
86
+ // B fails. That failure *is* B's own answer, so publishing it here is right.
87
+ source.set('B')
88
+ yield* until(
89
+ () => store.get(),
90
+ (view) => view._tag === 'Error',
91
+ )
92
+
93
+ // C is the question now. B's failure says nothing about C, and A's payload is
94
+ // the thing `keepPreviousData` promised to keep showing meanwhile. The store
95
+ // instead reports Error(Boom about 'B') with refetching: true.
96
+ yield* gate.hold
97
+ source.set('C')
98
+ yield* gate.awaitEntry(1)
99
+
100
+ expect(store.get()._tag).not.toBe('Error')
101
+ }).pipe(Effect.provide(TestLayer))
102
+ })
103
+
104
+ // ---------------------------------------------------------------------------
105
+ // 2. compose/structure.ts — `deepEquals` stopped looking inside Effect values.
106
+ //
107
+ // Restricting the structural walk to plain records is right for Map/Set/RegExp,
108
+ // but the `Equal.equals` fallback is not a deep comparison: `Option` compares its
109
+ // payload with `Equal.equals` too, so `Option.some({ id })` is unequal to an
110
+ // identical `Option.some({ id })`. An Option-valued prop is not an edge case —
111
+ // the house rules require it in place of `T | null`, and it encodes over the wire
112
+ // fine. Before this change `deepEquals` walked into the Option and answered true.
113
+ // ---------------------------------------------------------------------------
114
+
115
+ interface RowContract extends UiContract {
116
+ readonly props: {
117
+ readonly label: string
118
+ readonly selected: Option.Option<{ readonly id: string }>
119
+ }
120
+ }
121
+
122
+ const rowFrame = (selected: Option.Option<{ readonly id: string }>) =>
123
+ mount<RowContract>({ props: { label: 'row', selected }, slots: {} })
124
+
125
+ it('structureEquals: value-equal frames carrying an Option of a record compare equal', () => {
126
+ // Controls: Option support in general is intact, and a genuinely different
127
+ // payload still reports a change.
128
+ expect(structureEquals(rowFrame(Option.none()), rowFrame(Option.none()))).toBe(true)
129
+ expect(
130
+ structureEquals(rowFrame(Option.some({ id: 'x' })), rowFrame(Option.some({ id: 'y' }))),
131
+ ).toBe(false)
132
+
133
+ expect(
134
+ structureEquals(rowFrame(Option.some({ id: 'a' })), rowFrame(Option.some({ id: 'a' }))),
135
+ ).toBe(true)
136
+ })
137
+
138
+ // ---------------------------------------------------------------------------
139
+ // 3. internal/scheduler.ts — the liveness re-read happens once per drain.
140
+ //
141
+ // A write now queues `{ source, members }` so the live source can be re-read at
142
+ // flush and anyone who unsubscribed in between dropped. But `new Set(source)` is
143
+ // taken once, before any listener runs, so a listener that an earlier listener in
144
+ // the same flush unsubscribes is still woken — the exact condition the re-read
145
+ // exists to prevent. Re-reading `source` per listener costs nothing extra.
146
+ // ---------------------------------------------------------------------------
147
+
148
+ const drainMicrotasks = (turns = 5): Promise<void> =>
149
+ Arr.range(1, turns).reduce<Promise<void>>(
150
+ (chain) => chain.then(() => new Promise<void>((resolve) => queueMicrotask(resolve))),
151
+ Promise.resolve(),
152
+ )
153
+
154
+ it('scheduler: a listener unsubscribed during the flush is not woken', async () => {
155
+ const scheduler = makeScheduler()
156
+ const listeners = new Set<() => void>()
157
+ const woken: Array<string> = []
158
+ const second = (): void => void woken.push('second')
159
+ const first = (): void => {
160
+ woken.push('first')
161
+ // A teardown reached from another listener's wake: a composition unmounting,
162
+ // a scope closing, a family key being forgotten.
163
+ listeners.delete(second)
164
+ }
165
+ listeners.add(first)
166
+ listeners.add(second)
167
+ scheduler.schedule(listeners)
168
+ await drainMicrotasks()
169
+ expect(woken).toEqual(['first'])
170
+ })
@@ -0,0 +1,111 @@
1
+ import { expect, it } from '@effect/vitest'
2
+ import * as Arr from 'effect/Array'
3
+ import { makeScheduler } from './scheduler'
4
+
5
+ /**
6
+ * Round-3 adversarial audit of the key-attribution / identity fix set.
7
+ *
8
+ * Only findings that survived an attempt to refute them are filed here. Every
9
+ * assertion states the behaviour the FIXED code is supposed to have; the test
10
+ * fails on the current working tree.
11
+ *
12
+ * Checked and found sound (no test filed): the read-ticketing in reform-db
13
+ * (`nextRead`/`applied` are per live query, tickets are strictly monotonic so
14
+ * `applied` can never drop a genuinely newer read, and `tapErrorCause` unregisters
15
+ * a refresher whose first read failed); proof's `{identity, event}` coverage
16
+ * credit (`compositionIdentity` in engine.ts and `compositionIdentityOf` in
17
+ * engineSink.ts are the same `capture(exact => exact.identity)` read, `Namespace`
18
+ * never rewraps a composition, and a requirement's declared events are typed as
19
+ * its OWN contract's action names); `fingerprint.ts`'s memo (`[Circular]` is the
20
+ * only way `seen` reaches the printed string and it always propagates to every
21
+ * ancestor, so a print without it cannot depend on the path — a literal
22
+ * `[Circular]` string in the data only costs a memo entry); `queryState`'s
23
+ * `toAsyncData`/`isLoading` pair (they differ only for `!gated`, which maps a
24
+ * settled-nothing state to `Loading` — true before `isPreviousKey` existed too).
25
+ */
26
+
27
+ // The scheduler flushes on `queueMicrotask`, so draining turns is exact where a
28
+ // duration would be a guess.
29
+ const drainMicrotasks = (turns = 5): Promise<void> =>
30
+ Arr.range(1, turns).reduce<Promise<void>>(
31
+ (chain) => chain.then(() => new Promise<void>((resolve) => queueMicrotask(resolve))),
32
+ Promise.resolve(),
33
+ )
34
+
35
+ // ---------------------------------------------------------------------------
36
+ // internal/scheduler.ts — the per-wake liveness re-check evaluates each listener
37
+ // against ONE source, so a second write's wake is silently dropped.
38
+ //
39
+ // `drain` builds `dueListeners: Map<listener, source>` and keeps only the FIRST
40
+ // queued entry that still holds the listener. A listener that is a member of two
41
+ // sources — the shape `sources.ts` builds for every multi-input query, where one
42
+ // `trigger` is registered into each input store's listener set — therefore gets
43
+ // its liveness answered by whichever store happened to be written first. If an
44
+ // earlier listener in the same flush unsubscribes it from THAT store only, the
45
+ // wake owed by the OTHER store, which it is still subscribed to, is dropped: that
46
+ // write is never delivered and the reader stays stale until something else moves.
47
+ //
48
+ // Dropping a listener that unsubscribed is exactly what the re-check is for; this
49
+ // listener did not unsubscribe from the source whose write is owed to it.
50
+ // ---------------------------------------------------------------------------
51
+
52
+ it('scheduler: a wake owed by a source the listener is still subscribed to is delivered', async () => {
53
+ const scheduler = makeScheduler()
54
+ // Two independent stores' listener sets. `target` reads both — one query
55
+ // trigger, two inputs.
56
+ const storeA = new Set<() => void>()
57
+ const storeB = new Set<() => void>()
58
+ const woken: Array<string> = []
59
+ const target = (): void => void woken.push('target')
60
+ // A teardown reached from another listener's wake: a scope closing, a family key
61
+ // forgotten, a composition unmounting. It ends `target`'s interest in A only.
62
+ const tearer = (): void => {
63
+ woken.push('tearer')
64
+ storeA.delete(target)
65
+ }
66
+ storeA.add(tearer)
67
+ storeA.add(target)
68
+ storeB.add(target)
69
+
70
+ // Control — with no teardown at all, both writes reach `target` exactly once.
71
+ const controlScheduler = makeScheduler()
72
+ const controlStoreA = new Set<() => void>()
73
+ const controlStoreB = new Set<() => void>()
74
+ const controlWoken: Array<string> = []
75
+ const controlTarget = (): void => void controlWoken.push('target')
76
+ controlStoreA.add(controlTarget)
77
+ controlStoreB.add(controlTarget)
78
+ controlScheduler.schedule(controlStoreA)
79
+ controlScheduler.schedule(controlStoreB)
80
+ await drainMicrotasks()
81
+ expect(controlWoken).toEqual(['target'])
82
+
83
+ // Control — the same teardown, with B's write queued FIRST. `target` is woken,
84
+ // which shows the drop below is an artefact of which store was written first,
85
+ // not a rule the scheduler is applying.
86
+ const orderScheduler = makeScheduler()
87
+ const orderStoreA = new Set<() => void>()
88
+ const orderStoreB = new Set<() => void>()
89
+ const orderWoken: Array<string> = []
90
+ const orderTarget = (): void => void orderWoken.push('target')
91
+ const orderTearer = (): void => {
92
+ orderWoken.push('tearer')
93
+ orderStoreA.delete(orderTarget)
94
+ }
95
+ orderStoreA.add(orderTearer)
96
+ orderStoreA.add(orderTarget)
97
+ orderStoreB.add(orderTarget)
98
+ orderScheduler.schedule(orderStoreB)
99
+ orderScheduler.schedule(orderStoreA)
100
+ await drainMicrotasks()
101
+ expect(orderWoken).toEqual(['target', 'tearer'])
102
+
103
+ // The defect — A is written, then B, in the same frame. `tearer` ends `target`'s
104
+ // subscription to A during the flush. B's write is still owed to `target`, which
105
+ // never unsubscribed from B, and is never delivered.
106
+ scheduler.schedule(storeA)
107
+ scheduler.schedule(storeB)
108
+ await drainMicrotasks()
109
+ expect(storeB.has(target)).toBe(true)
110
+ expect(woken).toEqual(['tearer', 'target'])
111
+ })
@@ -1,19 +1,50 @@
1
- import { Context, Effect, Layer, MutableRef, Option } from 'effect'
1
+ import { Context, Effect, Layer, MutableRef, Option, Predicate } from 'effect'
2
+ import { microtask } from './platform'
2
3
 
3
4
  export interface Scheduler {
4
5
  readonly schedule: (listeners: Iterable<() => void>) => void
5
6
  }
6
7
 
8
+ /**
9
+ * Notifications normally coalesce onto a microtask. Inside this window they run
10
+ * inline instead, because the synchronous trigger path needs subscribers
11
+ * re-rendered before it returns: React restores a controlled input's DOM value at
12
+ * the end of the discrete event, and a listener that has not run by then leaves it
13
+ * restoring stale props over the character the user just typed.
14
+ *
15
+ * A module-level flag rather than a method on one Scheduler: stores and triggers
16
+ * resolve `Notifications` from their own contexts and can legitimately land on
17
+ * different instances, so flushing "the" scheduler would flush the wrong one.
18
+ * Nothing awaits inside the window, so it cannot interleave with another trigger.
19
+ */
20
+ const inlineNotifications = MutableRef.make(false)
21
+
22
+ export const withInlineNotifications = <A>(run: () => A): A => {
23
+ const outer = MutableRef.get(inlineNotifications)
24
+ MutableRef.set(inlineNotifications, true)
25
+ const ran = run()
26
+ MutableRef.set(inlineNotifications, outer)
27
+ return ran
28
+ }
29
+
7
30
  export const makeScheduler = (): Scheduler => {
8
- const pending = new Set<() => void>()
31
+ // Each write queues the members present at that moment, so a listener that
32
+ // subscribes later is not told about a write that predates it; the live source is
33
+ // kept alongside and re-read at flush to drop anyone who unsubscribed in between.
34
+ interface Queued {
35
+ readonly source: Iterable<() => void>
36
+ readonly members: ReadonlyArray<() => void>
37
+ }
38
+ const pending = new Set<Queued>()
9
39
  const armed = MutableRef.make(false)
40
+ const flushing = MutableRef.make(false)
10
41
 
11
42
  const runListener = (listener: () => void): void =>
12
43
  Effect.runSync(
13
44
  Effect.try({ try: listener, catch: (error) => error }).pipe(
14
45
  Effect.catchAll((error) =>
15
46
  Effect.sync(() =>
16
- queueMicrotask(() => {
47
+ microtask(() => {
17
48
  // oxlint-disable-next-line reform-rules/no-throw -- only way to surface a listener defect to the host's global error handler
18
49
  throw error
19
50
  }),
@@ -22,26 +53,69 @@ export const makeScheduler = (): Scheduler => {
22
53
  ),
23
54
  )
24
55
 
56
+ // Membership at flush time. A re-readable source — a `Set`, the hot path every store
57
+ // passes, or an array — is read again so a listener that unsubscribed in between is
58
+ // dropped. Anything else is judged against the schedule-time snapshot, because an
59
+ // `Iterable` may be one-shot (`map.values()`, a generator) and taking that snapshot
60
+ // already drained it.
61
+ const isLive = (queued: Queued, listener: () => void): boolean => {
62
+ if (Predicate.isSet(queued.source)) {
63
+ return queued.source.has(listener)
64
+ }
65
+ if (Array.isArray(queued.source)) {
66
+ return queued.source.includes(listener)
67
+ }
68
+ return queued.members.includes(listener)
69
+ }
70
+
25
71
  const drain = (): void => {
26
72
  if (pending.size === 0) {
27
73
  return
28
74
  }
29
75
  const due = [...pending]
30
76
  pending.clear()
31
- due.forEach(runListener)
77
+ // One wake per listener, however many writes queued it, in schedule order. Each
78
+ // write is judged against its OWN source at the moment it would run: an earlier
79
+ // listener in this same flush can tear a later one down — a scope closing, a
80
+ // family key forgotten, a composition unmounting — and a listener that has since
81
+ // unsubscribed must not be woken. A listener registered in several stores is
82
+ // still owed the wakes of the stores it did not leave, so each queued write gets
83
+ // its own answer rather than one memoised for the whole drain.
84
+ const woken = new Set<() => void>()
85
+ due.forEach((queued) => {
86
+ queued.members.forEach((listener) => {
87
+ if (!woken.has(listener) && isLive(queued, listener)) {
88
+ woken.add(listener)
89
+ runListener(listener)
90
+ }
91
+ })
92
+ })
32
93
  drain()
33
94
  }
34
95
 
96
+ // A listener that triggers another event re-enters here; the guard drops the inner
97
+ // call because `drain` already loops until `pending` is empty, so the outer pass
98
+ // still runs whatever the listener queued.
35
99
  const flush = () => {
100
+ if (MutableRef.get(flushing)) {
101
+ return
102
+ }
103
+ MutableRef.set(flushing, true)
36
104
  MutableRef.set(armed, false)
105
+ // `runListener` absorbs every listener defect, so `drain` cannot throw past here.
37
106
  drain()
107
+ MutableRef.set(flushing, false)
38
108
  }
39
109
 
40
110
  const schedule = (listeners: Iterable<() => void>) => {
41
- ;[...listeners].forEach((listener) => pending.add(listener))
111
+ pending.add({ source: listeners, members: Array.from(listeners) })
112
+ if (MutableRef.get(inlineNotifications)) {
113
+ flush()
114
+ return
115
+ }
42
116
  if (pending.size > 0 && !MutableRef.get(armed)) {
43
117
  MutableRef.set(armed, true)
44
- queueMicrotask(flush)
118
+ microtask(flush)
45
119
  }
46
120
  }
47
121
 
package/src/internal.ts CHANGED
@@ -220,3 +220,4 @@ export { makeStore, type Store } from './internal/store'
220
220
  // first-class `Source`s which must participate in dependency tracking and
221
221
  // reflection exactly like a `Calc`.
222
222
  export { type Kind, type Manifest, yieldableClass } from './definition/definition'
223
+ export { microtask, operationId } from './internal/platform'
@@ -128,7 +128,7 @@ export function live(
128
128
  // The loop calls entries only for handled events. At this
129
129
  // existential boundary the family's decoders restore the
130
130
  // exact key/value types before either touches its store.
131
- const key = familyDefinition.decodeKeySync(
131
+ const key = familyDefinition.validateKeySync(
132
132
  sync(Reflect.apply(config.keyOf, undefined, [narrowHandled(event)])),
133
133
  )
134
134
  const previous = family.at(key).get()
@@ -138,7 +138,7 @@ export function live(
138
138
  } else if (next !== previous) {
139
139
  // Same-reference return means "no change": skip the nominal re-decode,
140
140
  // which would mint a fresh value and repaint every tracker of this slice.
141
- family.at(key).set(familyDefinition.decodeValueSync(next))
141
+ family.at(key).set(familyDefinition.validateValueSync(next))
142
142
  }
143
143
  },
144
144
  })),
@@ -157,7 +157,7 @@ export function live(
157
157
  if (next === previous) {
158
158
  return
159
159
  }
160
- store.set(state.decodeUnknownSync(next))
160
+ store.set(state.validateSync(next))
161
161
  },
162
162
  })),
163
163
  )
@@ -1,11 +1,15 @@
1
1
  import { Equal, MutableRef, Option } from 'effect'
2
2
  import { inspectable } from '../internal/inspect'
3
3
  import type { Scheduler } from '../internal/scheduler'
4
+ import { sameKey } from '../internal/sources'
4
5
  import type { Store } from '../internal/store'
5
6
  import type { PendingIntent } from './remoteStateTypes'
6
7
 
7
8
  export interface PendingNode<I> {
8
9
  readonly entry: MutableRef.MutableRef<PendingIntent<I>>
10
+ // None for entries linked through `store.set`, which carries no key: those stay
11
+ // unattributed and always apply, preserving the old behaviour on that path.
12
+ readonly key: Option.Option<ReadonlyArray<unknown>>
9
13
  }
10
14
 
11
15
  // Duplicate opIds retain every node so indexed updates preserve the old map semantics.
@@ -17,7 +21,13 @@ export interface PendingQueue<I> {
17
21
  readonly store: Store<ReadonlyArray<PendingIntent<I>>>
18
22
  readonly revision: () => number
19
23
  readonly fold: <B>(initial: B, combine: (accumulator: B, entry: PendingIntent<I>) => B) => B
20
- readonly append: (entry: PendingIntent<I>) => void
24
+ // Folds only the intents issued against `key` (plus unattributed ones).
25
+ readonly foldFor: <B>(
26
+ key: ReadonlyArray<unknown>,
27
+ initial: B,
28
+ combine: (accumulator: B, entry: PendingIntent<I>) => B,
29
+ ) => B
30
+ readonly append: (entry: PendingIntent<I>, key: Option.Option<ReadonlyArray<unknown>>) => void
21
31
  readonly ack: (opId: string) => void
22
32
  readonly settle: (opId: string) => void
23
33
  }
@@ -49,9 +59,10 @@ export const makePendingQueue = <I>(scheduler: Scheduler): PendingQueue<I> => {
49
59
  scheduler.schedule(listeners)
50
60
  }
51
61
 
52
- const link = (entry: PendingIntent<I>): void => {
62
+ const link = (entry: PendingIntent<I>, key: Option.Option<ReadonlyArray<unknown>>): void => {
53
63
  const node: PendingNode<I> = {
54
64
  entry: MutableRef.make(entry),
65
+ key,
55
66
  }
56
67
  ordered.add(node)
57
68
  const indexed = byOpId.get(entry.opId)
@@ -75,8 +86,26 @@ export const makePendingQueue = <I>(scheduler: Scheduler): PendingQueue<I> => {
75
86
  return MutableRef.get(folded)
76
87
  }
77
88
 
78
- const append = (entry: PendingIntent<I>): void => {
79
- link(entry)
89
+ const foldFor = <B>(
90
+ key: ReadonlyArray<unknown>,
91
+ initialValue: B,
92
+ combine: (accumulator: B, entry: PendingIntent<I>) => B,
93
+ ): B => {
94
+ const folded = MutableRef.make(initialValue)
95
+ ordered.forEach((node) => {
96
+ const applies = Option.match(node.key, {
97
+ onNone: () => true,
98
+ onSome: (issuedFor) => sameKey(issuedFor, key),
99
+ })
100
+ if (applies) {
101
+ MutableRef.update(folded, (accumulator) => combine(accumulator, MutableRef.get(node.entry)))
102
+ }
103
+ })
104
+ return MutableRef.get(folded)
105
+ }
106
+
107
+ const append = (entry: PendingIntent<I>, key: Option.Option<ReadonlyArray<unknown>>): void => {
108
+ link(entry, key)
80
109
  announce(Option.none())
81
110
  }
82
111
 
@@ -122,7 +151,7 @@ export const makePendingQueue = <I>(scheduler: Scheduler): PendingQueue<I> => {
122
151
  return
123
152
  }
124
153
  clear()
125
- next.forEach(link)
154
+ next.forEach((entry) => link(entry, Option.none()))
126
155
  announce(Option.some(next))
127
156
  },
128
157
  subscribe: (listener) => {
@@ -138,6 +167,7 @@ export const makePendingQueue = <I>(scheduler: Scheduler): PendingQueue<I> => {
138
167
  store,
139
168
  revision: () => MutableRef.get(revision),
140
169
  fold,
170
+ foldFor,
141
171
  append,
142
172
  ack,
143
173
  settle,