@playfast/reform-proof 1.1.1 → 1.2.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/README.md +12 -12
- package/package.json +18 -18
- package/src/engine.ts +1657 -8
- package/src/fingerprint.test.ts +24 -0
- package/src/fingerprint.ts +73 -0
- package/src/index.ts +352 -87
- package/src/nested-feature.test.ts +174 -0
- package/src/runner.ts +5 -1
- package/src/structure-events.test.ts +9 -3
- package/src/structure-locators.test.ts +19 -9
- package/src/typed-seams.test.ts +6 -3
- package/src/assertions.ts +0 -65
- package/src/driverTypes.ts +0 -19
- package/src/execution.ts +0 -100
- package/src/facade.ts +0 -258
- package/src/product.ts +0 -77
- package/src/runtimeFacade.ts +0 -269
- package/src/runtimeTreeDispose.ts +0 -21
- package/src/runtimeTreeDriver.ts +0 -285
- package/src/runtimeTreeFacade.ts +0 -134
- package/src/runtimeTreeSettle.ts +0 -34
- package/src/runtimeTreeTypes.ts +0 -99
- package/src/sink.ts +0 -73
- package/src/treeBindings.ts +0 -121
package/src/facade.ts
DELETED
|
@@ -1,258 +0,0 @@
|
|
|
1
|
-
import { Array as Arr, Effect, ManagedRuntime, Option, Schema as S } from 'effect'
|
|
2
|
-
import {
|
|
3
|
-
Composition,
|
|
4
|
-
type CompositionClass,
|
|
5
|
-
type CompositionService,
|
|
6
|
-
isStructure,
|
|
7
|
-
type RenderEnv,
|
|
8
|
-
type SlotChild,
|
|
9
|
-
type SlotClass,
|
|
10
|
-
type Trigger,
|
|
11
|
-
type UiCapture,
|
|
12
|
-
type UiContract,
|
|
13
|
-
} from '@playfast/reform'
|
|
14
|
-
import { AssertionFailed, UnknownAction, UnknownSlot } from './errors'
|
|
15
|
-
import { matchProps } from './assertions'
|
|
16
|
-
import type { Action, Facade, SlotFacade } from './index'
|
|
17
|
-
import {
|
|
18
|
-
keyed,
|
|
19
|
-
SETTLE_MAX_RENDERS,
|
|
20
|
-
SETTLE_STEP,
|
|
21
|
-
settleDrain,
|
|
22
|
-
type RuntimeServices,
|
|
23
|
-
type Sink,
|
|
24
|
-
uiNameOf,
|
|
25
|
-
} from './sink'
|
|
26
|
-
import {
|
|
27
|
-
collectBindings,
|
|
28
|
-
driveStructure,
|
|
29
|
-
type Mounted,
|
|
30
|
-
type NodeRef,
|
|
31
|
-
type SettleProgress,
|
|
32
|
-
slotsOf,
|
|
33
|
-
} from './treeBindings'
|
|
34
|
-
|
|
35
|
-
type AnyFacade = Facade<UiContract>
|
|
36
|
-
type AnySlotFacade = SlotFacade<UiContract>
|
|
37
|
-
const encodeUnknown = S.encodeSync(S.parseJson(S.Unknown))
|
|
38
|
-
|
|
39
|
-
const renderMounted = Effect.fn('renderMounted')(function* (
|
|
40
|
-
mounted: Mounted,
|
|
41
|
-
bindings: ReadonlyMap<SlotClass, CompositionClass<unknown>>,
|
|
42
|
-
sink: Sink,
|
|
43
|
-
): Effect.fn.Return<ReadonlyArray<Mounted>, never, CompositionService> {
|
|
44
|
-
const service = yield* mounted.comp.tag
|
|
45
|
-
const env: RenderEnv = { props: mounted.props, tracker: { add: () => {} } }
|
|
46
|
-
const endRenderSpan = sink.instrumentation.uiRendered(uiNameOf(mounted.comp))
|
|
47
|
-
const frame = yield* Composition.render(service, env)
|
|
48
|
-
endRenderSpan()
|
|
49
|
-
if (!isStructure(frame)) {
|
|
50
|
-
return yield* Effect.dieMessage(
|
|
51
|
-
`reform-proof: composition ${uiNameOf(mounted.comp)} did not return a Structure`,
|
|
52
|
-
)
|
|
53
|
-
}
|
|
54
|
-
return driveStructure(mounted.comp, frame, mounted.key, sink, bindings)
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
const renderLevel: (
|
|
58
|
-
frontier: ReadonlyArray<Mounted>,
|
|
59
|
-
bindings: ReadonlyMap<SlotClass, CompositionClass<unknown>>,
|
|
60
|
-
sink: Sink,
|
|
61
|
-
) => Effect.Effect<void, never, CompositionService> = Effect.fn('renderLevel')(function* (
|
|
62
|
-
frontier: ReadonlyArray<Mounted>,
|
|
63
|
-
bindings: ReadonlyMap<SlotClass, CompositionClass<unknown>>,
|
|
64
|
-
sink: Sink,
|
|
65
|
-
): Effect.fn.Return<void, never, CompositionService> {
|
|
66
|
-
if (frontier.length === 0) {
|
|
67
|
-
return
|
|
68
|
-
}
|
|
69
|
-
const levels = yield* Effect.forEach(frontier, (mounted) =>
|
|
70
|
-
renderMounted(mounted, bindings, sink),
|
|
71
|
-
)
|
|
72
|
-
yield* renderLevel(levels.flat(), bindings, sink)
|
|
73
|
-
})
|
|
74
|
-
|
|
75
|
-
const renderTree = Effect.fn('renderTree')(function* (
|
|
76
|
-
root: CompositionClass<unknown>,
|
|
77
|
-
bindings: ReadonlyMap<SlotClass, CompositionClass<unknown>>,
|
|
78
|
-
sink: Sink,
|
|
79
|
-
): Effect.fn.Return<void, never, CompositionService> {
|
|
80
|
-
sink.reset()
|
|
81
|
-
yield* renderLevel([{ comp: root, props: {}, key: Option.none() }], bindings, sink)
|
|
82
|
-
})
|
|
83
|
-
|
|
84
|
-
export const capturesFor = (sink: Sink, name: string): ReadonlyArray<UiCapture> =>
|
|
85
|
-
sink.captures.filter((capture) => capture.name === name)
|
|
86
|
-
|
|
87
|
-
export const fingerprint = (sink: Sink): string =>
|
|
88
|
-
encodeUnknown(sink.captures.map((capture) => [capture.name, capture.props]))
|
|
89
|
-
|
|
90
|
-
const settleTree = Effect.fn('settleTree')(function* (
|
|
91
|
-
root: CompositionClass<unknown>,
|
|
92
|
-
bindings: ReadonlyMap<SlotClass, CompositionClass<unknown>>,
|
|
93
|
-
sink: Sink,
|
|
94
|
-
): Effect.fn.Return<void, never, CompositionService> {
|
|
95
|
-
yield* settleDrain
|
|
96
|
-
yield* renderTree(root, bindings, sink)
|
|
97
|
-
yield* Effect.iterate(
|
|
98
|
-
{
|
|
99
|
-
previous: fingerprint(sink),
|
|
100
|
-
remaining: SETTLE_MAX_RENDERS,
|
|
101
|
-
stable: false,
|
|
102
|
-
},
|
|
103
|
-
{
|
|
104
|
-
while: (progress: SettleProgress) => !progress.stable && progress.remaining > 0,
|
|
105
|
-
body: (progress) =>
|
|
106
|
-
Effect.gen(function* () {
|
|
107
|
-
yield* settleDrain
|
|
108
|
-
yield* Effect.sleep(SETTLE_STEP)
|
|
109
|
-
yield* renderTree(root, bindings, sink)
|
|
110
|
-
const current = fingerprint(sink)
|
|
111
|
-
return {
|
|
112
|
-
previous: current,
|
|
113
|
-
remaining: progress.remaining - 1,
|
|
114
|
-
stable: current === progress.previous,
|
|
115
|
-
}
|
|
116
|
-
}),
|
|
117
|
-
},
|
|
118
|
-
)
|
|
119
|
-
})
|
|
120
|
-
|
|
121
|
-
export const makeFacadeEffect: (
|
|
122
|
-
root: CompositionClass<unknown>,
|
|
123
|
-
sink: Sink,
|
|
124
|
-
dispatched: Set<string>,
|
|
125
|
-
) => Effect.Effect<
|
|
126
|
-
{
|
|
127
|
-
readonly facade: AnyFacade
|
|
128
|
-
readonly settle: Effect.Effect<void, never, CompositionService>
|
|
129
|
-
},
|
|
130
|
-
never,
|
|
131
|
-
SlotChild
|
|
132
|
-
> = Effect.fn('makeFacadeEffect')(function* (
|
|
133
|
-
root: CompositionClass<unknown>,
|
|
134
|
-
sink: Sink,
|
|
135
|
-
dispatched: Set<string>,
|
|
136
|
-
): Effect.fn.Return<
|
|
137
|
-
{
|
|
138
|
-
readonly facade: AnyFacade
|
|
139
|
-
readonly settle: Effect.Effect<void, never, CompositionService>
|
|
140
|
-
},
|
|
141
|
-
never,
|
|
142
|
-
SlotChild
|
|
143
|
-
> {
|
|
144
|
-
const bindings = yield* collectBindings(root)
|
|
145
|
-
const settle = settleTree(root, bindings, sink)
|
|
146
|
-
const rerender = renderTree(root, bindings, sink)
|
|
147
|
-
|
|
148
|
-
const triggerOf = (ref: NodeRef, event: string): Effect.Effect<Trigger<unknown>> =>
|
|
149
|
-
Option.match(Option.fromNullable(capturesFor(sink, ref.name)[ref.index]?.events[event]), {
|
|
150
|
-
onNone: () =>
|
|
151
|
-
Effect.dieMessage(
|
|
152
|
-
new UnknownAction({
|
|
153
|
-
composition: ref.name,
|
|
154
|
-
action: event,
|
|
155
|
-
rendered: capturesFor(sink, ref.name).length,
|
|
156
|
-
}).message,
|
|
157
|
-
),
|
|
158
|
-
onSome: Effect.succeed,
|
|
159
|
-
})
|
|
160
|
-
|
|
161
|
-
const propsFor = (ref: NodeRef): Effect.Effect<unknown, never, CompositionService> =>
|
|
162
|
-
Effect.map(rerender, () => capturesFor(sink, ref.name)[ref.index]?.props)
|
|
163
|
-
|
|
164
|
-
const nodeFacade = (ref: NodeRef, comp: CompositionClass<unknown>): AnyFacade => ({
|
|
165
|
-
props: propsFor(ref),
|
|
166
|
-
expectProps: (partial) =>
|
|
167
|
-
propsFor(ref).pipe(
|
|
168
|
-
Effect.flatMap((props) => {
|
|
169
|
-
const mismatch = matchProps({ actual: props, expected: partial })
|
|
170
|
-
return mismatch === undefined
|
|
171
|
-
? Effect.void
|
|
172
|
-
: Effect.dieMessage(new AssertionFailed({ detail: mismatch }).message)
|
|
173
|
-
}),
|
|
174
|
-
),
|
|
175
|
-
actions: keyed(
|
|
176
|
-
(event): Action =>
|
|
177
|
-
(payload) =>
|
|
178
|
-
rerender.pipe(
|
|
179
|
-
Effect.flatMap(() => triggerOf(ref, event)),
|
|
180
|
-
Effect.flatMap((trigger) =>
|
|
181
|
-
Effect.sync(() => {
|
|
182
|
-
dispatched.add(event)
|
|
183
|
-
trigger(payload)
|
|
184
|
-
}),
|
|
185
|
-
),
|
|
186
|
-
Effect.flatMap(() => settle),
|
|
187
|
-
Effect.flatMap(() => propsFor(ref)),
|
|
188
|
-
),
|
|
189
|
-
),
|
|
190
|
-
slots: keyed((slotName) => slotFacade(comp, slotName)),
|
|
191
|
-
frame: settle,
|
|
192
|
-
})
|
|
193
|
-
|
|
194
|
-
const slotFacade = (parent: CompositionClass<unknown>, slotName: string): AnySlotFacade => {
|
|
195
|
-
const slotClass = slotsOf(parent)[slotName]
|
|
196
|
-
const child = slotClass && bindings.get(slotClass)
|
|
197
|
-
if (child === undefined) {
|
|
198
|
-
return Effect.runSync(
|
|
199
|
-
Effect.dieMessage(new UnknownSlot({ parent: uiNameOf(parent), slot: slotName }).message),
|
|
200
|
-
)
|
|
201
|
-
}
|
|
202
|
-
const childName = uiNameOf(child)
|
|
203
|
-
const atIndex = (index: number): Effect.Effect<AnyFacade, never, CompositionService> =>
|
|
204
|
-
Effect.as(rerender, nodeFacade({ name: childName, index }, child))
|
|
205
|
-
const first = nodeFacade({ name: childName, index: 0 }, child)
|
|
206
|
-
const indexOfKey = (key: string): Option.Option<number> =>
|
|
207
|
-
Arr.findFirstIndex(capturesFor(sink, childName), (capture) => capture.key === key)
|
|
208
|
-
return {
|
|
209
|
-
first: atIndex(0),
|
|
210
|
-
at: atIndex,
|
|
211
|
-
all: Effect.map(rerender, () =>
|
|
212
|
-
capturesFor(sink, childName).map((_capture, index) =>
|
|
213
|
-
nodeFacade({ name: childName, index }, child),
|
|
214
|
-
),
|
|
215
|
-
),
|
|
216
|
-
byKey: (key) =>
|
|
217
|
-
Effect.flatMap(rerender, () =>
|
|
218
|
-
Option.match(indexOfKey(key), {
|
|
219
|
-
onNone: () =>
|
|
220
|
-
Effect.dieMessage(
|
|
221
|
-
new UnknownSlot({
|
|
222
|
-
parent: uiNameOf(parent),
|
|
223
|
-
slot: `${slotName}[key=${key}]`,
|
|
224
|
-
}).message,
|
|
225
|
-
),
|
|
226
|
-
onSome: (index) => Effect.succeed(nodeFacade({ name: childName, index }, child)),
|
|
227
|
-
}),
|
|
228
|
-
),
|
|
229
|
-
where: (predicate) =>
|
|
230
|
-
Effect.map(rerender, () =>
|
|
231
|
-
capturesFor(sink, childName).flatMap((capture, index) =>
|
|
232
|
-
predicate(capture.props) ? [nodeFacade({ name: childName, index }, child)] : [],
|
|
233
|
-
),
|
|
234
|
-
),
|
|
235
|
-
count: Effect.map(rerender, () => capturesFor(sink, childName).length),
|
|
236
|
-
props: first.props,
|
|
237
|
-
expectProps: first.expectProps,
|
|
238
|
-
actions: first.actions,
|
|
239
|
-
slots: first.slots,
|
|
240
|
-
frame: first.frame,
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
return {
|
|
245
|
-
facade: nodeFacade({ name: uiNameOf(root), index: 0 }, root),
|
|
246
|
-
settle,
|
|
247
|
-
}
|
|
248
|
-
})
|
|
249
|
-
|
|
250
|
-
export const makeFacade = (
|
|
251
|
-
runtime: ManagedRuntime.ManagedRuntime<RuntimeServices, never>,
|
|
252
|
-
root: CompositionClass<unknown>,
|
|
253
|
-
sink: Sink,
|
|
254
|
-
dispatched: Set<string>,
|
|
255
|
-
): {
|
|
256
|
-
readonly facade: AnyFacade
|
|
257
|
-
readonly settle: Effect.Effect<void, never, CompositionService>
|
|
258
|
-
} => runtime.runSync(makeFacadeEffect(root, sink, dispatched))
|
package/src/product.ts
DELETED
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
import { Option, type Schema } from 'effect'
|
|
2
|
-
import type { CompositionClass } from '@playfast/reform'
|
|
3
|
-
import type { ContractOf, EventsOf } from './index'
|
|
4
|
-
|
|
5
|
-
type AnyValue = Schema.Schema.Type<Schema.Schema.Any>
|
|
6
|
-
type AnyComposition = CompositionClass<AnyValue, AnyValue>
|
|
7
|
-
|
|
8
|
-
export interface RequirementManifest<Comp extends AnyComposition, Statement extends string> {
|
|
9
|
-
readonly kind: 'ProductRequirement'
|
|
10
|
-
readonly name: Statement
|
|
11
|
-
readonly statement: Statement
|
|
12
|
-
readonly composition: Comp
|
|
13
|
-
readonly events: Option.Option<ReadonlyArray<keyof EventsOf<ContractOf<Comp>>>>
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export interface RequirementClass<Comp extends AnyComposition, Statement extends string> {
|
|
17
|
-
new (): {}
|
|
18
|
-
readonly manifest: RequirementManifest<Comp, Statement>
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export interface ProductManifest<Comp extends AnyComposition> {
|
|
22
|
-
readonly kind: 'Product'
|
|
23
|
-
readonly name: string
|
|
24
|
-
readonly composition: Comp
|
|
25
|
-
readonly requirements: ReadonlyArray<RequirementClass<Comp, string>>
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export interface ProductClass<Comp extends AnyComposition = AnyComposition> {
|
|
29
|
-
new (): {}
|
|
30
|
-
readonly manifest: ProductManifest<Comp>
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
interface MakeRequirementOptionsExternalApi<Comp extends AnyComposition> {
|
|
34
|
-
readonly events?: ReadonlyArray<keyof EventsOf<ContractOf<Comp>>>
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const makeRequirement = <Comp extends AnyComposition, const Statement extends string>(
|
|
38
|
-
composition: Comp,
|
|
39
|
-
statement: Statement,
|
|
40
|
-
options?: MakeRequirementOptionsExternalApi<Comp>,
|
|
41
|
-
): RequirementClass<Comp, Statement> => {
|
|
42
|
-
const manifest: RequirementManifest<Comp, Statement> = {
|
|
43
|
-
kind: 'ProductRequirement',
|
|
44
|
-
name: statement,
|
|
45
|
-
statement,
|
|
46
|
-
composition,
|
|
47
|
-
events: Option.fromNullable(options?.events),
|
|
48
|
-
}
|
|
49
|
-
return class {
|
|
50
|
-
static readonly manifest = manifest
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export const ProductRequirement: { readonly make: typeof makeRequirement } = {
|
|
55
|
-
make: makeRequirement,
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
interface MakeProductConfig<Comp extends AnyComposition> {
|
|
59
|
-
readonly requirements: ReadonlyArray<RequirementClass<Comp, string>>
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const makeProduct = <Comp extends AnyComposition>(
|
|
63
|
-
composition: Comp,
|
|
64
|
-
config: MakeProductConfig<Comp>,
|
|
65
|
-
): ProductClass<Comp> => {
|
|
66
|
-
const manifest: ProductManifest<Comp> = {
|
|
67
|
-
kind: 'Product',
|
|
68
|
-
name: composition.manifest.name,
|
|
69
|
-
composition,
|
|
70
|
-
requirements: config.requirements,
|
|
71
|
-
}
|
|
72
|
-
return class {
|
|
73
|
-
static readonly manifest = manifest
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export const Product: { readonly make: typeof makeProduct } = { make: makeProduct }
|
package/src/runtimeFacade.ts
DELETED
|
@@ -1,269 +0,0 @@
|
|
|
1
|
-
import { Array as Arr, Effect, Option, Record as Rec } from 'effect'
|
|
2
|
-
import {
|
|
3
|
-
Composition,
|
|
4
|
-
type CompositionClass,
|
|
5
|
-
isStructure,
|
|
6
|
-
type RenderEnv,
|
|
7
|
-
type RuntimeHandle,
|
|
8
|
-
type SlotClass,
|
|
9
|
-
type Trigger,
|
|
10
|
-
} from '@playfast/reform'
|
|
11
|
-
import { AssertionFailed, UnknownAction, UnknownSlot } from './errors'
|
|
12
|
-
import { matchProps } from './assertions'
|
|
13
|
-
import { capturesFor, fingerprint } from './facade'
|
|
14
|
-
import {
|
|
15
|
-
keyed,
|
|
16
|
-
SETTLE_MAX_RENDERS,
|
|
17
|
-
SETTLE_STEP,
|
|
18
|
-
settleDrain,
|
|
19
|
-
type Sink,
|
|
20
|
-
uiNameOf,
|
|
21
|
-
} from './sink'
|
|
22
|
-
import {
|
|
23
|
-
childComposition,
|
|
24
|
-
driveStructure,
|
|
25
|
-
type Mounted,
|
|
26
|
-
type NodeRef,
|
|
27
|
-
type SettleProgress,
|
|
28
|
-
slotsOf,
|
|
29
|
-
} from './treeBindings'
|
|
30
|
-
|
|
31
|
-
export interface MountedFacade {
|
|
32
|
-
readonly props: Effect.Effect<unknown, never, never>
|
|
33
|
-
readonly expectProps: (partial: unknown) => Effect.Effect<void, never, never>
|
|
34
|
-
readonly actions: Record<string, (payload: unknown) => Effect.Effect<unknown, never, never>>
|
|
35
|
-
readonly slots: Record<string, MountedSlotFacade>
|
|
36
|
-
readonly frame: Effect.Effect<void, never, never>
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export interface MountedSlotFacade extends MountedFacade {
|
|
40
|
-
readonly first: Effect.Effect<MountedFacade, never, never>
|
|
41
|
-
readonly at: (index: number) => Effect.Effect<MountedFacade, never, never>
|
|
42
|
-
readonly all: Effect.Effect<ReadonlyArray<MountedFacade>, never, never>
|
|
43
|
-
readonly byKey: (key: string) => Effect.Effect<MountedFacade, never, never>
|
|
44
|
-
readonly where: (
|
|
45
|
-
predicate: (props: unknown) => boolean,
|
|
46
|
-
) => Effect.Effect<ReadonlyArray<MountedFacade>, never, never>
|
|
47
|
-
readonly count: Effect.Effect<number, never, never>
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const collectRuntimeBindings = (
|
|
51
|
-
root: CompositionClass<unknown>,
|
|
52
|
-
runtime: RuntimeHandle,
|
|
53
|
-
): Map<SlotClass, CompositionClass<unknown>> => {
|
|
54
|
-
const bindings = new Map<SlotClass, CompositionClass<unknown>>()
|
|
55
|
-
const walk = (comp: CompositionClass<unknown>): void => {
|
|
56
|
-
Rec.values(slotsOf(comp)).forEach((slotClass) => {
|
|
57
|
-
if (bindings.has(slotClass)) {
|
|
58
|
-
return
|
|
59
|
-
}
|
|
60
|
-
const child = childComposition(runtime.read(slotClass.tag))
|
|
61
|
-
bindings.set(slotClass, child)
|
|
62
|
-
walk(child)
|
|
63
|
-
})
|
|
64
|
-
}
|
|
65
|
-
walk(root)
|
|
66
|
-
return bindings
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const renderMountedRuntime = (
|
|
70
|
-
runtime: RuntimeHandle,
|
|
71
|
-
mounted: Mounted,
|
|
72
|
-
bindings: ReadonlyMap<SlotClass, CompositionClass<unknown>>,
|
|
73
|
-
sink: Sink,
|
|
74
|
-
): ReadonlyArray<Mounted> => {
|
|
75
|
-
const service = runtime.read(mounted.comp.tag)
|
|
76
|
-
const env: RenderEnv = { props: mounted.props, tracker: { add: () => {} } }
|
|
77
|
-
const endRenderSpan = sink.instrumentation.uiRendered(uiNameOf(mounted.comp))
|
|
78
|
-
const frame = Effect.runSync(Composition.render(service, env))
|
|
79
|
-
endRenderSpan()
|
|
80
|
-
if (!isStructure(frame)) {
|
|
81
|
-
return Effect.runSync(
|
|
82
|
-
Effect.dieMessage(
|
|
83
|
-
`reform-proof: composition ${uiNameOf(mounted.comp)} did not return a Structure`,
|
|
84
|
-
),
|
|
85
|
-
)
|
|
86
|
-
}
|
|
87
|
-
return driveStructure(mounted.comp, frame, mounted.key, sink, bindings)
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const renderLevelRuntime = (
|
|
91
|
-
runtime: RuntimeHandle,
|
|
92
|
-
frontier: ReadonlyArray<Mounted>,
|
|
93
|
-
bindings: ReadonlyMap<SlotClass, CompositionClass<unknown>>,
|
|
94
|
-
sink: Sink,
|
|
95
|
-
): void => {
|
|
96
|
-
if (frontier.length === 0) {
|
|
97
|
-
return
|
|
98
|
-
}
|
|
99
|
-
const levels = frontier.map((mounted) => renderMountedRuntime(runtime, mounted, bindings, sink))
|
|
100
|
-
renderLevelRuntime(runtime, levels.flat(), bindings, sink)
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const renderTreeRuntime = (
|
|
104
|
-
runtime: RuntimeHandle,
|
|
105
|
-
root: CompositionClass<unknown>,
|
|
106
|
-
bindings: ReadonlyMap<SlotClass, CompositionClass<unknown>>,
|
|
107
|
-
sink: Sink,
|
|
108
|
-
): Effect.Effect<void, never, never> =>
|
|
109
|
-
Effect.sync(() => {
|
|
110
|
-
sink.reset()
|
|
111
|
-
renderLevelRuntime(runtime, [{ comp: root, props: {}, key: Option.none() }], bindings, sink)
|
|
112
|
-
})
|
|
113
|
-
|
|
114
|
-
const settleTreeRuntime = Effect.fn('settleTreeRuntime')(function* (
|
|
115
|
-
runtime: RuntimeHandle,
|
|
116
|
-
root: CompositionClass<unknown>,
|
|
117
|
-
bindings: ReadonlyMap<SlotClass, CompositionClass<unknown>>,
|
|
118
|
-
sink: Sink,
|
|
119
|
-
): Effect.fn.Return<void, never, never> {
|
|
120
|
-
yield* settleDrain
|
|
121
|
-
yield* renderTreeRuntime(runtime, root, bindings, sink)
|
|
122
|
-
yield* Effect.iterate(
|
|
123
|
-
{
|
|
124
|
-
previous: fingerprint(sink),
|
|
125
|
-
remaining: SETTLE_MAX_RENDERS,
|
|
126
|
-
stable: false,
|
|
127
|
-
},
|
|
128
|
-
{
|
|
129
|
-
while: (progress: SettleProgress) => !progress.stable && progress.remaining > 0,
|
|
130
|
-
body: (progress) =>
|
|
131
|
-
Effect.gen(function* () {
|
|
132
|
-
yield* settleDrain
|
|
133
|
-
yield* Effect.sleep(SETTLE_STEP)
|
|
134
|
-
yield* renderTreeRuntime(runtime, root, bindings, sink)
|
|
135
|
-
const current = fingerprint(sink)
|
|
136
|
-
return {
|
|
137
|
-
previous: current,
|
|
138
|
-
remaining: progress.remaining - 1,
|
|
139
|
-
stable: current === progress.previous,
|
|
140
|
-
}
|
|
141
|
-
}),
|
|
142
|
-
},
|
|
143
|
-
)
|
|
144
|
-
})
|
|
145
|
-
|
|
146
|
-
const makeMountedFacade = (
|
|
147
|
-
runtime: RuntimeHandle,
|
|
148
|
-
root: CompositionClass<unknown>,
|
|
149
|
-
sink: Sink,
|
|
150
|
-
dispatched: Set<string>,
|
|
151
|
-
): {
|
|
152
|
-
readonly facade: MountedFacade
|
|
153
|
-
readonly settle: Effect.Effect<void, never, never>
|
|
154
|
-
} => {
|
|
155
|
-
const bindings = collectRuntimeBindings(root, runtime)
|
|
156
|
-
const settle = settleTreeRuntime(runtime, root, bindings, sink)
|
|
157
|
-
const rerender = renderTreeRuntime(runtime, root, bindings, sink)
|
|
158
|
-
|
|
159
|
-
const triggerOf = (ref: NodeRef, event: string): Effect.Effect<Trigger<unknown>, never, never> =>
|
|
160
|
-
Option.match(Option.fromNullable(capturesFor(sink, ref.name)[ref.index]?.events[event]), {
|
|
161
|
-
onNone: () =>
|
|
162
|
-
Effect.dieMessage(
|
|
163
|
-
new UnknownAction({
|
|
164
|
-
composition: ref.name,
|
|
165
|
-
action: event,
|
|
166
|
-
rendered: capturesFor(sink, ref.name).length,
|
|
167
|
-
}).message,
|
|
168
|
-
),
|
|
169
|
-
onSome: Effect.succeed,
|
|
170
|
-
})
|
|
171
|
-
|
|
172
|
-
const propsFor = (ref: NodeRef): Effect.Effect<unknown, never, never> =>
|
|
173
|
-
Effect.map(rerender, () => capturesFor(sink, ref.name)[ref.index]?.props)
|
|
174
|
-
|
|
175
|
-
const nodeFacade = (ref: NodeRef, comp: CompositionClass<unknown>): MountedFacade => ({
|
|
176
|
-
props: propsFor(ref),
|
|
177
|
-
expectProps: (partial) =>
|
|
178
|
-
propsFor(ref).pipe(
|
|
179
|
-
Effect.flatMap((props) => {
|
|
180
|
-
const mismatch = matchProps({ actual: props, expected: partial })
|
|
181
|
-
return mismatch === undefined
|
|
182
|
-
? Effect.void
|
|
183
|
-
: Effect.dieMessage(new AssertionFailed({ detail: mismatch }).message)
|
|
184
|
-
}),
|
|
185
|
-
),
|
|
186
|
-
actions: keyed(
|
|
187
|
-
(event) => (payload: unknown) =>
|
|
188
|
-
rerender.pipe(
|
|
189
|
-
Effect.flatMap(() => triggerOf(ref, event)),
|
|
190
|
-
Effect.flatMap((trigger) =>
|
|
191
|
-
Effect.sync(() => {
|
|
192
|
-
dispatched.add(event)
|
|
193
|
-
trigger(payload)
|
|
194
|
-
}),
|
|
195
|
-
),
|
|
196
|
-
Effect.flatMap(() => settle),
|
|
197
|
-
Effect.flatMap(() => propsFor(ref)),
|
|
198
|
-
),
|
|
199
|
-
),
|
|
200
|
-
slots: keyed((slotName) => slotFacade(comp, slotName)),
|
|
201
|
-
frame: settle,
|
|
202
|
-
})
|
|
203
|
-
|
|
204
|
-
const slotFacade = (parent: CompositionClass<unknown>, slotName: string): MountedSlotFacade => {
|
|
205
|
-
const slotClass = slotsOf(parent)[slotName]
|
|
206
|
-
const child = slotClass && bindings.get(slotClass)
|
|
207
|
-
if (child === undefined) {
|
|
208
|
-
return Effect.runSync(
|
|
209
|
-
Effect.dieMessage(new UnknownSlot({ parent: uiNameOf(parent), slot: slotName }).message),
|
|
210
|
-
)
|
|
211
|
-
}
|
|
212
|
-
const childName = uiNameOf(child)
|
|
213
|
-
const atIndex = (index: number): Effect.Effect<MountedFacade, never, never> =>
|
|
214
|
-
Effect.as(rerender, nodeFacade({ name: childName, index }, child))
|
|
215
|
-
const first = nodeFacade({ name: childName, index: 0 }, child)
|
|
216
|
-
const indexOfKey = (key: string): Option.Option<number> =>
|
|
217
|
-
Arr.findFirstIndex(capturesFor(sink, childName), (capture) => capture.key === key)
|
|
218
|
-
return {
|
|
219
|
-
first: atIndex(0),
|
|
220
|
-
at: atIndex,
|
|
221
|
-
all: Effect.map(rerender, () =>
|
|
222
|
-
capturesFor(sink, childName).map((_capture, index) =>
|
|
223
|
-
nodeFacade({ name: childName, index }, child),
|
|
224
|
-
),
|
|
225
|
-
),
|
|
226
|
-
byKey: (key) =>
|
|
227
|
-
Effect.flatMap(rerender, () =>
|
|
228
|
-
Option.match(indexOfKey(key), {
|
|
229
|
-
onNone: () =>
|
|
230
|
-
Effect.dieMessage(
|
|
231
|
-
new UnknownSlot({
|
|
232
|
-
parent: uiNameOf(parent),
|
|
233
|
-
slot: `${slotName}[key=${key}]`,
|
|
234
|
-
}).message,
|
|
235
|
-
),
|
|
236
|
-
onSome: (index) => Effect.succeed(nodeFacade({ name: childName, index }, child)),
|
|
237
|
-
}),
|
|
238
|
-
),
|
|
239
|
-
where: (predicate) =>
|
|
240
|
-
Effect.map(rerender, () =>
|
|
241
|
-
capturesFor(sink, childName).flatMap((capture, index) =>
|
|
242
|
-
predicate(capture.props) ? [nodeFacade({ name: childName, index }, child)] : [],
|
|
243
|
-
),
|
|
244
|
-
),
|
|
245
|
-
count: Effect.map(rerender, () => capturesFor(sink, childName).length),
|
|
246
|
-
props: first.props,
|
|
247
|
-
expectProps: first.expectProps,
|
|
248
|
-
actions: first.actions,
|
|
249
|
-
slots: first.slots,
|
|
250
|
-
frame: first.frame,
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
return {
|
|
255
|
-
facade: nodeFacade({ name: uiNameOf(root), index: 0 }, root),
|
|
256
|
-
settle,
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
export const makeRuntimeHandleFacade = (
|
|
261
|
-
runtime: RuntimeHandle,
|
|
262
|
-
root: CompositionClass<unknown>,
|
|
263
|
-
sink: Sink,
|
|
264
|
-
dispatched: Set<string>,
|
|
265
|
-
): {
|
|
266
|
-
readonly facade: MountedFacade
|
|
267
|
-
readonly settle: Effect.Effect<void, never, never>
|
|
268
|
-
} => makeMountedFacade(runtime, root, sink, dispatched)
|
|
269
|
-
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { Array as Arr, Order } from 'effect'
|
|
2
|
-
import type { TreeFeatureMount } from './runtimeTreeTypes'
|
|
3
|
-
|
|
4
|
-
interface DisposeTreeMountsOptions {
|
|
5
|
-
readonly status: { disposed: boolean }
|
|
6
|
-
readonly mounts: Map<string, TreeFeatureMount>
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export const disposeTreeMounts = ({ status, mounts }: DisposeTreeMountsOptions): void => {
|
|
10
|
-
if (status.disposed) {
|
|
11
|
-
return
|
|
12
|
-
}
|
|
13
|
-
status.disposed = true
|
|
14
|
-
Arr.sortWith(Array.from(mounts.values()), (record) => -record.depth, Order.number).forEach(
|
|
15
|
-
(record) => {
|
|
16
|
-
record.attempt += 1
|
|
17
|
-
record.dispose()
|
|
18
|
-
},
|
|
19
|
-
)
|
|
20
|
-
mounts.clear()
|
|
21
|
-
}
|