@playfast/reform 1.1.1 → 1.2.1
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 +82 -33
- package/src/boundary/boundary.ts +141 -32
- package/src/calc/asyncCalc.invalidate.test.ts +733 -32
- package/src/calc/asyncCalc.test.ts +397 -225
- package/src/calc/asyncCalc.ts +115 -100
- package/src/calc/asyncCalcTypes.ts +147 -0
- package/src/calc/calc.test.ts +13 -16
- package/src/calc/calc.ts +44 -18
- package/src/calc/calcFamily.test.ts +90 -32
- package/src/calc/calcFamily.ts +56 -25
- package/src/calc/compose.ts +1 -14
- package/src/channel/channel.ts +25 -6
- package/src/channel/{procedureRegistry.ts → procedures.ts} +41 -23
- package/src/compose/composition.test.ts +19 -0
- package/src/compose/composition.ts +168 -81
- package/src/compose/compositionTypes.ts +249 -0
- 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 +222 -262
- package/src/feature/feature.typecheck.ts +224 -91
- package/src/feature/featureBinding.ts +130 -20
- package/src/feature/featureClass.ts +162 -0
- package/src/feature/featureConfig.ts +192 -0
- package/src/feature/featureFactory.ts +163 -0
- package/src/feature/featureModule.ts +136 -0
- package/src/feature/featureModule.typecheck.ts +63 -0
- package/src/feature/featureNativeTree.ts +152 -0
- package/src/feature/featureRequirement.ts +82 -0
- package/src/feature/featureVariants.ts +234 -0
- package/src/feature/mountFeature.ts +54 -0
- package/src/graph/closure.ts +222 -0
- package/src/graph/services.ts +95 -0
- package/src/graph/summary.ts +134 -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 +61 -25
- package/src/internal/queryDriverHydrate.ts +45 -0
- package/src/internal/queryDriverStore.ts +8 -5
- package/src/internal/queryDriverTypes.ts +11 -11
- 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/store.test.ts +27 -6
- 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/pendingQueue.ts +145 -0
- package/src/remote/remoteState.test.ts +536 -354
- package/src/remote/remoteState.ts +145 -106
- package/src/remote/remoteState.typecheck.ts +47 -23
- package/src/remote/remoteStateMake.ts +97 -0
- package/src/remote/remoteStateSend.ts +95 -37
- package/src/remote/remoteStateTypes.ts +155 -0
- package/src/runtime/appRuntime.activation.test.ts +104 -0
- package/src/runtime/appRuntime.test.ts +187 -111
- package/src/runtime/appRuntime.ts +36 -130
- package/src/runtime/capturedAppRuntime.ts +274 -0
- package/src/runtime/eventBudget.test.ts +39 -12
- package/src/runtime/eventBudget.ts +9 -8
- package/src/runtime/featureMount.ts +94 -0
- package/src/runtime/hardening.test.ts +22 -13
- package/src/runtime/instrumentation.test.ts +213 -194
- package/src/runtime/instrumentation.ts +6 -11
- package/src/runtime/loop.test.ts +84 -7
- package/src/runtime/loop.ts +69 -40
- package/src/runtime/runtimeHandle.ts +124 -0
- 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 +39 -8
- 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 +84 -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/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/calc/asyncCalc.ts
CHANGED
|
@@ -1,111 +1,111 @@
|
|
|
1
|
-
import { Context, Effect, Layer, Match, Option } from 'effect'
|
|
2
|
-
import
|
|
3
|
-
import { type Manifest } from '../definition/definition'
|
|
1
|
+
import { Context, Effect, Layer, Match, Option, Schema } from 'effect'
|
|
2
|
+
import { yieldableClass } from '../definition/definition'
|
|
4
3
|
import { type AnyEvent } from '../event/event'
|
|
5
|
-
import {
|
|
6
|
-
type InputsObject,
|
|
7
|
-
type InputStores,
|
|
8
|
-
type InvalidateBy,
|
|
9
|
-
} from '../internal/sources'
|
|
4
|
+
import { type InputStores } from '../internal/sources'
|
|
10
5
|
import {
|
|
11
6
|
bumpRevision,
|
|
7
|
+
type GatedOf,
|
|
8
|
+
gatedFlag,
|
|
12
9
|
makeQueryDriver,
|
|
10
|
+
type QueryCodec,
|
|
13
11
|
RevisionSchema,
|
|
14
12
|
revisionZero,
|
|
15
13
|
} from '../internal/queryDriver'
|
|
16
14
|
import { type Store } from '../internal/store'
|
|
15
|
+
import { readTracked } from '../internal/track'
|
|
17
16
|
import * as Reducer from '../reducer/reducer'
|
|
18
17
|
import { Reducers } from '../runtime/loop'
|
|
18
|
+
import { Queries } from '../runtime/queries'
|
|
19
19
|
import * as State from '../state/state'
|
|
20
|
-
import { type AnySource } from '../state/token'
|
|
20
|
+
import { type AnySource, type SourceCapture } from '../state/token'
|
|
21
21
|
import { type AsyncData, narrowStore } from './asyncData'
|
|
22
|
-
import type {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export interface AsyncCalcClass<
|
|
35
|
-
out N extends string,
|
|
36
|
-
out Inputs extends ReadonlyArray<AnySource>,
|
|
37
|
-
in out A,
|
|
38
|
-
in out E,
|
|
39
|
-
in out Gated extends boolean,
|
|
40
|
-
> extends EffectType<AsyncData<A, E, Gated>, never, Store<AsyncData<A, E, Gated>>> {
|
|
41
|
-
new (): {}
|
|
42
|
-
readonly manifest: AsyncCalcManifest<N, A, E>
|
|
43
|
-
readonly store: Context.Tag<Store<AsyncData<A, E, Gated>>, Store<AsyncData<A, E, Gated>>>
|
|
44
|
-
readonly name: N
|
|
45
|
-
readonly inputs: Inputs
|
|
46
|
-
readonly gated: Gated
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export interface AsyncCalcConfig<Inputs extends ReadonlyArray<AnySource>, A, E, AlwaysOn extends boolean> {
|
|
50
|
-
readonly inputs: Inputs
|
|
51
|
-
readonly output: AnySchema<A>
|
|
52
|
-
// 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?: AnySchema<E>
|
|
54
|
-
// 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
|
-
readonly alwaysOn?: AlwaysOn
|
|
56
|
-
}
|
|
22
|
+
import type {
|
|
23
|
+
AsyncCalcClass,
|
|
24
|
+
AsyncCalcConfig,
|
|
25
|
+
AsyncCalcLive,
|
|
26
|
+
AsyncCalcLiveView,
|
|
27
|
+
AsyncCalcLiveWithInvalidateOn,
|
|
28
|
+
AsyncCalcMakeConfigExternalApi,
|
|
29
|
+
AsyncCalcManifest,
|
|
30
|
+
AsyncCalcStore,
|
|
31
|
+
DefinedAsyncCalcClass,
|
|
32
|
+
} from './asyncCalcTypes'
|
|
57
33
|
|
|
58
|
-
export
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
// 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
|
-
readonly reuse?: boolean
|
|
72
|
-
// 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
|
-
readonly persist?:
|
|
74
|
-
| 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
|
-
| { readonly key?: string | ((inputs: InputsObject<Inputs>) => string) }
|
|
77
|
-
} & (Gated extends true
|
|
78
|
-
? // 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
|
|
79
|
-
{ readonly disabled?: (inputs: InputsObject<Inputs>) => boolean }
|
|
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 })
|
|
34
|
+
export { AsyncCalcStoreTypeId } from './asyncCalcTypes'
|
|
35
|
+
export type {
|
|
36
|
+
AsyncCalcClass,
|
|
37
|
+
AsyncCalcConfig,
|
|
38
|
+
AsyncCalcLive,
|
|
39
|
+
AsyncCalcLiveView,
|
|
40
|
+
AsyncCalcLiveWithInvalidateOn,
|
|
41
|
+
AsyncCalcManifest,
|
|
42
|
+
AsyncCalcSchemaReflection,
|
|
43
|
+
AsyncCalcStore,
|
|
44
|
+
DefinedAsyncCalcClass,
|
|
45
|
+
} from './asyncCalcTypes'
|
|
82
46
|
|
|
83
|
-
export type AsyncCalcLiveWithInvalidateOn<
|
|
84
|
-
Inputs extends ReadonlyArray<AnySource>,
|
|
85
|
-
A,
|
|
86
|
-
E,
|
|
87
|
-
Gated extends boolean,
|
|
88
|
-
R,
|
|
89
|
-
> = 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
|
-
readonly invalidateOn?: ReadonlyArray<AnyEvent>
|
|
92
|
-
}
|
|
93
47
|
|
|
94
|
-
export
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
48
|
+
export function make<
|
|
49
|
+
const N extends string,
|
|
50
|
+
const Inputs extends ReadonlyArray<AnySource>,
|
|
51
|
+
OutputSchema extends Schema.Schema.AnyNoContext,
|
|
52
|
+
ErrorSchema extends Schema.Schema.AnyNoContext | undefined = undefined,
|
|
53
|
+
const AlwaysOn extends boolean = false,
|
|
54
|
+
>(
|
|
55
|
+
name: N,
|
|
56
|
+
config: AsyncCalcConfig<Inputs, OutputSchema, ErrorSchema, AlwaysOn>,
|
|
57
|
+
): DefinedAsyncCalcClass<
|
|
58
|
+
N,
|
|
59
|
+
Inputs,
|
|
60
|
+
Schema.Schema.Type<OutputSchema>,
|
|
61
|
+
ErrorSchema extends Schema.Schema.AnyNoContext ? Schema.Schema.Type<ErrorSchema> : never,
|
|
62
|
+
GatedOf<AlwaysOn>
|
|
63
|
+
>
|
|
64
|
+
export function make<
|
|
65
|
+
const N extends string,
|
|
66
|
+
const Inputs extends ReadonlyArray<AnySource>,
|
|
67
|
+
OutputSchema extends Schema.Schema.AnyNoContext,
|
|
68
|
+
ErrorSchema extends Schema.Schema.AnyNoContext | undefined = undefined,
|
|
69
|
+
const AlwaysOn extends boolean = false,
|
|
70
|
+
>(
|
|
71
|
+
name: N,
|
|
72
|
+
config: AsyncCalcMakeConfigExternalApi<Inputs, OutputSchema, ErrorSchema, AlwaysOn>,
|
|
73
|
+
): DefinedAsyncCalcClass<
|
|
74
|
+
N,
|
|
75
|
+
Inputs,
|
|
76
|
+
Schema.Schema.Type<OutputSchema>,
|
|
77
|
+
ErrorSchema extends Schema.Schema.AnyNoContext ? Schema.Schema.Type<ErrorSchema> : never,
|
|
78
|
+
GatedOf<AlwaysOn>
|
|
79
|
+
> {
|
|
80
|
+
type A = Schema.Schema.Type<OutputSchema>
|
|
81
|
+
type E = ErrorSchema extends Schema.Schema.AnyNoContext ? Schema.Schema.Type<ErrorSchema> : never
|
|
82
|
+
const store = Context.GenericTag<
|
|
83
|
+
AsyncCalcStore<N, A, E, GatedOf<AlwaysOn>>,
|
|
84
|
+
Store<AsyncData<A, E, GatedOf<AlwaysOn>>>
|
|
85
|
+
>(`reform/asyncCalc/${name}`)
|
|
86
|
+
const gated = gatedFlag(config.alwaysOn)
|
|
87
|
+
const manifest: AsyncCalcManifest<N, A, E> = {
|
|
88
|
+
kind: 'AsyncCalc',
|
|
89
|
+
name,
|
|
90
|
+
output: config.output,
|
|
91
|
+
gated,
|
|
92
|
+
...(config.error !== undefined ? { error: config.error } : {}),
|
|
93
|
+
}
|
|
94
|
+
const codec: QueryCodec<A> = {
|
|
95
|
+
encode: Schema.encode(config.output),
|
|
96
|
+
decode: Schema.decodeUnknown(config.output),
|
|
97
|
+
}
|
|
98
|
+
const read = Effect.flatMap(store, readTracked)
|
|
99
|
+
const calc: DefinedAsyncCalcClass<N, Inputs, A, E, GatedOf<AlwaysOn>> = yieldableClass(read, {
|
|
100
|
+
manifest,
|
|
101
|
+
codec,
|
|
102
|
+
store,
|
|
103
|
+
name,
|
|
104
|
+
inputs: config.inputs,
|
|
105
|
+
gated,
|
|
106
|
+
capture: <Result>(visit: SourceCapture<Result>): Result => visit(calc),
|
|
107
|
+
})
|
|
108
|
+
return calc
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
export function live<
|
|
@@ -120,7 +120,7 @@ export function live<
|
|
|
120
120
|
config: AsyncCalcLive<Inputs, A, E, Gated, R> & {
|
|
121
121
|
readonly invalidateOn: ReadonlyArray<AnyEvent>
|
|
122
122
|
},
|
|
123
|
-
): Layer.Layer<
|
|
123
|
+
): Layer.Layer<AsyncCalcStore<N, A, E, Gated>, never, InputStores<Inputs> | R | Reducers>
|
|
124
124
|
export function live<
|
|
125
125
|
N extends string,
|
|
126
126
|
Inputs extends ReadonlyArray<AnySource>,
|
|
@@ -131,7 +131,7 @@ export function live<
|
|
|
131
131
|
>(
|
|
132
132
|
calc: AsyncCalcClass<N, Inputs, A, E, Gated>,
|
|
133
133
|
config: AsyncCalcLive<Inputs, A, E, Gated, R>,
|
|
134
|
-
): Layer.Layer<
|
|
134
|
+
): Layer.Layer<AsyncCalcStore<N, A, E, Gated>, never, InputStores<Inputs> | R>
|
|
135
135
|
export function live<
|
|
136
136
|
N extends string,
|
|
137
137
|
Inputs extends ReadonlyArray<AnySource>,
|
|
@@ -142,7 +142,7 @@ export function live<
|
|
|
142
142
|
>(
|
|
143
143
|
calc: AsyncCalcClass<N, Inputs, A, E, Gated>,
|
|
144
144
|
config: AsyncCalcLiveWithInvalidateOn<Inputs, A, E, Gated, R>,
|
|
145
|
-
): Layer.Layer<
|
|
145
|
+
): Layer.Layer<AsyncCalcStore<N, A, E, Gated>, never, InputStores<Inputs> | R | Reducers> {
|
|
146
146
|
const revisionState =
|
|
147
147
|
config.invalidateOn === undefined
|
|
148
148
|
? undefined
|
|
@@ -151,18 +151,21 @@ export function live<
|
|
|
151
151
|
const driver = Layer.scoped(
|
|
152
152
|
calc.store,
|
|
153
153
|
Effect.gen(function* () {
|
|
154
|
-
|
|
155
|
-
const cfg = config as AsyncCalcLiveView<Inputs, A, E, R>
|
|
154
|
+
const cfg: AsyncCalcLiveView<Inputs, A, E, R> = config
|
|
156
155
|
|
|
157
156
|
const persistOption = cfg.persist
|
|
158
157
|
const persistKey =
|
|
159
158
|
persistOption === true || persistOption === undefined || persistOption === false
|
|
160
159
|
? calc.manifest.name
|
|
161
|
-
: persistOption.key ?? calc.manifest.name
|
|
160
|
+
: (persistOption.key ?? calc.manifest.name)
|
|
161
|
+
const persistCodec: QueryCodec<A> = {
|
|
162
|
+
encode: Schema.encode(calc.manifest.output),
|
|
163
|
+
decode: Schema.decodeUnknown(calc.manifest.output),
|
|
164
|
+
}
|
|
162
165
|
const persist =
|
|
163
166
|
persistOption === undefined || persistOption === false
|
|
164
167
|
? undefined
|
|
165
|
-
: { key: persistKey,
|
|
168
|
+
: { key: persistKey, codec: persistCodec }
|
|
166
169
|
|
|
167
170
|
const revision = yield* Match.value(revisionState).pipe(
|
|
168
171
|
Match.when(undefined, () => Effect.succeed(undefined)),
|
|
@@ -204,3 +207,15 @@ export function live<
|
|
|
204
207
|
Layer.provide(State.live(revisionState, revisionZero)),
|
|
205
208
|
)
|
|
206
209
|
}
|
|
210
|
+
|
|
211
|
+
export interface NamedCalc {
|
|
212
|
+
readonly name: string
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export const invalidate = (calc: NamedCalc): Effect.Effect<void, never, Queries> =>
|
|
216
|
+
Effect.flatMap(Queries, (queries) =>
|
|
217
|
+
Effect.sync(() => queries.byName.get(calc.name)?.invalidate()),
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
export const refetch = (calc: NamedCalc): Effect.Effect<void, never, Queries> =>
|
|
221
|
+
Effect.flatMap(Queries, (queries) => Effect.sync(() => queries.byName.get(calc.name)?.refetch()))
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import type { Context, Effect, Schema } from 'effect'
|
|
2
|
+
import type { Effect as EffectType } from 'effect/Effect'
|
|
3
|
+
import type { Manifest } from '../definition/definition'
|
|
4
|
+
import type { AnyEvent } from '../event/event'
|
|
5
|
+
import type { InputsObject, InvalidateBy } from '../internal/sources'
|
|
6
|
+
import type { QueryCodec } from '../internal/queryDriver'
|
|
7
|
+
import type { Store } from '../internal/store'
|
|
8
|
+
import type { AnySchema } from '../internal/variance'
|
|
9
|
+
import type { AnySource, Source, SourceCapture } from '../state/token'
|
|
10
|
+
import type { AsyncData } from './asyncData'
|
|
11
|
+
|
|
12
|
+
export interface AsyncCalcSchemaReflection {
|
|
13
|
+
readonly ast: Schema.Schema<unknown, unknown, unknown>['ast']
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface AsyncCalcManifest<N extends string, A = unknown, E = never> extends Manifest {
|
|
17
|
+
readonly kind: 'AsyncCalc'
|
|
18
|
+
readonly name: N
|
|
19
|
+
readonly output: AnySchema<A>
|
|
20
|
+
// 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
|
|
21
|
+
readonly error?: AnySchema<E>
|
|
22
|
+
readonly gated: boolean
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const AsyncCalcStoreTypeId: unique symbol = Symbol.for('reform/AsyncCalcStore')
|
|
26
|
+
export type AsyncCalcStoreTypeId = typeof AsyncCalcStoreTypeId
|
|
27
|
+
|
|
28
|
+
interface AsyncCalcStoreCompatibilityExternalApi<
|
|
29
|
+
N extends string,
|
|
30
|
+
in out A,
|
|
31
|
+
in out E,
|
|
32
|
+
in out Gated extends boolean,
|
|
33
|
+
> extends Store<AsyncData<A, E, Gated>> {
|
|
34
|
+
readonly [AsyncCalcStoreTypeId]?: N
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type AsyncCalcStore<
|
|
38
|
+
N extends string,
|
|
39
|
+
A,
|
|
40
|
+
E,
|
|
41
|
+
Gated extends boolean,
|
|
42
|
+
> = AsyncCalcStoreCompatibilityExternalApi<N, A, E, Gated>
|
|
43
|
+
|
|
44
|
+
export interface AsyncCalcClass<
|
|
45
|
+
N extends string,
|
|
46
|
+
Inputs extends ReadonlyArray<AnySource>,
|
|
47
|
+
A,
|
|
48
|
+
E,
|
|
49
|
+
Gated extends boolean,
|
|
50
|
+
>
|
|
51
|
+
extends
|
|
52
|
+
EffectType<AsyncData<A, E, Gated>, never, AsyncCalcStore<N, A, E, Gated>>,
|
|
53
|
+
Source<N, AsyncData<A, E, Gated>, AsyncCalcStore<N, A, E, Gated>> {
|
|
54
|
+
new (): {}
|
|
55
|
+
readonly manifest: AsyncCalcManifest<N, A, E>
|
|
56
|
+
readonly store: Context.Tag<AsyncCalcStore<N, A, E, Gated>, Store<AsyncData<A, E, Gated>>>
|
|
57
|
+
readonly name: N
|
|
58
|
+
readonly inputs: Inputs
|
|
59
|
+
readonly gated: Gated
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface DefinedAsyncCalcClass<
|
|
63
|
+
N extends string,
|
|
64
|
+
Inputs extends ReadonlyArray<AnySource>,
|
|
65
|
+
A,
|
|
66
|
+
E,
|
|
67
|
+
Gated extends boolean,
|
|
68
|
+
> extends AsyncCalcClass<N, Inputs, A, E, Gated> {
|
|
69
|
+
readonly codec: QueryCodec<A>
|
|
70
|
+
readonly capture: <Result>(visit: SourceCapture<Result>) => Result
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
type ConfigSchema<ValueOrSchema> = [ValueOrSchema] extends [Schema.Schema.AnyNoContext]
|
|
74
|
+
? ValueOrSchema
|
|
75
|
+
: AnySchema<ValueOrSchema>
|
|
76
|
+
|
|
77
|
+
type ConfigErrorSchema<ValueOrSchema> = [ValueOrSchema] extends [undefined]
|
|
78
|
+
? ValueOrSchema
|
|
79
|
+
: ConfigSchema<ValueOrSchema>
|
|
80
|
+
|
|
81
|
+
export interface AsyncCalcConfig<
|
|
82
|
+
Inputs extends ReadonlyArray<AnySource>,
|
|
83
|
+
OutputSchema,
|
|
84
|
+
ErrorSchema,
|
|
85
|
+
AlwaysOn extends boolean,
|
|
86
|
+
> {
|
|
87
|
+
readonly inputs: Inputs
|
|
88
|
+
readonly output: ConfigSchema<OutputSchema>
|
|
89
|
+
// 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
|
|
90
|
+
readonly error?: ConfigErrorSchema<ErrorSchema>
|
|
91
|
+
// 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
|
|
92
|
+
readonly alwaysOn?: AlwaysOn
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface AsyncCalcMakeConfigExternalApi<
|
|
96
|
+
Inputs extends ReadonlyArray<AnySource>,
|
|
97
|
+
OutputSchema extends Schema.Schema.AnyNoContext,
|
|
98
|
+
ErrorSchema extends Schema.Schema.AnyNoContext | undefined,
|
|
99
|
+
AlwaysOn extends boolean,
|
|
100
|
+
> {
|
|
101
|
+
readonly inputs: Inputs
|
|
102
|
+
readonly output: OutputSchema
|
|
103
|
+
readonly error?: ErrorSchema
|
|
104
|
+
readonly alwaysOn?: AlwaysOn
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export type AsyncCalcLive<
|
|
108
|
+
Inputs extends ReadonlyArray<AnySource>,
|
|
109
|
+
A,
|
|
110
|
+
E,
|
|
111
|
+
Gated extends boolean,
|
|
112
|
+
R,
|
|
113
|
+
> = {
|
|
114
|
+
readonly query: (inputs: InputsObject<Inputs>) => Effect.Effect<A, E, R>
|
|
115
|
+
readonly invalidateBy?: InvalidateBy<Inputs>
|
|
116
|
+
// 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
|
|
117
|
+
readonly coalesce?: 'switch' | 'trailing'
|
|
118
|
+
// 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
|
|
119
|
+
readonly reuse?: 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
|
+
readonly persist?:
|
|
122
|
+
| boolean
|
|
123
|
+
| { readonly key?: string | ((inputs: InputsObject<Inputs>) => string) }
|
|
124
|
+
} & (Gated extends true
|
|
125
|
+
? { readonly disabled?: (inputs: InputsObject<Inputs>) => boolean }
|
|
126
|
+
: { readonly disabled?: never })
|
|
127
|
+
|
|
128
|
+
export type AsyncCalcLiveWithInvalidateOn<
|
|
129
|
+
Inputs extends ReadonlyArray<AnySource>,
|
|
130
|
+
A,
|
|
131
|
+
E,
|
|
132
|
+
Gated extends boolean,
|
|
133
|
+
R,
|
|
134
|
+
> = AsyncCalcLive<Inputs, A, E, Gated, R> & {
|
|
135
|
+
readonly invalidateOn?: ReadonlyArray<AnyEvent>
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export interface AsyncCalcLiveView<Inputs extends ReadonlyArray<AnySource>, A, E, R> {
|
|
139
|
+
readonly query: (inputs: InputsObject<Inputs>) => Effect.Effect<A, E, R>
|
|
140
|
+
readonly invalidateBy?: InvalidateBy<Inputs>
|
|
141
|
+
readonly disabled?: (inputs: InputsObject<Inputs>) => boolean
|
|
142
|
+
readonly coalesce?: 'switch' | 'trailing'
|
|
143
|
+
readonly reuse?: boolean
|
|
144
|
+
readonly persist?:
|
|
145
|
+
| boolean
|
|
146
|
+
| { readonly key?: string | ((inputs: InputsObject<Inputs>) => string) }
|
|
147
|
+
}
|
package/src/calc/calc.test.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { expect, it } from '@effect/vitest'
|
|
2
|
-
import {
|
|
2
|
+
import { Effect, Layer, Schema as S } from 'effect'
|
|
3
3
|
import { Calc, State, StateGroup } from '../index'
|
|
4
|
-
|
|
5
|
-
const tick = Effect.sleep(Duration.millis(1))
|
|
4
|
+
import { flush } from '../testkit/flight.testkit'
|
|
6
5
|
|
|
7
6
|
it.live('a source change that does not move the output notifies no subscriber', () => {
|
|
8
7
|
class Count extends State.make('count', S.Number) {}
|
|
@@ -30,12 +29,12 @@ it.live('a source change that does not move the output notifies no subscriber',
|
|
|
30
29
|
expect(derived.get()).toBe(true)
|
|
31
30
|
|
|
32
31
|
source.set(2)
|
|
33
|
-
yield*
|
|
32
|
+
yield* flush()
|
|
34
33
|
expect(notifications.n).toBe(0)
|
|
35
34
|
expect(derived.get()).toBe(true)
|
|
36
35
|
|
|
37
36
|
source.set(-1)
|
|
38
|
-
yield*
|
|
37
|
+
yield* flush()
|
|
39
38
|
expect(notifications.n).toBe(1)
|
|
40
39
|
expect(derived.get()).toBe(false)
|
|
41
40
|
}).pipe(Effect.provide(TestLayer))
|
|
@@ -89,7 +88,7 @@ it.live('a calc can depend on another calc as an input source', () => {
|
|
|
89
88
|
const derived = yield* PlusDoubled.store
|
|
90
89
|
expect(derived.get()).toBe(15)
|
|
91
90
|
source.set(3)
|
|
92
|
-
yield*
|
|
91
|
+
yield* flush()
|
|
93
92
|
expect(derived.get()).toBe(9)
|
|
94
93
|
}).pipe(Effect.provide(TestLayer))
|
|
95
94
|
})
|
|
@@ -125,7 +124,7 @@ it.live('a diamond dependency resolves in one flush with a single leaf notificat
|
|
|
125
124
|
expect(c.get()).toBe(11)
|
|
126
125
|
|
|
127
126
|
source.set(5)
|
|
128
|
-
yield*
|
|
127
|
+
yield* flush()
|
|
129
128
|
expect(c.get()).toBe(21)
|
|
130
129
|
expect(wakes.n).toBe(1)
|
|
131
130
|
}).pipe(Effect.provide(TestLayer))
|
|
@@ -158,12 +157,12 @@ it.live('invalidateBy: a calc recomputes only when the projected key moves', ()
|
|
|
158
157
|
expect(sum.get()).toBe(2)
|
|
159
158
|
|
|
160
159
|
sb.set(100)
|
|
161
|
-
yield*
|
|
160
|
+
yield* flush()
|
|
162
161
|
expect(runs.n).toBe(base)
|
|
163
162
|
expect(sum.get()).toBe(2)
|
|
164
163
|
|
|
165
164
|
sa.set(5)
|
|
166
|
-
yield*
|
|
165
|
+
yield* flush()
|
|
167
166
|
expect(sum.get()).toBe(105)
|
|
168
167
|
}).pipe(Effect.provide(TestLayer))
|
|
169
168
|
})
|
|
@@ -211,11 +210,9 @@ it.live('reuse: an unchanged subtree keeps its identity across recompute', () =>
|
|
|
211
210
|
inputs: [StateGroup.select(Inputs, 'a'), StateGroup.select(Inputs, 'b')],
|
|
212
211
|
output: Pair,
|
|
213
212
|
}) {}
|
|
214
|
-
const SplitLive = Calc.live(
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
{ reuse: true },
|
|
218
|
-
)
|
|
213
|
+
const SplitLive = Calc.live(Split, ({ a, b }) => ({ left: { value: a }, right: { value: b } }), {
|
|
214
|
+
reuse: true,
|
|
215
|
+
})
|
|
219
216
|
const TestLayer = SplitLive.pipe(Layer.provideMerge(StateGroup.live(Inputs, { a: 1, b: 1 })))
|
|
220
217
|
|
|
221
218
|
return Effect.gen(function* () {
|
|
@@ -224,7 +221,7 @@ it.live('reuse: an unchanged subtree keeps its identity across recompute', () =>
|
|
|
224
221
|
const before = derived.get()
|
|
225
222
|
|
|
226
223
|
b.set(2)
|
|
227
|
-
yield*
|
|
224
|
+
yield* flush()
|
|
228
225
|
const after = derived.get()
|
|
229
226
|
expect(after).not.toBe(before)
|
|
230
227
|
expect(after.left).toBe(before.left)
|
|
@@ -256,7 +253,7 @@ it.live('reuse: a value-equal recompute keeps the previous reference and wakes n
|
|
|
256
253
|
const before = derived.get()
|
|
257
254
|
|
|
258
255
|
source.set(2)
|
|
259
|
-
yield*
|
|
256
|
+
yield* flush()
|
|
260
257
|
expect(derived.get()).toBe(before)
|
|
261
258
|
expect(notifications.n).toBe(0)
|
|
262
259
|
}).pipe(Effect.provide(TestLayer))
|
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* () {
|