@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
|
@@ -1,22 +1,24 @@
|
|
|
1
|
-
import { Cause,
|
|
1
|
+
import { Cause, Effect, MutableRef, Option, Queue, Runtime, type Scope } from 'effect'
|
|
2
2
|
import { empty, type QueryState } from '../calc/queryState'
|
|
3
3
|
import { Queries, type QueryHandle } from '../runtime/queries'
|
|
4
|
-
import { reuse } from './reuse'
|
|
5
4
|
import { resolveInstrumentation } from '../runtime/instrumentation'
|
|
6
5
|
import { resolveQueryStore } from './queryStore'
|
|
6
|
+
import { makePersistWrite, persistCacheKey } from './queryDriverPersist'
|
|
7
|
+
import { makeSettlers } from './queryDriverSettle'
|
|
7
8
|
import { hydratePersistedQuery } from './queryDriverHydrate'
|
|
8
9
|
import {
|
|
9
10
|
incrementGeneration,
|
|
10
11
|
makeQueryProjection,
|
|
11
12
|
subscribeQuerySources,
|
|
12
|
-
settleQueryError,
|
|
13
13
|
withReaderTracking,
|
|
14
14
|
} from './queryDriverStore'
|
|
15
15
|
import type { QueryDriver, QueryDriverOptions } from './queryDriverTypes'
|
|
16
|
+
import { consumeRequests } from './queryDriverConsume'
|
|
16
17
|
import { resolveScheduler } from './scheduler'
|
|
17
18
|
import { type InputsObject, type InputStores, sameKey, wireSources } from './sources'
|
|
18
19
|
import { makeStore } from './store'
|
|
19
20
|
import { type AnySource } from '../state/token'
|
|
21
|
+
import { microtask } from './platform'
|
|
20
22
|
|
|
21
23
|
export {
|
|
22
24
|
bumpRevision,
|
|
@@ -44,19 +46,23 @@ export const makeQueryDriver = <Inputs extends ReadonlyArray<AnySource>, A, E, R
|
|
|
44
46
|
|
|
45
47
|
const keyOf = (args: InputsObject<Inputs>): ReadonlyArray<unknown> =>
|
|
46
48
|
extraKey === undefined ? sources.keyOf(args) : [...sources.keyOf(args), extraKey.read()]
|
|
47
|
-
const persistKey = (
|
|
48
|
-
key: string | ((inputs: InputsObject<Inputs>) => string),
|
|
49
|
-
args: InputsObject<Inputs>,
|
|
50
|
-
): string => (typeof key === 'function' ? key(args) : key)
|
|
51
49
|
const disabledNow = (args: InputsObject<Inputs> = sources.snapshot()): boolean =>
|
|
52
50
|
options.gated && options.disabled !== undefined ? options.disabled(args) : false
|
|
53
51
|
|
|
54
|
-
const
|
|
55
|
-
const
|
|
52
|
+
const keepPreviousData = options.keepPreviousData === true
|
|
53
|
+
const idle: QueryState<A, E> = empty(false, keepPreviousData)
|
|
54
|
+
const stateStore = makeStore<QueryState<A, E>>(
|
|
55
|
+
disabledNow() ? idle : empty(true, keepPreviousData),
|
|
56
|
+
scheduler,
|
|
57
|
+
)
|
|
56
58
|
|
|
57
59
|
const readers = MutableRef.make(0)
|
|
58
60
|
|
|
59
61
|
const lastKey = MutableRef.make<ReadonlyArray<unknown> | undefined>(undefined)
|
|
62
|
+
// Attribution tracks the *source* key, not `keyOf`: `extraKey` carries the
|
|
63
|
+
// invalidateOn revision, and a revision bump revalidates the same question
|
|
64
|
+
// rather than asking a new one.
|
|
65
|
+
const lastSourceKey = MutableRef.make<ReadonlyArray<unknown> | undefined>(undefined)
|
|
60
66
|
// Monotonic source epoch for the one startup hydration attempt. Comparing
|
|
61
67
|
// only the current key is insufficient: A → B → A would otherwise let the
|
|
62
68
|
// original A cache land over the later A request. Same-key refetches do not
|
|
@@ -68,50 +74,31 @@ export const makeQueryDriver = <Inputs extends ReadonlyArray<AnySource>, A, E, R
|
|
|
68
74
|
// captures this before the startup kick, while invalidation captures it when
|
|
69
75
|
// the value is marked stale.
|
|
70
76
|
const staleAtGeneration = MutableRef.make(0)
|
|
71
|
-
const markFetching = (
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const prev = stateStore.get()
|
|
79
|
-
const shared =
|
|
80
|
-
options.reuse === true && Option.isSome(prev.data)
|
|
81
|
-
? reuse(prev.data.value, produced)
|
|
82
|
-
: produced
|
|
83
|
-
stateStore.set({
|
|
84
|
-
data: Option.some(shared),
|
|
85
|
-
error: Option.none(),
|
|
86
|
-
isFetching: false,
|
|
87
|
-
isStale: false,
|
|
88
|
-
})
|
|
89
|
-
return shared
|
|
90
|
-
}
|
|
91
|
-
const settleError = (error: E, failedGeneration: number) => {
|
|
92
|
-
// Preserve the stale cause after failure without duplicating a newer request.
|
|
93
|
-
MutableRef.update(staleAtGeneration, (current) => Math.max(current, failedGeneration))
|
|
94
|
-
settleQueryError(stateStore, error)
|
|
95
|
-
}
|
|
77
|
+
const { markFetching, settleSuccess, settleError, settleDefect } = makeSettlers<A, E>({
|
|
78
|
+
stateStore,
|
|
79
|
+
lastSourceKey,
|
|
80
|
+
generation,
|
|
81
|
+
staleAtGeneration,
|
|
82
|
+
reuse: options.reuse === true,
|
|
83
|
+
})
|
|
96
84
|
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
Effect.flatMap((encoded) => queryStore.set(persistKey(target.key, args), encoded)),
|
|
104
|
-
// Persistence is best-effort and must not block the visible write.
|
|
105
|
-
Effect.ignore,
|
|
106
|
-
)
|
|
107
|
-
const persistWrite = (produced: A, args: InputsObject<Inputs>): Effect.Effect<void> =>
|
|
108
|
-
persist === undefined ? Effect.void : persistWriteTo(persist, produced, args)
|
|
85
|
+
const persistWrite = makePersistWrite({
|
|
86
|
+
queryStore,
|
|
87
|
+
persist,
|
|
88
|
+
label: options.label,
|
|
89
|
+
name: options.name,
|
|
90
|
+
})
|
|
109
91
|
|
|
110
|
-
const derived = makeQueryProjection({
|
|
92
|
+
const derived = makeQueryProjection({
|
|
93
|
+
gated: options.gated,
|
|
94
|
+
scheduler,
|
|
95
|
+
stateStore,
|
|
96
|
+
})
|
|
111
97
|
yield* Effect.addFinalizer(() => Effect.sync(derived.unsubscribe))
|
|
112
98
|
|
|
113
99
|
interface Request {
|
|
114
100
|
readonly args: InputsObject<Inputs>
|
|
101
|
+
readonly sourceKey: ReadonlyArray<unknown>
|
|
115
102
|
readonly generation: number
|
|
116
103
|
}
|
|
117
104
|
|
|
@@ -120,7 +107,7 @@ export const makeQueryDriver = <Inputs extends ReadonlyArray<AnySource>, A, E, R
|
|
|
120
107
|
return Effect.void
|
|
121
108
|
}
|
|
122
109
|
return Effect.suspend(() => {
|
|
123
|
-
const shared = settleSuccess(produced)
|
|
110
|
+
const shared = settleSuccess(produced, request)
|
|
124
111
|
options.onSettled?.(request.generation)
|
|
125
112
|
return persistWrite(shared, request.args)
|
|
126
113
|
})
|
|
@@ -137,9 +124,11 @@ export const makeQueryDriver = <Inputs extends ReadonlyArray<AnySource>, A, E, R
|
|
|
137
124
|
Effect.matchCause({
|
|
138
125
|
onSuccess: (produced): Option.Option<A> => Option.some(produced),
|
|
139
126
|
onFailure: (cause): Option.Option<A> => {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
127
|
+
if (!disabledNow()) {
|
|
128
|
+
Option.match(Cause.failureOption(cause), {
|
|
129
|
+
onSome: (error) => settleError(error, request),
|
|
130
|
+
onNone: () => settleDefect(request),
|
|
131
|
+
})
|
|
143
132
|
}
|
|
144
133
|
return Option.none()
|
|
145
134
|
},
|
|
@@ -152,27 +141,15 @@ export const makeQueryDriver = <Inputs extends ReadonlyArray<AnySource>, A, E, R
|
|
|
152
141
|
),
|
|
153
142
|
)
|
|
154
143
|
|
|
155
|
-
const trailing = options.coalesce === 'trailing'
|
|
156
144
|
const requests = yield* Queue.unbounded<Request>()
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
yield* runQuery(request)
|
|
164
|
-
}
|
|
165
|
-
const pending = yield* Queue.size(requests)
|
|
166
|
-
if (pending > 0 && !disabledNow()) {
|
|
167
|
-
yield* Effect.sync(markFetching)
|
|
168
|
-
}
|
|
145
|
+
yield* Effect.forkScoped(
|
|
146
|
+
consumeRequests(options.coalesce === 'trailing', {
|
|
147
|
+
requests,
|
|
148
|
+
runQuery,
|
|
149
|
+
disabledNow: () => disabledNow(),
|
|
150
|
+
markFetching,
|
|
169
151
|
}),
|
|
170
152
|
)
|
|
171
|
-
const switchConsumer = Stream.fromQueue(requests).pipe(
|
|
172
|
-
Stream.flatMap((request) => Stream.fromEffect(runQuery(request)), { switch: true }),
|
|
173
|
-
Stream.runDrain,
|
|
174
|
-
)
|
|
175
|
-
yield* Effect.forkScoped(trailing ? trailingConsumer : switchConsumer)
|
|
176
153
|
|
|
177
154
|
const enqueue = (force: boolean) => {
|
|
178
155
|
const args = sources.snapshot()
|
|
@@ -181,6 +158,7 @@ export const makeQueryDriver = <Inputs extends ReadonlyArray<AnySource>, A, E, R
|
|
|
181
158
|
MutableRef.update(sourceEpoch, (current) => current + 1)
|
|
182
159
|
}
|
|
183
160
|
MutableRef.set(lastKey, undefined)
|
|
161
|
+
MutableRef.set(lastSourceKey, undefined)
|
|
184
162
|
stateStore.set(idle)
|
|
185
163
|
return
|
|
186
164
|
}
|
|
@@ -189,12 +167,16 @@ export const makeQueryDriver = <Inputs extends ReadonlyArray<AnySource>, A, E, R
|
|
|
189
167
|
if (!force && previous !== undefined && sameKey(key, previous)) {
|
|
190
168
|
return
|
|
191
169
|
}
|
|
170
|
+
const sourceKey = sources.keyOf(args)
|
|
171
|
+
const previousSourceKey = MutableRef.get(lastSourceKey)
|
|
172
|
+
const changedKey = previousSourceKey !== undefined && !sameKey(sourceKey, previousSourceKey)
|
|
192
173
|
if (previous !== undefined && !sameKey(key, previous)) {
|
|
193
174
|
MutableRef.update(sourceEpoch, (current) => current + 1)
|
|
194
175
|
}
|
|
195
176
|
MutableRef.set(lastKey, key)
|
|
196
|
-
|
|
197
|
-
|
|
177
|
+
MutableRef.set(lastSourceKey, sourceKey)
|
|
178
|
+
markFetching(changedKey)
|
|
179
|
+
Queue.unsafeOffer(requests, { args, sourceKey, generation: nextGeneration() })
|
|
198
180
|
}
|
|
199
181
|
const trigger = () => enqueue(false)
|
|
200
182
|
const refetch = () => enqueue(true)
|
|
@@ -205,14 +187,32 @@ export const makeQueryDriver = <Inputs extends ReadonlyArray<AnySource>, A, E, R
|
|
|
205
187
|
refetch()
|
|
206
188
|
}
|
|
207
189
|
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
190
|
+
// Every invalidation in a frame is one signal, not N: the mark is raised
|
|
191
|
+
// synchronously but the response is coalesced onto a microtask, so a storm
|
|
192
|
+
// (a focus burst, several mutations acking together) issues one request. A
|
|
193
|
+
// later frame arms it again — that invalidation is genuinely uncovered by a
|
|
194
|
+
// request issued before it.
|
|
195
|
+
const refetchArmed = MutableRef.make(false)
|
|
196
|
+
const scheduleAutoRefetch = () => {
|
|
197
|
+
if (MutableRef.get(refetchArmed)) {
|
|
211
198
|
return
|
|
212
199
|
}
|
|
200
|
+
MutableRef.set(refetchArmed, true)
|
|
201
|
+
microtask(() => {
|
|
202
|
+
MutableRef.set(refetchArmed, false)
|
|
203
|
+
maybeAutoRefetch()
|
|
204
|
+
})
|
|
205
|
+
}
|
|
206
|
+
const invalidate = () => {
|
|
207
|
+
const state = stateStore.get()
|
|
208
|
+
// Always advance the mark: an invalidation raised while an earlier one's
|
|
209
|
+
// revalidation is in flight is not covered by that flight, so returning early
|
|
210
|
+
// on `isStale` would drop it whole (the refetch-on-focus path).
|
|
213
211
|
MutableRef.set(staleAtGeneration, MutableRef.get(generation))
|
|
214
|
-
|
|
215
|
-
|
|
212
|
+
if (!state.isStale) {
|
|
213
|
+
stateStore.set({ ...state, isStale: true })
|
|
214
|
+
}
|
|
215
|
+
scheduleAutoRefetch()
|
|
216
216
|
}
|
|
217
217
|
|
|
218
218
|
const unsubscribe = subscribeQuerySources(sources, extraKey, trigger)
|
|
@@ -225,7 +225,7 @@ export const makeQueryDriver = <Inputs extends ReadonlyArray<AnySource>, A, E, R
|
|
|
225
225
|
const hydrateSourceEpoch = MutableRef.get(sourceEpoch)
|
|
226
226
|
const hydrate = hydratePersistedQuery({
|
|
227
227
|
queryStore,
|
|
228
|
-
cacheKey:
|
|
228
|
+
cacheKey: persistCacheKey(persist.key, hydrateArgs),
|
|
229
229
|
codec: persist.codec,
|
|
230
230
|
stateStore,
|
|
231
231
|
staleAtGeneration,
|
|
@@ -242,9 +242,16 @@ export const makeQueryDriver = <Inputs extends ReadonlyArray<AnySource>, A, E, R
|
|
|
242
242
|
|
|
243
243
|
const initial = sources.snapshot()
|
|
244
244
|
if (!disabledNow(initial)) {
|
|
245
|
-
|
|
245
|
+
const initialKey = keyOf(initial)
|
|
246
|
+
const initialSourceKey = sources.keyOf(initial)
|
|
247
|
+
MutableRef.set(lastKey, initialKey)
|
|
248
|
+
MutableRef.set(lastSourceKey, initialSourceKey)
|
|
246
249
|
markFetching()
|
|
247
|
-
Queue.unsafeOffer(requests, {
|
|
250
|
+
Queue.unsafeOffer(requests, {
|
|
251
|
+
args: initial,
|
|
252
|
+
sourceKey: initialSourceKey,
|
|
253
|
+
generation: nextGeneration(),
|
|
254
|
+
})
|
|
248
255
|
}
|
|
249
256
|
|
|
250
257
|
const store = withReaderTracking({
|
|
@@ -271,5 +278,10 @@ export const makeQueryDriver = <Inputs extends ReadonlyArray<AnySource>, A, E, R
|
|
|
271
278
|
yield* Effect.addFinalizer(() => Effect.sync(() => queries.unregister(handle)))
|
|
272
279
|
}
|
|
273
280
|
|
|
274
|
-
return {
|
|
281
|
+
return {
|
|
282
|
+
store,
|
|
283
|
+
requested: () => MutableRef.get(generation),
|
|
284
|
+
key: () => sources.keyOf(sources.snapshot()),
|
|
285
|
+
subscribeKey: sources.subscribe,
|
|
286
|
+
}
|
|
275
287
|
})
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Chunk, Effect, Option, type Queue, Stream } from 'effect'
|
|
2
|
+
|
|
3
|
+
// How a driver drains its request queue. `switch` keeps only the newest run alive;
|
|
4
|
+
// `trailing` runs one at a time and collapses everything that piled up behind it,
|
|
5
|
+
// re-raising the fetching mark when more is still waiting.
|
|
6
|
+
export interface ConsumeOptions<Request, R> {
|
|
7
|
+
readonly requests: Queue.Queue<Request>
|
|
8
|
+
readonly runQuery: (request: Request) => Effect.Effect<void, never, R>
|
|
9
|
+
readonly disabledNow: () => boolean
|
|
10
|
+
readonly markFetching: () => void
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const trailingConsumer = <Request, R>(
|
|
14
|
+
options: ConsumeOptions<Request, R>,
|
|
15
|
+
): Effect.Effect<never, never, R> =>
|
|
16
|
+
Effect.forever(
|
|
17
|
+
Effect.gen(function* () {
|
|
18
|
+
const first = yield* options.requests.take
|
|
19
|
+
const queued = yield* options.requests.takeAll
|
|
20
|
+
const request = Option.getOrElse(Chunk.last(queued), () => first)
|
|
21
|
+
if (!options.disabledNow()) {
|
|
22
|
+
yield* options.runQuery(request)
|
|
23
|
+
}
|
|
24
|
+
const pending = yield* options.requests.size
|
|
25
|
+
if (pending > 0 && !options.disabledNow()) {
|
|
26
|
+
yield* Effect.sync(options.markFetching)
|
|
27
|
+
}
|
|
28
|
+
}),
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
const switchConsumer = <Request, R>(
|
|
32
|
+
options: ConsumeOptions<Request, R>,
|
|
33
|
+
): Effect.Effect<void, never, R> =>
|
|
34
|
+
Stream.fromQueue(options.requests).pipe(
|
|
35
|
+
Stream.flatMap((request) => Stream.fromEffect(options.runQuery(request)), { switch: true }),
|
|
36
|
+
Stream.runDrain,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
export const consumeRequests = <Request, R>(
|
|
40
|
+
trailing: boolean,
|
|
41
|
+
options: ConsumeOptions<Request, R>,
|
|
42
|
+
): Effect.Effect<void, never, R> => (trailing ? trailingConsumer(options) : switchConsumer(options))
|
|
@@ -23,7 +23,12 @@ const paint = <A, E>(options: HydratePersistedOptions<A, E>, decoded: A): void =
|
|
|
23
23
|
MutableRef.update(options.staleAtGeneration, (current) =>
|
|
24
24
|
Math.max(current, options.capturedGeneration),
|
|
25
25
|
)
|
|
26
|
-
options.stateStore.set({
|
|
26
|
+
options.stateStore.set({
|
|
27
|
+
...state,
|
|
28
|
+
data: Option.some(decoded),
|
|
29
|
+
isStale: true,
|
|
30
|
+
isPreviousKey: false,
|
|
31
|
+
})
|
|
27
32
|
}
|
|
28
33
|
|
|
29
34
|
// Epoch, key, and settled-state guards prevent a late cache from replacing newer truth.
|
|
@@ -40,6 +45,7 @@ export const hydratePersistedQuery = <A, E>(
|
|
|
40
45
|
.pipe(Effect.tap((decoded) => Effect.sync(() => paint(options, decoded)))),
|
|
41
46
|
}),
|
|
42
47
|
),
|
|
43
|
-
// Corrupt/incompatible cache: ignore and fetch fresh.
|
|
44
|
-
|
|
48
|
+
// Corrupt/incompatible cache: ignore and fetch fresh. Catches defects too — a
|
|
49
|
+
// store backed by localStorage throws rather than failing typed.
|
|
50
|
+
Effect.catchAllCause(() => Effect.void),
|
|
45
51
|
)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Effect } from 'effect'
|
|
2
|
+
import type { QueryStoreApi } from './queryStore'
|
|
3
|
+
import type { QueryCodec } from './queryDriverTypes'
|
|
4
|
+
|
|
5
|
+
export interface PersistTarget<Args, A> {
|
|
6
|
+
readonly key: string | ((inputs: Args) => string)
|
|
7
|
+
readonly codec: QueryCodec<A>
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const persistCacheKey = <Args>(
|
|
11
|
+
key: string | ((inputs: Args) => string),
|
|
12
|
+
args: Args,
|
|
13
|
+
): string => (typeof key === 'function' ? key(args) : key)
|
|
14
|
+
|
|
15
|
+
export interface PersistWriterOptions<Args, A> {
|
|
16
|
+
readonly queryStore: QueryStoreApi
|
|
17
|
+
readonly persist: PersistTarget<Args, A> | undefined
|
|
18
|
+
readonly label: string
|
|
19
|
+
readonly name: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const makePersistWrite = <Args, A>(
|
|
23
|
+
options: PersistWriterOptions<Args, A>,
|
|
24
|
+
): ((produced: A, args: Args) => Effect.Effect<void>) => {
|
|
25
|
+
const target = options.persist
|
|
26
|
+
if (target === undefined) {
|
|
27
|
+
return () => Effect.void
|
|
28
|
+
}
|
|
29
|
+
return (produced, args) =>
|
|
30
|
+
target.codec.encode(produced).pipe(
|
|
31
|
+
Effect.flatMap((encoded) =>
|
|
32
|
+
options.queryStore.set(persistCacheKey(target.key, args), encoded),
|
|
33
|
+
),
|
|
34
|
+
// Persistence is best-effort and must not block the visible write. This
|
|
35
|
+
// catches defects, not just typed failures: a store backed by localStorage
|
|
36
|
+
// *throws* when quota is exhausted, and `Effect.ignore` would let that escape
|
|
37
|
+
// and kill the request consumer for the rest of the session.
|
|
38
|
+
Effect.catchAllCause((cause) =>
|
|
39
|
+
Effect.logWarning(`reform: ${options.label} '${options.name}' persist failed`, cause),
|
|
40
|
+
),
|
|
41
|
+
)
|
|
42
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Effect, Predicate } from 'effect'
|
|
2
|
+
|
|
3
|
+
// Mirrors the public `persist` config, whose shape is the author's to write.
|
|
4
|
+
export type PersistOptionExternalApi<Args> =
|
|
5
|
+
| boolean
|
|
6
|
+
| undefined
|
|
7
|
+
| { readonly key?: string | ((inputs: Args) => string) }
|
|
8
|
+
|
|
9
|
+
export interface PersistSlotInput<Args> {
|
|
10
|
+
readonly label: string
|
|
11
|
+
readonly name: string
|
|
12
|
+
readonly inputs: number
|
|
13
|
+
readonly option: PersistOptionExternalApi<Args>
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface SharedPersistSlot {
|
|
17
|
+
readonly label: string
|
|
18
|
+
readonly name: string
|
|
19
|
+
readonly inputs: number
|
|
20
|
+
readonly key: string | ((inputs: never) => string)
|
|
21
|
+
readonly enabled: boolean
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// The cache slot a query persists into, plus the warning if that slot is shared.
|
|
25
|
+
export const resolvePersistSlot = <Args>(
|
|
26
|
+
input: PersistSlotInput<Args>,
|
|
27
|
+
): Effect.Effect<string | ((inputs: Args) => string)> => {
|
|
28
|
+
const option = input.option
|
|
29
|
+
const key =
|
|
30
|
+
option === true || option === undefined || option === false
|
|
31
|
+
? input.name
|
|
32
|
+
: (option.key ?? input.name)
|
|
33
|
+
return Effect.as(
|
|
34
|
+
warnSharedPersistSlot({
|
|
35
|
+
label: input.label,
|
|
36
|
+
name: input.name,
|
|
37
|
+
inputs: input.inputs,
|
|
38
|
+
key,
|
|
39
|
+
enabled: option !== undefined && option !== false,
|
|
40
|
+
}),
|
|
41
|
+
key,
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// A constant `persist` key gives every input key ONE cache slot: the last key to
|
|
46
|
+
// settle overwrites the rest, and on the next boot that payload is hydrated as the
|
|
47
|
+
// answer to whichever key is being asked. Sharing a slot on purpose is a legitimate
|
|
48
|
+
// choice — a "last seen" cache — so this warns rather than refuses, but it must not
|
|
49
|
+
// be silent: it is the same misattribution `isPreviousKey` exists to stop, arriving
|
|
50
|
+
// across a reload where no epoch or key guard can see it.
|
|
51
|
+
const warnSharedPersistSlot = (slot: SharedPersistSlot): Effect.Effect<void> => {
|
|
52
|
+
if (!slot.enabled || slot.inputs === 0 || !Predicate.isString(slot.key)) {
|
|
53
|
+
return Effect.void
|
|
54
|
+
}
|
|
55
|
+
return Effect.logWarning(
|
|
56
|
+
`reform: ${slot.label} '${slot.name}' persists ${slot.inputs} input(s) into the single cache slot ` +
|
|
57
|
+
`'${slot.key}'. Every input key overwrites the same entry, and a reload hydrates it as the answer ` +
|
|
58
|
+
`to whichever key is being asked. Pass persist: { key: (inputs) => ... } for a slot per key, or ` +
|
|
59
|
+
`keep the shared slot deliberately.`,
|
|
60
|
+
)
|
|
61
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { MutableRef, Option } from 'effect'
|
|
2
|
+
import type { QueryState } from '../calc/queryState'
|
|
3
|
+
import { settleQueryError } from './queryDriverStore'
|
|
4
|
+
import { reuse } from './reuse'
|
|
5
|
+
import { sameKey } from './sources'
|
|
6
|
+
import type { Store } from './store'
|
|
7
|
+
|
|
8
|
+
// What a request is an answer to. `sourceKey` is the *input* key, excluding
|
|
9
|
+
// `extraKey`: a revision bump revalidates the same question rather than asking a
|
|
10
|
+
// new one, so it must not read as a key change.
|
|
11
|
+
export interface SettleRequest {
|
|
12
|
+
readonly sourceKey: ReadonlyArray<unknown>
|
|
13
|
+
readonly generation: number
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface SettlersOptions<A, E> {
|
|
17
|
+
readonly stateStore: Store<QueryState<A, E>>
|
|
18
|
+
readonly lastSourceKey: MutableRef.MutableRef<ReadonlyArray<unknown> | undefined>
|
|
19
|
+
readonly generation: MutableRef.MutableRef<number>
|
|
20
|
+
readonly staleAtGeneration: MutableRef.MutableRef<number>
|
|
21
|
+
readonly reuse: boolean
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface Settlers<A, E> {
|
|
25
|
+
readonly supersededKey: (requestSourceKey: ReadonlyArray<unknown>) => boolean
|
|
26
|
+
readonly markFetching: (superseded?: boolean) => void
|
|
27
|
+
readonly settleSuccess: (produced: A, request: SettleRequest) => A
|
|
28
|
+
readonly settleError: (error: E, request: SettleRequest) => void
|
|
29
|
+
readonly settleDefect: (request: SettleRequest) => void
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const makeSettlers = <A, E>(options: SettlersOptions<A, E>): Settlers<A, E> => {
|
|
33
|
+
const { stateStore } = options
|
|
34
|
+
|
|
35
|
+
// A settling request answers the key it was issued for. If the sources have
|
|
36
|
+
// moved on, whatever it writes is about a question nobody is asking any more.
|
|
37
|
+
const supersededKey = (requestSourceKey: ReadonlyArray<unknown>): boolean => {
|
|
38
|
+
const current = MutableRef.get(options.lastSourceKey)
|
|
39
|
+
return current === undefined || !sameKey(requestSourceKey, current)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const markFetching = (superseded = false): void => {
|
|
43
|
+
const state = stateStore.get()
|
|
44
|
+
const isPreviousKey = state.isPreviousKey || superseded
|
|
45
|
+
if (!state.isFetching || isPreviousKey !== state.isPreviousKey) {
|
|
46
|
+
stateStore.set({ ...state, isFetching: true, isPreviousKey })
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const settleSuccess = (produced: A, request: SettleRequest): A => {
|
|
51
|
+
const prev = stateStore.get()
|
|
52
|
+
const shared =
|
|
53
|
+
options.reuse && Option.isSome(prev.data) ? reuse(prev.data.value, produced) : produced
|
|
54
|
+
stateStore.set({
|
|
55
|
+
...prev,
|
|
56
|
+
data: Option.some(shared),
|
|
57
|
+
error: Option.none(),
|
|
58
|
+
isFetching: false,
|
|
59
|
+
// An answer requested before an invalidation cannot discharge it: the mark
|
|
60
|
+
// clears only for a strictly newer generation, matching `maybeAutoRefetch`.
|
|
61
|
+
isStale: request.generation <= MutableRef.get(options.staleAtGeneration),
|
|
62
|
+
isPreviousKey: supersededKey(request.sourceKey),
|
|
63
|
+
})
|
|
64
|
+
return shared
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const settleError = (error: E, request: SettleRequest): void => {
|
|
68
|
+
// Preserve the stale cause after failure without duplicating a newer request.
|
|
69
|
+
MutableRef.update(options.staleAtGeneration, (current) => Math.max(current, request.generation))
|
|
70
|
+
settleQueryError(stateStore, error, supersededKey(request.sourceKey))
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// A run that dies is over; leaving `isFetching` raised advertises a flight that
|
|
74
|
+
// no longer exists. Only the newest run may retract it — a superseded run's
|
|
75
|
+
// death says nothing about its successor.
|
|
76
|
+
const settleDefect = (request: SettleRequest): void => {
|
|
77
|
+
if (MutableRef.get(options.generation) !== request.generation) {
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
const state = stateStore.get()
|
|
81
|
+
stateStore.set({ ...state, isFetching: false })
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return { supersededKey, markFetching, settleSuccess, settleError, settleDefect }
|
|
85
|
+
}
|
|
@@ -1,78 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { MutableRef, Option } from 'effect'
|
|
2
2
|
import type { AnyAsyncData } from '../calc/asyncData'
|
|
3
3
|
import { type QueryState, toAsyncData } from '../calc/queryState'
|
|
4
|
-
import type { QueryStoreApi } from './queryStore'
|
|
5
4
|
import type { Scheduler } from './scheduler'
|
|
6
5
|
import { makeDerivedStore, type Store } from './store'
|
|
7
|
-
import type { AnySchema } from './variance'
|
|
8
|
-
import type { InputsObject } from './sources'
|
|
9
|
-
import type { AnySource } from '../state/token'
|
|
10
|
-
|
|
11
|
-
interface HydrateQueryOptions<A, E> {
|
|
12
|
-
readonly cacheKey: string
|
|
13
|
-
readonly queryStore: QueryStoreApi
|
|
14
|
-
readonly schema: AnySchema<A>
|
|
15
|
-
readonly stateStore: Store<QueryState<A, E>>
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export const hydrateQuery = <A, E>(options: HydrateQueryOptions<A, E>): Effect.Effect<void> =>
|
|
19
|
-
Effect.flatMap(options.queryStore.get(options.cacheKey), (cached) =>
|
|
20
|
-
Option.match(cached, {
|
|
21
|
-
onNone: () => Effect.void,
|
|
22
|
-
onSome: (raw) =>
|
|
23
|
-
Schema.decodeUnknown(options.schema)(raw).pipe(
|
|
24
|
-
Effect.match({
|
|
25
|
-
onFailure: () => undefined,
|
|
26
|
-
onSuccess: (decoded) =>
|
|
27
|
-
options.stateStore.set({
|
|
28
|
-
data: Option.some(decoded),
|
|
29
|
-
error: Option.none(),
|
|
30
|
-
isFetching: false,
|
|
31
|
-
isStale: true,
|
|
32
|
-
}),
|
|
33
|
-
}),
|
|
34
|
-
),
|
|
35
|
-
}),
|
|
36
|
-
)
|
|
37
|
-
|
|
38
|
-
interface PersistQueryValueOptions<A> {
|
|
39
|
-
readonly cacheKey: string
|
|
40
|
-
readonly produced: A
|
|
41
|
-
readonly queryStore: QueryStoreApi
|
|
42
|
-
readonly schema: AnySchema<A>
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export const persistQueryValue = <A>(options: PersistQueryValueOptions<A>): Effect.Effect<void> =>
|
|
46
|
-
Schema.encode(options.schema)(options.produced).pipe(
|
|
47
|
-
Effect.flatMap((encoded) => options.queryStore.set(options.cacheKey, encoded)),
|
|
48
|
-
Effect.ignore,
|
|
49
|
-
)
|
|
50
|
-
|
|
51
|
-
interface PersistWriterOptions<Inputs extends ReadonlyArray<AnySource>, A> {
|
|
52
|
-
readonly persist:
|
|
53
|
-
| {
|
|
54
|
-
readonly key: string | ((inputs: InputsObject<Inputs>) => string)
|
|
55
|
-
readonly schema: AnySchema<A>
|
|
56
|
-
}
|
|
57
|
-
| undefined
|
|
58
|
-
readonly queryStore: QueryStoreApi
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export const makePersistWriter = <Inputs extends ReadonlyArray<AnySource>, A>(
|
|
62
|
-
options: PersistWriterOptions<Inputs, A>,
|
|
63
|
-
): ((produced: A, inputs: InputsObject<Inputs>) => Effect.Effect<void>) => {
|
|
64
|
-
if (options.persist === undefined) {
|
|
65
|
-
return () => Effect.void
|
|
66
|
-
}
|
|
67
|
-
const persist = options.persist
|
|
68
|
-
return (produced, inputs) =>
|
|
69
|
-
persistQueryValue({
|
|
70
|
-
cacheKey: typeof persist.key === 'function' ? persist.key(inputs) : persist.key,
|
|
71
|
-
produced,
|
|
72
|
-
queryStore: options.queryStore,
|
|
73
|
-
schema: persist.schema,
|
|
74
|
-
})
|
|
75
|
-
}
|
|
76
6
|
|
|
77
7
|
interface SubscribableSource {
|
|
78
8
|
readonly subscribe: (listener: () => void) => () => void
|
|
@@ -91,9 +21,13 @@ export const subscribeQuerySources = (
|
|
|
91
21
|
}
|
|
92
22
|
}
|
|
93
23
|
|
|
94
|
-
export const settleQueryError = <A, E>(
|
|
24
|
+
export const settleQueryError = <A, E>(
|
|
25
|
+
stateStore: Store<QueryState<A, E>>,
|
|
26
|
+
error: E,
|
|
27
|
+
isPreviousKey: boolean,
|
|
28
|
+
): void => {
|
|
95
29
|
const previous = stateStore.get()
|
|
96
|
-
stateStore.set({ ...previous, error: Option.some(error), isFetching: false })
|
|
30
|
+
stateStore.set({ ...previous, error: Option.some(error), isFetching: false, isPreviousKey })
|
|
97
31
|
}
|
|
98
32
|
|
|
99
33
|
export const incrementGeneration = (generation: MutableRef.MutableRef<number>): number => {
|
|
@@ -120,7 +54,8 @@ export const makeQueryProjection = <A, E>(
|
|
|
120
54
|
previous !== undefined &&
|
|
121
55
|
previous.state.data === state.data &&
|
|
122
56
|
previous.state.error === state.error &&
|
|
123
|
-
previous.state.isFetching === state.isFetching
|
|
57
|
+
previous.state.isFetching === state.isFetching &&
|
|
58
|
+
previous.state.isPreviousKey === state.isPreviousKey
|
|
124
59
|
) {
|
|
125
60
|
return previous.view
|
|
126
61
|
}
|
|
@@ -36,6 +36,9 @@ export interface QueryDriverOptions<Inputs extends ReadonlyArray<AnySource>, A,
|
|
|
36
36
|
readonly disabled?: ((inputs: InputsObject<Inputs>) => boolean) | undefined
|
|
37
37
|
readonly coalesce?: 'switch' | 'trailing' | undefined
|
|
38
38
|
readonly reuse?: boolean | undefined
|
|
39
|
+
// Show the previous key's result while the new key is in flight. Off by default:
|
|
40
|
+
// a retained payload is not an answer to the key being asked.
|
|
41
|
+
readonly keepPreviousData?: boolean | undefined
|
|
39
42
|
readonly extraKey?:
|
|
40
43
|
| {
|
|
41
44
|
readonly read: () => unknown
|
|
@@ -54,4 +57,11 @@ export interface QueryDriverOptions<Inputs extends ReadonlyArray<AnySource>, A,
|
|
|
54
57
|
export interface QueryDriver<A, E> {
|
|
55
58
|
readonly store: Store<AnyAsyncData<A, E>>
|
|
56
59
|
readonly requested: () => number
|
|
60
|
+
// The current *input* key, excluding `extraKey`: what a result or an optimistic
|
|
61
|
+
// intent is about. A revision bump revalidates the same question.
|
|
62
|
+
readonly key: () => ReadonlyArray<unknown>
|
|
63
|
+
// Wake on a key move. A reader that derives from the key as well as the payload
|
|
64
|
+
// cannot see one through the store: a key change that settles nothing writes
|
|
65
|
+
// nothing, so the payload it reads is unchanged while its meaning is not.
|
|
66
|
+
readonly subscribeKey: (listener: () => void) => () => void
|
|
57
67
|
}
|