@playfast/reform 1.2.0 → 1.3.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 (383) hide show
  1. package/dist/boundary/boundary.d.ts +64 -0
  2. package/dist/boundary/boundary.d.ts.map +1 -0
  3. package/dist/boundary/boundary.js +102 -0
  4. package/dist/boundary/boundary.js.map +1 -0
  5. package/dist/calc/asyncCalc.d.ts +21 -0
  6. package/dist/calc/asyncCalc.d.ts.map +1 -0
  7. package/dist/calc/asyncCalc.js +90 -0
  8. package/dist/calc/asyncCalc.js.map +1 -0
  9. package/dist/calc/asyncCalcTypes.d.ts +78 -0
  10. package/dist/calc/asyncCalcTypes.d.ts.map +1 -0
  11. package/dist/calc/asyncCalcTypes.js +2 -0
  12. package/dist/calc/asyncCalcTypes.js.map +1 -0
  13. package/dist/calc/asyncData.d.ts +29 -0
  14. package/dist/calc/asyncData.d.ts.map +1 -0
  15. package/dist/calc/asyncData.js +21 -0
  16. package/dist/calc/asyncData.js.map +1 -0
  17. package/dist/calc/calc.d.ts +39 -0
  18. package/dist/calc/calc.d.ts.map +1 -0
  19. package/dist/calc/calc.js +48 -0
  20. package/dist/calc/calc.js.map +1 -0
  21. package/dist/calc/calcFamily.d.ts +40 -0
  22. package/dist/calc/calcFamily.d.ts.map +1 -0
  23. package/dist/calc/calcFamily.js +113 -0
  24. package/dist/calc/calcFamily.js.map +1 -0
  25. package/dist/calc/compose.d.ts +5 -0
  26. package/dist/calc/compose.d.ts.map +1 -0
  27. package/dist/calc/compose.js +5 -0
  28. package/dist/calc/compose.js.map +1 -0
  29. package/dist/calc/queryState.d.ts +12 -0
  30. package/dist/calc/queryState.d.ts.map +1 -0
  31. package/dist/calc/queryState.js +17 -0
  32. package/dist/calc/queryState.js.map +1 -0
  33. package/dist/channel/channel.d.ts +51 -0
  34. package/dist/channel/channel.d.ts.map +1 -0
  35. package/dist/channel/channel.js +86 -0
  36. package/dist/channel/channel.js.map +1 -0
  37. package/dist/channel/procedures.d.ts +24 -0
  38. package/dist/channel/procedures.d.ts.map +1 -0
  39. package/dist/channel/procedures.js +82 -0
  40. package/dist/channel/procedures.js.map +1 -0
  41. package/dist/compose/composition.d.ts +12 -0
  42. package/dist/compose/composition.d.ts.map +1 -0
  43. package/dist/compose/composition.js +56 -0
  44. package/dist/compose/composition.js.map +1 -0
  45. package/dist/compose/compositionTypes.d.ts +127 -0
  46. package/dist/compose/compositionTypes.d.ts.map +1 -0
  47. package/dist/compose/compositionTypes.js +2 -0
  48. package/dist/compose/compositionTypes.js.map +1 -0
  49. package/dist/compose/host.d.ts +6 -0
  50. package/dist/compose/host.d.ts.map +1 -0
  51. package/dist/compose/host.js +2 -0
  52. package/dist/compose/host.js.map +1 -0
  53. package/dist/compose/props.d.ts +10 -0
  54. package/dist/compose/props.d.ts.map +1 -0
  55. package/dist/compose/props.js +6 -0
  56. package/dist/compose/props.js.map +1 -0
  57. package/dist/compose/provide.d.ts +18 -0
  58. package/dist/compose/provide.d.ts.map +1 -0
  59. package/dist/compose/provide.js +49 -0
  60. package/dist/compose/provide.js.map +1 -0
  61. package/dist/compose/slot.d.ts +70 -0
  62. package/dist/compose/slot.d.ts.map +1 -0
  63. package/dist/compose/slot.js +26 -0
  64. package/dist/compose/slot.js.map +1 -0
  65. package/dist/compose/structure.d.ts +51 -0
  66. package/dist/compose/structure.d.ts.map +1 -0
  67. package/dist/compose/structure.js +92 -0
  68. package/dist/compose/structure.js.map +1 -0
  69. package/dist/compose/ui.d.ts +112 -0
  70. package/dist/compose/ui.d.ts.map +1 -0
  71. package/dist/compose/ui.js +47 -0
  72. package/dist/compose/ui.js.map +1 -0
  73. package/dist/definition/definition.d.ts +11 -0
  74. package/dist/definition/definition.d.ts.map +1 -0
  75. package/dist/definition/definition.js +35 -0
  76. package/dist/definition/definition.js.map +1 -0
  77. package/dist/event/event.d.ts +52 -0
  78. package/dist/event/event.d.ts.map +1 -0
  79. package/dist/event/event.js +67 -0
  80. package/dist/event/event.js.map +1 -0
  81. package/dist/event/eventGroup.d.ts +8 -0
  82. package/dist/event/eventGroup.d.ts.map +1 -0
  83. package/dist/event/eventGroup.js +3 -0
  84. package/dist/event/eventGroup.js.map +1 -0
  85. package/dist/feature/feature.d.ts +27 -0
  86. package/dist/feature/feature.d.ts.map +1 -0
  87. package/dist/feature/feature.js +20 -0
  88. package/dist/feature/feature.js.map +1 -0
  89. package/dist/feature/featureBinding.d.ts +85 -0
  90. package/dist/feature/featureBinding.d.ts.map +1 -0
  91. package/dist/feature/featureBinding.js +10 -0
  92. package/dist/feature/featureBinding.js.map +1 -0
  93. package/dist/feature/featureClass.d.ts +10 -0
  94. package/dist/feature/featureClass.d.ts.map +1 -0
  95. package/dist/feature/featureClass.js +2 -0
  96. package/dist/feature/featureClass.js.map +1 -0
  97. package/dist/feature/featureConfig.d.ts +86 -0
  98. package/dist/feature/featureConfig.d.ts.map +1 -0
  99. package/dist/feature/featureConfig.js +13 -0
  100. package/dist/feature/featureConfig.js.map +1 -0
  101. package/dist/feature/featureFactory.d.ts +15 -0
  102. package/dist/feature/featureFactory.d.ts.map +1 -0
  103. package/dist/feature/featureFactory.js +59 -0
  104. package/dist/feature/featureFactory.js.map +1 -0
  105. package/dist/feature/featureModule.d.ts +28 -0
  106. package/dist/feature/featureModule.d.ts.map +1 -0
  107. package/dist/feature/featureModule.js +33 -0
  108. package/dist/feature/featureModule.js.map +1 -0
  109. package/dist/feature/featureNativeTree.d.ts +26 -0
  110. package/dist/feature/featureNativeTree.d.ts.map +1 -0
  111. package/dist/feature/featureNativeTree.js +2 -0
  112. package/dist/feature/featureNativeTree.js.map +1 -0
  113. package/dist/feature/featureRequirement.d.ts +43 -0
  114. package/dist/feature/featureRequirement.d.ts.map +1 -0
  115. package/dist/feature/featureRequirement.js +6 -0
  116. package/dist/feature/featureRequirement.js.map +1 -0
  117. package/dist/feature/featureVariants.d.ts +9 -0
  118. package/dist/feature/featureVariants.d.ts.map +1 -0
  119. package/dist/feature/featureVariants.js +59 -0
  120. package/dist/feature/featureVariants.js.map +1 -0
  121. package/dist/feature/mountFeature.d.ts +10 -0
  122. package/dist/feature/mountFeature.d.ts.map +1 -0
  123. package/dist/feature/mountFeature.js +14 -0
  124. package/dist/feature/mountFeature.js.map +1 -0
  125. package/dist/graph/closure.d.ts +77 -0
  126. package/dist/graph/closure.d.ts.map +1 -0
  127. package/dist/graph/closure.js +4 -0
  128. package/dist/graph/closure.js.map +1 -0
  129. package/dist/graph/services.d.ts +30 -0
  130. package/dist/graph/services.d.ts.map +1 -0
  131. package/dist/graph/services.js +2 -0
  132. package/dist/graph/services.js.map +1 -0
  133. package/dist/graph/summary.d.ts +60 -0
  134. package/dist/graph/summary.d.ts.map +1 -0
  135. package/dist/graph/summary.js +7 -0
  136. package/dist/graph/summary.js.map +1 -0
  137. package/dist/index.d.ts +51 -0
  138. package/dist/index.d.ts.map +1 -0
  139. package/dist/index.js +34 -0
  140. package/dist/index.js.map +1 -0
  141. package/dist/internal/bucketCache.d.ts +19 -0
  142. package/dist/internal/bucketCache.d.ts.map +1 -0
  143. package/dist/internal/bucketCache.js +20 -0
  144. package/dist/internal/bucketCache.js.map +1 -0
  145. package/dist/internal/capture.d.ts +17 -0
  146. package/dist/internal/capture.d.ts.map +1 -0
  147. package/dist/internal/capture.js +5 -0
  148. package/dist/internal/capture.js.map +1 -0
  149. package/dist/internal/env.d.ts +2 -0
  150. package/dist/internal/env.d.ts.map +1 -0
  151. package/dist/internal/env.js +5 -0
  152. package/dist/internal/env.js.map +1 -0
  153. package/dist/internal/errors.d.ts +48 -0
  154. package/dist/internal/errors.d.ts.map +1 -0
  155. package/dist/internal/errors.js +69 -0
  156. package/dist/internal/errors.js.map +1 -0
  157. package/dist/internal/inspect.d.ts +5 -0
  158. package/dist/internal/inspect.d.ts.map +1 -0
  159. package/dist/internal/inspect.js +20 -0
  160. package/dist/internal/inspect.js.map +1 -0
  161. package/dist/internal/queryDriver.d.ts +7 -0
  162. package/dist/internal/queryDriver.d.ts.map +1 -0
  163. package/dist/internal/queryDriver.js +198 -0
  164. package/dist/internal/queryDriver.js.map +1 -0
  165. package/dist/internal/queryDriverHydrate.d.ts +17 -0
  166. package/dist/internal/queryDriverHydrate.d.ts.map +1 -0
  167. package/dist/internal/queryDriverHydrate.js +19 -0
  168. package/dist/internal/queryDriverHydrate.js.map +1 -0
  169. package/dist/internal/queryDriverStore.d.ts +54 -0
  170. package/dist/internal/queryDriverStore.d.ts.map +1 -0
  171. package/dist/internal/queryDriverStore.js +77 -0
  172. package/dist/internal/queryDriverStore.js.map +1 -0
  173. package/dist/internal/queryDriverTypes.d.ts +41 -0
  174. package/dist/internal/queryDriverTypes.d.ts.map +1 -0
  175. package/dist/internal/queryDriverTypes.js +9 -0
  176. package/dist/internal/queryDriverTypes.js.map +1 -0
  177. package/dist/internal/queryEvents.d.ts +12 -0
  178. package/dist/internal/queryEvents.d.ts.map +1 -0
  179. package/dist/internal/queryEvents.js +11 -0
  180. package/dist/internal/queryEvents.js.map +1 -0
  181. package/dist/internal/queryStore.d.ts +13 -0
  182. package/dist/internal/queryStore.d.ts.map +1 -0
  183. package/dist/internal/queryStore.js +11 -0
  184. package/dist/internal/queryStore.js.map +1 -0
  185. package/dist/internal/reuse.d.ts +2 -0
  186. package/dist/internal/reuse.d.ts.map +1 -0
  187. package/dist/internal/reuse.js +55 -0
  188. package/dist/internal/reuse.js.map +1 -0
  189. package/dist/internal/scheduler.d.ts +13 -0
  190. package/dist/internal/scheduler.d.ts.map +1 -0
  191. package/dist/internal/scheduler.js +38 -0
  192. package/dist/internal/scheduler.js.map +1 -0
  193. package/dist/internal/seeds.d.ts +3 -0
  194. package/dist/internal/seeds.d.ts.map +1 -0
  195. package/dist/internal/seeds.js +3 -0
  196. package/dist/internal/seeds.js.map +1 -0
  197. package/dist/internal/sources.d.ts +20 -0
  198. package/dist/internal/sources.d.ts.map +1 -0
  199. package/dist/internal/sources.js +39 -0
  200. package/dist/internal/sources.js.map +1 -0
  201. package/dist/internal/stateRegistry.d.ts +2 -0
  202. package/dist/internal/stateRegistry.d.ts.map +1 -0
  203. package/dist/internal/stateRegistry.js +15 -0
  204. package/dist/internal/stateRegistry.js.map +1 -0
  205. package/dist/internal/store.d.ts +15 -0
  206. package/dist/internal/store.d.ts.map +1 -0
  207. package/dist/internal/store.js +61 -0
  208. package/dist/internal/store.js.map +1 -0
  209. package/dist/internal/track.d.ts +15 -0
  210. package/dist/internal/track.d.ts.map +1 -0
  211. package/dist/internal/track.js +11 -0
  212. package/dist/internal/track.js.map +1 -0
  213. package/dist/internal/variance.d.ts +4 -0
  214. package/dist/internal/variance.d.ts.map +1 -0
  215. package/dist/internal/variance.js +2 -0
  216. package/dist/internal/variance.js.map +1 -0
  217. package/dist/internal.d.ts +42 -0
  218. package/dist/internal.d.ts.map +1 -0
  219. package/dist/internal.js +58 -0
  220. package/dist/internal.js.map +1 -0
  221. package/dist/namespace/namespace.d.ts +24 -0
  222. package/dist/namespace/namespace.d.ts.map +1 -0
  223. package/dist/namespace/namespace.js +20 -0
  224. package/dist/namespace/namespace.js.map +1 -0
  225. package/dist/procedure/procedure.d.ts +32 -0
  226. package/dist/procedure/procedure.d.ts.map +1 -0
  227. package/dist/procedure/procedure.js +34 -0
  228. package/dist/procedure/procedure.js.map +1 -0
  229. package/dist/reducer/reducer.d.ts +35 -0
  230. package/dist/reducer/reducer.d.ts.map +1 -0
  231. package/dist/reducer/reducer.js +73 -0
  232. package/dist/reducer/reducer.js.map +1 -0
  233. package/dist/remote/pendingQueue.d.ts +20 -0
  234. package/dist/remote/pendingQueue.d.ts.map +1 -0
  235. package/dist/remote/pendingQueue.js +113 -0
  236. package/dist/remote/pendingQueue.js.map +1 -0
  237. package/dist/remote/remoteState.d.ts +13 -0
  238. package/dist/remote/remoteState.d.ts.map +1 -0
  239. package/dist/remote/remoteState.js +176 -0
  240. package/dist/remote/remoteState.js.map +1 -0
  241. package/dist/remote/remoteStateMake.d.ts +8 -0
  242. package/dist/remote/remoteStateMake.d.ts.map +1 -0
  243. package/dist/remote/remoteStateMake.js +46 -0
  244. package/dist/remote/remoteStateMake.js.map +1 -0
  245. package/dist/remote/remoteStateSend.d.ts +41 -0
  246. package/dist/remote/remoteStateSend.d.ts.map +1 -0
  247. package/dist/remote/remoteStateSend.js +67 -0
  248. package/dist/remote/remoteStateSend.js.map +1 -0
  249. package/dist/remote/remoteStateTypes.d.ts +103 -0
  250. package/dist/remote/remoteStateTypes.d.ts.map +1 -0
  251. package/dist/remote/remoteStateTypes.js +4 -0
  252. package/dist/remote/remoteStateTypes.js.map +1 -0
  253. package/dist/runtime/appRuntime.d.ts +10 -0
  254. package/dist/runtime/appRuntime.d.ts.map +1 -0
  255. package/dist/runtime/appRuntime.js +6 -0
  256. package/dist/runtime/appRuntime.js.map +1 -0
  257. package/dist/runtime/bus.d.ts +17 -0
  258. package/dist/runtime/bus.d.ts.map +1 -0
  259. package/dist/runtime/bus.js +12 -0
  260. package/dist/runtime/bus.js.map +1 -0
  261. package/dist/runtime/capturedAppRuntime.d.ts +7 -0
  262. package/dist/runtime/capturedAppRuntime.d.ts.map +1 -0
  263. package/dist/runtime/capturedAppRuntime.js +151 -0
  264. package/dist/runtime/capturedAppRuntime.js.map +1 -0
  265. package/dist/runtime/eventBudget.d.ts +32 -0
  266. package/dist/runtime/eventBudget.d.ts.map +1 -0
  267. package/dist/runtime/eventBudget.js +67 -0
  268. package/dist/runtime/eventBudget.js.map +1 -0
  269. package/dist/runtime/featureMount.d.ts +16 -0
  270. package/dist/runtime/featureMount.d.ts.map +1 -0
  271. package/dist/runtime/featureMount.js +54 -0
  272. package/dist/runtime/featureMount.js.map +1 -0
  273. package/dist/runtime/instrumentation.d.ts +18 -0
  274. package/dist/runtime/instrumentation.d.ts.map +1 -0
  275. package/dist/runtime/instrumentation.js +16 -0
  276. package/dist/runtime/instrumentation.js.map +1 -0
  277. package/dist/runtime/loop.d.ts +32 -0
  278. package/dist/runtime/loop.d.ts.map +1 -0
  279. package/dist/runtime/loop.js +128 -0
  280. package/dist/runtime/loop.js.map +1 -0
  281. package/dist/runtime/queries.d.ts +20 -0
  282. package/dist/runtime/queries.d.ts.map +1 -0
  283. package/dist/runtime/queries.js +21 -0
  284. package/dist/runtime/queries.js.map +1 -0
  285. package/dist/runtime/runtimeHandle.d.ts +56 -0
  286. package/dist/runtime/runtimeHandle.d.ts.map +1 -0
  287. package/dist/runtime/runtimeHandle.js +2 -0
  288. package/dist/runtime/runtimeHandle.js.map +1 -0
  289. package/dist/scene/scene.d.ts +55 -0
  290. package/dist/scene/scene.d.ts.map +1 -0
  291. package/dist/scene/scene.js +74 -0
  292. package/dist/scene/scene.js.map +1 -0
  293. package/dist/state/state.d.ts +54 -0
  294. package/dist/state/state.d.ts.map +1 -0
  295. package/dist/state/state.js +41 -0
  296. package/dist/state/state.js.map +1 -0
  297. package/dist/state/stateFamily.d.ts +59 -0
  298. package/dist/state/stateFamily.d.ts.map +1 -0
  299. package/dist/state/stateFamily.js +91 -0
  300. package/dist/state/stateFamily.js.map +1 -0
  301. package/dist/state/stateGroup.d.ts +36 -0
  302. package/dist/state/stateGroup.d.ts.map +1 -0
  303. package/dist/state/stateGroup.js +33 -0
  304. package/dist/state/stateGroup.js.map +1 -0
  305. package/dist/state/token.d.ts +35 -0
  306. package/dist/state/token.d.ts.map +1 -0
  307. package/dist/state/token.js +18 -0
  308. package/dist/state/token.js.map +1 -0
  309. package/dist/synced/syncedStore.d.ts +44 -0
  310. package/dist/synced/syncedStore.d.ts.map +1 -0
  311. package/dist/synced/syncedStore.js +32 -0
  312. package/dist/synced/syncedStore.js.map +1 -0
  313. package/dist/testkit/flight.testkit.d.ts +18 -0
  314. package/dist/testkit/flight.testkit.d.ts.map +1 -0
  315. package/dist/testkit/flight.testkit.js +45 -0
  316. package/dist/testkit/flight.testkit.js.map +1 -0
  317. package/dist/ui/node.d.ts +3 -0
  318. package/dist/ui/node.d.ts.map +1 -0
  319. package/dist/ui/node.js +2 -0
  320. package/dist/ui/node.js.map +1 -0
  321. package/dist/ui/trigger.d.ts +2 -0
  322. package/dist/ui/trigger.d.ts.map +1 -0
  323. package/dist/ui/trigger.js +2 -0
  324. package/dist/ui/trigger.js.map +1 -0
  325. package/dist/wire/tree.d.ts +32 -0
  326. package/dist/wire/tree.d.ts.map +1 -0
  327. package/dist/wire/tree.js +59 -0
  328. package/dist/wire/tree.js.map +1 -0
  329. package/dist/wire/triggers.d.ts +25 -0
  330. package/dist/wire/triggers.d.ts.map +1 -0
  331. package/dist/wire/triggers.js +31 -0
  332. package/dist/wire/triggers.js.map +1 -0
  333. package/package.json +17 -5
  334. package/src/boundary/boundary.test.ts +26 -23
  335. package/src/calc/asyncCalc.invalidate.test.ts +258 -55
  336. package/src/calc/asyncCalc.test.ts +225 -85
  337. package/src/calc/asyncCalc.ts +27 -141
  338. package/src/calc/asyncCalcTypes.test.ts +64 -0
  339. package/src/calc/asyncCalcTypes.ts +139 -0
  340. package/src/calc/calc.test.ts +10 -11
  341. package/src/calc/calcFamily.test.ts +12 -12
  342. package/src/channel/channel.ts +8 -106
  343. package/src/channel/procedures.ts +108 -0
  344. package/src/compose/composition.ts +49 -246
  345. package/src/compose/compositionTypes.ts +249 -0
  346. package/src/event/event.fromSource.test.ts +12 -12
  347. package/src/feature/feature.ts +50 -1210
  348. package/src/feature/feature.typecheck.ts +2 -61
  349. package/src/feature/featureBinding.ts +158 -0
  350. package/src/feature/featureClass.ts +162 -0
  351. package/src/feature/featureConfig.ts +192 -0
  352. package/src/feature/featureFactory.ts +163 -0
  353. package/src/feature/featureModule.ts +136 -0
  354. package/src/feature/featureModule.typecheck.ts +63 -0
  355. package/src/feature/featureNativeTree.ts +152 -0
  356. package/src/feature/featureRequirement.ts +82 -0
  357. package/src/feature/featureVariants.ts +234 -0
  358. package/src/feature/mountFeature.ts +54 -0
  359. package/src/graph/closure.ts +45 -226
  360. package/src/graph/services.ts +95 -0
  361. package/src/graph/summary.ts +134 -0
  362. package/src/internal/queryDriver.ts +20 -89
  363. package/src/internal/queryDriverHydrate.ts +45 -0
  364. package/src/internal/queryDriverTypes.ts +8 -10
  365. package/src/internal/store.test.ts +27 -6
  366. package/src/remote/pendingQueue.ts +145 -0
  367. package/src/remote/remoteState.test.ts +28 -22
  368. package/src/remote/remoteState.ts +63 -489
  369. package/src/remote/remoteStateMake.ts +97 -0
  370. package/src/remote/remoteStateSend.ts +122 -0
  371. package/src/remote/remoteStateTypes.ts +155 -0
  372. package/src/runtime/appRuntime.activation.test.ts +9 -5
  373. package/src/runtime/appRuntime.test.ts +40 -16
  374. package/src/runtime/appRuntime.ts +17 -472
  375. package/src/runtime/capturedAppRuntime.ts +274 -0
  376. package/src/runtime/eventBudget.test.ts +38 -8
  377. package/src/runtime/featureMount.ts +94 -0
  378. package/src/runtime/hardening.test.ts +18 -4
  379. package/src/runtime/instrumentation.test.ts +49 -12
  380. package/src/runtime/loop.test.ts +51 -10
  381. package/src/runtime/runtimeHandle.ts +124 -0
  382. package/src/state/stateFamily.test.ts +25 -8
  383. package/src/testkit/flight.testkit.ts +9 -0
@@ -13,17 +13,7 @@ import {
13
13
  import { Channels, Procedures } from '../channel/channel'
14
14
  import { QueryStore } from '../internal/queryStore'
15
15
  import { Reducers } from '../runtime/loop'
16
-
17
- const tick = (ms = 10) => Effect.sleep(Duration.millis(ms))
18
-
19
- // Bounded poll — fixed tick budgets flake under parallel-suite host load
20
- const until = <A>(read: () => A, pred: (a: A) => boolean, rounds = 200): Effect.Effect<A> =>
21
- Effect.suspend(() => {
22
- const a = read()
23
- return pred(a) || rounds <= 0
24
- ? Effect.succeed(a)
25
- : Effect.flatMap(tick(10), () => until(read, pred, rounds - 1))
26
- })
16
+ import { makeFlightGate, tick, until } from '../testkit/flight.testkit'
27
17
 
28
18
  const Item = S.Struct({ id: S.String, name: S.String })
29
19
  type Item = typeof Item.Type
@@ -284,9 +274,10 @@ it.live(
284
274
  intents: [Added, Renamed],
285
275
  alwaysOn: true,
286
276
  }) {}
277
+ const gate = makeFlightGate()
287
278
  const ItemsLive = RemoteState.live(Items, {
288
- query: () => server.fetch(60),
289
- send: (intent) => server.receive(intent, 15),
279
+ query: () => gate.through(server.fetch(0)),
280
+ send: (intent) => server.receive(intent, 0),
290
281
  apply: applyIntent,
291
282
  invalidateOn: [Poke],
292
283
  // Trailing lets pre-ack run land — stale Success must not settle
@@ -312,22 +303,25 @@ it.live(
312
303
  if (v._tag === 'Success') frames.push(v.value.length)
313
304
  })
314
305
 
306
+ // The gen-2 fetch is pinned open, so the intent dispatched after it can only be
307
+ // settled by a later run — the stale snapshot is structurally denied the chance.
308
+ yield* gate.hold
315
309
  yield* Event.dispatch(Poke, {})
316
- yield* tick(5)
310
+ yield* gate.awaitEntry(2)
311
+ expect(server.state.fetches).toBe(2)
317
312
  yield* Event.dispatch(Added, { id: 'b', name: 'beta' })
318
313
 
319
- // Gen-2 Success settles nobody (generation rule)
320
314
  yield* until(
321
- () => server.state.fetches,
322
- (n) => n === 2,
315
+ () => queue.get(),
316
+ (q) => q[0]?.status === 'confirmed',
323
317
  )
324
- yield* tick(25)
325
318
  expect(queue.get()).toHaveLength(1)
326
319
  expect(queue.get()[0]).toMatchObject({ status: 'confirmed' })
327
320
  const midflight = store.get()
328
321
  expect(midflight).toMatchObject({ _tag: 'Success' })
329
322
  if (midflight._tag === 'Success') expect(midflight.value).toHaveLength(2)
330
323
 
324
+ yield* gate.release
331
325
  yield* until(
332
326
  () => queue.get(),
333
327
  (q) => q.length === 0,
@@ -617,7 +611,6 @@ it.live('gated: intents queue and send while Idle; re-enabling converges and set
617
611
  const source = yield* StateGroup.select(Inputs, 'page').store
618
612
  const store = yield* Items.store
619
613
  const queue = yield* Items.pending.store
620
- yield* tick()
621
614
  expect(store.get()._tag).toBe('Idle')
622
615
 
623
616
  // Intents still send while gated Idle; settle waits for re-enable
@@ -626,7 +619,11 @@ it.live('gated: intents queue and send while Idle; re-enabling converges and set
626
619
  () => server.state.items,
627
620
  (items) => items.length === 2,
628
621
  )
629
- yield* tick(20)
622
+ // The ack is the only thing that could settle the intent — assert the instant it lands.
623
+ yield* until(
624
+ () => queue.get(),
625
+ (q) => q[0]?.status === 'confirmed',
626
+ )
630
627
  expect(store.get()._tag).toBe('Idle')
631
628
  expect(queue.get()).toHaveLength(1)
632
629
  expect(server.state.fetches).toBe(0)
@@ -774,8 +771,13 @@ it.live('a query Error passes through by reference with intents still pending',
774
771
  intents: [Added, Renamed],
775
772
  alwaysOn: true,
776
773
  }) {}
774
+ const attempts = { n: 0 }
777
775
  const ItemsLive = RemoteState.live(Items, {
778
- query: () => Effect.fail('boom').pipe(Effect.delay(Duration.millis(5))),
776
+ query: () =>
777
+ Effect.suspend(() => {
778
+ attempts.n += 1
779
+ return Effect.fail('boom')
780
+ }),
779
781
  send: (intent) => server.receive(intent, 10),
780
782
  apply: applyIntent,
781
783
  })
@@ -799,11 +801,15 @@ it.live('a query Error passes through by reference with intents still pending',
799
801
  () => queue.get(),
800
802
  (q) => q.length === 1 && q[0]?.status === 'confirmed',
801
803
  )
804
+ // Only a completed run could settle the intent — wait for the post-ack one to land.
805
+ yield* until(
806
+ () => attempts.n,
807
+ (n) => n >= 2,
808
+ )
802
809
  yield* until(
803
810
  () => store.get(),
804
811
  (v) => v._tag === 'Error' && v.refetching === false,
805
812
  )
806
- yield* tick(20)
807
813
  expect(queue.get()).toHaveLength(1)
808
814
  expect(store.get()).toBe(truth.get())
809
815
  }).pipe(Effect.provide(TestLayer))
@@ -1,417 +1,55 @@
1
- import {
2
- Context,
3
- Effect,
4
- Either,
5
- Equal,
6
- Iterable,
7
- Layer,
8
- MutableRef,
9
- Option,
10
- Runtime,
11
- Schema,
12
- } from 'effect'
13
- import type { Effect as EffectType } from 'effect/Effect'
14
- import {
15
- type AnyAsyncData,
16
- AsyncData,
17
- type AsyncError,
18
- type AsyncSuccess,
19
- narrowStore,
20
- widenStore,
21
- } from '../calc/asyncData'
1
+ import { Context, Effect, Iterable, Layer, MutableRef, Option, Runtime, Schema } from 'effect'
2
+ import { type AnyAsyncData, AsyncData, narrowStore, widenStore } from '../calc/asyncData'
22
3
  import * as Channel from '../channel/channel'
23
- import { type Manifest, yieldableClass } from '../definition/definition'
24
4
  import * as Event from '../event/event'
25
5
  import {
26
6
  bumpRevision,
27
- type GatedOf,
28
- gatedFlag,
29
7
  makeQueryDriver,
30
- type QueryCodec,
31
8
  RevisionSchema,
32
9
  revisionZero,
33
10
  } from '../internal/queryDriver'
34
- import { inspectable } from '../internal/inspect'
35
11
  import { reuse as shareStructure } from '../internal/reuse'
36
- import { resolveScheduler, type Scheduler } from '../internal/scheduler'
37
- import {
38
- type InputsObject,
39
- type InputStores,
40
- type InvalidateBy,
41
- sameKey,
42
- } from '../internal/sources'
43
- import { makeDerivedStore, type Store } from '../internal/store'
44
- import { readTracked } from '../internal/track'
12
+ import { resolveScheduler } from '../internal/scheduler'
13
+ import { type InputStores, sameKey } from '../internal/sources'
14
+ import { makeDerivedStore } from '../internal/store'
45
15
  import * as Reducer from '../reducer/reducer'
46
- import { Bus, narrowHandled, publish } from '../runtime/bus'
47
- import { type ReducerEntry, Reducers } from '../runtime/loop'
16
+ import { Bus, publish } from '../runtime/bus'
17
+ import { Reducers } from '../runtime/loop'
48
18
  import * as State from '../state/state'
49
- import { type AnySource, type SourceCapture, StateToken } from '../state/token'
50
-
51
- // An intent acked at generation g settles only on Success with generation > g.
52
- // `apply` must be idempotent while refetched truth and the pending intent overlap.
53
-
54
- export interface PendingIntent<I> {
55
- readonly opId: string
56
- readonly intent: I
57
- readonly status: 'sending' | 'confirmed'
58
- }
59
-
60
- export interface FailedIntent<I> {
61
- readonly intent: I
62
- readonly error: unknown
63
- }
64
-
65
- export type RemoteStateSettled<A, E> = AsyncSuccess<A> | AsyncError<E>
66
-
67
- export const RemoteStateVisibleStoreTypeId: unique symbol = Symbol.for(
68
- 'reform/RemoteStateVisibleStore',
69
- )
70
- export type RemoteStateVisibleStoreTypeId = typeof RemoteStateVisibleStoreTypeId
71
-
72
- export interface RemoteStateVisibleStore<N extends string> {
73
- readonly [RemoteStateVisibleStoreTypeId]: N
74
- }
75
-
76
- export const RemoteStateTruthStoreTypeId: unique symbol = Symbol.for('reform/RemoteStateTruthStore')
77
- export type RemoteStateTruthStoreTypeId = typeof RemoteStateTruthStoreTypeId
78
-
79
- export interface RemoteStateTruthStore<N extends string> {
80
- readonly [RemoteStateTruthStoreTypeId]: N
81
- }
82
-
83
- export const RemoteStatePendingStoreTypeId: unique symbol = Symbol.for(
84
- 'reform/RemoteStatePendingStore',
85
- )
86
- export type RemoteStatePendingStoreTypeId = typeof RemoteStatePendingStoreTypeId
87
-
88
- export interface RemoteStatePendingStore<N extends string> {
89
- readonly [RemoteStatePendingStoreTypeId]: N
90
- }
91
-
92
- type EventPayload<Ev extends Event.AnyEvent> =
93
- Ev extends Event.EventClass<string, infer P> ? P : never
94
-
95
- // ExternalApi: optional `intent` only on Queued envelopes.
96
- interface QueueFoldEventExternalApi<I> {
97
- readonly _tag: string
98
- readonly opId: string
99
- readonly intent?: I
100
- }
101
-
102
- export interface RemoteStateSchemaReflection {
103
- readonly ast: Schema.Schema<unknown, unknown, unknown>['ast']
104
- }
105
-
106
- export interface RemoteStateManifestExternalApi<N extends string> extends Manifest {
107
- readonly kind: 'RemoteState'
108
- readonly name: N
109
- readonly output: RemoteStateSchemaReflection
110
- readonly error?: RemoteStateSchemaReflection
111
- readonly gated: boolean
112
- readonly intents: ReadonlyArray<string>
113
- }
114
-
115
- export interface RemoteStateClass<
116
- N extends string,
117
- out Inputs extends ReadonlyArray<AnySource>,
118
- in out Intents extends ReadonlyArray<Event.AnyEvent>,
119
- in out A,
120
- in out E,
121
- in out Gated extends boolean,
122
- > extends EffectType<AsyncData<A, E, Gated>, never, RemoteStateVisibleStore<N>> {
123
- new (): {}
124
- readonly manifest: RemoteStateManifestExternalApi<N>
125
- readonly codec: QueryCodec<A>
126
- readonly store: Context.Tag<RemoteStateVisibleStore<N>, Store<AsyncData<A, E, Gated>>>
127
- readonly name: N
128
- readonly inputs: Inputs
129
- readonly intents: Intents
130
- readonly gated: Gated
131
- readonly capture: <Result>(visit: SourceCapture<Result>) => Result
132
- readonly truth: StateToken<`${N}/truth`, AsyncData<A, E, Gated>, RemoteStateTruthStore<N>>
133
- // pending is a Source, not a StateClass, so user reducers cannot target it.
134
- readonly pending: StateToken<
135
- `${N}/pending`,
136
- ReadonlyArray<PendingIntent<Event.EventType<Intents[number]>>>,
137
- RemoteStatePendingStore<N>
138
- >
139
- readonly Failed: Event.EventClass<`${N}/Failed`, FailedIntent<Event.EventType<Intents[number]>>>
140
- }
141
-
142
- export interface RemoteStateConfigExternalApi<
143
- Inputs extends ReadonlyArray<AnySource>,
144
- Intents extends ReadonlyArray<Event.AnyEvent>,
145
- OutputSchema extends Schema.Schema.AnyNoContext,
146
- ErrorSchema extends Schema.Schema.AnyNoContext | undefined,
147
- AlwaysOn extends boolean,
148
- > {
149
- readonly inputs: Inputs
150
- readonly output: OutputSchema
151
- readonly error?: ErrorSchema
152
- readonly alwaysOn?: AlwaysOn
153
- readonly intents: Intents
154
- }
155
-
156
- // Schema.Unknown is structural because Intents is open at make-time; the declaration preserves its authored payload type.
157
- const looseSchema = <P>(schema: Schema.Schema.AnyNoContext): Schema.Schema<P, P, never> =>
158
- Schema.declare((input): input is P => Schema.is(schema)(input))
159
-
160
- export const make = <
161
- const N extends string,
162
- const Inputs extends ReadonlyArray<AnySource>,
163
- const Intents extends ReadonlyArray<Event.AnyEvent>,
164
- OutputSchema extends Schema.Schema.AnyNoContext,
165
- ErrorSchema extends Schema.Schema.AnyNoContext | undefined = undefined,
166
- const AlwaysOn extends boolean = false,
167
- >(
168
- name: N,
169
- config: RemoteStateConfigExternalApi<Inputs, Intents, OutputSchema, ErrorSchema, AlwaysOn>,
170
- ): RemoteStateClass<
171
- N,
172
- Inputs,
173
- Intents,
174
- Schema.Schema.Type<OutputSchema>,
175
- ErrorSchema extends Schema.Schema.AnyNoContext ? Schema.Schema.Type<ErrorSchema> : never,
176
- GatedOf<AlwaysOn>
177
- > => {
178
- type A = Schema.Schema.Type<OutputSchema>
179
- type E = ErrorSchema extends Schema.Schema.AnyNoContext ? Schema.Schema.Type<ErrorSchema> : never
180
- type I = Event.EventType<Intents[number]>
181
- const store = Context.GenericTag<
182
- RemoteStateVisibleStore<N>,
183
- Store<AsyncData<A, E, GatedOf<AlwaysOn>>>
184
- >(`reform/remoteState/${name}`)
185
- const truthTag = Context.GenericTag<
186
- RemoteStateTruthStore<N>,
187
- Store<AsyncData<A, E, GatedOf<AlwaysOn>>>
188
- >(`reform/remoteState/${name}/truth`)
189
- const pendingTag = Context.GenericTag<
190
- RemoteStatePendingStore<N>,
191
- Store<ReadonlyArray<PendingIntent<I>>>
192
- >(`reform/remoteState/${name}/pending`)
193
- const gated = gatedFlag(config.alwaysOn)
194
- const manifest: RemoteStateManifestExternalApi<N> = {
195
- kind: 'RemoteState',
196
- name,
197
- output: config.output,
198
- gated,
199
- intents: config.intents.map((event) => event.tag),
200
- ...(config.error !== undefined ? { error: config.error } : {}),
201
- }
202
- const codec: QueryCodec<A> = {
203
- encode: Schema.encode(config.output),
204
- decode: Schema.decodeUnknown(config.output),
205
- }
206
- // Annotations preserve template-literal types for the public statics.
207
- const truthName: `${N}/truth` = `${name}/truth`
208
- const pendingName: `${N}/pending` = `${name}/pending`
209
- const failedName: `${N}/Failed` = `${name}/Failed`
210
- const read = Effect.flatMap(store, readTracked)
211
- const remote: RemoteStateClass<N, Inputs, Intents, A, E, GatedOf<AlwaysOn>> = yieldableClass(
212
- read,
213
- {
214
- manifest,
215
- codec,
216
- store,
217
- name,
218
- inputs: config.inputs,
219
- intents: config.intents,
220
- gated,
221
- capture: <Result>(visit: SourceCapture<Result>): Result => visit(remote),
222
- truth: new StateToken(truthName, truthTag),
223
- pending: new StateToken(pendingName, pendingTag),
224
- Failed: Event.make(
225
- failedName,
226
- looseSchema<FailedIntent<I>>(
227
- Schema.Struct({ intent: Schema.Unknown, error: Schema.Unknown }),
228
- ),
229
- ),
230
- },
231
- )
232
- return remote
233
- }
234
-
235
- export type RemoteStateLiveExternalApi<
236
- Inputs extends ReadonlyArray<AnySource>,
237
- Intents extends ReadonlyArray<Event.AnyEvent>,
238
- A,
239
- E,
240
- Gated extends boolean,
241
- R,
242
- R2,
243
- SettledEvent extends Event.AnyEvent,
244
- > = {
245
- readonly query: (inputs: InputsObject<Inputs>) => Effect.Effect<A, E, R>
246
- // Any failure settles and emits Failed; truth comes only from the invalidated query.
247
- readonly send: (intent: Event.EventType<Intents[number]>) => Effect.Effect<unknown, unknown, R2>
248
- readonly apply: (value: A, intent: Event.EventType<Intents[number]>) => A
249
- readonly channel?: Channel.ChannelClass
250
- readonly invalidateBy?: InvalidateBy<Inputs>
251
- readonly invalidateOn?: ReadonlyArray<Event.AnyEvent>
252
- readonly coalesce?: 'switch' | 'trailing'
253
- readonly reuse?: boolean
254
- readonly settled?: {
255
- readonly event: SettledEvent
256
- readonly payload: (result: RemoteStateSettled<A, E>) => EventPayload<SettledEvent>
257
- }
258
- // Only settled server truth is persisted; the optimistic overlay is a separate store.
259
- readonly persist?:
260
- | boolean
261
- | { readonly key?: string | ((inputs: InputsObject<Inputs>) => string) }
262
- } & (Gated extends true
263
- ? { readonly disabled?: (inputs: InputsObject<Inputs>) => boolean }
264
- : { readonly disabled?: never })
265
-
266
- interface SettleLink {
267
- readonly requested: () => number
268
- readonly register: (opId: string, waitFor: number) => void
269
- }
270
-
271
- interface WaiterRegistration {
272
- readonly opId: string
273
- readonly waitFor: number
274
- }
275
-
276
- interface PendingNode<I> {
277
- readonly entry: MutableRef.MutableRef<PendingIntent<I>>
278
- }
279
-
280
- // Duplicate opIds retain every node so indexed updates preserve the old map semantics.
281
- interface PendingOpEntry<I> {
282
- readonly nodes: Set<PendingNode<I>>
283
- }
284
-
285
- interface PendingQueue<I> {
286
- readonly store: Store<ReadonlyArray<PendingIntent<I>>>
287
- readonly revision: () => number
288
- readonly fold: <B>(initial: B, combine: (accumulator: B, entry: PendingIntent<I>) => B) => B
289
- readonly append: (entry: PendingIntent<I>) => void
290
- readonly ack: (opId: string) => void
291
- readonly settle: (opId: string) => void
292
- }
293
-
294
- const makePendingQueue = <I>(scheduler: Scheduler): PendingQueue<I> => {
295
- // Set insertion order preserves dispatch/apply order while allowing O(1) updates.
296
- const ordered = new Set<PendingNode<I>>()
297
- const byOpId = new Map<string, PendingOpEntry<I>>()
298
- const listeners = new Set<() => void>()
299
- const revision = MutableRef.make(0)
300
- const initial: ReadonlyArray<PendingIntent<I>> = []
301
- const snapshot = MutableRef.make<Option.Option<ReadonlyArray<PendingIntent<I>>>>(
302
- Option.some(initial),
303
- )
304
-
305
- const materialize = (): ReadonlyArray<PendingIntent<I>> => {
306
- const cached = MutableRef.get(snapshot)
307
- if (Option.isSome(cached)) {
308
- return cached.value
309
- }
310
- const current = Array.from(ordered, (node) => MutableRef.get(node.entry))
311
- MutableRef.set(snapshot, Option.some(current))
312
- return current
313
- }
314
-
315
- const announce = (next: Option.Option<ReadonlyArray<PendingIntent<I>>>): void => {
316
- MutableRef.set(snapshot, next)
317
- MutableRef.update(revision, (current) => current + 1)
318
- scheduler.schedule(listeners)
319
- }
320
-
321
- const link = (entry: PendingIntent<I>): void => {
322
- const node: PendingNode<I> = {
323
- entry: MutableRef.make(entry),
324
- }
325
- ordered.add(node)
326
- const indexed = byOpId.get(entry.opId)
327
- if (indexed === undefined) {
328
- byOpId.set(entry.opId, { nodes: new Set([node]) })
329
- } else {
330
- indexed.nodes.add(node)
331
- }
332
- }
333
-
334
- const clear = (): void => {
335
- ordered.clear()
336
- byOpId.clear()
337
- }
338
-
339
- const fold = <B>(initialValue: B, combine: (accumulator: B, entry: PendingIntent<I>) => B): B => {
340
- const folded = MutableRef.make(initialValue)
341
- ordered.forEach((node) => {
342
- MutableRef.update(folded, (accumulator) => combine(accumulator, MutableRef.get(node.entry)))
343
- })
344
- return MutableRef.get(folded)
345
- }
346
-
347
- const append = (entry: PendingIntent<I>): void => {
348
- link(entry)
349
- announce(Option.none())
350
- }
351
-
352
- const ack = (opId: string): void => {
353
- const indexed = byOpId.get(opId)
354
- if (indexed === undefined) {
355
- return
356
- }
357
- const stillSending = MutableRef.make(false)
358
- indexed.nodes.forEach((node) => {
359
- if (MutableRef.get(node.entry).status === 'sending') {
360
- MutableRef.set(stillSending, true)
361
- }
362
- })
363
- if (!MutableRef.get(stillSending)) {
364
- return
365
- }
366
- // Rebuild every duplicate once one is sending, matching the former array map.
367
- indexed.nodes.forEach((node) => {
368
- MutableRef.update(node.entry, (entry) => ({ ...entry, status: 'confirmed' as const }))
369
- })
370
- announce(Option.none())
371
- }
372
-
373
- const settle = (opId: string): void => {
374
- const indexed = byOpId.get(opId)
375
- if (indexed === undefined) {
376
- return
377
- }
378
- indexed.nodes.forEach((node) => {
379
- ordered.delete(node)
380
- })
381
- byOpId.delete(opId)
382
- announce(Option.none())
383
- }
384
-
385
- const store: Store<ReadonlyArray<PendingIntent<I>>> = {
386
- get: materialize,
387
- getSnapshot: materialize,
388
- getVersion: () => MutableRef.get(revision),
389
- set: (next) => {
390
- if (Equal.equals(materialize(), next)) {
391
- return
392
- }
393
- clear()
394
- next.forEach(link)
395
- announce(Option.some(next))
396
- },
397
- subscribe: (listener) => {
398
- listeners.add(listener)
399
- return () => {
400
- listeners.delete(listener)
401
- }
402
- },
403
- ...inspectable(() => ({ _id: 'reform/Store', value: materialize() })),
404
- }
405
-
406
- return {
407
- store,
408
- revision: () => MutableRef.get(revision),
409
- fold,
410
- append,
411
- ack,
412
- settle,
413
- }
414
- }
19
+ import { type AnySource } from '../state/token'
20
+ import { makePendingQueue, type PendingQueue } from './pendingQueue'
21
+ import { looseSchema } from './remoteStateMake'
22
+ import { queueReducerLayer, sendProcedureLayer } from './remoteStateSend'
23
+ import type {
24
+ RemoteStateClass,
25
+ RemoteStateLiveExternalApi,
26
+ RemoteStatePendingStore,
27
+ RemoteStateSettled,
28
+ RemoteStateTruthStore,
29
+ RemoteStateVisibleStore,
30
+ SettleLink,
31
+ WaiterRegistration,
32
+ } from './remoteStateTypes'
33
+
34
+ export { make } from './remoteStateMake'
35
+ export {
36
+ RemoteStatePendingStoreTypeId,
37
+ RemoteStateTruthStoreTypeId,
38
+ RemoteStateVisibleStoreTypeId,
39
+ } from './remoteStateTypes'
40
+ export type {
41
+ FailedIntent,
42
+ PendingIntent,
43
+ RemoteStateClass,
44
+ RemoteStateConfigExternalApi,
45
+ RemoteStateLiveExternalApi,
46
+ RemoteStateManifestExternalApi,
47
+ RemoteStatePendingStore,
48
+ RemoteStateSchemaReflection,
49
+ RemoteStateSettled,
50
+ RemoteStateTruthStore,
51
+ RemoteStateVisibleStore,
52
+ } from './remoteStateTypes'
415
53
 
416
54
  export const live = <
417
55
  N extends string,
@@ -604,91 +242,27 @@ export const live = <
604
242
  }),
605
243
  )
606
244
 
607
- // Direct registry assembly avoids deferred conditional parameters while Intents is generic.
608
- const queueReducerName = `${name}/pending`
609
- const queueHandles: ReadonlySet<string> = new Set([Queued.tag, Acked.tag, Settled.tag])
610
- const queueReducerLayer = Layer.scopedDiscard(
611
- Effect.gen(function* () {
612
- const reducers = yield* Reducers
613
- if (reducers.entries.some((entry) => entry.name === queueReducerName)) {
614
- yield* Effect.logWarning(`reform: duplicate reducer name '${queueReducerName}' registered`)
615
- }
616
- const queue = yield* pendingQueueTag
617
- const entry: ReducerEntry = {
618
- name: queueReducerName,
619
- handles: queueHandles,
620
- apply: (event) => {
621
- const handled = narrowHandled<QueueFoldEventExternalApi<I>>(event)
622
- if (handled._tag === Queued.tag) {
623
- if (handled.intent !== undefined) {
624
- queue.append({
625
- opId: handled.opId,
626
- intent: handled.intent,
627
- status: 'sending',
628
- })
629
- }
630
- return
631
- }
632
- if (handled._tag === Acked.tag) {
633
- queue.ack(handled.opId)
634
- return
635
- }
636
- queue.settle(handled.opId)
637
- },
638
- }
639
- yield* Effect.acquireRelease(
640
- Effect.sync(() => reducers.register(entry)),
641
- () => Effect.sync(() => reducers.unregister(entry)),
642
- )
643
- }),
644
- )
645
245
  const channel = config.channel ?? Channel.make(`${name}/sends`, { policy: { _tag: 'merge' } })
646
- const procedureName = `${name}/send`
647
- const sendHandles: ReadonlySet<string> = new Set(remote.intents.map((event) => event.tag))
648
- const sendProcedureLayer = Layer.scopedDiscard(
649
- Effect.gen(function* () {
650
- const procedures = yield* Channel.Procedures
651
- if (procedures.entries.some((entry) => entry.name === procedureName)) {
652
- yield* Effect.logWarning(`reform: duplicate procedure name '${procedureName}' registered`)
653
- }
654
- const link = yield* linkTag
655
- const runtime = yield* Effect.runtime<Bus | R2>()
656
- const deliver = Effect.fn('deliver')(function* (
657
- intent: I,
658
- ): Effect.fn.Return<void, never, Bus | R2> {
659
- const opId = yield* Effect.sync(() => crypto.randomUUID())
660
- yield* Event.dispatch(Queued, { opId, intent })
661
- const outcome = yield* config.send(intent).pipe(
662
- Effect.catchAllDefect((defect) => Effect.fail(defect)),
663
- Effect.either,
664
- Effect.onInterrupt(() => Event.dispatch(Settled, { opId })),
665
- )
666
- yield* Either.match(outcome, {
667
- onLeft: (error) =>
668
- Effect.zipRight(
669
- Event.dispatch(remote.Failed, { intent, error }),
670
- Event.dispatch(Settled, { opId }),
671
- ),
672
- onRight: () =>
673
- // Register before invalidation so a pre-ack refetch cannot settle this intent.
674
- Effect.sync(() => link.register(opId, link.requested() + 1)).pipe(
675
- Effect.zipRight(Event.dispatch(Acked, { opId })),
676
- Effect.zipRight(Event.dispatch(Invalidated, {})),
677
- ),
678
- })
679
- })
680
- const entry: Channel.ProcedureEntry = {
681
- name: procedureName,
682
- channelName: channel.manifest.name,
683
- handles: sendHandles,
684
- run: (event) => Effect.provide(deliver(narrowHandled<I>(event)), runtime),
685
- }
686
- yield* Effect.acquireRelease(
687
- Effect.sync(() => procedures.register(entry)),
688
- () => Effect.sync(() => procedures.unregister(entry)),
689
- )
690
- }),
691
- )
246
+ const intentEvents = {
247
+ queued: Queued,
248
+ acked: Acked,
249
+ settled: Settled,
250
+ invalidated: Invalidated,
251
+ }
252
+ const pendingReducer = queueReducerLayer({
253
+ name: `${name}/pending`,
254
+ queueTag: pendingQueueTag,
255
+ events: intentEvents,
256
+ })
257
+ const sendProcedure = sendProcedureLayer({
258
+ name: `${name}/send`,
259
+ channel,
260
+ handles: new Set(remote.intents.map((event) => event.tag)),
261
+ linkTag,
262
+ failed: remote.Failed,
263
+ send: config.send,
264
+ events: intentEvents,
265
+ })
692
266
 
693
267
  const pendingStoreLayer = Layer.scopedContext(
694
268
  Effect.map(resolveScheduler, (scheduler) => {
@@ -698,9 +272,9 @@ export const live = <
698
272
  )
699
273
  return Layer.mergeAll(
700
274
  overlayLayer,
701
- queueReducerLayer,
275
+ pendingReducer,
702
276
  Reducer.live(revisionReducer, bumpRevision),
703
- sendProcedureLayer,
277
+ sendProcedure,
704
278
  Channel.live(channel),
705
279
  ).pipe(
706
280
  Layer.provideMerge(driverLayer),