@playfast/reform 1.1.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 +85 -55
- package/package.json +17 -17
- package/src/boundary/boundary.test.ts +62 -16
- package/src/boundary/boundary.ts +141 -32
- package/src/calc/asyncCalc.invalidate.test.ts +516 -18
- package/src/calc/asyncCalc.test.ts +207 -175
- package/src/calc/asyncCalc.ts +168 -39
- package/src/calc/calc.test.ts +3 -5
- package/src/calc/calc.ts +44 -18
- package/src/calc/calcFamily.test.ts +80 -22
- package/src/calc/calcFamily.ts +56 -25
- package/src/calc/compose.ts +1 -14
- package/src/channel/channel.ts +128 -11
- package/src/compose/composition.test.ts +19 -0
- package/src/compose/composition.ts +346 -62
- package/src/compose/props.ts +13 -6
- package/src/compose/provide.ts +187 -46
- package/src/compose/slot.ts +156 -19
- package/src/compose/structure.test.ts +6 -3
- package/src/compose/structure.ts +106 -13
- package/src/compose/ui.test.ts +19 -3
- package/src/compose/ui.ts +147 -54
- package/src/compose/ui.typecheck.ts +40 -4
- package/src/definition/definition.ts +22 -9
- package/src/event/event.fromSource.test.ts +172 -0
- package/src/event/event.test.ts +10 -0
- package/src/event/event.ts +102 -18
- package/src/feature/feature.mount.test.ts +123 -56
- package/src/feature/feature.test.ts +67 -63
- package/src/feature/feature.ts +1317 -197
- package/src/feature/feature.typecheck.ts +253 -61
- package/src/graph/closure.ts +403 -0
- package/src/index.ts +171 -245
- package/src/internal/bucketCache.ts +39 -0
- package/src/internal/capture.ts +9 -4
- package/src/internal/env.ts +1 -3
- package/src/internal/errors.ts +21 -10
- package/src/internal/queryDriver.ts +120 -15
- package/src/internal/queryDriverStore.ts +8 -5
- package/src/internal/queryDriverTypes.ts +3 -1
- package/src/internal/reuse.test.ts +10 -3
- package/src/internal/reuse.ts +5 -2
- package/src/internal/scheduler.ts +4 -1
- package/src/internal/sources.ts +25 -11
- package/src/internal/track.ts +3 -2
- package/src/internal.ts +222 -0
- package/src/namespace/namespace.ts +46 -25
- package/src/procedure/procedure.ts +38 -11
- package/src/reducer/reducer.ts +78 -34
- package/src/remote/remoteState.test.ts +515 -339
- package/src/remote/remoteState.ts +572 -107
- package/src/remote/remoteState.typecheck.ts +47 -23
- package/src/runtime/appRuntime.activation.test.ts +100 -0
- package/src/runtime/appRuntime.test.ts +157 -105
- package/src/runtime/appRuntime.ts +394 -33
- package/src/runtime/eventBudget.test.ts +1 -4
- package/src/runtime/eventBudget.ts +9 -8
- package/src/runtime/hardening.test.ts +4 -9
- package/src/runtime/instrumentation.test.ts +174 -192
- package/src/runtime/instrumentation.ts +6 -11
- package/src/runtime/loop.test.ts +36 -0
- package/src/runtime/loop.ts +69 -40
- package/src/scene/featureScene.test.ts +4 -2
- package/src/scene/scene.ts +234 -64
- package/src/scene/seedScene.test.ts +69 -86
- package/src/state/state.nominal.typecheck.ts +68 -0
- package/src/state/state.test.ts +17 -0
- package/src/state/state.ts +79 -26
- package/src/state/stateFamily.test.ts +14 -0
- package/src/state/stateFamily.ts +55 -22
- package/src/state/stateGroup.ts +53 -17
- package/src/state/token.ts +40 -10
- package/src/synced/syncedStore.ts +46 -23
- package/src/testkit/flight.testkit.ts +75 -0
- package/src/wire/tree.test.ts +8 -4
- package/src/wire/triggers.test.ts +6 -2
- package/src/wire/triggers.ts +14 -10
- package/src/calc/asyncCalcDefinitions.ts +0 -48
- package/src/channel/procedureRegistry.ts +0 -90
- package/src/feature/featureBinding.ts +0 -48
- package/src/internal/ctx.ts +0 -9
- package/src/remote/remoteStateDefinition.ts +0 -135
- package/src/remote/remoteStateLayers.ts +0 -118
- package/src/remote/remoteStateLiveTypes.ts +0 -59
- package/src/remote/remoteStateSend.ts +0 -64
package/src/calc/asyncCalc.ts
CHANGED
|
@@ -1,28 +1,32 @@
|
|
|
1
|
-
import { Context, Effect, Layer, Match, Option } from 'effect'
|
|
1
|
+
import { Context, Effect, Layer, Match, Option, Schema } from 'effect'
|
|
2
2
|
import type { Effect as EffectType } from 'effect/Effect'
|
|
3
|
-
import { type Manifest } from '../definition/definition'
|
|
3
|
+
import { type Manifest, yieldableClass } from '../definition/definition'
|
|
4
4
|
import { type AnyEvent } from '../event/event'
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
type InputStores,
|
|
8
|
-
type InvalidateBy,
|
|
9
|
-
} from '../internal/sources'
|
|
5
|
+
import { type InputsObject, type InputStores, type InvalidateBy } from '../internal/sources'
|
|
6
|
+
import type { AnySchema } from '../internal/variance'
|
|
10
7
|
import {
|
|
11
8
|
bumpRevision,
|
|
9
|
+
type GatedOf,
|
|
10
|
+
gatedFlag,
|
|
12
11
|
makeQueryDriver,
|
|
12
|
+
type QueryCodec,
|
|
13
13
|
RevisionSchema,
|
|
14
14
|
revisionZero,
|
|
15
15
|
} from '../internal/queryDriver'
|
|
16
16
|
import { type Store } from '../internal/store'
|
|
17
|
+
import { readTracked } from '../internal/track'
|
|
17
18
|
import * as Reducer from '../reducer/reducer'
|
|
18
19
|
import { Reducers } from '../runtime/loop'
|
|
20
|
+
import { Queries } from '../runtime/queries'
|
|
19
21
|
import * as State from '../state/state'
|
|
20
|
-
import { type AnySource } from '../state/token'
|
|
22
|
+
import { type AnySource, type Source, type SourceCapture } from '../state/token'
|
|
21
23
|
import { type AsyncData, narrowStore } from './asyncData'
|
|
22
|
-
import type { AnySchema, AnyValue } from '../internal/variance'
|
|
23
|
-
export { invalidate, make, type NamedCalc, refetch } from './asyncCalcDefinitions'
|
|
24
24
|
|
|
25
|
-
export interface
|
|
25
|
+
export interface AsyncCalcSchemaReflection {
|
|
26
|
+
readonly ast: Schema.Schema<unknown, unknown, unknown>['ast']
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface AsyncCalcManifest<N extends string, A = unknown, E = never> extends Manifest {
|
|
26
30
|
readonly kind: 'AsyncCalc'
|
|
27
31
|
readonly name: N
|
|
28
32
|
readonly output: AnySchema<A>
|
|
@@ -31,30 +35,88 @@ export interface AsyncCalcManifest<N extends string, A, E> extends Manifest {
|
|
|
31
35
|
readonly gated: boolean
|
|
32
36
|
}
|
|
33
37
|
|
|
34
|
-
export
|
|
35
|
-
|
|
36
|
-
|
|
38
|
+
export const AsyncCalcStoreTypeId: unique symbol = Symbol.for('reform/AsyncCalcStore')
|
|
39
|
+
export type AsyncCalcStoreTypeId = typeof AsyncCalcStoreTypeId
|
|
40
|
+
|
|
41
|
+
interface AsyncCalcStoreCompatibilityExternalApi<
|
|
42
|
+
N extends string,
|
|
37
43
|
in out A,
|
|
38
44
|
in out E,
|
|
39
45
|
in out Gated extends boolean,
|
|
40
|
-
> 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>> {
|
|
41
67
|
new (): {}
|
|
42
68
|
readonly manifest: AsyncCalcManifest<N, A, E>
|
|
43
|
-
readonly store: Context.Tag<
|
|
69
|
+
readonly store: Context.Tag<AsyncCalcStore<N, A, E, Gated>, Store<AsyncData<A, E, Gated>>>
|
|
44
70
|
readonly name: N
|
|
45
71
|
readonly inputs: Inputs
|
|
46
72
|
readonly gated: Gated
|
|
47
73
|
}
|
|
48
74
|
|
|
49
|
-
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
|
+
> {
|
|
50
100
|
readonly inputs: Inputs
|
|
51
|
-
readonly output:
|
|
101
|
+
readonly output: ConfigSchema<OutputSchema>
|
|
52
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
|
|
53
|
-
readonly error?:
|
|
103
|
+
readonly error?: ConfigErrorSchema<ErrorSchema>
|
|
54
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
|
|
55
105
|
readonly alwaysOn?: AlwaysOn
|
|
56
106
|
}
|
|
57
107
|
|
|
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
|
+
|
|
58
120
|
export type AsyncCalcLive<
|
|
59
121
|
Inputs extends ReadonlyArray<AnySource>,
|
|
60
122
|
A,
|
|
@@ -63,7 +125,6 @@ export type AsyncCalcLive<
|
|
|
63
125
|
R,
|
|
64
126
|
> = {
|
|
65
127
|
readonly query: (inputs: InputsObject<Inputs>) => Effect.Effect<A, E, R>
|
|
66
|
-
// 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
|
|
67
128
|
readonly invalidateBy?: InvalidateBy<Inputs>
|
|
68
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
|
|
69
130
|
readonly coalesce?: 'switch' | 'trailing'
|
|
@@ -72,13 +133,10 @@ export type AsyncCalcLive<
|
|
|
72
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
|
|
73
134
|
readonly persist?:
|
|
74
135
|
| boolean
|
|
75
|
-
// 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
|
|
76
136
|
| { readonly key?: string | ((inputs: InputsObject<Inputs>) => string) }
|
|
77
137
|
} & (Gated extends true
|
|
78
|
-
?
|
|
79
|
-
|
|
80
|
-
: // 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
|
|
81
|
-
{ readonly disabled?: never })
|
|
138
|
+
? { readonly disabled?: (inputs: InputsObject<Inputs>) => boolean }
|
|
139
|
+
: { readonly disabled?: never })
|
|
82
140
|
|
|
83
141
|
export type AsyncCalcLiveWithInvalidateOn<
|
|
84
142
|
Inputs extends ReadonlyArray<AnySource>,
|
|
@@ -87,27 +145,83 @@ export type AsyncCalcLiveWithInvalidateOn<
|
|
|
87
145
|
Gated extends boolean,
|
|
88
146
|
R,
|
|
89
147
|
> = AsyncCalcLive<Inputs, A, E, Gated, R> & {
|
|
90
|
-
// 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
|
|
91
148
|
readonly invalidateOn?: ReadonlyArray<AnyEvent>
|
|
92
149
|
}
|
|
93
150
|
|
|
94
151
|
export interface AsyncCalcLiveView<Inputs extends ReadonlyArray<AnySource>, A, E, R> {
|
|
95
152
|
readonly query: (inputs: InputsObject<Inputs>) => Effect.Effect<A, E, R>
|
|
96
|
-
// 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
153
|
readonly invalidateBy?: InvalidateBy<Inputs>
|
|
98
|
-
// 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
|
|
99
154
|
readonly disabled?: (inputs: InputsObject<Inputs>) => boolean
|
|
100
|
-
// 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
|
|
101
155
|
readonly coalesce?: 'switch' | 'trailing'
|
|
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
|
|
103
156
|
readonly reuse?: boolean
|
|
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
|
|
105
157
|
readonly persist?:
|
|
106
158
|
| boolean
|
|
107
|
-
// 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
|
|
108
159
|
| { readonly key?: string | ((inputs: InputsObject<Inputs>) => string) }
|
|
109
160
|
}
|
|
110
161
|
|
|
162
|
+
export function make<
|
|
163
|
+
const N extends string,
|
|
164
|
+
const Inputs extends ReadonlyArray<AnySource>,
|
|
165
|
+
OutputSchema extends Schema.Schema.AnyNoContext,
|
|
166
|
+
ErrorSchema extends Schema.Schema.AnyNoContext | undefined = undefined,
|
|
167
|
+
const AlwaysOn extends boolean = false,
|
|
168
|
+
>(
|
|
169
|
+
name: N,
|
|
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}`)
|
|
200
|
+
const gated = gatedFlag(config.alwaysOn)
|
|
201
|
+
const manifest: AsyncCalcManifest<N, A, E> = {
|
|
202
|
+
kind: 'AsyncCalc',
|
|
203
|
+
name,
|
|
204
|
+
output: config.output,
|
|
205
|
+
gated,
|
|
206
|
+
...(config.error !== undefined ? { error: config.error } : {}),
|
|
207
|
+
}
|
|
208
|
+
const codec: QueryCodec<A> = {
|
|
209
|
+
encode: Schema.encode(config.output),
|
|
210
|
+
decode: Schema.decodeUnknown(config.output),
|
|
211
|
+
}
|
|
212
|
+
const read = Effect.flatMap(store, readTracked)
|
|
213
|
+
const calc: DefinedAsyncCalcClass<N, Inputs, A, E, GatedOf<AlwaysOn>> = yieldableClass(read, {
|
|
214
|
+
manifest,
|
|
215
|
+
codec,
|
|
216
|
+
store,
|
|
217
|
+
name,
|
|
218
|
+
inputs: config.inputs,
|
|
219
|
+
gated,
|
|
220
|
+
capture: <Result>(visit: SourceCapture<Result>): Result => visit(calc),
|
|
221
|
+
})
|
|
222
|
+
return calc
|
|
223
|
+
}
|
|
224
|
+
|
|
111
225
|
export function live<
|
|
112
226
|
N extends string,
|
|
113
227
|
Inputs extends ReadonlyArray<AnySource>,
|
|
@@ -120,7 +234,7 @@ export function live<
|
|
|
120
234
|
config: AsyncCalcLive<Inputs, A, E, Gated, R> & {
|
|
121
235
|
readonly invalidateOn: ReadonlyArray<AnyEvent>
|
|
122
236
|
},
|
|
123
|
-
): Layer.Layer<
|
|
237
|
+
): Layer.Layer<AsyncCalcStore<N, A, E, Gated>, never, InputStores<Inputs> | R | Reducers>
|
|
124
238
|
export function live<
|
|
125
239
|
N extends string,
|
|
126
240
|
Inputs extends ReadonlyArray<AnySource>,
|
|
@@ -131,7 +245,7 @@ export function live<
|
|
|
131
245
|
>(
|
|
132
246
|
calc: AsyncCalcClass<N, Inputs, A, E, Gated>,
|
|
133
247
|
config: AsyncCalcLive<Inputs, A, E, Gated, R>,
|
|
134
|
-
): Layer.Layer<
|
|
248
|
+
): Layer.Layer<AsyncCalcStore<N, A, E, Gated>, never, InputStores<Inputs> | R>
|
|
135
249
|
export function live<
|
|
136
250
|
N extends string,
|
|
137
251
|
Inputs extends ReadonlyArray<AnySource>,
|
|
@@ -142,7 +256,7 @@ export function live<
|
|
|
142
256
|
>(
|
|
143
257
|
calc: AsyncCalcClass<N, Inputs, A, E, Gated>,
|
|
144
258
|
config: AsyncCalcLiveWithInvalidateOn<Inputs, A, E, Gated, R>,
|
|
145
|
-
): Layer.Layer<
|
|
259
|
+
): Layer.Layer<AsyncCalcStore<N, A, E, Gated>, never, InputStores<Inputs> | R | Reducers> {
|
|
146
260
|
const revisionState =
|
|
147
261
|
config.invalidateOn === undefined
|
|
148
262
|
? undefined
|
|
@@ -151,18 +265,21 @@ export function live<
|
|
|
151
265
|
const driver = Layer.scoped(
|
|
152
266
|
calc.store,
|
|
153
267
|
Effect.gen(function* () {
|
|
154
|
-
|
|
155
|
-
const cfg = config as AsyncCalcLiveView<Inputs, A, E, R>
|
|
268
|
+
const cfg: AsyncCalcLiveView<Inputs, A, E, R> = config
|
|
156
269
|
|
|
157
270
|
const persistOption = cfg.persist
|
|
158
271
|
const persistKey =
|
|
159
272
|
persistOption === true || persistOption === undefined || persistOption === false
|
|
160
273
|
? calc.manifest.name
|
|
161
|
-
: 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
|
+
}
|
|
162
279
|
const persist =
|
|
163
280
|
persistOption === undefined || persistOption === false
|
|
164
281
|
? undefined
|
|
165
|
-
: { key: persistKey,
|
|
282
|
+
: { key: persistKey, codec: persistCodec }
|
|
166
283
|
|
|
167
284
|
const revision = yield* Match.value(revisionState).pipe(
|
|
168
285
|
Match.when(undefined, () => Effect.succeed(undefined)),
|
|
@@ -204,3 +321,15 @@ export function live<
|
|
|
204
321
|
Layer.provide(State.live(revisionState, revisionZero)),
|
|
205
322
|
)
|
|
206
323
|
}
|
|
324
|
+
|
|
325
|
+
export interface NamedCalc {
|
|
326
|
+
readonly name: string
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export const invalidate = (calc: NamedCalc): Effect.Effect<void, never, Queries> =>
|
|
330
|
+
Effect.flatMap(Queries, (queries) =>
|
|
331
|
+
Effect.sync(() => queries.byName.get(calc.name)?.invalidate()),
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
export const refetch = (calc: NamedCalc): Effect.Effect<void, never, Queries> =>
|
|
335
|
+
Effect.flatMap(Queries, (queries) => Effect.sync(() => queries.byName.get(calc.name)?.refetch()))
|
package/src/calc/calc.test.ts
CHANGED
|
@@ -211,11 +211,9 @@ it.live('reuse: an unchanged subtree keeps its identity across recompute', () =>
|
|
|
211
211
|
inputs: [StateGroup.select(Inputs, 'a'), StateGroup.select(Inputs, 'b')],
|
|
212
212
|
output: Pair,
|
|
213
213
|
}) {}
|
|
214
|
-
const SplitLive = Calc.live(
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
{ reuse: true },
|
|
218
|
-
)
|
|
214
|
+
const SplitLive = Calc.live(Split, ({ a, b }) => ({ left: { value: a }, right: { value: b } }), {
|
|
215
|
+
reuse: true,
|
|
216
|
+
})
|
|
219
217
|
const TestLayer = SplitLive.pipe(Layer.provideMerge(StateGroup.live(Inputs, { a: 1, b: 1 })))
|
|
220
218
|
|
|
221
219
|
return Effect.gen(function* () {
|
package/src/calc/calc.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Context, Effect, Layer, MutableRef } from 'effect'
|
|
1
|
+
import { Context, Effect, Layer, MutableRef, type Schema } from 'effect'
|
|
2
2
|
import type { Effect as EffectType } from 'effect/Effect'
|
|
3
3
|
import { type Manifest, yieldableClass } from '../definition/definition'
|
|
4
4
|
import {
|
|
@@ -13,8 +13,7 @@ import { resolveInstrumentation, stateUpdateHook } from '../runtime/instrumentat
|
|
|
13
13
|
import { reuse } from '../internal/reuse'
|
|
14
14
|
import { makeDerivedStore, type Store } from '../internal/store'
|
|
15
15
|
import { readTracked } from '../internal/track'
|
|
16
|
-
import { type AnySource } from '../state/token'
|
|
17
|
-
import type { AnySchema } from '../internal/variance'
|
|
16
|
+
import { type AnySource, type Source, type SourceCapture } from '../state/token'
|
|
18
17
|
|
|
19
18
|
export { type InputsObject, type InputStores, type InvalidateBy } from '../internal/sources'
|
|
20
19
|
|
|
@@ -23,41 +22,68 @@ export interface CalcOptionsExternalApi<Inputs extends ReadonlyArray<AnySource>>
|
|
|
23
22
|
readonly reuse?: boolean
|
|
24
23
|
}
|
|
25
24
|
|
|
26
|
-
export interface
|
|
25
|
+
export interface CalcSchemaReflection {
|
|
26
|
+
readonly ast: Schema.Schema<unknown, unknown, unknown>['ast']
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface CalcManifest<N extends string> extends Manifest {
|
|
27
30
|
readonly kind: 'Calc'
|
|
28
31
|
readonly name: N
|
|
29
|
-
readonly output:
|
|
32
|
+
readonly output: CalcSchemaReflection
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const CalcStoreTypeId: unique symbol = Symbol.for('reform/CalcStore')
|
|
36
|
+
export type CalcStoreTypeId = typeof CalcStoreTypeId
|
|
37
|
+
|
|
38
|
+
export interface CalcStore<N extends string, in out Out> extends Store<Out> {
|
|
39
|
+
readonly [CalcStoreTypeId]: N
|
|
30
40
|
}
|
|
31
41
|
|
|
32
|
-
export interface CalcClass<
|
|
33
|
-
extends EffectType<Out, never,
|
|
42
|
+
export interface CalcClass<N extends string, Inputs extends ReadonlyArray<AnySource>, Out>
|
|
43
|
+
extends EffectType<Out, never, CalcStore<N, Out>>, Source<N, Out, CalcStore<N, Out>> {
|
|
34
44
|
new (): {}
|
|
35
|
-
readonly manifest: CalcManifest<N
|
|
36
|
-
readonly store: Context.Tag<
|
|
45
|
+
readonly manifest: CalcManifest<N>
|
|
46
|
+
readonly store: Context.Tag<CalcStore<N, Out>, Store<Out>>
|
|
37
47
|
readonly name: N
|
|
38
48
|
readonly inputs: Inputs
|
|
49
|
+
readonly capture: <Result>(visit: SourceCapture<Result>) => Result
|
|
39
50
|
}
|
|
40
51
|
|
|
41
|
-
export interface CalcConfig<
|
|
52
|
+
export interface CalcConfig<
|
|
53
|
+
Inputs extends ReadonlyArray<AnySource>,
|
|
54
|
+
OutputSchema extends Schema.Schema.AnyNoContext,
|
|
55
|
+
> {
|
|
42
56
|
readonly inputs: Inputs
|
|
43
|
-
readonly output:
|
|
57
|
+
readonly output: OutputSchema
|
|
44
58
|
}
|
|
45
59
|
|
|
46
|
-
export const make = <
|
|
60
|
+
export const make = <
|
|
61
|
+
const N extends string,
|
|
62
|
+
const Inputs extends ReadonlyArray<AnySource>,
|
|
63
|
+
OutputSchema extends Schema.Schema.AnyNoContext,
|
|
64
|
+
>(
|
|
47
65
|
name: N,
|
|
48
|
-
config: CalcConfig<Inputs,
|
|
49
|
-
): CalcClass<N, Inputs,
|
|
50
|
-
|
|
51
|
-
const
|
|
66
|
+
config: CalcConfig<Inputs, OutputSchema>,
|
|
67
|
+
): CalcClass<N, Inputs, Schema.Schema.Type<OutputSchema>> => {
|
|
68
|
+
type Out = Schema.Schema.Type<OutputSchema>
|
|
69
|
+
const store = Context.GenericTag<CalcStore<N, Out>, Store<Out>>(`reform/calc/${name}`)
|
|
70
|
+
const manifest: CalcManifest<N> = { kind: 'Calc', name, output: config.output }
|
|
52
71
|
const read = Effect.flatMap(store, readTracked)
|
|
53
|
-
|
|
72
|
+
const calc: CalcClass<N, Inputs, Out> = yieldableClass(read, {
|
|
73
|
+
manifest,
|
|
74
|
+
store,
|
|
75
|
+
name,
|
|
76
|
+
inputs: config.inputs,
|
|
77
|
+
capture: <Result>(visit: SourceCapture<Result>): Result => visit(calc),
|
|
78
|
+
})
|
|
79
|
+
return calc
|
|
54
80
|
}
|
|
55
81
|
|
|
56
82
|
export const live = <N extends string, Inputs extends ReadonlyArray<AnySource>, Out>(
|
|
57
83
|
calc: CalcClass<N, Inputs, Out>,
|
|
58
84
|
compute: (inputs: InputsObject<Inputs>) => Out,
|
|
59
85
|
options: CalcOptionsExternalApi<Inputs> = {},
|
|
60
|
-
): Layer.Layer<
|
|
86
|
+
): Layer.Layer<CalcStore<N, Out>, never, InputStores<Inputs>> =>
|
|
61
87
|
Layer.scoped(
|
|
62
88
|
calc.store,
|
|
63
89
|
Effect.gen(function* () {
|
|
@@ -64,19 +64,79 @@ it.live('a member only notifies when its own slice of the projection moves', ()
|
|
|
64
64
|
}).pipe(Effect.provide(TestLayer))
|
|
65
65
|
})
|
|
66
66
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
67
|
+
const reuseFixture = () => {
|
|
68
|
+
class Feed extends State.make('reuse-feed', Board) {}
|
|
69
|
+
class Revision extends State.make('reuse-revision', S.Number) {}
|
|
70
|
+
class Inputs extends StateGroup.make(Feed, Revision) {}
|
|
71
|
+
class Presentation extends CalcFamily.make('ReusePresentation', {
|
|
72
|
+
key: S.String,
|
|
73
|
+
inputs: [StateGroup.select(Inputs, 'reuse-feed'), StateGroup.select(Inputs, 'reuse-revision')],
|
|
74
|
+
output: S.Struct({ nested: S.Array(S.Struct({ value: S.Number })) }),
|
|
75
|
+
}) {}
|
|
76
|
+
const PresentationLive = CalcFamily.live(
|
|
77
|
+
Presentation,
|
|
78
|
+
(key) =>
|
|
79
|
+
({ 'reuse-feed': feed }) => ({
|
|
80
|
+
nested: [{ value: feed[key] ?? 0 }],
|
|
81
|
+
}),
|
|
82
|
+
{ reuse: true },
|
|
83
|
+
)
|
|
84
|
+
return { Inputs, Presentation, PresentationLive }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
it.live('reuse preserves each member output when equal and replaces it when changed', () => {
|
|
88
|
+
const { Inputs, Presentation, PresentationLive } = reuseFixture()
|
|
89
|
+
const TestLayer = PresentationLive.pipe(
|
|
90
|
+
Layer.provideMerge(
|
|
91
|
+
StateGroup.live(Inputs, { 'reuse-feed': { a: 1, b: 1 }, 'reuse-revision': 0 }),
|
|
92
|
+
),
|
|
71
93
|
)
|
|
72
94
|
return Effect.gen(function* () {
|
|
73
|
-
yield*
|
|
74
|
-
yield*
|
|
75
|
-
yield*
|
|
76
|
-
|
|
95
|
+
const feed = yield* StateGroup.select(Inputs, 'reuse-feed').store
|
|
96
|
+
const revision = yield* StateGroup.select(Inputs, 'reuse-revision').store
|
|
97
|
+
const family = yield* Presentation.store
|
|
98
|
+
const initialA = family.at('a').get()
|
|
99
|
+
const initialB = family.at('b').get()
|
|
100
|
+
const wakes = { a: 0, b: 0 }
|
|
101
|
+
family.at('a').subscribe(() => {
|
|
102
|
+
wakes.a += 1
|
|
103
|
+
})
|
|
104
|
+
family.at('b').subscribe(() => {
|
|
105
|
+
wakes.b += 1
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
expect(Object.is(initialA.nested, initialB.nested)).toBe(false)
|
|
109
|
+
revision.set(1)
|
|
110
|
+
yield* tick
|
|
111
|
+
expect(Object.is(family.at('a').get().nested, initialA.nested)).toBe(true)
|
|
112
|
+
expect(Object.is(family.at('b').get().nested, initialB.nested)).toBe(true)
|
|
113
|
+
expect(wakes).toEqual({ a: 0, b: 0 })
|
|
114
|
+
|
|
115
|
+
feed.set({ a: 2, b: 1 })
|
|
116
|
+
yield* tick
|
|
117
|
+
expect(family.at('a').get().nested).toEqual([{ value: 2 }])
|
|
118
|
+
expect(Object.is(family.at('a').get().nested, initialA.nested)).toBe(false)
|
|
119
|
+
expect(Object.is(family.at('b').get().nested, initialB.nested)).toBe(true)
|
|
120
|
+
expect(wakes).toEqual({ a: 1, b: 0 })
|
|
77
121
|
}).pipe(Effect.provide(TestLayer))
|
|
78
122
|
})
|
|
79
123
|
|
|
124
|
+
it.live(
|
|
125
|
+
'reading does not recompute while the shared inputs are unchanged (memoized per member)',
|
|
126
|
+
() => {
|
|
127
|
+
const { Inputs, Slice, SliceLive, runs } = fixture()
|
|
128
|
+
const TestLayer = SliceLive.pipe(
|
|
129
|
+
Layer.provideMerge(StateGroup.live(Inputs, { feed: { a: 1 } })),
|
|
130
|
+
)
|
|
131
|
+
return Effect.gen(function* () {
|
|
132
|
+
yield* CalcFamily.read(Slice, 'a')
|
|
133
|
+
yield* CalcFamily.read(Slice, 'a')
|
|
134
|
+
yield* CalcFamily.read(Slice, 'a')
|
|
135
|
+
expect(runs.get('a')).toBe(1)
|
|
136
|
+
}).pipe(Effect.provide(TestLayer))
|
|
137
|
+
},
|
|
138
|
+
)
|
|
139
|
+
|
|
80
140
|
it.live('invalidateBy: shared-input churn outside the key recomputes no member', () => {
|
|
81
141
|
class Feed extends State.make('feed', Board) {}
|
|
82
142
|
class Noise extends State.make('noise', S.Number) {}
|
|
@@ -89,10 +149,11 @@ it.live('invalidateBy: shared-input churn outside the key recomputes no member',
|
|
|
89
149
|
}) {}
|
|
90
150
|
const SliceLive = CalcFamily.live(
|
|
91
151
|
Slice,
|
|
92
|
-
(key) =>
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
152
|
+
(key) =>
|
|
153
|
+
({ feed }) => {
|
|
154
|
+
runs.set(key, (runs.get(key) ?? 0) + 1)
|
|
155
|
+
return feed[key] ?? 0
|
|
156
|
+
},
|
|
96
157
|
{ invalidateBy: ({ feed }) => [feed] },
|
|
97
158
|
)
|
|
98
159
|
const TestLayer = SliceLive.pipe(
|
|
@@ -112,9 +173,7 @@ it.live('invalidateBy: shared-input churn outside the key recomputes no member',
|
|
|
112
173
|
|
|
113
174
|
it.live('forget releases the member and its upstream subscription', () => {
|
|
114
175
|
const { Inputs, Slice, SliceLive, runs } = fixture()
|
|
115
|
-
const TestLayer = SliceLive.pipe(
|
|
116
|
-
Layer.provideMerge(StateGroup.live(Inputs, { feed: { a: 1 } })),
|
|
117
|
-
)
|
|
176
|
+
const TestLayer = SliceLive.pipe(Layer.provideMerge(StateGroup.live(Inputs, { feed: { a: 1 } })))
|
|
118
177
|
return Effect.gen(function* () {
|
|
119
178
|
const feed = yield* StateGroup.select(Inputs, 'feed').store
|
|
120
179
|
const family = yield* Slice.store
|
|
@@ -143,15 +202,14 @@ it.live('evictWhenUnused: an idle member is dropped on the next microtask and un
|
|
|
143
202
|
}) {}
|
|
144
203
|
const SliceLive = CalcFamily.live(
|
|
145
204
|
Slice,
|
|
146
|
-
(key) =>
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
205
|
+
(key) =>
|
|
206
|
+
({ feed }) => {
|
|
207
|
+
runs.set(key, (runs.get(key) ?? 0) + 1)
|
|
208
|
+
return feed[key] ?? 0
|
|
209
|
+
},
|
|
150
210
|
{ evictWhenUnused: true },
|
|
151
211
|
)
|
|
152
|
-
const TestLayer = SliceLive.pipe(
|
|
153
|
-
Layer.provideMerge(StateGroup.live(Inputs, { feed: { a: 1 } })),
|
|
154
|
-
)
|
|
212
|
+
const TestLayer = SliceLive.pipe(Layer.provideMerge(StateGroup.live(Inputs, { feed: { a: 1 } })))
|
|
155
213
|
return Effect.gen(function* () {
|
|
156
214
|
const feed = yield* StateGroup.select(Inputs, 'feed').store
|
|
157
215
|
const family = yield* Slice.store
|