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.
Files changed (82) hide show
  1. package/dist/devTools/host.d.ts +2 -2
  2. package/dist/devTools/host.d.ts.map +1 -1
  3. package/dist/devTools/host.js +1 -1
  4. package/dist/experimental/machine/machine.d.ts +278 -84
  5. package/dist/experimental/machine/machine.d.ts.map +1 -1
  6. package/dist/experimental/machine/machine.js +362 -212
  7. package/dist/runtime/browserListeners.d.ts +7 -1
  8. package/dist/runtime/browserListeners.d.ts.map +1 -1
  9. package/dist/runtime/browserScheduler.d.ts +3 -0
  10. package/dist/runtime/browserScheduler.d.ts.map +1 -0
  11. package/dist/runtime/browserScheduler.js +22 -0
  12. package/dist/runtime/crashUI.d.ts +36 -5
  13. package/dist/runtime/crashUI.d.ts.map +1 -1
  14. package/dist/runtime/crashUI.js +55 -2
  15. package/dist/runtime/devToolsConfig.d.ts +85 -0
  16. package/dist/runtime/devToolsConfig.d.ts.map +1 -0
  17. package/dist/runtime/devToolsConfig.js +49 -0
  18. package/dist/runtime/devToolsIntegration.d.ts +56 -0
  19. package/dist/runtime/devToolsIntegration.d.ts.map +1 -0
  20. package/dist/runtime/devToolsIntegration.js +186 -0
  21. package/dist/runtime/dispatch.d.ts +10 -0
  22. package/dist/runtime/dispatch.d.ts.map +1 -0
  23. package/dist/runtime/dispatch.js +4 -0
  24. package/dist/runtime/documentMetadata.d.ts +3 -0
  25. package/dist/runtime/documentMetadata.d.ts.map +1 -0
  26. package/dist/runtime/documentMetadata.js +148 -0
  27. package/dist/runtime/duplicateIdScanner.d.ts +16 -0
  28. package/dist/runtime/duplicateIdScanner.d.ts.map +1 -0
  29. package/dist/runtime/duplicateIdScanner.js +70 -0
  30. package/dist/runtime/hmrModelBridge.d.ts +4 -0
  31. package/dist/runtime/hmrModelBridge.d.ts.map +1 -0
  32. package/dist/runtime/hmrModelBridge.js +43 -0
  33. package/dist/runtime/hostConnector.d.ts +61 -0
  34. package/dist/runtime/hostConnector.d.ts.map +1 -0
  35. package/dist/runtime/hostConnector.js +141 -0
  36. package/dist/runtime/hydrationHandoff.d.ts +44 -0
  37. package/dist/runtime/hydrationHandoff.d.ts.map +1 -0
  38. package/dist/runtime/hydrationHandoff.js +395 -0
  39. package/dist/runtime/index.d.ts +16 -1
  40. package/dist/runtime/index.d.ts.map +1 -1
  41. package/dist/runtime/index.js +5 -1
  42. package/dist/runtime/makeApplication.d.ts +71 -0
  43. package/dist/runtime/makeApplication.d.ts.map +1 -0
  44. package/dist/runtime/makeApplication.js +98 -0
  45. package/dist/runtime/makeElement.d.ts +69 -0
  46. package/dist/runtime/makeElement.d.ts.map +1 -0
  47. package/dist/runtime/makeElement.js +81 -0
  48. package/dist/runtime/managedResourceFibers.d.ts +18 -0
  49. package/dist/runtime/managedResourceFibers.d.ts.map +1 -0
  50. package/dist/runtime/managedResourceFibers.js +47 -0
  51. package/dist/runtime/messageQueue.d.ts +27 -0
  52. package/dist/runtime/messageQueue.d.ts.map +1 -0
  53. package/dist/runtime/messageQueue.js +133 -0
  54. package/dist/runtime/public.d.ts +2 -3
  55. package/dist/runtime/public.d.ts.map +1 -1
  56. package/dist/runtime/public.js +1 -1
  57. package/dist/runtime/renderer.d.ts +69 -0
  58. package/dist/runtime/renderer.d.ts.map +1 -0
  59. package/dist/runtime/renderer.js +512 -0
  60. package/dist/runtime/resourceProvider.d.ts +33 -0
  61. package/dist/runtime/resourceProvider.d.ts.map +1 -0
  62. package/dist/runtime/resourceProvider.js +98 -0
  63. package/dist/runtime/runtime.d.ts +133 -412
  64. package/dist/runtime/runtime.d.ts.map +1 -1
  65. package/dist/runtime/runtime.js +118 -2184
  66. package/dist/runtime/runtimeStatus.d.ts +15 -0
  67. package/dist/runtime/runtimeStatus.d.ts.map +1 -0
  68. package/dist/runtime/runtimeStatus.js +6 -0
  69. package/dist/runtime/slowPhase.d.ts +82 -0
  70. package/dist/runtime/slowPhase.d.ts.map +1 -0
  71. package/dist/runtime/slowPhase.js +83 -0
  72. package/dist/runtime/start.d.ts +96 -0
  73. package/dist/runtime/start.d.ts.map +1 -0
  74. package/dist/runtime/start.js +99 -0
  75. package/dist/runtime/subscriptionFibers.d.ts +21 -0
  76. package/dist/runtime/subscriptionFibers.d.ts.map +1 -0
  77. package/dist/runtime/subscriptionFibers.js +47 -0
  78. package/dist/runtime/visibility.d.ts +8 -0
  79. package/dist/runtime/visibility.d.ts.map +1 -0
  80. package/dist/runtime/visibility.js +6 -0
  81. package/dist/test/scene.js +1 -1
  82. package/package.json +1 -1
@@ -1,5 +1,36 @@
1
- import { Array, Match, Option, Predicate, Record, pipe } from 'effect';
2
- const makeEdge = (target, build, commands) => {
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
- build: input => build(narrowGuardValue(input)),
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
- * `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.
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` callback may return Commands whose Effects need services. The
24
- * requirements they carry flow into the Machine's `R` through {@link define}.
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 {@link TransitionTable}: the source and trigger
27
- * types flow in from the table position contextually.
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, build, commands) => makeEdge(target, build, commands);
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 build and commands callbacks as `guardValue`) or returns a
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
- * The state and Message parameters are `NoInfer` so they resolve from the
41
- * guard list's table position alone.
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, build, commands) => ({
46
- _tag: 'When',
47
- guard: (state, message) => {
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
- edge: makeEdge(target, build, commands),
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 edge may carry
60
- * Commands whose Effects need services, threading their requirements into the
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
- const isGuardList = (edgeOrGuardedEdges) => globalThis.Array.isArray(edgeOrGuardedEdges);
70
- const extractLiteralTag = (tagField) => {
71
- if (Predicate.hasProperty(tagField, 'literal') &&
72
- Predicate.isString(tagField.literal)) {
73
- return Option.some(tagField.literal);
74
- }
75
- else if (Predicate.hasProperty(tagField, 'schema')) {
76
- return extractLiteralTag(tagField.schema);
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 Option.none();
121
+ return config.machine.transition(state, message);
80
122
  }
81
123
  };
82
- 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)));
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
- * Two stages: the first takes the state and Message union Schemas and fixes
87
- * the type parameters, the second takes the initial state and the transition
88
- * table. The split is what lets TypeScript narrow `state` and `message`
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 machine = define({ state: DialogState, message: DialogMessage })({
112
- * initial: Idle(),
113
- * states: {
114
- * Idle: {
115
- * on: {
116
- * ClickedSubmit: to('Uploading', () => Uploading(), () => [Presign()]),
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
- * // Commands on the return from machine.transition(...) require UploadsClient.
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 define = (schemas) => (definition) => {
127
- /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */
128
- const looseStates = definition.states;
129
- const initialTag = definition.initial._tag;
130
- 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'))),
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
- tags => tags);
133
- const makeEdgeSummary = (from, messageTag, target, guard) => ({ from, messageTag, target, guard });
134
- const summarizeEntry = (from, messageTag, edgeOrGuardedEdges) => isGuardList(edgeOrGuardedEdges)
135
- ? Array.map(edgeOrGuardedEdges, (guardedEdge, position) => guardedEdge._tag === 'When'
136
- ? makeEdgeSummary(from, messageTag, guardedEdge.edge.target, {
137
- _tag: 'When',
138
- position,
139
- })
140
- : makeEdgeSummary(from, messageTag, guardedEdge.edge.target, {
141
- _tag: 'Otherwise',
142
- position,
143
- }))
144
- : [
145
- makeEdgeSummary(from, messageTag, edgeOrGuardedEdges.target, {
146
- _tag: 'Unguarded',
147
- }),
148
- ];
149
- const edges = pipe(Record.toEntries(looseStates), Array.flatMap(([sourceTag, stateEntry]) => pipe(Record.toEntries(stateEntry.on), Array.flatMap(([messageTag, edgeOrGuardedEdges]) => summarizeEntry(sourceTag, messageTag, edgeOrGuardedEdges)))));
150
- const selectFromGuardList = (guardedEdges, state, message) => Array.matchLeft(guardedEdges, {
151
- onEmpty: () => Option.none(),
152
- onNonEmpty: (guardedEdge, rest) => {
153
- if (guardedEdge._tag === 'When') {
154
- const maybeGuardValue = guardedEdge.guard(state, message);
155
- if (Option.isSome(maybeGuardValue)) {
156
- return Option.some({
157
- edge: guardedEdge.edge,
158
- guardValue: maybeGuardValue.value,
159
- });
160
- }
161
- else {
162
- return selectFromGuardList(rest, state, message);
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 Option.some({
167
- edge: guardedEdge.edge,
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
- const chooseEdge = (edgeOrGuardedEdges, state, message) => isGuardList(edgeOrGuardedEdges)
174
- ? selectFromGuardList(edgeOrGuardedEdges, state, message)
175
- : Option.some({ edge: edgeOrGuardedEdges, guardValue: undefined });
176
- const makeTransitioned = (state, message, selectedEdge) => ({
177
- _tag: 'Transitioned',
178
- from: state._tag,
179
- target: selectedEdge.edge.target,
180
- messageTag: message._tag,
181
- state: selectedEdge.edge.build({
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
- message,
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
- return visit(roots, new Set());
223
- };
224
- const reachableFrom = (tag) => reachableFromRoots([tag]);
225
- const unreachableStates = (extraRoots = []) => {
226
- const reachable = reachableFromRoots([initialTag, ...extraRoots]);
227
- return Array.filter(stateTags, stateTag => !reachable.has(stateTag));
228
- };
229
- const makeDeadTransition = (edge, reason) => ({ edge, reason });
230
- const guardPosition = (edgeSummary) => edgeSummary.guard._tag === 'Unguarded'
231
- ? Option.none()
232
- : Option.some(edgeSummary.guard.position);
233
- const shadowedEdges = () => pipe(edges, Array.groupBy(edgeSummary => `${edgeSummary.from}|${edgeSummary.messageTag}`), Record.values, Array.flatMap(group => {
234
- const maybeOtherwisePosition = pipe(group, Array.findFirst(edgeSummary => edgeSummary.guard._tag === 'Otherwise'), Option.flatMap(guardPosition));
235
- return Option.match(maybeOtherwisePosition, {
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: otherwisePosition => pipe(group, Array.filter(edgeSummary => Option.match(guardPosition(edgeSummary), {
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 stateLines = Array.map(stateTags, stateTag => ` ${stateTag}`);
255
- const transitionLines = Array.map(edges, edgeSummary => ` ${edgeSummary.from} --> ${edgeSummary.target}: ${edgeSummary.messageTag}${guardLabel(edgeSummary.guard)}`);
256
- return Array.join([
257
- 'stateDiagram-v2',
258
- ...stateLines,
259
- ` [*] --> ${initialTag}`,
260
- ...transitionLines,
261
- ], '\n');
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
- return {
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 { RoutingConfig } from './runtime.js';
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