foldkit 0.127.0 → 0.128.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 +2 -1
- package/dist/asyncData/asyncData.d.ts +6 -6
- package/dist/asyncData/asyncData.d.ts.map +1 -1
- package/dist/asyncData/asyncData.js +7 -7
- package/dist/asyncData/public.d.ts +1 -1
- package/dist/asyncData/public.d.ts.map +1 -1
- package/dist/asyncData/public.js +1 -1
- package/dist/command/index.d.ts +23 -5
- package/dist/command/index.d.ts.map +1 -1
- package/dist/command/index.js +35 -7
- package/dist/experimental/index.d.ts +2 -0
- package/dist/experimental/index.d.ts.map +1 -0
- package/dist/experimental/index.js +1 -0
- package/dist/experimental/machine/index.d.ts +2 -0
- package/dist/experimental/machine/index.d.ts.map +1 -0
- package/dist/experimental/machine/index.js +1 -0
- package/dist/experimental/machine/machine.d.ts +194 -0
- package/dist/experimental/machine/machine.d.ts.map +1 -0
- package/dist/experimental/machine/machine.js +237 -0
- package/dist/experimental/machine/public.d.ts +2 -0
- package/dist/experimental/machine/public.d.ts.map +1 -0
- package/dist/experimental/machine/public.js +1 -0
- package/dist/experimental/public.d.ts +2 -0
- package/dist/experimental/public.d.ts.map +1 -0
- package/dist/experimental/public.js +1 -0
- package/dist/html/boundary.d.ts +9 -0
- package/dist/html/boundary.d.ts.map +1 -1
- package/dist/html/boundary.js +24 -0
- package/dist/html/childAttribute.d.ts +4 -1
- package/dist/html/childAttribute.d.ts.map +1 -1
- package/dist/html/childAttribute.js +3 -1
- package/dist/html/index.d.ts +45 -2
- package/dist/html/index.d.ts.map +1 -1
- package/dist/html/index.js +46 -3
- package/dist/html/runtimeSingleton.d.ts +7 -0
- package/dist/html/runtimeSingleton.d.ts.map +1 -1
- package/dist/html/runtimeSingleton.js +14 -1
- package/dist/html/submodel.d.ts +10 -3
- package/dist/html/submodel.d.ts.map +1 -1
- package/dist/managedResource/index.d.ts +1 -37
- package/dist/managedResource/index.d.ts.map +1 -1
- package/dist/managedResource/index.js +1 -20
- package/dist/{runtime → managedResource}/managedResource.d.ts +36 -2
- package/dist/managedResource/managedResource.d.ts.map +1 -0
- package/dist/{runtime → managedResource}/managedResource.js +20 -1
- package/dist/managedResource/public.d.ts +2 -4
- package/dist/managedResource/public.d.ts.map +1 -1
- package/dist/managedResource/public.js +1 -2
- package/dist/port/port.d.ts +1 -1
- package/dist/port/port.d.ts.map +1 -1
- package/dist/port/port.js +1 -1
- package/dist/route/index.d.ts +1 -1
- package/dist/route/index.d.ts.map +1 -1
- package/dist/route/index.js +1 -1
- package/dist/route/parser.d.ts +31 -0
- package/dist/route/parser.d.ts.map +1 -1
- package/dist/route/parser.js +64 -0
- package/dist/route/public.d.ts +3 -2
- package/dist/route/public.d.ts.map +1 -1
- package/dist/route/public.js +2 -1
- package/dist/route/transition.d.ts +73 -12
- package/dist/route/transition.d.ts.map +1 -1
- package/dist/route/transition.js +63 -12
- package/dist/runtime/runtime.d.ts +2 -4
- package/dist/runtime/runtime.d.ts.map +1 -1
- package/dist/runtime/runtime.js +5 -17
- package/dist/submodel/submodel.d.ts +4 -4
- package/dist/submodel/submodel.d.ts.map +1 -1
- package/dist/subscription/public.d.ts +2 -2
- package/dist/subscription/public.d.ts.map +1 -1
- package/dist/subscription/public.js +1 -1
- package/dist/subscription/subscription.d.ts.map +1 -0
- package/dist/test/apps/formChild.d.ts +1 -1
- package/dist/test/apps/formChild.d.ts.map +1 -1
- package/dist/test/apps/formChild.js +2 -1
- package/dist/test/internal.d.ts +44 -28
- package/dist/test/internal.d.ts.map +1 -1
- package/dist/test/internal.js +52 -50
- package/dist/test/scene.d.ts +1 -6
- package/dist/test/scene.d.ts.map +1 -1
- package/dist/test/scene.js +13 -24
- package/dist/test/story.d.ts +0 -2
- package/dist/test/story.d.ts.map +1 -1
- package/dist/test/story.js +2 -5
- package/dist/vdom.d.ts +2 -0
- package/dist/vdom.d.ts.map +1 -1
- package/dist/vdom.js +11 -1
- package/package.json +9 -1
- package/dist/runtime/managedResource.d.ts.map +0 -1
- package/dist/runtime/subscription.d.ts.map +0 -1
- /package/dist/{runtime → subscription}/subscription.d.ts +0 -0
- /package/dist/{runtime → subscription}/subscription.js +0 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { Array, Match as M, Option, Predicate, Record, pipe, } from 'effect';
|
|
2
|
+
const makeEdge = (target, build, commands) => {
|
|
3
|
+
const narrowGuardValue = (input) => {
|
|
4
|
+
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */
|
|
5
|
+
return input;
|
|
6
|
+
};
|
|
7
|
+
return {
|
|
8
|
+
_tag: 'Edge',
|
|
9
|
+
target,
|
|
10
|
+
build: input => build(narrowGuardValue(input)),
|
|
11
|
+
maybeCommands: commands === undefined
|
|
12
|
+
? Option.none()
|
|
13
|
+
: Option.some(input => commands(narrowGuardValue(input))),
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Declares a transition Edge to the state variant named by `target`. The
|
|
18
|
+
* `build` callback receives an {@link EdgeInput} whose state and Message are
|
|
19
|
+
* narrowed to the variants the Edge sits under in the transition table, and
|
|
20
|
+
* must return the target variant. Optional `commands` attach transition-time
|
|
21
|
+
* effects.
|
|
22
|
+
*
|
|
23
|
+
* Only meaningful inside a {@link TransitionTable}: the source and trigger
|
|
24
|
+
* types flow in from the table position contextually.
|
|
25
|
+
*/
|
|
26
|
+
export const to = (target, build, commands) => makeEdge(target, build, commands);
|
|
27
|
+
/**
|
|
28
|
+
* Guards an Edge. Guard lists run in order, and the first guard that passes
|
|
29
|
+
* fires its Edge. A guard either resolves the state and Message to an
|
|
30
|
+
* `Option` (returning `Option.some` passes, and the wrapped value flows into
|
|
31
|
+
* the Edge's build and commands callbacks as `guardValue`) or returns a
|
|
32
|
+
* plain boolean when there is nothing to extract (returning `true` passes,
|
|
33
|
+
* and `guardValue` is `void`).
|
|
34
|
+
*
|
|
35
|
+
* The state and Message parameters are `NoInfer` so they resolve from the
|
|
36
|
+
* guard list's table position alone.
|
|
37
|
+
*/
|
|
38
|
+
export const when = (guard, target, build, commands) => ({
|
|
39
|
+
_tag: 'When',
|
|
40
|
+
guard: (state, message) => {
|
|
41
|
+
const result = guard(state, message);
|
|
42
|
+
if (Predicate.isBoolean(result)) {
|
|
43
|
+
return result ? Option.some(undefined) : Option.none();
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
edge: makeEdge(target, build, commands),
|
|
50
|
+
});
|
|
51
|
+
/** The unconditional fallback at the end of a guard list. The parameter is `NoInfer` for the same reason as {@link when}'s. */
|
|
52
|
+
export const otherwise = (edge) => ({
|
|
53
|
+
_tag: 'Otherwise',
|
|
54
|
+
edge,
|
|
55
|
+
});
|
|
56
|
+
const isGuardList = (edgeOrGuardedEdges) => globalThis.Array.isArray(edgeOrGuardedEdges);
|
|
57
|
+
const extractLiteralTag = (tagField) => {
|
|
58
|
+
if (Predicate.hasProperty(tagField, 'literal') &&
|
|
59
|
+
Predicate.isString(tagField.literal)) {
|
|
60
|
+
return Option.some(tagField.literal);
|
|
61
|
+
}
|
|
62
|
+
else if (Predicate.hasProperty(tagField, 'schema')) {
|
|
63
|
+
return extractLiteralTag(tagField.schema);
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
return Option.none();
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
const extractMemberTag = (member) => pipe(Option.some(member), Option.filter(Predicate.hasProperty('fields')), Option.map(struct => struct.fields), Option.filter(Predicate.hasProperty('_tag')), Option.flatMap(fields => extractLiteralTag(fields._tag)));
|
|
70
|
+
/**
|
|
71
|
+
* Compiles a declarative transition table into a {@link Machine}.
|
|
72
|
+
*
|
|
73
|
+
* Two stages: the first takes the state and Message union Schemas and fixes
|
|
74
|
+
* the type parameters, the second takes the initial state and the transition
|
|
75
|
+
* table. The split is what lets TypeScript narrow `state` and `message`
|
|
76
|
+
* inside every Edge from its table position: a single-call form checks the
|
|
77
|
+
* table while the type parameters are still being inferred, and the
|
|
78
|
+
* narrowing collapses.
|
|
79
|
+
*
|
|
80
|
+
* The Machine is not a runtime: `transition` has the same shape as a Foldkit
|
|
81
|
+
* `update` branch and returns `[nextState, commands]`, so the machine state
|
|
82
|
+
* lives in the Model and the Foldkit runtime never learns the Machine
|
|
83
|
+
* exists. Messages that match no Edge leave the state unchanged; use `step`
|
|
84
|
+
* when the `Ignored` outcome should be observable.
|
|
85
|
+
*
|
|
86
|
+
* Because every Edge names a literal target tag, the Edge set is plain data:
|
|
87
|
+
* `reachableFrom`, `unreachableStates`, `deadTransitions`, and `toMermaid`
|
|
88
|
+
* all read it directly.
|
|
89
|
+
*/
|
|
90
|
+
export const define = (schemas) => (definition) => {
|
|
91
|
+
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */
|
|
92
|
+
const looseStates = definition.states;
|
|
93
|
+
const initialTag = definition.initial._tag;
|
|
94
|
+
const stateTags = pipe(schemas.state.members, Array.map(member => Option.getOrThrowWith(extractMemberTag(member), () => new Error('Machine.define: every member of the state union Schema must be a Struct with a literal _tag field'))),
|
|
95
|
+
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */
|
|
96
|
+
tags => tags);
|
|
97
|
+
const makeEdgeSummary = (from, messageTag, target, guard) => ({ from, messageTag, target, guard });
|
|
98
|
+
const summarizeEntry = (from, messageTag, edgeOrGuardedEdges) => isGuardList(edgeOrGuardedEdges)
|
|
99
|
+
? Array.map(edgeOrGuardedEdges, (guardedEdge, position) => guardedEdge._tag === 'When'
|
|
100
|
+
? makeEdgeSummary(from, messageTag, guardedEdge.edge.target, {
|
|
101
|
+
_tag: 'When',
|
|
102
|
+
position,
|
|
103
|
+
})
|
|
104
|
+
: makeEdgeSummary(from, messageTag, guardedEdge.edge.target, {
|
|
105
|
+
_tag: 'Otherwise',
|
|
106
|
+
position,
|
|
107
|
+
}))
|
|
108
|
+
: [
|
|
109
|
+
makeEdgeSummary(from, messageTag, edgeOrGuardedEdges.target, {
|
|
110
|
+
_tag: 'Unguarded',
|
|
111
|
+
}),
|
|
112
|
+
];
|
|
113
|
+
const edges = pipe(Record.toEntries(looseStates), Array.flatMap(([sourceTag, stateEntry]) => pipe(Record.toEntries(stateEntry.on), Array.flatMap(([messageTag, edgeOrGuardedEdges]) => summarizeEntry(sourceTag, messageTag, edgeOrGuardedEdges)))));
|
|
114
|
+
const selectFromGuardList = (guardedEdges, state, message) => Array.matchLeft(guardedEdges, {
|
|
115
|
+
onEmpty: () => Option.none(),
|
|
116
|
+
onNonEmpty: (guardedEdge, rest) => {
|
|
117
|
+
if (guardedEdge._tag === 'When') {
|
|
118
|
+
const maybeGuardValue = guardedEdge.guard(state, message);
|
|
119
|
+
if (Option.isSome(maybeGuardValue)) {
|
|
120
|
+
return Option.some({
|
|
121
|
+
edge: guardedEdge.edge,
|
|
122
|
+
guardValue: maybeGuardValue.value,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
return selectFromGuardList(rest, state, message);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
return Option.some({
|
|
131
|
+
edge: guardedEdge.edge,
|
|
132
|
+
guardValue: undefined,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
const chooseEdge = (edgeOrGuardedEdges, state, message) => isGuardList(edgeOrGuardedEdges)
|
|
138
|
+
? selectFromGuardList(edgeOrGuardedEdges, state, message)
|
|
139
|
+
: Option.some({ edge: edgeOrGuardedEdges, guardValue: undefined });
|
|
140
|
+
const makeTransitioned = (state, message, selectedEdge) => ({
|
|
141
|
+
_tag: 'Transitioned',
|
|
142
|
+
from: state._tag,
|
|
143
|
+
target: selectedEdge.edge.target,
|
|
144
|
+
messageTag: message._tag,
|
|
145
|
+
state: selectedEdge.edge.build({
|
|
146
|
+
state,
|
|
147
|
+
message,
|
|
148
|
+
guardValue: selectedEdge.guardValue,
|
|
149
|
+
}),
|
|
150
|
+
commands: Option.match(selectedEdge.edge.maybeCommands, {
|
|
151
|
+
onNone: () => [],
|
|
152
|
+
onSome: buildCommands => buildCommands({
|
|
153
|
+
state,
|
|
154
|
+
message,
|
|
155
|
+
guardValue: selectedEdge.guardValue,
|
|
156
|
+
}),
|
|
157
|
+
}),
|
|
158
|
+
});
|
|
159
|
+
const makeIgnored = (state, message) => ({
|
|
160
|
+
_tag: 'Ignored',
|
|
161
|
+
stateTag: state._tag,
|
|
162
|
+
messageTag: message._tag,
|
|
163
|
+
state,
|
|
164
|
+
});
|
|
165
|
+
const step = (state, message) => pipe(Record.get(looseStates, state._tag), Option.flatMap(stateEntry => Record.get(stateEntry.on, message._tag)), Option.flatMap(edgeOrGuardedEdges => chooseEdge(edgeOrGuardedEdges, state, message)), Option.match({
|
|
166
|
+
onNone: () => makeIgnored(state, message),
|
|
167
|
+
onSome: selectedEdge => makeTransitioned(state, message, selectedEdge),
|
|
168
|
+
}));
|
|
169
|
+
const transition = (state, message) => {
|
|
170
|
+
const result = step(state, message);
|
|
171
|
+
if (result._tag === 'Transitioned') {
|
|
172
|
+
return [result.state, result.commands];
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
return [result.state, []];
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
const targetsFrom = (tag) => pipe(edges, Array.filter(edgeSummary => edgeSummary.from === tag), Array.map(edgeSummary => edgeSummary.target));
|
|
179
|
+
const reachableFrom = (tag) => {
|
|
180
|
+
const visit = (frontier, visited) => Array.matchLeft(frontier, {
|
|
181
|
+
onEmpty: () => visited,
|
|
182
|
+
onNonEmpty: (head, tail) => visited.has(head)
|
|
183
|
+
? visit(tail, visited)
|
|
184
|
+
: visit([...tail, ...targetsFrom(head)], new Set([...visited, head])),
|
|
185
|
+
});
|
|
186
|
+
return visit([tag], new Set());
|
|
187
|
+
};
|
|
188
|
+
const unreachableStates = () => {
|
|
189
|
+
const reachable = reachableFrom(initialTag);
|
|
190
|
+
return Array.filter(stateTags, stateTag => !reachable.has(stateTag));
|
|
191
|
+
};
|
|
192
|
+
const makeDeadTransition = (edge, reason) => ({ edge, reason });
|
|
193
|
+
const guardPosition = (edgeSummary) => edgeSummary.guard._tag === 'Unguarded'
|
|
194
|
+
? Option.none()
|
|
195
|
+
: Option.some(edgeSummary.guard.position);
|
|
196
|
+
const shadowedEdges = () => pipe(edges, Array.groupBy(edgeSummary => `${edgeSummary.from}|${edgeSummary.messageTag}`), Record.values, Array.flatMap(group => {
|
|
197
|
+
const maybeOtherwisePosition = pipe(group, Array.findFirst(edgeSummary => edgeSummary.guard._tag === 'Otherwise'), Option.flatMap(guardPosition));
|
|
198
|
+
return Option.match(maybeOtherwisePosition, {
|
|
199
|
+
onNone: () => [],
|
|
200
|
+
onSome: otherwisePosition => pipe(group, Array.filter(edgeSummary => Option.match(guardPosition(edgeSummary), {
|
|
201
|
+
onNone: () => false,
|
|
202
|
+
onSome: position => position > otherwisePosition,
|
|
203
|
+
})), Array.map(edgeSummary => makeDeadTransition(edgeSummary, 'ShadowedByOtherwise'))),
|
|
204
|
+
});
|
|
205
|
+
}));
|
|
206
|
+
const deadTransitions = () => {
|
|
207
|
+
const reachable = reachableFrom(initialTag);
|
|
208
|
+
const unreachableSourceEdges = pipe(edges, Array.filter(edgeSummary => !reachable.has(edgeSummary.from)), Array.map(edgeSummary => makeDeadTransition(edgeSummary, 'UnreachableSource')));
|
|
209
|
+
return [...unreachableSourceEdges, ...shadowedEdges()];
|
|
210
|
+
};
|
|
211
|
+
const toMermaid = () => {
|
|
212
|
+
const guardLabel = (guard) => M.value(guard).pipe(M.tagsExhaustive({
|
|
213
|
+
Unguarded: () => '',
|
|
214
|
+
When: ({ position }) => ` [when ${position + 1}]`,
|
|
215
|
+
Otherwise: () => ' [otherwise]',
|
|
216
|
+
}));
|
|
217
|
+
const stateLines = Array.map(stateTags, stateTag => ` ${stateTag}`);
|
|
218
|
+
const transitionLines = Array.map(edges, edgeSummary => ` ${edgeSummary.from} --> ${edgeSummary.target}: ${edgeSummary.messageTag}${guardLabel(edgeSummary.guard)}`);
|
|
219
|
+
return Array.join([
|
|
220
|
+
'stateDiagram-v2',
|
|
221
|
+
...stateLines,
|
|
222
|
+
` [*] --> ${initialTag}`,
|
|
223
|
+
...transitionLines,
|
|
224
|
+
], '\n');
|
|
225
|
+
};
|
|
226
|
+
return {
|
|
227
|
+
initial: definition.initial,
|
|
228
|
+
stateTags,
|
|
229
|
+
edges,
|
|
230
|
+
transition,
|
|
231
|
+
step,
|
|
232
|
+
reachableFrom,
|
|
233
|
+
unreachableStates,
|
|
234
|
+
deadTransitions,
|
|
235
|
+
toMermaid,
|
|
236
|
+
};
|
|
237
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"public.d.ts","sourceRoot":"","sources":["../../../src/experimental/machine/public.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './index.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"public.d.ts","sourceRoot":"","sources":["../../src/experimental/public.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './index.js';
|
package/dist/html/boundary.d.ts
CHANGED
|
@@ -105,6 +105,15 @@ export declare const deregisterBoundaryWrap: (registry: BoundaryRegistry, bounda
|
|
|
105
105
|
* avoids that race. Throws here (at resolve time, boundary alive) if a wrap is
|
|
106
106
|
* somehow already missing, surfacing a real corruption rather than misrouting. */
|
|
107
107
|
export declare const resolveBoundaryDispatchThunk: (registry: BoundaryRegistry, outerDispatch: DispatchSync, boundaryId: BoundaryId, message: unknown) => (() => void);
|
|
108
|
+
/** Collects the `toParentMessage` wrapping chain for `boundaryId`, innermost
|
|
109
|
+
* ancestor first, against the wraps present right now. Folding the returned
|
|
110
|
+
* functions left-to-right over a child message reproduces exactly what
|
|
111
|
+
* {@link dispatchAcrossBoundary} dispatches, without dispatching. Returns an
|
|
112
|
+
* empty array at the root boundary. Used by `OnMount` to snapshot a
|
|
113
|
+
* Submodel-embedded mount's lift eagerly so the Scene test harness can replay
|
|
114
|
+
* it when the mount is resolved. Throws when an ancestor wrap is missing
|
|
115
|
+
* (boundary alive at call time), matching {@link dispatchAcrossBoundary}. */
|
|
116
|
+
export declare const boundaryMappers: (registry: BoundaryRegistry, boundaryId: BoundaryId) => ReadonlyArray<(message: unknown) => unknown>;
|
|
108
117
|
export declare const getOrCreateBoundaryDispatch: (registry: BoundaryRegistry, outerDispatch: DispatchSync, boundaryId: BoundaryId) => DispatchSync;
|
|
109
118
|
/** Called at the start of each top-level render. Clears the
|
|
110
119
|
* per-render duplicate-slotId tracking map so siblings inside the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"boundary.d.ts","sourceRoot":"","sources":["../../src/html/boundary.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAA;AAEzD,wDAAwD;AACxD,MAAM,MAAM,cAAc,GAAG,QAAQ,CAAC;IACpC,eAAe,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAA;CAC/C,CAAC,CAAA;AAEF;;;;eAIe;AACf,MAAM,MAAM,UAAU,GAAG,MAAM,CAAA;AAI/B,eAAO,MAAM,aAAa,EAAE,UAAe,CAAA;AAE3C,eAAO,MAAM,eAAe,GAC1B,QAAQ,UAAU,EAClB,SAAS,MAAM,KACd,UAUF,CAAA;AAKD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;aAwCa;AACb,MAAM,MAAM,gBAAgB,GAAG;IAC7B,QAAQ,CAAC,KAAK,EAAE,GAAG,CAAC,UAAU,EAAE,cAAc,CAAC,CAAA;IAC/C,QAAQ,CAAC,kBAAkB,EAAE,OAAO,CAClC,YAAY,EACZ,GAAG,CAAC,UAAU,EAAE,YAAY,CAAC,CAC9B,CAAA;IACD,QAAQ,CAAC,cAAc,EAAE,GAAG,CAAC,UAAU,EAAE,MAAM,CAAC,CAAA;IAChD,QAAQ,CAAC,iBAAiB,EAAE,KAAK,CAAC,GAAG,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC,CAAA;IAI1D,QAAQ,CAAC,UAAU,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;CACjC,CAAA;AAED,eAAO,MAAM,sBAAsB,QAAO,gBAMxC,CAAA;AAqBF,eAAO,MAAM,oBAAoB,GAC/B,UAAU,gBAAgB,EAC1B,YAAY,UAAU,EACtB,YAAY,cAAc,KACzB,IA6BF,CAAA;AAED;;;;wCAIwC;AACxC,eAAO,MAAM,iBAAiB,GAC5B,UAAU,gBAAgB,KACzB,GAAG,CAAC,UAAU,EAAE,MAAM,CAIxB,CAAA;AAED;;yCAEyC;AACzC,eAAO,MAAM,eAAe,GAAI,UAAU,gBAAgB,KAAG,IAQ5D,CAAA;AAED;;;;;;;4CAO4C;AAC5C,eAAO,MAAM,kBAAkB,GAC7B,UAAU,gBAAgB,EAC1B,YAAY,WAAW,CAAC,UAAU,EAAE,MAAM,CAAC,KAC1C,IAWF,CAAA;AAED;;;;;;;;;gBASgB;AAChB,eAAO,MAAM,sBAAsB,GACjC,UAAU,gBAAgB,EAC1B,YAAY,UAAU,KACrB,IAEF,CAAA;AA8CD;;;;;;;;;;;;mFAYmF;AACnF,eAAO,MAAM,4BAA4B,GACvC,UAAU,gBAAgB,EAC1B,eAAe,YAAY,EAC3B,YAAY,UAAU,EACtB,SAAS,OAAO,KACf,CAAC,MAAM,IAAI,CAqBb,CAAA;AAED,eAAO,MAAM,2BAA2B,GACtC,UAAU,gBAAgB,EAC1B,eAAe,YAAY,EAC3B,YAAY,UAAU,KACrB,YAkBF,CAAA;AAED;;;;;;;;;;iEAUiE;AACjE,eAAO,MAAM,WAAW,GAAI,UAAU,gBAAgB,KAAG,IAGxD,CAAA"}
|
|
1
|
+
{"version":3,"file":"boundary.d.ts","sourceRoot":"","sources":["../../src/html/boundary.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAA;AAEzD,wDAAwD;AACxD,MAAM,MAAM,cAAc,GAAG,QAAQ,CAAC;IACpC,eAAe,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAA;CAC/C,CAAC,CAAA;AAEF;;;;eAIe;AACf,MAAM,MAAM,UAAU,GAAG,MAAM,CAAA;AAI/B,eAAO,MAAM,aAAa,EAAE,UAAe,CAAA;AAE3C,eAAO,MAAM,eAAe,GAC1B,QAAQ,UAAU,EAClB,SAAS,MAAM,KACd,UAUF,CAAA;AAKD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;aAwCa;AACb,MAAM,MAAM,gBAAgB,GAAG;IAC7B,QAAQ,CAAC,KAAK,EAAE,GAAG,CAAC,UAAU,EAAE,cAAc,CAAC,CAAA;IAC/C,QAAQ,CAAC,kBAAkB,EAAE,OAAO,CAClC,YAAY,EACZ,GAAG,CAAC,UAAU,EAAE,YAAY,CAAC,CAC9B,CAAA;IACD,QAAQ,CAAC,cAAc,EAAE,GAAG,CAAC,UAAU,EAAE,MAAM,CAAC,CAAA;IAChD,QAAQ,CAAC,iBAAiB,EAAE,KAAK,CAAC,GAAG,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC,CAAA;IAI1D,QAAQ,CAAC,UAAU,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;CACjC,CAAA;AAED,eAAO,MAAM,sBAAsB,QAAO,gBAMxC,CAAA;AAqBF,eAAO,MAAM,oBAAoB,GAC/B,UAAU,gBAAgB,EAC1B,YAAY,UAAU,EACtB,YAAY,cAAc,KACzB,IA6BF,CAAA;AAED;;;;wCAIwC;AACxC,eAAO,MAAM,iBAAiB,GAC5B,UAAU,gBAAgB,KACzB,GAAG,CAAC,UAAU,EAAE,MAAM,CAIxB,CAAA;AAED;;yCAEyC;AACzC,eAAO,MAAM,eAAe,GAAI,UAAU,gBAAgB,KAAG,IAQ5D,CAAA;AAED;;;;;;;4CAO4C;AAC5C,eAAO,MAAM,kBAAkB,GAC7B,UAAU,gBAAgB,EAC1B,YAAY,WAAW,CAAC,UAAU,EAAE,MAAM,CAAC,KAC1C,IAWF,CAAA;AAED;;;;;;;;;gBASgB;AAChB,eAAO,MAAM,sBAAsB,GACjC,UAAU,gBAAgB,EAC1B,YAAY,UAAU,KACrB,IAEF,CAAA;AA8CD;;;;;;;;;;;;mFAYmF;AACnF,eAAO,MAAM,4BAA4B,GACvC,UAAU,gBAAgB,EAC1B,eAAe,YAAY,EAC3B,YAAY,UAAU,EACtB,SAAS,OAAO,KACf,CAAC,MAAM,IAAI,CAqBb,CAAA;AAED;;;;;;;8EAO8E;AAC9E,eAAO,MAAM,eAAe,GAC1B,UAAU,gBAAgB,EAC1B,YAAY,UAAU,KACrB,aAAa,CAAC,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAiB7C,CAAA;AAED,eAAO,MAAM,2BAA2B,GACtC,UAAU,gBAAgB,EAC1B,eAAe,YAAY,EAC3B,YAAY,UAAU,KACrB,YAkBF,CAAA;AAED;;;;;;;;;;iEAUiE;AACjE,eAAO,MAAM,WAAW,GAAI,UAAU,gBAAgB,KAAG,IAGxD,CAAA"}
|
package/dist/html/boundary.js
CHANGED
|
@@ -183,6 +183,30 @@ export const resolveBoundaryDispatchThunk = (registry, outerDispatch, boundaryId
|
|
|
183
183
|
const rootMessage = wrapped;
|
|
184
184
|
return () => outerDispatch(rootMessage);
|
|
185
185
|
};
|
|
186
|
+
/** Collects the `toParentMessage` wrapping chain for `boundaryId`, innermost
|
|
187
|
+
* ancestor first, against the wraps present right now. Folding the returned
|
|
188
|
+
* functions left-to-right over a child message reproduces exactly what
|
|
189
|
+
* {@link dispatchAcrossBoundary} dispatches, without dispatching. Returns an
|
|
190
|
+
* empty array at the root boundary. Used by `OnMount` to snapshot a
|
|
191
|
+
* Submodel-embedded mount's lift eagerly so the Scene test harness can replay
|
|
192
|
+
* it when the mount is resolved. Throws when an ancestor wrap is missing
|
|
193
|
+
* (boundary alive at call time), matching {@link dispatchAcrossBoundary}. */
|
|
194
|
+
export const boundaryMappers = (registry, boundaryId) => {
|
|
195
|
+
const parts = splitBoundary(boundaryId);
|
|
196
|
+
const mappers = [];
|
|
197
|
+
for (let depth = parts.length; depth > 0; depth--) {
|
|
198
|
+
const ancestorBoundary = parts.slice(0, depth).join(BOUNDARY_SEPARATOR);
|
|
199
|
+
const descriptor = registry.wraps.get(ancestorBoundary);
|
|
200
|
+
if (descriptor === undefined) {
|
|
201
|
+
throw new Error(`Foldkit: boundaryMappers missing wrap for ancestor ` +
|
|
202
|
+
`"${ancestorBoundary}" of boundary "${boundaryId}" while snapshotting ` +
|
|
203
|
+
`an OnMount lift. The Submodel's wrap was absent from the registry ` +
|
|
204
|
+
`during render, which should not happen for a live boundary.`);
|
|
205
|
+
}
|
|
206
|
+
mappers.push(descriptor.toParentMessage);
|
|
207
|
+
}
|
|
208
|
+
return mappers;
|
|
209
|
+
};
|
|
186
210
|
export const getOrCreateBoundaryDispatch = (registry, outerDispatch, boundaryId) => {
|
|
187
211
|
if (boundaryId === ROOT_BOUNDARY) {
|
|
188
212
|
return outerDispatch;
|
|
@@ -10,7 +10,9 @@ declare const BRAND = "__childAttribute";
|
|
|
10
10
|
* `resolveUnmount` snapshots the boundary's wrapping chain at the time the
|
|
11
11
|
* group was published (child boundary alive) so `OnUnmount` can dispatch a
|
|
12
12
|
* root message from a destroy hook that fires after the boundary has been
|
|
13
|
-
* torn down.
|
|
13
|
+
* torn down. `boundaryMappers` snapshots the same chain as a pure list of
|
|
14
|
+
* `toParentMessage` lifts (innermost first) so `OnMount` can stamp it on the
|
|
15
|
+
* mount marker; the Scene test harness folds it to replay the lift.
|
|
14
16
|
*
|
|
15
17
|
* Created via {@link childAttributes}. Element constructors accept
|
|
16
18
|
* `ChildAttribute` alongside `Attribute<Message>` in their attribute
|
|
@@ -20,6 +22,7 @@ export type ChildAttribute = Readonly<{
|
|
|
20
22
|
readonly attribute: unknown;
|
|
21
23
|
readonly dispatch: DispatchSync;
|
|
22
24
|
readonly resolveUnmount: (message: unknown) => () => void;
|
|
25
|
+
readonly boundaryMappers: ReadonlyArray<(message: unknown) => unknown>;
|
|
23
26
|
}>;
|
|
24
27
|
export declare const isChildAttribute: (value: unknown) => value is ChildAttribute;
|
|
25
28
|
/** Captures the current boundary's dispatcher and wraps each attribute
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"childAttribute.d.ts","sourceRoot":"","sources":["../../src/html/childAttribute.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,YAAY,
|
|
1
|
+
{"version":3,"file":"childAttribute.d.ts","sourceRoot":"","sources":["../../src/html/childAttribute.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,YAAY,EAIlB,MAAM,uBAAuB,CAAA;AAE9B,QAAA,MAAM,KAAK,qBAAqB,CAAA;AAEhC;;;;;;;;;;;;;;;;cAgBc;AACd,MAAM,MAAM,cAAc,GAAG,QAAQ,CAAC;IACpC,QAAQ,CAAC,CAAC,KAAK,CAAC,EAAE,IAAI,CAAA;IACtB,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAA;IAC3B,QAAQ,CAAC,QAAQ,EAAE,YAAY,CAAA;IAC/B,QAAQ,CAAC,cAAc,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,MAAM,IAAI,CAAA;IACzD,QAAQ,CAAC,eAAe,EAAE,aAAa,CAAC,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,CAAA;CACvE,CAAC,CAAA;AAEF,eAAO,MAAM,gBAAgB,GAAI,OAAO,OAAO,KAAG,KAAK,IAAI,cACI,CAAA;AAE/D;;;;;;;;;;;;;;;;;;;;;;oCAsBoC;AACpC,eAAO,MAAM,eAAe,GAAI,SAAS,EACvC,YAAY,aAAa,CAAC,SAAS,CAAC,KACnC,aAAa,CAAC,cAAc,CAW9B,CAAA"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { requireDispatch, requireUnmountResolver, } from './runtimeSingleton.js';
|
|
1
|
+
import { requireBoundaryMappers, requireDispatch, requireUnmountResolver, } from './runtimeSingleton.js';
|
|
2
2
|
const BRAND = '__childAttribute';
|
|
3
3
|
export const isChildAttribute = (value) => typeof value === 'object' && value !== null && BRAND in value;
|
|
4
4
|
/** Captures the current boundary's dispatcher and wraps each attribute
|
|
@@ -27,10 +27,12 @@ export const isChildAttribute = (value) => typeof value === 'object' && value !=
|
|
|
27
27
|
export const childAttributes = (attributes) => {
|
|
28
28
|
const dispatch = requireDispatch();
|
|
29
29
|
const resolveUnmount = requireUnmountResolver();
|
|
30
|
+
const boundaryMappers = requireBoundaryMappers();
|
|
30
31
|
return attributes.map(attribute => ({
|
|
31
32
|
[BRAND]: true,
|
|
32
33
|
attribute,
|
|
33
34
|
dispatch,
|
|
34
35
|
resolveUnmount,
|
|
36
|
+
boundaryMappers,
|
|
35
37
|
}));
|
|
36
38
|
};
|
package/dist/html/index.d.ts
CHANGED
|
@@ -93,11 +93,16 @@ export declare const __endReplayRender: () => void;
|
|
|
93
93
|
* observable by VNode walkers (Scene tests). */
|
|
94
94
|
export declare const FOLDKIT_MOUNT_KEY: "foldkitMount";
|
|
95
95
|
/** Marker stamped on `VNodeData[FOLDKIT_MOUNT_KEY]` for any element with an
|
|
96
|
-
* `OnMount` attribute. Carries the Mount Definition's name so test
|
|
97
|
-
* introspection can identify pending mounts.
|
|
96
|
+
* `OnMount` attribute. Carries the Mount Definition's name (and args) so test
|
|
97
|
+
* introspection can identify pending mounts. When the mount lives inside a
|
|
98
|
+
* Submodel boundary, it also carries that boundary's `toParentMessage` chain
|
|
99
|
+
* (innermost first), snapshotted at render time, so `Scene.Mount.resolve` can
|
|
100
|
+
* replay the lift the result travels through in production. The chain is read
|
|
101
|
+
* only by the Scene harness; production dispatch lifts via `ctx.dispatch`. */
|
|
98
102
|
export type FoldkitMountMarker = Readonly<{
|
|
99
103
|
name: string;
|
|
100
104
|
args?: Record<string, unknown>;
|
|
105
|
+
messageMappers?: ReadonlyArray<(message: unknown) => unknown>;
|
|
101
106
|
}>;
|
|
102
107
|
/** Union of all HTML, SVG, and MathML attributes a virtual DOM element can carry.
|
|
103
108
|
*
|
|
@@ -206,6 +211,12 @@ export type Attribute<Message> = Data.TaggedEnum<{
|
|
|
206
211
|
OnKeyDownPreventDefault: {
|
|
207
212
|
readonly f: (key: string, modifiers: KeyboardModifiers) => Option.Option<Message>;
|
|
208
213
|
};
|
|
214
|
+
OnKeyDownFocus: {
|
|
215
|
+
readonly f: (key: string, modifiers: KeyboardModifiers) => Option.Option<Readonly<{
|
|
216
|
+
focusSelector: string;
|
|
217
|
+
message: Message;
|
|
218
|
+
}>>;
|
|
219
|
+
};
|
|
209
220
|
OnKeyUp: {
|
|
210
221
|
readonly f: (key: string, modifiers: KeyboardModifiers) => Message;
|
|
211
222
|
};
|
|
@@ -1063,6 +1074,7 @@ declare const buildHtmlFactory: <Message>() => {
|
|
|
1063
1074
|
readonly attribute: unknown;
|
|
1064
1075
|
readonly dispatch: DispatchSync;
|
|
1065
1076
|
readonly resolveUnmount: (message: unknown) => () => void;
|
|
1077
|
+
readonly boundaryMappers: ReadonlyArray<(message: unknown) => unknown>;
|
|
1066
1078
|
readonly __childAttribute: true;
|
|
1067
1079
|
}> | {
|
|
1068
1080
|
readonly _tag: "Type";
|
|
@@ -1189,6 +1201,17 @@ declare const buildHtmlFactory: <Message>() => {
|
|
|
1189
1201
|
altKey: boolean;
|
|
1190
1202
|
metaKey: boolean;
|
|
1191
1203
|
}>) => Option.Option<Message>;
|
|
1204
|
+
} | {
|
|
1205
|
+
readonly _tag: "OnKeyDownFocus";
|
|
1206
|
+
readonly f: (key: string, modifiers: Readonly<{
|
|
1207
|
+
shiftKey: boolean;
|
|
1208
|
+
ctrlKey: boolean;
|
|
1209
|
+
altKey: boolean;
|
|
1210
|
+
metaKey: boolean;
|
|
1211
|
+
}>) => Option.Option<Readonly<{
|
|
1212
|
+
focusSelector: string;
|
|
1213
|
+
message: Message;
|
|
1214
|
+
}>>;
|
|
1192
1215
|
} | {
|
|
1193
1216
|
readonly _tag: "OnKeyUp";
|
|
1194
1217
|
readonly f: (key: string, modifiers: Readonly<{
|
|
@@ -2161,6 +2184,26 @@ declare const buildHtmlFactory: <Message>() => {
|
|
|
2161
2184
|
metaKey: boolean;
|
|
2162
2185
|
}>) => Message;
|
|
2163
2186
|
};
|
|
2187
|
+
OnKeyDownFocus: (f: (key: string, modifiers: Readonly<{
|
|
2188
|
+
shiftKey: boolean;
|
|
2189
|
+
ctrlKey: boolean;
|
|
2190
|
+
altKey: boolean;
|
|
2191
|
+
metaKey: boolean;
|
|
2192
|
+
}>) => Option.Option<Readonly<{
|
|
2193
|
+
focusSelector: string;
|
|
2194
|
+
message: Message;
|
|
2195
|
+
}>>) => {
|
|
2196
|
+
readonly _tag: "OnKeyDownFocus";
|
|
2197
|
+
readonly f: (key: string, modifiers: Readonly<{
|
|
2198
|
+
shiftKey: boolean;
|
|
2199
|
+
ctrlKey: boolean;
|
|
2200
|
+
altKey: boolean;
|
|
2201
|
+
metaKey: boolean;
|
|
2202
|
+
}>) => Option.Option<Readonly<{
|
|
2203
|
+
focusSelector: string;
|
|
2204
|
+
message: Message;
|
|
2205
|
+
}>>;
|
|
2206
|
+
};
|
|
2164
2207
|
OnKeyDownPreventDefault: (f: (key: string, modifiers: Readonly<{
|
|
2165
2208
|
shiftKey: boolean;
|
|
2166
2209
|
ctrlKey: boolean;
|