@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,539 @@
1
+ import { Context, Deferred, Effect, Function as Fn, Layer, Option, Schema as S } from 'effect'
2
+ import { expect, it } from '@effect/vitest'
3
+ import * as Calc from '../calc/calc'
4
+ import * as Composition from '../compose/composition'
5
+ import { provide } from '../compose/provide'
6
+ import { mount } from '../compose/structure'
7
+ import * as Ui from '../compose/ui'
8
+ import { ui } from '../compose/ui'
9
+ import * as Event from '../event/event'
10
+ import * as Feature from '../feature/feature'
11
+ import { featureModule } from '../feature/feature'
12
+ import * as Reducer from '../reducer/reducer'
13
+ import { profiledScene, scene } from '../scene/scene'
14
+ import * as State from '../state/state'
15
+ import { flush, until } from '../testkit/flight.testkit'
16
+ import { Engine } from './loop'
17
+ import { makeAppRuntime } from './appRuntime'
18
+ import type { Instrumentation } from './instrumentation'
19
+ import type { RuntimeHandle } from './runtimeHandle'
20
+
21
+ // ===========================================================================
22
+ // A hunt over the mount/teardown lifecycle of `makeAppRuntime`.
23
+ //
24
+ // Two defects, each with a PASSING control in the same test:
25
+ //
26
+ // 1. A feature mounted through a `RuntimeHandle` gets a standalone `Scope`
27
+ // that nothing owns. `mountScoped` registers its disposer in
28
+ // `activationDisposers` and `app.dispose()` runs them; the three feature
29
+ // mount paths register nothing, so the runtime (and a parent feature) tear
30
+ // down while every feature mounted on them keeps its finalizers unrun.
31
+ //
32
+ // 2. `profiledScene` publishes its `Instrumentation` through the
33
+ // `CurrentInstrumentation` FiberRef, set by `Layer.locally` over the scene's
34
+ // layers. A feature layer is built later, on a fiber forked from the
35
+ // ManagedRuntime, which never carried that local — so a mounted feature's
36
+ // state/calc spans are credited to the no-op sink while the very same
37
+ // frame's reducer spans (recorded by the scene-level drain loop) land.
38
+ // ===========================================================================
39
+
40
+ // 200 rounds x 10ms is `until`'s default; a shorter budget keeps a failing arm
41
+ // quick while staying far above the microtask cost of a real release.
42
+ const settledCount = (read: () => number, count: number): Effect.Effect<number> =>
43
+ until(read, (seen) => seen >= count, 40)
44
+
45
+ // ---------------------------------------------------------------------------
46
+ // Shared definitions
47
+ // ---------------------------------------------------------------------------
48
+
49
+ interface LifecycleProbeService {
50
+ readonly acquired: Array<string>
51
+ readonly released: Array<string>
52
+ }
53
+
54
+ const LifecycleProbeBase: Context.TagClass<
55
+ LifecycleProbe,
56
+ 'hunt.LifecycleProbe',
57
+ LifecycleProbeService
58
+ > = Context.Tag('hunt.LifecycleProbe')<LifecycleProbe, LifecycleProbeService>()
59
+ class LifecycleProbe extends LifecycleProbeBase {}
60
+
61
+ const makeProbe = (): LifecycleProbeService => ({ acquired: [], released: [] })
62
+
63
+ interface ParentValueService {
64
+ readonly label: string
65
+ }
66
+
67
+ const ParentValueBase: Context.TagClass<ParentValue, 'hunt.ParentValue', ParentValueService> =
68
+ Context.Tag('hunt.ParentValue')<ParentValue, ParentValueService>()
69
+ class ParentValue extends ParentValueBase {}
70
+
71
+ class HuntUi extends ui('hunt.HuntUi')<{ props: {} }>() {}
72
+ class HuntComp extends Composition.make('hunt.HuntComp', {
73
+ title: 'Hunt',
74
+ ui: HuntUi,
75
+ })<HuntComp>() {}
76
+ const HuntLive = Composition.live(HuntComp, function* () {
77
+ return mount({ props: {}, slots: {} })
78
+ })
79
+
80
+ class RootUi extends ui('hunt.RootUi')<{ props: {} }>() {}
81
+ class RootComp extends Composition.make('hunt.RootComp', {
82
+ title: 'Root',
83
+ ui: RootUi,
84
+ })<RootComp>() {}
85
+ const RootLive = Composition.live(RootComp, function* () {
86
+ return mount({ props: {}, slots: {} })
87
+ })
88
+
89
+ const rootViews = RootLive.pipe(
90
+ Layer.provideMerge(
91
+ provide(
92
+ RootUi,
93
+ Ui.make(RootUi, () => null),
94
+ ),
95
+ ),
96
+ )
97
+
98
+ // A scoped marker: a mount whose scope is never closed leaves no `released` entry.
99
+ const scopedMark = (label: string): Layer.Layer<never, never, LifecycleProbe> =>
100
+ Layer.scopedDiscard(
101
+ Effect.gen(function* () {
102
+ const probe = yield* LifecycleProbe
103
+ yield* Effect.acquireRelease(
104
+ Effect.sync(() => probe.acquired.push(label)),
105
+ () => Effect.sync(() => probe.released.push(label)),
106
+ )
107
+ }),
108
+ )
109
+
110
+ // ---------------------------------------------------------------------------
111
+ // 1a. `app.dispose()` and the feature mounted on it
112
+ // ---------------------------------------------------------------------------
113
+
114
+ class LeakFeature extends Feature.make('hunt.leak', {
115
+ composition: HuntComp,
116
+ module: featureModule([LifecycleProbe], Layer.merge(HuntLive, scopedMark('leak'))),
117
+ }) {}
118
+
119
+ class ControlFeature extends Feature.make('hunt.control', {
120
+ composition: HuntComp,
121
+ module: featureModule([LifecycleProbe], Layer.merge(HuntLive, scopedMark('control'))),
122
+ }) {}
123
+
124
+ const LeakNative = Feature.tree<typeof LeakFeature>(LeakFeature.token)
125
+ const ControlNative = Feature.tree<typeof ControlFeature>(ControlFeature.token)
126
+
127
+ const probeScene = (probe: LifecycleProbeService) =>
128
+ scene(RootComp, {
129
+ provide: [
130
+ rootViews.pipe(
131
+ Layer.provideMerge(Layer.succeed(LifecycleProbe, probe)),
132
+ Layer.provideMerge(Engine),
133
+ ),
134
+ ],
135
+ })
136
+
137
+ it.scopedLive('app.dispose() closes the scope of a feature mounted on that runtime', () =>
138
+ Effect.gen(function* () {
139
+ const probe = makeProbe()
140
+ const app = makeAppRuntime(probeScene(probe))
141
+ yield* Effect.addFinalizer(() => Effect.sync(() => app.dispose()))
142
+
143
+ const seen = { leak: 0, control: 0, failed: 0 }
144
+ app.mountFeature(LeakNative.binding, {
145
+ onLive: () => {
146
+ seen.leak += 1
147
+ },
148
+ onFailed: () => {
149
+ seen.failed += 1
150
+ },
151
+ })
152
+ const disposeControl = app.mountFeature(ControlNative.binding, {
153
+ onLive: () => {
154
+ seen.control += 1
155
+ },
156
+ onFailed: () => {
157
+ seen.failed += 1
158
+ },
159
+ })
160
+
161
+ yield* settledCount(() => seen.leak + seen.control, 2)
162
+ expect(seen).toEqual({ leak: 1, control: 1, failed: 0 })
163
+ expect(probe.acquired.slice().sort()).toEqual(['control', 'leak'])
164
+
165
+ // CONTROL — same mount path, same scope shape: the mount's OWN disposer
166
+ // closes its scope and the release lands.
167
+ disposeControl()
168
+ yield* settledCount(() => probe.released.length, 1)
169
+ expect(probe.released).toEqual(['control'])
170
+
171
+ // The runtime is torn down while `leak` is still mounted. Its scope was
172
+ // created and forked by this runtime, so disposing the runtime must close it
173
+ // — `mountScoped`'s disposers are collected and run for exactly that reason.
174
+ app.dispose()
175
+ yield* settledCount(() => probe.released.length, 2)
176
+ yield* flush()
177
+ expect(probe.released.slice().sort()).toEqual(['control', 'leak'])
178
+ }),
179
+ )
180
+
181
+ // ---------------------------------------------------------------------------
182
+ // 1b. A nested feature outlives the parent it was built against
183
+ // ---------------------------------------------------------------------------
184
+
185
+ class NestParentFeature extends Feature.make('hunt.nestParent', {
186
+ composition: HuntComp,
187
+ module: featureModule(
188
+ [LifecycleProbe],
189
+ Layer.mergeAll(
190
+ HuntLive,
191
+ Layer.succeed(ParentValue, { label: 'parent' }),
192
+ scopedMark('nest-parent'),
193
+ ),
194
+ ),
195
+ }) {}
196
+
197
+ class NestChildFeature extends Feature.make('hunt.nestChild', {
198
+ composition: HuntComp,
199
+ module: featureModule(
200
+ [LifecycleProbe, ParentValue],
201
+ Layer.merge(
202
+ HuntLive,
203
+ Layer.scopedDiscard(
204
+ Effect.gen(function* () {
205
+ const probe = yield* LifecycleProbe
206
+ const parent = yield* ParentValue
207
+ yield* Effect.acquireRelease(
208
+ Effect.sync(() => probe.acquired.push(`nest-child-of-${parent.label}`)),
209
+ () => Effect.sync(() => probe.released.push(`nest-child-of-${parent.label}`)),
210
+ )
211
+ }),
212
+ ),
213
+ ),
214
+ ),
215
+ }) {}
216
+
217
+ const NestParentNative = Feature.tree<typeof NestParentFeature>(NestParentFeature.token)
218
+ const NestChildNative = Feature.tree<typeof NestChildFeature>(NestChildFeature.token)
219
+
220
+ it.scopedLive('disposing a parent feature closes the child mounted through its handle', () =>
221
+ Effect.gen(function* () {
222
+ const probe = makeProbe()
223
+ const app = makeAppRuntime(probeScene(probe))
224
+ yield* Effect.addFinalizer(() => Effect.sync(() => app.dispose()))
225
+
226
+ const failures: Array<string> = []
227
+ const disposeParent = app.mountFeature(NestParentNative.binding, {
228
+ onFailed: () => failures.push('parent'),
229
+ onLive: (parent) => {
230
+ // The child is mounted through the parent's handle and its layer reads a
231
+ // service the parent's scope owns, so it is built against the parent.
232
+ parent.mountFeature(NestChildNative.binding, {
233
+ onFailed: () => failures.push('child'),
234
+ onLive: () => undefined,
235
+ })
236
+ },
237
+ })
238
+
239
+ yield* settledCount(() => probe.acquired.length, 2)
240
+ expect(failures).toEqual([])
241
+ expect(probe.acquired.slice().sort()).toEqual(['nest-child-of-parent', 'nest-parent'])
242
+
243
+ disposeParent()
244
+
245
+ // CONTROL — the parent's own disposer closes the parent's scope.
246
+ yield* until(
247
+ () => probe.released.includes('nest-parent'),
248
+ (found) => found,
249
+ 40,
250
+ )
251
+ expect(probe.released).toContain('nest-parent')
252
+
253
+ // The child's layer was built against services the parent's scope has now
254
+ // finalized. It must not survive the parent it was mounted on.
255
+ yield* settledCount(() => probe.released.length, 2)
256
+ yield* flush()
257
+ expect(probe.released.slice().sort()).toEqual(['nest-child-of-parent', 'nest-parent'])
258
+ }),
259
+ )
260
+
261
+ // ---------------------------------------------------------------------------
262
+ // 2. A runtime-mounted feature escapes the scene's instrumentation
263
+ // ---------------------------------------------------------------------------
264
+
265
+ interface Recorder {
266
+ readonly instrumentation: Instrumentation
267
+ readonly reducers: Array<string>
268
+ readonly states: Array<string>
269
+ readonly calcs: Array<string>
270
+ }
271
+
272
+ const makeRecorder = (): Recorder => {
273
+ const reducers: Array<string> = []
274
+ const states: Array<string> = []
275
+ const calcs: Array<string> = []
276
+ return {
277
+ reducers,
278
+ states,
279
+ calcs,
280
+ instrumentation: {
281
+ eventDispatched: () => {},
282
+ reducerRun: (name) => {
283
+ reducers.push(name)
284
+ return () => {}
285
+ },
286
+ stateUpdated: (name) => {
287
+ states.push(name)
288
+ },
289
+ calcRecomputed: (name) => {
290
+ calcs.push(name)
291
+ return () => {}
292
+ },
293
+ queryRun: () => () => {},
294
+ procedureRun: () => () => {},
295
+ uiRendered: () => () => {},
296
+ frame: () => () => {},
297
+ },
298
+ }
299
+ }
300
+
301
+ class HuntBumped extends Event.make('hunt.Bumped', S.Struct({})) {}
302
+
303
+ class HuntSceneCount extends State.make('huntSceneCount', S.Number) {}
304
+ class HuntSceneFold extends Reducer.make('hunt.SceneFold', {
305
+ states: [HuntSceneCount],
306
+ events: [HuntBumped],
307
+ }) {}
308
+ class HuntSceneDouble extends Calc.make('huntSceneDouble', {
309
+ inputs: [HuntSceneCount],
310
+ output: S.Number,
311
+ }) {}
312
+
313
+ class HuntFeatCount extends State.make('huntFeatCount', S.Number) {}
314
+ class HuntFeatFold extends Reducer.make('hunt.FeatFold', {
315
+ states: [HuntFeatCount],
316
+ events: [HuntBumped],
317
+ }) {}
318
+ class HuntFeatDouble extends Calc.make('huntFeatDouble', {
319
+ inputs: [HuntFeatCount],
320
+ output: S.Number,
321
+ }) {}
322
+
323
+ class ProfiledFeature extends Feature.make('hunt.profiled', {
324
+ composition: HuntComp,
325
+ module: featureModule(
326
+ [],
327
+ Layer.merge(
328
+ HuntLive,
329
+ Calc.live(HuntFeatDouble, ({ huntFeatCount }) => huntFeatCount * 2).pipe(
330
+ Layer.provideMerge(Reducer.live(HuntFeatFold, (n) => n + 1)),
331
+ Layer.provideMerge(State.live(HuntFeatCount, 0)),
332
+ ),
333
+ ),
334
+ ),
335
+ }) {}
336
+
337
+ const ProfiledNative = Feature.tree<typeof ProfiledFeature>(ProfiledFeature.token)
338
+
339
+ const profiledBase = scene(RootComp, {
340
+ provide: [
341
+ rootViews.pipe(
342
+ Layer.provideMerge(
343
+ Calc.live(HuntSceneDouble, ({ huntSceneCount }) => huntSceneCount * 2).pipe(
344
+ Layer.provideMerge(Reducer.live(HuntSceneFold, (n) => n + 1)),
345
+ Layer.provideMerge(State.live(HuntSceneCount, 0)),
346
+ ),
347
+ ),
348
+ Layer.provideMerge(Engine),
349
+ ),
350
+ ],
351
+ })
352
+
353
+ it.scopedLive("a profiled scene records a runtime-mounted feature's state and calc spans", () =>
354
+ Effect.gen(function* () {
355
+ const recorder = makeRecorder()
356
+ const app = makeAppRuntime(profiledScene(profiledBase, recorder.instrumentation))
357
+ yield* Effect.addFinalizer(() => Effect.sync(() => app.dispose()))
358
+
359
+ const seen = { live: 0, failed: 0 }
360
+ app.mountFeature(ProfiledNative.binding, {
361
+ onLive: () => {
362
+ seen.live += 1
363
+ },
364
+ onFailed: () => {
365
+ seen.failed += 1
366
+ },
367
+ })
368
+ yield* settledCount(() => seen.live + seen.failed, 1)
369
+ expect(seen).toEqual({ live: 1, failed: 0 })
370
+
371
+ // Both derived stores exist before the event, so both are subscribed to it.
372
+ const sceneDouble = app.read(HuntSceneDouble.store)
373
+ expect(sceneDouble.get()).toBe(0)
374
+
375
+ // ONE event, folded by BOTH reducers in the SAME frame of the SAME loop.
376
+ app.dispatch('High', Event.construct(HuntBumped, {}))
377
+ yield* settledCount(() => recorder.reducers.length, 2)
378
+ yield* until(
379
+ () => sceneDouble.get(),
380
+ (value) => value === 2,
381
+ 40,
382
+ )
383
+ yield* flush()
384
+
385
+ // CONTROL — the recorder is live and the feature really folded on this
386
+ // runtime: the drain loop (a scene-level layer) reports BOTH reducer spans.
387
+ expect(recorder.reducers.slice().sort()).toEqual(['hunt.FeatFold', 'hunt.SceneFold'])
388
+ // CONTROL — state and calc spans are reported at all, for the scene's slices.
389
+ expect(recorder.states).toContain('huntSceneCount')
390
+ expect(recorder.calcs).toContain('huntSceneDouble')
391
+
392
+ // The feature's fold provably wrote its state and moved its calc in that same
393
+ // frame, but neither span reaches the scene's instrumentation.
394
+ expect(recorder.states).toContain('huntFeatCount')
395
+ expect(recorder.calcs).toContain('huntFeatDouble')
396
+ }),
397
+ )
398
+
399
+ // ---------------------------------------------------------------------------
400
+ // 3. A composition activation still building when its owner feature is disposed
401
+ //
402
+ // `mountActivation` forks the activation's scope from the owner and then builds it
403
+ // on a deferred `runFork`. Nothing registers that fiber with the scope, so an owner
404
+ // that closes after the fork does not stop the build — it acquires against an
405
+ // already-closed scope, releasing at the instant of acquisition, and the host is
406
+ // handed an activation whose services are gone. Same shape as `forkFeatureMount`.
407
+ // ---------------------------------------------------------------------------
408
+
409
+ interface ActivationGateService {
410
+ readonly open: Deferred.Deferred<void>
411
+ }
412
+
413
+ const ActivationGateBase: Context.TagClass<
414
+ ActivationGate,
415
+ 'hunt.ActivationGate',
416
+ ActivationGateService
417
+ > = Context.Tag('hunt.ActivationGate')<ActivationGate, ActivationGateService>()
418
+ class ActivationGate extends ActivationGateBase {}
419
+
420
+ class ParkedUi extends ui('hunt.ParkedUi')<{ props: {} }>() {}
421
+ class ParkedComp extends Composition.make('hunt.ParkedComp', {
422
+ title: 'Parked',
423
+ ui: ParkedUi,
424
+ })<ParkedComp>() {}
425
+
426
+ const ParkedLive = Composition.liveScoped(ParkedComp, {
427
+ render: function* () {
428
+ return mount({ props: {}, slots: {} })
429
+ },
430
+ activate: function* () {
431
+ const gate = yield* ActivationGate
432
+ const probe = yield* LifecycleProbe
433
+ // Parked before anything is acquired: a lazy import, a socket, a config read.
434
+ yield* Deferred.await(gate.open)
435
+ yield* Effect.acquireRelease(
436
+ Effect.sync(() => probe.acquired.push('parked-activation')),
437
+ () => Effect.sync(() => probe.released.push('parked-activation')),
438
+ )
439
+ },
440
+ })
441
+
442
+ class ActivationHostFeature extends Feature.make('hunt.activationHost', {
443
+ composition: HuntComp,
444
+ module: featureModule(
445
+ [LifecycleProbe, ActivationGate],
446
+ Layer.merge(
447
+ HuntLive,
448
+ ParkedLive.pipe(
449
+ Layer.provideMerge(
450
+ provide(
451
+ ParkedUi,
452
+ Ui.make(ParkedUi, () => null),
453
+ ),
454
+ ),
455
+ ),
456
+ ),
457
+ ),
458
+ }) {}
459
+
460
+ const ActivationHostNative = Feature.tree<typeof ActivationHostFeature>(ActivationHostFeature.token)
461
+
462
+ // `RuntimeHandle<never>.read` cannot be spelled for a tag the erased handle does not
463
+ // carry, so the read is coerced once, here, rather than at every call site.
464
+ type ParkedService = Composition.CompositionService<{}, Ui.UiContract>
465
+ const readParked = (handle: RuntimeHandle<never>): ParkedService => {
466
+ const read: (tag: typeof ParkedComp.tag) => ParkedService = Fn.unsafeCoerce(handle.read)
467
+ return read(ParkedComp.tag)
468
+ }
469
+
470
+ const gateScene = (probe: LifecycleProbeService, gate: ActivationGateService) =>
471
+ scene(RootComp, {
472
+ provide: [
473
+ rootViews.pipe(
474
+ Layer.provideMerge(Layer.succeed(LifecycleProbe, probe)),
475
+ Layer.provideMerge(Layer.succeed(ActivationGate, gate)),
476
+ Layer.provideMerge(Engine),
477
+ ),
478
+ ],
479
+ })
480
+
481
+ it.scopedLive('an activation still building when its owner feature is disposed is stopped', () =>
482
+ Effect.gen(function* () {
483
+ const probe = makeProbe()
484
+ const gate = { open: yield* Deferred.make<void>() }
485
+ const app = makeAppRuntime(gateScene(probe, gate))
486
+ yield* Effect.addFinalizer(() => Effect.sync(() => app.dispose()))
487
+
488
+ const mounted = { handle: Option.none<RuntimeHandle<never>>() }
489
+ const disposeHost = app.mountFeature(ActivationHostNative.binding, {
490
+ onFailed: () => undefined,
491
+ onLive: (child) => {
492
+ mounted.handle = Option.some(Fn.unsafeCoerce(child))
493
+ },
494
+ })
495
+ yield* until(
496
+ () => Option.isSome(mounted.handle),
497
+ (ready) => ready,
498
+ 40,
499
+ )
500
+ const child: RuntimeHandle<never> = Option.getOrThrow(mounted.handle)
501
+
502
+ // CONTROL — the same activation, on a live owner, acquires once the gate opens.
503
+ const controlGate = { open: yield* Deferred.make<void>() }
504
+ const controlProbe = makeProbe()
505
+ const controlApp = makeAppRuntime(gateScene(controlProbe, controlGate))
506
+ yield* Effect.addFinalizer(() => Effect.sync(() => controlApp.dispose()))
507
+ const controlMounted = { handle: Option.none<RuntimeHandle<never>>() }
508
+ controlApp.mountFeature(ActivationHostNative.binding, {
509
+ onFailed: () => undefined,
510
+ onLive: (childRuntime) => {
511
+ controlMounted.handle = Option.some(Fn.unsafeCoerce(childRuntime))
512
+ },
513
+ })
514
+ yield* until(
515
+ () => Option.isSome(controlMounted.handle),
516
+ (ready) => ready,
517
+ 40,
518
+ )
519
+ const controlChild: RuntimeHandle<never> = Option.getOrThrow(controlMounted.handle)
520
+ controlChild.mountScoped(
521
+ Option.getOrThrow(Composition.activation(readParked(controlChild), {})),
522
+ )
523
+ yield* Deferred.succeed(controlGate.open, undefined)
524
+ yield* settledCount(() => controlProbe.acquired.length, 1)
525
+ expect(controlProbe.acquired).toEqual(['parked-activation'])
526
+ expect(controlProbe.released).toEqual([])
527
+
528
+ // SUBJECT — mount the parked activation, dispose the owner feature while it is
529
+ // still in flight, then let it finish.
530
+ child.mountScoped(Option.getOrThrow(Composition.activation(readParked(child), {})))
531
+ disposeHost()
532
+ yield* flush()
533
+ yield* Deferred.succeed(gate.open, undefined)
534
+ yield* flush()
535
+ yield* flush()
536
+
537
+ expect(probe.acquired).toEqual([])
538
+ }).pipe(Effect.provide(Layer.empty)),
539
+ )
@@ -1,6 +1,17 @@
1
- import { Cause, Context, Effect, Exit, Fiber, Option, Scope } from 'effect'
1
+ import {
2
+ Cause,
3
+ Context,
4
+ Effect,
5
+ ExecutionStrategy,
6
+ Exit,
7
+ Fiber,
8
+ MutableRef,
9
+ Option,
10
+ Scope,
11
+ } from 'effect'
2
12
  import type { FeatureRequirement } from '../feature/feature'
3
13
  import { FeatureLoadFailed } from '../internal/errors'
14
+ import { CurrentInstrumentation, type Instrumentation } from './instrumentation'
4
15
  import { EngineRequirements, type EngineServices } from './loop'
5
16
 
6
17
  const isObjectLike = (candidate: unknown): candidate is object =>
@@ -48,11 +59,101 @@ export const hasFeatureRequirements = <
48
59
  export const featureMountFailure = (message: string): FeatureLoadFailed =>
49
60
  new FeatureLoadFailed({ cause: message })
50
61
 
62
+ export const missingHostServices = (name: string): Effect.Effect<never, FeatureLoadFailed> =>
63
+ Effect.fail(
64
+ featureMountFailure(`Feature ${name} requires services missing from the host runtime`),
65
+ )
66
+
67
+ export const invalidMountProps = (name: string): Effect.Effect<never, FeatureLoadFailed> =>
68
+ Effect.fail(featureMountFailure(`Feature ${name} received invalid props`))
69
+
70
+ /**
71
+ * `ManagedRuntime.make` captures its runtime outside `profiledScene`'s
72
+ * `Layer.locally`, so anything forked onto it later would build against the no-op
73
+ * sink and its state, calc, query and render spans would vanish from the report.
74
+ */
75
+ export const withInstrumentation =
76
+ (instrumentation: Instrumentation) =>
77
+ <A, E, R>(effect: Effect.Effect<A, E, R>): Effect.Effect<A, E, R> =>
78
+ Effect.locally(effect, CurrentInstrumentation, instrumentation)
79
+
51
80
  export interface FeatureMountHost {
52
- readonly runFork: (effect: Effect.Effect<void, never, never>) => Fiber.RuntimeFiber<void, never>
81
+ readonly runFork: <A>(effect: Effect.Effect<A, never, never>) => Fiber.RuntimeFiber<A, never>
53
82
  readonly isDisposed: () => boolean
54
83
  }
55
84
 
85
+ /**
86
+ * Every scope a runtime mints for something it mounts is forked from an owner, so
87
+ * closing the owner closes the mount. A forked scope deregisters itself from its
88
+ * owner when it closes first, so mount churn does not accumulate finalizers.
89
+ *
90
+ * Forking a *closed* owner is not an error in Effect — it hands back an already-closed
91
+ * scope, against which a layer builds successfully while releasing everything at the
92
+ * instant it is acquired. A mount reported live over finalized services is worse than
93
+ * no mount, so that case is refused. A finalizer added to a closed scope runs
94
+ * immediately, which is how the state is read without reaching into Effect's internals.
95
+ */
96
+ export const forkMountScope = (owner: Scope.Scope): Option.Option<Scope.CloseableScope> =>
97
+ Effect.runSync(
98
+ Effect.gen(function* () {
99
+ const scope = yield* Scope.fork(owner, ExecutionStrategy.sequential)
100
+ const alive = MutableRef.make(true)
101
+ yield* Scope.addFinalizer(
102
+ scope,
103
+ Effect.sync(() => MutableRef.set(alive, false)),
104
+ )
105
+ return MutableRef.get(alive) ? Option.some(scope) : Option.none()
106
+ }),
107
+ )
108
+
109
+ /**
110
+ * Build a composition activation into a scope forked from `ownerScope` and hand back
111
+ * its disposer, which is also registered with `disposers` so the host's own teardown
112
+ * reaches it.
113
+ */
114
+ /**
115
+ * Guard shared by every mount path: a runtime that is going away, or an owner scope
116
+ * that has already closed, refuses the mount rather than reporting a dead one live.
117
+ */
118
+ export const startMount = (
119
+ host: FeatureMountHost,
120
+ ownerScope: Scope.Scope,
121
+ start: (scope: Scope.CloseableScope) => () => void,
122
+ ): (() => void) =>
123
+ host.isDisposed()
124
+ ? () => {}
125
+ : Option.match(forkMountScope(ownerScope), { onNone: () => () => {}, onSome: start })
126
+
127
+ export const mountActivation = (
128
+ host: FeatureMountHost,
129
+ disposers: Set<() => void>,
130
+ build: Effect.Effect<unknown, never, Scope.Scope>,
131
+ ownerScope: Scope.Scope,
132
+ ): (() => void) =>
133
+ startMount(host, ownerScope, (scope) => {
134
+ const liveness = { active: true }
135
+ // Same ordering as `forkFeatureMount`: forked first so it finalizes last, and the
136
+ // fiber registered with `scope` so an owner closing mid-build stops it rather than
137
+ // letting it acquire against a scope that is already gone.
138
+ const buildScope = Effect.runSync(Scope.fork(scope, ExecutionStrategy.sequential))
139
+ const fiber = host.runFork(Effect.provideService(build, Scope.Scope, buildScope))
140
+ Effect.runSync(Scope.addFinalizer(scope, Fiber.interrupt(fiber)))
141
+ const dispose = (): void => {
142
+ if (!liveness.active) {
143
+ return
144
+ }
145
+ liveness.active = false
146
+ fiber.unsafeInterruptAsFork(fiber.id())
147
+ const close = Fiber.await(fiber).pipe(
148
+ Effect.zipRight(Scope.close(scope, Exit.succeed(undefined))),
149
+ Effect.ensuring(Effect.sync(() => disposers.delete(dispose))),
150
+ )
151
+ Effect.runFork(close)
152
+ }
153
+ disposers.add(dispose)
154
+ return dispose
155
+ })
156
+
56
157
  /**
57
158
  * Fork a feature mount onto the host runtime and hand back its disposer. Both
58
159
  * the typed and captured mount paths settle failures and tear down identically.
@@ -63,9 +164,14 @@ export const forkFeatureMount = (
63
164
  mount: Effect.Effect<void, FeatureLoadFailed, Scope.Scope>,
64
165
  onFailed: (error: FeatureLoadFailed) => void,
65
166
  ): (() => void) => {
167
+ // Forked before the mount fiber so it finalizes after it. Closing `scope` — which the
168
+ // owner does when it closes — must interrupt a mount still in flight before releasing
169
+ // what it has already built; the mount runs on a deferred `runFork`, so an owner that
170
+ // closes after the fork would otherwise let it go live against finalized services.
171
+ const buildScope = Effect.runSync(Scope.fork(scope, ExecutionStrategy.sequential))
66
172
  const fiber = host.runFork(
67
173
  mount.pipe(
68
- Effect.provideService(Scope.Scope, scope),
174
+ Effect.provideService(Scope.Scope, buildScope),
69
175
  Effect.matchCause({
70
176
  onFailure: (cause) => {
71
177
  // Disposer interruption is cancellation, not a failed feature.
@@ -79,6 +185,7 @@ export const forkFeatureMount = (
79
185
  }),
80
186
  ),
81
187
  )
188
+ Effect.runSync(Scope.addFinalizer(scope, Fiber.interrupt(fiber)))
82
189
  return () => {
83
190
  // Interrupt first, then await finalizer registration before closing the scope.
84
191
  fiber.unsafeInterruptAsFork(fiber.id())