@playfast/reform 0.0.8 → 0.0.10
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/package.json +1 -1
- package/src/calc/asyncCalc.invalidate.test.ts +166 -0
- package/src/calc/asyncCalc.ts +44 -0
- package/src/calc/queryState.ts +52 -0
- package/src/compose/composition.ts +57 -23
- package/src/compose/host.ts +7 -13
- package/src/compose/structure.test.ts +90 -0
- package/src/compose/structure.ts +145 -0
- package/src/compose/ui.test.ts +2 -30
- package/src/compose/ui.ts +21 -43
- package/src/index.ts +39 -3
- package/src/internal/capture.ts +8 -0
- package/src/internal/errors.ts +0 -12
- package/src/internal/queryDriver.ts +209 -71
- package/src/internal/queryEvents.ts +34 -0
- package/src/internal/queryStore.ts +36 -0
- package/src/runtime/loop.ts +14 -7
- package/src/runtime/queries.ts +53 -0
- package/src/scene/scene.ts +26 -14
- package/src/scene/seedScene.test.ts +37 -30
- package/src/state/stateGroup.ts +20 -0
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { Effect, Layer, ManagedRuntime, Schema as S } from 'effect'
|
|
2
2
|
import { expect, test } from 'vitest'
|
|
3
3
|
import {
|
|
4
|
-
type CaptureSinkApi,
|
|
5
|
-
CaptureSink,
|
|
6
4
|
Composition,
|
|
7
5
|
Engine,
|
|
8
6
|
Event,
|
|
7
|
+
isStructure,
|
|
8
|
+
mount,
|
|
9
9
|
provide,
|
|
10
10
|
publish,
|
|
11
11
|
Reducer,
|
|
@@ -18,13 +18,14 @@ import {
|
|
|
18
18
|
Ui,
|
|
19
19
|
ui,
|
|
20
20
|
} from '../index'
|
|
21
|
+
import { CurrentSeedOverrides } from '../internal/seeds'
|
|
21
22
|
|
|
22
23
|
// `seedScene` is the tooling seam over CLOSED scenes: the app bundle below seeds
|
|
23
24
|
// `count: 1` itself (no open store), and the overlay must reach the nested
|
|
24
25
|
// `State.live` through `Layer.locally(CurrentSeedOverrides, …)` on the scene's
|
|
25
26
|
// pre-composed layers. The harness mirrors the dev-tool preview: merge the
|
|
26
27
|
// scene's `provide`, mount it in a ManagedRuntime, and read the root
|
|
27
|
-
// composition's rendered props through the headless
|
|
28
|
+
// composition's rendered props through the headless Structure.
|
|
28
29
|
|
|
29
30
|
class CountState extends State.make('count', S.Number) {}
|
|
30
31
|
class MiniStates extends StateGroup.make(CountState) {}
|
|
@@ -46,8 +47,7 @@ class Counter extends Composition.make('Counter', {
|
|
|
46
47
|
}) {}
|
|
47
48
|
const CounterLive = Composition.live(Counter, function* () {
|
|
48
49
|
const count = yield* StateGroup.select(MiniStates, 'count')
|
|
49
|
-
|
|
50
|
-
return view({ count })
|
|
50
|
+
return mount({ props: { count }, slots: {} })
|
|
51
51
|
})
|
|
52
52
|
|
|
53
53
|
// The closed app layer a scene provides: views + logic + Engine, with the state
|
|
@@ -61,37 +61,44 @@ const MiniApp = Views.pipe(Layer.provideMerge(Logic))
|
|
|
61
61
|
|
|
62
62
|
const CounterScene = scene(Counter, { provide: [MiniApp] })
|
|
63
63
|
|
|
64
|
+
// The dev-tool inspector overlays raw, user-typed seeds (`Record<string, unknown>`)
|
|
65
|
+
// — the untyped seam that `seedScene` wraps once a composition's state tuple is
|
|
66
|
+
// known. These two cases (absent key / schema-invalid value) are compile errors
|
|
67
|
+
// through the now-typed `seedScene`, so they're exercised here at the raw overlay
|
|
68
|
+
// to prove `resolveSeed`'s runtime defensiveness still falls back to the seed.
|
|
69
|
+
const overlayRawSeeds = (base: Scene, seeds: Readonly<Record<string, unknown>>): Scene => ({
|
|
70
|
+
...base,
|
|
71
|
+
provide: base.provide.map(Layer.locally(CurrentSeedOverrides, seeds)),
|
|
72
|
+
})
|
|
73
|
+
|
|
64
74
|
const headlessEnv: RenderEnv = {
|
|
65
75
|
props: {},
|
|
66
76
|
tracker: { add: () => {} },
|
|
67
|
-
slots: { slot: () => () => null },
|
|
68
77
|
}
|
|
69
78
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
79
|
+
const isCountProps = (props: unknown): props is { readonly count: number } =>
|
|
80
|
+
typeof props === 'object' &&
|
|
81
|
+
props !== null &&
|
|
82
|
+
'count' in props &&
|
|
83
|
+
typeof props.count === 'number'
|
|
84
|
+
|
|
85
|
+
// One ManagedRuntime per scene under test. Each `read` re-renders the root
|
|
86
|
+
// composition and reads the Structure built from the stores that runtime actually
|
|
87
|
+
// constructed.
|
|
74
88
|
const makeHarness = (s: Scene) => {
|
|
75
|
-
const
|
|
76
|
-
const sink: CaptureSinkApi = {
|
|
77
|
-
record: (capture) => {
|
|
78
|
-
const props = capture.props
|
|
79
|
-
if (
|
|
80
|
-
typeof props === 'object' &&
|
|
81
|
-
props !== null &&
|
|
82
|
-
'count' in props &&
|
|
83
|
-
typeof props.count === 'number'
|
|
84
|
-
) {
|
|
85
|
-
captured.count = props.count
|
|
86
|
-
}
|
|
87
|
-
},
|
|
88
|
-
}
|
|
89
|
-
const layer = s.provide
|
|
90
|
-
.reduce((a, b) => Layer.merge(a, b))
|
|
91
|
-
.pipe(Layer.provideMerge(Layer.succeed(CaptureSink, sink)))
|
|
89
|
+
const layer = s.provide.reduce((a, b) => Layer.merge(a, b))
|
|
92
90
|
const runtime = ManagedRuntime.make(layer)
|
|
93
91
|
const read = Effect.flatMap(Counter.tag, (c) =>
|
|
94
|
-
Composition.render(c, headlessEnv).pipe(
|
|
92
|
+
Composition.render(c, headlessEnv).pipe(
|
|
93
|
+
Effect.flatMap((frame) => {
|
|
94
|
+
if (!isStructure(frame)) {
|
|
95
|
+
return Effect.dieMessage('Counter must render a Structure')
|
|
96
|
+
}
|
|
97
|
+
return isCountProps(frame.props)
|
|
98
|
+
? Effect.succeed(frame.props.count)
|
|
99
|
+
: Effect.dieMessage('Counter Structure props must include numeric count')
|
|
100
|
+
}),
|
|
101
|
+
),
|
|
95
102
|
)
|
|
96
103
|
return { runtime, read }
|
|
97
104
|
}
|
|
@@ -134,7 +141,7 @@ test('the live reducer drives the SAME overridden store', async () => {
|
|
|
134
141
|
})
|
|
135
142
|
|
|
136
143
|
test('an absent key leaves the authored seed in place', async () => {
|
|
137
|
-
const { runtime, read } = makeHarness(
|
|
144
|
+
const { runtime, read } = makeHarness(overlayRawSeeds(CounterScene, { other: 1 }))
|
|
138
145
|
try {
|
|
139
146
|
expect(await runtime.runPromise(read)).toBe(1)
|
|
140
147
|
} finally {
|
|
@@ -143,7 +150,7 @@ test('an absent key leaves the authored seed in place', async () => {
|
|
|
143
150
|
})
|
|
144
151
|
|
|
145
152
|
test('a schema-invalid override falls back to the authored seed', async () => {
|
|
146
|
-
const { runtime, read } = makeHarness(
|
|
153
|
+
const { runtime, read } = makeHarness(overlayRawSeeds(CounterScene, { count: 'not-a-number' }))
|
|
147
154
|
try {
|
|
148
155
|
expect(await runtime.runPromise(read)).toBe(1)
|
|
149
156
|
} finally {
|
package/src/state/stateGroup.ts
CHANGED
|
@@ -51,6 +51,26 @@ export type GroupSeeds<G extends AnyStateGroup> =
|
|
|
51
51
|
? { readonly [N in StateName<Members[number]>]: ValueForName<Members, N> }
|
|
52
52
|
: never
|
|
53
53
|
|
|
54
|
+
// Collapse a union of records into their intersection — merges every group's
|
|
55
|
+
// seed record into one. (The classic contravariant-position trick.)
|
|
56
|
+
type UnionToIntersection<U> = (U extends unknown ? (k: U) => void : never) extends (
|
|
57
|
+
k: infer I,
|
|
58
|
+
) => void
|
|
59
|
+
? I
|
|
60
|
+
: never
|
|
61
|
+
|
|
62
|
+
/** The seed record for one tuple entry: a group's `GroupSeeds`, else nothing. */
|
|
63
|
+
type EntrySeeds<G> = G extends AnyStateGroup ? GroupSeeds<G> : never
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The optional seed overrides for a composition's whole `states` tuple, keyed by
|
|
67
|
+
* member name and typed to each member's value. Every key is optional (seeds are
|
|
68
|
+
* overrides over the authored initial). Tuple entries that aren't state groups
|
|
69
|
+
* contribute no keys, so a partially-grouped `states` still types its groups.
|
|
70
|
+
* Powers `seedScene` — a typo'd key or mistyped value is a compile error.
|
|
71
|
+
*/
|
|
72
|
+
export type SeedsOf<St extends ReadonlyArray<unknown>> = Partial<UnionToIntersection<EntrySeeds<St[number]>>>
|
|
73
|
+
|
|
54
74
|
/** Compose atomic States into a group provided (and addressed) as a unit. */
|
|
55
75
|
export const make = <const Members extends ReadonlyArray<AnyState>>(
|
|
56
76
|
...members: Members & NoDuplicateNames<Members>
|