@playfast/reform-proof 1.1.1 → 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/README.md +12 -12
- package/package.json +18 -18
- package/src/{assertions.ts → assert.ts} +3 -3
- package/src/engine.ts +285 -8
- 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/{runtimeTreeTypes.ts → engineTreeTypes.ts} +26 -22
- package/src/errors.ts +19 -0
- package/src/facade.ts +71 -251
- package/src/fingerprint.test.ts +24 -0
- package/src/fingerprint.ts +73 -0
- package/src/index.ts +165 -115
- package/src/nested-feature.test.ts +174 -0
- package/src/product.ts +50 -19
- package/src/proofCase.ts +46 -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/test-clock.test.ts +135 -0
- package/src/typed-seams.test.ts +6 -3
- package/src/driverTypes.ts +0 -19
- package/src/execution.ts +0 -100
- 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/sink.ts +0 -73
- package/src/treeBindings.ts +0 -121
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { Effect, Match, Option, Record as Rec } from 'effect'
|
|
2
|
+
import { isFeatureBinding } from '@playfast/reform'
|
|
3
|
+
import { formatFingerprint } from './fingerprint'
|
|
4
|
+
import {
|
|
5
|
+
eventsOf,
|
|
6
|
+
type HostRuntime,
|
|
7
|
+
renderCompositionRuntime,
|
|
8
|
+
SETTLE_MAX_RENDERS,
|
|
9
|
+
settleDrain,
|
|
10
|
+
settleStep,
|
|
11
|
+
type Sink,
|
|
12
|
+
uiNameOf,
|
|
13
|
+
} from './engineSink'
|
|
14
|
+
import type { AnyComposition } from '@playfast/reform/internal'
|
|
15
|
+
import {
|
|
16
|
+
ensureSettled,
|
|
17
|
+
readRuntimeSlotChild,
|
|
18
|
+
type SettleProgress,
|
|
19
|
+
slotsOf,
|
|
20
|
+
structureFills,
|
|
21
|
+
} from './engineTree'
|
|
22
|
+
import { makeFeatureMountRegistry } from './engineMounts'
|
|
23
|
+
import {
|
|
24
|
+
encodeIdentityPart,
|
|
25
|
+
fillEntries,
|
|
26
|
+
type RenderCompositionInput,
|
|
27
|
+
type TreeDriver,
|
|
28
|
+
type TreeNode,
|
|
29
|
+
type TreeRenderState,
|
|
30
|
+
treeFingerprint,
|
|
31
|
+
} from './engineTreeTypes'
|
|
32
|
+
|
|
33
|
+
export const makeTreeDriver = (
|
|
34
|
+
runtime: HostRuntime,
|
|
35
|
+
root: AnyComposition,
|
|
36
|
+
rootProps: unknown,
|
|
37
|
+
sink: Sink,
|
|
38
|
+
): TreeDriver => { const status = { disposed: false }
|
|
39
|
+
const registry = makeFeatureMountRegistry(status)
|
|
40
|
+
const { disposeStale, mountId, retryMount } = registry
|
|
41
|
+
const featureRecordForRender = registry.recordForRender
|
|
42
|
+
|
|
43
|
+
const renderComposition = ({
|
|
44
|
+
runtime: currentRuntime,
|
|
45
|
+
comp,
|
|
46
|
+
props,
|
|
47
|
+
key,
|
|
48
|
+
path,
|
|
49
|
+
depth,
|
|
50
|
+
seen,
|
|
51
|
+
mountMissing,
|
|
52
|
+
}: RenderCompositionInput): TreeNode => {
|
|
53
|
+
const endRenderSpan = sink.instrumentation.uiRendered(uiNameOf(comp))
|
|
54
|
+
const frame = renderCompositionRuntime(currentRuntime, comp, props)
|
|
55
|
+
endRenderSpan()
|
|
56
|
+
sink.api.record({
|
|
57
|
+
name: uiNameOf(comp),
|
|
58
|
+
props: frame.props,
|
|
59
|
+
events: eventsOf(frame),
|
|
60
|
+
...(Option.isSome(key) ? { key: key.value } : {}),
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
const fills = structureFills(frame)
|
|
64
|
+
const renderedSlots = Rec.map(slotsOf(comp), (slotClass, slotName) => {
|
|
65
|
+
const fill = fills[slotName]
|
|
66
|
+
if (fill === undefined) {
|
|
67
|
+
return []
|
|
68
|
+
}
|
|
69
|
+
const child = readRuntimeSlotChild({
|
|
70
|
+
runtime: currentRuntime,
|
|
71
|
+
slotDefinition: slotClass,
|
|
72
|
+
parent: comp,
|
|
73
|
+
})
|
|
74
|
+
return fillEntries(slotName, fill).flatMap((entry) => {
|
|
75
|
+
const childPath = `${path}${encodeIdentityPart(slotName)}${encodeIdentityPart(entry.key)}`
|
|
76
|
+
if (!isFeatureBinding(child)) {
|
|
77
|
+
return [
|
|
78
|
+
renderComposition({
|
|
79
|
+
runtime: currentRuntime,
|
|
80
|
+
comp: child,
|
|
81
|
+
props: entry.props,
|
|
82
|
+
key: Option.some(entry.key),
|
|
83
|
+
path: childPath,
|
|
84
|
+
depth: depth + 1,
|
|
85
|
+
seen,
|
|
86
|
+
mountMissing,
|
|
87
|
+
}),
|
|
88
|
+
]
|
|
89
|
+
}
|
|
90
|
+
const id = mountId({
|
|
91
|
+
parent: currentRuntime,
|
|
92
|
+
parentPath: path,
|
|
93
|
+
slotName,
|
|
94
|
+
key: entry.key,
|
|
95
|
+
binding: child,
|
|
96
|
+
})
|
|
97
|
+
seen.add(id)
|
|
98
|
+
const record = featureRecordForRender(
|
|
99
|
+
{
|
|
100
|
+
id,
|
|
101
|
+
parent: currentRuntime,
|
|
102
|
+
binding: child,
|
|
103
|
+
depth: depth + 1,
|
|
104
|
+
props: entry.props,
|
|
105
|
+
},
|
|
106
|
+
mountMissing,
|
|
107
|
+
)
|
|
108
|
+
if (record === undefined) {
|
|
109
|
+
return []
|
|
110
|
+
}
|
|
111
|
+
return child.capture<ReadonlyArray<TreeNode>>((binding) =>
|
|
112
|
+
Match.value(record.state).pipe(
|
|
113
|
+
Match.when({ _tag: 'Live' }, (live) => [
|
|
114
|
+
renderComposition({
|
|
115
|
+
runtime: live.runtime,
|
|
116
|
+
comp: binding.composition,
|
|
117
|
+
props: entry.props,
|
|
118
|
+
key: Option.some(entry.key),
|
|
119
|
+
path: childPath,
|
|
120
|
+
depth: depth + 1,
|
|
121
|
+
seen,
|
|
122
|
+
mountMissing,
|
|
123
|
+
}),
|
|
124
|
+
]),
|
|
125
|
+
Match.when({ _tag: 'Failed' }, (failed) =>
|
|
126
|
+
Option.match(binding.placeholder, {
|
|
127
|
+
onNone: () => [],
|
|
128
|
+
onSome: (placeholder) => [
|
|
129
|
+
renderComposition({
|
|
130
|
+
runtime: currentRuntime,
|
|
131
|
+
comp: placeholder.failed,
|
|
132
|
+
props: {
|
|
133
|
+
error: failed.error,
|
|
134
|
+
retry: () => retryMount(record),
|
|
135
|
+
},
|
|
136
|
+
key: Option.some(entry.key),
|
|
137
|
+
path: `${childPath}/failed`,
|
|
138
|
+
depth: depth + 1,
|
|
139
|
+
seen,
|
|
140
|
+
mountMissing,
|
|
141
|
+
}),
|
|
142
|
+
],
|
|
143
|
+
}),
|
|
144
|
+
),
|
|
145
|
+
Match.when({ _tag: 'Loading' }, () =>
|
|
146
|
+
Option.match(binding.placeholder, {
|
|
147
|
+
onNone: () => [],
|
|
148
|
+
onSome: (placeholder) => [
|
|
149
|
+
renderComposition({
|
|
150
|
+
runtime: currentRuntime,
|
|
151
|
+
comp: placeholder.loading,
|
|
152
|
+
props: entry.props,
|
|
153
|
+
key: Option.some(entry.key),
|
|
154
|
+
path: `${childPath}/loading`,
|
|
155
|
+
depth: depth + 1,
|
|
156
|
+
seen,
|
|
157
|
+
mountMissing,
|
|
158
|
+
}),
|
|
159
|
+
],
|
|
160
|
+
}),
|
|
161
|
+
),
|
|
162
|
+
Match.exhaustive,
|
|
163
|
+
),
|
|
164
|
+
)
|
|
165
|
+
})
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
path,
|
|
170
|
+
depth,
|
|
171
|
+
key,
|
|
172
|
+
comp,
|
|
173
|
+
props: frame.props,
|
|
174
|
+
events: eventsOf(frame),
|
|
175
|
+
slots: renderedSlots,
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
const render = (): TreeRenderState => {
|
|
179
|
+
// Discover the desired graph without acquiring missing Features, then release
|
|
180
|
+
// removed scopes deepest-first before the live pass mounts replacements.
|
|
181
|
+
sink.reset()
|
|
182
|
+
const retained = new Set<string>()
|
|
183
|
+
renderComposition({
|
|
184
|
+
runtime,
|
|
185
|
+
comp: root,
|
|
186
|
+
props: rootProps,
|
|
187
|
+
key: Option.none(),
|
|
188
|
+
path: 'root',
|
|
189
|
+
depth: 0,
|
|
190
|
+
seen: retained,
|
|
191
|
+
mountMissing: false,
|
|
192
|
+
})
|
|
193
|
+
disposeStale(retained)
|
|
194
|
+
sink.reset()
|
|
195
|
+
const seen = new Set<string>()
|
|
196
|
+
const rootNode = renderComposition({
|
|
197
|
+
runtime,
|
|
198
|
+
comp: root,
|
|
199
|
+
props: rootProps,
|
|
200
|
+
key: Option.none(),
|
|
201
|
+
path: 'root',
|
|
202
|
+
depth: 0,
|
|
203
|
+
seen,
|
|
204
|
+
mountMissing: true,
|
|
205
|
+
})
|
|
206
|
+
return {
|
|
207
|
+
root: rootNode,
|
|
208
|
+
fingerprint: formatFingerprint(treeFingerprint(rootNode)),
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const settle = Effect.gen(function* () {
|
|
213
|
+
yield* settleDrain
|
|
214
|
+
const first = render()
|
|
215
|
+
const progress = yield* Effect.iterate(
|
|
216
|
+
{
|
|
217
|
+
previous: first.fingerprint,
|
|
218
|
+
remaining: SETTLE_MAX_RENDERS,
|
|
219
|
+
stable: false,
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
while: (progress: SettleProgress) => !progress.stable && progress.remaining > 0,
|
|
223
|
+
body: (progress) =>
|
|
224
|
+
Effect.gen(function* () {
|
|
225
|
+
yield* settleDrain
|
|
226
|
+
yield* settleStep
|
|
227
|
+
const current = render()
|
|
228
|
+
return {
|
|
229
|
+
previous: current.fingerprint,
|
|
230
|
+
remaining: progress.remaining - 1,
|
|
231
|
+
stable: current.fingerprint === progress.previous,
|
|
232
|
+
}
|
|
233
|
+
}),
|
|
234
|
+
},
|
|
235
|
+
)
|
|
236
|
+
yield* ensureSettled(progress)
|
|
237
|
+
})
|
|
238
|
+
const dispose = (): void => {
|
|
239
|
+
if (status.disposed) {
|
|
240
|
+
return
|
|
241
|
+
}
|
|
242
|
+
status.disposed = true
|
|
243
|
+
registry.disposeAll()
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return { render, settle, dispose }
|
|
247
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { Array as Arr, type Duration, Effect, Option, TestClock } from 'effect'
|
|
2
|
+
import { AssertionFailed, NoTestClock, UnknownAction, UnknownSlot } from './errors'
|
|
3
|
+
import { matchProps } from './assert'
|
|
4
|
+
import { type RuntimeHandle, type UiContract } from '@playfast/reform'
|
|
5
|
+
import type { AnyComposition } from '@playfast/reform/internal'
|
|
6
|
+
import {
|
|
7
|
+
type HostRuntime,
|
|
8
|
+
keyed,
|
|
9
|
+
type MountedFacadeErasure,
|
|
10
|
+
type MountedSlotFacadeErasure,
|
|
11
|
+
type RuntimeRootComposition,
|
|
12
|
+
type Sink,
|
|
13
|
+
uiNameOf,
|
|
14
|
+
} from './engineSink'
|
|
15
|
+
import { makeTreeDriver } from './engineTreeDriver'
|
|
16
|
+
import type { RuntimeTreeFacade, RuntimeTreeFacadeErasure, TreeNode } from './engineTreeTypes'
|
|
17
|
+
|
|
18
|
+
export function makeRuntimeTreeFacade<
|
|
19
|
+
Services,
|
|
20
|
+
RootIdentifier extends NoInfer<Services>,
|
|
21
|
+
P,
|
|
22
|
+
C extends UiContract,
|
|
23
|
+
S extends ReadonlyArray<unknown>,
|
|
24
|
+
N extends string,
|
|
25
|
+
Identity,
|
|
26
|
+
>(
|
|
27
|
+
runtime: RuntimeHandle<Services>,
|
|
28
|
+
root: RuntimeRootComposition<RootIdentifier, P, C, S, N, Identity>,
|
|
29
|
+
rootProps: P,
|
|
30
|
+
sink: Sink,
|
|
31
|
+
dispatched: Set<string>,
|
|
32
|
+
): RuntimeTreeFacade<C>
|
|
33
|
+
export function makeRuntimeTreeFacade(
|
|
34
|
+
runtime: HostRuntime,
|
|
35
|
+
root: AnyComposition,
|
|
36
|
+
rootProps: unknown,
|
|
37
|
+
sink: Sink,
|
|
38
|
+
dispatched: Set<string>,
|
|
39
|
+
): RuntimeTreeFacadeErasure {
|
|
40
|
+
const driver = makeTreeDriver(runtime, root, rootProps, sink)
|
|
41
|
+
const currentRoot = (): TreeNode => driver.render().root
|
|
42
|
+
|
|
43
|
+
// The nested-Feature twin of the mounted facade's `advance`: same virtual clock
|
|
44
|
+
// off the same runtime, settled through this driver instead.
|
|
45
|
+
const advance = (duration: Duration.DurationInput): Effect.Effect<void, never, never> =>
|
|
46
|
+
Option.match(runtime.readOption(TestClock.TestClock), {
|
|
47
|
+
onNone: () => Effect.dieMessage(new NoTestClock().message),
|
|
48
|
+
onSome: (clock) => clock.adjust(duration).pipe(Effect.flatMap(() => driver.settle)),
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
// `peek` is the non-dying half of `resolve`: None once the node has left the
|
|
52
|
+
// tree, so an action that removed its own node can still report a result.
|
|
53
|
+
const nodeFacade = (
|
|
54
|
+
resolve: () => TreeNode,
|
|
55
|
+
peek: () => Option.Option<TreeNode>,
|
|
56
|
+
): MountedFacadeErasure => ({
|
|
57
|
+
props: Effect.sync(() => resolve().props),
|
|
58
|
+
expectProps: (partial) =>
|
|
59
|
+
Effect.sync(() => {
|
|
60
|
+
const mismatch = matchProps({
|
|
61
|
+
actual: resolve().props,
|
|
62
|
+
expected: partial,
|
|
63
|
+
})
|
|
64
|
+
if (mismatch !== undefined) {
|
|
65
|
+
return Effect.runSync(
|
|
66
|
+
Effect.dieMessage(new AssertionFailed({ detail: mismatch }).message),
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
}),
|
|
70
|
+
actions: keyed(
|
|
71
|
+
(event) => (payload: unknown) =>
|
|
72
|
+
Effect.gen(function* () {
|
|
73
|
+
const node = resolve()
|
|
74
|
+
const trigger = node.events[event]
|
|
75
|
+
if (trigger === undefined) {
|
|
76
|
+
return yield* Effect.dieMessage(
|
|
77
|
+
new UnknownAction({
|
|
78
|
+
composition: uiNameOf(node.comp),
|
|
79
|
+
action: event,
|
|
80
|
+
rendered: 1,
|
|
81
|
+
}).message,
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
dispatched.add(event)
|
|
85
|
+
trigger(payload)
|
|
86
|
+
yield* driver.settle
|
|
87
|
+
// A self-removing action leaves nothing to re-read; report what the
|
|
88
|
+
// node last rendered instead of dying on the vanished child.
|
|
89
|
+
return Option.match(peek(), {
|
|
90
|
+
onNone: () => node.props,
|
|
91
|
+
onSome: (settled) => settled.props,
|
|
92
|
+
})
|
|
93
|
+
}),
|
|
94
|
+
),
|
|
95
|
+
slots: keyed((slotName) => slotFacade(resolve, slotName)),
|
|
96
|
+
frame: driver.settle,
|
|
97
|
+
advance,
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
const slotFacade = (
|
|
101
|
+
resolveParent: () => TreeNode,
|
|
102
|
+
slotName: string,
|
|
103
|
+
): MountedSlotFacadeErasure => {
|
|
104
|
+
const children = (): ReadonlyArray<TreeNode> => {
|
|
105
|
+
const parent = resolveParent()
|
|
106
|
+
const resolved = parent.slots[slotName]
|
|
107
|
+
if (resolved === undefined) {
|
|
108
|
+
return Effect.runSync(
|
|
109
|
+
Effect.dieMessage(
|
|
110
|
+
new UnknownSlot({ parent: uiNameOf(parent.comp), slot: slotName }).message,
|
|
111
|
+
),
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
return resolved
|
|
115
|
+
}
|
|
116
|
+
const peekAt = (index: number): Option.Option<TreeNode> =>
|
|
117
|
+
Option.fromNullable(children()[index])
|
|
118
|
+
const peekByKey = (key: string): Option.Option<TreeNode> =>
|
|
119
|
+
Arr.findFirst(children(), (candidate) => Option.contains(candidate.key, key))
|
|
120
|
+
const childAt = (index: number): MountedFacadeErasure =>
|
|
121
|
+
nodeFacade(
|
|
122
|
+
() => children()[index] ?? missingChild(index),
|
|
123
|
+
() => peekAt(index),
|
|
124
|
+
)
|
|
125
|
+
const childByKey = (key: string): MountedFacadeErasure =>
|
|
126
|
+
nodeFacade(
|
|
127
|
+
() => Option.getOrElse(peekByKey(key), () => missingKey(key)),
|
|
128
|
+
() => peekByKey(key),
|
|
129
|
+
)
|
|
130
|
+
const missingChild = (index: number): never => {
|
|
131
|
+
const parent = resolveParent()
|
|
132
|
+
return Effect.runSync(
|
|
133
|
+
Effect.dieMessage(
|
|
134
|
+
new UnknownSlot({
|
|
135
|
+
parent: uiNameOf(parent.comp),
|
|
136
|
+
slot: `${slotName}[${index}]`,
|
|
137
|
+
}).message,
|
|
138
|
+
),
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
const missingKey = (key: string): never => {
|
|
142
|
+
const parent = resolveParent()
|
|
143
|
+
return Effect.runSync(
|
|
144
|
+
Effect.dieMessage(
|
|
145
|
+
new UnknownSlot({
|
|
146
|
+
parent: uiNameOf(parent.comp),
|
|
147
|
+
slot: `${slotName}[key=${key}]`,
|
|
148
|
+
}).message,
|
|
149
|
+
),
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
const first = childAt(0)
|
|
153
|
+
return {
|
|
154
|
+
first: Effect.sync(() => childAt(0)),
|
|
155
|
+
at: (index) => Effect.sync(() => childAt(index)),
|
|
156
|
+
all: Effect.sync(() => children().map((_child, index) => childAt(index))),
|
|
157
|
+
byKey: (key) =>
|
|
158
|
+
Effect.sync(() => {
|
|
159
|
+
const child = Arr.findFirst(children(), (candidate) =>
|
|
160
|
+
Option.contains(candidate.key, key),
|
|
161
|
+
)
|
|
162
|
+
return Option.isNone(child) ? missingKey(key) : childByKey(key)
|
|
163
|
+
}),
|
|
164
|
+
where: (predicate) =>
|
|
165
|
+
Effect.sync(() =>
|
|
166
|
+
children().flatMap((child, index) => (predicate(child.props) ? [childAt(index)] : [])),
|
|
167
|
+
),
|
|
168
|
+
count: Effect.sync(() => children().length),
|
|
169
|
+
props: first.props,
|
|
170
|
+
expectProps: first.expectProps,
|
|
171
|
+
actions: first.actions,
|
|
172
|
+
slots: first.slots,
|
|
173
|
+
frame: driver.settle,
|
|
174
|
+
advance,
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
facade: nodeFacade(currentRoot, () => Option.some(currentRoot())),
|
|
180
|
+
settle: driver.settle,
|
|
181
|
+
dispose: driver.dispose,
|
|
182
|
+
}
|
|
183
|
+
}
|
|
@@ -1,23 +1,22 @@
|
|
|
1
|
-
import { Effect, Match, Option, Record as Rec } from 'effect'
|
|
1
|
+
import { type Effect, Match, type Option, Record as Rec } from 'effect'
|
|
2
|
+
import type { FeatureLoadFailed, SlotFill, Trigger, UiContract } from '@playfast/reform'
|
|
3
|
+
import type { AnyComposition, AnyFeatureBinding } from '@playfast/reform/internal'
|
|
2
4
|
import {
|
|
3
|
-
type
|
|
4
|
-
type
|
|
5
|
-
type
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
} from '@playfast/reform'
|
|
9
|
-
import type { MountedFacade } from './runtimeFacade'
|
|
10
|
-
import { uiNameOf } from './sink'
|
|
5
|
+
type HostRuntime,
|
|
6
|
+
type MountedFacade,
|
|
7
|
+
type MountedFacadeHandleErasure,
|
|
8
|
+
uiNameOf,
|
|
9
|
+
} from './engineSink'
|
|
11
10
|
|
|
12
11
|
export type FeatureMountState =
|
|
13
12
|
| { readonly _tag: 'Loading' }
|
|
14
|
-
| { readonly _tag: 'Live'; readonly runtime:
|
|
15
|
-
| { readonly _tag: 'Failed'; readonly error:
|
|
13
|
+
| { readonly _tag: 'Live'; readonly runtime: HostRuntime }
|
|
14
|
+
| { readonly _tag: 'Failed'; readonly error: FeatureLoadFailed }
|
|
16
15
|
|
|
17
16
|
export interface TreeFeatureMount {
|
|
18
17
|
readonly id: string
|
|
19
|
-
readonly binding:
|
|
20
|
-
readonly parent:
|
|
18
|
+
readonly binding: AnyFeatureBinding
|
|
19
|
+
readonly parent: HostRuntime
|
|
21
20
|
readonly depth: number
|
|
22
21
|
readonly props: unknown
|
|
23
22
|
attempt: number
|
|
@@ -29,7 +28,7 @@ export interface TreeNode {
|
|
|
29
28
|
readonly path: string
|
|
30
29
|
readonly depth: number
|
|
31
30
|
readonly key: Option.Option<string>
|
|
32
|
-
readonly comp:
|
|
31
|
+
readonly comp: AnyComposition
|
|
33
32
|
readonly props: unknown
|
|
34
33
|
readonly events: Record<string, Trigger<unknown>>
|
|
35
34
|
readonly slots: Readonly<Record<string, ReadonlyArray<TreeNode>>>
|
|
@@ -40,12 +39,16 @@ export interface TreeRenderState {
|
|
|
40
39
|
readonly fingerprint: string
|
|
41
40
|
}
|
|
42
41
|
|
|
43
|
-
export interface RuntimeTreeFacade {
|
|
44
|
-
readonly facade: MountedFacade
|
|
42
|
+
export interface RuntimeTreeFacade<C extends UiContract> {
|
|
43
|
+
readonly facade: MountedFacade<C>
|
|
45
44
|
readonly settle: Effect.Effect<void, never, never>
|
|
46
45
|
dispose(): void
|
|
47
46
|
}
|
|
48
47
|
|
|
48
|
+
export interface RuntimeTreeFacadeErasure extends MountedFacadeHandleErasure {
|
|
49
|
+
dispose(): void
|
|
50
|
+
}
|
|
51
|
+
|
|
49
52
|
export interface TreeDriver {
|
|
50
53
|
readonly render: () => TreeRenderState
|
|
51
54
|
readonly settle: Effect.Effect<void, never, never>
|
|
@@ -53,29 +56,30 @@ export interface TreeDriver {
|
|
|
53
56
|
}
|
|
54
57
|
|
|
55
58
|
export interface MountIdentityInput {
|
|
56
|
-
readonly parent:
|
|
59
|
+
readonly parent: HostRuntime
|
|
57
60
|
readonly parentPath: string
|
|
58
61
|
readonly slotName: string
|
|
59
62
|
readonly key: string
|
|
60
|
-
readonly binding:
|
|
63
|
+
readonly binding: AnyFeatureBinding
|
|
61
64
|
}
|
|
62
65
|
|
|
63
66
|
export interface FeatureRecordInput {
|
|
64
67
|
readonly id: string
|
|
65
|
-
readonly parent:
|
|
66
|
-
readonly binding:
|
|
68
|
+
readonly parent: HostRuntime
|
|
69
|
+
readonly binding: AnyFeatureBinding
|
|
67
70
|
readonly depth: number
|
|
68
71
|
readonly props: unknown
|
|
69
72
|
}
|
|
70
73
|
|
|
71
74
|
export interface RenderCompositionInput {
|
|
72
|
-
readonly runtime:
|
|
73
|
-
readonly comp:
|
|
75
|
+
readonly runtime: HostRuntime
|
|
76
|
+
readonly comp: AnyComposition
|
|
74
77
|
readonly props: unknown
|
|
75
78
|
readonly key: Option.Option<string>
|
|
76
79
|
readonly path: string
|
|
77
80
|
readonly depth: number
|
|
78
81
|
readonly seen: Set<string>
|
|
82
|
+
readonly mountMissing: boolean
|
|
79
83
|
}
|
|
80
84
|
|
|
81
85
|
export const encodeIdentityPart = (part: string): string => `${part.length}:${part}`
|
package/src/errors.ts
CHANGED
|
@@ -40,3 +40,22 @@ export class UnknownSlot extends UnknownSlotBase {
|
|
|
40
40
|
return `reform-proof: '${this.parent}' has no slot '${this.slot}'`
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
|
+
|
|
44
|
+
/** The `Data.TaggedError(tag)` shape for a payload-free error: a `void` argument. */
|
|
45
|
+
type TaggedErrorClassVoid<Tag extends string> = new (
|
|
46
|
+
args: void,
|
|
47
|
+
) => Cause.YieldableError & { readonly _tag: Tag }
|
|
48
|
+
|
|
49
|
+
const NoTestClockBase: TaggedErrorClassVoid<'reform-proof/NoTestClock'> = Data.TaggedError(
|
|
50
|
+
'reform-proof/NoTestClock',
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* `app.advance(...)` ran against a scene whose runtime still holds the real clock,
|
|
55
|
+
* so there is no product time to move.
|
|
56
|
+
*/
|
|
57
|
+
export class NoTestClock extends NoTestClockBase {
|
|
58
|
+
override get message(): string {
|
|
59
|
+
return "reform-proof: app.advance(...) needs a virtual clock — wrap the scene's layers with `Proof.withTestClock(...)`. Without it the scene's procedures sleep on the real clock and only elapse in real time."
|
|
60
|
+
}
|
|
61
|
+
}
|