@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.
Files changed (85) hide show
  1. package/dist/assert.d.ts +15 -0
  2. package/dist/assert.d.ts.map +1 -0
  3. package/dist/assert.js +39 -0
  4. package/dist/assert.js.map +1 -0
  5. package/dist/engine.d.ts +16 -0
  6. package/dist/engine.d.ts.map +1 -0
  7. package/dist/engine.js +119 -0
  8. package/dist/engine.js.map +1 -0
  9. package/dist/engineMounted.d.ts +18 -0
  10. package/dist/engineMounted.d.ts.map +1 -0
  11. package/dist/engineMounted.js +96 -0
  12. package/dist/engineMounted.js.map +1 -0
  13. package/dist/engineMounts.d.ts +13 -0
  14. package/dist/engineMounts.d.ts.map +1 -0
  15. package/dist/engineMounts.js +116 -0
  16. package/dist/engineMounts.js.map +1 -0
  17. package/dist/engineRender.d.ts +10 -0
  18. package/dist/engineRender.d.ts.map +1 -0
  19. package/dist/engineRender.js +64 -0
  20. package/dist/engineRender.js.map +1 -0
  21. package/dist/engineSink.d.ts +66 -0
  22. package/dist/engineSink.d.ts.map +1 -0
  23. package/dist/engineSink.js +72 -0
  24. package/dist/engineSink.js.map +1 -0
  25. package/dist/engineTree.d.ts +63 -0
  26. package/dist/engineTree.d.ts.map +1 -0
  27. package/dist/engineTree.js +137 -0
  28. package/dist/engineTree.js.map +1 -0
  29. package/dist/engineTreeDriver.d.ts +5 -0
  30. package/dist/engineTreeDriver.d.ts.map +1 -0
  31. package/dist/engineTreeDriver.js +187 -0
  32. package/dist/engineTreeDriver.js.map +1 -0
  33. package/dist/engineTreeFacade.d.ts +5 -0
  34. package/dist/engineTreeFacade.d.ts.map +1 -0
  35. package/dist/engineTreeFacade.js +105 -0
  36. package/dist/engineTreeFacade.js.map +1 -0
  37. package/dist/engineTreeTypes.d.ts +80 -0
  38. package/dist/engineTreeTypes.d.ts.map +1 -0
  39. package/dist/engineTreeTypes.js +11 -0
  40. package/dist/engineTreeTypes.js.map +1 -0
  41. package/dist/errors.d.ts +39 -0
  42. package/dist/errors.d.ts.map +1 -0
  43. package/dist/errors.js +30 -0
  44. package/dist/errors.js.map +1 -0
  45. package/dist/facade.d.ts +54 -0
  46. package/dist/facade.d.ts.map +1 -0
  47. package/dist/facade.js +2 -0
  48. package/dist/facade.js.map +1 -0
  49. package/dist/fingerprint.d.ts +2 -0
  50. package/dist/fingerprint.d.ts.map +1 -0
  51. package/dist/fingerprint.js +55 -0
  52. package/dist/fingerprint.js.map +1 -0
  53. package/dist/index.d.ts +88 -0
  54. package/dist/index.d.ts.map +1 -0
  55. package/dist/index.js +73 -0
  56. package/dist/index.js.map +1 -0
  57. package/dist/product.d.ts +56 -0
  58. package/dist/product.d.ts.map +1 -0
  59. package/dist/product.js +35 -0
  60. package/dist/product.js.map +1 -0
  61. package/dist/proofCase.d.ts +24 -0
  62. package/dist/proofCase.d.ts.map +1 -0
  63. package/dist/proofCase.js +2 -0
  64. package/dist/proofCase.js.map +1 -0
  65. package/dist/runner.d.ts +15 -0
  66. package/dist/runner.d.ts.map +1 -0
  67. package/dist/runner.js +13 -0
  68. package/dist/runner.js.map +1 -0
  69. package/package.json +17 -5
  70. package/src/assert.ts +65 -0
  71. package/src/engine.ts +30 -1402
  72. package/src/engineMounted.ts +271 -0
  73. package/src/engineMounts.ts +165 -0
  74. package/src/engineRender.ts +136 -0
  75. package/src/engineSink.ts +195 -0
  76. package/src/engineTree.ts +298 -0
  77. package/src/engineTreeDriver.ts +247 -0
  78. package/src/engineTreeFacade.ts +183 -0
  79. package/src/engineTreeTypes.ts +103 -0
  80. package/src/errors.ts +19 -0
  81. package/src/facade.ts +78 -0
  82. package/src/index.ts +61 -276
  83. package/src/product.ts +108 -0
  84. package/src/proofCase.ts +46 -0
  85. 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
+ })