@playfast/reform-proof 1.2.0 → 1.3.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/dist/assert.d.ts +15 -0
- package/dist/assert.d.ts.map +1 -0
- package/dist/assert.js +39 -0
- package/dist/assert.js.map +1 -0
- package/dist/engine.d.ts +16 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +119 -0
- package/dist/engine.js.map +1 -0
- package/dist/engineMounted.d.ts +18 -0
- package/dist/engineMounted.d.ts.map +1 -0
- package/dist/engineMounted.js +96 -0
- package/dist/engineMounted.js.map +1 -0
- package/dist/engineMounts.d.ts +13 -0
- package/dist/engineMounts.d.ts.map +1 -0
- package/dist/engineMounts.js +116 -0
- package/dist/engineMounts.js.map +1 -0
- package/dist/engineRender.d.ts +10 -0
- package/dist/engineRender.d.ts.map +1 -0
- package/dist/engineRender.js +64 -0
- package/dist/engineRender.js.map +1 -0
- package/dist/engineSink.d.ts +66 -0
- package/dist/engineSink.d.ts.map +1 -0
- package/dist/engineSink.js +72 -0
- package/dist/engineSink.js.map +1 -0
- package/dist/engineTree.d.ts +63 -0
- package/dist/engineTree.d.ts.map +1 -0
- package/dist/engineTree.js +137 -0
- package/dist/engineTree.js.map +1 -0
- package/dist/engineTreeDriver.d.ts +5 -0
- package/dist/engineTreeDriver.d.ts.map +1 -0
- package/dist/engineTreeDriver.js +187 -0
- package/dist/engineTreeDriver.js.map +1 -0
- package/dist/engineTreeFacade.d.ts +5 -0
- package/dist/engineTreeFacade.d.ts.map +1 -0
- package/dist/engineTreeFacade.js +105 -0
- package/dist/engineTreeFacade.js.map +1 -0
- package/dist/engineTreeTypes.d.ts +80 -0
- package/dist/engineTreeTypes.d.ts.map +1 -0
- package/dist/engineTreeTypes.js +11 -0
- package/dist/engineTreeTypes.js.map +1 -0
- package/dist/errors.d.ts +39 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +30 -0
- package/dist/errors.js.map +1 -0
- package/dist/facade.d.ts +54 -0
- package/dist/facade.d.ts.map +1 -0
- package/dist/facade.js +2 -0
- package/dist/facade.js.map +1 -0
- package/dist/fingerprint.d.ts +2 -0
- package/dist/fingerprint.d.ts.map +1 -0
- package/dist/fingerprint.js +55 -0
- package/dist/fingerprint.js.map +1 -0
- package/dist/index.d.ts +88 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +73 -0
- package/dist/index.js.map +1 -0
- package/dist/product.d.ts +56 -0
- package/dist/product.d.ts.map +1 -0
- package/dist/product.js +35 -0
- package/dist/product.js.map +1 -0
- package/dist/proofCase.d.ts +24 -0
- package/dist/proofCase.d.ts.map +1 -0
- package/dist/proofCase.js +2 -0
- package/dist/proofCase.js.map +1 -0
- package/dist/runner.d.ts +15 -0
- package/dist/runner.d.ts.map +1 -0
- package/dist/runner.js +13 -0
- package/dist/runner.js.map +1 -0
- package/package.json +17 -5
- package/src/assert.ts +65 -0
- package/src/engine.ts +30 -1402
- package/src/engineMounted.ts +271 -0
- package/src/engineMounts.ts +165 -0
- package/src/engineRender.ts +136 -0
- package/src/engineSink.ts +195 -0
- package/src/engineTree.ts +298 -0
- package/src/engineTreeDriver.ts +247 -0
- package/src/engineTreeFacade.ts +183 -0
- package/src/engineTreeTypes.ts +103 -0
- package/src/errors.ts +19 -0
- package/src/facade.ts +78 -0
- package/src/index.ts +61 -276
- package/src/product.ts +108 -0
- package/src/proofCase.ts +46 -0
- package/src/test-clock.test.ts +135 -0
package/src/engine.ts
CHANGED
|
@@ -1,1414 +1,41 @@
|
|
|
1
|
-
import {
|
|
2
|
-
Array as Arr,
|
|
3
|
-
Cause,
|
|
4
|
-
Context,
|
|
5
|
-
Effect,
|
|
6
|
-
Layer,
|
|
7
|
-
ManagedRuntime,
|
|
8
|
-
Match,
|
|
9
|
-
MutableRef,
|
|
10
|
-
Option,
|
|
11
|
-
Order,
|
|
12
|
-
Record as Rec,
|
|
13
|
-
Ref,
|
|
14
|
-
} from 'effect'
|
|
1
|
+
import { Cause, Effect, Option, Record as Rec, Ref } from 'effect'
|
|
15
2
|
import { yieldWrapGet } from 'effect/Utils'
|
|
16
|
-
import { AssertionFailed, UnknownAction, UnknownSlot } from './errors'
|
|
17
3
|
import {
|
|
18
4
|
type CapturedScene,
|
|
19
|
-
Composition,
|
|
20
5
|
type CompositionClass,
|
|
21
|
-
type CompositionService,
|
|
22
|
-
type FeatureLoadFailed,
|
|
23
|
-
isFeatureBinding,
|
|
24
|
-
isSlot,
|
|
25
|
-
isStructure,
|
|
26
6
|
makeAppRuntime,
|
|
27
|
-
publish,
|
|
28
|
-
type RuntimeHandle,
|
|
29
7
|
sceneInstrumentation,
|
|
30
|
-
type SlotFill,
|
|
31
|
-
type Structure,
|
|
32
|
-
type Trigger,
|
|
33
8
|
type UiContract,
|
|
34
9
|
} from '@playfast/reform'
|
|
10
|
+
import { type AnyComposition, type AnyScene } from '@playfast/reform/internal'
|
|
35
11
|
import {
|
|
36
|
-
|
|
37
|
-
type
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
type
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
} from '
|
|
48
|
-
import {
|
|
49
|
-
import type {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
type
|
|
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.
|
|
119
|
-
const SETTLE_DRAIN = 30
|
|
120
|
-
const SETTLE_STEP = '1 milli'
|
|
121
|
-
const SETTLE_MAX_RENDERS = 100
|
|
122
|
-
|
|
123
|
-
const settleDrain = Effect.yieldNow().pipe(Effect.repeatN(SETTLE_DRAIN))
|
|
124
|
-
|
|
125
|
-
const keyed = <V>(get: (key: string) => V): Record<string, V> => {
|
|
126
|
-
const target: Record<string, V> = Object.create(null)
|
|
127
|
-
return new Proxy(target, { get: (_target, key) => get(String(key)) })
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
const messageOf = (error: unknown): string =>
|
|
131
|
-
error instanceof Error ? error.message : String(error)
|
|
132
|
-
|
|
133
|
-
const uiNameOf = (comp: AnyComposition): string => comp.manifest.ui.manifest.name
|
|
134
|
-
|
|
135
|
-
// UiContract erases event values to never, so this boundary needs no assertion.
|
|
136
|
-
const eventsOf = (structure: Structure<UiContract>): Record<string, Trigger<unknown>> => ({
|
|
137
|
-
...structure.events,
|
|
138
|
-
})
|
|
139
|
-
|
|
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
|
-
|
|
194
|
-
export interface Sink {
|
|
195
|
-
readonly api: CaptureSinkApi
|
|
196
|
-
readonly captures: ReadonlyArray<CapturedRender>
|
|
197
|
-
readonly instrumentation: Instrumentation
|
|
198
|
-
reset(): void
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
export const makeSink = (instrumentation: Instrumentation = noopInstrumentation): Sink => {
|
|
202
|
-
// CaptureSink.record is synchronous, so this uses MutableRef rather than Ref.
|
|
203
|
-
const captures = MutableRef.make<CapturedRender[]>([])
|
|
204
|
-
return {
|
|
205
|
-
api: {
|
|
206
|
-
record: (capture) => {
|
|
207
|
-
MutableRef.update(captures, (current) => [...current, capture])
|
|
208
|
-
},
|
|
209
|
-
},
|
|
210
|
-
get captures() {
|
|
211
|
-
return MutableRef.get(captures)
|
|
212
|
-
},
|
|
213
|
-
instrumentation,
|
|
214
|
-
reset: () => {
|
|
215
|
-
MutableRef.set(captures, [])
|
|
216
|
-
},
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
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
|
-
}
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
interface Mounted {
|
|
262
|
-
readonly comp: AnyComposition
|
|
263
|
-
readonly props: unknown
|
|
264
|
-
readonly ref: NodeRef
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
interface SlotInstanceRef {
|
|
268
|
-
readonly name: string
|
|
269
|
-
readonly index: number
|
|
270
|
-
readonly key: Option.Option<string>
|
|
271
|
-
readonly keyOccurrence: number
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
interface NodeRef {
|
|
275
|
-
readonly name: string
|
|
276
|
-
readonly parent: Option.Option<NodeRef>
|
|
277
|
-
readonly slot: Option.Option<SlotInstanceRef>
|
|
278
|
-
}
|
|
279
|
-
|
|
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
|
-
|
|
351
|
-
interface SettleProgress {
|
|
352
|
-
readonly previous: string
|
|
353
|
-
readonly remaining: number
|
|
354
|
-
readonly stable: boolean
|
|
355
|
-
}
|
|
356
|
-
|
|
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
|
-
|
|
364
|
-
interface PumpStep {
|
|
365
|
-
readonly input: unknown
|
|
366
|
-
readonly index: number
|
|
367
|
-
}
|
|
368
|
-
|
|
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
|
-
})
|
|
391
|
-
|
|
392
|
-
interface RuntimeSlotRead {
|
|
393
|
-
readonly runtime: RenderRuntime
|
|
394
|
-
readonly slotDefinition: AnySlot
|
|
395
|
-
readonly parent: AnyComposition
|
|
396
|
-
}
|
|
397
|
-
|
|
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.
|
|
410
|
-
const isSlotFill = (candidate: unknown): candidate is SlotFill<unknown> =>
|
|
411
|
-
typeof candidate === 'object' &&
|
|
412
|
-
candidate !== null &&
|
|
413
|
-
'_tag' in candidate &&
|
|
414
|
-
(candidate._tag === 'Each' || candidate._tag === 'One' || candidate._tag === 'Absent')
|
|
415
|
-
|
|
416
|
-
const structureFills = (structure: Structure<UiContract>): Record<string, SlotFill<unknown>> =>
|
|
417
|
-
Rec.fromEntries(
|
|
418
|
-
Rec.toEntries(structure.slots).flatMap(
|
|
419
|
-
([slotName, fillValue]): ReadonlyArray<readonly [string, SlotFill<unknown>]> =>
|
|
420
|
-
isSlotFill(fillValue) ? [[slotName, fillValue]] : [],
|
|
421
|
-
),
|
|
422
|
-
)
|
|
423
|
-
|
|
424
|
-
const enqueueFill = (
|
|
425
|
-
parent: NodeRef,
|
|
426
|
-
slotName: string,
|
|
427
|
-
fill: SlotFill<unknown>,
|
|
428
|
-
child: AnyComposition,
|
|
429
|
-
): ReadonlyArray<Mounted> =>
|
|
430
|
-
Match.value(fill).pipe(
|
|
431
|
-
Match.when({ _tag: 'Each' }, (each) =>
|
|
432
|
-
each.items.map((entry, index) => ({
|
|
433
|
-
comp: child,
|
|
434
|
-
props: entry.props,
|
|
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
|
-
},
|
|
447
|
-
})),
|
|
448
|
-
),
|
|
449
|
-
Match.when({ _tag: 'One' }, (oneFill) => [
|
|
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
|
-
},
|
|
464
|
-
]),
|
|
465
|
-
Match.when({ _tag: 'Absent' }, () => []),
|
|
466
|
-
Match.exhaustive,
|
|
467
|
-
)
|
|
468
|
-
|
|
469
|
-
const driveStructure = (
|
|
470
|
-
comp: AnyComposition,
|
|
471
|
-
structure: Structure<UiContract>,
|
|
472
|
-
ref: NodeRef,
|
|
473
|
-
sink: Sink,
|
|
474
|
-
lookup: CaptureLookup,
|
|
475
|
-
bindings: ReadonlyMap<AnySlot, AnyComposition>,
|
|
476
|
-
): ReadonlyArray<Mounted> => {
|
|
477
|
-
const key = Option.flatMap(ref.slot, (instance) => instance.key)
|
|
478
|
-
const capture: CapturedRender = {
|
|
479
|
-
name: uiNameOf(comp),
|
|
480
|
-
props: structure.props,
|
|
481
|
-
events: eventsOf(structure),
|
|
482
|
-
...(Option.isSome(key) ? { key: key.value } : {}),
|
|
483
|
-
}
|
|
484
|
-
sink.api.record(capture)
|
|
485
|
-
lookup.record(ref, capture)
|
|
486
|
-
const fills = structureFills(structure)
|
|
487
|
-
return Rec.toEntries(slotsOf(comp)).flatMap(([slotName, slotClass]) => {
|
|
488
|
-
const child = bindings.get(slotClass)
|
|
489
|
-
const fill = fills[slotName]
|
|
490
|
-
return child === undefined || fill === undefined ? [] : enqueueFill(ref, slotName, fill, child)
|
|
491
|
-
})
|
|
492
|
-
}
|
|
493
|
-
|
|
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,
|
|
550
|
-
mounted: Mounted,
|
|
551
|
-
bindings: ReadonlyMap<AnySlot, AnyComposition>,
|
|
552
|
-
sink: Sink,
|
|
553
|
-
lookup: CaptureLookup,
|
|
554
|
-
): ReadonlyArray<Mounted> => {
|
|
555
|
-
const endRenderSpan = sink.instrumentation.uiRendered(uiNameOf(mounted.comp))
|
|
556
|
-
const frame = renderCompositionRuntime(runtime, mounted.comp, mounted.props)
|
|
557
|
-
endRenderSpan()
|
|
558
|
-
return driveStructure(mounted.comp, frame, mounted.ref, sink, lookup, bindings)
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
const renderLevelRuntime = (
|
|
562
|
-
runtime: RenderRuntime,
|
|
563
|
-
frontier: ReadonlyArray<Mounted>,
|
|
564
|
-
bindings: ReadonlyMap<AnySlot, AnyComposition>,
|
|
565
|
-
sink: Sink,
|
|
566
|
-
lookup: CaptureLookup,
|
|
567
|
-
): void => {
|
|
568
|
-
if (frontier.length === 0) {
|
|
569
|
-
return
|
|
570
|
-
}
|
|
571
|
-
const levels = frontier.map((mounted) =>
|
|
572
|
-
renderMountedRuntime(runtime, mounted, bindings, sink, lookup),
|
|
573
|
-
)
|
|
574
|
-
renderLevelRuntime(runtime, levels.flat(), bindings, sink, lookup)
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
const renderTreeRuntime = (
|
|
578
|
-
runtime: RenderRuntime,
|
|
579
|
-
root: AnyComposition,
|
|
580
|
-
bindings: ReadonlyMap<AnySlot, AnyComposition>,
|
|
581
|
-
sink: Sink,
|
|
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
|
-
})
|
|
595
|
-
|
|
596
|
-
const settleTreeRuntime = Effect.fn('settleTreeRuntime')(function* (
|
|
597
|
-
runtime: RenderRuntime,
|
|
598
|
-
root: AnyComposition,
|
|
599
|
-
bindings: ReadonlyMap<AnySlot, AnyComposition>,
|
|
600
|
-
sink: Sink,
|
|
601
|
-
lookup: CaptureLookup,
|
|
602
|
-
): Effect.fn.Return<void, never, never> {
|
|
603
|
-
yield* settleDrain
|
|
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
|
-
},
|
|
611
|
-
{
|
|
612
|
-
while: (progress: SettleProgress) => !progress.stable && progress.remaining > 0,
|
|
613
|
-
body: (progress) =>
|
|
614
|
-
Effect.gen(function* () {
|
|
615
|
-
yield* settleDrain
|
|
616
|
-
yield* Effect.sleep(SETTLE_STEP)
|
|
617
|
-
yield* renderTreeRuntime(runtime, root, bindings, sink, lookup)
|
|
618
|
-
const current = fingerprint(lookup)
|
|
619
|
-
return {
|
|
620
|
-
previous: current,
|
|
621
|
-
remaining: progress.remaining - 1,
|
|
622
|
-
stable: current === progress.previous,
|
|
623
|
-
}
|
|
624
|
-
}),
|
|
625
|
-
},
|
|
626
|
-
)
|
|
627
|
-
yield* ensureSettled(progress)
|
|
628
|
-
})
|
|
629
|
-
|
|
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>,
|
|
639
|
-
sink: Sink,
|
|
640
|
-
dispatched: Set<string>,
|
|
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)
|
|
664
|
-
|
|
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
|
-
)
|
|
690
|
-
|
|
691
|
-
const nodeFacade = (ref: NodeRef, comp: AnyComposition): MountedFacadeErasure => ({
|
|
692
|
-
props: propsFor(ref),
|
|
693
|
-
expectProps: (partial) =>
|
|
694
|
-
propsFor(ref).pipe(
|
|
695
|
-
Effect.flatMap((props) => expectMatchingProps({ actual: props, expected: partial })),
|
|
696
|
-
),
|
|
697
|
-
actions: keyed(
|
|
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
|
-
}),
|
|
706
|
-
),
|
|
707
|
-
Effect.flatMap(() => settle),
|
|
708
|
-
Effect.flatMap(() => propsFor(ref)),
|
|
709
|
-
),
|
|
710
|
-
),
|
|
711
|
-
slots: keyed((slotName) => slotFacade(ref, comp, slotName)),
|
|
712
|
-
frame: settle,
|
|
713
|
-
})
|
|
714
|
-
|
|
715
|
-
const slotFacade = (
|
|
716
|
-
parentRef: NodeRef,
|
|
717
|
-
parent: AnyComposition,
|
|
718
|
-
slotName: string,
|
|
719
|
-
): MountedSlotFacadeErasure => {
|
|
720
|
-
const slotClass = slotsOf(parent)[slotName]
|
|
721
|
-
const child = slotClass && bindings.get(slotClass)
|
|
722
|
-
if (child === undefined) {
|
|
723
|
-
return Effect.runSync(
|
|
724
|
-
Effect.dieMessage(new UnknownSlot({ parent: uiNameOf(parent), slot: slotName }).message),
|
|
725
|
-
)
|
|
726
|
-
}
|
|
727
|
-
const childName = uiNameOf(child)
|
|
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)
|
|
742
|
-
return {
|
|
743
|
-
first: atIndex(0),
|
|
744
|
-
at: atIndex,
|
|
745
|
-
all: Effect.map(rerender, () =>
|
|
746
|
-
slotCapturesFor(lookup, parentRef, slotName).map((entry) => nodeFacade(entry.ref, child)),
|
|
747
|
-
),
|
|
748
|
-
byKey: (key) =>
|
|
749
|
-
Effect.flatMap(rerender, () =>
|
|
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
|
-
),
|
|
766
|
-
),
|
|
767
|
-
where: (predicate) =>
|
|
768
|
-
Effect.map(rerender, () =>
|
|
769
|
-
slotCapturesFor(lookup, parentRef, slotName).flatMap((entry) =>
|
|
770
|
-
predicate(entry.capture.props) ? [nodeFacade(entry.ref, child)] : [],
|
|
771
|
-
),
|
|
772
|
-
),
|
|
773
|
-
count: Effect.map(rerender, () => slotCapturesFor(lookup, parentRef, slotName).length),
|
|
774
|
-
props: first.props,
|
|
775
|
-
expectProps: first.expectProps,
|
|
776
|
-
actions: first.actions,
|
|
777
|
-
slots: first.slots,
|
|
778
|
-
frame: first.frame,
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
|
-
|
|
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
|
-
}
|
|
806
|
-
|
|
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>,
|
|
818
|
-
sink: Sink,
|
|
819
|
-
dispatched: Set<string>,
|
|
820
|
-
): {
|
|
821
|
-
readonly facade: MountedFacade<C>
|
|
822
|
-
readonly settle: Effect.Effect<void, never, never>
|
|
823
|
-
} => makeMountedFacade(runtime, root, sink, dispatched, directSettle)
|
|
824
|
-
|
|
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
|
-
}
|
|
12
|
+
invalidCompositionProps,
|
|
13
|
+
type HostRuntime,
|
|
14
|
+
makeSink,
|
|
15
|
+
messageOf,
|
|
16
|
+
type RenderRuntime,
|
|
17
|
+
type Sink,
|
|
18
|
+
} from './engineSink'
|
|
19
|
+
import { childComposition, readRuntimeSlotChild, slotsOf } from './engineTree'
|
|
20
|
+
import type { RuntimeTreeFacade } from './engineTreeTypes'
|
|
21
|
+
import { makeRuntimeTreeFacade } from './engineTreeFacade'
|
|
22
|
+
import type { ProofCase, ProofResult } from './proofCase'
|
|
23
|
+
import type { PumpStep } from './engineTree'
|
|
24
|
+
import type { DriveResult, StepFrame } from './index'
|
|
25
|
+
import type { ProductComposition } from './product'
|
|
26
|
+
import type { Proof } from './index'
|
|
27
|
+
|
|
28
|
+
export { makeSink } from './engineSink'
|
|
29
|
+
export type { Sink } from './engineSink'
|
|
30
|
+
export { proofLayer } from './engineTree'
|
|
31
|
+
export { makeFacade, makeProofFacade, makeRuntimeHandleFacade } from './engineMounted'
|
|
32
|
+
export type {
|
|
33
|
+
MountedFacade,
|
|
34
|
+
MountedSlotFacade,
|
|
35
|
+
MountedSlotFacadesOf,
|
|
36
|
+
} from './engineSink'
|
|
37
|
+
export { makeRuntimeTreeFacade } from './engineTreeFacade'
|
|
38
|
+
export type { RuntimeTreeFacade } from './engineTreeTypes'
|
|
1412
39
|
|
|
1413
40
|
const proofRootProps = <
|
|
1414
41
|
P,
|
|
@@ -1655,3 +282,4 @@ export const driveProofEffect = (proof: Proof): Effect.Effect<DriveResult, never
|
|
|
1655
282
|
|
|
1656
283
|
export const driveProof = (proof: Proof): Promise<DriveResult> =>
|
|
1657
284
|
Effect.runPromise(driveProofEffect(proof))
|
|
285
|
+
|