@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
package/src/runtime/loop.ts
CHANGED
|
@@ -14,6 +14,10 @@ export interface ReducerEntry {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
export interface ReducerRegistry {
|
|
17
|
+
// Captured where the drain captures it, so the synchronous trigger path and the
|
|
18
|
+
// drain report to the same profiler even though triggers are built later, at mount,
|
|
19
|
+
// outside the scene's `Layer.locally(CurrentInstrumentation, ...)` scope.
|
|
20
|
+
readonly instrumentation: Instrumentation
|
|
17
21
|
readonly entries: Array<ReducerEntry>
|
|
18
22
|
readonly byName: Map<string, Set<ReducerEntry>>
|
|
19
23
|
readonly byTag: Map<string, Set<ReducerEntry>>
|
|
@@ -38,7 +42,7 @@ const dropFromBuckets = <T>(map: Map<string, Set<T>>, key: string, target: T): v
|
|
|
38
42
|
}
|
|
39
43
|
}
|
|
40
44
|
|
|
41
|
-
const makeReducerRegistry = (): ReducerRegistry => {
|
|
45
|
+
const makeReducerRegistry = (instrumentation: Instrumentation): ReducerRegistry => {
|
|
42
46
|
const entries = new Set<ReducerEntry>()
|
|
43
47
|
const byName = new Map<string, Set<ReducerEntry>>()
|
|
44
48
|
const byTag = new Map<string, Set<ReducerEntry>>()
|
|
@@ -47,6 +51,7 @@ const makeReducerRegistry = (): ReducerRegistry => {
|
|
|
47
51
|
value: Option.some([]),
|
|
48
52
|
}
|
|
49
53
|
return {
|
|
54
|
+
instrumentation,
|
|
50
55
|
get entries() {
|
|
51
56
|
return Option.getOrElse(snapshot.value, () => {
|
|
52
57
|
const current = Array.from(entries)
|
|
@@ -108,7 +113,26 @@ const isolateApply = (
|
|
|
108
113
|
)
|
|
109
114
|
}
|
|
110
115
|
|
|
111
|
-
export
|
|
116
|
+
export interface ApplyReducersInput {
|
|
117
|
+
readonly registry: ReducerRegistry
|
|
118
|
+
readonly event: Tagged
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface ReducerFailure {
|
|
122
|
+
readonly event: Tagged
|
|
123
|
+
readonly error: unknown
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Run every reducer registered for one event. A defect in one must not skip the rest. */
|
|
127
|
+
export const applyReducers = (input: ApplyReducersInput): ReadonlyArray<ReducerFailure> =>
|
|
128
|
+
Arr.filterMap(input.registry.tagged(input.event._tag), (reducer) =>
|
|
129
|
+
isolateApply(reducer, input.event, input.registry.instrumentation),
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
export const reducersLayer: Layer.Layer<Reducers> = Layer.effect(
|
|
133
|
+
Reducers,
|
|
134
|
+
Effect.map(resolveInstrumentation, makeReducerRegistry),
|
|
135
|
+
)
|
|
112
136
|
|
|
113
137
|
// Partitioning keeps dispatch order stable within each priority.
|
|
114
138
|
const rank = (priority: Envelope['priority']): number => (priority === 'High' ? 0 : 1)
|
|
@@ -145,10 +169,9 @@ const drain = Effect.gen(function* () {
|
|
|
145
169
|
instrumentation.eventDispatched(envelope.event._tag, envelope.priority),
|
|
146
170
|
)
|
|
147
171
|
const collected = Arr.flatMap(ordered, (envelope) =>
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
),
|
|
172
|
+
envelope.reduced
|
|
173
|
+
? []
|
|
174
|
+
: applyReducers({ registry: reducers, event: envelope.event }),
|
|
152
175
|
)
|
|
153
176
|
ordered.forEach((envelope) => {
|
|
154
177
|
// channelsFor is distinct; per-procedure offers would double-run shared channels.
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
import { Context, Deferred, Effect, Layer, Option } from 'effect'
|
|
2
|
+
import { expect, it, test } from '@effect/vitest'
|
|
3
|
+
import * as Composition from '../compose/composition'
|
|
4
|
+
import { provide } from '../compose/provide'
|
|
5
|
+
import { mount } from '../compose/structure'
|
|
6
|
+
import * as Ui from '../compose/ui'
|
|
7
|
+
import { ui } from '../compose/ui'
|
|
8
|
+
import * as Feature from '../feature/feature'
|
|
9
|
+
import { featureModule } from '../feature/feature'
|
|
10
|
+
import { scene } from '../scene/scene'
|
|
11
|
+
import { makeScheduler } from '../internal/scheduler'
|
|
12
|
+
import { flush, until } from '../testkit/flight.testkit'
|
|
13
|
+
import { Engine } from './loop'
|
|
14
|
+
import { makeAppRuntime } from './appRuntime'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Round-5 adversarial audit — `reform`'s half of the mount-ownership /
|
|
18
|
+
* scheduler fix set.
|
|
19
|
+
*
|
|
20
|
+
* Under audit:
|
|
21
|
+
* * `runtime/featureMount.ts` `forkMountScope` / `startMount` — a mount whose
|
|
22
|
+
* owner scope has already closed is refused.
|
|
23
|
+
* * `runtime/capturedAppRuntime.ts` — every mount is forked from one parallel
|
|
24
|
+
* `rootScope`.
|
|
25
|
+
* * `internal/scheduler.ts` `isLive` — flush-time membership.
|
|
26
|
+
*
|
|
27
|
+
* Only findings that survived an attempt to refute them are filed. Each test
|
|
28
|
+
* states the behaviour the FIXED code is supposed to have, fails on the current
|
|
29
|
+
* working tree, and carries a passing CONTROL in the same body. Every handler is
|
|
30
|
+
* attached before anything is awaited; nothing waits a fixed duration.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
// `until`'s poll budget, same as the round-4 hunt: 40 rounds x 10ms.
|
|
34
|
+
const ROUNDS = 40
|
|
35
|
+
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// Shared scene / feature definitions
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
interface LifecycleProbeService {
|
|
41
|
+
readonly acquired: Array<string>
|
|
42
|
+
readonly released: Array<string>
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const LifecycleProbeBase: Context.TagClass<
|
|
46
|
+
LifecycleProbe,
|
|
47
|
+
'audit5.LifecycleProbe',
|
|
48
|
+
LifecycleProbeService
|
|
49
|
+
> = Context.Tag('audit5.LifecycleProbe')<LifecycleProbe, LifecycleProbeService>()
|
|
50
|
+
class LifecycleProbe extends LifecycleProbeBase {}
|
|
51
|
+
|
|
52
|
+
const makeProbe = (): LifecycleProbeService => ({ acquired: [], released: [] })
|
|
53
|
+
|
|
54
|
+
class AuditUi extends ui('audit5.AuditUi')<{ props: {} }>() {}
|
|
55
|
+
class AuditComp extends Composition.make('audit5.AuditComp', {
|
|
56
|
+
title: 'Audit',
|
|
57
|
+
ui: AuditUi,
|
|
58
|
+
})<AuditComp>() {}
|
|
59
|
+
const AuditLive = Composition.live(AuditComp, function* () {
|
|
60
|
+
return mount({ props: {}, slots: {} })
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
class AuditRootUi extends ui('audit5.AuditRootUi')<{ props: {} }>() {}
|
|
64
|
+
class AuditRootComp extends Composition.make('audit5.AuditRootComp', {
|
|
65
|
+
title: 'AuditRoot',
|
|
66
|
+
ui: AuditRootUi,
|
|
67
|
+
})<AuditRootComp>() {}
|
|
68
|
+
const AuditRootLive = Composition.live(AuditRootComp, function* () {
|
|
69
|
+
return mount({ props: {}, slots: {} })
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
const auditRootViews = AuditRootLive.pipe(
|
|
73
|
+
Layer.provideMerge(
|
|
74
|
+
provide(
|
|
75
|
+
AuditRootUi,
|
|
76
|
+
Ui.make(AuditRootUi, () => null),
|
|
77
|
+
),
|
|
78
|
+
),
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
const auditScene = (probe: LifecycleProbeService) =>
|
|
82
|
+
scene(AuditRootComp, {
|
|
83
|
+
provide: [
|
|
84
|
+
auditRootViews.pipe(
|
|
85
|
+
Layer.provideMerge(Layer.succeed(LifecycleProbe, probe)),
|
|
86
|
+
Layer.provideMerge(Engine),
|
|
87
|
+
),
|
|
88
|
+
],
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
// A scoped marker: `acquired` records that the layer built, `released` that its
|
|
92
|
+
// scope closed.
|
|
93
|
+
const scopedMark = (label: string): Layer.Layer<never, never, LifecycleProbe> =>
|
|
94
|
+
Layer.scopedDiscard(
|
|
95
|
+
Effect.gen(function* () {
|
|
96
|
+
const probe = yield* LifecycleProbe
|
|
97
|
+
yield* Effect.acquireRelease(
|
|
98
|
+
Effect.sync(() => probe.acquired.push(label)),
|
|
99
|
+
() => Effect.sync(() => probe.released.push(label)),
|
|
100
|
+
)
|
|
101
|
+
}),
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
// The same marker, but the module parks BEFORE it acquires anything: a module
|
|
105
|
+
// that awaits a socket, a config fetch, a lazy `import()`, a DB handshake. The
|
|
106
|
+
// mount is in flight and owns nothing yet.
|
|
107
|
+
const gatedMark = (
|
|
108
|
+
label: string,
|
|
109
|
+
gate: Deferred.Deferred<void>,
|
|
110
|
+
): Layer.Layer<never, never, LifecycleProbe> =>
|
|
111
|
+
Layer.scopedDiscard(
|
|
112
|
+
Effect.gen(function* () {
|
|
113
|
+
const probe = yield* LifecycleProbe
|
|
114
|
+
yield* Deferred.await(gate)
|
|
115
|
+
yield* Effect.acquireRelease(
|
|
116
|
+
Effect.sync(() => probe.acquired.push(label)),
|
|
117
|
+
() => Effect.sync(() => probe.released.push(label)),
|
|
118
|
+
)
|
|
119
|
+
}),
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
const runOption = (candidate: Option.Option<() => void>): void =>
|
|
123
|
+
Option.match(candidate, { onNone: () => undefined, onSome: (run) => run() })
|
|
124
|
+
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
// FINDING 3 — `runtime/featureMount.ts:96-125`.
|
|
127
|
+
//
|
|
128
|
+
// `forkMountScope` refuses a mount whose owner is closed AT FORK TIME, and the
|
|
129
|
+
// doc comment states the contract absolutely: "A mount reported live over
|
|
130
|
+
// finalized services is worse than no mount, so that case is refused."
|
|
131
|
+
//
|
|
132
|
+
// The probe is a snapshot, and the thing it guards is not synchronous. The mount
|
|
133
|
+
// runs on `host.runFork`, which is `ManagedRuntime.runFork` — always deferred,
|
|
134
|
+
// because it must first await the runtime effect. Nothing on the mount path
|
|
135
|
+
// registers the mount fiber with the scope it was forked from, so once the fork
|
|
136
|
+
// has happened the owner closing NO LONGER stops the mount: the mount scope is
|
|
137
|
+
// closed underneath the running fiber, `Layer.build` against a closed scope
|
|
138
|
+
// still succeeds (`Scope.addFinalizer` on a closed scope runs the finalizer
|
|
139
|
+
// there and then), and `onLive` is called with a context whose finalizers have
|
|
140
|
+
// already run — the exact outcome the comment says is refused, reached by the
|
|
141
|
+
// other ordering of the same two events.
|
|
142
|
+
//
|
|
143
|
+
// The window is not a microtask: it is however long the mount takes. Any module
|
|
144
|
+
// that awaits anything — a lazy `import()`, a socket, a config read — holds it
|
|
145
|
+
// open across a whole route change or panel close.
|
|
146
|
+
//
|
|
147
|
+
// A fix that closes it: register the mount fiber with the scope it is forked
|
|
148
|
+
// from (`Scope.addFinalizer(scope, Fiber.interrupt(fiber))`) so closing the
|
|
149
|
+
// owner interrupts an in-flight mount. `forkFeatureMount` already treats
|
|
150
|
+
// `Cause.isInterruptedOnly` as cancellation, so that path stays silent — no
|
|
151
|
+
// `onLive`, no `onFailed`, and nothing acquired.
|
|
152
|
+
// ---------------------------------------------------------------------------
|
|
153
|
+
|
|
154
|
+
class OwnerFeature extends Feature.make('audit5.owner', {
|
|
155
|
+
composition: AuditComp,
|
|
156
|
+
module: featureModule([LifecycleProbe], Layer.merge(AuditLive, scopedMark('owner'))),
|
|
157
|
+
}) {}
|
|
158
|
+
const OwnerNative = Feature.tree<typeof OwnerFeature>(OwnerFeature.token)
|
|
159
|
+
|
|
160
|
+
class ControlChildFeature extends Feature.make('audit5.controlChild', {
|
|
161
|
+
composition: AuditComp,
|
|
162
|
+
module: featureModule([LifecycleProbe], Layer.merge(AuditLive, scopedMark('control-child'))),
|
|
163
|
+
}) {}
|
|
164
|
+
const ControlChildNative = Feature.tree<typeof ControlChildFeature>(ControlChildFeature.token)
|
|
165
|
+
|
|
166
|
+
const inFlightFeature = (gate: Deferred.Deferred<void>) =>
|
|
167
|
+
class InFlightChildFeature extends Feature.make('audit5.inFlightChild', {
|
|
168
|
+
composition: AuditComp,
|
|
169
|
+
module: featureModule(
|
|
170
|
+
[LifecycleProbe],
|
|
171
|
+
Layer.merge(AuditLive, gatedMark('in-flight-child', gate)),
|
|
172
|
+
),
|
|
173
|
+
}) {}
|
|
174
|
+
|
|
175
|
+
it.scopedLive(
|
|
176
|
+
'a feature mount whose owner scope closes while it is still in flight is not reported live',
|
|
177
|
+
() =>
|
|
178
|
+
Effect.gen(function* () {
|
|
179
|
+
const probe = makeProbe()
|
|
180
|
+
const app = makeAppRuntime(auditScene(probe))
|
|
181
|
+
yield* Effect.addFinalizer(() => Effect.sync(() => app.dispose()))
|
|
182
|
+
|
|
183
|
+
const gate = yield* Deferred.make<void>()
|
|
184
|
+
const InFlightChild = inFlightFeature(gate)
|
|
185
|
+
const InFlightChildNative = Feature.tree<typeof InFlightChild>(InFlightChild.token)
|
|
186
|
+
|
|
187
|
+
// Every handler attached before anything is awaited.
|
|
188
|
+
const owner = { live: 0, failed: 0 }
|
|
189
|
+
const control = { live: 0, failed: 0 }
|
|
190
|
+
const inFlight = { live: 0, failed: 0 }
|
|
191
|
+
const mounts = {
|
|
192
|
+
control: Option.none<() => void>(),
|
|
193
|
+
inFlight: Option.none<() => void>(),
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const disposeOwner = app.mountFeature(OwnerNative.binding, {
|
|
197
|
+
onLive: (ownerRuntime) => {
|
|
198
|
+
owner.live += 1
|
|
199
|
+
mounts.control = Option.some(() => {
|
|
200
|
+
ownerRuntime.mountFeature(ControlChildNative.binding, {
|
|
201
|
+
onLive: () => {
|
|
202
|
+
control.live += 1
|
|
203
|
+
},
|
|
204
|
+
onFailed: () => {
|
|
205
|
+
control.failed += 1
|
|
206
|
+
},
|
|
207
|
+
})
|
|
208
|
+
})
|
|
209
|
+
mounts.inFlight = Option.some(() => {
|
|
210
|
+
ownerRuntime.mountFeature(InFlightChildNative.binding, {
|
|
211
|
+
onLive: () => {
|
|
212
|
+
inFlight.live += 1
|
|
213
|
+
},
|
|
214
|
+
onFailed: () => {
|
|
215
|
+
inFlight.failed += 1
|
|
216
|
+
},
|
|
217
|
+
})
|
|
218
|
+
})
|
|
219
|
+
},
|
|
220
|
+
onFailed: () => {
|
|
221
|
+
owner.failed += 1
|
|
222
|
+
},
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
yield* until(
|
|
226
|
+
() => owner.live + owner.failed,
|
|
227
|
+
(n) => n >= 1,
|
|
228
|
+
ROUNDS,
|
|
229
|
+
)
|
|
230
|
+
expect(owner).toEqual({ live: 1, failed: 0 })
|
|
231
|
+
|
|
232
|
+
// ---- CONTROL — the same handle, the same mount path, a module that does
|
|
233
|
+
// not park. It goes live, acquires, and holds its resource. This proves the
|
|
234
|
+
// scene, the probe, the child-mount path and the handle all work.
|
|
235
|
+
runOption(mounts.control)
|
|
236
|
+
yield* until(
|
|
237
|
+
() => control.live + control.failed,
|
|
238
|
+
(n) => n >= 1,
|
|
239
|
+
ROUNDS,
|
|
240
|
+
)
|
|
241
|
+
expect(control).toEqual({ live: 1, failed: 0 })
|
|
242
|
+
expect(probe.acquired).toContain('control-child')
|
|
243
|
+
expect(probe.released).toEqual([])
|
|
244
|
+
|
|
245
|
+
// ---- THE DEFECT — start a mount whose module parks before it acquires
|
|
246
|
+
// anything, so the mount is genuinely in flight and owns nothing yet...
|
|
247
|
+
runOption(mounts.inFlight)
|
|
248
|
+
yield* flush()
|
|
249
|
+
expect(probe.acquired).not.toContain('in-flight-child')
|
|
250
|
+
|
|
251
|
+
// ...then close the owner. Its scope, and every mount scope forked from it,
|
|
252
|
+
// is now closed — including the in-flight one.
|
|
253
|
+
disposeOwner()
|
|
254
|
+
yield* until(
|
|
255
|
+
() => probe.released.length,
|
|
256
|
+
(n) => n >= 2,
|
|
257
|
+
ROUNDS,
|
|
258
|
+
)
|
|
259
|
+
yield* flush()
|
|
260
|
+
expect(probe.released.slice().sort()).toEqual(['control-child', 'owner'])
|
|
261
|
+
|
|
262
|
+
// The module finishes what it was waiting for, after its owner is gone.
|
|
263
|
+
yield* Deferred.succeed(gate, undefined)
|
|
264
|
+
yield* until(
|
|
265
|
+
() => inFlight.live + inFlight.failed + probe.acquired.length,
|
|
266
|
+
(n) => n >= 3,
|
|
267
|
+
ROUNDS,
|
|
268
|
+
)
|
|
269
|
+
yield* flush()
|
|
270
|
+
|
|
271
|
+
// A mount the runtime can no longer own must not acquire resources it
|
|
272
|
+
// releases in the same breath...
|
|
273
|
+
// Actual today: 'in-flight-child' appears in BOTH lists — acquired against
|
|
274
|
+
// an already-closed scope, so its finalizer ran at the instant it was added.
|
|
275
|
+
expect(probe.acquired).not.toContain('in-flight-child')
|
|
276
|
+
// ...and must not be reported live over them.
|
|
277
|
+
// Actual today: 1.
|
|
278
|
+
expect(inFlight.live).toBe(0)
|
|
279
|
+
}),
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
// ---------------------------------------------------------------------------
|
|
283
|
+
// FINDING 4 — `internal/scheduler.ts:36-37`.
|
|
284
|
+
//
|
|
285
|
+
// const isLive = (queued, listener) =>
|
|
286
|
+
// Predicate.isSet(queued.source) ? queued.source.has(listener)
|
|
287
|
+
// : queued.members.includes(listener)
|
|
288
|
+
//
|
|
289
|
+
// Round-3 established the invariant the code still states one comment further
|
|
290
|
+
// down: "a listener that has since unsubscribed must not be woken". Round-4
|
|
291
|
+
// found that re-reading a ONE-SHOT iterable drops every wake, and round-5 fixed
|
|
292
|
+
// that by keeping a schedule-time snapshot. But the new answer for "anything
|
|
293
|
+
// that is not a Set" is the snapshot for ALL non-Set sources, including the ones
|
|
294
|
+
// that are perfectly re-readable — an `Array`, or any custom collection a
|
|
295
|
+
// first-party Source keeps its subscribers in.
|
|
296
|
+
//
|
|
297
|
+
// `Scheduler` is exported from `@playfast/reform/internal` as part of the
|
|
298
|
+
// primitive-authoring toolkit and its parameter is typed `Iterable<() => void>`,
|
|
299
|
+
// so an Array-backed subscriber list is a supported caller. For that caller the
|
|
300
|
+
// round-3 invariant silently no longer holds: an earlier listener in the same
|
|
301
|
+
// flush can tear a later one down — a scope closing, a family key forgotten, a
|
|
302
|
+
// composition unmounting — and the torn-down listener is woken anyway, reading a
|
|
303
|
+
// store whose finalizers have run.
|
|
304
|
+
//
|
|
305
|
+
// The distinction the code needs is re-readable vs one-shot, not Set vs
|
|
306
|
+
// everything else: `Predicate.isSet(source) || Array.isArray(source)` re-reads,
|
|
307
|
+
// everything else falls back to the snapshot.
|
|
308
|
+
//
|
|
309
|
+
// No in-repo caller passes a mutable non-Set today — `store.ts`, `pendingQueue`,
|
|
310
|
+
// `stateFamily` and `reform-profiler/recorder` all pass Sets, and `boundary`,
|
|
311
|
+
// `event` and `remoteState` pass single-element array LITERALS, which are never
|
|
312
|
+
// mutated and so behave identically either way. This is a latent SPI regression,
|
|
313
|
+
// filed at that severity.
|
|
314
|
+
// ---------------------------------------------------------------------------
|
|
315
|
+
|
|
316
|
+
const drainMicrotasks = async (turns = 5): Promise<void> => {
|
|
317
|
+
await Array.from({ length: turns }).reduce<Promise<void>>(
|
|
318
|
+
(chain) => chain.then(() => new Promise<void>((resolve) => queueMicrotask(resolve))),
|
|
319
|
+
Promise.resolve(),
|
|
320
|
+
)
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
test('scheduler: a listener dropped from a re-readable source mid-flush is not woken', async () => {
|
|
324
|
+
// ---- CONTROL — a Set source. `first` unsubscribes `second` while the flush is
|
|
325
|
+
// running and `second` is correctly dropped. Same scheduler, same two
|
|
326
|
+
// listeners, same mutation: only the container type differs.
|
|
327
|
+
const controlScheduler = makeScheduler()
|
|
328
|
+
const controlWoken: Array<string> = []
|
|
329
|
+
const controlSource = new Set<() => void>()
|
|
330
|
+
const controlSecond = (): void => {
|
|
331
|
+
controlWoken.push('second')
|
|
332
|
+
}
|
|
333
|
+
const controlFirst = (): void => {
|
|
334
|
+
controlWoken.push('first')
|
|
335
|
+
controlSource.delete(controlSecond)
|
|
336
|
+
}
|
|
337
|
+
controlSource.add(controlFirst)
|
|
338
|
+
controlSource.add(controlSecond)
|
|
339
|
+
|
|
340
|
+
// ---- DEFECT — an Array source, re-readable, mutated exactly the same way by
|
|
341
|
+
// exactly the same listener.
|
|
342
|
+
const scheduler = makeScheduler()
|
|
343
|
+
const woken: Array<string> = []
|
|
344
|
+
const source: Array<() => void> = []
|
|
345
|
+
const second = (): void => {
|
|
346
|
+
woken.push('second')
|
|
347
|
+
}
|
|
348
|
+
const first = (): void => {
|
|
349
|
+
woken.push('first')
|
|
350
|
+
source.splice(source.indexOf(second), 1)
|
|
351
|
+
}
|
|
352
|
+
source.push(first, second)
|
|
353
|
+
|
|
354
|
+
controlScheduler.schedule(controlSource)
|
|
355
|
+
scheduler.schedule(source)
|
|
356
|
+
await drainMicrotasks()
|
|
357
|
+
|
|
358
|
+
expect(controlWoken).toEqual(['first'])
|
|
359
|
+
// Actual today: ['first', 'second'] — the unsubscribed listener is woken.
|
|
360
|
+
expect(woken).toEqual(['first'])
|
|
361
|
+
})
|