@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/scene/scene.ts
CHANGED
|
@@ -1,13 +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'
|
|
5
|
-
import type
|
|
6
|
-
EagerFeatureClass,
|
|
7
|
-
EngineServices,
|
|
8
|
-
FeatureRequirement,
|
|
9
|
-
ProvidedBy,
|
|
10
|
-
} from '../feature/feature'
|
|
11
|
+
import { type EagerFeatureClass, type FeatureRequirement, tree } from '../feature/feature'
|
|
11
12
|
import { CurrentSeedOverrides } from '../internal/seeds'
|
|
12
13
|
import type { SeedsOf } from '../state/stateGroup'
|
|
13
14
|
import type { Bus } from '../runtime/bus'
|
|
@@ -16,88 +17,257 @@ import {
|
|
|
16
17
|
type Instrumentation,
|
|
17
18
|
noopInstrumentation,
|
|
18
19
|
} from '../runtime/instrumentation'
|
|
20
|
+
import type { EngineServices } from '../runtime/loop'
|
|
19
21
|
|
|
20
|
-
export type MountedServices =
|
|
22
|
+
export type MountedServices = CompositionId<string> | Bus
|
|
21
23
|
|
|
22
|
-
type
|
|
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
|
|
23
57
|
|
|
24
58
|
export interface Scene<
|
|
25
59
|
C extends UiContract = UiContract,
|
|
26
60
|
S extends ReadonlyArray<unknown> = ReadonlyArray<unknown>,
|
|
27
|
-
> {
|
|
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> {
|
|
28
76
|
readonly kind: 'Scene'
|
|
29
|
-
readonly composition: CompositionClass<
|
|
30
|
-
readonly provide: ReadonlyArray<Layer.Layer<
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
|
36
120
|
}
|
|
37
121
|
|
|
38
|
-
export
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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)
|
|
42
134
|
}
|
|
43
135
|
|
|
44
|
-
export
|
|
45
|
-
|
|
46
|
-
config: SceneConfig,
|
|
47
|
-
): Scene<C, S> => ({
|
|
48
|
-
kind: 'Scene',
|
|
49
|
-
composition,
|
|
50
|
-
provide: config.provide,
|
|
51
|
-
...(config.boot !== undefined ? { boot: config.boot } : {}),
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
export interface FeatureSceneConfig<
|
|
55
|
-
Requires extends ReadonlyArray<FeatureRequirement>,
|
|
56
|
-
> {
|
|
57
|
-
readonly provide: Layer.Layer<EngineServices | ProvidedBy<Requires>, never, never>
|
|
136
|
+
export interface FeatureSceneConfig<OpenServices> {
|
|
137
|
+
readonly provide: Layer.Layer<EngineServices | OpenServices, never, never>
|
|
58
138
|
}
|
|
59
139
|
|
|
60
140
|
export const featureScene = <
|
|
141
|
+
FeatureIdentity,
|
|
61
142
|
P,
|
|
62
143
|
C extends UiContract,
|
|
63
144
|
ROut,
|
|
64
|
-
|
|
145
|
+
DirectRequires extends ReadonlyArray<FeatureRequirement>,
|
|
146
|
+
OpenServices,
|
|
147
|
+
S extends ReadonlyArray<unknown>,
|
|
148
|
+
N extends string,
|
|
149
|
+
Identity,
|
|
65
150
|
>(
|
|
66
|
-
feature: EagerFeatureClass<
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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,
|
|
75
169
|
})
|
|
76
170
|
}
|
|
77
171
|
|
|
78
|
-
|
|
79
|
-
|
|
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>>(
|
|
80
184
|
base: Scene<C, S>,
|
|
81
185
|
seeds: SeedsOf<S>,
|
|
82
|
-
): Scene<C, S>
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
186
|
+
): Scene<C, S>
|
|
187
|
+
export function seedScene<C extends UiContract, S extends ReadonlyArray<unknown>>(
|
|
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
|
+
}
|
|
86
206
|
|
|
87
|
-
export const
|
|
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>,
|
|
235
|
+
instrumentation: Instrumentation,
|
|
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>>(
|
|
88
242
|
base: Scene<C, S>,
|
|
89
243
|
instrumentation: Instrumentation,
|
|
90
|
-
): Scene<C, S>
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
+
}
|
|
95
254
|
|
|
96
|
-
export const sceneInstrumentation = (candidate:
|
|
255
|
+
export const sceneInstrumentation = (candidate: AnyScene): Instrumentation =>
|
|
97
256
|
candidate.instrumentation ?? noopInstrumentation
|
|
98
257
|
|
|
99
|
-
|
|
100
|
-
typeof candidate === '
|
|
101
|
-
candidate
|
|
102
|
-
|
|
103
|
-
candidate.kind === '
|
|
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, Schema as S } from
|
|
2
|
-
import { expect, it } from
|
|
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,90 +10,82 @@ import {
|
|
|
10
10
|
publish,
|
|
11
11
|
Reducer,
|
|
12
12
|
type RenderEnv,
|
|
13
|
-
type Scene,
|
|
14
13
|
scene,
|
|
15
14
|
seedScene,
|
|
16
15
|
State,
|
|
17
16
|
StateGroup,
|
|
18
17
|
Ui,
|
|
19
18
|
ui,
|
|
20
|
-
} from
|
|
21
|
-
import { CurrentSeedOverrides } from
|
|
19
|
+
} from '../index'
|
|
20
|
+
import { CurrentSeedOverrides } from '../internal/seeds'
|
|
22
21
|
|
|
23
|
-
class CountState extends State.make(
|
|
22
|
+
class CountState extends State.make('count', S.Number) {}
|
|
24
23
|
class MiniStates extends StateGroup.make(CountState) {}
|
|
25
24
|
|
|
26
|
-
class Bumped extends Event.make(
|
|
25
|
+
class Bumped extends Event.make('Bumped', S.Struct({})) {}
|
|
27
26
|
|
|
28
|
-
class BumpReducer extends Reducer.make(
|
|
27
|
+
class BumpReducer extends Reducer.make('BumpReducer', {
|
|
29
28
|
states: [CountState],
|
|
30
29
|
events: [Bumped],
|
|
31
30
|
}) {}
|
|
32
|
-
const BumpReducerLive = Reducer.live(BumpReducer, (n) => n + 1)
|
|
31
|
+
const BumpReducerLive = Reducer.live(BumpReducer, (n) => n + 1)
|
|
33
32
|
|
|
34
|
-
class CounterUi extends ui(
|
|
35
|
-
class Counter extends Composition.make(
|
|
36
|
-
title:
|
|
33
|
+
class CounterUi extends ui('Counter')<{ props: { count: number } }>() {}
|
|
34
|
+
class Counter extends Composition.make('Counter', {
|
|
35
|
+
title: 'Counter',
|
|
37
36
|
states: [MiniStates],
|
|
38
37
|
events: [Bumped],
|
|
39
38
|
ui: CounterUi,
|
|
40
|
-
}) {}
|
|
39
|
+
})<Counter>() {}
|
|
41
40
|
const CounterLive = Composition.live(Counter, function* () {
|
|
42
|
-
const count = yield* StateGroup.select(MiniStates,
|
|
43
|
-
return mount({ props: { count }, slots: {} })
|
|
44
|
-
})
|
|
41
|
+
const count = yield* StateGroup.select(MiniStates, 'count')
|
|
42
|
+
return mount({ props: { count }, slots: {} })
|
|
43
|
+
})
|
|
45
44
|
|
|
46
45
|
const presentations = provide(
|
|
47
46
|
CounterUi,
|
|
48
47
|
Ui.make(CounterUi, () => null),
|
|
49
|
-
)
|
|
50
|
-
const Views = CounterLive.pipe(Layer.provideMerge(presentations))
|
|
48
|
+
)
|
|
49
|
+
const Views = CounterLive.pipe(Layer.provideMerge(presentations))
|
|
51
50
|
const Logic = Layer.mergeAll(BumpReducerLive).pipe(
|
|
52
|
-
Layer.provideMerge(
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
);
|
|
56
|
-
const MiniApp = Views.pipe(Layer.provideMerge(Logic));
|
|
51
|
+
Layer.provideMerge(Layer.mergeAll(Engine, StateGroup.live(MiniStates, { count: 1 }))),
|
|
52
|
+
)
|
|
53
|
+
const MiniApp = Views.pipe(Layer.provideMerge(Logic))
|
|
57
54
|
|
|
58
|
-
const CounterScene = scene(Counter, { provide: [MiniApp] })
|
|
55
|
+
const CounterScene = scene(Counter, { provide: [MiniApp] })
|
|
59
56
|
|
|
60
57
|
const overlayRawSeeds = (
|
|
61
|
-
base:
|
|
58
|
+
base: typeof CounterScene,
|
|
62
59
|
seeds: Readonly<Record<string, unknown>>,
|
|
63
|
-
):
|
|
60
|
+
): typeof CounterScene => ({
|
|
64
61
|
...base,
|
|
65
62
|
provide: base.provide.map(Layer.locally(CurrentSeedOverrides, seeds)),
|
|
66
|
-
})
|
|
63
|
+
})
|
|
67
64
|
|
|
68
65
|
const headlessEnv: RenderEnv = {
|
|
69
66
|
props: {},
|
|
70
67
|
tracker: { add: () => {} },
|
|
71
|
-
}
|
|
68
|
+
}
|
|
72
69
|
|
|
73
70
|
const isCountProps = (props: unknown): props is { readonly count: number } =>
|
|
74
|
-
typeof props ===
|
|
75
|
-
props !== null &&
|
|
76
|
-
"count" in props &&
|
|
77
|
-
typeof props.count === "number";
|
|
71
|
+
typeof props === 'object' && props !== null && 'count' in props && typeof props.count === 'number'
|
|
78
72
|
|
|
79
|
-
const makeHarness = (s:
|
|
80
|
-
const layer = s.provide.reduce((a, b) => Layer.merge(a, b))
|
|
73
|
+
const makeHarness = (s: typeof CounterScene) => {
|
|
74
|
+
const layer = s.provide.reduce((a, b) => Layer.merge(a, b))
|
|
81
75
|
const read = Effect.flatMap(Counter.tag, (c) =>
|
|
82
76
|
Composition.render(c, headlessEnv).pipe(
|
|
83
77
|
Effect.flatMap((frame) => {
|
|
84
78
|
if (!isStructure(frame)) {
|
|
85
|
-
return Effect.dieMessage(
|
|
79
|
+
return Effect.dieMessage('Counter must render a Structure')
|
|
86
80
|
}
|
|
87
81
|
return isCountProps(frame.props)
|
|
88
82
|
? Effect.succeed(frame.props.count)
|
|
89
|
-
: Effect.dieMessage(
|
|
90
|
-
"Counter Structure props must include numeric count",
|
|
91
|
-
);
|
|
83
|
+
: Effect.dieMessage('Counter Structure props must include numeric count')
|
|
92
84
|
}),
|
|
93
85
|
),
|
|
94
|
-
)
|
|
95
|
-
return { layer, read }
|
|
96
|
-
}
|
|
86
|
+
)
|
|
87
|
+
return { layer, read }
|
|
88
|
+
}
|
|
97
89
|
|
|
98
90
|
const waitUntil = <A, R>(
|
|
99
91
|
read: Effect.Effect<A, never, R>,
|
|
@@ -103,54 +95,45 @@ const waitUntil = <A, R>(
|
|
|
103
95
|
Effect.flatMap(read, (a) =>
|
|
104
96
|
pred(a) || rounds <= 0
|
|
105
97
|
? Effect.succeed(a)
|
|
106
|
-
: Effect.flatMap(Effect.yieldNow(), () =>
|
|
107
|
-
|
|
108
|
-
),
|
|
109
|
-
);
|
|
98
|
+
: Effect.flatMap(Effect.yieldNow(), () => waitUntil(read, pred, rounds - 1)),
|
|
99
|
+
)
|
|
110
100
|
|
|
111
|
-
it.scopedLive(
|
|
112
|
-
const { layer, read } = makeHarness(seedScene(CounterScene, { count: 5 }))
|
|
101
|
+
it.scopedLive('a seeded scene boots its store from the override', () => {
|
|
102
|
+
const { layer, read } = makeHarness(seedScene(CounterScene, { count: 5 }))
|
|
113
103
|
return Effect.gen(function* () {
|
|
114
|
-
expect(yield* read).toBe(5)
|
|
115
|
-
}).pipe(Effect.provide(layer))
|
|
116
|
-
})
|
|
104
|
+
expect(yield* read).toBe(5)
|
|
105
|
+
}).pipe(Effect.provide(layer))
|
|
106
|
+
})
|
|
117
107
|
|
|
118
|
-
it.scopedLive(
|
|
119
|
-
const { layer, read } = makeHarness(seedScene(CounterScene, { count: 5 }))
|
|
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)
|
|
114
|
+
expect(next).toBe(6)
|
|
115
|
+
}).pipe(Effect.provide(layer))
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it.scopedLive('an absent key leaves the authored seed in place', () => {
|
|
119
|
+
const { layer, read } = makeHarness(overlayRawSeeds(CounterScene, { other: 1 }))
|
|
120
120
|
return Effect.gen(function* () {
|
|
121
|
-
expect(yield* read).toBe(
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
})
|
|
127
|
-
|
|
128
|
-
it.scopedLive("an absent key leaves the authored seed in place", () => {
|
|
129
|
-
const { layer, read } = makeHarness(
|
|
130
|
-
overlayRawSeeds(CounterScene, { other: 1 }),
|
|
131
|
-
);
|
|
121
|
+
expect(yield* read).toBe(1)
|
|
122
|
+
}).pipe(Effect.provide(layer))
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it.scopedLive('a schema-invalid override falls back to the authored seed', () => {
|
|
126
|
+
const { layer, read } = makeHarness(overlayRawSeeds(CounterScene, { count: 'not-a-number' }))
|
|
132
127
|
return Effect.gen(function* () {
|
|
133
|
-
expect(yield* read).toBe(1)
|
|
134
|
-
}).pipe(Effect.provide(layer))
|
|
135
|
-
})
|
|
136
|
-
|
|
137
|
-
it.scopedLive(
|
|
138
|
-
|
|
139
|
-
()
|
|
140
|
-
const { layer, read } = makeHarness(
|
|
141
|
-
overlayRawSeeds(CounterScene, { count: "not-a-number" }),
|
|
142
|
-
);
|
|
143
|
-
return Effect.gen(function* () {
|
|
144
|
-
expect(yield* read).toBe(1);
|
|
145
|
-
}).pipe(Effect.provide(layer));
|
|
146
|
-
},
|
|
147
|
-
);
|
|
148
|
-
|
|
149
|
-
it.scopedLive("seeding does not leak into the original scene", () => {
|
|
150
|
-
const seeded = makeHarness(seedScene(CounterScene, { count: 5 }));
|
|
151
|
-
const original = makeHarness(CounterScene);
|
|
128
|
+
expect(yield* read).toBe(1)
|
|
129
|
+
}).pipe(Effect.provide(layer))
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it.scopedLive('seeding does not leak into the original scene', () => {
|
|
133
|
+
const seeded = makeHarness(seedScene(CounterScene, { count: 5 }))
|
|
134
|
+
const original = makeHarness(CounterScene)
|
|
152
135
|
return Effect.gen(function* () {
|
|
153
|
-
expect(yield* seeded.read.pipe(Effect.provide(seeded.layer))).toBe(5)
|
|
154
|
-
expect(yield* original.read.pipe(Effect.provide(original.layer))).toBe(1)
|
|
155
|
-
})
|
|
156
|
-
})
|
|
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
|
+
})
|
|
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
|
+
})
|