@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,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]))
|