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