@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/asyncCalc.ts
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
import { Context, Effect, Layer, Match, Option, Schema } from 'effect'
|
|
2
|
+
import type { Effect as EffectType } from 'effect/Effect'
|
|
2
3
|
import { type Manifest, yieldableClass } from '../definition/definition'
|
|
3
4
|
import { type AnyEvent } from '../event/event'
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
type InputStores,
|
|
7
|
-
type InvalidateBy,
|
|
8
|
-
} from '../internal/sources'
|
|
5
|
+
import { type InputsObject, type InputStores, type InvalidateBy } from '../internal/sources'
|
|
6
|
+
import type { AnySchema } from '../internal/variance'
|
|
9
7
|
import {
|
|
10
8
|
bumpRevision,
|
|
11
9
|
type GatedOf,
|
|
12
10
|
gatedFlag,
|
|
13
11
|
makeQueryDriver,
|
|
12
|
+
type QueryCodec,
|
|
14
13
|
RevisionSchema,
|
|
15
14
|
revisionZero,
|
|
16
15
|
} from '../internal/queryDriver'
|
|
@@ -20,62 +19,104 @@ import * as Reducer from '../reducer/reducer'
|
|
|
20
19
|
import { Reducers } from '../runtime/loop'
|
|
21
20
|
import { Queries } from '../runtime/queries'
|
|
22
21
|
import * as State from '../state/state'
|
|
23
|
-
import { type AnySource } from '../state/token'
|
|
22
|
+
import { type AnySource, type Source, type SourceCapture } from '../state/token'
|
|
24
23
|
import { type AsyncData, narrowStore } from './asyncData'
|
|
25
24
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
// Definition (`make`) is reflectable data; implementation (`live`) is the effect.
|
|
30
|
-
// The reactive machinery lives in `internal/queryDriver` (shared with
|
|
31
|
-
// `RemoteState`); this module owns the definition shape and the `invalidateOn`
|
|
32
|
-
// sugar.
|
|
25
|
+
export interface AsyncCalcSchemaReflection {
|
|
26
|
+
readonly ast: Schema.Schema<unknown, unknown, unknown>['ast']
|
|
27
|
+
}
|
|
33
28
|
|
|
34
|
-
export interface AsyncCalcManifest<N extends string, A, E> extends Manifest {
|
|
29
|
+
export interface AsyncCalcManifest<N extends string, A = unknown, E = never> extends Manifest {
|
|
35
30
|
readonly kind: 'AsyncCalc'
|
|
36
31
|
readonly name: N
|
|
37
|
-
readonly output:
|
|
32
|
+
readonly output: AnySchema<A>
|
|
38
33
|
// oxlint-disable-next-line reform-rules/no-optional-fields -- caller-omitted option built at the asyncCalc/remoteState construction site; Option<T> would change that call shape
|
|
39
|
-
readonly error?:
|
|
40
|
-
/** Whether the query can be disabled (drives the `Idle` arm). */
|
|
34
|
+
readonly error?: AnySchema<E>
|
|
41
35
|
readonly gated: boolean
|
|
42
36
|
}
|
|
43
37
|
|
|
44
|
-
export
|
|
45
|
-
|
|
46
|
-
|
|
38
|
+
export const AsyncCalcStoreTypeId: unique symbol = Symbol.for('reform/AsyncCalcStore')
|
|
39
|
+
export type AsyncCalcStoreTypeId = typeof AsyncCalcStoreTypeId
|
|
40
|
+
|
|
41
|
+
interface AsyncCalcStoreCompatibilityExternalApi<
|
|
42
|
+
N extends string,
|
|
47
43
|
in out A,
|
|
48
44
|
in out E,
|
|
49
45
|
in out Gated extends boolean,
|
|
50
|
-
> extends
|
|
46
|
+
> extends Store<AsyncData<A, E, Gated>> {
|
|
47
|
+
readonly [AsyncCalcStoreTypeId]?: N
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export type AsyncCalcStore<
|
|
51
|
+
N extends string,
|
|
52
|
+
A,
|
|
53
|
+
E,
|
|
54
|
+
Gated extends boolean,
|
|
55
|
+
> = AsyncCalcStoreCompatibilityExternalApi<N, A, E, Gated>
|
|
56
|
+
|
|
57
|
+
export interface AsyncCalcClass<
|
|
58
|
+
N extends string,
|
|
59
|
+
Inputs extends ReadonlyArray<AnySource>,
|
|
60
|
+
A,
|
|
61
|
+
E,
|
|
62
|
+
Gated extends boolean,
|
|
63
|
+
>
|
|
64
|
+
extends
|
|
65
|
+
EffectType<AsyncData<A, E, Gated>, never, AsyncCalcStore<N, A, E, Gated>>,
|
|
66
|
+
Source<N, AsyncData<A, E, Gated>, AsyncCalcStore<N, A, E, Gated>> {
|
|
51
67
|
new (): {}
|
|
52
68
|
readonly manifest: AsyncCalcManifest<N, A, E>
|
|
53
|
-
readonly store: Context.Tag<
|
|
54
|
-
/** The calc's name, so it doubles as a `Source` input to another (async) calc. */
|
|
69
|
+
readonly store: Context.Tag<AsyncCalcStore<N, A, E, Gated>, Store<AsyncData<A, E, Gated>>>
|
|
55
70
|
readonly name: N
|
|
56
|
-
/** Marker data read by `AsyncCalc.live`. */
|
|
57
71
|
readonly inputs: Inputs
|
|
58
|
-
/** Runtime mirror of `Gated`: whether `disabled` is honored / `Idle` can occur. */
|
|
59
72
|
readonly gated: Gated
|
|
60
73
|
}
|
|
61
74
|
|
|
62
|
-
export interface
|
|
75
|
+
export interface DefinedAsyncCalcClass<
|
|
76
|
+
N extends string,
|
|
77
|
+
Inputs extends ReadonlyArray<AnySource>,
|
|
78
|
+
A,
|
|
79
|
+
E,
|
|
80
|
+
Gated extends boolean,
|
|
81
|
+
> extends AsyncCalcClass<N, Inputs, A, E, Gated> {
|
|
82
|
+
readonly codec: QueryCodec<A>
|
|
83
|
+
readonly capture: <Result>(visit: SourceCapture<Result>) => Result
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
type ConfigSchema<ValueOrSchema> = [ValueOrSchema] extends [Schema.Schema.AnyNoContext]
|
|
87
|
+
? ValueOrSchema
|
|
88
|
+
: AnySchema<ValueOrSchema>
|
|
89
|
+
|
|
90
|
+
type ConfigErrorSchema<ValueOrSchema> = [ValueOrSchema] extends [undefined]
|
|
91
|
+
? ValueOrSchema
|
|
92
|
+
: ConfigSchema<ValueOrSchema>
|
|
93
|
+
|
|
94
|
+
export interface AsyncCalcConfig<
|
|
95
|
+
Inputs extends ReadonlyArray<AnySource>,
|
|
96
|
+
OutputSchema,
|
|
97
|
+
ErrorSchema,
|
|
98
|
+
AlwaysOn extends boolean,
|
|
99
|
+
> {
|
|
63
100
|
readonly inputs: Inputs
|
|
64
|
-
|
|
65
|
-
readonly output: Schema.Schema<A, any>
|
|
66
|
-
/** Schema of the failure. Omitted ⇒ the query is infallible and there is no `Error` arm. */
|
|
101
|
+
readonly output: ConfigSchema<OutputSchema>
|
|
67
102
|
// oxlint-disable-next-line reform-rules/no-optional-fields -- caller-omitted option built at the asyncCalc/remoteState construction site; Option<T> would change that call shape
|
|
68
|
-
readonly error?:
|
|
69
|
-
/** `true` ⇒ the query is always on: no `Idle` arm and `disabled` is rejected on `.live`. */
|
|
103
|
+
readonly error?: ConfigErrorSchema<ErrorSchema>
|
|
70
104
|
// oxlint-disable-next-line reform-rules/no-optional-fields -- caller-omitted option built at the asyncCalc/remoteState construction site; Option<T> would change that call shape
|
|
71
105
|
readonly alwaysOn?: AlwaysOn
|
|
72
106
|
}
|
|
73
107
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
108
|
+
interface AsyncCalcMakeConfigExternalApi<
|
|
109
|
+
Inputs extends ReadonlyArray<AnySource>,
|
|
110
|
+
OutputSchema extends Schema.Schema.AnyNoContext,
|
|
111
|
+
ErrorSchema extends Schema.Schema.AnyNoContext | undefined,
|
|
112
|
+
AlwaysOn extends boolean,
|
|
113
|
+
> {
|
|
114
|
+
readonly inputs: Inputs
|
|
115
|
+
readonly output: OutputSchema
|
|
116
|
+
readonly error?: ErrorSchema
|
|
117
|
+
readonly alwaysOn?: AlwaysOn
|
|
118
|
+
}
|
|
119
|
+
|
|
79
120
|
export type AsyncCalcLive<
|
|
80
121
|
Inputs extends ReadonlyArray<AnySource>,
|
|
81
122
|
A,
|
|
@@ -84,48 +125,19 @@ export type AsyncCalcLive<
|
|
|
84
125
|
R,
|
|
85
126
|
> = {
|
|
86
127
|
readonly query: (inputs: InputsObject<Inputs>) => Effect.Effect<A, E, R>
|
|
87
|
-
// oxlint-disable-next-line reform-rules/no-optional-fields -- caller-omitted option built at the asyncCalc/remoteState construction site; Option<T> would change that call shape
|
|
88
128
|
readonly invalidateBy?: InvalidateBy<Inputs>
|
|
89
|
-
/**
|
|
90
|
-
* Refetch conciliation. `'switch'` (default): a new key cancels the in-flight
|
|
91
|
-
* run (latest-wins). `'trailing'`: the in-flight run completes; key changes
|
|
92
|
-
* that arrive meanwhile conflate to the latest, and exactly ONE trailing
|
|
93
|
-
* refetch runs after it settles — a burst of N invalidations during one
|
|
94
|
-
* flight costs 2 fetches, not N cancel-restarts.
|
|
95
|
-
*/
|
|
96
129
|
// oxlint-disable-next-line reform-rules/no-optional-fields -- caller-omitted option built at the asyncCalc/remoteState construction site; Option<T> would change that call shape
|
|
97
130
|
readonly coalesce?: 'switch' | 'trailing'
|
|
98
|
-
/**
|
|
99
|
-
* Structural sharing for `Success` values: reconcile each refetch result
|
|
100
|
-
* against the previous one, substituting previous nodes wherever value
|
|
101
|
-
* equality holds — refetches returning mostly-identical data keep unchanged
|
|
102
|
-
* subtree identities, so downstream memo boundaries skip them. Same pass as
|
|
103
|
-
* `Calc`'s `reuse`; opt-in (one O(result) walk per settle).
|
|
104
|
-
*/
|
|
105
131
|
// oxlint-disable-next-line reform-rules/no-optional-fields -- caller-omitted option built at the asyncCalc/remoteState construction site; Option<T> would change that call shape
|
|
106
132
|
readonly reuse?: boolean
|
|
107
|
-
/**
|
|
108
|
-
* Persist the `Success` value through an optional host `QueryStore` (e.g.
|
|
109
|
-
* `@playfast/reform-query-browser`'s localStorage layer): hydrate it on build
|
|
110
|
-
* so the value shows instantly (marked stale, so it still refetches) and write
|
|
111
|
-
* through on each settle. `true` keys it by the calc name; pass `{ key }` to
|
|
112
|
-
* override — a static string for a singleton, or a function of the inputs to
|
|
113
|
-
* key a family per-entity (so reactive-input keyed reads never collide on one
|
|
114
|
-
* slot). Values (de)serialize through the calc's `output` schema. No host
|
|
115
|
-
* `QueryStore` in context ⇒ inert (no hard requirement added).
|
|
116
|
-
*/
|
|
117
133
|
// oxlint-disable-next-line reform-rules/no-optional-fields -- caller-omitted option built at the asyncCalc/remoteState construction site; Option<T> would change that call shape
|
|
118
134
|
readonly persist?:
|
|
119
135
|
| boolean
|
|
120
|
-
// oxlint-disable-next-line reform-rules/no-optional-fields -- caller-omitted option built at the asyncCalc/remoteState construction site; Option<T> would change that call shape
|
|
121
136
|
| { readonly key?: string | ((inputs: InputsObject<Inputs>) => string) }
|
|
122
137
|
} & (Gated extends true
|
|
123
|
-
?
|
|
124
|
-
|
|
125
|
-
: // oxlint-disable-next-line reform-rules/no-optional-fields -- caller-omitted option built at the asyncCalc/remoteState construction site; Option<T> would change that call shape
|
|
126
|
-
{ readonly disabled?: never })
|
|
138
|
+
? { readonly disabled?: (inputs: InputsObject<Inputs>) => boolean }
|
|
139
|
+
: { readonly disabled?: never })
|
|
127
140
|
|
|
128
|
-
/** `.live` config plus the optional `invalidateOn` event list (impl signature). */
|
|
129
141
|
export type AsyncCalcLiveWithInvalidateOn<
|
|
130
142
|
Inputs extends ReadonlyArray<AnySource>,
|
|
131
143
|
A,
|
|
@@ -133,47 +145,58 @@ export type AsyncCalcLiveWithInvalidateOn<
|
|
|
133
145
|
Gated extends boolean,
|
|
134
146
|
R,
|
|
135
147
|
> = AsyncCalcLive<Inputs, A, E, Gated, R> & {
|
|
136
|
-
// oxlint-disable-next-line reform-rules/no-optional-fields -- caller-omitted option built at the asyncCalc/remoteState construction site; Option<T> would change that call shape
|
|
137
148
|
readonly invalidateOn?: ReadonlyArray<AnyEvent>
|
|
138
149
|
}
|
|
139
150
|
|
|
140
|
-
/** Loose runtime view of `.live` config — `disabled` recovered past its type erasure. */
|
|
141
151
|
export interface AsyncCalcLiveView<Inputs extends ReadonlyArray<AnySource>, A, E, R> {
|
|
142
152
|
readonly query: (inputs: InputsObject<Inputs>) => Effect.Effect<A, E, R>
|
|
143
|
-
// oxlint-disable-next-line reform-rules/no-optional-fields -- caller-omitted option built at the asyncCalc/remoteState construction site; Option<T> would change that call shape
|
|
144
153
|
readonly invalidateBy?: InvalidateBy<Inputs>
|
|
145
|
-
// oxlint-disable-next-line reform-rules/no-optional-fields -- caller-omitted option built at the asyncCalc/remoteState construction site; Option<T> would change that call shape
|
|
146
154
|
readonly disabled?: (inputs: InputsObject<Inputs>) => boolean
|
|
147
|
-
// oxlint-disable-next-line reform-rules/no-optional-fields -- caller-omitted option built at the asyncCalc/remoteState construction site; Option<T> would change that call shape
|
|
148
155
|
readonly coalesce?: 'switch' | 'trailing'
|
|
149
|
-
// oxlint-disable-next-line reform-rules/no-optional-fields -- caller-omitted option built at the asyncCalc/remoteState construction site; Option<T> would change that call shape
|
|
150
156
|
readonly reuse?: boolean
|
|
151
|
-
// oxlint-disable-next-line reform-rules/no-optional-fields -- caller-omitted option built at the asyncCalc/remoteState construction site; Option<T> would change that call shape
|
|
152
157
|
readonly persist?:
|
|
153
158
|
| boolean
|
|
154
|
-
// oxlint-disable-next-line reform-rules/no-optional-fields -- caller-omitted option built at the asyncCalc/remoteState construction site; Option<T> would change that call shape
|
|
155
159
|
| { readonly key?: string | ((inputs: InputsObject<Inputs>) => string) }
|
|
156
160
|
}
|
|
157
161
|
|
|
158
|
-
|
|
159
|
-
* Define an async derived value. `output`/`error` schemas and `alwaysOn` shape
|
|
160
|
-
* the value type, so `yield* MyQuery` is typed to exactly the arms that can
|
|
161
|
-
* occur. The query effect itself is supplied by `AsyncCalc.live`.
|
|
162
|
-
*/
|
|
163
|
-
export const make = <
|
|
162
|
+
export function make<
|
|
164
163
|
const N extends string,
|
|
165
164
|
const Inputs extends ReadonlyArray<AnySource>,
|
|
166
|
-
|
|
167
|
-
|
|
165
|
+
OutputSchema extends Schema.Schema.AnyNoContext,
|
|
166
|
+
ErrorSchema extends Schema.Schema.AnyNoContext | undefined = undefined,
|
|
168
167
|
const AlwaysOn extends boolean = false,
|
|
169
168
|
>(
|
|
170
169
|
name: N,
|
|
171
|
-
config: AsyncCalcConfig<Inputs,
|
|
172
|
-
):
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
170
|
+
config: AsyncCalcConfig<Inputs, OutputSchema, ErrorSchema, AlwaysOn>,
|
|
171
|
+
): DefinedAsyncCalcClass<
|
|
172
|
+
N,
|
|
173
|
+
Inputs,
|
|
174
|
+
Schema.Schema.Type<OutputSchema>,
|
|
175
|
+
ErrorSchema extends Schema.Schema.AnyNoContext ? Schema.Schema.Type<ErrorSchema> : never,
|
|
176
|
+
GatedOf<AlwaysOn>
|
|
177
|
+
>
|
|
178
|
+
export function make<
|
|
179
|
+
const N extends string,
|
|
180
|
+
const Inputs extends ReadonlyArray<AnySource>,
|
|
181
|
+
OutputSchema extends Schema.Schema.AnyNoContext,
|
|
182
|
+
ErrorSchema extends Schema.Schema.AnyNoContext | undefined = undefined,
|
|
183
|
+
const AlwaysOn extends boolean = false,
|
|
184
|
+
>(
|
|
185
|
+
name: N,
|
|
186
|
+
config: AsyncCalcMakeConfigExternalApi<Inputs, OutputSchema, ErrorSchema, AlwaysOn>,
|
|
187
|
+
): DefinedAsyncCalcClass<
|
|
188
|
+
N,
|
|
189
|
+
Inputs,
|
|
190
|
+
Schema.Schema.Type<OutputSchema>,
|
|
191
|
+
ErrorSchema extends Schema.Schema.AnyNoContext ? Schema.Schema.Type<ErrorSchema> : never,
|
|
192
|
+
GatedOf<AlwaysOn>
|
|
193
|
+
> {
|
|
194
|
+
type A = Schema.Schema.Type<OutputSchema>
|
|
195
|
+
type E = ErrorSchema extends Schema.Schema.AnyNoContext ? Schema.Schema.Type<ErrorSchema> : never
|
|
196
|
+
const store = Context.GenericTag<
|
|
197
|
+
AsyncCalcStore<N, A, E, GatedOf<AlwaysOn>>,
|
|
198
|
+
Store<AsyncData<A, E, GatedOf<AlwaysOn>>>
|
|
199
|
+
>(`reform/asyncCalc/${name}`)
|
|
177
200
|
const gated = gatedFlag(config.alwaysOn)
|
|
178
201
|
const manifest: AsyncCalcManifest<N, A, E> = {
|
|
179
202
|
kind: 'AsyncCalc',
|
|
@@ -182,30 +205,23 @@ export const make = <
|
|
|
182
205
|
gated,
|
|
183
206
|
...(config.error !== undefined ? { error: config.error } : {}),
|
|
184
207
|
}
|
|
208
|
+
const codec: QueryCodec<A> = {
|
|
209
|
+
encode: Schema.encode(config.output),
|
|
210
|
+
decode: Schema.decodeUnknown(config.output),
|
|
211
|
+
}
|
|
185
212
|
const read = Effect.flatMap(store, readTracked)
|
|
186
|
-
|
|
213
|
+
const calc: DefinedAsyncCalcClass<N, Inputs, A, E, GatedOf<AlwaysOn>> = yieldableClass(read, {
|
|
187
214
|
manifest,
|
|
215
|
+
codec,
|
|
188
216
|
store,
|
|
189
217
|
name,
|
|
190
218
|
inputs: config.inputs,
|
|
191
219
|
gated,
|
|
220
|
+
capture: <Result>(visit: SourceCapture<Result>): Result => visit(calc),
|
|
192
221
|
})
|
|
222
|
+
return calc
|
|
193
223
|
}
|
|
194
224
|
|
|
195
|
-
/**
|
|
196
|
-
* Wire the query and its reactive logic. The query runs on the runtime (its `R`
|
|
197
|
-
* — RPC clients, etc. — is captured from the layer context). A change to an
|
|
198
|
-
* input re-runs it latest-wins (a new run cancels the in-flight one; or, with
|
|
199
|
-
* `coalesce: 'trailing'`, lets it finish and runs one trailing refetch); while a
|
|
200
|
-
* re-run is in flight the last `Success`/`Error` is kept with `refetching: true`.
|
|
201
|
-
* Re-runs are driven by inputs / `invalidateBy` / `disabled` — and, with
|
|
202
|
-
* `invalidateOn`, by listed events: pure sugar that generates a hidden revision
|
|
203
|
-
* state + reducer (the sole writer) and appends the revision to the calc's key,
|
|
204
|
-
* so an event-driven refetch is still just key movement under `Equal.equals`.
|
|
205
|
-
* The hidden pair stays internal — the user's `query`/`invalidateBy` see exactly
|
|
206
|
-
* the declared inputs — and costs the layer one extra requirement: the
|
|
207
|
-
* `Reducers` registry (the `Engine`).
|
|
208
|
-
*/
|
|
209
225
|
export function live<
|
|
210
226
|
N extends string,
|
|
211
227
|
Inputs extends ReadonlyArray<AnySource>,
|
|
@@ -218,7 +234,7 @@ export function live<
|
|
|
218
234
|
config: AsyncCalcLive<Inputs, A, E, Gated, R> & {
|
|
219
235
|
readonly invalidateOn: ReadonlyArray<AnyEvent>
|
|
220
236
|
},
|
|
221
|
-
): Layer.Layer<
|
|
237
|
+
): Layer.Layer<AsyncCalcStore<N, A, E, Gated>, never, InputStores<Inputs> | R | Reducers>
|
|
222
238
|
export function live<
|
|
223
239
|
N extends string,
|
|
224
240
|
Inputs extends ReadonlyArray<AnySource>,
|
|
@@ -229,7 +245,7 @@ export function live<
|
|
|
229
245
|
>(
|
|
230
246
|
calc: AsyncCalcClass<N, Inputs, A, E, Gated>,
|
|
231
247
|
config: AsyncCalcLive<Inputs, A, E, Gated, R>,
|
|
232
|
-
): Layer.Layer<
|
|
248
|
+
): Layer.Layer<AsyncCalcStore<N, A, E, Gated>, never, InputStores<Inputs> | R>
|
|
233
249
|
export function live<
|
|
234
250
|
N extends string,
|
|
235
251
|
Inputs extends ReadonlyArray<AnySource>,
|
|
@@ -240,49 +256,31 @@ export function live<
|
|
|
240
256
|
>(
|
|
241
257
|
calc: AsyncCalcClass<N, Inputs, A, E, Gated>,
|
|
242
258
|
config: AsyncCalcLiveWithInvalidateOn<Inputs, A, E, Gated, R>,
|
|
243
|
-
|
|
244
|
-
// seam `Reducer.live` uses; both overload returns are assignable (RIn is
|
|
245
|
-
// covariant), so no value is ever cast.
|
|
246
|
-
): Layer.Layer<Store<AsyncData<A, E, Gated>>, never, any> {
|
|
247
|
-
// The hidden revision pair, built once per `live` call. Namespacing the state
|
|
248
|
-
// by the calc's name (`reform/state/${calcName}/invalidateOn`) keeps the tag
|
|
249
|
-
// out of any plausible user namespace; the duplicate-reducer check is
|
|
250
|
-
// warning-only and calc names are unique by convention.
|
|
259
|
+
): Layer.Layer<AsyncCalcStore<N, A, E, Gated>, never, InputStores<Inputs> | R | Reducers> {
|
|
251
260
|
const revisionState =
|
|
252
261
|
config.invalidateOn === undefined
|
|
253
262
|
? undefined
|
|
254
263
|
: State.make(`${calc.manifest.name}/invalidateOn`, RevisionSchema)
|
|
255
264
|
|
|
256
|
-
// Scoped so the source subscriptions and the driver fiber are released when
|
|
257
|
-
// the layer's scope closes (each proof/test builds and disposes its runtime).
|
|
258
265
|
const driver = Layer.scoped(
|
|
259
266
|
calc.store,
|
|
260
267
|
Effect.gen(function* () {
|
|
261
|
-
|
|
262
|
-
// `never` there); read it through a loose view for the runtime.
|
|
263
|
-
// oxlint-disable-next-line reform-rules/no-type-assertion -- loose runtime view: `disabled` is type-erased to `never` for non-gated calcs, no guard recovers it
|
|
264
|
-
const cfg = config as AsyncCalcLiveView<Inputs, A, E, R>
|
|
268
|
+
const cfg: AsyncCalcLiveView<Inputs, A, E, R> = config
|
|
265
269
|
|
|
266
|
-
// `persist: true` keys by the calc name; `{ key }` overrides — a static
|
|
267
|
-
// string for a singleton, or a function of the inputs for a keyed family
|
|
268
|
-
// (the driver resolves the function per-input at hydrate/write). The driver
|
|
269
|
-
// (de)serializes through the calc's own `output` schema.
|
|
270
270
|
const persistOption = cfg.persist
|
|
271
271
|
const persistKey =
|
|
272
272
|
persistOption === true || persistOption === undefined || persistOption === false
|
|
273
273
|
? calc.manifest.name
|
|
274
|
-
: persistOption.key ?? calc.manifest.name
|
|
274
|
+
: (persistOption.key ?? calc.manifest.name)
|
|
275
|
+
const persistCodec: QueryCodec<A> = {
|
|
276
|
+
encode: Schema.encode(calc.manifest.output),
|
|
277
|
+
decode: Schema.decodeUnknown(calc.manifest.output),
|
|
278
|
+
}
|
|
275
279
|
const persist =
|
|
276
280
|
persistOption === undefined || persistOption === false
|
|
277
281
|
? undefined
|
|
278
|
-
: { key: persistKey,
|
|
282
|
+
: { key: persistKey, codec: persistCodec }
|
|
279
283
|
|
|
280
|
-
// The hidden revision store, read requirement-free (`serviceOption`): the
|
|
281
|
-
// assembly below always provides it alongside this driver, and feeding it
|
|
282
|
-
// through `extraKey` (not `wireSources`) keeps it out of the snapshot —
|
|
283
|
-
// the user's `query` and `invalidateBy` receive exactly
|
|
284
|
-
// `InputsObject<Inputs>`, with no hidden property to leak into
|
|
285
|
-
// spread-into-RPC payloads.
|
|
286
284
|
const revision = yield* Match.value(revisionState).pipe(
|
|
287
285
|
Match.when(undefined, () => Effect.succeed(undefined)),
|
|
288
286
|
Match.orElse((state) =>
|
|
@@ -308,7 +306,6 @@ export function live<
|
|
|
308
306
|
extraKey,
|
|
309
307
|
})
|
|
310
308
|
|
|
311
|
-
// The internal store is the full union; narrow to the definition's arms.
|
|
312
309
|
return narrowStore<A, E, Gated>(driver.store)
|
|
313
310
|
}),
|
|
314
311
|
)
|
|
@@ -316,12 +313,6 @@ export function live<
|
|
|
316
313
|
if (config.invalidateOn === undefined || revisionState === undefined) {
|
|
317
314
|
return driver
|
|
318
315
|
}
|
|
319
|
-
// The hidden reducer: an ordinary `Reducer` folding every listed event to
|
|
320
|
-
// n + 1 — the revision's sole writer, registered/unregistered with this
|
|
321
|
-
// layer's scope like any user reducer. `Layer.provide` builds the hidden
|
|
322
|
-
// state once and feeds the same store to both the driver and the reducer,
|
|
323
|
-
// while the outer context (user inputs, the `Reducers` registry) passes
|
|
324
|
-
// through untouched.
|
|
325
316
|
const revisionReducer = Reducer.make(`${calc.manifest.name}/invalidateOn`, {
|
|
326
317
|
states: [revisionState],
|
|
327
318
|
events: config.invalidateOn,
|
|
@@ -331,14 +322,6 @@ export function live<
|
|
|
331
322
|
)
|
|
332
323
|
}
|
|
333
324
|
|
|
334
|
-
/**
|
|
335
|
-
* Mark a query's value stale (`isStale := true`) — without fetching. A stale
|
|
336
|
-
* query with active readers (a mounted view) refetches; an unread one refetches
|
|
337
|
-
* when next subscribed. The imperative counterpart of `invalidateOn`, and what a
|
|
338
|
-
* focus/online provider layer calls. A no-op if the calc isn't live; resolves the
|
|
339
|
-
* `Queries` registry (part of `Engine`).
|
|
340
|
-
*/
|
|
341
|
-
/** The minimal calc shape `invalidate`/`refetch` need: just its registry name. */
|
|
342
325
|
export interface NamedCalc {
|
|
343
326
|
readonly name: string
|
|
344
327
|
}
|
|
@@ -348,11 +331,5 @@ export const invalidate = (calc: NamedCalc): Effect.Effect<void, never, Queries>
|
|
|
348
331
|
Effect.sync(() => queries.byName.get(calc.name)?.invalidate()),
|
|
349
332
|
)
|
|
350
333
|
|
|
351
|
-
/**
|
|
352
|
-
* Force an immediate refetch of the current key, bypassing the no-op-key guard
|
|
353
|
-
* and independent of `isStale`. A no-op if the calc isn't live or is disabled.
|
|
354
|
-
*/
|
|
355
334
|
export const refetch = (calc: NamedCalc): Effect.Effect<void, never, Queries> =>
|
|
356
|
-
Effect.flatMap(Queries, (queries) =>
|
|
357
|
-
Effect.sync(() => queries.byName.get(calc.name)?.refetch()),
|
|
358
|
-
)
|
|
335
|
+
Effect.flatMap(Queries, (queries) => Effect.sync(() => queries.byName.get(calc.name)?.refetch()))
|
package/src/calc/asyncData.ts
CHANGED
|
@@ -1,50 +1,31 @@
|
|
|
1
1
|
import { type Store } from '../internal/store'
|
|
2
2
|
|
|
3
|
-
// The value an `AsyncCalc` holds: a tagged union of the query's lifecycle,
|
|
4
|
-
// consumed with `effect`'s `Match` (`Match.value(data).pipe(Match.tag('Success', …))`).
|
|
5
|
-
//
|
|
6
|
-
// The shape is narrowed by the definition, so you only handle arms that can
|
|
7
|
-
// occur: `Idle` exists only for a gateable query (no `alwaysOn`), and `Error`
|
|
8
|
-
// exists only when the query declares an `error` schema. There is no separate
|
|
9
|
-
// "refreshing" arm — a re-fetch keeps the last `Success`/`Error` and flips its
|
|
10
|
-
// `refetching` flag (stale-while-revalidate), so the UI never flashes empty.
|
|
11
|
-
|
|
12
|
-
/** A gated query that is currently switched off (only when not `alwaysOn`). */
|
|
13
3
|
export interface AsyncIdle {
|
|
14
4
|
readonly _tag: 'Idle'
|
|
15
5
|
}
|
|
16
6
|
|
|
17
|
-
/** The first fetch, before any value exists. */
|
|
18
7
|
export interface AsyncLoading {
|
|
19
8
|
readonly _tag: 'Loading'
|
|
20
9
|
}
|
|
21
10
|
|
|
22
|
-
/** A resolved value; `refetching` is true while a background re-fetch runs. */
|
|
23
11
|
export interface AsyncSuccess<A> {
|
|
24
12
|
readonly _tag: 'Success'
|
|
25
13
|
readonly value: A
|
|
26
14
|
readonly refetching: boolean
|
|
27
15
|
}
|
|
28
16
|
|
|
29
|
-
/** A failed query; `refetching` is true while a background re-fetch runs. */
|
|
30
17
|
export interface AsyncError<E> {
|
|
31
18
|
readonly _tag: 'Error'
|
|
32
19
|
readonly error: E
|
|
33
20
|
readonly refetching: boolean
|
|
34
21
|
}
|
|
35
22
|
|
|
36
|
-
/**
|
|
37
|
-
* The query lifecycle. `Gated` adds the `Idle` arm (a disable-able query); a
|
|
38
|
-
* non-`never` `E` adds the `Error` arm. Both are decided at `AsyncCalc.make`, so
|
|
39
|
-
* `yield* MyQuery` is typed to exactly the arms that can occur.
|
|
40
|
-
*/
|
|
41
23
|
export type AsyncData<A, E = never, Gated extends boolean = false> =
|
|
42
24
|
| (Gated extends true ? AsyncIdle : never)
|
|
43
25
|
| AsyncLoading
|
|
44
26
|
| AsyncSuccess<A>
|
|
45
27
|
| ([E] extends [never] ? never : AsyncError<E>)
|
|
46
28
|
|
|
47
|
-
/** The full (un-narrowed) union — used internally by the live store. */
|
|
48
29
|
export type AnyAsyncData<A, E> = AsyncIdle | AsyncLoading | AsyncSuccess<A> | AsyncError<E>
|
|
49
30
|
|
|
50
31
|
const idle: AsyncIdle = { _tag: 'Idle' }
|
|
@@ -60,7 +41,6 @@ const error = <E>(err: E, refetching = false): AsyncError<E> => ({
|
|
|
60
41
|
refetching,
|
|
61
42
|
})
|
|
62
43
|
|
|
63
|
-
/** The arm-constructor namespace exposed as `AsyncData`. */
|
|
64
44
|
export interface AsyncDataConstructors {
|
|
65
45
|
readonly idle: AsyncIdle
|
|
66
46
|
readonly loading: AsyncLoading
|
|
@@ -68,31 +48,14 @@ export interface AsyncDataConstructors {
|
|
|
68
48
|
readonly error: <E>(err: E, refetching?: boolean) => AsyncError<E>
|
|
69
49
|
}
|
|
70
50
|
|
|
71
|
-
/**
|
|
72
|
-
* Constructors for the arms, namespaced under the same name as the type so call
|
|
73
|
-
* sites read `AsyncData.success(v)` / `AsyncData.error(e)` — no `error` import
|
|
74
|
-
* shadowing the keyword and one obvious home for every arm.
|
|
75
|
-
*/
|
|
76
51
|
export const AsyncData: AsyncDataConstructors = { idle, loading, success, error }
|
|
77
52
|
|
|
78
|
-
/**
|
|
79
|
-
* Narrow the live store (which holds the full `AnyAsyncData` union) to the arms
|
|
80
|
-
* the definition actually permits (`Gated`/`E`). The single documented home for
|
|
81
|
-
* that narrowing, so `AsyncCalc.live` returns it without an inline cast.
|
|
82
|
-
*/
|
|
83
53
|
export const narrowStore = <A, E, Gated extends boolean>(
|
|
84
54
|
store: Store<AnyAsyncData<A, E>>,
|
|
85
55
|
): Store<AsyncData<A, E, Gated>> =>
|
|
86
56
|
// oxlint-disable-next-line reform-rules/no-type-assertion -- type-level narrow of the SAME runtime store to the definition's permitted arms; the single documented home for this seam
|
|
87
57
|
store as unknown as Store<AsyncData<A, E, Gated>>
|
|
88
58
|
|
|
89
|
-
/**
|
|
90
|
-
* The read-only inverse of `narrowStore`: widen a definition-narrowed store back
|
|
91
|
-
* to the full union, so a consumer (the `RemoteState` overlay) can dispatch on the
|
|
92
|
-
* lifecycle arms without carrying the definition's `Gated`/`E` conditionals.
|
|
93
|
-
* Sound for reads only — every narrowed value IS an `AnyAsyncData` — so callers
|
|
94
|
-
* must never `set` through the widened view (derived stores ignore `set` anyway).
|
|
95
|
-
*/
|
|
96
59
|
export const widenStore = <A, E, Gated extends boolean>(
|
|
97
60
|
store: Store<AsyncData<A, E, Gated>>,
|
|
98
61
|
): Store<AnyAsyncData<A, E>> =>
|