@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/state/state.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Context, Effect, FiberRef, Layer, Option, Schema } from 'effect'
|
|
1
|
+
import { Context, Effect, FiberRef, Layer, Option, type ParseResult, 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 { resolveScheduler } from '../internal/scheduler'
|
|
@@ -7,7 +7,6 @@ import { resolveInstrumentation, stateUpdateHook } from '../runtime/instrumentat
|
|
|
7
7
|
import { claimStateTag } from '../internal/stateRegistry'
|
|
8
8
|
import { makeStore, type Store } from '../internal/store'
|
|
9
9
|
import { readTracked } from '../internal/track'
|
|
10
|
-
import type { AnySchema, AnyValue } from '../internal/variance'
|
|
11
10
|
|
|
12
11
|
export interface StateOptionsExternalApi {
|
|
13
12
|
readonly title?: string
|
|
@@ -15,34 +14,72 @@ export interface StateOptionsExternalApi {
|
|
|
15
14
|
}
|
|
16
15
|
export type StateOptions = StateOptionsExternalApi
|
|
17
16
|
|
|
18
|
-
|
|
17
|
+
// Full Schema parameters are invariant; heterogeneous manifests expose only their AST.
|
|
18
|
+
export interface StateSchemaReflection {
|
|
19
|
+
readonly ast: Schema.Schema<unknown, unknown, unknown>['ast']
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface StateManifestExternalApi<N extends string> extends Manifest {
|
|
19
23
|
readonly kind: 'State'
|
|
20
24
|
readonly name: N
|
|
21
|
-
readonly schema:
|
|
25
|
+
readonly schema: StateSchemaReflection
|
|
22
26
|
readonly title?: string
|
|
23
27
|
readonly description?: string
|
|
24
28
|
}
|
|
25
|
-
export type StateManifest<N extends string
|
|
29
|
+
export type StateManifest<N extends string> = StateManifestExternalApi<N>
|
|
30
|
+
|
|
31
|
+
export const StateStoreTypeId: unique symbol = Symbol.for('reform/StateStore')
|
|
32
|
+
export type StateStoreTypeId = typeof StateStoreTypeId
|
|
33
|
+
|
|
34
|
+
export interface StateStore<N extends string, in out A> extends Store<A> {
|
|
35
|
+
readonly [StateStoreTypeId]: N
|
|
36
|
+
}
|
|
26
37
|
|
|
27
|
-
export
|
|
38
|
+
export type StateCapture<Result> = <N extends string, A>(state: StateClass<N, A>) => Result
|
|
39
|
+
|
|
40
|
+
export interface AnyState {
|
|
28
41
|
new (): {}
|
|
29
|
-
readonly manifest: StateManifest<
|
|
30
|
-
readonly
|
|
42
|
+
readonly manifest: StateManifest<string>
|
|
43
|
+
readonly capture: <Result>(visit: StateCapture<Result>) => Result
|
|
31
44
|
}
|
|
32
45
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
46
|
+
interface StateStatics<N extends string, A> {
|
|
47
|
+
readonly manifest: StateManifest<N>
|
|
48
|
+
// Subclasses change the class function name; this preserves the authored source name.
|
|
49
|
+
readonly name: N
|
|
50
|
+
readonly capture: <Result>(visit: StateCapture<Result>) => Result
|
|
51
|
+
readonly store: Context.Tag<StateStore<N, A>, Store<A>>
|
|
52
|
+
readonly decodeUnknown: (input: unknown) => Effect.Effect<A, ParseResult.ParseError, never>
|
|
53
|
+
readonly decodeUnknownSync: (input: unknown) => A
|
|
54
|
+
readonly decodeUnknownOption: (input: unknown) => Option.Option<A>
|
|
55
|
+
}
|
|
36
56
|
|
|
37
|
-
export
|
|
57
|
+
export interface StateClass<N extends string, A>
|
|
58
|
+
extends EffectType<A, never, StateStore<N, A>>, StateStatics<N, A> {
|
|
59
|
+
new (): {}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export type StateDefinition<
|
|
63
|
+
N extends string,
|
|
64
|
+
A,
|
|
65
|
+
StateSchema extends Schema.Schema.AnyNoContext,
|
|
66
|
+
> = StateClass<N, A> & {
|
|
67
|
+
readonly manifest: StateManifest<N> & { readonly schema: StateSchema }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export type StateValue<S> = S extends StateClass<infer _N, infer A> ? A : never
|
|
71
|
+
export type StateName<S> = S extends StateClass<infer N, infer _A> ? N : never
|
|
72
|
+
|
|
73
|
+
export const make = <const N extends string, StateSchema extends Schema.Schema.AnyNoContext>(
|
|
38
74
|
name: N,
|
|
39
|
-
schema:
|
|
75
|
+
schema: StateSchema,
|
|
40
76
|
options: StateOptions = {},
|
|
41
|
-
):
|
|
77
|
+
): StateDefinition<N, Schema.Schema.Type<StateSchema>, StateSchema> => {
|
|
78
|
+
type A = Schema.Schema.Type<StateSchema>
|
|
42
79
|
const identifier = `reform/state/${name}`
|
|
43
80
|
claimStateTag(identifier)
|
|
44
|
-
const store = Context.GenericTag<
|
|
45
|
-
const manifest: StateManifest<N
|
|
81
|
+
const store = Context.GenericTag<StateStore<N, A>, Store<A>>(identifier)
|
|
82
|
+
const manifest: StateManifest<N> & { readonly schema: StateSchema } = {
|
|
46
83
|
kind: 'State',
|
|
47
84
|
name,
|
|
48
85
|
schema,
|
|
@@ -50,25 +87,41 @@ export const make = <const N extends string, A>(
|
|
|
50
87
|
...(options.description !== undefined ? { description: options.description } : {}),
|
|
51
88
|
}
|
|
52
89
|
const read = Effect.flatMap(store, readTracked)
|
|
53
|
-
|
|
90
|
+
const state: StateDefinition<N, A, StateSchema> = yieldableClass<
|
|
91
|
+
A,
|
|
92
|
+
never,
|
|
93
|
+
StateStore<N, A>,
|
|
94
|
+
StateStatics<N, A> & {
|
|
95
|
+
readonly manifest: StateManifest<N> & { readonly schema: StateSchema }
|
|
96
|
+
}
|
|
97
|
+
>(read, {
|
|
98
|
+
manifest,
|
|
99
|
+
name,
|
|
100
|
+
store,
|
|
101
|
+
decodeUnknown: Schema.decodeUnknown(schema),
|
|
102
|
+
decodeUnknownSync: Schema.decodeUnknownSync(schema),
|
|
103
|
+
decodeUnknownOption: Schema.decodeUnknownOption(schema),
|
|
104
|
+
capture: <Result>(visit: StateCapture<Result>): Result => visit(state),
|
|
105
|
+
})
|
|
106
|
+
return state
|
|
54
107
|
}
|
|
55
108
|
|
|
56
|
-
const resolveSeed = <
|
|
57
|
-
state:
|
|
58
|
-
initial:
|
|
59
|
-
): Effect.Effect<
|
|
109
|
+
const resolveSeed = <N extends string, A>(
|
|
110
|
+
state: StateClass<N, A>,
|
|
111
|
+
initial: A,
|
|
112
|
+
): Effect.Effect<A, never, never> =>
|
|
60
113
|
Effect.map(FiberRef.get(CurrentSeedOverrides), (overrides) => {
|
|
61
114
|
if (!(state.manifest.name in overrides)) {
|
|
62
115
|
return initial
|
|
63
116
|
}
|
|
64
|
-
const override =
|
|
117
|
+
const override = state.decodeUnknownOption(overrides[state.manifest.name])
|
|
65
118
|
return Option.getOrElse(override, () => initial)
|
|
66
119
|
})
|
|
67
120
|
|
|
68
|
-
export const live = <
|
|
69
|
-
state:
|
|
70
|
-
initial:
|
|
71
|
-
): Layer.Layer<
|
|
121
|
+
export const live = <N extends string, A>(
|
|
122
|
+
state: StateClass<N, A>,
|
|
123
|
+
initial: A,
|
|
124
|
+
): Layer.Layer<StateStore<N, A>> =>
|
|
72
125
|
Layer.effect(
|
|
73
126
|
state.store,
|
|
74
127
|
Effect.all([resolveScheduler, resolveSeed(state, initial), resolveInstrumentation]).pipe(
|
|
@@ -1,8 +1,26 @@
|
|
|
1
1
|
import { expect, it } from '@effect/vitest'
|
|
2
|
-
import {
|
|
2
|
+
import { Effect, Layer, Schema as S } from 'effect'
|
|
3
3
|
import { Engine, Event, publish, Reducer, StateFamily } from '../index'
|
|
4
|
+
import { until } from '../testkit/flight.testkit'
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
// Eviction is queued with queueMicrotask; one turn of the queue provably drains it.
|
|
7
|
+
const drainMicrotasks = Effect.promise(
|
|
8
|
+
() => new Promise<void>((resolve) => queueMicrotask(resolve)),
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
it.live('same-shaped families resolve their own stores', () => {
|
|
12
|
+
class First extends StateFamily.make('family.nominal.first', S.String, S.Number) {}
|
|
13
|
+
class Second extends StateFamily.make('family.nominal.second', S.String, S.Number) {}
|
|
14
|
+
const Live = Layer.mergeAll(StateFamily.live(First, 1), StateFamily.live(Second, 2))
|
|
15
|
+
|
|
16
|
+
return Effect.gen(function* () {
|
|
17
|
+
const first = yield* First.store
|
|
18
|
+
const second = yield* Second.store
|
|
19
|
+
|
|
20
|
+
expect(first.at('key').get()).toBe(1)
|
|
21
|
+
expect(second.at('key').get()).toBe(2)
|
|
22
|
+
}).pipe(Effect.provide(Live))
|
|
23
|
+
})
|
|
6
24
|
|
|
7
25
|
it.live("writing one key does not notify another key's subscribers", () => {
|
|
8
26
|
class Items extends StateFamily.make('items', S.String, S.Number) {}
|
|
@@ -22,7 +40,10 @@ it.live("writing one key does not notify another key's subscribers", () => {
|
|
|
22
40
|
})
|
|
23
41
|
|
|
24
42
|
a.set(1)
|
|
25
|
-
yield*
|
|
43
|
+
yield* until(
|
|
44
|
+
() => aWakes.n,
|
|
45
|
+
(n) => n > 0,
|
|
46
|
+
)
|
|
26
47
|
expect(aWakes.n).toBe(1)
|
|
27
48
|
expect(bWakes.n).toBe(0)
|
|
28
49
|
expect(b.get()).toBe(0)
|
|
@@ -67,12 +88,16 @@ it.live('evictWhenUnused drops a key once its last subscriber leaves', () => {
|
|
|
67
88
|
const offB = store.subscribe(() => {})
|
|
68
89
|
expect(family.size()).toBe(1)
|
|
69
90
|
|
|
91
|
+
// Releasing one of two subscribers queues no eviction at all — assert with no window.
|
|
70
92
|
offA()
|
|
71
|
-
yield*
|
|
93
|
+
yield* drainMicrotasks
|
|
72
94
|
expect(family.size()).toBe(1)
|
|
73
95
|
|
|
74
96
|
offB()
|
|
75
|
-
yield*
|
|
97
|
+
yield* until(
|
|
98
|
+
() => family.size(),
|
|
99
|
+
(size) => size === 0,
|
|
100
|
+
)
|
|
76
101
|
expect(family.size()).toBe(0)
|
|
77
102
|
expect(family.at('k').get()).toBe(0)
|
|
78
103
|
}).pipe(Effect.provide(TestLayer))
|
|
@@ -89,7 +114,7 @@ it.live('evictWhenUnused: a same-tick re-subscribe cancels eviction', () => {
|
|
|
89
114
|
const off = store.subscribe(() => {})
|
|
90
115
|
off()
|
|
91
116
|
store.subscribe(() => {})
|
|
92
|
-
yield*
|
|
117
|
+
yield* drainMicrotasks
|
|
93
118
|
expect(family.size()).toBe(1)
|
|
94
119
|
expect(family.at('k').get()).toBe(7)
|
|
95
120
|
}).pipe(Effect.provide(TestLayer))
|
|
@@ -116,12 +141,18 @@ it.live('a family reducer fold returning Tombstone evicts the key', () => {
|
|
|
116
141
|
const family = yield* Items.store
|
|
117
142
|
|
|
118
143
|
yield* publish('High', Event.construct(Bumped, { id: 'a' }))
|
|
119
|
-
yield*
|
|
144
|
+
yield* until(
|
|
145
|
+
() => family.at('a').get(),
|
|
146
|
+
(value) => value === 1,
|
|
147
|
+
)
|
|
120
148
|
const before = family.at('a')
|
|
121
149
|
expect(before.get()).toBe(1)
|
|
122
150
|
|
|
123
151
|
yield* publish('High', Event.construct(Removed, { id: 'a' }))
|
|
124
|
-
yield*
|
|
152
|
+
yield* until(
|
|
153
|
+
() => family.at('a'),
|
|
154
|
+
(store) => store !== before,
|
|
155
|
+
)
|
|
125
156
|
const after = family.at('a')
|
|
126
157
|
expect(after).not.toBe(before)
|
|
127
158
|
expect(after.get()).toBe(0)
|
package/src/state/stateFamily.ts
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import { Context, Effect, Layer, Option } from 'effect'
|
|
1
|
+
import { Context, Effect, Layer, Option, Schema } from 'effect'
|
|
2
2
|
import { type Manifest, definitionClass } from '../definition/definition'
|
|
3
3
|
import { resolveScheduler, type Scheduler } from '../internal/scheduler'
|
|
4
4
|
import { claimStateTag } from '../internal/stateRegistry'
|
|
5
5
|
import { resolveInstrumentation, stateUpdateHook } from '../runtime/instrumentation'
|
|
6
6
|
import { makeStore, type Store } from '../internal/store'
|
|
7
7
|
import { readTracked } from '../internal/track'
|
|
8
|
-
import type { AnySchema, AnyValue } from '../internal/variance'
|
|
9
8
|
|
|
10
9
|
export const Tombstone: unique symbol = Symbol.for('reform/StateFamily/Tombstone')
|
|
11
10
|
export type Tombstone = typeof Tombstone
|
|
@@ -84,41 +83,66 @@ const makeFamilyStore = <K, V>(
|
|
|
84
83
|
}
|
|
85
84
|
}
|
|
86
85
|
|
|
87
|
-
|
|
86
|
+
// Full Schema parameters are invariant; heterogeneous manifests expose only their AST.
|
|
87
|
+
export interface FamilySchemaReflection {
|
|
88
|
+
readonly ast: Schema.Schema<unknown, unknown, unknown>['ast']
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface StateFamilyManifestExternalApi<N extends string> extends Manifest {
|
|
88
92
|
readonly kind: 'StateFamily'
|
|
89
93
|
readonly name: N
|
|
90
|
-
readonly key:
|
|
91
|
-
readonly value:
|
|
94
|
+
readonly key: FamilySchemaReflection
|
|
95
|
+
readonly value: FamilySchemaReflection
|
|
92
96
|
readonly title?: string
|
|
93
97
|
readonly description?: string
|
|
94
98
|
}
|
|
95
|
-
export type StateFamilyManifest<N extends string
|
|
99
|
+
export type StateFamilyManifest<N extends string> = StateFamilyManifestExternalApi<N>
|
|
100
|
+
|
|
101
|
+
export const StateFamilyStoreTypeId: unique symbol = Symbol.for('reform/StateFamilyStore')
|
|
102
|
+
export type StateFamilyStoreTypeId = typeof StateFamilyStoreTypeId
|
|
103
|
+
|
|
104
|
+
export interface StateFamilyStore<N extends string, in out K, in out V> extends FamilyStore<K, V> {
|
|
105
|
+
readonly [StateFamilyStoreTypeId]: N
|
|
106
|
+
}
|
|
96
107
|
|
|
97
|
-
export
|
|
108
|
+
export type StateFamilyCapture<Result> = <N extends string, K, V>(
|
|
109
|
+
family: StateFamilyClass<N, K, V>,
|
|
110
|
+
) => Result
|
|
111
|
+
|
|
112
|
+
export interface AnyFamily {
|
|
113
|
+
new (): {}
|
|
114
|
+
readonly manifest: StateFamilyManifest<string>
|
|
115
|
+
readonly capture: <Result>(visit: StateFamilyCapture<Result>) => Result
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export interface StateFamilyClass<N extends string, K, V> {
|
|
98
119
|
new (): { readonly Key: K }
|
|
99
|
-
readonly manifest: StateFamilyManifest<N
|
|
100
|
-
readonly
|
|
120
|
+
readonly manifest: StateFamilyManifest<N>
|
|
121
|
+
readonly capture: <Result>(visit: StateFamilyCapture<Result>) => Result
|
|
122
|
+
readonly store: Context.Tag<StateFamilyStore<N, K, V>, FamilyStore<K, V>>
|
|
123
|
+
readonly decodeKeySync: (input: unknown) => K
|
|
124
|
+
readonly decodeValueSync: (input: unknown) => V
|
|
101
125
|
}
|
|
102
126
|
|
|
103
|
-
export type
|
|
104
|
-
export type FamilyKey<F> = F extends StateFamilyClass<
|
|
105
|
-
export type FamilyValue<F> = F extends StateFamilyClass<
|
|
127
|
+
export type FamilyName<F> = F extends StateFamilyClass<infer N, infer _K, infer _V> ? N : never
|
|
128
|
+
export type FamilyKey<F> = F extends StateFamilyClass<infer _N, infer K, infer _V> ? K : never
|
|
129
|
+
export type FamilyValue<F> = F extends StateFamilyClass<infer _N, infer _K, infer V> ? V : never
|
|
106
130
|
|
|
107
131
|
export interface StateFamilyMakeOptionsExternalApi {
|
|
108
132
|
readonly title?: string
|
|
109
133
|
readonly description?: string
|
|
110
134
|
}
|
|
111
135
|
|
|
112
|
-
export const make = <const N extends string, K, V>(
|
|
136
|
+
export const make = <const N extends string, K, KeyEncoded, V, ValueEncoded>(
|
|
113
137
|
name: N,
|
|
114
|
-
key:
|
|
115
|
-
valueSchema:
|
|
138
|
+
key: Schema.Schema<K, KeyEncoded, never>,
|
|
139
|
+
valueSchema: Schema.Schema<V, ValueEncoded, never>,
|
|
116
140
|
options: StateFamilyMakeOptionsExternalApi = {},
|
|
117
141
|
): StateFamilyClass<N, K, V> => {
|
|
118
142
|
const identifier = `reform/family/${name}`
|
|
119
143
|
claimStateTag(identifier)
|
|
120
|
-
const store = Context.GenericTag<
|
|
121
|
-
const manifest: StateFamilyManifest<N
|
|
144
|
+
const store = Context.GenericTag<StateFamilyStore<N, K, V>, FamilyStore<K, V>>(identifier)
|
|
145
|
+
const manifest: StateFamilyManifest<N> = {
|
|
122
146
|
kind: 'StateFamily',
|
|
123
147
|
name,
|
|
124
148
|
key,
|
|
@@ -126,22 +150,31 @@ export const make = <const N extends string, K, V>(
|
|
|
126
150
|
...(options.title !== undefined ? { title: options.title } : {}),
|
|
127
151
|
...(options.description !== undefined ? { description: options.description } : {}),
|
|
128
152
|
}
|
|
129
|
-
|
|
153
|
+
const family: StateFamilyClass<N, K, V> = definitionClass<StateFamilyClass<N, K, V>>({
|
|
154
|
+
manifest,
|
|
155
|
+
store,
|
|
156
|
+
decodeKeySync: Schema.decodeUnknownSync(key),
|
|
157
|
+
decodeValueSync: Schema.decodeUnknownSync(valueSchema),
|
|
158
|
+
capture: <Result>(visit: StateFamilyCapture<Result>): Result => visit(family),
|
|
159
|
+
})
|
|
160
|
+
return family
|
|
130
161
|
}
|
|
131
162
|
|
|
132
163
|
export const read = <N extends string, K, V>(
|
|
133
164
|
family: StateFamilyClass<N, K, V>,
|
|
134
165
|
key: K,
|
|
135
|
-
): Effect.Effect<V, never,
|
|
166
|
+
): Effect.Effect<V, never, StateFamilyStore<N, K, V>> =>
|
|
136
167
|
Effect.flatMap(family.store, (familyStore) => readTracked(familyStore.at(key)))
|
|
137
168
|
|
|
138
169
|
export const live = <N extends string, K, V>(
|
|
139
170
|
family: StateFamilyClass<N, K, V>,
|
|
140
171
|
initial: V | ((key: K) => V),
|
|
141
172
|
options: FamilyOptions = {},
|
|
142
|
-
): Layer.Layer<
|
|
143
|
-
|
|
144
|
-
|
|
173
|
+
): Layer.Layer<StateFamilyStore<N, K, V>> => {
|
|
174
|
+
const seed = (key: K): V =>
|
|
175
|
+
typeof initial === 'function'
|
|
176
|
+
? family.decodeValueSync(Reflect.apply(initial, undefined, [key]))
|
|
177
|
+
: initial
|
|
145
178
|
return Layer.effect(
|
|
146
179
|
family.store,
|
|
147
180
|
Effect.zipWith(resolveScheduler, resolveInstrumentation, (scheduler, instrumentation) =>
|
package/src/state/stateGroup.ts
CHANGED
|
@@ -1,13 +1,25 @@
|
|
|
1
|
-
import { Array as Arr, Layer, Option } from 'effect'
|
|
1
|
+
import { Array as Arr, Context, Effect, Layer, Option } from 'effect'
|
|
2
2
|
import { definitionClass } from '../definition/definition'
|
|
3
3
|
import { DuplicateRegistration, UnknownGroupState } from '../internal/errors'
|
|
4
4
|
import { type StoresOf } from '../internal/sources'
|
|
5
|
-
import
|
|
5
|
+
import type { Store } from '../internal/store'
|
|
6
|
+
import {
|
|
7
|
+
type AnyState,
|
|
8
|
+
live as stateLive,
|
|
9
|
+
type StateName,
|
|
10
|
+
type StateStore,
|
|
11
|
+
type StateValue,
|
|
12
|
+
} from './state'
|
|
6
13
|
import { StateToken } from './token'
|
|
7
|
-
import type { AnyValue } from '../internal/variance'
|
|
8
14
|
|
|
9
|
-
type
|
|
10
|
-
|
|
15
|
+
type MemberForName<Members extends ReadonlyArray<AnyState>, N extends string> = Extract<
|
|
16
|
+
Members[number],
|
|
17
|
+
{ readonly manifest: { readonly name: N } }
|
|
18
|
+
>
|
|
19
|
+
|
|
20
|
+
type ValueForName<Members extends ReadonlyArray<AnyState>, N extends string> = StateValue<
|
|
21
|
+
MemberForName<Members, N>
|
|
22
|
+
>
|
|
11
23
|
|
|
12
24
|
export interface StateGroupClass<Members extends ReadonlyArray<AnyState>> {
|
|
13
25
|
new (): {}
|
|
@@ -23,7 +35,10 @@ type DuplicateStateName<N extends string> = readonly ['reform: duplicate state n
|
|
|
23
35
|
type NoDuplicateNames<
|
|
24
36
|
Members extends ReadonlyArray<AnyState>,
|
|
25
37
|
Seen extends string = never,
|
|
26
|
-
> = Members extends readonly [
|
|
38
|
+
> = Members extends readonly [
|
|
39
|
+
infer Head extends AnyState,
|
|
40
|
+
...infer Tail extends ReadonlyArray<AnyState>,
|
|
41
|
+
]
|
|
27
42
|
? readonly [
|
|
28
43
|
StateName<Head> extends Seen ? DuplicateStateName<StateName<Head>> : Head,
|
|
29
44
|
...NoDuplicateNames<Tail, Seen | StateName<Head>>,
|
|
@@ -43,7 +58,9 @@ type UnionToIntersection<U> = (U extends unknown ? (k: U) => void : never) exten
|
|
|
43
58
|
|
|
44
59
|
type EntrySeeds<G> = G extends AnyStateGroup ? GroupSeeds<G> : never
|
|
45
60
|
|
|
46
|
-
export type SeedsOf<St extends ReadonlyArray<unknown>> = Partial<
|
|
61
|
+
export type SeedsOf<St extends ReadonlyArray<unknown>> = Partial<
|
|
62
|
+
UnionToIntersection<EntrySeeds<St[number]>>
|
|
63
|
+
>
|
|
47
64
|
|
|
48
65
|
export const make = <const Members extends ReadonlyArray<AnyState>>(
|
|
49
66
|
...members: Members & NoDuplicateNames<Members>
|
|
@@ -66,7 +83,10 @@ export const make = <const Members extends ReadonlyArray<AnyState>>(
|
|
|
66
83
|
})
|
|
67
84
|
}
|
|
68
85
|
|
|
69
|
-
export const select = <
|
|
86
|
+
export const select = <
|
|
87
|
+
Members extends ReadonlyArray<AnyState>,
|
|
88
|
+
N extends StateName<Members[number]>,
|
|
89
|
+
>(
|
|
70
90
|
group: StateGroupClass<Members>,
|
|
71
91
|
name: N,
|
|
72
92
|
): StateToken<N, ValueForName<Members, N>> => {
|
|
@@ -75,17 +95,33 @@ export const select = <Members extends ReadonlyArray<AnyState>, N extends StateN
|
|
|
75
95
|
// oxlint-disable-next-line reform-rules/no-throw -- definition-time invariant; sync lookup has no Effect context (see internal/errors.ts)
|
|
76
96
|
throw new UnknownGroupState({ name })
|
|
77
97
|
}
|
|
78
|
-
|
|
98
|
+
const store = Context.GenericTag<
|
|
99
|
+
StateStore<N, ValueForName<Members, N>>,
|
|
100
|
+
Store<ValueForName<Members, N>>
|
|
101
|
+
>(`reform/state/${name}`)
|
|
102
|
+
return new StateToken(name, store)
|
|
79
103
|
}
|
|
80
104
|
|
|
81
|
-
export
|
|
105
|
+
export function live<Members extends ReadonlyArray<AnyState>>(
|
|
82
106
|
group: StateGroupClass<Members>,
|
|
83
107
|
seeds: GroupSeeds<StateGroupClass<Members>>,
|
|
84
|
-
): Layer.Layer<StoresOf<Members>>
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
.
|
|
108
|
+
): Layer.Layer<StoresOf<Members>>
|
|
109
|
+
export function live(
|
|
110
|
+
group: AnyStateGroup,
|
|
111
|
+
seeds: Readonly<Record<string, unknown>>,
|
|
112
|
+
): Layer.Layer<never> {
|
|
113
|
+
const layers = group.members.map((member) =>
|
|
114
|
+
member.capture<Layer.Layer<never>>((state) =>
|
|
115
|
+
Layer.unwrapEffect(
|
|
116
|
+
state.decodeUnknown(seeds[state.manifest.name]).pipe(
|
|
117
|
+
Effect.orDie,
|
|
118
|
+
Effect.map((seed) => stateLive(state, seed)),
|
|
119
|
+
),
|
|
120
|
+
),
|
|
121
|
+
),
|
|
122
|
+
)
|
|
123
|
+
return layers.reduce<Layer.Layer<never>>(
|
|
124
|
+
(merged, memberLayer) => Layer.merge(merged, memberLayer),
|
|
125
|
+
Layer.empty,
|
|
126
|
+
)
|
|
91
127
|
}
|
package/src/state/token.ts
CHANGED
|
@@ -1,26 +1,56 @@
|
|
|
1
1
|
import { Context, Effect, Effectable } from 'effect'
|
|
2
2
|
import type { Store } from '../internal/store'
|
|
3
|
-
import type { AnyValue } from '../internal/variance'
|
|
4
3
|
import { readTracked } from '../internal/track'
|
|
4
|
+
import type { StateStore } from './state'
|
|
5
5
|
|
|
6
|
-
export class StateToken<
|
|
6
|
+
export class StateToken<
|
|
7
|
+
N extends string,
|
|
8
|
+
in out A,
|
|
9
|
+
Identifier = StateStore<N, A>,
|
|
10
|
+
> extends Effectable.Class<A, never, Identifier> {
|
|
7
11
|
readonly name: N
|
|
8
|
-
readonly store: Context.Tag<
|
|
9
|
-
constructor(name: N, store: Context.Tag<
|
|
12
|
+
readonly store: Context.Tag<Identifier, Store<A>>
|
|
13
|
+
constructor(name: N, store: Context.Tag<Identifier, Store<A>>) {
|
|
10
14
|
super()
|
|
11
15
|
this.name = name
|
|
12
16
|
this.store = store
|
|
13
17
|
}
|
|
14
|
-
commit(): Effect.Effect<A, never,
|
|
18
|
+
commit(): Effect.Effect<A, never, Identifier> {
|
|
15
19
|
return Effect.flatMap(this.store, readTracked)
|
|
16
20
|
}
|
|
21
|
+
capture<Result>(visit: SourceCapture<Result>): Result {
|
|
22
|
+
return visit(this)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type SourceCapture<Result> = <N extends string, A, Identifier>(
|
|
27
|
+
source: Source<N, A, Identifier>,
|
|
28
|
+
) => Result
|
|
29
|
+
|
|
30
|
+
export interface AnySource {
|
|
31
|
+
readonly name: string
|
|
32
|
+
readonly store: { readonly key: string }
|
|
17
33
|
}
|
|
18
34
|
|
|
19
|
-
export interface Source<
|
|
35
|
+
export interface Source<N extends string, A, Identifier> extends AnySource {
|
|
20
36
|
readonly name: N
|
|
21
|
-
readonly store: Context.Tag<
|
|
37
|
+
readonly store: Context.Tag<Identifier, Store<A>>
|
|
22
38
|
}
|
|
23
39
|
|
|
24
|
-
export type
|
|
25
|
-
|
|
26
|
-
|
|
40
|
+
export type SourceName<S> = S extends {
|
|
41
|
+
readonly manifest: { readonly name: infer N extends string }
|
|
42
|
+
}
|
|
43
|
+
? N
|
|
44
|
+
: S extends { readonly name: infer N extends string }
|
|
45
|
+
? N
|
|
46
|
+
: never
|
|
47
|
+
export type SourceValue<S> = S extends {
|
|
48
|
+
readonly store: Context.Tag<infer _Identifier, Store<infer A>>
|
|
49
|
+
}
|
|
50
|
+
? A
|
|
51
|
+
: never
|
|
52
|
+
export type SourceIdentifier<S> = S extends {
|
|
53
|
+
readonly store: Context.Tag<infer Identifier, infer _Service>
|
|
54
|
+
}
|
|
55
|
+
? Identifier
|
|
56
|
+
: never
|
|
@@ -1,49 +1,66 @@
|
|
|
1
|
-
import { Context, Effect, Layer, Option, type Scope } from 'effect'
|
|
1
|
+
import { Context, Effect, Layer, Option, type Schema, type Scope } from 'effect'
|
|
2
2
|
import type { Effect as EffectType } from 'effect/Effect'
|
|
3
3
|
import { type Manifest, yieldableClass } from '../definition/definition'
|
|
4
4
|
import { resolveScheduler } from '../internal/scheduler'
|
|
5
5
|
import { makeDerivedStore, type Store } from '../internal/store'
|
|
6
6
|
import { readTracked } from '../internal/track'
|
|
7
|
-
import type { Source } from '../state/token'
|
|
7
|
+
import type { AnySource, Source, SourceCapture } from '../state/token'
|
|
8
8
|
import type { StateOptions } from '../state/state'
|
|
9
|
-
import type { AnySchema, AnyValue } from '../internal/variance'
|
|
10
9
|
|
|
11
10
|
export interface SyncedSource<A> {
|
|
12
11
|
readonly snapshot: () => A
|
|
13
12
|
readonly subscribe: (onChange: () => void) => () => void
|
|
14
13
|
}
|
|
15
14
|
|
|
16
|
-
export interface
|
|
15
|
+
export interface SyncedStoreSchemaReflection {
|
|
16
|
+
readonly ast: Schema.Schema<unknown, unknown, unknown>['ast']
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface SyncedStoreManifest<N extends string> extends Manifest {
|
|
17
20
|
readonly kind: 'SyncedStore'
|
|
18
21
|
readonly name: N
|
|
19
|
-
readonly schema:
|
|
22
|
+
readonly schema: SyncedStoreSchemaReflection
|
|
20
23
|
readonly title: Option.Option<string>
|
|
21
24
|
readonly description: Option.Option<string>
|
|
22
25
|
}
|
|
23
26
|
|
|
24
|
-
export
|
|
25
|
-
|
|
26
|
-
|
|
27
|
+
export const SyncedStoreIdentifierTypeId: unique symbol = Symbol.for('reform/SyncedStoreIdentifier')
|
|
28
|
+
export type SyncedStoreIdentifierTypeId = typeof SyncedStoreIdentifierTypeId
|
|
29
|
+
|
|
30
|
+
export interface SyncedStoreIdentifier<N extends string, in out A> extends Store<A> {
|
|
31
|
+
readonly [SyncedStoreIdentifierTypeId]: N
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface SyncedStoreClass<N extends string, in out A>
|
|
35
|
+
extends
|
|
36
|
+
EffectType<A, never, SyncedStoreIdentifier<N, A>>,
|
|
37
|
+
Source<N, A, SyncedStoreIdentifier<N, A>> {
|
|
27
38
|
new (): {}
|
|
28
|
-
readonly manifest: SyncedStoreManifest<N
|
|
29
|
-
readonly store: Context.Tag<
|
|
39
|
+
readonly manifest: SyncedStoreManifest<N>
|
|
40
|
+
readonly store: Context.Tag<SyncedStoreIdentifier<N, A>, Store<A>>
|
|
30
41
|
readonly name: N
|
|
42
|
+
readonly capture: <Result>(visit: SourceCapture<Result>) => Result
|
|
31
43
|
}
|
|
32
44
|
|
|
33
|
-
export interface StoreCarrier<A> {
|
|
34
|
-
readonly store: Context.Tag<
|
|
45
|
+
export interface StoreCarrier<Identifier, A> {
|
|
46
|
+
readonly store: Context.Tag<Identifier, Store<A>>
|
|
35
47
|
}
|
|
36
48
|
|
|
37
|
-
export type AnySyncedStore =
|
|
38
|
-
|
|
49
|
+
export type AnySyncedStore = AnySource & {
|
|
50
|
+
readonly manifest: Manifest & { readonly kind: 'SyncedStore' }
|
|
51
|
+
}
|
|
52
|
+
export type SyncedValue<S> = S extends SyncedStoreClass<infer _N, infer A> ? A : never
|
|
39
53
|
|
|
40
|
-
export const make = <const N extends string,
|
|
54
|
+
export const make = <const N extends string, ValueSchema extends Schema.Schema.AnyNoContext>(
|
|
41
55
|
name: N,
|
|
42
|
-
schema:
|
|
56
|
+
schema: ValueSchema,
|
|
43
57
|
options: StateOptions = {},
|
|
44
|
-
): SyncedStoreClass<N,
|
|
45
|
-
|
|
46
|
-
const
|
|
58
|
+
): SyncedStoreClass<N, Schema.Schema.Type<ValueSchema>> => {
|
|
59
|
+
type A = Schema.Schema.Type<ValueSchema>
|
|
60
|
+
const store = Context.GenericTag<SyncedStoreIdentifier<N, A>, Store<A>>(
|
|
61
|
+
`reform/syncedStore/${name}`,
|
|
62
|
+
)
|
|
63
|
+
const manifest: SyncedStoreManifest<N> = {
|
|
47
64
|
kind: 'SyncedStore',
|
|
48
65
|
name,
|
|
49
66
|
schema,
|
|
@@ -51,13 +68,19 @@ export const make = <const N extends string, A>(
|
|
|
51
68
|
description: Option.fromNullable(options.description),
|
|
52
69
|
}
|
|
53
70
|
const read = Effect.flatMap(store, readTracked)
|
|
54
|
-
|
|
71
|
+
const synced: SyncedStoreClass<N, A> = yieldableClass(read, {
|
|
72
|
+
manifest,
|
|
73
|
+
store,
|
|
74
|
+
name,
|
|
75
|
+
capture: <Result>(visit: SourceCapture<Result>): Result => visit(synced),
|
|
76
|
+
})
|
|
77
|
+
return synced
|
|
55
78
|
}
|
|
56
79
|
|
|
57
|
-
export const live = <A, R>(
|
|
58
|
-
def: StoreCarrier<A>,
|
|
80
|
+
export const live = <Identifier, A, R>(
|
|
81
|
+
def: StoreCarrier<Identifier, A>,
|
|
59
82
|
acquire: Effect.Effect<SyncedSource<A>, never, R>,
|
|
60
|
-
): Layer.Layer<
|
|
83
|
+
): Layer.Layer<Identifier, never, Exclude<R, Scope.Scope>> =>
|
|
61
84
|
Layer.scoped(
|
|
62
85
|
def.store,
|
|
63
86
|
Effect.gen(function* () {
|