@playfast/reform 1.0.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +103 -56
- package/package.json +17 -17
- package/src/boundary/boundary.test.ts +63 -49
- package/src/boundary/boundary.ts +144 -113
- package/src/calc/asyncCalc.invalidate.test.ts +518 -32
- package/src/calc/asyncCalc.test.ts +207 -213
- package/src/calc/asyncCalc.ts +131 -154
- package/src/calc/asyncData.ts +0 -37
- package/src/calc/calc.test.ts +7 -33
- package/src/calc/calc.ts +44 -58
- package/src/calc/calcFamily.test.ts +80 -34
- package/src/calc/calcFamily.ts +54 -65
- package/src/calc/compose.test.ts +1 -12
- package/src/calc/compose.ts +1 -36
- package/src/calc/queryState.ts +0 -23
- package/src/channel/channel.ts +126 -111
- package/src/compose/composition.test.ts +19 -0
- package/src/compose/composition.ts +351 -127
- package/src/compose/host.ts +0 -6
- package/src/compose/props.ts +13 -12
- package/src/compose/provide.ts +189 -62
- package/src/compose/slot.ts +158 -49
- package/src/compose/structure.test.ts +6 -9
- package/src/compose/structure.ts +108 -79
- package/src/compose/ui.test.ts +19 -7
- package/src/compose/ui.ts +155 -156
- package/src/compose/ui.typecheck.ts +40 -18
- package/src/definition/definition.ts +22 -41
- package/src/event/event.fromSource.test.ts +172 -0
- package/src/event/event.test.ts +10 -3
- package/src/event/event.ts +102 -27
- package/src/event/eventGroup.ts +0 -1
- package/src/feature/feature.mount.test.ts +122 -43
- package/src/feature/feature.test.ts +46 -21
- package/src/feature/feature.ts +1347 -256
- package/src/feature/feature.typecheck.ts +273 -68
- package/src/graph/closure.ts +403 -0
- package/src/index.ts +68 -126
- package/src/internal/bucketCache.ts +39 -0
- package/src/internal/capture.ts +9 -24
- package/src/internal/env.ts +7 -0
- package/src/internal/errors.test.ts +0 -6
- package/src/internal/errors.ts +37 -60
- package/src/internal/inspect.test.ts +0 -6
- package/src/internal/inspect.ts +0 -12
- package/src/internal/queryDriver.ts +105 -201
- package/src/internal/queryDriverStore.ts +156 -0
- package/src/internal/queryDriverTypes.ts +59 -0
- package/src/internal/queryEvents.ts +0 -12
- package/src/internal/queryStore.ts +0 -14
- package/src/internal/reuse.test.ts +10 -14
- package/src/internal/reuse.ts +5 -30
- package/src/internal/scheduler.ts +4 -44
- package/src/internal/seeds.ts +0 -14
- package/src/internal/sources.ts +26 -49
- package/src/internal/stateRegistry.ts +0 -14
- package/src/internal/store.test.ts +1 -3
- package/src/internal/store.ts +0 -37
- package/src/internal/track.ts +3 -20
- package/src/internal/variance.ts +5 -0
- package/src/internal.ts +222 -0
- package/src/namespace/namespace.test.ts +46 -0
- package/src/namespace/namespace.ts +106 -0
- package/src/procedure/procedure.ts +40 -35
- package/src/reducer/reducer.ts +78 -52
- package/src/remote/remoteState.test.ts +590 -397
- package/src/remote/remoteState.ts +425 -347
- package/src/remote/remoteState.typecheck.ts +47 -56
- package/src/runtime/appRuntime.activation.test.ts +100 -0
- package/src/runtime/appRuntime.test.ts +249 -0
- package/src/runtime/appRuntime.ts +415 -97
- package/src/runtime/bus.ts +2 -15
- package/src/runtime/eventBudget.test.ts +152 -0
- package/src/runtime/eventBudget.ts +120 -0
- package/src/runtime/hardening.test.ts +73 -13
- package/src/runtime/instrumentation.test.ts +55 -52
- package/src/runtime/instrumentation.ts +6 -60
- package/src/runtime/loop.test.ts +36 -17
- package/src/runtime/loop.ts +87 -88
- package/src/runtime/queries.ts +0 -17
- package/src/scene/featureScene.test.ts +61 -0
- package/src/scene/scene.ts +247 -104
- package/src/scene/seedScene.test.ts +42 -79
- package/src/state/state.nominal.typecheck.ts +68 -0
- package/src/state/state.test.ts +17 -0
- package/src/state/state.ts +80 -46
- package/src/state/stateFamily.test.ts +16 -11
- package/src/state/stateFamily.ts +55 -65
- package/src/state/stateGroup.ts +53 -64
- package/src/state/token.ts +40 -23
- package/src/synced/syncedStore.ts +46 -58
- package/src/testkit/flight.testkit.ts +75 -0
- package/src/ui/node.ts +0 -6
- package/src/ui/trigger.ts +0 -5
- package/src/wire/tree.test.ts +8 -7
- package/src/wire/tree.ts +0 -39
- package/src/wire/triggers.test.ts +6 -6
- package/src/wire/triggers.ts +14 -32
- package/src/internal/ctx.ts +0 -16
package/src/reducer/reducer.ts
CHANGED
|
@@ -2,19 +2,18 @@ import { Effect, Layer, Predicate } from 'effect'
|
|
|
2
2
|
import { type Manifest, definitionClass } from '../definition/definition'
|
|
3
3
|
import { AsyncReducer } from '../internal/errors'
|
|
4
4
|
import { type AnyEvent, type EventType } from '../event/event'
|
|
5
|
-
import type
|
|
6
|
-
import { type AnyState, type StateValue } from '../state/state'
|
|
5
|
+
import { type AnyState, type StateName, type StateStore, type StateValue } from '../state/state'
|
|
7
6
|
import {
|
|
8
7
|
type AnyFamily,
|
|
9
8
|
type FamilyKey,
|
|
10
|
-
type
|
|
9
|
+
type FamilyName,
|
|
11
10
|
type FamilyValue,
|
|
11
|
+
type StateFamilyStore,
|
|
12
12
|
Tombstone,
|
|
13
13
|
} from '../state/stateFamily'
|
|
14
14
|
import { narrowHandled, type Tagged } from '../runtime/bus'
|
|
15
15
|
import { type ReducerEntry, Reducers } from '../runtime/loop'
|
|
16
16
|
|
|
17
|
-
/** The reflectable descriptor a reducer carries, like every other primitive's `*Manifest`. */
|
|
18
17
|
export interface ReducerManifest extends Manifest {
|
|
19
18
|
readonly kind: 'Reducer'
|
|
20
19
|
}
|
|
@@ -33,7 +32,6 @@ export interface FamilyReducerConfig<F extends AnyFamily, Events extends Readonl
|
|
|
33
32
|
export interface StateReducerClass<S extends AnyState, Events extends ReadonlyArray<AnyEvent>> {
|
|
34
33
|
new (): {}
|
|
35
34
|
readonly manifest: ReducerManifest
|
|
36
|
-
/** Marker data read by `Reducer.live`. */
|
|
37
35
|
readonly config: StateReducerConfig<S, Events>
|
|
38
36
|
readonly handles: ReadonlySet<string>
|
|
39
37
|
}
|
|
@@ -45,12 +43,26 @@ export interface FamilyReducerClass<F extends AnyFamily, Events extends Readonly
|
|
|
45
43
|
readonly handles: ReadonlySet<string>
|
|
46
44
|
}
|
|
47
45
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
46
|
+
type ErasedFold = (...args: ReadonlyArray<never>) => unknown
|
|
47
|
+
|
|
48
|
+
interface RuntimeStateReducerConfig {
|
|
49
|
+
readonly states: readonly [AnyState]
|
|
50
|
+
readonly events: ReadonlyArray<AnyEvent>
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface RuntimeFamilyReducerConfig {
|
|
54
|
+
readonly family: AnyFamily
|
|
55
|
+
readonly keyOf: ErasedFold
|
|
56
|
+
readonly events: ReadonlyArray<AnyEvent>
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface RuntimeReducerClass {
|
|
60
|
+
new (): {}
|
|
61
|
+
readonly manifest: ReducerManifest
|
|
62
|
+
readonly config: RuntimeStateReducerConfig | RuntimeFamilyReducerConfig
|
|
63
|
+
readonly handles: ReadonlySet<string>
|
|
64
|
+
}
|
|
65
|
+
|
|
54
66
|
export function make<S extends AnyState, const Events extends ReadonlyArray<AnyEvent>>(
|
|
55
67
|
name: string,
|
|
56
68
|
config: StateReducerConfig<S, Events>,
|
|
@@ -61,9 +73,7 @@ export function make<F extends AnyFamily, const Events extends ReadonlyArray<Any
|
|
|
61
73
|
): FamilyReducerClass<F, Events>
|
|
62
74
|
export function make(
|
|
63
75
|
name: string,
|
|
64
|
-
config:
|
|
65
|
-
| StateReducerConfig<AnyState, ReadonlyArray<AnyEvent>>
|
|
66
|
-
| FamilyReducerConfig<AnyFamily, ReadonlyArray<AnyEvent>>,
|
|
76
|
+
config: RuntimeStateReducerConfig | RuntimeFamilyReducerConfig,
|
|
67
77
|
): unknown {
|
|
68
78
|
return definitionClass<unknown>({
|
|
69
79
|
manifest: { kind: 'Reducer' as const, name } satisfies ReducerManifest,
|
|
@@ -72,11 +82,9 @@ export function make(
|
|
|
72
82
|
})
|
|
73
83
|
}
|
|
74
84
|
|
|
75
|
-
/** A thenable check that excludes null/undefined without an assertion. */
|
|
76
85
|
const isThenable = (candidate: unknown): candidate is PromiseLike<unknown> =>
|
|
77
86
|
Predicate.isRecord(candidate) && typeof candidate['then'] === 'function'
|
|
78
87
|
|
|
79
|
-
/** Reject an accidentally-async fold up front: a reducer must be a pure sync write. */
|
|
80
88
|
const sync = <A>(candidate: A): A => {
|
|
81
89
|
if (isThenable(candidate)) {
|
|
82
90
|
// oxlint-disable-next-line reform-rules/no-throw -- sync reducer-write boundary can't yield an Effect; an async fold is a programmer defect
|
|
@@ -88,57 +96,75 @@ const sync = <A>(candidate: A): A => {
|
|
|
88
96
|
export function live<S extends AnyState, Events extends ReadonlyArray<AnyEvent>>(
|
|
89
97
|
reducer: StateReducerClass<S, Events>,
|
|
90
98
|
fold: (value: StateValue<S>, event: EventType<Events[number]>) => StateValue<S>,
|
|
91
|
-
): Layer.Layer<never, never, Reducers |
|
|
99
|
+
): Layer.Layer<never, never, Reducers | StateStore<StateName<S>, StateValue<S>>>
|
|
92
100
|
export function live<F extends AnyFamily, Events extends ReadonlyArray<AnyEvent>>(
|
|
93
101
|
reducer: FamilyReducerClass<F, Events>,
|
|
94
102
|
fold: (entry: FamilyValue<F>, event: EventType<Events[number]>) => FamilyValue<F> | Tombstone,
|
|
95
|
-
): Layer.Layer<
|
|
103
|
+
): Layer.Layer<
|
|
104
|
+
never,
|
|
105
|
+
never,
|
|
106
|
+
Reducers | StateFamilyStore<FamilyName<F>, FamilyKey<F>, FamilyValue<F>>
|
|
107
|
+
>
|
|
96
108
|
export function live(
|
|
97
|
-
reducer:
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
// oxlint-disable-next-line reform-rules/no-explicit-any-value -- overload-impl signature: `unknown` params fail contravariance against the State/Family folds (TS2394); only `any` subsumes both
|
|
101
|
-
fold: (acc: any, event: any) => any,
|
|
102
|
-
): Layer.Layer<never, never, any> {
|
|
109
|
+
reducer: RuntimeReducerClass,
|
|
110
|
+
fold: ErasedFold,
|
|
111
|
+
): Layer.Layer<never, never, unknown> {
|
|
103
112
|
const { config, handles } = reducer
|
|
104
113
|
const name = reducer.manifest.name
|
|
105
|
-
// Scoped registration: register on build, unregister on scope close. For an eager
|
|
106
|
-
// app the scope is the root runtime's (closes only at dispose), so behavior is
|
|
107
|
-
// unchanged; for a lazy feature the scope is the feature's, so unmount removes the
|
|
108
|
-
// entry — it stops folding and is reclaimed instead of leaking onto a dead store.
|
|
109
114
|
return Layer.scopedDiscard(
|
|
110
115
|
Effect.gen(function* () {
|
|
111
116
|
const reducers = yield* Reducers
|
|
112
|
-
if (reducers.
|
|
117
|
+
if (reducers.byName.has(name)) {
|
|
113
118
|
yield* Effect.logWarning(`reform: duplicate reducer name '${name}' registered`)
|
|
114
119
|
}
|
|
115
120
|
const entry: ReducerEntry = yield* Effect.gen(function* () {
|
|
116
121
|
if ('family' in config) {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
122
|
+
return yield* config.family.capture<Effect.Effect<ReducerEntry, never, unknown>>(
|
|
123
|
+
(familyDefinition) =>
|
|
124
|
+
Effect.map(familyDefinition.store, (family) => ({
|
|
125
|
+
name,
|
|
126
|
+
handles,
|
|
127
|
+
apply: (event: Tagged) => {
|
|
128
|
+
// The loop calls entries only for handled events. At this
|
|
129
|
+
// existential boundary the family's decoders restore the
|
|
130
|
+
// exact key/value types before either touches its store.
|
|
131
|
+
const key = familyDefinition.decodeKeySync(
|
|
132
|
+
sync(Reflect.apply(config.keyOf, undefined, [narrowHandled(event)])),
|
|
133
|
+
)
|
|
134
|
+
const previous = family.at(key).get()
|
|
135
|
+
const next = sync(Reflect.apply(fold, undefined, [previous, event]))
|
|
136
|
+
if (next === Tombstone) {
|
|
137
|
+
family.forget(key)
|
|
138
|
+
} else if (next !== previous) {
|
|
139
|
+
// Same-reference return means "no change": skip the nominal re-decode,
|
|
140
|
+
// which would mint a fresh value and repaint every tracker of this slice.
|
|
141
|
+
family.at(key).set(familyDefinition.decodeValueSync(next))
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
})),
|
|
145
|
+
)
|
|
136
146
|
}
|
|
137
|
-
|
|
138
|
-
|
|
147
|
+
return yield* config.states[0].capture<Effect.Effect<ReducerEntry, never, unknown>>(
|
|
148
|
+
(state) =>
|
|
149
|
+
Effect.map(state.store, (store) => ({
|
|
150
|
+
name,
|
|
151
|
+
handles,
|
|
152
|
+
apply: (event: Tagged) => {
|
|
153
|
+
const previous = store.get()
|
|
154
|
+
const next = sync(Reflect.apply(fold, undefined, [previous, event]))
|
|
155
|
+
// Same-reference return means "no change": skip the nominal re-decode,
|
|
156
|
+
// which would mint a fresh value and repaint every tracker of this slice.
|
|
157
|
+
if (next === previous) {
|
|
158
|
+
return
|
|
159
|
+
}
|
|
160
|
+
store.set(state.decodeUnknownSync(next))
|
|
161
|
+
},
|
|
162
|
+
})),
|
|
163
|
+
)
|
|
139
164
|
})
|
|
140
|
-
yield* Effect.acquireRelease(
|
|
141
|
-
Effect.sync(() => reducers.
|
|
165
|
+
yield* Effect.acquireRelease(
|
|
166
|
+
Effect.sync(() => reducers.register(entry)),
|
|
167
|
+
() => Effect.sync(() => reducers.unregister(entry)),
|
|
142
168
|
)
|
|
143
169
|
}),
|
|
144
170
|
)
|