@playfast/reform-proof 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 +12 -12
- package/package.json +18 -18
- package/src/engine.ts +1405 -337
- package/src/errors.ts +0 -14
- package/src/fingerprint.test.ts +24 -0
- package/src/fingerprint.ts +73 -0
- package/src/index.ts +240 -216
- package/src/nested-feature.test.ts +174 -0
- package/src/runner.ts +5 -17
- package/src/structure-events.test.ts +9 -14
- package/src/structure-locators.test.ts +19 -32
- package/src/typed-seams.test.ts +6 -14
package/src/engine.ts
CHANGED
|
@@ -1,104 +1,206 @@
|
|
|
1
1
|
import {
|
|
2
2
|
Array as Arr,
|
|
3
3
|
Cause,
|
|
4
|
+
Context,
|
|
4
5
|
Effect,
|
|
5
6
|
Layer,
|
|
6
7
|
ManagedRuntime,
|
|
7
8
|
Match,
|
|
8
9
|
MutableRef,
|
|
9
10
|
Option,
|
|
11
|
+
Order,
|
|
10
12
|
Record as Rec,
|
|
11
13
|
Ref,
|
|
12
14
|
} from 'effect'
|
|
13
15
|
import { yieldWrapGet } from 'effect/Utils'
|
|
14
16
|
import { AssertionFailed, UnknownAction, UnknownSlot } from './errors'
|
|
15
17
|
import {
|
|
16
|
-
|
|
17
|
-
CaptureSink,
|
|
18
|
-
type CaptureSinkApi,
|
|
18
|
+
type CapturedScene,
|
|
19
19
|
Composition,
|
|
20
20
|
type CompositionClass,
|
|
21
21
|
type CompositionService,
|
|
22
|
-
type
|
|
22
|
+
type FeatureLoadFailed,
|
|
23
23
|
isFeatureBinding,
|
|
24
|
+
isSlot,
|
|
24
25
|
isStructure,
|
|
25
|
-
|
|
26
|
+
makeAppRuntime,
|
|
26
27
|
publish,
|
|
27
|
-
type
|
|
28
|
-
type Scene,
|
|
28
|
+
type RuntimeHandle,
|
|
29
29
|
sceneInstrumentation,
|
|
30
|
-
type SlotChild,
|
|
31
|
-
type SlotClass,
|
|
32
30
|
type SlotFill,
|
|
33
31
|
type Structure,
|
|
34
32
|
type Trigger,
|
|
35
|
-
type UiCapture,
|
|
36
33
|
type UiContract,
|
|
37
34
|
} from '@playfast/reform'
|
|
35
|
+
import {
|
|
36
|
+
type AnyComposition,
|
|
37
|
+
type AnyFeatureBinding,
|
|
38
|
+
type AnyScene,
|
|
39
|
+
type AnySlot,
|
|
40
|
+
type AnySlotChild,
|
|
41
|
+
Bus,
|
|
42
|
+
type CapturedRender,
|
|
43
|
+
CaptureSink,
|
|
44
|
+
type CaptureSinkApi,
|
|
45
|
+
type Instrumentation,
|
|
46
|
+
noopInstrumentation,
|
|
47
|
+
} from '@playfast/reform/internal'
|
|
38
48
|
import { matchProps } from './index'
|
|
39
|
-
import type {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
49
|
+
import type {
|
|
50
|
+
DriveResult,
|
|
51
|
+
Facade,
|
|
52
|
+
Proof,
|
|
53
|
+
ProofCase,
|
|
54
|
+
ProductComposition,
|
|
55
|
+
ProofResult,
|
|
56
|
+
SlotFacade,
|
|
57
|
+
SlotFacadesOf,
|
|
58
|
+
StepFrame,
|
|
59
|
+
} from './index'
|
|
60
|
+
import { formatFingerprint } from './fingerprint'
|
|
61
|
+
|
|
62
|
+
type RenderRuntime = Pick<RuntimeHandle, 'dispatch' | 'instrumentation' | 'readOption'>
|
|
63
|
+
|
|
64
|
+
type HostRuntime = Pick<
|
|
65
|
+
RuntimeHandle,
|
|
66
|
+
'dispatch' | 'instrumentation' | 'mountAnyFeature' | 'readOption'
|
|
67
|
+
>
|
|
68
|
+
|
|
69
|
+
type RuntimeRootComposition<
|
|
70
|
+
Identifier,
|
|
71
|
+
P,
|
|
72
|
+
C extends UiContract,
|
|
73
|
+
S extends ReadonlyArray<unknown>,
|
|
74
|
+
N extends string,
|
|
75
|
+
Identity,
|
|
76
|
+
> = CompositionClass<P, C, S, N, Identity> & {
|
|
77
|
+
readonly tag: Context.Tag<Identifier, CompositionService<P, C>>
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
interface MountedFacadeErasure {
|
|
81
|
+
readonly props: Effect.Effect<unknown, never, never>
|
|
82
|
+
readonly expectProps: (partial: unknown) => Effect.Effect<void, never, never>
|
|
83
|
+
readonly actions: Readonly<
|
|
84
|
+
Record<string, (payload: unknown) => Effect.Effect<unknown, never, never>>
|
|
85
|
+
>
|
|
86
|
+
readonly slots: Readonly<Record<string, MountedSlotFacadeErasure>>
|
|
87
|
+
readonly frame: Effect.Effect<void, never, never>
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
interface MountedSlotFacadeErasure extends MountedFacadeErasure {
|
|
91
|
+
readonly first: Effect.Effect<MountedFacadeErasure, never, never>
|
|
92
|
+
readonly at: (index: number) => Effect.Effect<MountedFacadeErasure, never, never>
|
|
93
|
+
readonly all: Effect.Effect<ReadonlyArray<MountedFacadeErasure>, never, never>
|
|
94
|
+
readonly byKey: (key: string) => Effect.Effect<MountedFacadeErasure, never, never>
|
|
95
|
+
readonly where: (
|
|
96
|
+
predicate: (props: unknown) => boolean,
|
|
97
|
+
) => Effect.Effect<ReadonlyArray<MountedFacadeErasure>, never, never>
|
|
98
|
+
readonly count: Effect.Effect<number, never, never>
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
interface MountedFacadeHandleErasure {
|
|
102
|
+
readonly facade: {
|
|
103
|
+
readonly props: Effect.Effect<unknown, never, never>
|
|
104
|
+
readonly expectProps: (partial: never) => Effect.Effect<void, never, never>
|
|
105
|
+
readonly actions: object
|
|
106
|
+
readonly slots: object
|
|
107
|
+
readonly frame: Effect.Effect<void, never, never>
|
|
108
|
+
}
|
|
109
|
+
readonly settle: Effect.Effect<void, never, never>
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
type Settlement = Effect.Effect<void, never, never>
|
|
113
|
+
type SettleBoundary = (settlement: Settlement) => Settlement
|
|
114
|
+
|
|
115
|
+
const directSettle: SettleBoundary = (settlement) => settlement
|
|
116
|
+
|
|
117
|
+
// Cooperative yields flush synchronous fibers; the real-time sleep prevents a
|
|
118
|
+
// genuinely delayed flow from looking stable between renders.
|
|
61
119
|
const SETTLE_DRAIN = 30
|
|
62
120
|
const SETTLE_STEP = '1 milli'
|
|
63
121
|
const SETTLE_MAX_RENDERS = 100
|
|
64
122
|
|
|
65
|
-
/** Cooperatively run forked engine fibers to a fixpoint without advancing real time. */
|
|
66
123
|
const settleDrain = Effect.yieldNow().pipe(Effect.repeatN(SETTLE_DRAIN))
|
|
67
124
|
|
|
68
|
-
// A keyed view backed by a getter — the only place dynamic keys are needed.
|
|
69
125
|
const keyed = <V>(get: (key: string) => V): Record<string, V> => {
|
|
70
126
|
const target: Record<string, V> = Object.create(null)
|
|
71
127
|
return new Proxy(target, { get: (_target, key) => get(String(key)) })
|
|
72
128
|
}
|
|
73
129
|
|
|
74
|
-
/** Surface an unknown failure/defect as the message string a result DTO carries. */
|
|
75
130
|
const messageOf = (error: unknown): string =>
|
|
76
131
|
error instanceof Error ? error.message : String(error)
|
|
77
132
|
|
|
78
|
-
const uiNameOf = (comp:
|
|
133
|
+
const uiNameOf = (comp: AnyComposition): string => comp.manifest.ui.manifest.name
|
|
79
134
|
|
|
80
|
-
//
|
|
81
|
-
// `UiContract` boundary the contract's event map is `Record<never, never>`, so
|
|
82
|
-
// each value is `never` — assignable to `Trigger<unknown>` without a cast — and an
|
|
83
|
-
// omitted `events` defaults to the empty map. This is the structure-path analog of
|
|
84
|
-
// the view path's `capture.events`.
|
|
135
|
+
// UiContract erases event values to never, so this boundary needs no assertion.
|
|
85
136
|
const eventsOf = (structure: Structure<UiContract>): Record<string, Trigger<unknown>> => ({
|
|
86
137
|
...structure.events,
|
|
87
138
|
})
|
|
88
139
|
|
|
89
|
-
|
|
90
|
-
|
|
140
|
+
interface ExpectedProps {
|
|
141
|
+
readonly actual: unknown
|
|
142
|
+
readonly expected: unknown
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const expectMatchingProps = ({
|
|
146
|
+
actual,
|
|
147
|
+
expected,
|
|
148
|
+
}: ExpectedProps): Effect.Effect<void, never, never> =>
|
|
149
|
+
Option.match(Option.fromNullable(matchProps({ actual, expected })), {
|
|
150
|
+
onNone: () => Effect.void,
|
|
151
|
+
onSome: (detail) => Effect.dieMessage(new AssertionFailed({ detail }).message),
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
const invalidCompositionProps = (composition: AnyComposition): string =>
|
|
155
|
+
`reform-proof: invalid props for composition ${composition.manifest.name}`
|
|
156
|
+
|
|
157
|
+
const renderCompositionRuntime = (
|
|
158
|
+
runtime: RenderRuntime,
|
|
159
|
+
composition: AnyComposition,
|
|
160
|
+
props: unknown,
|
|
161
|
+
): Structure<UiContract> =>
|
|
162
|
+
composition.capture(
|
|
163
|
+
<P, C extends UiContract, S extends ReadonlyArray<unknown>, N extends string, Identity>(
|
|
164
|
+
exact: CompositionClass<P, C, S, N, Identity>,
|
|
165
|
+
): Structure<UiContract> => {
|
|
166
|
+
const resolved = Option.flatMap(exact.parseProps(props), (parsed) =>
|
|
167
|
+
Option.map(runtime.readOption(exact.tag), (service) => ({
|
|
168
|
+
parsed,
|
|
169
|
+
service,
|
|
170
|
+
})),
|
|
171
|
+
)
|
|
172
|
+
return Option.match(resolved, {
|
|
173
|
+
onNone: () => Effect.runSync(Effect.dieMessage(invalidCompositionProps(composition))),
|
|
174
|
+
onSome: ({ parsed, service }) => {
|
|
175
|
+
const frame = Effect.runSync(
|
|
176
|
+
Composition.render(service, {
|
|
177
|
+
props: parsed,
|
|
178
|
+
tracker: { add: () => {} },
|
|
179
|
+
}),
|
|
180
|
+
)
|
|
181
|
+
if (isStructure(frame)) {
|
|
182
|
+
return frame
|
|
183
|
+
}
|
|
184
|
+
return Effect.runSync(
|
|
185
|
+
Effect.dieMessage(
|
|
186
|
+
`reform-proof: composition ${composition.manifest.name} did not return a Structure`,
|
|
187
|
+
),
|
|
188
|
+
)
|
|
189
|
+
},
|
|
190
|
+
})
|
|
191
|
+
},
|
|
192
|
+
)
|
|
193
|
+
|
|
91
194
|
export interface Sink {
|
|
92
195
|
readonly api: CaptureSinkApi
|
|
93
|
-
readonly captures: ReadonlyArray<
|
|
94
|
-
/** The scene's profiling hooks — render spans record here (noop unless `profileScene`d). */
|
|
196
|
+
readonly captures: ReadonlyArray<CapturedRender>
|
|
95
197
|
readonly instrumentation: Instrumentation
|
|
96
198
|
reset(): void
|
|
97
199
|
}
|
|
98
200
|
|
|
99
201
|
export const makeSink = (instrumentation: Instrumentation = noopInstrumentation): Sink => {
|
|
100
202
|
// CaptureSink.record is synchronous, so this uses MutableRef rather than Ref.
|
|
101
|
-
const captures = MutableRef.make<
|
|
203
|
+
const captures = MutableRef.make<CapturedRender[]>([])
|
|
102
204
|
return {
|
|
103
205
|
api: {
|
|
104
206
|
record: (capture) => {
|
|
@@ -115,72 +217,196 @@ export const makeSink = (instrumentation: Instrumentation = noopInstrumentation)
|
|
|
115
217
|
}
|
|
116
218
|
}
|
|
117
219
|
|
|
118
|
-
export type
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
220
|
+
export type MountedSlotFacadesOf<C extends UiContract> = SlotFacadesOf<C, never>
|
|
221
|
+
|
|
222
|
+
export type MountedFacade<C extends UiContract> = Facade<C, never>
|
|
223
|
+
|
|
224
|
+
export type MountedSlotFacade<C extends UiContract> = SlotFacade<C, never>
|
|
225
|
+
|
|
226
|
+
export const proofLayer = <
|
|
227
|
+
C extends UiContract,
|
|
228
|
+
S extends ReadonlyArray<unknown>,
|
|
229
|
+
Services,
|
|
230
|
+
P,
|
|
231
|
+
N extends string,
|
|
232
|
+
Identity,
|
|
233
|
+
>(
|
|
234
|
+
scene: CapturedScene<C, S, Services, P, N, Identity>,
|
|
235
|
+
sink: Sink,
|
|
236
|
+
): Layer.Layer<Services | CaptureSink, never, never> => {
|
|
237
|
+
const sceneLayer = scene.provide.reduce((merged, layer) => Layer.merge(merged, layer))
|
|
238
|
+
return Layer.merge(sceneLayer, Layer.succeed(CaptureSink, sink.api))
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const missingRuntimeBus = (): never =>
|
|
242
|
+
Effect.runSync(Effect.dieMessage('reform-proof: scene does not provide Bus'))
|
|
243
|
+
|
|
244
|
+
const renderRuntimeFromContext = <Services>(
|
|
245
|
+
context: Context.Context<Services>,
|
|
246
|
+
instrumentation: Instrumentation,
|
|
247
|
+
): RenderRuntime => {
|
|
248
|
+
const bus = Context.getOption(context, Bus)
|
|
249
|
+
return {
|
|
250
|
+
dispatch: (priority, event) =>
|
|
251
|
+
Option.match(bus, {
|
|
252
|
+
onNone: missingRuntimeBus,
|
|
253
|
+
onSome: (service) =>
|
|
254
|
+
Effect.runSync(publish(priority, event).pipe(Effect.provideService(Bus, service))),
|
|
255
|
+
}),
|
|
256
|
+
instrumentation,
|
|
257
|
+
readOption: (tag) => Context.getOption(context, tag),
|
|
258
|
+
}
|
|
138
259
|
}
|
|
139
260
|
|
|
140
|
-
/** A composition queued to render, with the props its parent handed it. On the
|
|
141
|
-
* structure path it also carries the slot-fill `key` it was mounted under, so the
|
|
142
|
-
* facade can select a child by key (`byKey`); the legacy Node path omits it. */
|
|
143
261
|
interface Mounted {
|
|
144
|
-
readonly comp:
|
|
262
|
+
readonly comp: AnyComposition
|
|
145
263
|
readonly props: unknown
|
|
264
|
+
readonly ref: NodeRef
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
interface SlotInstanceRef {
|
|
268
|
+
readonly name: string
|
|
269
|
+
readonly index: number
|
|
146
270
|
readonly key: Option.Option<string>
|
|
271
|
+
readonly keyOccurrence: number
|
|
147
272
|
}
|
|
148
273
|
|
|
149
|
-
/** A rendered node addressed by its composition ui-name and render index. */
|
|
150
274
|
interface NodeRef {
|
|
151
275
|
readonly name: string
|
|
152
|
-
readonly
|
|
276
|
+
readonly parent: Option.Option<NodeRef>
|
|
277
|
+
readonly slot: Option.Option<SlotInstanceRef>
|
|
153
278
|
}
|
|
154
279
|
|
|
155
|
-
|
|
280
|
+
interface NodeCapture {
|
|
281
|
+
readonly ref: NodeRef
|
|
282
|
+
readonly capture: CapturedRender
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
interface CaptureLookup {
|
|
286
|
+
readonly captures: ReadonlyArray<NodeCapture>
|
|
287
|
+
record(ref: NodeRef, capture: CapturedRender): void
|
|
288
|
+
reset(): void
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const makeCaptureLookup = (): CaptureLookup => {
|
|
292
|
+
const captures = MutableRef.make<ReadonlyArray<NodeCapture>>([])
|
|
293
|
+
return {
|
|
294
|
+
get captures() {
|
|
295
|
+
return MutableRef.get(captures)
|
|
296
|
+
},
|
|
297
|
+
record: (ref, capture) => {
|
|
298
|
+
MutableRef.update(captures, (current) => [...current, { ref, capture }])
|
|
299
|
+
},
|
|
300
|
+
reset: () => {
|
|
301
|
+
MutableRef.set(captures, [])
|
|
302
|
+
},
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const sameSlotInstance = (expected: SlotInstanceRef, actual: SlotInstanceRef): boolean =>
|
|
307
|
+
expected.name === actual.name &&
|
|
308
|
+
Option.match(expected.key, {
|
|
309
|
+
onNone: () => expected.index === actual.index,
|
|
310
|
+
onSome: (key) =>
|
|
311
|
+
expected.keyOccurrence === actual.keyOccurrence && Option.contains(actual.key, key),
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
const sameNodeRef = (expected: NodeRef, actual: NodeRef): boolean =>
|
|
315
|
+
expected.name === actual.name &&
|
|
316
|
+
Option.match(expected.parent, {
|
|
317
|
+
onNone: () => Option.isNone(actual.parent) && Option.isNone(actual.slot),
|
|
318
|
+
onSome: (expectedParent) =>
|
|
319
|
+
Option.isSome(actual.parent) &&
|
|
320
|
+
Option.isSome(expected.slot) &&
|
|
321
|
+
Option.isSome(actual.slot) &&
|
|
322
|
+
sameNodeRef(expectedParent, actual.parent.value) &&
|
|
323
|
+
sameSlotInstance(expected.slot.value, actual.slot.value),
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
const rootRef = (root: AnyComposition): NodeRef => ({
|
|
327
|
+
name: uiNameOf(root),
|
|
328
|
+
parent: Option.none(),
|
|
329
|
+
slot: Option.none(),
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
const nodeCaptureFor = (lookup: CaptureLookup, ref: NodeRef): Option.Option<CapturedRender> =>
|
|
333
|
+
Option.map(
|
|
334
|
+
Arr.findFirst(lookup.captures, (entry) => sameNodeRef(ref, entry.ref)),
|
|
335
|
+
(entry) => entry.capture,
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
const slotCapturesFor = (
|
|
339
|
+
lookup: CaptureLookup,
|
|
340
|
+
parent: NodeRef,
|
|
341
|
+
slotName: string,
|
|
342
|
+
): ReadonlyArray<NodeCapture> =>
|
|
343
|
+
lookup.captures.filter(
|
|
344
|
+
(entry) =>
|
|
345
|
+
Option.isSome(entry.ref.parent) &&
|
|
346
|
+
Option.isSome(entry.ref.slot) &&
|
|
347
|
+
sameNodeRef(parent, entry.ref.parent.value) &&
|
|
348
|
+
entry.ref.slot.value.name === slotName,
|
|
349
|
+
)
|
|
350
|
+
|
|
156
351
|
interface SettleProgress {
|
|
157
352
|
readonly previous: string
|
|
158
353
|
readonly remaining: number
|
|
159
354
|
readonly stable: boolean
|
|
160
355
|
}
|
|
161
356
|
|
|
162
|
-
|
|
357
|
+
const ensureSettled = (progress: SettleProgress): Effect.Effect<void, never, never> => {
|
|
358
|
+
if (progress.stable) {
|
|
359
|
+
return Effect.void
|
|
360
|
+
}
|
|
361
|
+
return Effect.dieMessage(`reform-proof: tree did not settle after ${SETTLE_MAX_RENDERS} renders`)
|
|
362
|
+
}
|
|
363
|
+
|
|
163
364
|
interface PumpStep {
|
|
164
365
|
readonly input: unknown
|
|
165
366
|
readonly index: number
|
|
166
367
|
}
|
|
167
368
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
const
|
|
175
|
-
|
|
369
|
+
const childComposition = (child: AnySlotChild): AnyComposition =>
|
|
370
|
+
isFeatureBinding(child) ? child.capture<AnyComposition>((binding) => binding.composition) : child
|
|
371
|
+
|
|
372
|
+
const isUnknownRecord = (candidate: unknown): candidate is Record<string, unknown> =>
|
|
373
|
+
typeof candidate === 'object' && candidate !== null && !Array.isArray(candidate)
|
|
374
|
+
|
|
375
|
+
const slotsOf = (comp: AnyComposition): Record<string, AnySlot> =>
|
|
376
|
+
comp.capture<Record<string, AnySlot>>((exact) => {
|
|
377
|
+
if (!('slots' in exact.manifest)) {
|
|
378
|
+
return {}
|
|
379
|
+
}
|
|
380
|
+
const candidate: unknown = exact.manifest.slots
|
|
381
|
+
if (!isUnknownRecord(candidate)) {
|
|
382
|
+
return {}
|
|
383
|
+
}
|
|
384
|
+
return Rec.fromEntries(
|
|
385
|
+
Rec.toEntries(candidate).flatMap(
|
|
386
|
+
([name, slotDefinition]): ReadonlyArray<readonly [string, AnySlot]> =>
|
|
387
|
+
isSlot(slotDefinition) ? [[name, slotDefinition]] : [],
|
|
388
|
+
),
|
|
389
|
+
)
|
|
390
|
+
})
|
|
176
391
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
392
|
+
interface RuntimeSlotRead {
|
|
393
|
+
readonly runtime: RenderRuntime
|
|
394
|
+
readonly slotDefinition: AnySlot
|
|
395
|
+
readonly parent: AnyComposition
|
|
396
|
+
}
|
|
180
397
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
398
|
+
const readRuntimeSlotChild = ({ runtime, slotDefinition, parent }: RuntimeSlotRead): AnySlotChild =>
|
|
399
|
+
slotDefinition.capture<AnySlotChild>((exact) =>
|
|
400
|
+
Option.match(runtime.readOption(exact.provider), {
|
|
401
|
+
onNone: () =>
|
|
402
|
+
Effect.runSync(
|
|
403
|
+
Effect.dieMessage(`reform-proof: slot provider missing in ${uiNameOf(parent)}`),
|
|
404
|
+
),
|
|
405
|
+
onSome: (child) => child,
|
|
406
|
+
}),
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
// UiContract erases per-slot fill types here, so recover them by discriminant.
|
|
184
410
|
const isSlotFill = (candidate: unknown): candidate is SlotFill<unknown> =>
|
|
185
411
|
typeof candidate === 'object' &&
|
|
186
412
|
candidate !== null &&
|
|
@@ -189,156 +415,207 @@ const isSlotFill = (candidate: unknown): candidate is SlotFill<unknown> =>
|
|
|
189
415
|
|
|
190
416
|
const structureFills = (structure: Structure<UiContract>): Record<string, SlotFill<unknown>> =>
|
|
191
417
|
Rec.fromEntries(
|
|
192
|
-
Rec.toEntries(structure.slots).flatMap(
|
|
193
|
-
|
|
418
|
+
Rec.toEntries(structure.slots).flatMap(
|
|
419
|
+
([slotName, fillValue]): ReadonlyArray<readonly [string, SlotFill<unknown>]> =>
|
|
420
|
+
isSlotFill(fillValue) ? [[slotName, fillValue]] : [],
|
|
194
421
|
),
|
|
195
422
|
)
|
|
196
423
|
|
|
197
|
-
// Slot bindings (`provide(slot, composition)`) are static, so resolve the whole
|
|
198
|
-
// child tree once, up front — keeping `renderTree` and slot navigation
|
|
199
|
-
// synchronous (no nested runtime drive).
|
|
200
|
-
const collectBindings = Effect.fn('collectBindings')(function* (
|
|
201
|
-
root: CompositionClass<unknown>,
|
|
202
|
-
): Effect.fn.Return<Map<SlotClass, CompositionClass<unknown>>, never, SlotChild> {
|
|
203
|
-
const bindings = new Map<SlotClass, CompositionClass<unknown>>()
|
|
204
|
-
const walk: (comp: CompositionClass<unknown>) => Effect.Effect<void, never, SlotChild> = Effect.fn(
|
|
205
|
-
'collectBindings.walk',
|
|
206
|
-
)(function* (comp: CompositionClass<unknown>): Effect.fn.Return<void, never, SlotChild> {
|
|
207
|
-
yield* Effect.forEach(Rec.values(slotsOf(comp)), (slotClass) =>
|
|
208
|
-
Effect.gen(function* () {
|
|
209
|
-
if (bindings.has(slotClass)) {
|
|
210
|
-
return
|
|
211
|
-
}
|
|
212
|
-
const child = childComposition(yield* slotClass.tag)
|
|
213
|
-
bindings.set(slotClass, child)
|
|
214
|
-
yield* walk(child)
|
|
215
|
-
}),
|
|
216
|
-
)
|
|
217
|
-
})
|
|
218
|
-
yield* walk(root)
|
|
219
|
-
return bindings
|
|
220
|
-
})
|
|
221
|
-
|
|
222
|
-
// Expand one slot fill into the children to render next. `Each` mounts one child
|
|
223
|
-
// per keyed item with that item's props and key; `One` mounts a single child
|
|
224
|
-
// under a synthesized singleton key; `Absent` mounts nothing.
|
|
225
424
|
const enqueueFill = (
|
|
425
|
+
parent: NodeRef,
|
|
226
426
|
slotName: string,
|
|
227
427
|
fill: SlotFill<unknown>,
|
|
228
|
-
child:
|
|
428
|
+
child: AnyComposition,
|
|
229
429
|
): ReadonlyArray<Mounted> =>
|
|
230
430
|
Match.value(fill).pipe(
|
|
231
431
|
Match.when({ _tag: 'Each' }, (each) =>
|
|
232
|
-
each.items.map((entry) => ({
|
|
432
|
+
each.items.map((entry, index) => ({
|
|
233
433
|
comp: child,
|
|
234
434
|
props: entry.props,
|
|
235
|
-
|
|
435
|
+
ref: {
|
|
436
|
+
name: uiNameOf(child),
|
|
437
|
+
parent: Option.some(parent),
|
|
438
|
+
slot: Option.some({
|
|
439
|
+
name: slotName,
|
|
440
|
+
index,
|
|
441
|
+
key: Option.some(entry.key),
|
|
442
|
+
keyOccurrence: each.items
|
|
443
|
+
.slice(0, index)
|
|
444
|
+
.filter((candidate) => candidate.key === entry.key).length,
|
|
445
|
+
}),
|
|
446
|
+
},
|
|
236
447
|
})),
|
|
237
448
|
),
|
|
238
449
|
Match.when({ _tag: 'One' }, (oneFill) => [
|
|
239
|
-
{
|
|
450
|
+
{
|
|
451
|
+
comp: child,
|
|
452
|
+
props: oneFill.props,
|
|
453
|
+
ref: {
|
|
454
|
+
name: uiNameOf(child),
|
|
455
|
+
parent: Option.some(parent),
|
|
456
|
+
slot: Option.some({
|
|
457
|
+
name: slotName,
|
|
458
|
+
index: 0,
|
|
459
|
+
key: Option.some(`${slotName}.0`),
|
|
460
|
+
keyOccurrence: 0,
|
|
461
|
+
}),
|
|
462
|
+
},
|
|
463
|
+
},
|
|
240
464
|
]),
|
|
241
465
|
Match.when({ _tag: 'Absent' }, () => []),
|
|
242
466
|
Match.exhaustive,
|
|
243
467
|
)
|
|
244
468
|
|
|
245
|
-
// Consume a `Structure` frame: record computed props and event triggers, then
|
|
246
|
-
// enqueue one child per slot fill with the per-item props the fill carries.
|
|
247
469
|
const driveStructure = (
|
|
248
|
-
comp:
|
|
470
|
+
comp: AnyComposition,
|
|
249
471
|
structure: Structure<UiContract>,
|
|
250
|
-
|
|
472
|
+
ref: NodeRef,
|
|
251
473
|
sink: Sink,
|
|
252
|
-
|
|
474
|
+
lookup: CaptureLookup,
|
|
475
|
+
bindings: ReadonlyMap<AnySlot, AnyComposition>,
|
|
253
476
|
): ReadonlyArray<Mounted> => {
|
|
254
|
-
|
|
477
|
+
const key = Option.flatMap(ref.slot, (instance) => instance.key)
|
|
478
|
+
const capture: CapturedRender = {
|
|
255
479
|
name: uiNameOf(comp),
|
|
256
480
|
props: structure.props,
|
|
257
481
|
events: eventsOf(structure),
|
|
258
482
|
...(Option.isSome(key) ? { key: key.value } : {}),
|
|
259
|
-
}
|
|
483
|
+
}
|
|
484
|
+
sink.api.record(capture)
|
|
485
|
+
lookup.record(ref, capture)
|
|
260
486
|
const fills = structureFills(structure)
|
|
261
487
|
return Rec.toEntries(slotsOf(comp)).flatMap(([slotName, slotClass]) => {
|
|
262
488
|
const child = bindings.get(slotClass)
|
|
263
489
|
const fill = fills[slotName]
|
|
264
|
-
return child === undefined || fill === undefined ? [] : enqueueFill(slotName, fill, child)
|
|
490
|
+
return child === undefined || fill === undefined ? [] : enqueueFill(ref, slotName, fill, child)
|
|
265
491
|
})
|
|
266
492
|
}
|
|
267
493
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
494
|
+
const fingerprint = (lookup: CaptureLookup): string =>
|
|
495
|
+
formatFingerprint(lookup.captures.map(({ ref, capture }) => [ref, capture.props]))
|
|
496
|
+
|
|
497
|
+
export function makeFacade<
|
|
498
|
+
C extends UiContract,
|
|
499
|
+
S extends ReadonlyArray<unknown>,
|
|
500
|
+
Services,
|
|
501
|
+
P,
|
|
502
|
+
N extends string,
|
|
503
|
+
Identity,
|
|
504
|
+
>(
|
|
505
|
+
runtime: ManagedRuntime.ManagedRuntime<Services | CaptureSink, never>,
|
|
506
|
+
scene: CapturedScene<C, S, Services, P, N, Identity>,
|
|
507
|
+
sink: Sink,
|
|
508
|
+
dispatched: Set<string>,
|
|
509
|
+
): {
|
|
510
|
+
readonly facade: Facade<C, never>
|
|
511
|
+
readonly settle: Effect.Effect<void, never, never>
|
|
512
|
+
} {
|
|
513
|
+
const context = runtime.runSync(Effect.context<Services | CaptureSink>())
|
|
514
|
+
return makeMountedFacade(
|
|
515
|
+
renderRuntimeFromContext(context, sink.instrumentation),
|
|
516
|
+
scene.composition,
|
|
517
|
+
sink,
|
|
518
|
+
dispatched,
|
|
519
|
+
(settlement) => Effect.promise(() => runtime.runPromise(settlement)),
|
|
520
|
+
)
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const collectRuntimeBindings = (
|
|
524
|
+
root: AnyComposition,
|
|
525
|
+
runtime: RenderRuntime,
|
|
526
|
+
): Map<AnySlot, AnyComposition> => {
|
|
527
|
+
const bindings = new Map<AnySlot, AnyComposition>()
|
|
528
|
+
const walk = (comp: AnyComposition): void => {
|
|
529
|
+
Rec.values(slotsOf(comp)).forEach((slotClass) => {
|
|
530
|
+
if (bindings.has(slotClass)) {
|
|
531
|
+
return
|
|
532
|
+
}
|
|
533
|
+
const child = childComposition(
|
|
534
|
+
readRuntimeSlotChild({
|
|
535
|
+
runtime,
|
|
536
|
+
slotDefinition: slotClass,
|
|
537
|
+
parent: comp,
|
|
538
|
+
}),
|
|
539
|
+
)
|
|
540
|
+
bindings.set(slotClass, child)
|
|
541
|
+
walk(child)
|
|
542
|
+
})
|
|
543
|
+
}
|
|
544
|
+
walk(root)
|
|
545
|
+
return bindings
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const renderMountedRuntime = (
|
|
549
|
+
runtime: RenderRuntime,
|
|
272
550
|
mounted: Mounted,
|
|
273
|
-
bindings: ReadonlyMap<
|
|
551
|
+
bindings: ReadonlyMap<AnySlot, AnyComposition>,
|
|
274
552
|
sink: Sink,
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
const env: RenderEnv = { props: mounted.props, tracker: { add: () => {} } }
|
|
553
|
+
lookup: CaptureLookup,
|
|
554
|
+
): ReadonlyArray<Mounted> => {
|
|
278
555
|
const endRenderSpan = sink.instrumentation.uiRendered(uiNameOf(mounted.comp))
|
|
279
|
-
const frame =
|
|
556
|
+
const frame = renderCompositionRuntime(runtime, mounted.comp, mounted.props)
|
|
280
557
|
endRenderSpan()
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
`reform-proof: composition ${uiNameOf(mounted.comp)} did not return a Structure`,
|
|
284
|
-
)
|
|
285
|
-
}
|
|
286
|
-
return driveStructure(mounted.comp, frame, mounted.key, sink, bindings)
|
|
287
|
-
})
|
|
558
|
+
return driveStructure(mounted.comp, frame, mounted.ref, sink, lookup, bindings)
|
|
559
|
+
}
|
|
288
560
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
const renderLevel: (
|
|
561
|
+
const renderLevelRuntime = (
|
|
562
|
+
runtime: RenderRuntime,
|
|
292
563
|
frontier: ReadonlyArray<Mounted>,
|
|
293
|
-
bindings: ReadonlyMap<
|
|
564
|
+
bindings: ReadonlyMap<AnySlot, AnyComposition>,
|
|
294
565
|
sink: Sink,
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
bindings: ReadonlyMap<SlotClass, CompositionClass<unknown>>,
|
|
298
|
-
sink: Sink,
|
|
299
|
-
): Effect.fn.Return<void, never, CompositionService> {
|
|
566
|
+
lookup: CaptureLookup,
|
|
567
|
+
): void => {
|
|
300
568
|
if (frontier.length === 0) {
|
|
301
569
|
return
|
|
302
570
|
}
|
|
303
|
-
const levels =
|
|
304
|
-
|
|
305
|
-
|
|
571
|
+
const levels = frontier.map((mounted) =>
|
|
572
|
+
renderMountedRuntime(runtime, mounted, bindings, sink, lookup),
|
|
573
|
+
)
|
|
574
|
+
renderLevelRuntime(runtime, levels.flat(), bindings, sink, lookup)
|
|
575
|
+
}
|
|
306
576
|
|
|
307
|
-
const
|
|
308
|
-
|
|
309
|
-
|
|
577
|
+
const renderTreeRuntime = (
|
|
578
|
+
runtime: RenderRuntime,
|
|
579
|
+
root: AnyComposition,
|
|
580
|
+
bindings: ReadonlyMap<AnySlot, AnyComposition>,
|
|
310
581
|
sink: Sink,
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
582
|
+
lookup: CaptureLookup,
|
|
583
|
+
): Effect.Effect<void, never, never> =>
|
|
584
|
+
Effect.sync(() => {
|
|
585
|
+
sink.reset()
|
|
586
|
+
lookup.reset()
|
|
587
|
+
renderLevelRuntime(
|
|
588
|
+
runtime,
|
|
589
|
+
[{ comp: root, props: {}, ref: rootRef(root) }],
|
|
590
|
+
bindings,
|
|
591
|
+
sink,
|
|
592
|
+
lookup,
|
|
593
|
+
)
|
|
594
|
+
})
|
|
324
595
|
|
|
325
|
-
const
|
|
326
|
-
|
|
327
|
-
|
|
596
|
+
const settleTreeRuntime = Effect.fn('settleTreeRuntime')(function* (
|
|
597
|
+
runtime: RenderRuntime,
|
|
598
|
+
root: AnyComposition,
|
|
599
|
+
bindings: ReadonlyMap<AnySlot, AnyComposition>,
|
|
328
600
|
sink: Sink,
|
|
329
|
-
|
|
601
|
+
lookup: CaptureLookup,
|
|
602
|
+
): Effect.fn.Return<void, never, never> {
|
|
330
603
|
yield* settleDrain
|
|
331
|
-
yield*
|
|
332
|
-
yield* Effect.iterate(
|
|
333
|
-
{
|
|
604
|
+
yield* renderTreeRuntime(runtime, root, bindings, sink, lookup)
|
|
605
|
+
const progress = yield* Effect.iterate(
|
|
606
|
+
{
|
|
607
|
+
previous: fingerprint(lookup),
|
|
608
|
+
remaining: SETTLE_MAX_RENDERS,
|
|
609
|
+
stable: false,
|
|
610
|
+
},
|
|
334
611
|
{
|
|
335
612
|
while: (progress: SettleProgress) => !progress.stable && progress.remaining > 0,
|
|
336
613
|
body: (progress) =>
|
|
337
614
|
Effect.gen(function* () {
|
|
338
615
|
yield* settleDrain
|
|
339
616
|
yield* Effect.sleep(SETTLE_STEP)
|
|
340
|
-
yield*
|
|
341
|
-
const current = fingerprint(
|
|
617
|
+
yield* renderTreeRuntime(runtime, root, bindings, sink, lookup)
|
|
618
|
+
const current = fingerprint(lookup)
|
|
342
619
|
return {
|
|
343
620
|
previous: current,
|
|
344
621
|
remaining: progress.remaining - 1,
|
|
@@ -347,123 +624,153 @@ const settleTree = Effect.fn('settleTree')(function* (
|
|
|
347
624
|
}),
|
|
348
625
|
},
|
|
349
626
|
)
|
|
627
|
+
yield* ensureSettled(progress)
|
|
350
628
|
})
|
|
351
629
|
|
|
352
|
-
|
|
353
|
-
|
|
630
|
+
function makeMountedFacade<
|
|
631
|
+
P,
|
|
632
|
+
C extends UiContract,
|
|
633
|
+
S extends ReadonlyArray<unknown>,
|
|
634
|
+
N extends string,
|
|
635
|
+
Identity,
|
|
636
|
+
>(
|
|
637
|
+
runtime: RenderRuntime,
|
|
638
|
+
root: CompositionClass<P, C, S, N, Identity>,
|
|
354
639
|
sink: Sink,
|
|
355
|
-
// Records every event name dispatched through the facade, so a proof's
|
|
356
|
-
// requirement-declared event coverage can be verified after the body runs.
|
|
357
640
|
dispatched: Set<string>,
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
641
|
+
settleBoundary: SettleBoundary,
|
|
642
|
+
): {
|
|
643
|
+
readonly facade: MountedFacade<C>
|
|
644
|
+
readonly settle: Effect.Effect<void, never, never>
|
|
645
|
+
}
|
|
646
|
+
function makeMountedFacade(
|
|
647
|
+
runtime: RenderRuntime,
|
|
648
|
+
root: AnyComposition,
|
|
649
|
+
sink: Sink,
|
|
650
|
+
dispatched: Set<string>,
|
|
651
|
+
settleBoundary: SettleBoundary,
|
|
652
|
+
): MountedFacadeHandleErasure
|
|
653
|
+
function makeMountedFacade(
|
|
654
|
+
runtime: RenderRuntime,
|
|
655
|
+
root: AnyComposition,
|
|
656
|
+
sink: Sink,
|
|
657
|
+
dispatched: Set<string>,
|
|
658
|
+
settleBoundary: SettleBoundary,
|
|
659
|
+
): MountedFacadeHandleErasure {
|
|
660
|
+
const bindings = collectRuntimeBindings(root, runtime)
|
|
661
|
+
const lookup = makeCaptureLookup()
|
|
662
|
+
const settle = settleBoundary(settleTreeRuntime(runtime, root, bindings, sink, lookup))
|
|
663
|
+
const rerender = renderTreeRuntime(runtime, root, bindings, sink, lookup)
|
|
379
664
|
|
|
380
|
-
const
|
|
381
|
-
|
|
665
|
+
const triggerOf = (ref: NodeRef, event: string): Effect.Effect<Trigger<unknown>, never, never> =>
|
|
666
|
+
Option.match(
|
|
667
|
+
Option.flatMap(nodeCaptureFor(lookup, ref), (capture) =>
|
|
668
|
+
Option.fromNullable(capture.events[event]),
|
|
669
|
+
),
|
|
670
|
+
{
|
|
671
|
+
onNone: () =>
|
|
672
|
+
Effect.dieMessage(
|
|
673
|
+
new UnknownAction({
|
|
674
|
+
composition: ref.name,
|
|
675
|
+
action: event,
|
|
676
|
+
rendered: lookup.captures.filter((entry) => entry.ref.name === ref.name).length,
|
|
677
|
+
}).message,
|
|
678
|
+
),
|
|
679
|
+
onSome: Effect.succeed,
|
|
680
|
+
},
|
|
681
|
+
)
|
|
682
|
+
|
|
683
|
+
const propsFor = (ref: NodeRef): Effect.Effect<unknown, never, never> =>
|
|
684
|
+
Effect.map(rerender, () =>
|
|
685
|
+
Option.match(nodeCaptureFor(lookup, ref), {
|
|
686
|
+
onNone: () => undefined,
|
|
687
|
+
onSome: (capture) => capture.props,
|
|
688
|
+
}),
|
|
689
|
+
)
|
|
382
690
|
|
|
383
|
-
const nodeFacade = (ref: NodeRef, comp:
|
|
691
|
+
const nodeFacade = (ref: NodeRef, comp: AnyComposition): MountedFacadeErasure => ({
|
|
384
692
|
props: propsFor(ref),
|
|
385
|
-
// Assert the computed props match a typed subset. Reuses the same partial-match
|
|
386
|
-
// logic as `expect(...).toMatchObject`; dies with `AssertionFailed` on mismatch.
|
|
387
693
|
expectProps: (partial) =>
|
|
388
694
|
propsFor(ref).pipe(
|
|
389
|
-
Effect.flatMap((props) => {
|
|
390
|
-
const mismatch = matchProps({ actual: props, expected: partial })
|
|
391
|
-
return mismatch === undefined
|
|
392
|
-
? Effect.void
|
|
393
|
-
: Effect.dieMessage(new AssertionFailed({ detail: mismatch }).message)
|
|
394
|
-
}),
|
|
695
|
+
Effect.flatMap((props) => expectMatchingProps({ actual: props, expected: partial })),
|
|
395
696
|
),
|
|
396
|
-
// `keyed` resolves event/slot names at runtime; the string-keyed proxy is the one
|
|
397
|
-
// reflection boundary — every name it serves is a real contract member, so reading
|
|
398
|
-
// it as the contract-typed surface is sound. The action resolves to the props this
|
|
399
|
-
// node computed once the dispatch settled (the typed "result" of the event).
|
|
400
|
-
// oxlint-disable-next-line reform-rules/no-type-assertion -- reflection boundary: the string-keyed proxy serves only real contract event names
|
|
401
697
|
actions: keyed(
|
|
402
|
-
(event):
|
|
403
|
-
(
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
Effect.
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
}),
|
|
411
|
-
),
|
|
412
|
-
Effect.flatMap(() => settle),
|
|
413
|
-
Effect.flatMap(() => propsFor(ref)),
|
|
698
|
+
(event) => (payload: unknown) =>
|
|
699
|
+
rerender.pipe(
|
|
700
|
+
Effect.flatMap(() => triggerOf(ref, event)),
|
|
701
|
+
Effect.flatMap((trigger) =>
|
|
702
|
+
Effect.sync(() => {
|
|
703
|
+
dispatched.add(event)
|
|
704
|
+
trigger(payload)
|
|
705
|
+
}),
|
|
414
706
|
),
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
707
|
+
Effect.flatMap(() => settle),
|
|
708
|
+
Effect.flatMap(() => propsFor(ref)),
|
|
709
|
+
),
|
|
710
|
+
),
|
|
711
|
+
slots: keyed((slotName) => slotFacade(ref, comp, slotName)),
|
|
418
712
|
frame: settle,
|
|
419
713
|
})
|
|
420
714
|
|
|
421
|
-
const slotFacade = (
|
|
715
|
+
const slotFacade = (
|
|
716
|
+
parentRef: NodeRef,
|
|
717
|
+
parent: AnyComposition,
|
|
718
|
+
slotName: string,
|
|
719
|
+
): MountedSlotFacadeErasure => {
|
|
422
720
|
const slotClass = slotsOf(parent)[slotName]
|
|
423
721
|
const child = slotClass && bindings.get(slotClass)
|
|
424
722
|
if (child === undefined) {
|
|
425
|
-
|
|
426
|
-
|
|
723
|
+
return Effect.runSync(
|
|
724
|
+
Effect.dieMessage(new UnknownSlot({ parent: uiNameOf(parent), slot: slotName }).message),
|
|
725
|
+
)
|
|
427
726
|
}
|
|
428
727
|
const childName = uiNameOf(child)
|
|
429
|
-
const
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
728
|
+
const childRefAt = (index: number): NodeRef =>
|
|
729
|
+
slotCapturesFor(lookup, parentRef, slotName)[index]?.ref ?? {
|
|
730
|
+
name: childName,
|
|
731
|
+
parent: Option.some(parentRef),
|
|
732
|
+
slot: Option.some({
|
|
733
|
+
name: slotName,
|
|
734
|
+
index,
|
|
735
|
+
key: Option.none(),
|
|
736
|
+
keyOccurrence: 0,
|
|
737
|
+
}),
|
|
738
|
+
}
|
|
739
|
+
const atIndex = (index: number): Effect.Effect<MountedFacadeErasure, never, never> =>
|
|
740
|
+
Effect.as(rerender, nodeFacade(childRefAt(index), child))
|
|
741
|
+
const first = nodeFacade(childRefAt(0), child)
|
|
437
742
|
return {
|
|
438
743
|
first: atIndex(0),
|
|
439
744
|
at: atIndex,
|
|
440
745
|
all: Effect.map(rerender, () =>
|
|
441
|
-
|
|
746
|
+
slotCapturesFor(lookup, parentRef, slotName).map((entry) => nodeFacade(entry.ref, child)),
|
|
442
747
|
),
|
|
443
|
-
// Select the one child mounted under `key` (the `each` item key). Re-renders
|
|
444
|
-
// first so the fill keys reflect the latest frame, then resolves its facade;
|
|
445
|
-
// dies with `UnknownSlot` (key in the slot position) when no fill carries it.
|
|
446
748
|
byKey: (key) =>
|
|
447
749
|
Effect.flatMap(rerender, () =>
|
|
448
|
-
Option.match(
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
750
|
+
Option.match(
|
|
751
|
+
Arr.findFirst(
|
|
752
|
+
slotCapturesFor(lookup, parentRef, slotName),
|
|
753
|
+
(entry) => entry.capture.key === key,
|
|
754
|
+
),
|
|
755
|
+
{
|
|
756
|
+
onNone: () =>
|
|
757
|
+
Effect.dieMessage(
|
|
758
|
+
new UnknownSlot({
|
|
759
|
+
parent: uiNameOf(parent),
|
|
760
|
+
slot: `${slotName}[key=${key}]`,
|
|
761
|
+
}).message,
|
|
762
|
+
),
|
|
763
|
+
onSome: (entry) => Effect.succeed(nodeFacade(entry.ref, child)),
|
|
764
|
+
},
|
|
765
|
+
),
|
|
455
766
|
),
|
|
456
|
-
// Every child whose computed props satisfy `predicate`. Re-renders, then maps
|
|
457
|
-
// the matching captures back to their facades by render index.
|
|
458
767
|
where: (predicate) =>
|
|
459
768
|
Effect.map(rerender, () =>
|
|
460
|
-
|
|
461
|
-
predicate(capture.props) ? [nodeFacade(
|
|
769
|
+
slotCapturesFor(lookup, parentRef, slotName).flatMap((entry) =>
|
|
770
|
+
predicate(entry.capture.props) ? [nodeFacade(entry.ref, child)] : [],
|
|
462
771
|
),
|
|
463
772
|
),
|
|
464
|
-
|
|
465
|
-
count: Effect.map(rerender, () => capturesFor(sink, childName).length),
|
|
466
|
-
// Used directly, a slot behaves as its first instance.
|
|
773
|
+
count: Effect.map(rerender, () => slotCapturesFor(lookup, parentRef, slotName).length),
|
|
467
774
|
props: first.props,
|
|
468
775
|
expectProps: first.expectProps,
|
|
469
776
|
actions: first.actions,
|
|
@@ -472,104 +779,849 @@ const makeFacadeEffect = Effect.fn('makeFacadeEffect')(function* (
|
|
|
472
779
|
}
|
|
473
780
|
}
|
|
474
781
|
|
|
475
|
-
return {
|
|
476
|
-
|
|
782
|
+
return {
|
|
783
|
+
facade: nodeFacade(rootRef(root), root),
|
|
784
|
+
settle,
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
export const makeProofFacade = <
|
|
789
|
+
C extends UiContract,
|
|
790
|
+
S extends ReadonlyArray<unknown>,
|
|
791
|
+
Services,
|
|
792
|
+
P,
|
|
793
|
+
N extends string,
|
|
794
|
+
Identity,
|
|
795
|
+
>(
|
|
796
|
+
runtime: ManagedRuntime.ManagedRuntime<Services | CaptureSink, never>,
|
|
797
|
+
scene: CapturedScene<C, S, Services, P, N, Identity>,
|
|
798
|
+
sink: Sink,
|
|
799
|
+
dispatched: Set<string>,
|
|
800
|
+
): {
|
|
801
|
+
readonly facade: Facade<C, never>
|
|
802
|
+
readonly settle: Effect.Effect<void, never, never>
|
|
803
|
+
} => {
|
|
804
|
+
return makeFacade(runtime, scene, sink, dispatched)
|
|
805
|
+
}
|
|
477
806
|
|
|
478
|
-
export const
|
|
479
|
-
|
|
480
|
-
|
|
807
|
+
export const makeRuntimeHandleFacade = <
|
|
808
|
+
Services,
|
|
809
|
+
RootIdentifier extends NoInfer<Services>,
|
|
810
|
+
P,
|
|
811
|
+
C extends UiContract,
|
|
812
|
+
S extends ReadonlyArray<unknown>,
|
|
813
|
+
N extends string,
|
|
814
|
+
Identity,
|
|
815
|
+
>(
|
|
816
|
+
runtime: RuntimeHandle<Services>,
|
|
817
|
+
root: RuntimeRootComposition<RootIdentifier, P, C, S, N, Identity>,
|
|
481
818
|
sink: Sink,
|
|
482
819
|
dispatched: Set<string>,
|
|
483
|
-
): {
|
|
484
|
-
|
|
820
|
+
): {
|
|
821
|
+
readonly facade: MountedFacade<C>
|
|
822
|
+
readonly settle: Effect.Effect<void, never, never>
|
|
823
|
+
} => makeMountedFacade(runtime, root, sink, dispatched, directSettle)
|
|
485
824
|
|
|
486
|
-
|
|
487
|
-
|
|
825
|
+
type FeatureMountState =
|
|
826
|
+
| { readonly _tag: 'Loading' }
|
|
827
|
+
| { readonly _tag: 'Live'; readonly runtime: HostRuntime }
|
|
828
|
+
| { readonly _tag: 'Failed'; readonly error: FeatureLoadFailed }
|
|
829
|
+
|
|
830
|
+
interface TreeFeatureMount {
|
|
831
|
+
readonly id: string
|
|
832
|
+
readonly binding: AnyFeatureBinding
|
|
833
|
+
readonly parent: HostRuntime
|
|
834
|
+
readonly depth: number
|
|
835
|
+
readonly props: unknown
|
|
836
|
+
attempt: number
|
|
837
|
+
state: FeatureMountState
|
|
838
|
+
dispose: () => void
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
interface TreeNode {
|
|
842
|
+
readonly path: string
|
|
843
|
+
readonly depth: number
|
|
844
|
+
readonly key: Option.Option<string>
|
|
845
|
+
readonly comp: AnyComposition
|
|
846
|
+
readonly props: unknown
|
|
847
|
+
readonly events: Record<string, Trigger<unknown>>
|
|
848
|
+
readonly slots: Readonly<Record<string, ReadonlyArray<TreeNode>>>
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
interface TreeRenderState {
|
|
852
|
+
readonly root: TreeNode
|
|
853
|
+
readonly fingerprint: string
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
export interface RuntimeTreeFacade<C extends UiContract> {
|
|
857
|
+
readonly facade: MountedFacade<C>
|
|
858
|
+
readonly settle: Effect.Effect<void, never, never>
|
|
859
|
+
dispose(): void
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
interface RuntimeTreeFacadeErasure extends MountedFacadeHandleErasure {
|
|
863
|
+
dispose(): void
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
interface TreeDriver {
|
|
867
|
+
readonly render: () => TreeRenderState
|
|
868
|
+
readonly settle: Effect.Effect<void, never, never>
|
|
869
|
+
readonly dispose: () => void
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
interface MountIdentityInput {
|
|
873
|
+
readonly parent: HostRuntime
|
|
874
|
+
readonly parentPath: string
|
|
875
|
+
readonly slotName: string
|
|
876
|
+
readonly key: string
|
|
877
|
+
readonly binding: AnyFeatureBinding
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
interface FeatureRecordInput {
|
|
881
|
+
readonly id: string
|
|
882
|
+
readonly parent: HostRuntime
|
|
883
|
+
readonly binding: AnyFeatureBinding
|
|
884
|
+
readonly depth: number
|
|
885
|
+
readonly props: unknown
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
interface RenderCompositionInput {
|
|
889
|
+
readonly runtime: HostRuntime
|
|
890
|
+
readonly comp: AnyComposition
|
|
891
|
+
readonly props: unknown
|
|
892
|
+
readonly key: Option.Option<string>
|
|
893
|
+
readonly path: string
|
|
894
|
+
readonly depth: number
|
|
895
|
+
readonly seen: Set<string>
|
|
896
|
+
readonly mountMissing: boolean
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
const encodeIdentityPart = (part: string): string => `${part.length}:${part}`
|
|
900
|
+
|
|
901
|
+
const fillEntries = (
|
|
902
|
+
slotName: string,
|
|
903
|
+
fill: SlotFill<unknown>,
|
|
904
|
+
): ReadonlyArray<{ readonly key: string; readonly props: unknown }> =>
|
|
905
|
+
Match.value(fill).pipe(
|
|
906
|
+
Match.when({ _tag: 'Each' }, (each) => each.items),
|
|
907
|
+
Match.when({ _tag: 'One' }, (oneFill) => [{ key: `${slotName}.0`, props: oneFill.props }]),
|
|
908
|
+
Match.when({ _tag: 'Absent' }, () => []),
|
|
909
|
+
Match.exhaustive,
|
|
910
|
+
)
|
|
911
|
+
|
|
912
|
+
const treeFingerprint = (node: TreeNode): unknown => [
|
|
913
|
+
uiNameOf(node.comp),
|
|
914
|
+
node.key,
|
|
915
|
+
node.props,
|
|
916
|
+
Rec.map(node.slots, (children) => children.map(treeFingerprint)),
|
|
917
|
+
]
|
|
918
|
+
|
|
919
|
+
const makeTreeDriver = (
|
|
920
|
+
runtime: HostRuntime,
|
|
921
|
+
root: AnyComposition,
|
|
922
|
+
rootProps: unknown,
|
|
923
|
+
sink: Sink,
|
|
924
|
+
): TreeDriver => {
|
|
925
|
+
const mounts = new Map<string, TreeFeatureMount>()
|
|
926
|
+
const runtimeIds = new WeakMap<HostRuntime, number>()
|
|
927
|
+
const bindingIds = new WeakMap<object, number>()
|
|
928
|
+
const sequence = { runtime: 0, binding: 0 }
|
|
929
|
+
const status = { disposed: false }
|
|
930
|
+
|
|
931
|
+
const identity = <Subject extends object>(
|
|
932
|
+
identities: WeakMap<Subject, number>,
|
|
933
|
+
subject: Subject,
|
|
934
|
+
next: () => number,
|
|
935
|
+
): number => {
|
|
936
|
+
const current = identities.get(subject)
|
|
937
|
+
if (current !== undefined) {
|
|
938
|
+
return current
|
|
939
|
+
}
|
|
940
|
+
const created = next()
|
|
941
|
+
identities.set(subject, created)
|
|
942
|
+
return created
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
const mountId = ({ parent, parentPath, slotName, key, binding }: MountIdentityInput): string => {
|
|
946
|
+
const runtimeId = identity(runtimeIds, parent, () => ++sequence.runtime)
|
|
947
|
+
const bindingId = identity(bindingIds, binding, () => ++sequence.binding)
|
|
948
|
+
return [String(runtimeId), parentPath, slotName, String(bindingId), key]
|
|
949
|
+
.map(encodeIdentityPart)
|
|
950
|
+
.join('')
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
const startMount = (record: TreeFeatureMount): void => {
|
|
954
|
+
record.attempt += 1
|
|
955
|
+
const attempt = record.attempt
|
|
956
|
+
record.binding.capture<void>((binding) => {
|
|
957
|
+
if (binding.strategy === 'default') {
|
|
958
|
+
record.state = { _tag: 'Live', runtime: record.parent }
|
|
959
|
+
binding.boot.forEach((event) => record.parent.dispatch('High', event))
|
|
960
|
+
record.dispose = () => {}
|
|
961
|
+
return
|
|
962
|
+
}
|
|
963
|
+
record.state = { _tag: 'Loading' }
|
|
964
|
+
record.dispose = record.parent.mountAnyFeature(binding, {
|
|
965
|
+
props: record.props,
|
|
966
|
+
onLive: (childRuntime) => {
|
|
967
|
+
if (status.disposed || record.attempt !== attempt || mounts.get(record.id) !== record) {
|
|
968
|
+
return
|
|
969
|
+
}
|
|
970
|
+
record.state = { _tag: 'Live', runtime: childRuntime }
|
|
971
|
+
},
|
|
972
|
+
onFailed: (error) => {
|
|
973
|
+
if (status.disposed || record.attempt !== attempt || mounts.get(record.id) !== record) {
|
|
974
|
+
return
|
|
975
|
+
}
|
|
976
|
+
record.state = { _tag: 'Failed', error }
|
|
977
|
+
},
|
|
978
|
+
})
|
|
979
|
+
})
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
const retryMount = (record: TreeFeatureMount): void => {
|
|
983
|
+
if (status.disposed || mounts.get(record.id) !== record || record.state._tag !== 'Failed') {
|
|
984
|
+
return
|
|
985
|
+
}
|
|
986
|
+
record.dispose()
|
|
987
|
+
startMount(record)
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
const featureRecord = ({
|
|
991
|
+
id,
|
|
992
|
+
parent,
|
|
993
|
+
binding,
|
|
994
|
+
depth,
|
|
995
|
+
props,
|
|
996
|
+
}: FeatureRecordInput): TreeFeatureMount => {
|
|
997
|
+
const current = mounts.get(id)
|
|
998
|
+
if (current !== undefined && current.binding === binding && current.parent === parent) {
|
|
999
|
+
return current
|
|
1000
|
+
}
|
|
1001
|
+
if (current !== undefined) {
|
|
1002
|
+
current.attempt += 1
|
|
1003
|
+
current.dispose()
|
|
1004
|
+
}
|
|
1005
|
+
const created: TreeFeatureMount = {
|
|
1006
|
+
id,
|
|
1007
|
+
binding,
|
|
1008
|
+
parent,
|
|
1009
|
+
depth,
|
|
1010
|
+
props,
|
|
1011
|
+
attempt: 0,
|
|
1012
|
+
state: { _tag: 'Loading' },
|
|
1013
|
+
dispose: () => {},
|
|
1014
|
+
}
|
|
1015
|
+
mounts.set(id, created)
|
|
1016
|
+
startMount(created)
|
|
1017
|
+
return created
|
|
1018
|
+
}
|
|
1019
|
+
const featureRecordForRender = (
|
|
1020
|
+
input: FeatureRecordInput,
|
|
1021
|
+
mountMissing: boolean,
|
|
1022
|
+
): TreeFeatureMount | undefined => {
|
|
1023
|
+
const current = mounts.get(input.id)
|
|
1024
|
+
if (current !== undefined || !mountMissing) {
|
|
1025
|
+
return current
|
|
1026
|
+
}
|
|
1027
|
+
return featureRecord(input)
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
const renderComposition = ({
|
|
1031
|
+
runtime: currentRuntime,
|
|
1032
|
+
comp,
|
|
1033
|
+
props,
|
|
1034
|
+
key,
|
|
1035
|
+
path,
|
|
1036
|
+
depth,
|
|
1037
|
+
seen,
|
|
1038
|
+
mountMissing,
|
|
1039
|
+
}: RenderCompositionInput): TreeNode => {
|
|
1040
|
+
const endRenderSpan = sink.instrumentation.uiRendered(uiNameOf(comp))
|
|
1041
|
+
const frame = renderCompositionRuntime(currentRuntime, comp, props)
|
|
1042
|
+
endRenderSpan()
|
|
1043
|
+
sink.api.record({
|
|
1044
|
+
name: uiNameOf(comp),
|
|
1045
|
+
props: frame.props,
|
|
1046
|
+
events: eventsOf(frame),
|
|
1047
|
+
...(Option.isSome(key) ? { key: key.value } : {}),
|
|
1048
|
+
})
|
|
1049
|
+
|
|
1050
|
+
const fills = structureFills(frame)
|
|
1051
|
+
const renderedSlots = Rec.map(slotsOf(comp), (slotClass, slotName) => {
|
|
1052
|
+
const fill = fills[slotName]
|
|
1053
|
+
if (fill === undefined) {
|
|
1054
|
+
return []
|
|
1055
|
+
}
|
|
1056
|
+
const child = readRuntimeSlotChild({
|
|
1057
|
+
runtime: currentRuntime,
|
|
1058
|
+
slotDefinition: slotClass,
|
|
1059
|
+
parent: comp,
|
|
1060
|
+
})
|
|
1061
|
+
return fillEntries(slotName, fill).flatMap((entry) => {
|
|
1062
|
+
const childPath = `${path}${encodeIdentityPart(slotName)}${encodeIdentityPart(entry.key)}`
|
|
1063
|
+
if (!isFeatureBinding(child)) {
|
|
1064
|
+
return [
|
|
1065
|
+
renderComposition({
|
|
1066
|
+
runtime: currentRuntime,
|
|
1067
|
+
comp: child,
|
|
1068
|
+
props: entry.props,
|
|
1069
|
+
key: Option.some(entry.key),
|
|
1070
|
+
path: childPath,
|
|
1071
|
+
depth: depth + 1,
|
|
1072
|
+
seen,
|
|
1073
|
+
mountMissing,
|
|
1074
|
+
}),
|
|
1075
|
+
]
|
|
1076
|
+
}
|
|
1077
|
+
const id = mountId({
|
|
1078
|
+
parent: currentRuntime,
|
|
1079
|
+
parentPath: path,
|
|
1080
|
+
slotName,
|
|
1081
|
+
key: entry.key,
|
|
1082
|
+
binding: child,
|
|
1083
|
+
})
|
|
1084
|
+
seen.add(id)
|
|
1085
|
+
const record = featureRecordForRender(
|
|
1086
|
+
{
|
|
1087
|
+
id,
|
|
1088
|
+
parent: currentRuntime,
|
|
1089
|
+
binding: child,
|
|
1090
|
+
depth: depth + 1,
|
|
1091
|
+
props: entry.props,
|
|
1092
|
+
},
|
|
1093
|
+
mountMissing,
|
|
1094
|
+
)
|
|
1095
|
+
if (record === undefined) {
|
|
1096
|
+
return []
|
|
1097
|
+
}
|
|
1098
|
+
return child.capture<ReadonlyArray<TreeNode>>((binding) =>
|
|
1099
|
+
Match.value(record.state).pipe(
|
|
1100
|
+
Match.when({ _tag: 'Live' }, (live) => [
|
|
1101
|
+
renderComposition({
|
|
1102
|
+
runtime: live.runtime,
|
|
1103
|
+
comp: binding.composition,
|
|
1104
|
+
props: entry.props,
|
|
1105
|
+
key: Option.some(entry.key),
|
|
1106
|
+
path: childPath,
|
|
1107
|
+
depth: depth + 1,
|
|
1108
|
+
seen,
|
|
1109
|
+
mountMissing,
|
|
1110
|
+
}),
|
|
1111
|
+
]),
|
|
1112
|
+
Match.when({ _tag: 'Failed' }, (failed) =>
|
|
1113
|
+
Option.match(binding.placeholder, {
|
|
1114
|
+
onNone: () => [],
|
|
1115
|
+
onSome: (placeholder) => [
|
|
1116
|
+
renderComposition({
|
|
1117
|
+
runtime: currentRuntime,
|
|
1118
|
+
comp: placeholder.failed,
|
|
1119
|
+
props: {
|
|
1120
|
+
error: failed.error,
|
|
1121
|
+
retry: () => retryMount(record),
|
|
1122
|
+
},
|
|
1123
|
+
key: Option.some(entry.key),
|
|
1124
|
+
path: `${childPath}/failed`,
|
|
1125
|
+
depth: depth + 1,
|
|
1126
|
+
seen,
|
|
1127
|
+
mountMissing,
|
|
1128
|
+
}),
|
|
1129
|
+
],
|
|
1130
|
+
}),
|
|
1131
|
+
),
|
|
1132
|
+
Match.when({ _tag: 'Loading' }, () =>
|
|
1133
|
+
Option.match(binding.placeholder, {
|
|
1134
|
+
onNone: () => [],
|
|
1135
|
+
onSome: (placeholder) => [
|
|
1136
|
+
renderComposition({
|
|
1137
|
+
runtime: currentRuntime,
|
|
1138
|
+
comp: placeholder.loading,
|
|
1139
|
+
props: entry.props,
|
|
1140
|
+
key: Option.some(entry.key),
|
|
1141
|
+
path: `${childPath}/loading`,
|
|
1142
|
+
depth: depth + 1,
|
|
1143
|
+
seen,
|
|
1144
|
+
mountMissing,
|
|
1145
|
+
}),
|
|
1146
|
+
],
|
|
1147
|
+
}),
|
|
1148
|
+
),
|
|
1149
|
+
Match.exhaustive,
|
|
1150
|
+
),
|
|
1151
|
+
)
|
|
1152
|
+
})
|
|
1153
|
+
})
|
|
1154
|
+
|
|
1155
|
+
return {
|
|
1156
|
+
path,
|
|
1157
|
+
depth,
|
|
1158
|
+
key,
|
|
1159
|
+
comp,
|
|
1160
|
+
props: frame.props,
|
|
1161
|
+
events: eventsOf(frame),
|
|
1162
|
+
slots: renderedSlots,
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
const disposeStale = (seen: ReadonlySet<string>): void => {
|
|
1167
|
+
Arr.sortWith(
|
|
1168
|
+
Array.from(mounts.values()).filter((record) => !seen.has(record.id)),
|
|
1169
|
+
(record) => -record.depth,
|
|
1170
|
+
Order.number,
|
|
1171
|
+
).forEach((record) => {
|
|
1172
|
+
mounts.delete(record.id)
|
|
1173
|
+
record.attempt += 1
|
|
1174
|
+
record.dispose()
|
|
1175
|
+
})
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
const render = (): TreeRenderState => {
|
|
1179
|
+
// Discover the desired graph without acquiring missing Features, then release
|
|
1180
|
+
// removed scopes deepest-first before the live pass mounts replacements.
|
|
1181
|
+
sink.reset()
|
|
1182
|
+
const retained = new Set<string>()
|
|
1183
|
+
renderComposition({
|
|
1184
|
+
runtime,
|
|
1185
|
+
comp: root,
|
|
1186
|
+
props: rootProps,
|
|
1187
|
+
key: Option.none(),
|
|
1188
|
+
path: 'root',
|
|
1189
|
+
depth: 0,
|
|
1190
|
+
seen: retained,
|
|
1191
|
+
mountMissing: false,
|
|
1192
|
+
})
|
|
1193
|
+
disposeStale(retained)
|
|
1194
|
+
sink.reset()
|
|
1195
|
+
const seen = new Set<string>()
|
|
1196
|
+
const rootNode = renderComposition({
|
|
1197
|
+
runtime,
|
|
1198
|
+
comp: root,
|
|
1199
|
+
props: rootProps,
|
|
1200
|
+
key: Option.none(),
|
|
1201
|
+
path: 'root',
|
|
1202
|
+
depth: 0,
|
|
1203
|
+
seen,
|
|
1204
|
+
mountMissing: true,
|
|
1205
|
+
})
|
|
1206
|
+
return {
|
|
1207
|
+
root: rootNode,
|
|
1208
|
+
fingerprint: formatFingerprint(treeFingerprint(rootNode)),
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
const settle = Effect.gen(function* () {
|
|
1213
|
+
yield* settleDrain
|
|
1214
|
+
const first = render()
|
|
1215
|
+
const progress = yield* Effect.iterate(
|
|
1216
|
+
{
|
|
1217
|
+
previous: first.fingerprint,
|
|
1218
|
+
remaining: SETTLE_MAX_RENDERS,
|
|
1219
|
+
stable: false,
|
|
1220
|
+
},
|
|
1221
|
+
{
|
|
1222
|
+
while: (progress: SettleProgress) => !progress.stable && progress.remaining > 0,
|
|
1223
|
+
body: (progress) =>
|
|
1224
|
+
Effect.gen(function* () {
|
|
1225
|
+
yield* settleDrain
|
|
1226
|
+
yield* Effect.sleep(SETTLE_STEP)
|
|
1227
|
+
const current = render()
|
|
1228
|
+
return {
|
|
1229
|
+
previous: current.fingerprint,
|
|
1230
|
+
remaining: progress.remaining - 1,
|
|
1231
|
+
stable: current.fingerprint === progress.previous,
|
|
1232
|
+
}
|
|
1233
|
+
}),
|
|
1234
|
+
},
|
|
1235
|
+
)
|
|
1236
|
+
yield* ensureSettled(progress)
|
|
1237
|
+
})
|
|
1238
|
+
|
|
1239
|
+
const dispose = (): void => {
|
|
1240
|
+
if (status.disposed) {
|
|
1241
|
+
return
|
|
1242
|
+
}
|
|
1243
|
+
status.disposed = true
|
|
1244
|
+
Arr.sortWith(Array.from(mounts.values()), (record) => -record.depth, Order.number).forEach(
|
|
1245
|
+
(record) => {
|
|
1246
|
+
record.attempt += 1
|
|
1247
|
+
record.dispose()
|
|
1248
|
+
},
|
|
1249
|
+
)
|
|
1250
|
+
mounts.clear()
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
return { render, settle, dispose }
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
export function makeRuntimeTreeFacade<
|
|
1257
|
+
Services,
|
|
1258
|
+
RootIdentifier extends NoInfer<Services>,
|
|
1259
|
+
P,
|
|
1260
|
+
C extends UiContract,
|
|
1261
|
+
S extends ReadonlyArray<unknown>,
|
|
1262
|
+
N extends string,
|
|
1263
|
+
Identity,
|
|
1264
|
+
>(
|
|
1265
|
+
runtime: RuntimeHandle<Services>,
|
|
1266
|
+
root: RuntimeRootComposition<RootIdentifier, P, C, S, N, Identity>,
|
|
1267
|
+
rootProps: P,
|
|
1268
|
+
sink: Sink,
|
|
1269
|
+
dispatched: Set<string>,
|
|
1270
|
+
): RuntimeTreeFacade<C>
|
|
1271
|
+
export function makeRuntimeTreeFacade(
|
|
1272
|
+
runtime: HostRuntime,
|
|
1273
|
+
root: AnyComposition,
|
|
1274
|
+
rootProps: unknown,
|
|
1275
|
+
sink: Sink,
|
|
1276
|
+
dispatched: Set<string>,
|
|
1277
|
+
): RuntimeTreeFacadeErasure {
|
|
1278
|
+
const driver = makeTreeDriver(runtime, root, rootProps, sink)
|
|
1279
|
+
const currentRoot = (): TreeNode => driver.render().root
|
|
1280
|
+
|
|
1281
|
+
// `peek` is the non-dying half of `resolve`: None once the node has left the
|
|
1282
|
+
// tree, so an action that removed its own node can still report a result.
|
|
1283
|
+
const nodeFacade = (
|
|
1284
|
+
resolve: () => TreeNode,
|
|
1285
|
+
peek: () => Option.Option<TreeNode>,
|
|
1286
|
+
): MountedFacadeErasure => ({
|
|
1287
|
+
props: Effect.sync(() => resolve().props),
|
|
1288
|
+
expectProps: (partial) =>
|
|
1289
|
+
Effect.sync(() => {
|
|
1290
|
+
const mismatch = matchProps({
|
|
1291
|
+
actual: resolve().props,
|
|
1292
|
+
expected: partial,
|
|
1293
|
+
})
|
|
1294
|
+
if (mismatch !== undefined) {
|
|
1295
|
+
return Effect.runSync(
|
|
1296
|
+
Effect.dieMessage(new AssertionFailed({ detail: mismatch }).message),
|
|
1297
|
+
)
|
|
1298
|
+
}
|
|
1299
|
+
}),
|
|
1300
|
+
actions: keyed(
|
|
1301
|
+
(event) => (payload: unknown) =>
|
|
1302
|
+
Effect.gen(function* () {
|
|
1303
|
+
const node = resolve()
|
|
1304
|
+
const trigger = node.events[event]
|
|
1305
|
+
if (trigger === undefined) {
|
|
1306
|
+
return yield* Effect.dieMessage(
|
|
1307
|
+
new UnknownAction({
|
|
1308
|
+
composition: uiNameOf(node.comp),
|
|
1309
|
+
action: event,
|
|
1310
|
+
rendered: 1,
|
|
1311
|
+
}).message,
|
|
1312
|
+
)
|
|
1313
|
+
}
|
|
1314
|
+
dispatched.add(event)
|
|
1315
|
+
trigger(payload)
|
|
1316
|
+
yield* driver.settle
|
|
1317
|
+
// A self-removing action leaves nothing to re-read; report what the
|
|
1318
|
+
// node last rendered instead of dying on the vanished child.
|
|
1319
|
+
return Option.match(peek(), {
|
|
1320
|
+
onNone: () => node.props,
|
|
1321
|
+
onSome: (settled) => settled.props,
|
|
1322
|
+
})
|
|
1323
|
+
}),
|
|
1324
|
+
),
|
|
1325
|
+
slots: keyed((slotName) => slotFacade(resolve, slotName)),
|
|
1326
|
+
frame: driver.settle,
|
|
1327
|
+
})
|
|
1328
|
+
|
|
1329
|
+
const slotFacade = (
|
|
1330
|
+
resolveParent: () => TreeNode,
|
|
1331
|
+
slotName: string,
|
|
1332
|
+
): MountedSlotFacadeErasure => {
|
|
1333
|
+
const children = (): ReadonlyArray<TreeNode> => {
|
|
1334
|
+
const parent = resolveParent()
|
|
1335
|
+
const resolved = parent.slots[slotName]
|
|
1336
|
+
if (resolved === undefined) {
|
|
1337
|
+
return Effect.runSync(
|
|
1338
|
+
Effect.dieMessage(
|
|
1339
|
+
new UnknownSlot({ parent: uiNameOf(parent.comp), slot: slotName }).message,
|
|
1340
|
+
),
|
|
1341
|
+
)
|
|
1342
|
+
}
|
|
1343
|
+
return resolved
|
|
1344
|
+
}
|
|
1345
|
+
const peekAt = (index: number): Option.Option<TreeNode> =>
|
|
1346
|
+
Option.fromNullable(children()[index])
|
|
1347
|
+
const peekByKey = (key: string): Option.Option<TreeNode> =>
|
|
1348
|
+
Arr.findFirst(children(), (candidate) => Option.contains(candidate.key, key))
|
|
1349
|
+
const childAt = (index: number): MountedFacadeErasure =>
|
|
1350
|
+
nodeFacade(
|
|
1351
|
+
() => children()[index] ?? missingChild(index),
|
|
1352
|
+
() => peekAt(index),
|
|
1353
|
+
)
|
|
1354
|
+
const childByKey = (key: string): MountedFacadeErasure =>
|
|
1355
|
+
nodeFacade(
|
|
1356
|
+
() => Option.getOrElse(peekByKey(key), () => missingKey(key)),
|
|
1357
|
+
() => peekByKey(key),
|
|
1358
|
+
)
|
|
1359
|
+
const missingChild = (index: number): never => {
|
|
1360
|
+
const parent = resolveParent()
|
|
1361
|
+
return Effect.runSync(
|
|
1362
|
+
Effect.dieMessage(
|
|
1363
|
+
new UnknownSlot({
|
|
1364
|
+
parent: uiNameOf(parent.comp),
|
|
1365
|
+
slot: `${slotName}[${index}]`,
|
|
1366
|
+
}).message,
|
|
1367
|
+
),
|
|
1368
|
+
)
|
|
1369
|
+
}
|
|
1370
|
+
const missingKey = (key: string): never => {
|
|
1371
|
+
const parent = resolveParent()
|
|
1372
|
+
return Effect.runSync(
|
|
1373
|
+
Effect.dieMessage(
|
|
1374
|
+
new UnknownSlot({
|
|
1375
|
+
parent: uiNameOf(parent.comp),
|
|
1376
|
+
slot: `${slotName}[key=${key}]`,
|
|
1377
|
+
}).message,
|
|
1378
|
+
),
|
|
1379
|
+
)
|
|
1380
|
+
}
|
|
1381
|
+
const first = childAt(0)
|
|
1382
|
+
return {
|
|
1383
|
+
first: Effect.sync(() => childAt(0)),
|
|
1384
|
+
at: (index) => Effect.sync(() => childAt(index)),
|
|
1385
|
+
all: Effect.sync(() => children().map((_child, index) => childAt(index))),
|
|
1386
|
+
byKey: (key) =>
|
|
1387
|
+
Effect.sync(() => {
|
|
1388
|
+
const child = Arr.findFirst(children(), (candidate) =>
|
|
1389
|
+
Option.contains(candidate.key, key),
|
|
1390
|
+
)
|
|
1391
|
+
return Option.isNone(child) ? missingKey(key) : childByKey(key)
|
|
1392
|
+
}),
|
|
1393
|
+
where: (predicate) =>
|
|
1394
|
+
Effect.sync(() =>
|
|
1395
|
+
children().flatMap((child, index) => (predicate(child.props) ? [childAt(index)] : [])),
|
|
1396
|
+
),
|
|
1397
|
+
count: Effect.sync(() => children().length),
|
|
1398
|
+
props: first.props,
|
|
1399
|
+
expectProps: first.expectProps,
|
|
1400
|
+
actions: first.actions,
|
|
1401
|
+
slots: first.slots,
|
|
1402
|
+
frame: driver.settle,
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
return {
|
|
1407
|
+
facade: nodeFacade(currentRoot, () => Option.some(currentRoot())),
|
|
1408
|
+
settle: driver.settle,
|
|
1409
|
+
dispose: driver.dispose,
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
const proofRootProps = <
|
|
1414
|
+
P,
|
|
1415
|
+
C extends UiContract,
|
|
1416
|
+
S extends ReadonlyArray<unknown>,
|
|
1417
|
+
N extends string,
|
|
1418
|
+
Identity,
|
|
1419
|
+
>(
|
|
1420
|
+
composition: CompositionClass<P, C, S, N, Identity>,
|
|
1421
|
+
): Effect.Effect<P, never, never> =>
|
|
1422
|
+
Option.match(composition.parseProps({}), {
|
|
1423
|
+
onNone: () => Effect.dieMessage(invalidCompositionProps(composition)),
|
|
1424
|
+
onSome: Effect.succeed,
|
|
1425
|
+
})
|
|
1426
|
+
|
|
1427
|
+
const withProofRuntimeTree = <
|
|
1428
|
+
A,
|
|
1429
|
+
E,
|
|
1430
|
+
C extends UiContract,
|
|
1431
|
+
S extends ReadonlyArray<unknown>,
|
|
1432
|
+
Services,
|
|
1433
|
+
P,
|
|
1434
|
+
N extends string,
|
|
1435
|
+
Identity,
|
|
1436
|
+
>(
|
|
1437
|
+
scene: CapturedScene<C, S, Services, P, N, Identity>,
|
|
1438
|
+
sink: Sink,
|
|
1439
|
+
dispatched: Set<string>,
|
|
1440
|
+
use: (runtime: HostRuntime, tree: RuntimeTreeFacade<C>) => Effect.Effect<A, E, never>,
|
|
1441
|
+
): Effect.Effect<A, E, never> =>
|
|
1442
|
+
Effect.acquireUseRelease(
|
|
1443
|
+
Effect.sync(() => makeAppRuntime(scene)),
|
|
1444
|
+
(runtime) =>
|
|
1445
|
+
Effect.acquireUseRelease(
|
|
1446
|
+
Effect.gen(function* () {
|
|
1447
|
+
const rootProps = yield* proofRootProps(scene.composition)
|
|
1448
|
+
yield* Effect.sync(() => runtime.boot())
|
|
1449
|
+
return makeRuntimeTreeFacade(runtime, scene.composition, rootProps, sink, dispatched)
|
|
1450
|
+
}),
|
|
1451
|
+
(tree) => use(runtime, tree),
|
|
1452
|
+
(tree) => Effect.sync(tree.dispose),
|
|
1453
|
+
),
|
|
1454
|
+
(runtime) => Effect.sync(runtime.dispose),
|
|
1455
|
+
)
|
|
1456
|
+
|
|
1457
|
+
const missingCoverage = (
|
|
1458
|
+
proof: Pick<Proof, 'requirement'>,
|
|
1459
|
+
dispatched: Set<string>,
|
|
1460
|
+
): ReadonlyArray<string> =>
|
|
488
1461
|
Option.match(proof.requirement.manifest.events, {
|
|
489
1462
|
onNone: () => [],
|
|
490
1463
|
onSome: (declared) => declared.map(String).filter((event) => !dispatched.has(event)),
|
|
491
1464
|
})
|
|
492
1465
|
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
1466
|
+
interface ProofPlacement {
|
|
1467
|
+
readonly requirement: Proof['requirement']
|
|
1468
|
+
readonly placement: 'Root' | 'Slot'
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
const compositionIdentity = (composition: AnyComposition): symbol =>
|
|
1472
|
+
composition.capture<symbol>((exact) => exact.identity)
|
|
1473
|
+
|
|
1474
|
+
const verifyProofPlacement = (
|
|
1475
|
+
proof: ProofPlacement,
|
|
1476
|
+
scene: AnyScene,
|
|
1477
|
+
runtime: RenderRuntime,
|
|
1478
|
+
): Effect.Effect<void, never, never> => {
|
|
1479
|
+
const expected = proof.requirement.manifest.composition
|
|
1480
|
+
if (
|
|
1481
|
+
proof.placement === 'Root' &&
|
|
1482
|
+
Object.is(compositionIdentity(scene.composition), compositionIdentity(expected))
|
|
1483
|
+
) {
|
|
1484
|
+
return Effect.void
|
|
1485
|
+
}
|
|
1486
|
+
if (proof.placement === 'Slot') {
|
|
1487
|
+
const matches = Rec.values(slotsOf(scene.composition)).some((slotClass) => {
|
|
1488
|
+
const child = childComposition(
|
|
1489
|
+
readRuntimeSlotChild({
|
|
1490
|
+
runtime,
|
|
1491
|
+
slotDefinition: slotClass,
|
|
1492
|
+
parent: scene.composition,
|
|
1493
|
+
}),
|
|
1494
|
+
)
|
|
1495
|
+
return Object.is(compositionIdentity(child), compositionIdentity(expected))
|
|
1496
|
+
})
|
|
1497
|
+
if (matches) {
|
|
1498
|
+
return Effect.void
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
const location = proof.placement === 'Root' ? 'scene root' : 'direct scene slot'
|
|
1502
|
+
return Effect.dieMessage(
|
|
1503
|
+
`reform-proof: requirement composition ${expected.manifest.name} is not the ${location} by definition identity`,
|
|
1504
|
+
)
|
|
1505
|
+
}
|
|
498
1506
|
|
|
499
|
-
const
|
|
500
|
-
|
|
1507
|
+
const executeProofWithSink = Effect.fn('executeProofWithSink')(function* <
|
|
1508
|
+
Comp extends ProductComposition,
|
|
1509
|
+
C extends UiContract,
|
|
1510
|
+
S extends ReadonlyArray<unknown>,
|
|
1511
|
+
Services,
|
|
1512
|
+
P,
|
|
1513
|
+
N extends string,
|
|
1514
|
+
Identity,
|
|
1515
|
+
>(
|
|
1516
|
+
proof: ProofCase<Comp, C, S, Services, P, N, Identity>,
|
|
1517
|
+
scene: CapturedScene<C, S, Services, P, N, Identity>,
|
|
501
1518
|
sink: Sink,
|
|
502
|
-
): Effect.fn.Return<ProofResult, never,
|
|
1519
|
+
): Effect.fn.Return<ProofResult, never, never> {
|
|
503
1520
|
const dispatched = new Set<string>()
|
|
504
1521
|
const statement = proof.requirement.manifest.statement
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
const
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
1522
|
+
const runGenerator = Effect.fn('runProofGenerator')(function* (
|
|
1523
|
+
generator: ReturnType<typeof proof.body>,
|
|
1524
|
+
input: unknown,
|
|
1525
|
+
settleEffect: Effect.Effect<void, never, never>,
|
|
1526
|
+
): Effect.fn.Return<void, unknown, never> {
|
|
1527
|
+
const next = generator.next(input)
|
|
1528
|
+
if (next.done === true) {
|
|
1529
|
+
return
|
|
1530
|
+
}
|
|
1531
|
+
const output = yield* yieldWrapGet(next.value)
|
|
1532
|
+
yield* settleEffect
|
|
1533
|
+
yield* runGenerator(generator, output, settleEffect)
|
|
1534
|
+
})
|
|
1535
|
+
return yield* withProofRuntimeTree(scene, sink, dispatched, (runtime, tree) =>
|
|
1536
|
+
Effect.gen(function* () {
|
|
1537
|
+
yield* verifyProofPlacement(proof, scene, runtime)
|
|
1538
|
+
yield* tree.settle
|
|
1539
|
+
yield* runGenerator(proof.body(tree.facade), undefined, tree.settle)
|
|
1540
|
+
const missing = missingCoverage(proof, dispatched)
|
|
1541
|
+
const coverageError = `requirement declares events never dispatched: ${missing.join(', ')}`
|
|
1542
|
+
return missing.length > 0
|
|
1543
|
+
? { requirement: statement, ok: false, error: coverageError }
|
|
1544
|
+
: { requirement: statement, ok: true, error: undefined }
|
|
1545
|
+
}),
|
|
1546
|
+
).pipe(
|
|
516
1547
|
Effect.catchAllCause((cause) =>
|
|
517
|
-
Effect.succeed({
|
|
1548
|
+
Effect.succeed({
|
|
1549
|
+
requirement: statement,
|
|
1550
|
+
ok: false,
|
|
1551
|
+
error: messageOf(Cause.squash(cause)),
|
|
1552
|
+
}),
|
|
518
1553
|
),
|
|
519
1554
|
)
|
|
520
1555
|
})
|
|
521
1556
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
1557
|
+
export const executeProofEffect = (proof: Proof): Effect.Effect<ProofResult, never, never> =>
|
|
1558
|
+
proof.capture(
|
|
1559
|
+
<
|
|
1560
|
+
Comp extends ProductComposition,
|
|
1561
|
+
C extends UiContract,
|
|
1562
|
+
S extends ReadonlyArray<unknown>,
|
|
1563
|
+
Services,
|
|
1564
|
+
P,
|
|
1565
|
+
N extends string,
|
|
1566
|
+
Identity,
|
|
1567
|
+
>(
|
|
1568
|
+
exact: ProofCase<Comp, C, S, Services, P, N, Identity>,
|
|
1569
|
+
): Effect.Effect<ProofResult, never, never> => {
|
|
1570
|
+
const sink = makeSink(sceneInstrumentation(exact.scene))
|
|
1571
|
+
return executeProofWithSink<Comp, C, S, Services, P, N, Identity>(exact, exact.scene, sink)
|
|
1572
|
+
},
|
|
1573
|
+
)
|
|
1574
|
+
|
|
1575
|
+
export const executeProof = (proof: Proof): Promise<ProofResult> =>
|
|
1576
|
+
Effect.runPromise(executeProofEffect(proof))
|
|
1577
|
+
|
|
1578
|
+
const driveProofWithSink = Effect.fn('driveProofWithSink')(function* <
|
|
1579
|
+
Comp extends ProductComposition,
|
|
1580
|
+
C extends UiContract,
|
|
1581
|
+
S extends ReadonlyArray<unknown>,
|
|
1582
|
+
Services,
|
|
1583
|
+
P,
|
|
1584
|
+
N extends string,
|
|
1585
|
+
Identity,
|
|
1586
|
+
>(
|
|
1587
|
+
proof: ProofCase<Comp, C, S, Services, P, N, Identity>,
|
|
1588
|
+
scene: CapturedScene<C, S, Services, P, N, Identity>,
|
|
543
1589
|
sink: Sink,
|
|
544
|
-
): Effect.fn.Return<DriveResult, never,
|
|
1590
|
+
): Effect.fn.Return<DriveResult, never, never> {
|
|
545
1591
|
const frames = yield* Ref.make<ReadonlyArray<StepFrame>>([])
|
|
546
1592
|
const statement = proof.requirement.manifest.statement
|
|
547
|
-
//
|
|
548
|
-
// throwaway set so `makeFacadeEffect`'s contract is satisfied.
|
|
1593
|
+
// Driving tracks dispatches for the facade without enforcing coverage.
|
|
549
1594
|
const dispatched = new Set<string>()
|
|
550
1595
|
const record = (index: number): Effect.Effect<void> =>
|
|
551
1596
|
Ref.update(frames, (current) => [...current, { index, captures: [...sink.captures] }])
|
|
552
|
-
// Pump the generator: run each yielded effect on the runtime, snapshot, recur.
|
|
553
1597
|
const pump = Effect.fn('pump')(function* (
|
|
554
|
-
generator: ReturnType<
|
|
1598
|
+
generator: ReturnType<typeof proof.body>,
|
|
555
1599
|
step: PumpStep,
|
|
556
|
-
|
|
1600
|
+
settleEffect: Effect.Effect<void, never, never>,
|
|
1601
|
+
): Effect.fn.Return<void, unknown, never> {
|
|
557
1602
|
const next = generator.next(step.input)
|
|
558
1603
|
if (next.done === true) {
|
|
559
1604
|
return
|
|
560
1605
|
}
|
|
561
1606
|
const output = yield* yieldWrapGet(next.value)
|
|
1607
|
+
yield* settleEffect
|
|
562
1608
|
yield* record(step.index)
|
|
563
|
-
yield* pump(generator, { input: output, index: step.index + 1 })
|
|
1609
|
+
yield* pump(generator, { input: output, index: step.index + 1 }, settleEffect)
|
|
564
1610
|
})
|
|
565
|
-
return yield*
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
1611
|
+
return yield* withProofRuntimeTree(scene, sink, dispatched, (runtime, tree) =>
|
|
1612
|
+
Effect.gen(function* () {
|
|
1613
|
+
yield* verifyProofPlacement(proof, scene, runtime)
|
|
1614
|
+
yield* tree.settle
|
|
1615
|
+
yield* record(0)
|
|
1616
|
+
yield* pump(proof.body(tree.facade), { input: undefined, index: 1 }, tree.settle)
|
|
1617
|
+
return {
|
|
1618
|
+
requirement: statement,
|
|
1619
|
+
frames: yield* Ref.get(frames),
|
|
1620
|
+
ok: true,
|
|
1621
|
+
error: undefined,
|
|
1622
|
+
}
|
|
1623
|
+
}),
|
|
1624
|
+
).pipe(
|
|
573
1625
|
Effect.catchAllCause((cause) =>
|
|
574
1626
|
Ref.get(frames).pipe(
|
|
575
1627
|
Effect.map((collected) => ({
|
|
@@ -583,7 +1635,23 @@ const driveProofEffect = Effect.fn('driveProofEffect')(function* (
|
|
|
583
1635
|
)
|
|
584
1636
|
})
|
|
585
1637
|
|
|
586
|
-
export const
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
1638
|
+
export const driveProofEffect = (proof: Proof): Effect.Effect<DriveResult, never, never> =>
|
|
1639
|
+
proof.capture(
|
|
1640
|
+
<
|
|
1641
|
+
Comp extends ProductComposition,
|
|
1642
|
+
C extends UiContract,
|
|
1643
|
+
S extends ReadonlyArray<unknown>,
|
|
1644
|
+
Services,
|
|
1645
|
+
P,
|
|
1646
|
+
N extends string,
|
|
1647
|
+
Identity,
|
|
1648
|
+
>(
|
|
1649
|
+
exact: ProofCase<Comp, C, S, Services, P, N, Identity>,
|
|
1650
|
+
): Effect.Effect<DriveResult, never, never> => {
|
|
1651
|
+
const sink = makeSink(sceneInstrumentation(exact.scene))
|
|
1652
|
+
return driveProofWithSink<Comp, C, S, Services, P, N, Identity>(exact, exact.scene, sink)
|
|
1653
|
+
},
|
|
1654
|
+
)
|
|
1655
|
+
|
|
1656
|
+
export const driveProof = (proof: Proof): Promise<DriveResult> =>
|
|
1657
|
+
Effect.runPromise(driveProofEffect(proof))
|