@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.
- package/README.md +15 -6
- package/dist/boundary/boundary.d.ts +64 -0
- package/dist/boundary/boundary.d.ts.map +1 -0
- package/dist/boundary/boundary.js +102 -0
- package/dist/boundary/boundary.js.map +1 -0
- package/dist/calc/asyncCalc.d.ts +23 -0
- package/dist/calc/asyncCalc.d.ts.map +1 -0
- package/dist/calc/asyncCalc.js +100 -0
- package/dist/calc/asyncCalc.js.map +1 -0
- package/dist/calc/asyncCalcTypes.d.ts +80 -0
- package/dist/calc/asyncCalcTypes.d.ts.map +1 -0
- package/dist/calc/asyncCalcTypes.js +2 -0
- package/dist/calc/asyncCalcTypes.js.map +1 -0
- package/dist/calc/asyncData.d.ts +29 -0
- package/dist/calc/asyncData.d.ts.map +1 -0
- package/dist/calc/asyncData.js +21 -0
- package/dist/calc/asyncData.js.map +1 -0
- package/dist/calc/calc.d.ts +39 -0
- package/dist/calc/calc.d.ts.map +1 -0
- package/dist/calc/calc.js +48 -0
- package/dist/calc/calc.js.map +1 -0
- package/dist/calc/calcFamily.d.ts +40 -0
- package/dist/calc/calcFamily.d.ts.map +1 -0
- package/dist/calc/calcFamily.js +156 -0
- package/dist/calc/calcFamily.js.map +1 -0
- package/dist/calc/compose.d.ts +5 -0
- package/dist/calc/compose.d.ts.map +1 -0
- package/dist/calc/compose.js +5 -0
- package/dist/calc/compose.js.map +1 -0
- package/dist/calc/queryState.d.ts +14 -0
- package/dist/calc/queryState.d.ts.map +1 -0
- package/dist/calc/queryState.js +39 -0
- package/dist/calc/queryState.js.map +1 -0
- package/dist/channel/channel.d.ts +51 -0
- package/dist/channel/channel.d.ts.map +1 -0
- package/dist/channel/channel.js +91 -0
- package/dist/channel/channel.js.map +1 -0
- package/dist/channel/procedures.d.ts +24 -0
- package/dist/channel/procedures.d.ts.map +1 -0
- package/dist/channel/procedures.js +82 -0
- package/dist/channel/procedures.js.map +1 -0
- package/dist/compose/composition.d.ts +12 -0
- package/dist/compose/composition.d.ts.map +1 -0
- package/dist/compose/composition.js +62 -0
- package/dist/compose/composition.js.map +1 -0
- package/dist/compose/compositionTypes.d.ts +127 -0
- package/dist/compose/compositionTypes.d.ts.map +1 -0
- package/dist/compose/compositionTypes.js +2 -0
- package/dist/compose/compositionTypes.js.map +1 -0
- package/dist/compose/host.d.ts +6 -0
- package/dist/compose/host.d.ts.map +1 -0
- package/dist/compose/host.js +2 -0
- package/dist/compose/host.js.map +1 -0
- package/dist/compose/props.d.ts +10 -0
- package/dist/compose/props.d.ts.map +1 -0
- package/dist/compose/props.js +6 -0
- package/dist/compose/props.js.map +1 -0
- package/dist/compose/provide.d.ts +18 -0
- package/dist/compose/provide.d.ts.map +1 -0
- package/dist/compose/provide.js +49 -0
- package/dist/compose/provide.js.map +1 -0
- package/dist/compose/slot.d.ts +70 -0
- package/dist/compose/slot.d.ts.map +1 -0
- package/dist/compose/slot.js +26 -0
- package/dist/compose/slot.js.map +1 -0
- package/dist/compose/structure.d.ts +51 -0
- package/dist/compose/structure.d.ts.map +1 -0
- package/dist/compose/structure.js +117 -0
- package/dist/compose/structure.js.map +1 -0
- package/dist/compose/ui.d.ts +112 -0
- package/dist/compose/ui.d.ts.map +1 -0
- package/dist/compose/ui.js +58 -0
- package/dist/compose/ui.js.map +1 -0
- package/dist/definition/definition.d.ts +11 -0
- package/dist/definition/definition.d.ts.map +1 -0
- package/dist/definition/definition.js +35 -0
- package/dist/definition/definition.js.map +1 -0
- package/dist/event/event.d.ts +52 -0
- package/dist/event/event.d.ts.map +1 -0
- package/dist/event/event.js +88 -0
- package/dist/event/event.js.map +1 -0
- package/dist/event/eventGroup.d.ts +8 -0
- package/dist/event/eventGroup.d.ts.map +1 -0
- package/dist/event/eventGroup.js +3 -0
- package/dist/event/eventGroup.js.map +1 -0
- package/dist/feature/feature.d.ts +27 -0
- package/dist/feature/feature.d.ts.map +1 -0
- package/dist/feature/feature.js +20 -0
- package/dist/feature/feature.js.map +1 -0
- package/dist/feature/featureBinding.d.ts +85 -0
- package/dist/feature/featureBinding.d.ts.map +1 -0
- package/dist/feature/featureBinding.js +10 -0
- package/dist/feature/featureBinding.js.map +1 -0
- package/dist/feature/featureClass.d.ts +10 -0
- package/dist/feature/featureClass.d.ts.map +1 -0
- package/dist/feature/featureClass.js +2 -0
- package/dist/feature/featureClass.js.map +1 -0
- package/dist/feature/featureConfig.d.ts +86 -0
- package/dist/feature/featureConfig.d.ts.map +1 -0
- package/dist/feature/featureConfig.js +13 -0
- package/dist/feature/featureConfig.js.map +1 -0
- package/dist/feature/featureFactory.d.ts +15 -0
- package/dist/feature/featureFactory.d.ts.map +1 -0
- package/dist/feature/featureFactory.js +59 -0
- package/dist/feature/featureFactory.js.map +1 -0
- package/dist/feature/featureModule.d.ts +28 -0
- package/dist/feature/featureModule.d.ts.map +1 -0
- package/dist/feature/featureModule.js +33 -0
- package/dist/feature/featureModule.js.map +1 -0
- package/dist/feature/featureNativeTree.d.ts +26 -0
- package/dist/feature/featureNativeTree.d.ts.map +1 -0
- package/dist/feature/featureNativeTree.js +2 -0
- package/dist/feature/featureNativeTree.js.map +1 -0
- package/dist/feature/featureRequirement.d.ts +43 -0
- package/dist/feature/featureRequirement.d.ts.map +1 -0
- package/dist/feature/featureRequirement.js +6 -0
- package/dist/feature/featureRequirement.js.map +1 -0
- package/dist/feature/featureVariants.d.ts +9 -0
- package/dist/feature/featureVariants.d.ts.map +1 -0
- package/dist/feature/featureVariants.js +59 -0
- package/dist/feature/featureVariants.js.map +1 -0
- package/dist/feature/mountFeature.d.ts +10 -0
- package/dist/feature/mountFeature.d.ts.map +1 -0
- package/dist/feature/mountFeature.js +14 -0
- package/dist/feature/mountFeature.js.map +1 -0
- package/dist/graph/closure.d.ts +77 -0
- package/dist/graph/closure.d.ts.map +1 -0
- package/dist/graph/closure.js +4 -0
- package/dist/graph/closure.js.map +1 -0
- package/dist/graph/services.d.ts +30 -0
- package/dist/graph/services.d.ts.map +1 -0
- package/dist/graph/services.js +2 -0
- package/dist/graph/services.js.map +1 -0
- package/dist/graph/summary.d.ts +60 -0
- package/dist/graph/summary.d.ts.map +1 -0
- package/dist/graph/summary.js +7 -0
- package/dist/graph/summary.js.map +1 -0
- package/dist/index.d.ts +51 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +34 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/bucketCache.d.ts +19 -0
- package/dist/internal/bucketCache.d.ts.map +1 -0
- package/dist/internal/bucketCache.js +20 -0
- package/dist/internal/bucketCache.js.map +1 -0
- package/dist/internal/capture.d.ts +17 -0
- package/dist/internal/capture.d.ts.map +1 -0
- package/dist/internal/capture.js +5 -0
- package/dist/internal/capture.js.map +1 -0
- package/dist/internal/env.d.ts +2 -0
- package/dist/internal/env.d.ts.map +1 -0
- package/dist/internal/env.js +5 -0
- package/dist/internal/env.js.map +1 -0
- package/dist/internal/errors.d.ts +48 -0
- package/dist/internal/errors.d.ts.map +1 -0
- package/dist/internal/errors.js +69 -0
- package/dist/internal/errors.js.map +1 -0
- package/dist/internal/familyKeys.d.ts +16 -0
- package/dist/internal/familyKeys.d.ts.map +1 -0
- package/dist/internal/familyKeys.js +35 -0
- package/dist/internal/familyKeys.js.map +1 -0
- package/dist/internal/inspect.d.ts +5 -0
- package/dist/internal/inspect.d.ts.map +1 -0
- package/dist/internal/inspect.js +20 -0
- package/dist/internal/inspect.js.map +1 -0
- package/dist/internal/platform.d.ts +43 -0
- package/dist/internal/platform.d.ts.map +1 -0
- package/dist/internal/platform.js +68 -0
- package/dist/internal/platform.js.map +1 -0
- package/dist/internal/queryDriver.d.ts +7 -0
- package/dist/internal/queryDriver.d.ts.map +1 -0
- package/dist/internal/queryDriver.js +222 -0
- package/dist/internal/queryDriver.js.map +1 -0
- package/dist/internal/queryDriverConsume.d.ts +9 -0
- package/dist/internal/queryDriverConsume.d.ts.map +1 -0
- package/dist/internal/queryDriverConsume.js +16 -0
- package/dist/internal/queryDriverConsume.js.map +1 -0
- package/dist/internal/queryDriverHydrate.d.ts +17 -0
- package/dist/internal/queryDriverHydrate.d.ts.map +1 -0
- package/dist/internal/queryDriverHydrate.js +25 -0
- package/dist/internal/queryDriverHydrate.js.map +1 -0
- package/dist/internal/queryDriverPersist.d.ts +16 -0
- package/dist/internal/queryDriverPersist.d.ts.map +1 -0
- package/dist/internal/queryDriverPersist.js +15 -0
- package/dist/internal/queryDriverPersist.js.map +1 -0
- package/dist/internal/queryDriverPersistWarn.d.ts +12 -0
- package/dist/internal/queryDriverPersistWarn.d.ts.map +1 -0
- package/dist/internal/queryDriverPersistWarn.js +31 -0
- package/dist/internal/queryDriverPersistWarn.js.map +1 -0
- package/dist/internal/queryDriverSettle.d.ts +23 -0
- package/dist/internal/queryDriverSettle.d.ts.map +1 -0
- package/dist/internal/queryDriverSettle.js +52 -0
- package/dist/internal/queryDriverSettle.js.map +1 -0
- package/dist/internal/queryDriverStore.d.ts +28 -0
- package/dist/internal/queryDriverStore.d.ts.map +1 -0
- package/dist/internal/queryDriverStore.js +53 -0
- package/dist/internal/queryDriverStore.js.map +1 -0
- package/dist/internal/queryDriverTypes.d.ts +44 -0
- package/dist/internal/queryDriverTypes.d.ts.map +1 -0
- package/dist/internal/queryDriverTypes.js +9 -0
- package/dist/internal/queryDriverTypes.js.map +1 -0
- package/dist/internal/queryEvents.d.ts +12 -0
- package/dist/internal/queryEvents.d.ts.map +1 -0
- package/dist/internal/queryEvents.js +11 -0
- package/dist/internal/queryEvents.js.map +1 -0
- package/dist/internal/queryStore.d.ts +13 -0
- package/dist/internal/queryStore.d.ts.map +1 -0
- package/dist/internal/queryStore.js +11 -0
- package/dist/internal/queryStore.js.map +1 -0
- package/dist/internal/reuse.d.ts +2 -0
- package/dist/internal/reuse.d.ts.map +1 -0
- package/dist/internal/reuse.js +55 -0
- package/dist/internal/reuse.js.map +1 -0
- package/dist/internal/scheduler.d.ts +14 -0
- package/dist/internal/scheduler.d.ts.map +1 -0
- package/dist/internal/scheduler.js +101 -0
- package/dist/internal/scheduler.js.map +1 -0
- package/dist/internal/seeds.d.ts +3 -0
- package/dist/internal/seeds.d.ts.map +1 -0
- package/dist/internal/seeds.js +3 -0
- package/dist/internal/seeds.js.map +1 -0
- package/dist/internal/sources.d.ts +20 -0
- package/dist/internal/sources.d.ts.map +1 -0
- package/dist/internal/sources.js +39 -0
- package/dist/internal/sources.js.map +1 -0
- package/dist/internal/stateRegistry.d.ts +2 -0
- package/dist/internal/stateRegistry.d.ts.map +1 -0
- package/dist/internal/stateRegistry.js +15 -0
- package/dist/internal/stateRegistry.js.map +1 -0
- package/dist/internal/store.d.ts +15 -0
- package/dist/internal/store.d.ts.map +1 -0
- package/dist/internal/store.js +61 -0
- package/dist/internal/store.js.map +1 -0
- package/dist/internal/track.d.ts +15 -0
- package/dist/internal/track.d.ts.map +1 -0
- package/dist/internal/track.js +11 -0
- package/dist/internal/track.js.map +1 -0
- package/dist/internal/variance.d.ts +4 -0
- package/dist/internal/variance.d.ts.map +1 -0
- package/dist/internal/variance.js +2 -0
- package/dist/internal/variance.js.map +1 -0
- package/dist/internal.d.ts +43 -0
- package/dist/internal.d.ts.map +1 -0
- package/dist/internal.js +59 -0
- package/dist/internal.js.map +1 -0
- package/dist/namespace/namespace.d.ts +24 -0
- package/dist/namespace/namespace.d.ts.map +1 -0
- package/dist/namespace/namespace.js +20 -0
- package/dist/namespace/namespace.js.map +1 -0
- package/dist/procedure/procedure.d.ts +32 -0
- package/dist/procedure/procedure.d.ts.map +1 -0
- package/dist/procedure/procedure.js +34 -0
- package/dist/procedure/procedure.js.map +1 -0
- package/dist/reducer/reducer.d.ts +35 -0
- package/dist/reducer/reducer.d.ts.map +1 -0
- package/dist/reducer/reducer.js +73 -0
- package/dist/reducer/reducer.js.map +1 -0
- package/dist/remote/pendingQueue.d.ts +22 -0
- package/dist/remote/pendingQueue.d.ts.map +1 -0
- package/dist/remote/pendingQueue.js +129 -0
- package/dist/remote/pendingQueue.js.map +1 -0
- package/dist/remote/remoteState.d.ts +13 -0
- package/dist/remote/remoteState.d.ts.map +1 -0
- package/dist/remote/remoteState.js +189 -0
- package/dist/remote/remoteState.js.map +1 -0
- package/dist/remote/remoteStateMake.d.ts +8 -0
- package/dist/remote/remoteStateMake.d.ts.map +1 -0
- package/dist/remote/remoteStateMake.js +46 -0
- package/dist/remote/remoteStateMake.js.map +1 -0
- package/dist/remote/remoteStateSend.d.ts +43 -0
- package/dist/remote/remoteStateSend.d.ts.map +1 -0
- package/dist/remote/remoteStateSend.js +83 -0
- package/dist/remote/remoteStateSend.js.map +1 -0
- package/dist/remote/remoteStateTypes.d.ts +106 -0
- package/dist/remote/remoteStateTypes.d.ts.map +1 -0
- package/dist/remote/remoteStateTypes.js +4 -0
- package/dist/remote/remoteStateTypes.js.map +1 -0
- package/dist/runtime/appRuntime.d.ts +10 -0
- package/dist/runtime/appRuntime.d.ts.map +1 -0
- package/dist/runtime/appRuntime.js +6 -0
- package/dist/runtime/appRuntime.js.map +1 -0
- package/dist/runtime/bus.d.ts +22 -0
- package/dist/runtime/bus.d.ts.map +1 -0
- package/dist/runtime/bus.js +12 -0
- package/dist/runtime/bus.js.map +1 -0
- package/dist/runtime/capturedAppRuntime.d.ts +7 -0
- package/dist/runtime/capturedAppRuntime.d.ts.map +1 -0
- package/dist/runtime/capturedAppRuntime.js +137 -0
- package/dist/runtime/capturedAppRuntime.js.map +1 -0
- package/dist/runtime/eventBudget.d.ts +32 -0
- package/dist/runtime/eventBudget.d.ts.map +1 -0
- package/dist/runtime/eventBudget.js +68 -0
- package/dist/runtime/eventBudget.js.map +1 -0
- package/dist/runtime/featureMount.d.ts +48 -0
- package/dist/runtime/featureMount.d.ts.map +1 -0
- package/dist/runtime/featureMount.js +118 -0
- package/dist/runtime/featureMount.js.map +1 -0
- package/dist/runtime/instrumentation.d.ts +18 -0
- package/dist/runtime/instrumentation.d.ts.map +1 -0
- package/dist/runtime/instrumentation.js +16 -0
- package/dist/runtime/instrumentation.js.map +1 -0
- package/dist/runtime/loop.d.ts +44 -0
- package/dist/runtime/loop.d.ts.map +1 -0
- package/dist/runtime/loop.js +131 -0
- package/dist/runtime/loop.js.map +1 -0
- package/dist/runtime/queries.d.ts +20 -0
- package/dist/runtime/queries.d.ts.map +1 -0
- package/dist/runtime/queries.js +21 -0
- package/dist/runtime/queries.js.map +1 -0
- package/dist/runtime/runtimeHandle.d.ts +56 -0
- package/dist/runtime/runtimeHandle.d.ts.map +1 -0
- package/dist/runtime/runtimeHandle.js +2 -0
- package/dist/runtime/runtimeHandle.js.map +1 -0
- package/dist/scene/scene.d.ts +55 -0
- package/dist/scene/scene.d.ts.map +1 -0
- package/dist/scene/scene.js +74 -0
- package/dist/scene/scene.js.map +1 -0
- package/dist/state/state.d.ts +54 -0
- package/dist/state/state.d.ts.map +1 -0
- package/dist/state/state.js +41 -0
- package/dist/state/state.js.map +1 -0
- package/dist/state/stateFamily.d.ts +59 -0
- package/dist/state/stateFamily.d.ts.map +1 -0
- package/dist/state/stateFamily.js +139 -0
- package/dist/state/stateFamily.js.map +1 -0
- package/dist/state/stateGroup.d.ts +36 -0
- package/dist/state/stateGroup.d.ts.map +1 -0
- package/dist/state/stateGroup.js +33 -0
- package/dist/state/stateGroup.js.map +1 -0
- package/dist/state/token.d.ts +35 -0
- package/dist/state/token.d.ts.map +1 -0
- package/dist/state/token.js +18 -0
- package/dist/state/token.js.map +1 -0
- package/dist/synced/syncedStore.d.ts +44 -0
- package/dist/synced/syncedStore.d.ts.map +1 -0
- package/dist/synced/syncedStore.js +32 -0
- package/dist/synced/syncedStore.js.map +1 -0
- package/dist/testkit/flight.testkit.d.ts +18 -0
- package/dist/testkit/flight.testkit.d.ts.map +1 -0
- package/dist/testkit/flight.testkit.js +46 -0
- package/dist/testkit/flight.testkit.js.map +1 -0
- package/dist/ui/node.d.ts +3 -0
- package/dist/ui/node.d.ts.map +1 -0
- package/dist/ui/node.js +2 -0
- package/dist/ui/node.js.map +1 -0
- package/dist/ui/trigger.d.ts +2 -0
- package/dist/ui/trigger.d.ts.map +1 -0
- package/dist/ui/trigger.js +2 -0
- package/dist/ui/trigger.js.map +1 -0
- package/dist/wire/tree.d.ts +32 -0
- package/dist/wire/tree.d.ts.map +1 -0
- package/dist/wire/tree.js +64 -0
- package/dist/wire/tree.js.map +1 -0
- package/dist/wire/triggers.d.ts +25 -0
- package/dist/wire/triggers.d.ts.map +1 -0
- package/dist/wire/triggers.js +31 -0
- package/dist/wire/triggers.js.map +1 -0
- package/package.json +17 -5
- package/src/calc/asyncCalc-key-attribution.test.ts +182 -0
- package/src/calc/asyncCalc-registry-name.test.ts +82 -0
- package/src/calc/asyncCalc.superseded-commit.test.ts +162 -0
- package/src/calc/asyncCalc.test.ts +35 -7
- package/src/calc/asyncCalc.ts +33 -10
- package/src/calc/asyncCalcTypes.test.ts +64 -0
- package/src/calc/asyncCalcTypes.ts +8 -10
- package/src/calc/calcFamily.ts +81 -34
- package/src/calc/family-identity.test.ts +231 -0
- package/src/calc/queryState.ts +36 -5
- package/src/channel/channel-throttle-frames.test.ts +163 -0
- package/src/channel/channel.ts +5 -0
- package/src/compose/compose-identity.test.ts +175 -0
- package/src/compose/composition.ts +8 -4
- package/src/compose/structure.ts +45 -13
- package/src/compose/ui.ts +22 -5
- package/src/event/event.ts +31 -2
- package/src/event/syncTrigger.test.ts +66 -0
- package/src/internal/familyKeys.ts +41 -0
- package/src/internal/platform.test.ts +55 -0
- package/src/internal/platform.ts +72 -0
- package/src/internal/queryDriver-defect-attribution.test.ts +107 -0
- package/src/internal/queryDriver-persist-key.test.ts +118 -0
- package/src/internal/queryDriver.persistence-staleness.test.ts +209 -0
- package/src/internal/queryDriver.ts +92 -80
- package/src/internal/queryDriverConsume.ts +42 -0
- package/src/internal/queryDriverHydrate.ts +9 -3
- package/src/internal/queryDriverPersist.ts +42 -0
- package/src/internal/queryDriverPersistWarn.ts +61 -0
- package/src/internal/queryDriverSettle.ts +85 -0
- package/src/internal/queryDriverStore.ts +9 -74
- package/src/internal/queryDriverTypes.ts +10 -0
- package/src/internal/queryState-and-scheduler-audit.test.ts +170 -0
- package/src/internal/scheduler-multi-source-wake.test.ts +111 -0
- package/src/internal/scheduler.ts +80 -6
- package/src/internal.ts +1 -0
- package/src/reducer/reducer.ts +3 -3
- package/src/remote/pendingQueue.ts +35 -5
- package/src/remote/remoteState.intent-key-stamp.test.ts +207 -0
- package/src/remote/remoteState.key-attribution.test.ts +174 -0
- package/src/remote/remoteState.ts +27 -15
- package/src/remote/remoteStateSend.ts +29 -8
- package/src/remote/remoteStateTypes.ts +7 -0
- package/src/runtime/bus.ts +6 -1
- package/src/runtime/capturedAppRuntime.ts +134 -110
- package/src/runtime/eventBudget.ts +2 -1
- package/src/runtime/feature-mount-ownership.test.ts +539 -0
- package/src/runtime/featureMount.ts +110 -3
- package/src/runtime/loop.ts +29 -6
- package/src/runtime/mount-inflight-ownership.test.ts +361 -0
- package/src/runtime/mount-scope-hardening.test.ts +440 -0
- package/src/runtime/runtime.schema-identity.test.ts +188 -0
- package/src/state/state.ts +8 -4
- package/src/state/stateFamily.ts +86 -36
- package/src/state/stateGroup.ts +24 -6
- package/src/testkit/flight.testkit.ts +2 -1
- package/src/wire/tree-key-presence.test.ts +46 -0
- 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 {
|
|
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<
|
|
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,
|
|
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())
|