@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/scene/scene.ts
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
|
-
import { Layer, Struct } from 'effect'
|
|
2
|
-
import type {
|
|
1
|
+
import { Layer, Predicate, Struct } from 'effect'
|
|
2
|
+
import type {
|
|
3
|
+
AnyComposition,
|
|
4
|
+
CompositionClass,
|
|
5
|
+
CompositionId,
|
|
6
|
+
CompositionReflection,
|
|
7
|
+
HasExactCompositionId,
|
|
8
|
+
} from '../compose/composition'
|
|
3
9
|
import type { UiContract } from '../compose/ui'
|
|
4
10
|
import type { EventOf } from '../event/event'
|
|
11
|
+
import { type EagerFeatureClass, type FeatureRequirement, tree } from '../feature/feature'
|
|
5
12
|
import { CurrentSeedOverrides } from '../internal/seeds'
|
|
6
13
|
import type { SeedsOf } from '../state/stateGroup'
|
|
7
14
|
import type { Bus } from '../runtime/bus'
|
|
@@ -10,121 +17,257 @@ import {
|
|
|
10
17
|
type Instrumentation,
|
|
11
18
|
noopInstrumentation,
|
|
12
19
|
} from '../runtime/instrumentation'
|
|
20
|
+
import type { EngineServices } from '../runtime/loop'
|
|
13
21
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
22
|
+
export type MountedServices = CompositionId<string> | Bus
|
|
23
|
+
|
|
24
|
+
export type SceneCapture<C extends UiContract, S extends ReadonlyArray<unknown>, Result> = <
|
|
25
|
+
Services,
|
|
26
|
+
P,
|
|
27
|
+
N extends string,
|
|
28
|
+
Identity,
|
|
29
|
+
>(
|
|
30
|
+
scene: CapturedScene<C, S, Services, P, N, Identity>,
|
|
31
|
+
) => Result
|
|
32
|
+
|
|
33
|
+
export type AnySceneCapture<Result> = <
|
|
34
|
+
C extends UiContract,
|
|
35
|
+
S extends ReadonlyArray<unknown>,
|
|
36
|
+
Services,
|
|
37
|
+
P,
|
|
38
|
+
N extends string,
|
|
39
|
+
Identity,
|
|
40
|
+
>(
|
|
41
|
+
scene: CapturedScene<C, S, Services, P, N, Identity>,
|
|
42
|
+
) => Result
|
|
43
|
+
|
|
44
|
+
interface SceneOptionalFields {
|
|
45
|
+
readonly boot: ReadonlyArray<BootEvent>
|
|
46
|
+
// Hosts record render spans outside the scene's layer context.
|
|
47
|
+
readonly instrumentation: Instrumentation
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface AnySceneExternalApi extends Partial<SceneOptionalFields> {
|
|
51
|
+
readonly kind: 'Scene'
|
|
52
|
+
readonly composition: AnyComposition
|
|
53
|
+
readonly captureAny: <Result>(visit: AnySceneCapture<Result>) => Result
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export type AnyScene = AnySceneExternalApi
|
|
38
57
|
|
|
39
58
|
export interface Scene<
|
|
40
59
|
C extends UiContract = UiContract,
|
|
41
60
|
S extends ReadonlyArray<unknown> = ReadonlyArray<unknown>,
|
|
42
|
-
> {
|
|
61
|
+
> extends AnySceneExternalApi {
|
|
62
|
+
readonly composition: AnyComposition & CompositionReflection<unknown, C, S>
|
|
63
|
+
readonly capture: <Result>(visit: SceneCapture<C, S, Result>) => Result
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
type BootEvent = EventOf<string, unknown>
|
|
67
|
+
|
|
68
|
+
export interface CapturedScene<
|
|
69
|
+
C extends UiContract,
|
|
70
|
+
S extends ReadonlyArray<unknown>,
|
|
71
|
+
Services,
|
|
72
|
+
P,
|
|
73
|
+
N extends string,
|
|
74
|
+
Identity = unknown,
|
|
75
|
+
> extends Scene<C, S> {
|
|
43
76
|
readonly kind: 'Scene'
|
|
44
|
-
|
|
45
|
-
readonly
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
77
|
+
readonly composition: CompositionClass<P, C, S, N, Identity>
|
|
78
|
+
readonly provide: ReadonlyArray<Layer.Layer<Services, never, never>>
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
interface SceneConfigOptionalFields {
|
|
82
|
+
readonly boot: ReadonlyArray<BootEvent>
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface SceneConfig<Services> extends Partial<SceneConfigOptionalFields> {
|
|
86
|
+
readonly provide: ReadonlyArray<Layer.Layer<Services, never, never>>
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
type SceneConfigIncludingRootComposition<Services, N extends string, Identity> =
|
|
90
|
+
HasExactCompositionId<Services, N, Identity> extends true
|
|
91
|
+
? SceneConfig<Services>
|
|
92
|
+
: SceneConfig<Services> & {
|
|
93
|
+
readonly missingCompositionService: 'The scene must provide its root composition'
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
interface SceneValueConfigExternalApi<Services>
|
|
97
|
+
extends SceneConfig<Services>, Partial<Pick<SceneOptionalFields, 'instrumentation'>> {}
|
|
98
|
+
|
|
99
|
+
const makeSceneValue = <
|
|
100
|
+
P,
|
|
101
|
+
C extends UiContract,
|
|
102
|
+
S extends ReadonlyArray<unknown>,
|
|
103
|
+
N extends string,
|
|
104
|
+
Identity,
|
|
105
|
+
Services,
|
|
106
|
+
>(
|
|
107
|
+
composition: CompositionClass<P, C, S, N, Identity>,
|
|
108
|
+
config: SceneValueConfigExternalApi<Services>,
|
|
109
|
+
): CapturedScene<C, S, Services, P, N, Identity> => {
|
|
110
|
+
const sceneValue: CapturedScene<C, S, Services, P, N, Identity> = {
|
|
111
|
+
kind: 'Scene',
|
|
112
|
+
composition,
|
|
113
|
+
provide: config.provide,
|
|
114
|
+
...(config.boot !== undefined ? { boot: config.boot } : {}),
|
|
115
|
+
...(config.instrumentation !== undefined ? { instrumentation: config.instrumentation } : {}),
|
|
116
|
+
capture: <Result>(visit: SceneCapture<C, S, Result>): Result => visit(sceneValue),
|
|
117
|
+
captureAny: <Result>(visit: AnySceneCapture<Result>): Result => visit(sceneValue),
|
|
118
|
+
}
|
|
119
|
+
return sceneValue
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export const scene = <
|
|
123
|
+
P,
|
|
124
|
+
C extends UiContract,
|
|
125
|
+
S extends ReadonlyArray<unknown>,
|
|
126
|
+
N extends string,
|
|
127
|
+
Identity,
|
|
128
|
+
Services,
|
|
129
|
+
>(
|
|
130
|
+
composition: CompositionClass<P, C, S, N, Identity>,
|
|
131
|
+
config: SceneConfigIncludingRootComposition<Services, N, Identity>,
|
|
132
|
+
): CapturedScene<C, S, Services, P, N, Identity> => {
|
|
133
|
+
return makeSceneValue(composition, config)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export interface FeatureSceneConfig<OpenServices> {
|
|
137
|
+
readonly provide: Layer.Layer<EngineServices | OpenServices, never, never>
|
|
59
138
|
}
|
|
60
139
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
140
|
+
export const featureScene = <
|
|
141
|
+
FeatureIdentity,
|
|
142
|
+
P,
|
|
143
|
+
C extends UiContract,
|
|
144
|
+
ROut,
|
|
145
|
+
DirectRequires extends ReadonlyArray<FeatureRequirement>,
|
|
146
|
+
OpenServices,
|
|
147
|
+
S extends ReadonlyArray<unknown>,
|
|
148
|
+
N extends string,
|
|
149
|
+
Identity,
|
|
150
|
+
>(
|
|
151
|
+
feature: EagerFeatureClass<
|
|
152
|
+
FeatureIdentity,
|
|
153
|
+
P,
|
|
154
|
+
C,
|
|
155
|
+
ROut,
|
|
156
|
+
DirectRequires,
|
|
157
|
+
OpenServices,
|
|
158
|
+
S,
|
|
159
|
+
N,
|
|
160
|
+
Identity
|
|
161
|
+
>,
|
|
162
|
+
config: FeatureSceneConfig<OpenServices>,
|
|
163
|
+
): CapturedScene<C, S, ROut | EngineServices | OpenServices, P, N, Identity> => {
|
|
164
|
+
const native = tree<typeof feature>(feature.token)
|
|
165
|
+
const app = native.eagerModule.layer.pipe(Layer.provideMerge(config.provide))
|
|
166
|
+
return makeSceneValue(native.composition, {
|
|
167
|
+
provide: [app],
|
|
168
|
+
boot: native.binding.boot,
|
|
169
|
+
})
|
|
66
170
|
}
|
|
67
171
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
...(config.boot !== undefined ? { boot: config.boot } : {}),
|
|
81
|
-
})
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Overlay seed values onto a CLOSED scene — the tooling/test seam (dev-tool
|
|
85
|
-
* inspector overrides, proofs). Scenes stay closed for authors: this does not
|
|
86
|
-
* reopen `provide`, it wraps each layer with `Layer.locally(CurrentSeedOverrides,
|
|
87
|
-
* seeds)` so `State.live` boots matching stores from the override instead of
|
|
88
|
-
* the authored seed.
|
|
89
|
-
*
|
|
90
|
-
* Keys are state MEMBER NAMES as reflected in `manifest.states` (e.g. `count`),
|
|
91
|
-
* not group names. If two groups in one scene share a member name, both receive
|
|
92
|
-
* the override (documented limitation). Values are schema-validated at build:
|
|
93
|
-
* an invalid value silently falls back to the authored seed. `StateFamily`
|
|
94
|
-
* entries are not covered.
|
|
95
|
-
*/
|
|
96
|
-
export const seedScene = <C extends UiContract, S extends ReadonlyArray<unknown>>(
|
|
172
|
+
export function seedScene<
|
|
173
|
+
C extends UiContract,
|
|
174
|
+
S extends ReadonlyArray<unknown>,
|
|
175
|
+
Services,
|
|
176
|
+
P,
|
|
177
|
+
N extends string,
|
|
178
|
+
Identity,
|
|
179
|
+
>(
|
|
180
|
+
base: CapturedScene<C, S, Services, P, N, Identity>,
|
|
181
|
+
seeds: SeedsOf<S>,
|
|
182
|
+
): CapturedScene<C, S, Services, P, N, Identity>
|
|
183
|
+
export function seedScene<C extends UiContract, S extends ReadonlyArray<unknown>>(
|
|
97
184
|
base: Scene<C, S>,
|
|
98
185
|
seeds: SeedsOf<S>,
|
|
99
|
-
): Scene<C, S>
|
|
100
|
-
|
|
101
|
-
? base
|
|
102
|
-
: { ...base, provide: base.provide.map(Layer.locally(CurrentSeedOverrides, seeds)) }
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Overlay profiler hooks onto a CLOSED scene — the profiling analog of
|
|
106
|
-
* {@link seedScene}. Wraps each provided layer with
|
|
107
|
-
* `Layer.locally(CurrentInstrumentation, instr)` so engine/state/calc layers
|
|
108
|
-
* capture `instr` at build time, and carries `instr` on the scene so hosts can
|
|
109
|
-
* time renders. Scenes stay closed; production wiring never calls this —
|
|
110
|
-
* `@playfast/reform-profiler`'s `profileScene` is the intended caller.
|
|
111
|
-
*/
|
|
112
|
-
export const profiledScene = <C extends UiContract, S extends ReadonlyArray<unknown>>(
|
|
186
|
+
): Scene<C, S>
|
|
187
|
+
export function seedScene<C extends UiContract, S extends ReadonlyArray<unknown>>(
|
|
113
188
|
base: Scene<C, S>,
|
|
189
|
+
seeds: SeedsOf<S>,
|
|
190
|
+
): Scene<C, S> {
|
|
191
|
+
if (Struct.keys(seeds).length === 0) {
|
|
192
|
+
return base
|
|
193
|
+
}
|
|
194
|
+
// Tooling overlays closed layers; member names, not group names, are the keys.
|
|
195
|
+
return base.capture<Scene<C, S>>(
|
|
196
|
+
(exactScene): Scene<C, S> =>
|
|
197
|
+
makeSceneValue(exactScene.composition, {
|
|
198
|
+
provide: exactScene.provide.map(Layer.locally(CurrentSeedOverrides, seeds)),
|
|
199
|
+
...(exactScene.boot === undefined ? {} : { boot: exactScene.boot }),
|
|
200
|
+
...(exactScene.instrumentation === undefined
|
|
201
|
+
? {}
|
|
202
|
+
: { instrumentation: exactScene.instrumentation }),
|
|
203
|
+
}),
|
|
204
|
+
)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export const seedAnyScene = (
|
|
208
|
+
base: AnyScene,
|
|
209
|
+
seeds: Readonly<Record<string, unknown>>,
|
|
210
|
+
): AnyScene => {
|
|
211
|
+
if (Struct.keys(seeds).length === 0) {
|
|
212
|
+
return base
|
|
213
|
+
}
|
|
214
|
+
return base.captureAny<AnyScene>(
|
|
215
|
+
(exactScene): AnyScene =>
|
|
216
|
+
makeSceneValue(exactScene.composition, {
|
|
217
|
+
provide: exactScene.provide.map(Layer.locally(CurrentSeedOverrides, seeds)),
|
|
218
|
+
...(exactScene.boot === undefined ? {} : { boot: exactScene.boot }),
|
|
219
|
+
...(exactScene.instrumentation === undefined
|
|
220
|
+
? {}
|
|
221
|
+
: { instrumentation: exactScene.instrumentation }),
|
|
222
|
+
}),
|
|
223
|
+
)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function profiledScene<
|
|
227
|
+
C extends UiContract,
|
|
228
|
+
S extends ReadonlyArray<unknown>,
|
|
229
|
+
Services,
|
|
230
|
+
P,
|
|
231
|
+
N extends string,
|
|
232
|
+
Identity,
|
|
233
|
+
>(
|
|
234
|
+
base: CapturedScene<C, S, Services, P, N, Identity>,
|
|
114
235
|
instrumentation: Instrumentation,
|
|
115
|
-
):
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
236
|
+
): CapturedScene<C, S, Services, P, N, Identity>
|
|
237
|
+
export function profiledScene<C extends UiContract, S extends ReadonlyArray<unknown>>(
|
|
238
|
+
base: Scene<C, S>,
|
|
239
|
+
instrumentation: Instrumentation,
|
|
240
|
+
): Scene<C, S>
|
|
241
|
+
export function profiledScene<C extends UiContract, S extends ReadonlyArray<unknown>>(
|
|
242
|
+
base: Scene<C, S>,
|
|
243
|
+
instrumentation: Instrumentation,
|
|
244
|
+
): Scene<C, S> {
|
|
245
|
+
return base.capture<Scene<C, S>>(
|
|
246
|
+
(exactScene): Scene<C, S> =>
|
|
247
|
+
makeSceneValue(exactScene.composition, {
|
|
248
|
+
provide: exactScene.provide.map(Layer.locally(CurrentInstrumentation, instrumentation)),
|
|
249
|
+
...(exactScene.boot === undefined ? {} : { boot: exactScene.boot }),
|
|
250
|
+
instrumentation,
|
|
251
|
+
}),
|
|
252
|
+
)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export const sceneInstrumentation = (candidate: AnyScene): Instrumentation =>
|
|
123
256
|
candidate.instrumentation ?? noopInstrumentation
|
|
124
257
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
candidate
|
|
129
|
-
'kind'
|
|
130
|
-
candidate
|
|
258
|
+
const isReflectableComposition = (candidate: unknown): candidate is AnyComposition =>
|
|
259
|
+
typeof candidate === 'function' &&
|
|
260
|
+
Predicate.hasProperty(candidate, 'manifest') &&
|
|
261
|
+
Predicate.isRecord(candidate.manifest) &&
|
|
262
|
+
candidate.manifest['kind'] === 'Composition' &&
|
|
263
|
+
Predicate.hasProperty(candidate, 'parseProps') &&
|
|
264
|
+
typeof candidate.parseProps === 'function' &&
|
|
265
|
+
Predicate.hasProperty(candidate, 'capture') &&
|
|
266
|
+
typeof candidate.capture === 'function'
|
|
267
|
+
|
|
268
|
+
export const isScene = (candidate: unknown): candidate is AnyScene =>
|
|
269
|
+
Predicate.isRecord(candidate) &&
|
|
270
|
+
candidate['kind'] === 'Scene' &&
|
|
271
|
+
isReflectableComposition(candidate['composition']) &&
|
|
272
|
+
typeof candidate['capture'] === 'function' &&
|
|
273
|
+
typeof candidate['captureAny'] === 'function'
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Effect, Layer,
|
|
2
|
-
import { expect,
|
|
1
|
+
import { Effect, Layer, Schema as S } from 'effect'
|
|
2
|
+
import { expect, it } from '@effect/vitest'
|
|
3
3
|
import {
|
|
4
4
|
Composition,
|
|
5
5
|
Engine,
|
|
@@ -10,7 +10,6 @@ import {
|
|
|
10
10
|
publish,
|
|
11
11
|
Reducer,
|
|
12
12
|
type RenderEnv,
|
|
13
|
-
type Scene,
|
|
14
13
|
scene,
|
|
15
14
|
seedScene,
|
|
16
15
|
State,
|
|
@@ -20,13 +19,6 @@ import {
|
|
|
20
19
|
} from '../index'
|
|
21
20
|
import { CurrentSeedOverrides } from '../internal/seeds'
|
|
22
21
|
|
|
23
|
-
// `seedScene` is the tooling seam over CLOSED scenes: the app bundle below seeds
|
|
24
|
-
// `count: 1` itself (no open store), and the overlay must reach the nested
|
|
25
|
-
// `State.live` through `Layer.locally(CurrentSeedOverrides, …)` on the scene's
|
|
26
|
-
// pre-composed layers. The harness mirrors the dev-tool preview: merge the
|
|
27
|
-
// scene's `provide`, mount it in a ManagedRuntime, and read the root
|
|
28
|
-
// composition's rendered props through the headless Structure.
|
|
29
|
-
|
|
30
22
|
class CountState extends State.make('count', S.Number) {}
|
|
31
23
|
class MiniStates extends StateGroup.make(CountState) {}
|
|
32
24
|
|
|
@@ -44,15 +36,16 @@ class Counter extends Composition.make('Counter', {
|
|
|
44
36
|
states: [MiniStates],
|
|
45
37
|
events: [Bumped],
|
|
46
38
|
ui: CounterUi,
|
|
47
|
-
}) {}
|
|
39
|
+
})<Counter>() {}
|
|
48
40
|
const CounterLive = Composition.live(Counter, function* () {
|
|
49
41
|
const count = yield* StateGroup.select(MiniStates, 'count')
|
|
50
42
|
return mount({ props: { count }, slots: {} })
|
|
51
43
|
})
|
|
52
44
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
45
|
+
const presentations = provide(
|
|
46
|
+
CounterUi,
|
|
47
|
+
Ui.make(CounterUi, () => null),
|
|
48
|
+
)
|
|
56
49
|
const Views = CounterLive.pipe(Layer.provideMerge(presentations))
|
|
57
50
|
const Logic = Layer.mergeAll(BumpReducerLive).pipe(
|
|
58
51
|
Layer.provideMerge(Layer.mergeAll(Engine, StateGroup.live(MiniStates, { count: 1 }))),
|
|
@@ -61,12 +54,10 @@ const MiniApp = Views.pipe(Layer.provideMerge(Logic))
|
|
|
61
54
|
|
|
62
55
|
const CounterScene = scene(Counter, { provide: [MiniApp] })
|
|
63
56
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
// to prove `resolveSeed`'s runtime defensiveness still falls back to the seed.
|
|
69
|
-
const overlayRawSeeds = (base: Scene, seeds: Readonly<Record<string, unknown>>): Scene => ({
|
|
57
|
+
const overlayRawSeeds = (
|
|
58
|
+
base: typeof CounterScene,
|
|
59
|
+
seeds: Readonly<Record<string, unknown>>,
|
|
60
|
+
): typeof CounterScene => ({
|
|
70
61
|
...base,
|
|
71
62
|
provide: base.provide.map(Layer.locally(CurrentSeedOverrides, seeds)),
|
|
72
63
|
})
|
|
@@ -77,17 +68,10 @@ const headlessEnv: RenderEnv = {
|
|
|
77
68
|
}
|
|
78
69
|
|
|
79
70
|
const isCountProps = (props: unknown): props is { readonly count: number } =>
|
|
80
|
-
typeof props === 'object' &&
|
|
81
|
-
|
|
82
|
-
|
|
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.
|
|
88
|
-
const makeHarness = (s: Scene) => {
|
|
71
|
+
typeof props === 'object' && props !== null && 'count' in props && typeof props.count === 'number'
|
|
72
|
+
|
|
73
|
+
const makeHarness = (s: typeof CounterScene) => {
|
|
89
74
|
const layer = s.provide.reduce((a, b) => Layer.merge(a, b))
|
|
90
|
-
const runtime = ManagedRuntime.make(layer)
|
|
91
75
|
const read = Effect.flatMap(Counter.tag, (c) =>
|
|
92
76
|
Composition.render(c, headlessEnv).pipe(
|
|
93
77
|
Effect.flatMap((frame) => {
|
|
@@ -100,10 +84,9 @@ const makeHarness = (s: Scene) => {
|
|
|
100
84
|
}),
|
|
101
85
|
),
|
|
102
86
|
)
|
|
103
|
-
return {
|
|
87
|
+
return { layer, read }
|
|
104
88
|
}
|
|
105
89
|
|
|
106
|
-
// Drive a read until a predicate holds, handing forked fibers the scheduler.
|
|
107
90
|
const waitUntil = <A, R>(
|
|
108
91
|
read: Effect.Effect<A, never, R>,
|
|
109
92
|
pred: (a: A) => boolean,
|
|
@@ -115,62 +98,42 @@ const waitUntil = <A, R>(
|
|
|
115
98
|
: Effect.flatMap(Effect.yieldNow(), () => waitUntil(read, pred, rounds - 1)),
|
|
116
99
|
)
|
|
117
100
|
|
|
118
|
-
|
|
119
|
-
const {
|
|
120
|
-
|
|
121
|
-
expect(
|
|
122
|
-
}
|
|
123
|
-
await runtime.dispose()
|
|
124
|
-
}
|
|
101
|
+
it.scopedLive('a seeded scene boots its store from the override', () => {
|
|
102
|
+
const { layer, read } = makeHarness(seedScene(CounterScene, { count: 5 }))
|
|
103
|
+
return Effect.gen(function* () {
|
|
104
|
+
expect(yield* read).toBe(5)
|
|
105
|
+
}).pipe(Effect.provide(layer))
|
|
125
106
|
})
|
|
126
107
|
|
|
127
|
-
|
|
128
|
-
const {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
yield* publish('High', Event.construct(Bumped, {}))
|
|
134
|
-
return yield* waitUntil(read, (n) => n === 6)
|
|
135
|
-
}),
|
|
136
|
-
)
|
|
108
|
+
it.scopedLive('the live reducer drives the SAME overridden store', () => {
|
|
109
|
+
const { layer, read } = makeHarness(seedScene(CounterScene, { count: 5 }))
|
|
110
|
+
return Effect.gen(function* () {
|
|
111
|
+
expect(yield* read).toBe(5)
|
|
112
|
+
yield* publish('High', Event.construct(Bumped, {}))
|
|
113
|
+
const next = yield* waitUntil(read, (n) => n === 6)
|
|
137
114
|
expect(next).toBe(6)
|
|
138
|
-
}
|
|
139
|
-
await runtime.dispose()
|
|
140
|
-
}
|
|
115
|
+
}).pipe(Effect.provide(layer))
|
|
141
116
|
})
|
|
142
117
|
|
|
143
|
-
|
|
144
|
-
const {
|
|
145
|
-
|
|
146
|
-
expect(
|
|
147
|
-
}
|
|
148
|
-
await runtime.dispose()
|
|
149
|
-
}
|
|
118
|
+
it.scopedLive('an absent key leaves the authored seed in place', () => {
|
|
119
|
+
const { layer, read } = makeHarness(overlayRawSeeds(CounterScene, { other: 1 }))
|
|
120
|
+
return Effect.gen(function* () {
|
|
121
|
+
expect(yield* read).toBe(1)
|
|
122
|
+
}).pipe(Effect.provide(layer))
|
|
150
123
|
})
|
|
151
124
|
|
|
152
|
-
|
|
153
|
-
const {
|
|
154
|
-
|
|
155
|
-
expect(
|
|
156
|
-
}
|
|
157
|
-
await runtime.dispose()
|
|
158
|
-
}
|
|
125
|
+
it.scopedLive('a schema-invalid override falls back to the authored seed', () => {
|
|
126
|
+
const { layer, read } = makeHarness(overlayRawSeeds(CounterScene, { count: 'not-a-number' }))
|
|
127
|
+
return Effect.gen(function* () {
|
|
128
|
+
expect(yield* read).toBe(1)
|
|
129
|
+
}).pipe(Effect.provide(layer))
|
|
159
130
|
})
|
|
160
131
|
|
|
161
|
-
|
|
132
|
+
it.scopedLive('seeding does not leak into the original scene', () => {
|
|
162
133
|
const seeded = makeHarness(seedScene(CounterScene, { count: 5 }))
|
|
163
|
-
try {
|
|
164
|
-
expect(await seeded.runtime.runPromise(seeded.read)).toBe(5)
|
|
165
|
-
} finally {
|
|
166
|
-
await seeded.runtime.dispose()
|
|
167
|
-
}
|
|
168
|
-
// Building the ORIGINAL scene's layers again boots from the authored seed —
|
|
169
|
-
// the overlay lives only on the seeded variant's wrapped layers.
|
|
170
134
|
const original = makeHarness(CounterScene)
|
|
171
|
-
|
|
172
|
-
expect(
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
}
|
|
135
|
+
return Effect.gen(function* () {
|
|
136
|
+
expect(yield* seeded.read.pipe(Effect.provide(seeded.layer))).toBe(5)
|
|
137
|
+
expect(yield* original.read.pipe(Effect.provide(original.layer))).toBe(1)
|
|
138
|
+
})
|
|
176
139
|
})
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { Layer, Schema as S } from 'effect'
|
|
2
|
+
import type { ProvidedBy } from '../feature/feature'
|
|
3
|
+
import * as State from './state'
|
|
4
|
+
import * as StateFamily from './stateFamily'
|
|
5
|
+
|
|
6
|
+
type Equal<Left, Right> =
|
|
7
|
+
(<Value>() => Value extends Left ? 1 : 2) extends <Value>() => Value extends Right ? 1 : 2
|
|
8
|
+
? true
|
|
9
|
+
: false
|
|
10
|
+
type Expect<Value extends true> = Value
|
|
11
|
+
|
|
12
|
+
class FirstState extends State.make('nominal.first', S.Number) {}
|
|
13
|
+
class SecondState extends State.make('nominal.second', S.Number) {}
|
|
14
|
+
class FirstFamily extends StateFamily.make('nominal.firstFamily', S.String, S.Number) {}
|
|
15
|
+
class SecondFamily extends StateFamily.make('nominal.secondFamily', S.String, S.Number) {}
|
|
16
|
+
|
|
17
|
+
const FirstStateLive = State.live(FirstState, 0)
|
|
18
|
+
const SecondStateLive = State.live(SecondState, 0)
|
|
19
|
+
const FirstFamilyLive = StateFamily.live(FirstFamily, 0)
|
|
20
|
+
const SecondFamilyLive = StateFamily.live(SecondFamily, 0)
|
|
21
|
+
|
|
22
|
+
type StateLiveKeepsDefinitionIdentity = Expect<
|
|
23
|
+
Equal<Layer.Layer.Success<typeof FirstStateLive>, State.StateStore<'nominal.first', number>>
|
|
24
|
+
>
|
|
25
|
+
type FamilyLiveKeepsDefinitionIdentity = Expect<
|
|
26
|
+
Equal<
|
|
27
|
+
Layer.Layer.Success<typeof FirstFamilyLive>,
|
|
28
|
+
StateFamily.StateFamilyStore<'nominal.firstFamily', string, number>
|
|
29
|
+
>
|
|
30
|
+
>
|
|
31
|
+
type WrongStateDoesNotCloseRequirement = Expect<
|
|
32
|
+
Equal<
|
|
33
|
+
Exclude<State.StateStore<'nominal.first', number>, Layer.Layer.Success<typeof SecondStateLive>>,
|
|
34
|
+
State.StateStore<'nominal.first', number>
|
|
35
|
+
>
|
|
36
|
+
>
|
|
37
|
+
type WrongFamilyDoesNotCloseRequirement = Expect<
|
|
38
|
+
Equal<
|
|
39
|
+
Exclude<
|
|
40
|
+
StateFamily.StateFamilyStore<'nominal.firstFamily', string, number>,
|
|
41
|
+
Layer.Layer.Success<typeof SecondFamilyLive>
|
|
42
|
+
>,
|
|
43
|
+
StateFamily.StateFamilyStore<'nominal.firstFamily', string, number>
|
|
44
|
+
>
|
|
45
|
+
>
|
|
46
|
+
type ProvidedByKeepsStateIdentity = Expect<
|
|
47
|
+
Equal<ProvidedBy<readonly [typeof FirstState]>, State.StateStore<'nominal.first', number>>
|
|
48
|
+
>
|
|
49
|
+
type ProvidedByKeepsFamilyIdentity = Expect<
|
|
50
|
+
Equal<
|
|
51
|
+
ProvidedBy<readonly [typeof FirstFamily]>,
|
|
52
|
+
StateFamily.StateFamilyStore<'nominal.firstFamily', string, number>
|
|
53
|
+
>
|
|
54
|
+
>
|
|
55
|
+
|
|
56
|
+
type StateIdentityProofs = [
|
|
57
|
+
StateLiveKeepsDefinitionIdentity,
|
|
58
|
+
FamilyLiveKeepsDefinitionIdentity,
|
|
59
|
+
WrongStateDoesNotCloseRequirement,
|
|
60
|
+
WrongFamilyDoesNotCloseRequirement,
|
|
61
|
+
ProvidedByKeepsStateIdentity,
|
|
62
|
+
ProvidedByKeepsFamilyIdentity,
|
|
63
|
+
]
|
|
64
|
+
declare const stateIdentityProofs: StateIdentityProofs
|
|
65
|
+
|
|
66
|
+
void stateIdentityProofs
|
|
67
|
+
|
|
68
|
+
export {}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { expect, it } from '@effect/vitest'
|
|
2
|
+
import { Effect, Layer, Schema as S } from 'effect'
|
|
3
|
+
import { State } from '../index'
|
|
4
|
+
|
|
5
|
+
it.live('same-shaped states resolve their own stores', () => {
|
|
6
|
+
class First extends State.make('state.nominal.first', S.Number) {}
|
|
7
|
+
class Second extends State.make('state.nominal.second', S.Number) {}
|
|
8
|
+
const Live = Layer.mergeAll(State.live(First, 1), State.live(Second, 2))
|
|
9
|
+
|
|
10
|
+
return Effect.gen(function* () {
|
|
11
|
+
const first = yield* First
|
|
12
|
+
const second = yield* Second
|
|
13
|
+
|
|
14
|
+
expect(first).toBe(1)
|
|
15
|
+
expect(second).toBe(2)
|
|
16
|
+
}).pipe(Effect.provide(Live))
|
|
17
|
+
})
|