@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,195 @@
1
+ import { Clock, type Context, type Duration, Effect, MutableRef, Option } from 'effect'
2
+ import { AssertionFailed } from './errors'
3
+ import {
4
+ Composition,
5
+ type CompositionClass,
6
+ type CompositionService,
7
+ isStructure,
8
+ type RuntimeHandle,
9
+ type Structure,
10
+ type Trigger,
11
+ type UiContract,
12
+ } from '@playfast/reform'
13
+ import {
14
+ type AnyComposition,
15
+ type CapturedRender,
16
+ type CaptureSinkApi,
17
+ type Instrumentation,
18
+ noopInstrumentation,
19
+ } from '@playfast/reform/internal'
20
+ import { matchProps } from './assert'
21
+ import type { Facade, SlotFacade, SlotFacadesOf } from './facade'
22
+
23
+ export type RenderRuntime = Pick<RuntimeHandle, 'dispatch' | 'instrumentation' | 'readOption'>
24
+
25
+ export type HostRuntime = Pick<
26
+ RuntimeHandle,
27
+ 'dispatch' | 'instrumentation' | 'mountAnyFeature' | 'readOption'
28
+ >
29
+
30
+ export type RuntimeRootComposition<
31
+ Identifier,
32
+ P,
33
+ C extends UiContract,
34
+ S extends ReadonlyArray<unknown>,
35
+ N extends string,
36
+ Identity,
37
+ > = CompositionClass<P, C, S, N, Identity> & {
38
+ readonly tag: Context.Tag<Identifier, CompositionService<P, C>>
39
+ }
40
+
41
+ export interface MountedFacadeErasure {
42
+ readonly props: Effect.Effect<unknown, never, never>
43
+ readonly expectProps: (partial: unknown) => Effect.Effect<void, never, never>
44
+ readonly actions: Readonly<
45
+ Record<string, (payload: unknown) => Effect.Effect<unknown, never, never>>
46
+ >
47
+ readonly slots: Readonly<Record<string, MountedSlotFacadeErasure>>
48
+ readonly frame: Effect.Effect<void, never, never>
49
+ readonly advance: (duration: Duration.DurationInput) => Effect.Effect<void, never, never>
50
+ }
51
+
52
+ export interface MountedSlotFacadeErasure extends MountedFacadeErasure {
53
+ readonly first: Effect.Effect<MountedFacadeErasure, never, never>
54
+ readonly at: (index: number) => Effect.Effect<MountedFacadeErasure, never, never>
55
+ readonly all: Effect.Effect<ReadonlyArray<MountedFacadeErasure>, never, never>
56
+ readonly byKey: (key: string) => Effect.Effect<MountedFacadeErasure, never, never>
57
+ readonly where: (
58
+ predicate: (props: unknown) => boolean,
59
+ ) => Effect.Effect<ReadonlyArray<MountedFacadeErasure>, never, never>
60
+ readonly count: Effect.Effect<number, never, never>
61
+ }
62
+
63
+ export interface MountedFacadeHandleErasure {
64
+ readonly facade: {
65
+ readonly props: Effect.Effect<unknown, never, never>
66
+ readonly expectProps: (partial: never) => Effect.Effect<void, never, never>
67
+ readonly actions: object
68
+ readonly slots: object
69
+ readonly frame: Effect.Effect<void, never, never>
70
+ readonly advance: (duration: Duration.DurationInput) => Effect.Effect<void, never, never>
71
+ }
72
+ readonly settle: Effect.Effect<void, never, never>
73
+ }
74
+
75
+ export type Settlement = Effect.Effect<void, never, never>
76
+ export type SettleBoundary = (settlement: Settlement) => Settlement
77
+
78
+ export const directSettle: SettleBoundary = (settlement) => settlement
79
+
80
+ // Cooperative yields flush synchronous fibers; the real-time sleep prevents a
81
+ // genuinely delayed flow from looking stable between renders.
82
+ export const SETTLE_DRAIN = 30
83
+ export const SETTLE_STEP = '1 milli'
84
+ export const SETTLE_MAX_RENDERS = 100
85
+
86
+ export const settleDrain: Effect.Effect<void> = Effect.yieldNow().pipe(Effect.repeatN(SETTLE_DRAIN))
87
+
88
+ // The drain's own pacing is harness time, and harness time is always real. A scene
89
+ // may install `Proof.withTestClock` to freeze PRODUCT time, and the proof body runs
90
+ // on that same runtime — so an unpinned sleep here would freeze with it and the
91
+ // settle loop would never make progress. Pinning it to a real clock is what lets the
92
+ // two clocks coexist: the harness keeps advancing while the product's timers hold
93
+ // still until `app.advance(...)` moves them.
94
+ export const settleStep: Effect.Effect<void> = Effect.sleep(SETTLE_STEP).pipe(Effect.withClock(Clock.make()))
95
+
96
+ export const keyed = <V>(get: (key: string) => V): Record<string, V> => {
97
+ const target: Record<string, V> = Object.create(null)
98
+ return new Proxy(target, { get: (_target, key) => get(String(key)) })
99
+ }
100
+
101
+ export const messageOf = (error: unknown): string =>
102
+ error instanceof Error ? error.message : String(error)
103
+
104
+ export const uiNameOf = (comp: AnyComposition): string => comp.manifest.ui.manifest.name
105
+
106
+ // UiContract erases event values to never, so this boundary needs no assertion.
107
+ export const eventsOf = (structure: Structure<UiContract>): Record<string, Trigger<unknown>> => ({
108
+ ...structure.events,
109
+ })
110
+
111
+ export interface ExpectedProps {
112
+ readonly actual: unknown
113
+ readonly expected: unknown
114
+ }
115
+
116
+ export const expectMatchingProps = ({
117
+ actual,
118
+ expected,
119
+ }: ExpectedProps): Effect.Effect<void, never, never> =>
120
+ Option.match(Option.fromNullable(matchProps({ actual, expected })), {
121
+ onNone: () => Effect.void,
122
+ onSome: (detail) => Effect.dieMessage(new AssertionFailed({ detail }).message),
123
+ })
124
+
125
+ export const invalidCompositionProps = (composition: AnyComposition): string =>
126
+ `reform-proof: invalid props for composition ${composition.manifest.name}`
127
+
128
+ export const renderCompositionRuntime = (
129
+ runtime: RenderRuntime,
130
+ composition: AnyComposition,
131
+ props: unknown,
132
+ ): Structure<UiContract> =>
133
+ composition.capture(
134
+ <P, C extends UiContract, S extends ReadonlyArray<unknown>, N extends string, Identity>(
135
+ exact: CompositionClass<P, C, S, N, Identity>,
136
+ ): Structure<UiContract> => {
137
+ const resolved = Option.flatMap(exact.parseProps(props), (parsed) =>
138
+ Option.map(runtime.readOption(exact.tag), (service) => ({
139
+ parsed,
140
+ service,
141
+ })),
142
+ )
143
+ return Option.match(resolved, {
144
+ onNone: () => Effect.runSync(Effect.dieMessage(invalidCompositionProps(composition))),
145
+ onSome: ({ parsed, service }) => {
146
+ const frame = Effect.runSync(
147
+ Composition.render(service, {
148
+ props: parsed,
149
+ tracker: { add: () => {} },
150
+ }),
151
+ )
152
+ if (isStructure(frame)) {
153
+ return frame
154
+ }
155
+ return Effect.runSync(
156
+ Effect.dieMessage(
157
+ `reform-proof: composition ${composition.manifest.name} did not return a Structure`,
158
+ ),
159
+ )
160
+ },
161
+ })
162
+ },
163
+ )
164
+
165
+ export interface Sink {
166
+ readonly api: CaptureSinkApi
167
+ readonly captures: ReadonlyArray<CapturedRender>
168
+ readonly instrumentation: Instrumentation
169
+ reset(): void
170
+ }
171
+
172
+ export const makeSink = (instrumentation: Instrumentation = noopInstrumentation): Sink => {
173
+ // CaptureSink.record is synchronous, so this uses MutableRef rather than Ref.
174
+ const captures = MutableRef.make<CapturedRender[]>([])
175
+ return {
176
+ api: {
177
+ record: (capture) => {
178
+ MutableRef.update(captures, (current) => [...current, capture])
179
+ },
180
+ },
181
+ get captures() {
182
+ return MutableRef.get(captures)
183
+ },
184
+ instrumentation,
185
+ reset: () => {
186
+ MutableRef.set(captures, [])
187
+ },
188
+ }
189
+ }
190
+
191
+ export type MountedSlotFacadesOf<C extends UiContract> = SlotFacadesOf<C, never>
192
+
193
+ export type MountedFacade<C extends UiContract> = Facade<C, never>
194
+
195
+ export type MountedSlotFacade<C extends UiContract> = SlotFacade<C, never>
@@ -0,0 +1,298 @@
1
+ import { Array as Arr, Context, Effect, Layer, Match, MutableRef, Option, Record as Rec } from 'effect'
2
+ import {
3
+ type CapturedScene,
4
+ isFeatureBinding,
5
+ isSlot,
6
+ publish,
7
+ type SlotFill,
8
+ type Structure,
9
+ type UiContract,
10
+ } from '@playfast/reform'
11
+ import {
12
+ type AnyComposition,
13
+ type AnySlot,
14
+ type AnySlotChild,
15
+ Bus,
16
+ type CapturedRender,
17
+ CaptureSink,
18
+ type Instrumentation,
19
+ } from '@playfast/reform/internal'
20
+ import { formatFingerprint } from './fingerprint'
21
+ import {
22
+ eventsOf,
23
+ type RenderRuntime,
24
+ SETTLE_MAX_RENDERS,
25
+ type Sink,
26
+ uiNameOf,
27
+ } from './engineSink'
28
+
29
+ export const proofLayer = <
30
+ C extends UiContract,
31
+ S extends ReadonlyArray<unknown>,
32
+ Services,
33
+ P,
34
+ N extends string,
35
+ Identity,
36
+ >(
37
+ scene: CapturedScene<C, S, Services, P, N, Identity>,
38
+ sink: Sink,
39
+ ): Layer.Layer<Services | CaptureSink, never, never> => {
40
+ const sceneLayer = scene.provide.reduce((merged, layer) => Layer.merge(merged, layer))
41
+ return Layer.merge(sceneLayer, Layer.succeed(CaptureSink, sink.api))
42
+ }
43
+
44
+ export const missingRuntimeBus = (): never =>
45
+ Effect.runSync(Effect.dieMessage('reform-proof: scene does not provide Bus'))
46
+
47
+ export const renderRuntimeFromContext = <Services>(
48
+ context: Context.Context<Services>,
49
+ instrumentation: Instrumentation,
50
+ ): RenderRuntime => {
51
+ const bus = Context.getOption(context, Bus)
52
+ return {
53
+ dispatch: (priority, event) =>
54
+ Option.match(bus, {
55
+ onNone: missingRuntimeBus,
56
+ onSome: (service) =>
57
+ Effect.runSync(publish(priority, event).pipe(Effect.provideService(Bus, service))),
58
+ }),
59
+ instrumentation,
60
+ readOption: (tag) => Context.getOption(context, tag),
61
+ }
62
+ }
63
+
64
+ export interface Mounted {
65
+ readonly comp: AnyComposition
66
+ readonly props: unknown
67
+ readonly ref: NodeRef
68
+ }
69
+
70
+ export interface SlotInstanceRef {
71
+ readonly name: string
72
+ readonly index: number
73
+ readonly key: Option.Option<string>
74
+ readonly keyOccurrence: number
75
+ }
76
+
77
+ export interface NodeRef {
78
+ readonly name: string
79
+ readonly parent: Option.Option<NodeRef>
80
+ readonly slot: Option.Option<SlotInstanceRef>
81
+ }
82
+
83
+ export interface NodeCapture {
84
+ readonly ref: NodeRef
85
+ readonly capture: CapturedRender
86
+ }
87
+
88
+ export interface CaptureLookup {
89
+ readonly captures: ReadonlyArray<NodeCapture>
90
+ record(ref: NodeRef, capture: CapturedRender): void
91
+ reset(): void
92
+ }
93
+
94
+ export const makeCaptureLookup = (): CaptureLookup => {
95
+ const captures = MutableRef.make<ReadonlyArray<NodeCapture>>([])
96
+ return {
97
+ get captures() {
98
+ return MutableRef.get(captures)
99
+ },
100
+ record: (ref, capture) => {
101
+ MutableRef.update(captures, (current) => [...current, { ref, capture }])
102
+ },
103
+ reset: () => {
104
+ MutableRef.set(captures, [])
105
+ },
106
+ }
107
+ }
108
+
109
+ export const sameSlotInstance = (expected: SlotInstanceRef, actual: SlotInstanceRef): boolean =>
110
+ expected.name === actual.name &&
111
+ Option.match(expected.key, {
112
+ onNone: () => expected.index === actual.index,
113
+ onSome: (key) =>
114
+ expected.keyOccurrence === actual.keyOccurrence && Option.contains(actual.key, key),
115
+ })
116
+
117
+ export const sameNodeRef = (expected: NodeRef, actual: NodeRef): boolean =>
118
+ expected.name === actual.name &&
119
+ Option.match(expected.parent, {
120
+ onNone: () => Option.isNone(actual.parent) && Option.isNone(actual.slot),
121
+ onSome: (expectedParent) =>
122
+ Option.isSome(actual.parent) &&
123
+ Option.isSome(expected.slot) &&
124
+ Option.isSome(actual.slot) &&
125
+ sameNodeRef(expectedParent, actual.parent.value) &&
126
+ sameSlotInstance(expected.slot.value, actual.slot.value),
127
+ })
128
+
129
+ export const rootRef = (root: AnyComposition): NodeRef => ({
130
+ name: uiNameOf(root),
131
+ parent: Option.none(),
132
+ slot: Option.none(),
133
+ })
134
+
135
+ export const nodeCaptureFor = (lookup: CaptureLookup, ref: NodeRef): Option.Option<CapturedRender> =>
136
+ Option.map(
137
+ Arr.findFirst(lookup.captures, (entry) => sameNodeRef(ref, entry.ref)),
138
+ (entry) => entry.capture,
139
+ )
140
+
141
+ export const slotCapturesFor = (
142
+ lookup: CaptureLookup,
143
+ parent: NodeRef,
144
+ slotName: string,
145
+ ): ReadonlyArray<NodeCapture> =>
146
+ lookup.captures.filter(
147
+ (entry) =>
148
+ Option.isSome(entry.ref.parent) &&
149
+ Option.isSome(entry.ref.slot) &&
150
+ sameNodeRef(parent, entry.ref.parent.value) &&
151
+ entry.ref.slot.value.name === slotName,
152
+ )
153
+
154
+ export interface SettleProgress {
155
+ readonly previous: string
156
+ readonly remaining: number
157
+ readonly stable: boolean
158
+ }
159
+
160
+ export const ensureSettled = (progress: SettleProgress): Effect.Effect<void, never, never> => {
161
+ if (progress.stable) {
162
+ return Effect.void
163
+ }
164
+ return Effect.dieMessage(`reform-proof: tree did not settle after ${SETTLE_MAX_RENDERS} renders`)
165
+ }
166
+
167
+ export interface PumpStep {
168
+ readonly input: unknown
169
+ readonly index: number
170
+ }
171
+
172
+ export const childComposition = (child: AnySlotChild): AnyComposition =>
173
+ isFeatureBinding(child) ? child.capture<AnyComposition>((binding) => binding.composition) : child
174
+
175
+ export const isUnknownRecord = (candidate: unknown): candidate is Record<string, unknown> =>
176
+ typeof candidate === 'object' && candidate !== null && !Array.isArray(candidate)
177
+
178
+ export const slotsOf = (comp: AnyComposition): Record<string, AnySlot> =>
179
+ comp.capture<Record<string, AnySlot>>((exact) => {
180
+ if (!('slots' in exact.manifest)) {
181
+ return {}
182
+ }
183
+ const candidate: unknown = exact.manifest.slots
184
+ if (!isUnknownRecord(candidate)) {
185
+ return {}
186
+ }
187
+ return Rec.fromEntries(
188
+ Rec.toEntries(candidate).flatMap(
189
+ ([name, slotDefinition]): ReadonlyArray<readonly [string, AnySlot]> =>
190
+ isSlot(slotDefinition) ? [[name, slotDefinition]] : [],
191
+ ),
192
+ )
193
+ })
194
+
195
+ export interface RuntimeSlotRead {
196
+ readonly runtime: RenderRuntime
197
+ readonly slotDefinition: AnySlot
198
+ readonly parent: AnyComposition
199
+ }
200
+
201
+ export const readRuntimeSlotChild = ({ runtime, slotDefinition, parent }: RuntimeSlotRead): AnySlotChild =>
202
+ slotDefinition.capture<AnySlotChild>((exact) =>
203
+ Option.match(runtime.readOption(exact.provider), {
204
+ onNone: () =>
205
+ Effect.runSync(
206
+ Effect.dieMessage(`reform-proof: slot provider missing in ${uiNameOf(parent)}`),
207
+ ),
208
+ onSome: (child) => child,
209
+ }),
210
+ )
211
+
212
+ // UiContract erases per-slot fill types here, so recover them by discriminant.
213
+ export const isSlotFill = (candidate: unknown): candidate is SlotFill<unknown> =>
214
+ typeof candidate === 'object' &&
215
+ candidate !== null &&
216
+ '_tag' in candidate &&
217
+ (candidate._tag === 'Each' || candidate._tag === 'One' || candidate._tag === 'Absent')
218
+
219
+ export const structureFills = (structure: Structure<UiContract>): Record<string, SlotFill<unknown>> =>
220
+ Rec.fromEntries(
221
+ Rec.toEntries(structure.slots).flatMap(
222
+ ([slotName, fillValue]): ReadonlyArray<readonly [string, SlotFill<unknown>]> =>
223
+ isSlotFill(fillValue) ? [[slotName, fillValue]] : [],
224
+ ),
225
+ )
226
+
227
+ export const enqueueFill = (
228
+ parent: NodeRef,
229
+ slotName: string,
230
+ fill: SlotFill<unknown>,
231
+ child: AnyComposition,
232
+ ): ReadonlyArray<Mounted> =>
233
+ Match.value(fill).pipe(
234
+ Match.when({ _tag: 'Each' }, (each) =>
235
+ each.items.map((entry, index) => ({
236
+ comp: child,
237
+ props: entry.props,
238
+ ref: {
239
+ name: uiNameOf(child),
240
+ parent: Option.some(parent),
241
+ slot: Option.some({
242
+ name: slotName,
243
+ index,
244
+ key: Option.some(entry.key),
245
+ keyOccurrence: each.items
246
+ .slice(0, index)
247
+ .filter((candidate) => candidate.key === entry.key).length,
248
+ }),
249
+ },
250
+ })),
251
+ ),
252
+ Match.when({ _tag: 'One' }, (oneFill) => [
253
+ {
254
+ comp: child,
255
+ props: oneFill.props,
256
+ ref: {
257
+ name: uiNameOf(child),
258
+ parent: Option.some(parent),
259
+ slot: Option.some({
260
+ name: slotName,
261
+ index: 0,
262
+ key: Option.some(`${slotName}.0`),
263
+ keyOccurrence: 0,
264
+ }),
265
+ },
266
+ },
267
+ ]),
268
+ Match.when({ _tag: 'Absent' }, () => []),
269
+ Match.exhaustive,
270
+ )
271
+
272
+ export const driveStructure = (
273
+ comp: AnyComposition,
274
+ structure: Structure<UiContract>,
275
+ ref: NodeRef,
276
+ sink: Sink,
277
+ lookup: CaptureLookup,
278
+ bindings: ReadonlyMap<AnySlot, AnyComposition>,
279
+ ): ReadonlyArray<Mounted> => {
280
+ const key = Option.flatMap(ref.slot, (instance) => instance.key)
281
+ const capture: CapturedRender = {
282
+ name: uiNameOf(comp),
283
+ props: structure.props,
284
+ events: eventsOf(structure),
285
+ ...(Option.isSome(key) ? { key: key.value } : {}),
286
+ }
287
+ sink.api.record(capture)
288
+ lookup.record(ref, capture)
289
+ const fills = structureFills(structure)
290
+ return Rec.toEntries(slotsOf(comp)).flatMap(([slotName, slotClass]) => {
291
+ const child = bindings.get(slotClass)
292
+ const fill = fills[slotName]
293
+ return child === undefined || fill === undefined ? [] : enqueueFill(ref, slotName, fill, child)
294
+ })
295
+ }
296
+
297
+ export const fingerprint = (lookup: CaptureLookup): string =>
298
+ formatFingerprint(lookup.captures.map(({ ref, capture }) => [ref, capture.props]))