@playfast/reform-proof 1.2.0 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- 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
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import { Array as Arr, type Duration, Effect, ManagedRuntime, Option, TestClock } from 'effect'
|
|
2
|
+
import { NoTestClock, UnknownAction, UnknownSlot } from './errors'
|
|
3
|
+
import {
|
|
4
|
+
type CapturedScene,
|
|
5
|
+
type CompositionClass,
|
|
6
|
+
type RuntimeHandle,
|
|
7
|
+
type Trigger,
|
|
8
|
+
type UiContract,
|
|
9
|
+
} from '@playfast/reform'
|
|
10
|
+
import { type AnyComposition, CaptureSink } from '@playfast/reform/internal'
|
|
11
|
+
import type { Facade } from './facade'
|
|
12
|
+
import {
|
|
13
|
+
directSettle,
|
|
14
|
+
expectMatchingProps,
|
|
15
|
+
keyed,
|
|
16
|
+
type MountedFacade,
|
|
17
|
+
type MountedFacadeErasure,
|
|
18
|
+
type MountedFacadeHandleErasure,
|
|
19
|
+
type MountedSlotFacadeErasure,
|
|
20
|
+
type RenderRuntime,
|
|
21
|
+
type RuntimeRootComposition,
|
|
22
|
+
type SettleBoundary,
|
|
23
|
+
type Sink,
|
|
24
|
+
uiNameOf,
|
|
25
|
+
} from './engineSink'
|
|
26
|
+
import {
|
|
27
|
+
makeCaptureLookup,
|
|
28
|
+
nodeCaptureFor,
|
|
29
|
+
type NodeRef,
|
|
30
|
+
renderRuntimeFromContext,
|
|
31
|
+
rootRef,
|
|
32
|
+
slotCapturesFor,
|
|
33
|
+
slotsOf,
|
|
34
|
+
} from './engineTree'
|
|
35
|
+
import {
|
|
36
|
+
collectRuntimeBindings,
|
|
37
|
+
renderTreeRuntime,
|
|
38
|
+
settleTreeRuntime,
|
|
39
|
+
} from './engineRender'
|
|
40
|
+
|
|
41
|
+
export function makeFacade<
|
|
42
|
+
C extends UiContract,
|
|
43
|
+
S extends ReadonlyArray<unknown>,
|
|
44
|
+
Services,
|
|
45
|
+
P,
|
|
46
|
+
N extends string,
|
|
47
|
+
Identity,
|
|
48
|
+
>(
|
|
49
|
+
runtime: ManagedRuntime.ManagedRuntime<Services | CaptureSink, never>,
|
|
50
|
+
scene: CapturedScene<C, S, Services, P, N, Identity>,
|
|
51
|
+
sink: Sink,
|
|
52
|
+
dispatched: Set<string>,
|
|
53
|
+
): {
|
|
54
|
+
readonly facade: Facade<C, never>
|
|
55
|
+
readonly settle: Effect.Effect<void, never, never>
|
|
56
|
+
} {
|
|
57
|
+
const context = runtime.runSync(Effect.context<Services | CaptureSink>())
|
|
58
|
+
return makeMountedFacade(
|
|
59
|
+
renderRuntimeFromContext(context, sink.instrumentation),
|
|
60
|
+
scene.composition,
|
|
61
|
+
sink,
|
|
62
|
+
dispatched,
|
|
63
|
+
(settlement) => Effect.promise(() => runtime.runPromise(settlement)),
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function makeMountedFacade<
|
|
68
|
+
P,
|
|
69
|
+
C extends UiContract,
|
|
70
|
+
S extends ReadonlyArray<unknown>,
|
|
71
|
+
N extends string,
|
|
72
|
+
Identity,
|
|
73
|
+
>(
|
|
74
|
+
runtime: RenderRuntime,
|
|
75
|
+
root: CompositionClass<P, C, S, N, Identity>,
|
|
76
|
+
sink: Sink,
|
|
77
|
+
dispatched: Set<string>,
|
|
78
|
+
settleBoundary: SettleBoundary,
|
|
79
|
+
): {
|
|
80
|
+
readonly facade: MountedFacade<C>
|
|
81
|
+
readonly settle: Effect.Effect<void, never, never>
|
|
82
|
+
}
|
|
83
|
+
function makeMountedFacade(
|
|
84
|
+
runtime: RenderRuntime,
|
|
85
|
+
root: AnyComposition,
|
|
86
|
+
sink: Sink,
|
|
87
|
+
dispatched: Set<string>,
|
|
88
|
+
settleBoundary: SettleBoundary,
|
|
89
|
+
): MountedFacadeHandleErasure
|
|
90
|
+
function makeMountedFacade(
|
|
91
|
+
runtime: RenderRuntime,
|
|
92
|
+
root: AnyComposition,
|
|
93
|
+
sink: Sink,
|
|
94
|
+
dispatched: Set<string>,
|
|
95
|
+
settleBoundary: SettleBoundary,
|
|
96
|
+
): MountedFacadeHandleErasure {
|
|
97
|
+
const bindings = collectRuntimeBindings(root, runtime)
|
|
98
|
+
const lookup = makeCaptureLookup()
|
|
99
|
+
const settle = settleBoundary(settleTreeRuntime(runtime, root, bindings, sink, lookup))
|
|
100
|
+
const rerender = renderTreeRuntime(runtime, root, bindings, sink, lookup)
|
|
101
|
+
|
|
102
|
+
// Move the scene's virtual clock, then settle the tree the woken fibers dirty.
|
|
103
|
+
// The clock is read off the runtime the scene's procedures run on, so adjusting it
|
|
104
|
+
// resumes their `Effect.sleep`s even though the proof body drives from outside.
|
|
105
|
+
const advance = (duration: Duration.DurationInput): Effect.Effect<void, never, never> =>
|
|
106
|
+
Option.match(runtime.readOption(TestClock.TestClock), {
|
|
107
|
+
onNone: () => Effect.dieMessage(new NoTestClock().message),
|
|
108
|
+
onSome: (clock) => clock.adjust(duration).pipe(Effect.flatMap(() => settle)),
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
const triggerOf = (ref: NodeRef, event: string): Effect.Effect<Trigger<unknown>, never, never> =>
|
|
112
|
+
Option.match(
|
|
113
|
+
Option.flatMap(nodeCaptureFor(lookup, ref), (capture) =>
|
|
114
|
+
Option.fromNullable(capture.events[event]),
|
|
115
|
+
),
|
|
116
|
+
{
|
|
117
|
+
onNone: () =>
|
|
118
|
+
Effect.dieMessage(
|
|
119
|
+
new UnknownAction({
|
|
120
|
+
composition: ref.name,
|
|
121
|
+
action: event,
|
|
122
|
+
rendered: lookup.captures.filter((entry) => entry.ref.name === ref.name).length,
|
|
123
|
+
}).message,
|
|
124
|
+
),
|
|
125
|
+
onSome: Effect.succeed,
|
|
126
|
+
},
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
const propsFor = (ref: NodeRef): Effect.Effect<unknown, never, never> =>
|
|
130
|
+
Effect.map(rerender, () =>
|
|
131
|
+
Option.match(nodeCaptureFor(lookup, ref), {
|
|
132
|
+
onNone: () => undefined,
|
|
133
|
+
onSome: (capture) => capture.props,
|
|
134
|
+
}),
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
const nodeFacade = (ref: NodeRef, comp: AnyComposition): MountedFacadeErasure => ({
|
|
138
|
+
props: propsFor(ref),
|
|
139
|
+
expectProps: (partial) =>
|
|
140
|
+
propsFor(ref).pipe(
|
|
141
|
+
Effect.flatMap((props) => expectMatchingProps({ actual: props, expected: partial })),
|
|
142
|
+
),
|
|
143
|
+
actions: keyed(
|
|
144
|
+
(event) => (payload: unknown) =>
|
|
145
|
+
rerender.pipe(
|
|
146
|
+
Effect.flatMap(() => triggerOf(ref, event)),
|
|
147
|
+
Effect.flatMap((trigger) =>
|
|
148
|
+
Effect.sync(() => {
|
|
149
|
+
dispatched.add(event)
|
|
150
|
+
trigger(payload)
|
|
151
|
+
}),
|
|
152
|
+
),
|
|
153
|
+
Effect.flatMap(() => settle),
|
|
154
|
+
Effect.flatMap(() => propsFor(ref)),
|
|
155
|
+
),
|
|
156
|
+
),
|
|
157
|
+
slots: keyed((slotName) => slotFacade(ref, comp, slotName)),
|
|
158
|
+
frame: settle,
|
|
159
|
+
advance,
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
const slotFacade = (
|
|
163
|
+
parentRef: NodeRef,
|
|
164
|
+
parent: AnyComposition,
|
|
165
|
+
slotName: string,
|
|
166
|
+
): MountedSlotFacadeErasure => {
|
|
167
|
+
const slotClass = slotsOf(parent)[slotName]
|
|
168
|
+
const child = slotClass && bindings.get(slotClass)
|
|
169
|
+
if (child === undefined) {
|
|
170
|
+
return Effect.runSync(
|
|
171
|
+
Effect.dieMessage(new UnknownSlot({ parent: uiNameOf(parent), slot: slotName }).message),
|
|
172
|
+
)
|
|
173
|
+
}
|
|
174
|
+
const childName = uiNameOf(child)
|
|
175
|
+
const childRefAt = (index: number): NodeRef =>
|
|
176
|
+
slotCapturesFor(lookup, parentRef, slotName)[index]?.ref ?? {
|
|
177
|
+
name: childName,
|
|
178
|
+
parent: Option.some(parentRef),
|
|
179
|
+
slot: Option.some({
|
|
180
|
+
name: slotName,
|
|
181
|
+
index,
|
|
182
|
+
key: Option.none(),
|
|
183
|
+
keyOccurrence: 0,
|
|
184
|
+
}),
|
|
185
|
+
}
|
|
186
|
+
const atIndex = (index: number): Effect.Effect<MountedFacadeErasure, never, never> =>
|
|
187
|
+
Effect.as(rerender, nodeFacade(childRefAt(index), child))
|
|
188
|
+
const first = nodeFacade(childRefAt(0), child)
|
|
189
|
+
return {
|
|
190
|
+
first: atIndex(0),
|
|
191
|
+
at: atIndex,
|
|
192
|
+
all: Effect.map(rerender, () =>
|
|
193
|
+
slotCapturesFor(lookup, parentRef, slotName).map((entry) => nodeFacade(entry.ref, child)),
|
|
194
|
+
),
|
|
195
|
+
byKey: (key) =>
|
|
196
|
+
Effect.flatMap(rerender, () =>
|
|
197
|
+
Option.match(
|
|
198
|
+
Arr.findFirst(
|
|
199
|
+
slotCapturesFor(lookup, parentRef, slotName),
|
|
200
|
+
(entry) => entry.capture.key === key,
|
|
201
|
+
),
|
|
202
|
+
{
|
|
203
|
+
onNone: () =>
|
|
204
|
+
Effect.dieMessage(
|
|
205
|
+
new UnknownSlot({
|
|
206
|
+
parent: uiNameOf(parent),
|
|
207
|
+
slot: `${slotName}[key=${key}]`,
|
|
208
|
+
}).message,
|
|
209
|
+
),
|
|
210
|
+
onSome: (entry) => Effect.succeed(nodeFacade(entry.ref, child)),
|
|
211
|
+
},
|
|
212
|
+
),
|
|
213
|
+
),
|
|
214
|
+
where: (predicate) =>
|
|
215
|
+
Effect.map(rerender, () =>
|
|
216
|
+
slotCapturesFor(lookup, parentRef, slotName).flatMap((entry) =>
|
|
217
|
+
predicate(entry.capture.props) ? [nodeFacade(entry.ref, child)] : [],
|
|
218
|
+
),
|
|
219
|
+
),
|
|
220
|
+
count: Effect.map(rerender, () => slotCapturesFor(lookup, parentRef, slotName).length),
|
|
221
|
+
props: first.props,
|
|
222
|
+
expectProps: first.expectProps,
|
|
223
|
+
actions: first.actions,
|
|
224
|
+
slots: first.slots,
|
|
225
|
+
frame: first.frame,
|
|
226
|
+
advance: first.advance,
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
facade: nodeFacade(rootRef(root), root),
|
|
232
|
+
settle,
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export const makeProofFacade = <
|
|
237
|
+
C extends UiContract,
|
|
238
|
+
S extends ReadonlyArray<unknown>,
|
|
239
|
+
Services,
|
|
240
|
+
P,
|
|
241
|
+
N extends string,
|
|
242
|
+
Identity,
|
|
243
|
+
>(
|
|
244
|
+
runtime: ManagedRuntime.ManagedRuntime<Services | CaptureSink, never>,
|
|
245
|
+
scene: CapturedScene<C, S, Services, P, N, Identity>,
|
|
246
|
+
sink: Sink,
|
|
247
|
+
dispatched: Set<string>,
|
|
248
|
+
): {
|
|
249
|
+
readonly facade: Facade<C, never>
|
|
250
|
+
readonly settle: Effect.Effect<void, never, never>
|
|
251
|
+
} => {
|
|
252
|
+
return makeFacade(runtime, scene, sink, dispatched)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export const makeRuntimeHandleFacade = <
|
|
256
|
+
Services,
|
|
257
|
+
RootIdentifier extends NoInfer<Services>,
|
|
258
|
+
P,
|
|
259
|
+
C extends UiContract,
|
|
260
|
+
S extends ReadonlyArray<unknown>,
|
|
261
|
+
N extends string,
|
|
262
|
+
Identity,
|
|
263
|
+
>(
|
|
264
|
+
runtime: RuntimeHandle<Services>,
|
|
265
|
+
root: RuntimeRootComposition<RootIdentifier, P, C, S, N, Identity>,
|
|
266
|
+
sink: Sink,
|
|
267
|
+
dispatched: Set<string>,
|
|
268
|
+
): {
|
|
269
|
+
readonly facade: MountedFacade<C>
|
|
270
|
+
readonly settle: Effect.Effect<void, never, never>
|
|
271
|
+
} => makeMountedFacade(runtime, root, sink, dispatched, directSettle)
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { Array as Arr, Order } from 'effect'
|
|
2
|
+
import type { HostRuntime } from './engineSink'
|
|
3
|
+
import {
|
|
4
|
+
encodeIdentityPart,
|
|
5
|
+
type FeatureRecordInput,
|
|
6
|
+
type MountIdentityInput,
|
|
7
|
+
type TreeFeatureMount,
|
|
8
|
+
} from './engineTreeTypes'
|
|
9
|
+
|
|
10
|
+
export interface RegistryStatus {
|
|
11
|
+
disposed: boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface FeatureMountRegistry {
|
|
15
|
+
readonly mountId: (input: MountIdentityInput) => string
|
|
16
|
+
readonly retryMount: (record: TreeFeatureMount) => void
|
|
17
|
+
readonly recordForRender: (
|
|
18
|
+
input: FeatureRecordInput,
|
|
19
|
+
mountMissing: boolean,
|
|
20
|
+
) => TreeFeatureMount | undefined
|
|
21
|
+
readonly disposeStale: (seen: ReadonlySet<string>) => void
|
|
22
|
+
readonly disposeAll: () => void
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Owns every mounted Feature scope: identity, (re)mount attempts, and deepest-first teardown.
|
|
26
|
+
export const makeFeatureMountRegistry = (status: RegistryStatus): FeatureMountRegistry => {
|
|
27
|
+
const mounts = new Map<string, TreeFeatureMount>()
|
|
28
|
+
const runtimeIds = new WeakMap<HostRuntime, number>()
|
|
29
|
+
const bindingIds = new WeakMap<object, number>()
|
|
30
|
+
const sequence = { runtime: 0, binding: 0 }
|
|
31
|
+
|
|
32
|
+
const identity = <Subject extends object>(
|
|
33
|
+
identities: WeakMap<Subject, number>,
|
|
34
|
+
subject: Subject,
|
|
35
|
+
next: () => number,
|
|
36
|
+
): number => {
|
|
37
|
+
const current = identities.get(subject)
|
|
38
|
+
if (current !== undefined) {
|
|
39
|
+
return current
|
|
40
|
+
}
|
|
41
|
+
const created = next()
|
|
42
|
+
identities.set(subject, created)
|
|
43
|
+
return created
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const mountId = ({ parent, parentPath, slotName, key, binding }: MountIdentityInput): string => {
|
|
47
|
+
const runtimeId = identity(runtimeIds, parent, () => ++sequence.runtime)
|
|
48
|
+
const bindingId = identity(bindingIds, binding, () => ++sequence.binding)
|
|
49
|
+
return [String(runtimeId), parentPath, slotName, String(bindingId), key]
|
|
50
|
+
.map(encodeIdentityPart)
|
|
51
|
+
.join('')
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const startMount = (record: TreeFeatureMount): void => {
|
|
55
|
+
record.attempt += 1
|
|
56
|
+
const attempt = record.attempt
|
|
57
|
+
record.binding.capture<void>((binding) => {
|
|
58
|
+
if (binding.strategy === 'default') {
|
|
59
|
+
record.state = { _tag: 'Live', runtime: record.parent }
|
|
60
|
+
binding.boot.forEach((event) => record.parent.dispatch('High', event))
|
|
61
|
+
record.dispose = () => {}
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
record.state = { _tag: 'Loading' }
|
|
65
|
+
record.dispose = record.parent.mountAnyFeature(binding, {
|
|
66
|
+
props: record.props,
|
|
67
|
+
onLive: (childRuntime) => {
|
|
68
|
+
if (status.disposed || record.attempt !== attempt || mounts.get(record.id) !== record) {
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
record.state = { _tag: 'Live', runtime: childRuntime }
|
|
72
|
+
},
|
|
73
|
+
onFailed: (error) => {
|
|
74
|
+
if (status.disposed || record.attempt !== attempt || mounts.get(record.id) !== record) {
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
record.state = { _tag: 'Failed', error }
|
|
78
|
+
},
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const retryMount = (record: TreeFeatureMount): void => {
|
|
84
|
+
if (status.disposed || mounts.get(record.id) !== record || record.state._tag !== 'Failed') {
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
record.dispose()
|
|
88
|
+
startMount(record)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const featureRecord = ({
|
|
92
|
+
id,
|
|
93
|
+
parent,
|
|
94
|
+
binding,
|
|
95
|
+
depth,
|
|
96
|
+
props,
|
|
97
|
+
}: FeatureRecordInput): TreeFeatureMount => {
|
|
98
|
+
const current = mounts.get(id)
|
|
99
|
+
if (current !== undefined && current.binding === binding && current.parent === parent) {
|
|
100
|
+
return current
|
|
101
|
+
}
|
|
102
|
+
if (current !== undefined) {
|
|
103
|
+
current.attempt += 1
|
|
104
|
+
current.dispose()
|
|
105
|
+
}
|
|
106
|
+
const created: TreeFeatureMount = {
|
|
107
|
+
id,
|
|
108
|
+
binding,
|
|
109
|
+
parent,
|
|
110
|
+
depth,
|
|
111
|
+
props,
|
|
112
|
+
attempt: 0,
|
|
113
|
+
state: { _tag: 'Loading' },
|
|
114
|
+
dispose: () => {},
|
|
115
|
+
}
|
|
116
|
+
mounts.set(id, created)
|
|
117
|
+
startMount(created)
|
|
118
|
+
return created
|
|
119
|
+
}
|
|
120
|
+
const featureRecordForRender = (
|
|
121
|
+
input: FeatureRecordInput,
|
|
122
|
+
mountMissing: boolean,
|
|
123
|
+
): TreeFeatureMount | undefined => {
|
|
124
|
+
const current = mounts.get(input.id)
|
|
125
|
+
if (current !== undefined || !mountMissing) {
|
|
126
|
+
return current
|
|
127
|
+
}
|
|
128
|
+
return featureRecord(input)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const disposeStale = (seen: ReadonlySet<string>): void => {
|
|
132
|
+
Arr.sortWith(
|
|
133
|
+
Array.from(mounts.values()).filter((record) => !seen.has(record.id)),
|
|
134
|
+
(record) => -record.depth,
|
|
135
|
+
Order.number,
|
|
136
|
+
).forEach((record) => {
|
|
137
|
+
mounts.delete(record.id)
|
|
138
|
+
record.attempt += 1
|
|
139
|
+
record.dispose()
|
|
140
|
+
})
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const disposeAll = (): void => {
|
|
144
|
+
if (status.disposed) {
|
|
145
|
+
return
|
|
146
|
+
}
|
|
147
|
+
status.disposed = true
|
|
148
|
+
Arr.sortWith(Array.from(mounts.values()), (record) => -record.depth, Order.number).forEach(
|
|
149
|
+
(record) => {
|
|
150
|
+
record.attempt += 1
|
|
151
|
+
record.dispose()
|
|
152
|
+
},
|
|
153
|
+
)
|
|
154
|
+
mounts.clear()
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
mountId,
|
|
160
|
+
retryMount,
|
|
161
|
+
recordForRender: featureRecordForRender,
|
|
162
|
+
disposeStale,
|
|
163
|
+
disposeAll,
|
|
164
|
+
}
|
|
165
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { Effect, Record as Rec } from 'effect'
|
|
2
|
+
import type { AnyComposition, AnySlot } from '@playfast/reform/internal'
|
|
3
|
+
import {
|
|
4
|
+
type RenderRuntime,
|
|
5
|
+
renderCompositionRuntime,
|
|
6
|
+
SETTLE_MAX_RENDERS,
|
|
7
|
+
settleDrain,
|
|
8
|
+
settleStep,
|
|
9
|
+
type Sink,
|
|
10
|
+
uiNameOf,
|
|
11
|
+
} from './engineSink'
|
|
12
|
+
import {
|
|
13
|
+
type CaptureLookup,
|
|
14
|
+
childComposition,
|
|
15
|
+
driveStructure,
|
|
16
|
+
ensureSettled,
|
|
17
|
+
fingerprint,
|
|
18
|
+
type Mounted,
|
|
19
|
+
readRuntimeSlotChild,
|
|
20
|
+
rootRef,
|
|
21
|
+
type SettleProgress,
|
|
22
|
+
slotsOf,
|
|
23
|
+
} from './engineTree'
|
|
24
|
+
|
|
25
|
+
export const collectRuntimeBindings = (
|
|
26
|
+
root: AnyComposition,
|
|
27
|
+
runtime: RenderRuntime,
|
|
28
|
+
): Map<AnySlot, AnyComposition> => {
|
|
29
|
+
const bindings = new Map<AnySlot, AnyComposition>()
|
|
30
|
+
const walk = (comp: AnyComposition): void => {
|
|
31
|
+
Rec.values(slotsOf(comp)).forEach((slotClass) => {
|
|
32
|
+
if (bindings.has(slotClass)) {
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
const child = childComposition(
|
|
36
|
+
readRuntimeSlotChild({
|
|
37
|
+
runtime,
|
|
38
|
+
slotDefinition: slotClass,
|
|
39
|
+
parent: comp,
|
|
40
|
+
}),
|
|
41
|
+
)
|
|
42
|
+
bindings.set(slotClass, child)
|
|
43
|
+
walk(child)
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
walk(root)
|
|
47
|
+
return bindings
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const renderMountedRuntime = (
|
|
51
|
+
runtime: RenderRuntime,
|
|
52
|
+
mounted: Mounted,
|
|
53
|
+
bindings: ReadonlyMap<AnySlot, AnyComposition>,
|
|
54
|
+
sink: Sink,
|
|
55
|
+
lookup: CaptureLookup,
|
|
56
|
+
): ReadonlyArray<Mounted> => {
|
|
57
|
+
const endRenderSpan = sink.instrumentation.uiRendered(uiNameOf(mounted.comp))
|
|
58
|
+
const frame = renderCompositionRuntime(runtime, mounted.comp, mounted.props)
|
|
59
|
+
endRenderSpan()
|
|
60
|
+
return driveStructure(mounted.comp, frame, mounted.ref, sink, lookup, bindings)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export const renderLevelRuntime = (
|
|
64
|
+
runtime: RenderRuntime,
|
|
65
|
+
frontier: ReadonlyArray<Mounted>,
|
|
66
|
+
bindings: ReadonlyMap<AnySlot, AnyComposition>,
|
|
67
|
+
sink: Sink,
|
|
68
|
+
lookup: CaptureLookup,
|
|
69
|
+
): void => {
|
|
70
|
+
if (frontier.length === 0) {
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
const levels = frontier.map((mounted) =>
|
|
74
|
+
renderMountedRuntime(runtime, mounted, bindings, sink, lookup),
|
|
75
|
+
)
|
|
76
|
+
renderLevelRuntime(runtime, levels.flat(), bindings, sink, lookup)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export const renderTreeRuntime = (
|
|
80
|
+
runtime: RenderRuntime,
|
|
81
|
+
root: AnyComposition,
|
|
82
|
+
bindings: ReadonlyMap<AnySlot, AnyComposition>,
|
|
83
|
+
sink: Sink,
|
|
84
|
+
lookup: CaptureLookup,
|
|
85
|
+
): Effect.Effect<void, never, never> =>
|
|
86
|
+
Effect.sync(() => {
|
|
87
|
+
sink.reset()
|
|
88
|
+
lookup.reset()
|
|
89
|
+
renderLevelRuntime(
|
|
90
|
+
runtime,
|
|
91
|
+
[{ comp: root, props: {}, ref: rootRef(root) }],
|
|
92
|
+
bindings,
|
|
93
|
+
sink,
|
|
94
|
+
lookup,
|
|
95
|
+
)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
export const settleTreeRuntime: (
|
|
99
|
+
runtime: RenderRuntime,
|
|
100
|
+
root: AnyComposition,
|
|
101
|
+
bindings: ReadonlyMap<AnySlot, AnyComposition>,
|
|
102
|
+
sink: Sink,
|
|
103
|
+
lookup: CaptureLookup,
|
|
104
|
+
) => Effect.Effect<void, never, never> = Effect.fn('settleTreeRuntime')(function* (
|
|
105
|
+
runtime: RenderRuntime,
|
|
106
|
+
root: AnyComposition,
|
|
107
|
+
bindings: ReadonlyMap<AnySlot, AnyComposition>,
|
|
108
|
+
sink: Sink,
|
|
109
|
+
lookup: CaptureLookup,
|
|
110
|
+
): Effect.fn.Return<void, never, never> {
|
|
111
|
+
yield* settleDrain
|
|
112
|
+
yield* renderTreeRuntime(runtime, root, bindings, sink, lookup)
|
|
113
|
+
const progress = yield* Effect.iterate(
|
|
114
|
+
{
|
|
115
|
+
previous: fingerprint(lookup),
|
|
116
|
+
remaining: SETTLE_MAX_RENDERS,
|
|
117
|
+
stable: false,
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
while: (progress: SettleProgress) => !progress.stable && progress.remaining > 0,
|
|
121
|
+
body: (progress) =>
|
|
122
|
+
Effect.gen(function* () {
|
|
123
|
+
yield* settleDrain
|
|
124
|
+
yield* settleStep
|
|
125
|
+
yield* renderTreeRuntime(runtime, root, bindings, sink, lookup)
|
|
126
|
+
const current = fingerprint(lookup)
|
|
127
|
+
return {
|
|
128
|
+
previous: current,
|
|
129
|
+
remaining: progress.remaining - 1,
|
|
130
|
+
stable: current === progress.previous,
|
|
131
|
+
}
|
|
132
|
+
}),
|
|
133
|
+
},
|
|
134
|
+
)
|
|
135
|
+
yield* ensureSettled(progress)
|
|
136
|
+
})
|