@playfast/reform 1.0.1 → 1.2.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 +103 -56
- package/package.json +17 -17
- package/src/boundary/boundary.test.ts +63 -49
- package/src/boundary/boundary.ts +144 -113
- package/src/calc/asyncCalc.invalidate.test.ts +518 -32
- package/src/calc/asyncCalc.test.ts +207 -213
- package/src/calc/asyncCalc.ts +131 -154
- package/src/calc/asyncData.ts +0 -37
- package/src/calc/calc.test.ts +7 -33
- package/src/calc/calc.ts +44 -58
- package/src/calc/calcFamily.test.ts +80 -34
- package/src/calc/calcFamily.ts +54 -65
- package/src/calc/compose.test.ts +1 -12
- package/src/calc/compose.ts +1 -36
- package/src/calc/queryState.ts +0 -23
- package/src/channel/channel.ts +126 -111
- package/src/compose/composition.test.ts +19 -0
- package/src/compose/composition.ts +351 -127
- package/src/compose/host.ts +0 -6
- package/src/compose/props.ts +13 -12
- package/src/compose/provide.ts +189 -62
- package/src/compose/slot.ts +158 -49
- package/src/compose/structure.test.ts +6 -9
- package/src/compose/structure.ts +108 -79
- package/src/compose/ui.test.ts +19 -7
- package/src/compose/ui.ts +155 -156
- package/src/compose/ui.typecheck.ts +40 -18
- package/src/definition/definition.ts +22 -41
- package/src/event/event.fromSource.test.ts +172 -0
- package/src/event/event.test.ts +10 -3
- package/src/event/event.ts +102 -27
- package/src/event/eventGroup.ts +0 -1
- package/src/feature/feature.mount.test.ts +122 -43
- package/src/feature/feature.test.ts +46 -21
- package/src/feature/feature.ts +1347 -256
- package/src/feature/feature.typecheck.ts +273 -68
- package/src/graph/closure.ts +403 -0
- package/src/index.ts +68 -126
- package/src/internal/bucketCache.ts +39 -0
- package/src/internal/capture.ts +9 -24
- package/src/internal/env.ts +7 -0
- package/src/internal/errors.test.ts +0 -6
- package/src/internal/errors.ts +37 -60
- package/src/internal/inspect.test.ts +0 -6
- package/src/internal/inspect.ts +0 -12
- package/src/internal/queryDriver.ts +105 -201
- package/src/internal/queryDriverStore.ts +156 -0
- package/src/internal/queryDriverTypes.ts +59 -0
- package/src/internal/queryEvents.ts +0 -12
- package/src/internal/queryStore.ts +0 -14
- package/src/internal/reuse.test.ts +10 -14
- package/src/internal/reuse.ts +5 -30
- package/src/internal/scheduler.ts +4 -44
- package/src/internal/seeds.ts +0 -14
- package/src/internal/sources.ts +26 -49
- package/src/internal/stateRegistry.ts +0 -14
- package/src/internal/store.test.ts +1 -3
- package/src/internal/store.ts +0 -37
- package/src/internal/track.ts +3 -20
- package/src/internal/variance.ts +5 -0
- package/src/internal.ts +222 -0
- package/src/namespace/namespace.test.ts +46 -0
- package/src/namespace/namespace.ts +106 -0
- package/src/procedure/procedure.ts +40 -35
- package/src/reducer/reducer.ts +78 -52
- package/src/remote/remoteState.test.ts +590 -397
- package/src/remote/remoteState.ts +425 -347
- package/src/remote/remoteState.typecheck.ts +47 -56
- package/src/runtime/appRuntime.activation.test.ts +100 -0
- package/src/runtime/appRuntime.test.ts +249 -0
- package/src/runtime/appRuntime.ts +415 -97
- package/src/runtime/bus.ts +2 -15
- package/src/runtime/eventBudget.test.ts +152 -0
- package/src/runtime/eventBudget.ts +120 -0
- package/src/runtime/hardening.test.ts +73 -13
- package/src/runtime/instrumentation.test.ts +55 -52
- package/src/runtime/instrumentation.ts +6 -60
- package/src/runtime/loop.test.ts +36 -17
- package/src/runtime/loop.ts +87 -88
- package/src/runtime/queries.ts +0 -17
- package/src/scene/featureScene.test.ts +61 -0
- package/src/scene/scene.ts +247 -104
- package/src/scene/seedScene.test.ts +42 -79
- package/src/state/state.nominal.typecheck.ts +68 -0
- package/src/state/state.test.ts +17 -0
- package/src/state/state.ts +80 -46
- package/src/state/stateFamily.test.ts +16 -11
- package/src/state/stateFamily.ts +55 -65
- package/src/state/stateGroup.ts +53 -64
- package/src/state/token.ts +40 -23
- package/src/synced/syncedStore.ts +46 -58
- package/src/testkit/flight.testkit.ts +75 -0
- package/src/ui/node.ts +0 -6
- package/src/ui/trigger.ts +0 -5
- package/src/wire/tree.test.ts +8 -7
- package/src/wire/tree.ts +0 -39
- package/src/wire/triggers.test.ts +6 -6
- package/src/wire/triggers.ts +14 -32
- package/src/internal/ctx.ts +0 -16
package/src/calc/calcFamily.ts
CHANGED
|
@@ -9,60 +9,70 @@ import {
|
|
|
9
9
|
} from '../internal/sources'
|
|
10
10
|
import { resolveScheduler } from '../internal/scheduler'
|
|
11
11
|
import { resolveInstrumentation, stateUpdateHook } from '../runtime/instrumentation'
|
|
12
|
+
import { reuse } from '../internal/reuse'
|
|
12
13
|
import { makeDerivedStore, type Store } from '../internal/store'
|
|
13
14
|
import { readTracked } from '../internal/track'
|
|
14
15
|
import { type FamilyOptions, type FamilyStore } from '../state/stateFamily'
|
|
15
16
|
import { type AnySource } from '../state/token'
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
// member recomputes lazily and notifies only when ITS slice of the projection
|
|
21
|
-
// moves (the derived store's Equal gate) — per-row/per-group UI subscribes per
|
|
22
|
-
// key and unrelated changes never wake it. Members are pure projections; a
|
|
23
|
-
// Reducer cannot target a CalcFamily (its config requires a `StateFamily`
|
|
24
|
-
// manifest), so calc purity holds structurally.
|
|
18
|
+
export interface CalcFamilySchemaReflection {
|
|
19
|
+
readonly ast: Schema.Schema<unknown, unknown, unknown>['ast']
|
|
20
|
+
}
|
|
25
21
|
|
|
26
|
-
export interface CalcFamilyManifest<N extends string
|
|
22
|
+
export interface CalcFamilyManifest<N extends string> extends Manifest {
|
|
27
23
|
readonly kind: 'CalcFamily'
|
|
28
24
|
readonly name: N
|
|
29
|
-
readonly key:
|
|
30
|
-
readonly output:
|
|
25
|
+
readonly key: CalcFamilySchemaReflection
|
|
26
|
+
readonly output: CalcFamilySchemaReflection
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const CalcFamilyStoreTypeId: unique symbol = Symbol.for('reform/CalcFamilyStore')
|
|
30
|
+
export type CalcFamilyStoreTypeId = typeof CalcFamilyStoreTypeId
|
|
31
|
+
|
|
32
|
+
export interface CalcFamilyStore<N extends string, in out K, in out Out> extends FamilyStore<
|
|
33
|
+
K,
|
|
34
|
+
Out
|
|
35
|
+
> {
|
|
36
|
+
readonly [CalcFamilyStoreTypeId]: N
|
|
31
37
|
}
|
|
32
38
|
|
|
33
39
|
export interface CalcFamilyClass<
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
40
|
+
N extends string,
|
|
41
|
+
Inputs extends ReadonlyArray<AnySource>,
|
|
42
|
+
K,
|
|
43
|
+
Out,
|
|
38
44
|
> {
|
|
39
|
-
/** Instance carries the key phantom so `Family['Key']` resolves in key types. */
|
|
40
45
|
new (): { readonly Key: K }
|
|
41
|
-
readonly manifest: CalcFamilyManifest<N
|
|
42
|
-
readonly store: Context.Tag<
|
|
43
|
-
/** Marker data read by `CalcFamily.live` and the editor's graph builder. */
|
|
46
|
+
readonly manifest: CalcFamilyManifest<N>
|
|
47
|
+
readonly store: Context.Tag<CalcFamilyStore<N, K, Out>, FamilyStore<K, Out>>
|
|
44
48
|
readonly inputs: Inputs
|
|
45
49
|
}
|
|
46
50
|
|
|
47
|
-
export interface CalcFamilyConfig<
|
|
48
|
-
|
|
51
|
+
export interface CalcFamilyConfig<
|
|
52
|
+
Inputs extends ReadonlyArray<AnySource>,
|
|
53
|
+
KeySchema extends Schema.Schema.AnyNoContext,
|
|
54
|
+
OutputSchema extends Schema.Schema.AnyNoContext,
|
|
55
|
+
> {
|
|
56
|
+
readonly key: KeySchema
|
|
49
57
|
readonly inputs: Inputs
|
|
50
|
-
readonly output:
|
|
58
|
+
readonly output: OutputSchema
|
|
51
59
|
}
|
|
52
60
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
61
|
+
export const make = <
|
|
62
|
+
const N extends string,
|
|
63
|
+
const Inputs extends ReadonlyArray<AnySource>,
|
|
64
|
+
KeySchema extends Schema.Schema.AnyNoContext,
|
|
65
|
+
OutputSchema extends Schema.Schema.AnyNoContext,
|
|
66
|
+
>(
|
|
59
67
|
name: N,
|
|
60
|
-
config: CalcFamilyConfig<Inputs,
|
|
61
|
-
): CalcFamilyClass<N, Inputs,
|
|
62
|
-
|
|
68
|
+
config: CalcFamilyConfig<Inputs, KeySchema, OutputSchema>,
|
|
69
|
+
): CalcFamilyClass<N, Inputs, Schema.Schema.Type<KeySchema>, Schema.Schema.Type<OutputSchema>> => {
|
|
70
|
+
type K = Schema.Schema.Type<KeySchema>
|
|
71
|
+
type Out = Schema.Schema.Type<OutputSchema>
|
|
72
|
+
const store = Context.GenericTag<CalcFamilyStore<N, K, Out>, FamilyStore<K, Out>>(
|
|
63
73
|
`reform/calcFamily/${name}`,
|
|
64
74
|
)
|
|
65
|
-
const manifest: CalcFamilyManifest<N
|
|
75
|
+
const manifest: CalcFamilyManifest<N> = {
|
|
66
76
|
kind: 'CalcFamily',
|
|
67
77
|
name,
|
|
68
78
|
key: config.key,
|
|
@@ -75,54 +85,33 @@ export const make = <const N extends string, const Inputs extends ReadonlyArray<
|
|
|
75
85
|
})
|
|
76
86
|
}
|
|
77
87
|
|
|
78
|
-
/**
|
|
79
|
-
* Read one member by key — `CalcFamily.read(GroupProjection, id)`. Inside a
|
|
80
|
-
* render it subscribes exactly that member's store (unrelated members never
|
|
81
|
-
* wake the subtree); elsewhere it just snapshots — the same one-read-API
|
|
82
|
-
* contract as `StateFamily.read`.
|
|
83
|
-
*/
|
|
84
88
|
export const read = <N extends string, Inputs extends ReadonlyArray<AnySource>, K, Out>(
|
|
85
89
|
family: CalcFamilyClass<N, Inputs, K, Out>,
|
|
86
90
|
key: K,
|
|
87
|
-
): Effect.Effect<Out, never,
|
|
91
|
+
): Effect.Effect<Out, never, CalcFamilyStore<N, K, Out>> =>
|
|
88
92
|
Effect.flatMap(family.store, (familyStore) => readTracked(familyStore.at(key)))
|
|
89
93
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
extends FamilyOptions {
|
|
94
|
-
/**
|
|
95
|
-
* Family-wide invalidation key over the SHARED inputs (the member key plays
|
|
96
|
-
* no part — it is fixed per member). Same contract as `Calc`'s.
|
|
97
|
-
*/
|
|
94
|
+
export interface CalcFamilyLiveOptionsExternalApi<
|
|
95
|
+
Inputs extends ReadonlyArray<AnySource>,
|
|
96
|
+
> extends FamilyOptions {
|
|
98
97
|
readonly invalidateBy?: InvalidateBy<Inputs>
|
|
98
|
+
readonly reuse?: boolean
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
/**
|
|
102
|
-
* Wire the per-key projection. `compute` is curried — `(key) => (inputs) =>
|
|
103
|
-
* Out` — so per-key construction (closing over key-derived constants) runs once
|
|
104
|
-
* per member while the inner projection runs per recompute. Each member is a
|
|
105
|
-
* lazy derived store over the shared wired sources: memoized on the family's
|
|
106
|
-
* invalidation key, notifying only when its own output moves. Unlike a
|
|
107
|
-
* `StateFamily` entry, a member holds an upstream subscription, so every
|
|
108
|
-
* removal path (forget / clear / eviction / the layer finalizer) releases it.
|
|
109
|
-
*/
|
|
110
101
|
export const live = <N extends string, Inputs extends ReadonlyArray<AnySource>, K, Out>(
|
|
111
102
|
family: CalcFamilyClass<N, Inputs, K, Out>,
|
|
112
103
|
compute: (key: K) => (inputs: InputsObject<Inputs>) => Out,
|
|
113
104
|
options: CalcFamilyLiveOptionsExternalApi<Inputs> = {},
|
|
114
|
-
): Layer.Layer<
|
|
105
|
+
): Layer.Layer<CalcFamilyStore<N, K, Out>, never, InputStores<Inputs>> =>
|
|
115
106
|
Layer.scoped(
|
|
116
107
|
family.store,
|
|
117
108
|
Effect.gen(function* () {
|
|
118
109
|
const scheduler = yield* resolveScheduler
|
|
119
110
|
const instrumentation = yield* resolveInstrumentation
|
|
120
111
|
const onOutputChange = stateUpdateHook(instrumentation, family.manifest.name)
|
|
121
|
-
// Wired ONCE for the whole family — members share the sensing surface.
|
|
122
112
|
const sources = yield* wireSources(family.inputs, options.invalidateBy)
|
|
123
113
|
const entries = new Map<K, { readonly store: Store<Out>; readonly unsubscribe: () => void }>()
|
|
124
114
|
const evictWhenUnused = options.evictWhenUnused === true
|
|
125
|
-
// Live subscriber count per key — maintained only when eviction is on.
|
|
126
115
|
const subscribers = new Map<K, number>()
|
|
127
116
|
|
|
128
117
|
const dropEntry = (key: K): void => {
|
|
@@ -134,8 +123,9 @@ export const live = <N extends string, Inputs extends ReadonlyArray<AnySource>,
|
|
|
134
123
|
subscribers.delete(key)
|
|
135
124
|
}
|
|
136
125
|
|
|
137
|
-
|
|
138
|
-
|
|
126
|
+
const createMember = (
|
|
127
|
+
key: K,
|
|
128
|
+
): { readonly store: Store<Out>; readonly unsubscribe: () => void } => {
|
|
139
129
|
const body = compute(key)
|
|
140
130
|
const memo = MutableRef.make<
|
|
141
131
|
{ readonly key: ReadonlyArray<unknown>; readonly output: Out } | undefined
|
|
@@ -148,7 +138,9 @@ export const live = <N extends string, Inputs extends ReadonlyArray<AnySource>,
|
|
|
148
138
|
return prev.output
|
|
149
139
|
}
|
|
150
140
|
const end = instrumentation.calcRecomputed(family.manifest.name)
|
|
151
|
-
const
|
|
141
|
+
const fresh = body(args)
|
|
142
|
+
const output =
|
|
143
|
+
options.reuse === true && prev !== undefined ? reuse(prev.output, fresh) : fresh
|
|
152
144
|
end()
|
|
153
145
|
MutableRef.set(memo, { key: memoKey, output })
|
|
154
146
|
return output
|
|
@@ -156,9 +148,6 @@ export const live = <N extends string, Inputs extends ReadonlyArray<AnySource>,
|
|
|
156
148
|
return makeDerivedStore(recompute, sources.subscribe, scheduler, onOutputChange)
|
|
157
149
|
}
|
|
158
150
|
|
|
159
|
-
// The `StateFamily` ref-count idiom: evict on the next microtask once a
|
|
160
|
-
// key falls idle (a same-commit re-subscribe cancels it) — here eviction
|
|
161
|
-
// also releases the member's upstream subscription via `dropEntry`.
|
|
162
151
|
const refCounted = (key: K, store: Store<Out>): Store<Out> => ({
|
|
163
152
|
...store,
|
|
164
153
|
subscribe: (listener) => {
|
package/src/calc/compose.test.ts
CHANGED
|
@@ -2,10 +2,6 @@ import { expect, it } from '@effect/vitest'
|
|
|
2
2
|
import { Effect, Layer, Schema as S } from 'effect'
|
|
3
3
|
import { Calc, composeCalcs, State, StateGroup } from '../index'
|
|
4
4
|
|
|
5
|
-
// `composeCalcs` wires a calc-on-calc chain in dependency order. The shape under
|
|
6
|
-
// test is the one that defeats a flat `Layer.mergeAll`: a linear chain where each
|
|
7
|
-
// calc's only input is the previous calc — count → Doubled → PlusOne → Tripled.
|
|
8
|
-
|
|
9
5
|
it.live('composeCalcs wires a linear calc chain; the result needs only the base state', () => {
|
|
10
6
|
class Count extends State.make('count', S.Number) {}
|
|
11
7
|
class Inputs extends StateGroup.make(Count) {}
|
|
@@ -22,23 +18,16 @@ it.live('composeCalcs wires a linear calc chain; the result needs only the base
|
|
|
22
18
|
class Tripled extends Calc.make('Tripled', { inputs: [PlusOne], output: S.Number }) {}
|
|
23
19
|
const TripledLive = Calc.live(Tripled, ({ PlusOne }) => PlusOne * 3)
|
|
24
20
|
|
|
25
|
-
// Leaf-first, then upstreams in dependency order. The composed layer's only
|
|
26
|
-
// remaining requirement is the base `count` store — which the type system
|
|
27
|
-
// proves by letting us provide *only* the state group below.
|
|
28
|
-
// The chain's only remaining requirement is the base `count` store — provide
|
|
29
|
-
// (and expose, via provideMerge) just the state group and everything builds.
|
|
30
21
|
const Chain = composeCalcs(TripledLive, PlusOneLive, DoubledLive)
|
|
31
22
|
const TestLayer = Chain.pipe(Layer.provideMerge(StateGroup.live(Inputs, { count: 5 })))
|
|
32
23
|
|
|
33
24
|
return Effect.gen(function* () {
|
|
34
25
|
const source = yield* StateGroup.select(Inputs, 'count').store
|
|
35
26
|
const tripled = yield* Tripled.store
|
|
36
|
-
// count 5 → Doubled 10 → PlusOne 11 → Tripled 33
|
|
37
27
|
expect(tripled.get()).toBe(33)
|
|
38
28
|
|
|
39
29
|
source.set(2)
|
|
40
30
|
yield* Effect.yieldNow()
|
|
41
|
-
// count 2 → Doubled 4 → PlusOne 5 → Tripled 15
|
|
42
31
|
expect(tripled.get()).toBe(15)
|
|
43
32
|
}).pipe(Effect.provide(TestLayer))
|
|
44
33
|
})
|
|
@@ -63,6 +52,6 @@ it.live('composeCalcs(leaf, upstream) matches the hand-written provideMerge chai
|
|
|
63
52
|
|
|
64
53
|
return Effect.gen(function* () {
|
|
65
54
|
const derived = yield* PlusDoubled.store
|
|
66
|
-
expect(derived.get()).toBe(15)
|
|
55
|
+
expect(derived.get()).toBe(15)
|
|
67
56
|
}).pipe(Effect.provide(TestLayer))
|
|
68
57
|
})
|
package/src/calc/compose.ts
CHANGED
|
@@ -1,25 +1,5 @@
|
|
|
1
1
|
import { Layer } from 'effect'
|
|
2
2
|
|
|
3
|
-
// Wiring a calc-on-calc chain (`FilteredAgents` ← `AllAgents` ← `AgentsQuery`)
|
|
4
|
-
// is the one calc-graph shape that trips people up: each upstream calc's store
|
|
5
|
-
// must be *provided into* the downstream live, in dependency order. A flat
|
|
6
|
-
// `Layer.mergeAll(FilteredLive, AllLive, QueryLive)` does NOT do this — `mergeAll`
|
|
7
|
-
// merges outputs but never provides members to one another, so the upstream
|
|
8
|
-
// calc stores stay as unmet `Store<…>` requirements (the TS2322 the migration
|
|
9
|
-
// hit). The correct shape is `leaf.pipe(provideMerge(u1), provideMerge(u2), …)`;
|
|
10
|
-
// `composeCalcs` is that fold under one name so the right thing is the easy thing.
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Compose a calc-dependency chain: provide each upstream calc's `.live` layer
|
|
14
|
-
* into the downstream one, nearest dependency first. The result exposes every
|
|
15
|
-
* calc store in the chain and requires only the chain's *external* inputs (the
|
|
16
|
-
* base state stores) — never the intermediate calc stores.
|
|
17
|
-
*
|
|
18
|
-
* Order is leaf-first, then its upstreams in dependency order:
|
|
19
|
-
* `composeCalcs(FilteredLive, AllLive, QueryLive)` ≡
|
|
20
|
-
* `FilteredLive.pipe(Layer.provideMerge(AllLive), Layer.provideMerge(QueryLive))`.
|
|
21
|
-
* For chains deeper than three upstreams, nest `composeCalcs`.
|
|
22
|
-
*/
|
|
23
3
|
export function composeCalcs<LOut, LErr, LIn, AOut, AErr, AIn>(
|
|
24
4
|
leaf: Layer.Layer<LOut, LErr, LIn>,
|
|
25
5
|
upstream: Layer.Layer<AOut, AErr, AIn>,
|
|
@@ -33,20 +13,7 @@ export function composeCalcs<LOut, LErr, LIn, AOut, AErr, AIn, BOut, BErr, BIn>(
|
|
|
33
13
|
LErr | AErr | BErr,
|
|
34
14
|
BIn | Exclude<AIn | Exclude<LIn, AOut>, BOut>
|
|
35
15
|
>
|
|
36
|
-
export function composeCalcs<
|
|
37
|
-
LOut,
|
|
38
|
-
LErr,
|
|
39
|
-
LIn,
|
|
40
|
-
AOut,
|
|
41
|
-
AErr,
|
|
42
|
-
AIn,
|
|
43
|
-
BOut,
|
|
44
|
-
BErr,
|
|
45
|
-
BIn,
|
|
46
|
-
COut,
|
|
47
|
-
CErr,
|
|
48
|
-
CIn,
|
|
49
|
-
>(
|
|
16
|
+
export function composeCalcs<LOut, LErr, LIn, AOut, AErr, AIn, BOut, BErr, BIn, COut, CErr, CIn>(
|
|
50
17
|
leaf: Layer.Layer<LOut, LErr, LIn>,
|
|
51
18
|
upstream1: Layer.Layer<AOut, AErr, AIn>,
|
|
52
19
|
upstream2: Layer.Layer<BOut, BErr, BIn>,
|
|
@@ -60,8 +27,6 @@ export function composeCalcs(
|
|
|
60
27
|
leaf: Layer.Layer<unknown, unknown, unknown>,
|
|
61
28
|
...upstream: ReadonlyArray<Layer.Layer<unknown, unknown, unknown>>
|
|
62
29
|
): Layer.Layer<unknown, unknown, unknown> {
|
|
63
|
-
// Fold leaf-first: each upstream is provided into the accumulated downstream,
|
|
64
|
-
// so dependency order is exactly the argument order.
|
|
65
30
|
return upstream.reduce<Layer.Layer<unknown, unknown, unknown>>(
|
|
66
31
|
(downstream, upstreamLayer) => Layer.provideMerge(downstream, upstreamLayer),
|
|
67
32
|
leaf,
|
package/src/calc/queryState.ts
CHANGED
|
@@ -1,26 +1,13 @@
|
|
|
1
1
|
import { Option } from 'effect'
|
|
2
2
|
import { type AnyAsyncData, AsyncData } from './asyncData'
|
|
3
3
|
|
|
4
|
-
// `QueryState` is the data-oriented value an `AsyncCalc` is driven by: the four
|
|
5
|
-
// axes of a query, stored *flat and independent* so each can move on its own.
|
|
6
|
-
// `data`/`error` survive a refetch (stale-while-revalidate for free); `isFetching`
|
|
7
|
-
// (activity) and `isStale` (freshness) are orthogonal to both — which is exactly
|
|
8
|
-
// what lets `invalidate` (flip `isStale`) and `refetch` (drive `isFetching`) be
|
|
9
|
-
// separate operations. The matchable `AsyncData` union is *derived* from this
|
|
10
|
-
// (`toAsyncData`), so view code and `RemoteState` keep their arm dispatch.
|
|
11
|
-
|
|
12
4
|
export interface QueryState<A, E> {
|
|
13
|
-
/** Last resolved value — kept across a refetch. */
|
|
14
5
|
readonly data: Option.Option<A>
|
|
15
|
-
/** Last failure — kept across a refetch. */
|
|
16
6
|
readonly error: Option.Option<E>
|
|
17
|
-
/** Activity axis: a run is in flight. */
|
|
18
7
|
readonly isFetching: boolean
|
|
19
|
-
/** Freshness axis: the value has been marked invalid (drives auto-refetch). */
|
|
20
8
|
readonly isStale: boolean
|
|
21
9
|
}
|
|
22
10
|
|
|
23
|
-
/** The empty cell. `fetching` seeds the first-kick `Loading` for an enabled query. */
|
|
24
11
|
export const empty = (fetching: boolean): QueryState<never, never> => ({
|
|
25
12
|
data: Option.none(),
|
|
26
13
|
error: Option.none(),
|
|
@@ -28,15 +15,6 @@ export const empty = (fetching: boolean): QueryState<never, never> => ({
|
|
|
28
15
|
isStale: false,
|
|
29
16
|
})
|
|
30
17
|
|
|
31
|
-
/**
|
|
32
|
-
* Project the flat state onto the public `AsyncData` union (the view contract) —
|
|
33
|
-
* the single `Match`-able view of a query, so call sites keep `Match.tag`-ing the
|
|
34
|
-
* arms. `error` wins over `data` so a failed refetch surfaces the `Error` arm,
|
|
35
|
-
* matching the pre-`QueryState` driver; `isFetching` becomes the arm's
|
|
36
|
-
* `refetching` flag. The data-less case is `Loading` while fetching (or for a
|
|
37
|
-
* non-gated query, which always fetches) and `Idle` only for a switched-off
|
|
38
|
-
* gateable query.
|
|
39
|
-
*/
|
|
40
18
|
export const toAsyncData = <A, E>(state: QueryState<A, E>, gated: boolean): AnyAsyncData<A, E> =>
|
|
41
19
|
Option.match(state.error, {
|
|
42
20
|
onSome: (error) => AsyncData.error(error, state.isFetching),
|
|
@@ -47,6 +25,5 @@ export const toAsyncData = <A, E>(state: QueryState<A, E>, gated: boolean): AnyA
|
|
|
47
25
|
}),
|
|
48
26
|
})
|
|
49
27
|
|
|
50
|
-
/** `Loading` in the React-Query sense: fetching with nothing to show yet. */
|
|
51
28
|
export const isLoading = <A, E>(state: QueryState<A, E>): boolean =>
|
|
52
29
|
state.isFetching && Option.isNone(state.data)
|