@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,107 @@
|
|
|
1
|
+
// ADVERSARIAL AUDIT of the identity/attribution fix diff.
|
|
2
|
+
//
|
|
3
|
+
// Every test below asserts the behaviour the *fixed* code is supposed to have;
|
|
4
|
+
// each one FAILS on the working tree as of this writing. Nothing here fixes
|
|
5
|
+
// anything — no source file and no existing test file is touched.
|
|
6
|
+
//
|
|
7
|
+
// Five further findings were proved during this audit and are NOT in this file
|
|
8
|
+
// because the working tree was edited concurrently and they no longer reproduce:
|
|
9
|
+
// * stateFamily `forget` + `evictWhenUnused` — a woken subscriber's re-subscribe
|
|
10
|
+
// was undone by the old subscription's release, evicting the live store
|
|
11
|
+
// (since fixed by per-instance counts + `stamps`).
|
|
12
|
+
// * scheduler — a listener subscribing after a write was woken by it, and was
|
|
13
|
+
// delivered ahead of an earlier store's listener (since fixed by snapshotting
|
|
14
|
+
// members at schedule time and intersecting with the live source at flush).
|
|
15
|
+
// * `isLoading` contradicted `toAsyncData` under `keepPreviousData` (since fixed
|
|
16
|
+
// by carrying `keepPreviousData` on `QueryState`).
|
|
17
|
+
// * RemoteState's overlay folded intents by `link.key()` while subscribing only to
|
|
18
|
+
// feed + queue, so a key change that wrote nothing to the feed changed the
|
|
19
|
+
// visible value with no wake (since fixed by `SettleLink.subscribeKey`).
|
|
20
|
+
// * `connect()`'s re-attachable subscription never re-baselined, so a `Delete`
|
|
21
|
+
// frame missed while detached left a permanent phantom node (since fixed by
|
|
22
|
+
// keeping the subscription for the binding's life and adding `close()`).
|
|
23
|
+
import { expect, it } from '@effect/vitest'
|
|
24
|
+
import { Data, Effect, Layer, Option, Schema as S } from 'effect'
|
|
25
|
+
import { AsyncCalc, Engine, State, StateGroup } from '../index'
|
|
26
|
+
import type { QueryState } from '../calc/queryState'
|
|
27
|
+
import { Queries } from '../runtime/queries'
|
|
28
|
+
import { until } from '../testkit/flight.testkit'
|
|
29
|
+
|
|
30
|
+
class Torn extends Data.TaggedError('audit/Torn')<{ readonly reason: string }> {}
|
|
31
|
+
|
|
32
|
+
// A negative wait: long enough that a missing transition would have landed,
|
|
33
|
+
// short enough that the assertion — not a timeout — reports the failure.
|
|
34
|
+
const settleRounds = 40
|
|
35
|
+
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// FINDING (retargeted) — claim 4 (`settleDefect` retracts only its own flight).
|
|
38
|
+
//
|
|
39
|
+
// The audit read this as a regression: a run that dies right after a key change
|
|
40
|
+
// leaves `data = Some(previous key)`, `isPreviousKey = true`, `isFetching = false`,
|
|
41
|
+
// which projects as a spinner that never resolves. Probed against the claim: an
|
|
42
|
+
// `alwaysOn` query whose FIRST run dies — no key change anywhere, no retained
|
|
43
|
+
// payload — is `Loading` forever too. `gatedFlag(alwaysOn: true)` is `gated: false`,
|
|
44
|
+
// and `unsettled` has no Idle to return for an ungated query, so a query that will
|
|
45
|
+
// never answer has no state to be in. The stranding is real and pre-dates the key
|
|
46
|
+
// work; it is a missing `AsyncData` arm for "this query is broken", not a
|
|
47
|
+
// misattribution, and inventing one is a contract change for its owner to make.
|
|
48
|
+
//
|
|
49
|
+
// What the key work does owe is retraction in the raw state: a defect on the
|
|
50
|
+
// NEWEST run must stop advertising a flight, must not retract a successor's, and
|
|
51
|
+
// must not re-credit the retained payload to the key that failed.
|
|
52
|
+
// queryDriverSettle.ts:75-81.
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
it.live('a defect retracts the flight it was issued for without re-crediting the payload', () => {
|
|
55
|
+
class Kid extends State.make('auditStrandKey', S.String) {}
|
|
56
|
+
class Inputs extends StateGroup.make(Kid) {}
|
|
57
|
+
const kid = StateGroup.select(Inputs, 'auditStrandKey')
|
|
58
|
+
|
|
59
|
+
class Dying extends AsyncCalc.make('audit/strand-dying', {
|
|
60
|
+
inputs: [kid],
|
|
61
|
+
output: S.Number,
|
|
62
|
+
alwaysOn: true,
|
|
63
|
+
}) {}
|
|
64
|
+
|
|
65
|
+
const TestLayer = AsyncCalc.live(Dying, {
|
|
66
|
+
query: ({ auditStrandKey: key }): Effect.Effect<number> =>
|
|
67
|
+
key === 'a' ? Effect.succeed(1) : Effect.die(new Torn({ reason: key })),
|
|
68
|
+
}).pipe(
|
|
69
|
+
Layer.provideMerge(StateGroup.live(Inputs, { auditStrandKey: 'a' })),
|
|
70
|
+
Layer.provideMerge(Engine),
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
return Effect.gen(function* () {
|
|
74
|
+
const source = yield* kid.store
|
|
75
|
+
const dying = yield* Dying.store
|
|
76
|
+
const registry = yield* Queries
|
|
77
|
+
yield* until(
|
|
78
|
+
() => dying.get(),
|
|
79
|
+
(v) => v._tag === 'Success',
|
|
80
|
+
)
|
|
81
|
+
const snapshot = () =>
|
|
82
|
+
Option.map(Option.fromNullable(registry.byName.get('audit/strand-dying')), (query) =>
|
|
83
|
+
query.snapshot(),
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
const reads = (predicate: (state: QueryState<unknown, unknown>) => boolean) => () =>
|
|
87
|
+
Option.match(snapshot(), { onNone: () => false, onSome: predicate })
|
|
88
|
+
|
|
89
|
+
source.set('b')
|
|
90
|
+
// First the key move registers: a flight is open and the retained payload is
|
|
91
|
+
// credited to the key it answered…
|
|
92
|
+
yield* until(
|
|
93
|
+
reads((state) => state.isFetching && state.isPreviousKey),
|
|
94
|
+
(raised) => raised,
|
|
95
|
+
)
|
|
96
|
+
// …then the run dies and that flight — and only that flight — is retracted.
|
|
97
|
+
yield* until(
|
|
98
|
+
reads((state) => !state.isFetching),
|
|
99
|
+
(retracted) => retracted,
|
|
100
|
+
settleRounds,
|
|
101
|
+
)
|
|
102
|
+
// The run is over, so nothing claims to be in flight…
|
|
103
|
+
expect(Option.map(snapshot(), (state) => state.isFetching)).toStrictEqual(Option.some(false))
|
|
104
|
+
// …and key 'a' answer is still credited to key 'a', not silently to 'b'.
|
|
105
|
+
expect(Option.map(snapshot(), (state) => state.isPreviousKey)).toStrictEqual(Option.some(true))
|
|
106
|
+
}).pipe(Effect.provide(TestLayer))
|
|
107
|
+
})
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { expect, it } from '@effect/vitest'
|
|
2
|
+
import { Effect, Layer, Logger, ManagedRuntime, Option, Schema as S } from 'effect'
|
|
3
|
+
import { AsyncCalc, Engine, State, StateGroup } from '../index'
|
|
4
|
+
import { QueryStore, type QueryStoreApi } from './queryStore'
|
|
5
|
+
import { flush, until } from '../testkit/flight.testkit'
|
|
6
|
+
|
|
7
|
+
// The persisted cache is the one place a query's answer outlives the process that
|
|
8
|
+
// asked the question, so it is the one place the driver cannot re-derive the input
|
|
9
|
+
// key an entry belongs to — it has to have been written down. `persistCacheKey`
|
|
10
|
+
// (queryDriverPersist.ts:10) writes down whatever `persist.key` says, and the
|
|
11
|
+
// default `persist.key` (asyncCalc.ts, remoteState.ts) is the query's *name*.
|
|
12
|
+
//
|
|
13
|
+
// One slot per definition is the DOCUMENTED contract, not an oversight:
|
|
14
|
+
// remoteState.test.ts pins `persist: true` keying by name, and its neighbour pins
|
|
15
|
+
// the function-key form as the way to get a slot per input key. What follows pins
|
|
16
|
+
// the consequence of that default — because it really does hand one input key's
|
|
17
|
+
// answer to another — and the warning that now says so out loud, so nobody meets it
|
|
18
|
+
// only after a reload paints the wrong row.
|
|
19
|
+
|
|
20
|
+
const Profile = S.Struct({ chatId: S.String, canWrite: S.Boolean })
|
|
21
|
+
type Profile = S.Schema.Type<typeof Profile>
|
|
22
|
+
|
|
23
|
+
class ChatId extends State.make('chatId', S.String) {}
|
|
24
|
+
class Inputs extends StateGroup.make(ChatId) {}
|
|
25
|
+
const chatId = StateGroup.select(Inputs, 'chatId')
|
|
26
|
+
|
|
27
|
+
class ChatProfile extends AsyncCalc.make('persist-key/ChatProfile', {
|
|
28
|
+
inputs: [chatId],
|
|
29
|
+
output: Profile,
|
|
30
|
+
alwaysOn: true,
|
|
31
|
+
}) {}
|
|
32
|
+
|
|
33
|
+
type PersistTarget = true | { readonly key: (inputs: { readonly chatId: string }) => string }
|
|
34
|
+
|
|
35
|
+
const answer = ({ chatId: id }: { readonly chatId: string }): Effect.Effect<Profile> =>
|
|
36
|
+
Effect.succeed({ chatId: id, canWrite: id === 'A' })
|
|
37
|
+
|
|
38
|
+
// A process boundary in miniature: one `QueryStore` outlives two runtimes. The
|
|
39
|
+
// first asks about A, then about B; the second comes back up asking about A with
|
|
40
|
+
// its query parked, so the only thing that can paint is the cache.
|
|
41
|
+
const acrossSessions = async (persist: PersistTarget) => {
|
|
42
|
+
const entries = new Map<string, unknown>()
|
|
43
|
+
const warnings: Array<string> = []
|
|
44
|
+
const captureWarnings = Logger.replace(
|
|
45
|
+
Logger.defaultLogger,
|
|
46
|
+
Logger.make(({ message }) => {
|
|
47
|
+
warnings.push(String(message))
|
|
48
|
+
}),
|
|
49
|
+
)
|
|
50
|
+
const storeApi: QueryStoreApi = {
|
|
51
|
+
get: (key) => Effect.sync(() => Option.fromNullable(entries.get(key))),
|
|
52
|
+
set: (key, value) => Effect.sync(() => void entries.set(key, value)),
|
|
53
|
+
remove: (key) => Effect.sync(() => void entries.delete(key)),
|
|
54
|
+
}
|
|
55
|
+
const session = (query: (inputs: { readonly chatId: string }) => Effect.Effect<Profile>) =>
|
|
56
|
+
ManagedRuntime.make(
|
|
57
|
+
AsyncCalc.live(ChatProfile, { query, persist }).pipe(
|
|
58
|
+
Layer.provideMerge(StateGroup.live(Inputs, { chatId: 'A' })),
|
|
59
|
+
Layer.provideMerge(Layer.succeed(QueryStore, storeApi)),
|
|
60
|
+
Layer.provideMerge(Engine),
|
|
61
|
+
Layer.provideMerge(captureWarnings),
|
|
62
|
+
),
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
const first = session(answer)
|
|
66
|
+
const firstView = await first.runPromise(ChatProfile.store)
|
|
67
|
+
const source = await first.runPromise(chatId.store)
|
|
68
|
+
await first.runPromise(
|
|
69
|
+
until(
|
|
70
|
+
() => firstView.get(),
|
|
71
|
+
(view) => view._tag === 'Success' && view.refetching === false,
|
|
72
|
+
),
|
|
73
|
+
)
|
|
74
|
+
source.set('B')
|
|
75
|
+
const asked = await first.runPromise(
|
|
76
|
+
until(
|
|
77
|
+
() => firstView.get(),
|
|
78
|
+
(view) => view._tag === 'Success' && view.refetching === false && view.value.chatId === 'B',
|
|
79
|
+
),
|
|
80
|
+
)
|
|
81
|
+
expect(asked).toMatchObject({ _tag: 'Success', value: { chatId: 'B', canWrite: false } })
|
|
82
|
+
await first.dispose()
|
|
83
|
+
|
|
84
|
+
// Second process. `Effect.never` keeps the fresh answer out of the way, so
|
|
85
|
+
// whatever shows is exactly what hydration decided to paint.
|
|
86
|
+
const second = session(() => Effect.never)
|
|
87
|
+
const secondView = await second.runPromise(ChatProfile.store)
|
|
88
|
+
await second.runPromise(flush(8))
|
|
89
|
+
const hydrated = secondView.get()
|
|
90
|
+
await second.dispose()
|
|
91
|
+
return { hydrated, slots: [...entries.keys()], warnings }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
it('an input-keyed persist target gives each input key its own slot', async () => {
|
|
95
|
+
const keyed = await acrossSessions({ key: (inputs) => `persist-key/Chat:${inputs.chatId}` })
|
|
96
|
+
expect(keyed.slots).toHaveLength(2)
|
|
97
|
+
// A asked, A answered — across a process boundary, from cache alone.
|
|
98
|
+
expect(keyed.hydrated).toMatchObject({
|
|
99
|
+
_tag: 'Success',
|
|
100
|
+
value: { chatId: 'A', canWrite: true },
|
|
101
|
+
refetching: true,
|
|
102
|
+
})
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it("a shared persist slot hydrates another input key's answer, and says so", async () => {
|
|
106
|
+
const defaulted = await acrossSessions(true)
|
|
107
|
+
// One slot for both keys, so B's answer is the last write…
|
|
108
|
+
expect(defaulted.slots).toEqual(['persist-key/ChatProfile'])
|
|
109
|
+
// …and the session that asked about A reads it back as its own, `canWrite` and
|
|
110
|
+
// all. `isPreviousKey` cannot catch this: hydration is a fresh publish in a fresh
|
|
111
|
+
// process, with nothing left to say which key the payload answered.
|
|
112
|
+
expect(defaulted.hydrated).toMatchObject({
|
|
113
|
+
_tag: 'Success',
|
|
114
|
+
value: { chatId: 'B', canWrite: false },
|
|
115
|
+
})
|
|
116
|
+
// Which is why building it warns rather than doing it quietly.
|
|
117
|
+
expect(defaulted.warnings.join(' ')).toContain('single cache slot')
|
|
118
|
+
})
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { expect, it } from '@effect/vitest'
|
|
2
|
+
import { Data, Effect, Layer, ManagedRuntime, MutableRef, Schema as S } from 'effect'
|
|
3
|
+
import { AsyncCalc, Engine, forceSync } from '../index'
|
|
4
|
+
import { QueryStore } from './queryStore'
|
|
5
|
+
import { Queries } from '../runtime/queries'
|
|
6
|
+
import { until } from '../testkit/flight.testkit'
|
|
7
|
+
|
|
8
|
+
// Class bindings deliberately match their registered names: `AsyncCalc.invalidate`
|
|
9
|
+
// looks a query up by `calc.name`, and a class declaration shadows the static the
|
|
10
|
+
// framework installs. That divergence is its own finding, filed separately — these
|
|
11
|
+
// tests keep the names aligned so they isolate the driver, not the lookup.
|
|
12
|
+
|
|
13
|
+
const isStale = (name: string): Effect.Effect<boolean, never, Queries> =>
|
|
14
|
+
Effect.map(Queries, (registry) => registry.byName.get(name)?.snapshot().isStale ?? false)
|
|
15
|
+
|
|
16
|
+
const controlled = <A>() => {
|
|
17
|
+
const resolver = MutableRef.make<(value: A) => void>(() => {})
|
|
18
|
+
const promise = new Promise<A>((resume) => {
|
|
19
|
+
MutableRef.set(resolver, resume)
|
|
20
|
+
})
|
|
21
|
+
return { promise, resolve: (value: A) => MutableRef.get(resolver)(value) }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// `localStorage.setItem` raises QuotaExceededError for real, and the shipped
|
|
25
|
+
// `reform-query-browser` store wraps it in a bare `Effect.sync` — so the throw
|
|
26
|
+
// reaches the driver as a defect, exactly like this fake.
|
|
27
|
+
class QuotaExceeded extends Data.TaggedError('QuotaExceeded')<{ readonly key: string }> {}
|
|
28
|
+
|
|
29
|
+
const throwingWriteQueryStore = () => {
|
|
30
|
+
const writes = { attempts: 0 }
|
|
31
|
+
const layer = Layer.succeed(QueryStore, {
|
|
32
|
+
get: () => Effect.succeedNone,
|
|
33
|
+
set: (key: string) =>
|
|
34
|
+
Effect.sync(() => {
|
|
35
|
+
writes.attempts += 1
|
|
36
|
+
throw new QuotaExceeded({ key })
|
|
37
|
+
}),
|
|
38
|
+
remove: () => Effect.void,
|
|
39
|
+
})
|
|
40
|
+
return { writes, layer }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// `persistWriteTo` ends in `Effect.ignore`, which only swallows *typed* failures.
|
|
44
|
+
// A defect walks straight through it, out of `commitSuccess`, and kills the
|
|
45
|
+
// request consumer forked in `makeQueryDriver` — after which nothing drains the
|
|
46
|
+
// request queue and the query can never run again.
|
|
47
|
+
it('a throwing persist write permanently wedges the query consumer', async () => {
|
|
48
|
+
const persisted = throwingWriteQueryStore()
|
|
49
|
+
const runs = { n: 0 }
|
|
50
|
+
class SupersedeWedgedPersistQ extends AsyncCalc.make('SupersedeWedgedPersistQ', {
|
|
51
|
+
inputs: [],
|
|
52
|
+
output: S.Number,
|
|
53
|
+
alwaysOn: true,
|
|
54
|
+
}) {}
|
|
55
|
+
const runtime = ManagedRuntime.make(
|
|
56
|
+
AsyncCalc.live(SupersedeWedgedPersistQ, {
|
|
57
|
+
query: () =>
|
|
58
|
+
Effect.sync(() => {
|
|
59
|
+
runs.n += 1
|
|
60
|
+
return runs.n
|
|
61
|
+
}),
|
|
62
|
+
persist: true,
|
|
63
|
+
}).pipe(Layer.provideMerge(persisted.layer), Layer.provideMerge(Engine)),
|
|
64
|
+
)
|
|
65
|
+
const view = forceSync(() => runtime.runSync(SupersedeWedgedPersistQ.store))
|
|
66
|
+
|
|
67
|
+
await runtime.runPromise(
|
|
68
|
+
until(
|
|
69
|
+
() => view.get(),
|
|
70
|
+
(value) => value._tag === 'Success' && value.refetching === false,
|
|
71
|
+
),
|
|
72
|
+
)
|
|
73
|
+
expect(runs.n).toBe(1)
|
|
74
|
+
expect(persisted.writes.attempts).toBe(1)
|
|
75
|
+
|
|
76
|
+
// The visible write already landed before persistence was attempted, so a
|
|
77
|
+
// rejected cache write must not be able to take the driver down with it.
|
|
78
|
+
await runtime.runPromise(AsyncCalc.refetch(SupersedeWedgedPersistQ))
|
|
79
|
+
await runtime.runPromise(
|
|
80
|
+
until(
|
|
81
|
+
() => runs.n,
|
|
82
|
+
(n) => n >= 2,
|
|
83
|
+
40,
|
|
84
|
+
),
|
|
85
|
+
)
|
|
86
|
+
expect(runs.n).toBe(2)
|
|
87
|
+
expect(view.get()).toMatchObject({ _tag: 'Success', value: 2, refetching: false })
|
|
88
|
+
|
|
89
|
+
await runtime.dispose()
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
// `settleSuccess` clears `isStale` for whatever result lands next, without asking
|
|
93
|
+
// whether that result's generation actually covers the recorded stale cause. The
|
|
94
|
+
// driver's own rule — `hasCoveringRequest = generation > staleAtGeneration` — says
|
|
95
|
+
// generation N does NOT cover a stale mark taken at generation N: that request was
|
|
96
|
+
// already in flight when the invalidation happened, so its answer predates it.
|
|
97
|
+
it('a success from the invalidated generation erases the stale mark', async () => {
|
|
98
|
+
const fresh = controlled<number>()
|
|
99
|
+
const runs = { n: 0 }
|
|
100
|
+
class SupersedeInFlightInvalidateQ extends AsyncCalc.make('SupersedeInFlightInvalidateQ', {
|
|
101
|
+
inputs: [],
|
|
102
|
+
output: S.Number,
|
|
103
|
+
alwaysOn: true,
|
|
104
|
+
}) {}
|
|
105
|
+
const runtime = ManagedRuntime.make(
|
|
106
|
+
AsyncCalc.live(SupersedeInFlightInvalidateQ, {
|
|
107
|
+
query: () =>
|
|
108
|
+
Effect.sync(() => {
|
|
109
|
+
runs.n += 1
|
|
110
|
+
}).pipe(Effect.zipRight(Effect.promise(() => fresh.promise))),
|
|
111
|
+
}).pipe(Layer.provideMerge(Engine)),
|
|
112
|
+
)
|
|
113
|
+
const view = forceSync(() => runtime.runSync(SupersedeInFlightInvalidateQ.store))
|
|
114
|
+
|
|
115
|
+
// No subscriber yet, so `invalidate` records the mark instead of refetching.
|
|
116
|
+
await runtime.runPromise(
|
|
117
|
+
until(
|
|
118
|
+
() => runs.n,
|
|
119
|
+
(n) => n > 0,
|
|
120
|
+
),
|
|
121
|
+
)
|
|
122
|
+
await runtime.runPromise(AsyncCalc.invalidate(SupersedeInFlightInvalidateQ))
|
|
123
|
+
expect(await runtime.runPromise(isStale('SupersedeInFlightInvalidateQ'))).toBe(true)
|
|
124
|
+
|
|
125
|
+
fresh.resolve(7)
|
|
126
|
+
await runtime.runPromise(
|
|
127
|
+
until(
|
|
128
|
+
() => view.get(),
|
|
129
|
+
(value) => value._tag === 'Success' && value.refetching === false,
|
|
130
|
+
),
|
|
131
|
+
)
|
|
132
|
+
expect(view.get()).toMatchObject({ _tag: 'Success', value: 7, refetching: false })
|
|
133
|
+
|
|
134
|
+
// Generation 1's answer was requested before the invalidation, so it cannot
|
|
135
|
+
// discharge it: the value stays stale and a fresh reader has to revalidate.
|
|
136
|
+
expect(await runtime.runPromise(isStale('SupersedeInFlightInvalidateQ'))).toBe(true)
|
|
137
|
+
const off = view.subscribe(() => {})
|
|
138
|
+
await runtime.runPromise(
|
|
139
|
+
until(
|
|
140
|
+
() => runs.n,
|
|
141
|
+
(n) => n >= 2,
|
|
142
|
+
40,
|
|
143
|
+
),
|
|
144
|
+
)
|
|
145
|
+
expect(runs.n).toBe(2)
|
|
146
|
+
off()
|
|
147
|
+
|
|
148
|
+
await runtime.dispose()
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
// `invalidate` returns early whenever the state is already stale, so it never
|
|
152
|
+
// advances `staleAtGeneration` and never reaches `maybeAutoRefetch`. An
|
|
153
|
+
// invalidation raised while an earlier one's revalidation is still in flight is
|
|
154
|
+
// dropped whole: nothing re-requests, and the in-flight answer — which predates
|
|
155
|
+
// the second invalidation — settles as if it had covered it. This is the path
|
|
156
|
+
// `RefetchOnFocus` takes on every focus event.
|
|
157
|
+
it('invalidate is a no-op while a revalidation it does not cover is in flight', async () => {
|
|
158
|
+
const runs = MutableRef.make(0)
|
|
159
|
+
class SupersedeRepeatInvalidateQ extends AsyncCalc.make('SupersedeRepeatInvalidateQ', {
|
|
160
|
+
inputs: [],
|
|
161
|
+
output: S.Number,
|
|
162
|
+
alwaysOn: true,
|
|
163
|
+
}) {}
|
|
164
|
+
const runtime = ManagedRuntime.make(
|
|
165
|
+
AsyncCalc.live(SupersedeRepeatInvalidateQ, {
|
|
166
|
+
query: () =>
|
|
167
|
+
Effect.suspend(() => {
|
|
168
|
+
const attempt = MutableRef.updateAndGet(runs, (count) => count + 1)
|
|
169
|
+
return attempt === 1 ? Effect.succeed(1) : Effect.never
|
|
170
|
+
}),
|
|
171
|
+
}).pipe(Layer.provideMerge(Engine)),
|
|
172
|
+
)
|
|
173
|
+
const view = forceSync(() => runtime.runSync(SupersedeRepeatInvalidateQ.store))
|
|
174
|
+
|
|
175
|
+
await runtime.runPromise(
|
|
176
|
+
until(
|
|
177
|
+
() => view.get(),
|
|
178
|
+
(value) => value._tag === 'Success' && value.refetching === false,
|
|
179
|
+
),
|
|
180
|
+
)
|
|
181
|
+
expect(MutableRef.get(runs)).toBe(1)
|
|
182
|
+
|
|
183
|
+
// An active reader is what makes `invalidate` refetch rather than only mark.
|
|
184
|
+
const off = view.subscribe(() => {})
|
|
185
|
+
await runtime.runPromise(AsyncCalc.invalidate(SupersedeRepeatInvalidateQ))
|
|
186
|
+
await runtime.runPromise(
|
|
187
|
+
until(
|
|
188
|
+
() => MutableRef.get(runs),
|
|
189
|
+
(n) => n >= 2,
|
|
190
|
+
),
|
|
191
|
+
)
|
|
192
|
+
expect(MutableRef.get(runs)).toBe(2)
|
|
193
|
+
expect(await runtime.runPromise(isStale('SupersedeRepeatInvalidateQ'))).toBe(true)
|
|
194
|
+
|
|
195
|
+
// Generation 2 was already in flight when this second invalidation arrived, so
|
|
196
|
+
// it cannot answer it; a third request has to go out.
|
|
197
|
+
await runtime.runPromise(AsyncCalc.invalidate(SupersedeRepeatInvalidateQ))
|
|
198
|
+
await runtime.runPromise(
|
|
199
|
+
until(
|
|
200
|
+
() => MutableRef.get(runs),
|
|
201
|
+
(n) => n >= 3,
|
|
202
|
+
40,
|
|
203
|
+
),
|
|
204
|
+
)
|
|
205
|
+
expect(MutableRef.get(runs)).toBe(3)
|
|
206
|
+
off()
|
|
207
|
+
|
|
208
|
+
await runtime.dispose()
|
|
209
|
+
})
|