foldkit 0.157.0 → 0.158.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/dist/devTools/host.d.ts +2 -2
- package/dist/devTools/host.d.ts.map +1 -1
- package/dist/devTools/host.js +1 -1
- package/dist/experimental/machine/machine.d.ts +278 -84
- package/dist/experimental/machine/machine.d.ts.map +1 -1
- package/dist/experimental/machine/machine.js +362 -212
- package/dist/runtime/browserListeners.d.ts +7 -1
- package/dist/runtime/browserListeners.d.ts.map +1 -1
- package/dist/runtime/browserScheduler.d.ts +3 -0
- package/dist/runtime/browserScheduler.d.ts.map +1 -0
- package/dist/runtime/browserScheduler.js +22 -0
- package/dist/runtime/crashUI.d.ts +36 -5
- package/dist/runtime/crashUI.d.ts.map +1 -1
- package/dist/runtime/crashUI.js +55 -2
- package/dist/runtime/devToolsConfig.d.ts +85 -0
- package/dist/runtime/devToolsConfig.d.ts.map +1 -0
- package/dist/runtime/devToolsConfig.js +49 -0
- package/dist/runtime/devToolsIntegration.d.ts +56 -0
- package/dist/runtime/devToolsIntegration.d.ts.map +1 -0
- package/dist/runtime/devToolsIntegration.js +186 -0
- package/dist/runtime/dispatch.d.ts +10 -0
- package/dist/runtime/dispatch.d.ts.map +1 -0
- package/dist/runtime/dispatch.js +4 -0
- package/dist/runtime/documentMetadata.d.ts +3 -0
- package/dist/runtime/documentMetadata.d.ts.map +1 -0
- package/dist/runtime/documentMetadata.js +148 -0
- package/dist/runtime/duplicateIdScanner.d.ts +16 -0
- package/dist/runtime/duplicateIdScanner.d.ts.map +1 -0
- package/dist/runtime/duplicateIdScanner.js +70 -0
- package/dist/runtime/hmrModelBridge.d.ts +4 -0
- package/dist/runtime/hmrModelBridge.d.ts.map +1 -0
- package/dist/runtime/hmrModelBridge.js +43 -0
- package/dist/runtime/hostConnector.d.ts +61 -0
- package/dist/runtime/hostConnector.d.ts.map +1 -0
- package/dist/runtime/hostConnector.js +141 -0
- package/dist/runtime/hydrationHandoff.d.ts +44 -0
- package/dist/runtime/hydrationHandoff.d.ts.map +1 -0
- package/dist/runtime/hydrationHandoff.js +395 -0
- package/dist/runtime/index.d.ts +16 -1
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/index.js +5 -1
- package/dist/runtime/makeApplication.d.ts +71 -0
- package/dist/runtime/makeApplication.d.ts.map +1 -0
- package/dist/runtime/makeApplication.js +98 -0
- package/dist/runtime/makeElement.d.ts +69 -0
- package/dist/runtime/makeElement.d.ts.map +1 -0
- package/dist/runtime/makeElement.js +81 -0
- package/dist/runtime/managedResourceFibers.d.ts +18 -0
- package/dist/runtime/managedResourceFibers.d.ts.map +1 -0
- package/dist/runtime/managedResourceFibers.js +47 -0
- package/dist/runtime/messageQueue.d.ts +27 -0
- package/dist/runtime/messageQueue.d.ts.map +1 -0
- package/dist/runtime/messageQueue.js +133 -0
- package/dist/runtime/public.d.ts +2 -3
- package/dist/runtime/public.d.ts.map +1 -1
- package/dist/runtime/public.js +1 -1
- package/dist/runtime/renderer.d.ts +69 -0
- package/dist/runtime/renderer.d.ts.map +1 -0
- package/dist/runtime/renderer.js +512 -0
- package/dist/runtime/resourceProvider.d.ts +33 -0
- package/dist/runtime/resourceProvider.d.ts.map +1 -0
- package/dist/runtime/resourceProvider.js +98 -0
- package/dist/runtime/runtime.d.ts +133 -412
- package/dist/runtime/runtime.d.ts.map +1 -1
- package/dist/runtime/runtime.js +118 -2184
- package/dist/runtime/runtimeStatus.d.ts +15 -0
- package/dist/runtime/runtimeStatus.d.ts.map +1 -0
- package/dist/runtime/runtimeStatus.js +6 -0
- package/dist/runtime/slowPhase.d.ts +82 -0
- package/dist/runtime/slowPhase.d.ts.map +1 -0
- package/dist/runtime/slowPhase.js +83 -0
- package/dist/runtime/start.d.ts +96 -0
- package/dist/runtime/start.d.ts.map +1 -0
- package/dist/runtime/start.js +99 -0
- package/dist/runtime/subscriptionFibers.d.ts +21 -0
- package/dist/runtime/subscriptionFibers.d.ts.map +1 -0
- package/dist/runtime/subscriptionFibers.js +47 -0
- package/dist/runtime/visibility.d.ts +8 -0
- package/dist/runtime/visibility.d.ts.map +1 -0
- package/dist/runtime/visibility.js +6 -0
- package/dist/test/scene.js +1 -1
- package/package.json +1 -1
|
@@ -1,5 +1,36 @@
|
|
|
1
|
-
import { Array, Match, Option, Predicate, Record, pipe } from 'effect';
|
|
2
|
-
const
|
|
1
|
+
import { Array, Function, HashMap, HashSet, Match, Option, Predicate, Record, pipe, } from 'effect';
|
|
2
|
+
const ForStatesTypeId = Symbol('foldkit/Machine/ForStates');
|
|
3
|
+
/** Selects source state tags for a shared transition map. The `on` handler's
|
|
4
|
+
* `state` is narrowed to the union of the selected variants, and its `message`
|
|
5
|
+
* is narrowed by the map key.
|
|
6
|
+
*
|
|
7
|
+
* Add the resulting fragment to a Machine definition's `shared` array. Shared
|
|
8
|
+
* transitions are defaults: a state-local transition for the same Message
|
|
9
|
+
* replaces the shared transition. Defining the same state/Message pair in two
|
|
10
|
+
* shared fragments throws when the Machine is defined.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* shared: [
|
|
15
|
+
* forStates(['Editing', 'Reviewing']).on({
|
|
16
|
+
* ClickedCancel: to('Cancelled', ({ state }) => ({
|
|
17
|
+
* model: CheckoutState.Cancelled({ draftId: state.draftId }),
|
|
18
|
+
* })),
|
|
19
|
+
* }),
|
|
20
|
+
* ]
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* @experimental Ships from `foldkit/experimental/machine`; expect breaking changes while the API settles.
|
|
24
|
+
*/
|
|
25
|
+
export const forStates = sourceTags => ({
|
|
26
|
+
on: transitions => ({
|
|
27
|
+
_tag: 'ForStates',
|
|
28
|
+
[ForStatesTypeId]: ForStatesTypeId,
|
|
29
|
+
sourceTags,
|
|
30
|
+
on: transitions,
|
|
31
|
+
}),
|
|
32
|
+
});
|
|
33
|
+
const makeEdge = (target, handler) => {
|
|
3
34
|
const narrowGuardValue = (input) => {
|
|
4
35
|
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */
|
|
5
36
|
return input;
|
|
@@ -7,58 +38,64 @@ const makeEdge = (target, build, commands) => {
|
|
|
7
38
|
return {
|
|
8
39
|
_tag: 'Edge',
|
|
9
40
|
target,
|
|
10
|
-
|
|
11
|
-
maybeCommands: commands === undefined
|
|
12
|
-
? Option.none()
|
|
13
|
-
: Option.some(input => commands(narrowGuardValue(input))),
|
|
41
|
+
handler: input => handler(narrowGuardValue(input)),
|
|
14
42
|
};
|
|
15
43
|
};
|
|
16
44
|
/**
|
|
17
45
|
* Declares a transition Edge to the state variant named by `target`. The
|
|
18
|
-
* `
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* effects.
|
|
46
|
+
* `handler` receives an {@link EdgeInput} whose state and Message are narrowed
|
|
47
|
+
* to the variants the Edge sits under in a state-local or shared transition
|
|
48
|
+
* map, and returns an `Update.Return` record whose `model` is the target
|
|
49
|
+
* variant and whose optional `commands` are transition-time effects. When the
|
|
50
|
+
* Machine declares a context, the input also contains that context for the
|
|
51
|
+
* current transition.
|
|
22
52
|
*
|
|
23
|
-
* The `commands`
|
|
24
|
-
* requirements they carry flow into the Machine's `R` through
|
|
53
|
+
* The handler's `commands` field may contain Commands whose Effects need
|
|
54
|
+
* services. The requirements they carry flow into the Machine's `R` through
|
|
55
|
+
* {@link define}.
|
|
25
56
|
*
|
|
26
|
-
* Only meaningful inside a
|
|
27
|
-
* types flow in from
|
|
57
|
+
* Only meaningful inside a Machine definition's state-local or shared
|
|
58
|
+
* transition map: the source and trigger types flow in from that position
|
|
59
|
+
* contextually.
|
|
28
60
|
*
|
|
29
61
|
* @experimental Ships from `foldkit/experimental/machine`; expect breaking changes while the API settles.
|
|
30
62
|
*/
|
|
31
|
-
export const to = (target,
|
|
63
|
+
export const to = (target, handler) => makeEdge(target, handler);
|
|
32
64
|
/**
|
|
33
65
|
* Guards an Edge. Guard lists run in order, and the first guard that passes
|
|
34
66
|
* fires its Edge. A guard either resolves the state and Message to an
|
|
35
67
|
* `Option` (returning `Option.some` passes, and the wrapped value flows into
|
|
36
|
-
* the Edge's
|
|
68
|
+
* the Edge's handler as `guardValue`) or returns a
|
|
37
69
|
* plain boolean when there is nothing to extract (returning `true` passes,
|
|
38
70
|
* and `guardValue` is `void`).
|
|
39
71
|
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
72
|
+
* When the Machine declares a context, the guard receives it as a third
|
|
73
|
+
* parameter and its Edge handler receives it in {@link EdgeInput}. The state,
|
|
74
|
+
* Message, and context parameters are `NoInfer` so they resolve from the guard
|
|
75
|
+
* list's transition-map position alone.
|
|
42
76
|
*
|
|
43
77
|
* @experimental Ships from `foldkit/experimental/machine`; expect breaking changes while the API settles.
|
|
44
78
|
*/
|
|
45
|
-
export const when = (guard, target,
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const result = guard(state, message);
|
|
79
|
+
export const when = (guard, target, handler) => {
|
|
80
|
+
const normalizeGuard = (state, message, ...context) => {
|
|
81
|
+
const result = guard(state, message, ...context);
|
|
49
82
|
if (Predicate.isBoolean(result)) {
|
|
50
83
|
return result ? Option.some(undefined) : Option.none();
|
|
51
84
|
}
|
|
52
85
|
else {
|
|
53
86
|
return result;
|
|
54
87
|
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
|
|
88
|
+
};
|
|
89
|
+
return {
|
|
90
|
+
_tag: 'When',
|
|
91
|
+
guard: normalizeGuard,
|
|
92
|
+
edge: makeEdge(target, handler),
|
|
93
|
+
};
|
|
94
|
+
};
|
|
58
95
|
/**
|
|
59
|
-
* The unconditional fallback at the end of a guard list. Its
|
|
60
|
-
* Commands whose Effects need services, threading their requirements
|
|
61
|
-
* Machine's `R` the same way {@link to} and {@link when} do.
|
|
96
|
+
* The unconditional fallback at the end of a guard list. Its Edge handler may
|
|
97
|
+
* return Commands whose Effects need services, threading their requirements
|
|
98
|
+
* into the Machine's `R` the same way {@link to} and {@link when} do.
|
|
62
99
|
*
|
|
63
100
|
* @experimental Ships from `foldkit/experimental/machine`; expect breaking changes while the API settles.
|
|
64
101
|
*/
|
|
@@ -66,209 +103,322 @@ export const otherwise = (edge) => ({
|
|
|
66
103
|
_tag: 'Otherwise',
|
|
67
104
|
edge,
|
|
68
105
|
});
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
106
|
+
/**
|
|
107
|
+
* Declares that a Message is intentionally ignored when every preceding
|
|
108
|
+
* {@link when} guard declines. Evaluation stops at this fallback, and
|
|
109
|
+
* {@link Machine.step} reports `ExplicitlyIgnored`. Edges listed after it are
|
|
110
|
+
* reported by {@link Machine.deadTransitions} as `ShadowedByIgnore`.
|
|
111
|
+
*
|
|
112
|
+
* @experimental Ships from `foldkit/experimental/machine`; expect breaking changes while the API settles.
|
|
113
|
+
*/
|
|
114
|
+
export const ignore = () => ({ _tag: 'Ignore' });
|
|
115
|
+
const transitionFoldedMachine = (config, model, state, message) => {
|
|
116
|
+
const readContext = config.context;
|
|
117
|
+
if (readContext !== undefined) {
|
|
118
|
+
return config.machine.transition(state, message, readContext(model));
|
|
77
119
|
}
|
|
78
120
|
else {
|
|
79
|
-
return
|
|
121
|
+
return config.machine.transition(state, message);
|
|
80
122
|
}
|
|
81
123
|
};
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
* Compiles a declarative transition table into a {@link Machine}.
|
|
124
|
+
/** Folds a Machine state field into an enclosing Model. Any transition-time
|
|
125
|
+
* Commands pass through unchanged.
|
|
85
126
|
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
* inside every Edge from its table position: a single-call form checks the
|
|
90
|
-
* table while the type parameters are still being inferred, and the
|
|
91
|
-
* narrowing collapses.
|
|
127
|
+
* When `read` returns `None`, the fold returns the original Model without
|
|
128
|
+
* running a transition. For a contextual Machine, `context` is read only when
|
|
129
|
+
* the Machine state is present and is supplied to that transition.
|
|
92
130
|
*
|
|
93
|
-
* The Machine is not a runtime: `transition` has the same shape as a Foldkit
|
|
94
|
-
* `update` branch and returns `[nextState, commands]`, so the machine state
|
|
95
|
-
* lives in the Model and the Foldkit runtime never learns the Machine
|
|
96
|
-
* exists. Messages that match no Edge leave the state unchanged; use `step`
|
|
97
|
-
* when the `Ignored` outcome should be observable.
|
|
98
|
-
*
|
|
99
|
-
* Because every Edge names a literal target tag, the Edge set is plain data:
|
|
100
|
-
* `reachableFrom`, `unreachableStates`, `deadTransitions`, and `toMermaid`
|
|
101
|
-
* all read it directly.
|
|
102
|
-
*
|
|
103
|
-
* The Machine's requirements `R` are the services its edge Commands need, and
|
|
104
|
-
* flow into `transition` and `step`. `R` defaults to `never`. When every edge
|
|
105
|
-
* Command shares one service, `R` is inferred from the table. When edges need
|
|
106
|
-
* distinct services `R` cannot be inferred to their union, so supply it on the
|
|
107
|
-
* second call: `define(schemas)<UploadsClient | SaveClient>({ ... })`.
|
|
108
|
-
*
|
|
109
|
-
* @example An edge Command that needs a service
|
|
110
131
|
* ```ts
|
|
111
|
-
* const
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
* },
|
|
118
|
-
* },
|
|
119
|
-
* },
|
|
132
|
+
* const foldUpload = Machine.fold({
|
|
133
|
+
* machine: uploadMachine,
|
|
134
|
+
* read: (model: Model) => Option.some(model.upload),
|
|
135
|
+
* write: (model, nextUpload) =>
|
|
136
|
+
* evo(model, { upload: () => nextUpload }),
|
|
137
|
+
* context: model => model.uploadQueues,
|
|
120
138
|
* })
|
|
121
|
-
*
|
|
139
|
+
*
|
|
140
|
+
* // Data-first in update
|
|
141
|
+
* foldUpload(model, message)
|
|
142
|
+
*
|
|
143
|
+
* // Data-last in a composed update
|
|
144
|
+
* Update.combine(model, [foldUpload(message), recordUploadAttempt])
|
|
122
145
|
* ```
|
|
123
146
|
*
|
|
124
147
|
* @experimental Ships from `foldkit/experimental/machine`; expect breaking changes while the API settles.
|
|
125
148
|
*/
|
|
126
|
-
export const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
149
|
+
export const fold = (config) => Function.dual(2, (model, message) => {
|
|
150
|
+
const maybeState = config.read(model);
|
|
151
|
+
if (Option.isNone(maybeState)) {
|
|
152
|
+
return { model };
|
|
153
|
+
}
|
|
154
|
+
const transition = transitionFoldedMachine(config, model, maybeState.value, message);
|
|
155
|
+
const nextModel = config.write(model, transition.model);
|
|
156
|
+
if (transition.commands === undefined) {
|
|
157
|
+
return { model: nextModel };
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
return { model: nextModel, commands: transition.commands };
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
const emptyLooseRecord = () => {
|
|
131
164
|
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
165
|
+
return {};
|
|
166
|
+
};
|
|
167
|
+
const addSharedTransition = (explicitStates, expanded, sourceTag, messageTag, transition) => {
|
|
168
|
+
const sharedMessageTags = pipe(HashMap.get(expanded.messageTagsByState, sourceTag), Option.getOrElse(() => HashSet.empty()));
|
|
169
|
+
if (HashSet.has(sharedMessageTags, messageTag)) {
|
|
170
|
+
throw new Error(`Machine.define: shared transitions overlap for state "${sourceTag}" and Message "${messageTag}"`);
|
|
171
|
+
}
|
|
172
|
+
const nextMessageTagsByState = pipe(expanded.messageTagsByState, HashMap.set(sourceTag, HashSet.add(sharedMessageTags, messageTag)));
|
|
173
|
+
const isStateLocal = pipe(Record.get(explicitStates, sourceTag), Option.exists(stateEntry => Record.has(stateEntry.on, messageTag)));
|
|
174
|
+
if (isStateLocal) {
|
|
175
|
+
return {
|
|
176
|
+
states: expanded.states,
|
|
177
|
+
messageTagsByState: nextMessageTagsByState,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
const stateEntry = pipe(Record.get(expanded.states, sourceTag), Option.getOrElse(() => ({
|
|
181
|
+
on: emptyLooseRecord(),
|
|
182
|
+
})));
|
|
183
|
+
return {
|
|
184
|
+
states: pipe(expanded.states, Record.set(sourceTag, {
|
|
185
|
+
on: Record.set(stateEntry.on, messageTag, transition),
|
|
186
|
+
})),
|
|
187
|
+
messageTagsByState: nextMessageTagsByState,
|
|
188
|
+
};
|
|
189
|
+
};
|
|
190
|
+
const expandSharedTransitions = (explicitStates, shared) => pipe(shared, Array.reduce({
|
|
191
|
+
states: explicitStates,
|
|
192
|
+
messageTagsByState: HashMap.empty(),
|
|
193
|
+
}, (expanded, fragment) => pipe(Array.dedupe(fragment.sourceTags), Array.reduce(expanded, (nextExpanded, sourceTag) => pipe(Record.toEntries(fragment.on), Array.reduce(nextExpanded, (nextStateExpanded, [messageTag, transition]) => addSharedTransition(explicitStates, nextStateExpanded, sourceTag, messageTag, transition)))))), result => result.states);
|
|
194
|
+
const isGuardList = (edgeOrGuardedEdges) => Array.isArray(edgeOrGuardedEdges);
|
|
195
|
+
const extractLiteralTag = (tagField) => {
|
|
196
|
+
if (Predicate.hasProperty(tagField, 'literal') &&
|
|
197
|
+
Predicate.isString(tagField.literal)) {
|
|
198
|
+
return Option.some(tagField.literal);
|
|
199
|
+
}
|
|
200
|
+
else if (Predicate.hasProperty(tagField, 'schema')) {
|
|
201
|
+
return extractLiteralTag(tagField.schema);
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
return Option.none();
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
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)));
|
|
208
|
+
const flattenUnionMembers = (members) => Array.flatMap(members, member => {
|
|
209
|
+
if (Predicate.hasProperty(member, 'members') &&
|
|
210
|
+
Array.isArray(member.members)) {
|
|
211
|
+
return flattenUnionMembers(member.members);
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
return [member];
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
export function define(schemas) {
|
|
218
|
+
return defineImplementation(schemas);
|
|
219
|
+
}
|
|
220
|
+
function defineImplementation(schemas) {
|
|
221
|
+
return (definition) => {
|
|
222
|
+
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */
|
|
223
|
+
const explicitStates = definition.states;
|
|
224
|
+
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */
|
|
225
|
+
const shared = (definition.shared ?? []);
|
|
226
|
+
const looseStates = expandSharedTransitions(explicitStates, shared);
|
|
227
|
+
const hasContext = Predicate.hasProperty(schemas, 'context');
|
|
228
|
+
const initialTag = definition.initial._tag;
|
|
229
|
+
const stateTags = pipe(schemas.state.members, flattenUnionMembers, 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'))),
|
|
230
|
+
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */
|
|
231
|
+
tags => tags);
|
|
232
|
+
const makeEdgeSummary = (from, messageTag, target, guard) => ({ from, messageTag, target, guard });
|
|
233
|
+
const summarizeEntry = (from, messageTag, edgeOrGuardedEdges) => isGuardList(edgeOrGuardedEdges)
|
|
234
|
+
? Array.flatMap(edgeOrGuardedEdges, (guardedEdge, position) => Match.value(guardedEdge).pipe(Match.withReturnType(), Match.tagsExhaustive({
|
|
235
|
+
When: guardedWhen => [
|
|
236
|
+
makeEdgeSummary(from, messageTag, guardedWhen.edge.target, {
|
|
237
|
+
_tag: 'When',
|
|
238
|
+
position,
|
|
239
|
+
}),
|
|
240
|
+
],
|
|
241
|
+
Otherwise: guardedOtherwise => [
|
|
242
|
+
makeEdgeSummary(from, messageTag, guardedOtherwise.edge.target, {
|
|
243
|
+
_tag: 'Otherwise',
|
|
244
|
+
position,
|
|
245
|
+
}),
|
|
246
|
+
],
|
|
247
|
+
Ignore: () => [],
|
|
248
|
+
})))
|
|
249
|
+
: [
|
|
250
|
+
makeEdgeSummary(from, messageTag, edgeOrGuardedEdges.target, {
|
|
251
|
+
_tag: 'Unguarded',
|
|
252
|
+
}),
|
|
253
|
+
];
|
|
254
|
+
const edges = pipe(Record.toEntries(looseStates), Array.flatMap(([sourceTag, stateEntry]) => pipe(Record.toEntries(stateEntry.on), Array.flatMap(([messageTag, edgeOrGuardedEdges]) => summarizeEntry(sourceTag, messageTag, edgeOrGuardedEdges)))));
|
|
255
|
+
const messageAlphabet = pipe(Record.values(looseStates), Array.flatMap(stateEntry => Record.keys(stateEntry.on)), HashSet.fromIterable);
|
|
256
|
+
const runGuard = (guardedEdge, state, message, context) => {
|
|
257
|
+
if (hasContext) {
|
|
258
|
+
return guardedEdge.guard(state, message, context);
|
|
164
259
|
}
|
|
165
260
|
else {
|
|
166
|
-
return
|
|
167
|
-
|
|
261
|
+
return guardedEdge.guard(state, message);
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
const selectFromGuardList = (guardedEdges, state, message, context) => Array.matchLeft(guardedEdges, {
|
|
265
|
+
onEmpty: () => ({
|
|
266
|
+
_tag: 'IgnoredEdge',
|
|
267
|
+
reason: 'GuardsFellThrough',
|
|
268
|
+
}),
|
|
269
|
+
onNonEmpty: (guardedEdge, rest) => Match.value(guardedEdge).pipe(Match.withReturnType(), Match.tagsExhaustive({
|
|
270
|
+
When: guardedWhen => {
|
|
271
|
+
const maybeGuardValue = runGuard(guardedWhen, state, message, context);
|
|
272
|
+
if (Option.isSome(maybeGuardValue)) {
|
|
273
|
+
return {
|
|
274
|
+
_tag: 'SelectedEdge',
|
|
275
|
+
edge: guardedWhen.edge,
|
|
276
|
+
guardValue: maybeGuardValue.value,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
return selectFromGuardList(rest, state, message, context);
|
|
281
|
+
}
|
|
282
|
+
},
|
|
283
|
+
Otherwise: guardedOtherwise => ({
|
|
284
|
+
_tag: 'SelectedEdge',
|
|
285
|
+
edge: guardedOtherwise.edge,
|
|
168
286
|
guardValue: undefined,
|
|
169
|
-
})
|
|
287
|
+
}),
|
|
288
|
+
Ignore: () => ({
|
|
289
|
+
_tag: 'IgnoredEdge',
|
|
290
|
+
reason: 'ExplicitlyIgnored',
|
|
291
|
+
}),
|
|
292
|
+
})),
|
|
293
|
+
});
|
|
294
|
+
const chooseEdge = (edgeOrGuardedEdges, state, message, context) => isGuardList(edgeOrGuardedEdges)
|
|
295
|
+
? selectFromGuardList(edgeOrGuardedEdges, state, message, context)
|
|
296
|
+
: {
|
|
297
|
+
_tag: 'SelectedEdge',
|
|
298
|
+
edge: edgeOrGuardedEdges,
|
|
299
|
+
guardValue: undefined,
|
|
300
|
+
};
|
|
301
|
+
const makeRuntimeEdgeInput = (state, message, guardValue, context) => {
|
|
302
|
+
if (hasContext) {
|
|
303
|
+
return { state, message, guardValue, context };
|
|
170
304
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
305
|
+
else {
|
|
306
|
+
return { state, message, guardValue };
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
const makeTransitioned = (state, message, selectedEdge, context) => {
|
|
310
|
+
const edgeInput = makeRuntimeEdgeInput(state, message, selectedEdge.guardValue, context);
|
|
311
|
+
const edgeUpdate = selectedEdge.edge.handler(edgeInput);
|
|
312
|
+
return {
|
|
313
|
+
_tag: 'Transitioned',
|
|
314
|
+
from: state._tag,
|
|
315
|
+
target: selectedEdge.edge.target,
|
|
316
|
+
messageTag: message._tag,
|
|
317
|
+
state: edgeUpdate.model,
|
|
318
|
+
commands: edgeUpdate.commands ?? [],
|
|
319
|
+
};
|
|
320
|
+
};
|
|
321
|
+
const makeIgnored = (state, message, reason) => ({
|
|
322
|
+
_tag: 'Ignored',
|
|
323
|
+
stateTag: state._tag,
|
|
324
|
+
messageTag: message._tag,
|
|
182
325
|
state,
|
|
183
|
-
|
|
184
|
-
guardValue: selectedEdge.guardValue,
|
|
185
|
-
}),
|
|
186
|
-
commands: Option.match(selectedEdge.edge.maybeCommands, {
|
|
187
|
-
onNone: () => [],
|
|
188
|
-
onSome: buildCommands => buildCommands({
|
|
189
|
-
state,
|
|
190
|
-
message,
|
|
191
|
-
guardValue: selectedEdge.guardValue,
|
|
192
|
-
}),
|
|
193
|
-
}),
|
|
194
|
-
});
|
|
195
|
-
const makeIgnored = (state, message) => ({
|
|
196
|
-
_tag: 'Ignored',
|
|
197
|
-
stateTag: state._tag,
|
|
198
|
-
messageTag: message._tag,
|
|
199
|
-
state,
|
|
200
|
-
});
|
|
201
|
-
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({
|
|
202
|
-
onNone: () => makeIgnored(state, message),
|
|
203
|
-
onSome: selectedEdge => makeTransitioned(state, message, selectedEdge),
|
|
204
|
-
}));
|
|
205
|
-
const transition = (state, message) => {
|
|
206
|
-
const result = step(state, message);
|
|
207
|
-
if (result._tag === 'Transitioned') {
|
|
208
|
-
return { model: result.state, commands: result.commands };
|
|
209
|
-
}
|
|
210
|
-
else {
|
|
211
|
-
return { model: result.state };
|
|
212
|
-
}
|
|
213
|
-
};
|
|
214
|
-
const targetsFrom = (tag) => pipe(edges, Array.filter(edgeSummary => edgeSummary.from === tag), Array.map(edgeSummary => edgeSummary.target));
|
|
215
|
-
const reachableFromRoots = (roots) => {
|
|
216
|
-
const visit = (frontier, visited) => Array.matchLeft(frontier, {
|
|
217
|
-
onEmpty: () => visited,
|
|
218
|
-
onNonEmpty: (head, tail) => visited.has(head)
|
|
219
|
-
? visit(tail, visited)
|
|
220
|
-
: visit([...tail, ...targetsFrom(head)], new Set([...visited, head])),
|
|
326
|
+
reason,
|
|
221
327
|
});
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
328
|
+
const step = (state, message, ...contextArguments) => {
|
|
329
|
+
const context = pipe(contextArguments, Array.head, Option.getOrUndefined);
|
|
330
|
+
return pipe(Record.get(looseStates, state._tag), Option.flatMap(stateEntry => Record.get(stateEntry.on, message._tag)), Option.match({
|
|
331
|
+
onNone: () => makeIgnored(state, message, HashSet.has(messageAlphabet, message._tag)
|
|
332
|
+
? 'NotApplicable'
|
|
333
|
+
: 'OutOfAlphabet'),
|
|
334
|
+
onSome: edgeOrGuardedEdges => {
|
|
335
|
+
const selection = chooseEdge(edgeOrGuardedEdges, state, message, context);
|
|
336
|
+
return Match.value(selection).pipe(Match.withReturnType(), Match.tagsExhaustive({
|
|
337
|
+
SelectedEdge: selectedEdge => makeTransitioned(state, message, selectedEdge, context),
|
|
338
|
+
IgnoredEdge: ignoredEdge => makeIgnored(state, message, ignoredEdge.reason),
|
|
339
|
+
}));
|
|
340
|
+
},
|
|
341
|
+
}));
|
|
342
|
+
};
|
|
343
|
+
const transition = (state, message, ...contextArguments) => {
|
|
344
|
+
const result = step(state, message, ...contextArguments);
|
|
345
|
+
return Match.value(result).pipe(Match.withReturnType(), Match.tagsExhaustive({
|
|
346
|
+
Transitioned: transitioned => ({
|
|
347
|
+
model: transitioned.state,
|
|
348
|
+
commands: transitioned.commands,
|
|
349
|
+
}),
|
|
350
|
+
Ignored: ignored => ({ model: ignored.state }),
|
|
351
|
+
}));
|
|
352
|
+
};
|
|
353
|
+
const makeDeadTransition = (edge, reason) => ({ edge, reason });
|
|
354
|
+
const isPositionedEdgeGuard = (guard) => guard._tag !== 'Unguarded';
|
|
355
|
+
const isGuardListFallback = (guardedEdge) => guardedEdge._tag !== 'When';
|
|
356
|
+
const fallbackToShadowingReason = (fallback) => Match.value(fallback).pipe(Match.withReturnType(), Match.tagsExhaustive({
|
|
357
|
+
Otherwise: () => 'ShadowedByOtherwise',
|
|
358
|
+
Ignore: () => 'ShadowedByIgnore',
|
|
359
|
+
}));
|
|
360
|
+
const maybeGuardPosition = (edgeSummary) => pipe(edgeSummary.guard, Option.liftPredicate(isPositionedEdgeGuard), Option.map(guard => guard.position));
|
|
361
|
+
const groupEdgesByMessageTag = (edgesFromState) => pipe(edgesFromState, Array.groupBy(edgeSummary => edgeSummary.messageTag), Record.values);
|
|
362
|
+
const edgesAfterGuardPosition = (edgeGroup, guardPosition) => Array.filter(edgeGroup, edgeSummary => pipe(maybeGuardPosition(edgeSummary), Option.exists(position => position > guardPosition)));
|
|
363
|
+
const guardListAt = (from, messageTag) => pipe(Record.get(looseStates, from), Option.flatMap(stateEntry => Record.get(stateEntry.on, messageTag)), Option.flatMap(Option.liftPredicate(isGuardList)));
|
|
364
|
+
const firstGuardListFallback = (guardedEdges) => pipe(guardedEdges, Array.findFirstWithIndex(isGuardListFallback), Option.map(([fallback, position]) => ({
|
|
365
|
+
position,
|
|
366
|
+
reason: fallbackToShadowingReason(fallback),
|
|
367
|
+
})));
|
|
368
|
+
const firstFallbackInEdgeGroup = (edgeGroup) => pipe(edgeGroup, Array.head, Option.flatMap(edgeSummary => guardListAt(edgeSummary.from, edgeSummary.messageTag)), Option.flatMap(firstGuardListFallback));
|
|
369
|
+
const shadowedTransitionsInGroup = (edgeGroup) => pipe(firstFallbackInEdgeGroup(edgeGroup), Option.match({
|
|
236
370
|
onNone: () => [],
|
|
237
|
-
onSome:
|
|
238
|
-
onNone: () => false,
|
|
239
|
-
onSome: position => position > otherwisePosition,
|
|
240
|
-
})), Array.map(edgeSummary => makeDeadTransition(edgeSummary, 'ShadowedByOtherwise'))),
|
|
241
|
-
});
|
|
242
|
-
}));
|
|
243
|
-
const deadTransitions = (extraRoots = []) => {
|
|
244
|
-
const reachable = reachableFromRoots([initialTag, ...extraRoots]);
|
|
245
|
-
const unreachableSourceEdges = pipe(edges, Array.filter(edgeSummary => !reachable.has(edgeSummary.from)), Array.map(edgeSummary => makeDeadTransition(edgeSummary, 'UnreachableSource')));
|
|
246
|
-
return [...unreachableSourceEdges, ...shadowedEdges()];
|
|
247
|
-
};
|
|
248
|
-
const toMermaid = () => {
|
|
249
|
-
const guardLabel = (guard) => Match.value(guard).pipe(Match.tagsExhaustive({
|
|
250
|
-
Unguarded: () => '',
|
|
251
|
-
When: ({ position }) => ` [when ${position + 1}]`,
|
|
252
|
-
Otherwise: () => ' [otherwise]',
|
|
371
|
+
onSome: fallback => pipe(edgesAfterGuardPosition(edgeGroup, fallback.position), Array.map(edgeSummary => makeDeadTransition(edgeSummary, fallback.reason))),
|
|
253
372
|
}));
|
|
254
|
-
const
|
|
255
|
-
const
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
373
|
+
const edgeGroups = pipe(edges, Array.groupBy(edgeSummary => edgeSummary.from), Record.values, Array.flatMap(groupEdgesByMessageTag));
|
|
374
|
+
const shadowedTransitions = pipe(edgeGroups, Array.flatMap(shadowedTransitionsInGroup));
|
|
375
|
+
const shadowedEdgeSet = pipe(shadowedTransitions, Array.map(shadowedTransition => shadowedTransition.edge), HashSet.fromIterable);
|
|
376
|
+
const selectableEdges = Array.filter(edges, edgeSummary => !HashSet.has(shadowedEdgeSet, edgeSummary));
|
|
377
|
+
const targetsFrom = (tag) => pipe(selectableEdges, Array.filter(edgeSummary => edgeSummary.from === tag), Array.map(edgeSummary => edgeSummary.target));
|
|
378
|
+
const reachableFromRoots = (roots) => {
|
|
379
|
+
const visit = (frontier, visited) => Array.matchLeft(frontier, {
|
|
380
|
+
onEmpty: () => visited,
|
|
381
|
+
onNonEmpty: (head, tail) => visited.has(head)
|
|
382
|
+
? visit(tail, visited)
|
|
383
|
+
: visit([...tail, ...targetsFrom(head)], new Set([...visited, head])),
|
|
384
|
+
});
|
|
385
|
+
return visit(roots, new Set());
|
|
386
|
+
};
|
|
387
|
+
const reachableFrom = (tag) => reachableFromRoots([tag]);
|
|
388
|
+
const unreachableStates = (extraRoots = []) => {
|
|
389
|
+
const reachable = reachableFromRoots([initialTag, ...extraRoots]);
|
|
390
|
+
return Array.filter(stateTags, stateTag => !reachable.has(stateTag));
|
|
391
|
+
};
|
|
392
|
+
const deadTransitions = (extraRoots = []) => {
|
|
393
|
+
const reachable = reachableFromRoots([initialTag, ...extraRoots]);
|
|
394
|
+
const unreachableSourceEdges = pipe(selectableEdges, Array.filter(edgeSummary => !reachable.has(edgeSummary.from)), Array.map(edgeSummary => makeDeadTransition(edgeSummary, 'UnreachableSource')));
|
|
395
|
+
return Array.flatten([unreachableSourceEdges, shadowedTransitions]);
|
|
396
|
+
};
|
|
397
|
+
const toMermaid = () => {
|
|
398
|
+
const guardLabel = (guard) => Match.value(guard).pipe(Match.tagsExhaustive({
|
|
399
|
+
Unguarded: () => '',
|
|
400
|
+
When: ({ position }) => ` [when ${position + 1}]`,
|
|
401
|
+
Otherwise: () => ' [otherwise]',
|
|
402
|
+
}));
|
|
403
|
+
const stateLines = Array.map(stateTags, stateTag => ` ${stateTag}`);
|
|
404
|
+
const transitionLines = Array.map(edges, edgeSummary => ` ${edgeSummary.from} --> ${edgeSummary.target}: ${edgeSummary.messageTag}${guardLabel(edgeSummary.guard)}`);
|
|
405
|
+
return Array.join([
|
|
406
|
+
'stateDiagram-v2',
|
|
407
|
+
...stateLines,
|
|
408
|
+
` [*] --> ${initialTag}`,
|
|
409
|
+
...transitionLines,
|
|
410
|
+
], '\n');
|
|
411
|
+
};
|
|
412
|
+
return {
|
|
413
|
+
initial: definition.initial,
|
|
414
|
+
stateTags,
|
|
415
|
+
edges,
|
|
416
|
+
transition,
|
|
417
|
+
step,
|
|
418
|
+
reachableFrom,
|
|
419
|
+
unreachableStates,
|
|
420
|
+
deadTransitions,
|
|
421
|
+
toMermaid,
|
|
422
|
+
};
|
|
262
423
|
};
|
|
263
|
-
|
|
264
|
-
initial: definition.initial,
|
|
265
|
-
stateTags,
|
|
266
|
-
edges,
|
|
267
|
-
transition,
|
|
268
|
-
step,
|
|
269
|
-
reachableFrom,
|
|
270
|
-
unreachableStates,
|
|
271
|
-
deadTransitions,
|
|
272
|
-
toMermaid,
|
|
273
|
-
};
|
|
274
|
-
};
|
|
424
|
+
}
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { UrlRequest } from '../navigation/urlRequest.js';
|
|
2
|
+
import { Url } from '../url/index.js';
|
|
3
|
+
/** Configuration for URL routing with handlers for URL requests and URL changes. */
|
|
4
|
+
export type RoutingConfig<Message> = Readonly<{
|
|
5
|
+
onUrlRequest: (request: UrlRequest) => Message;
|
|
6
|
+
onUrlChange: (url: Url) => Message;
|
|
7
|
+
}>;
|
|
2
8
|
export declare const addNavigationEventListeners: <Message>(dispatch: (message: Message) => void, routingConfig: RoutingConfig<Message>) => (() => void);
|
|
3
9
|
export declare const addLinkClickListener: <Message>(dispatch: (message: Message) => void, routingConfig: RoutingConfig<Message>) => (() => void);
|
|
4
10
|
//# sourceMappingURL=browserListeners.d.ts.map
|