xstate 5.19.4 → 5.20.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/README.md +0 -1
  2. package/actions/dist/xstate-actions.cjs.d.ts +1 -1
  3. package/actions/dist/xstate-actions.cjs.js +8 -7
  4. package/actions/dist/xstate-actions.development.cjs.js +8 -7
  5. package/actions/dist/xstate-actions.development.esm.js +3 -2
  6. package/actions/dist/xstate-actions.esm.js +3 -2
  7. package/actions/dist/xstate-actions.umd.min.js +1 -1
  8. package/actions/dist/xstate-actions.umd.min.js.map +1 -1
  9. package/actors/dist/xstate-actors.cjs.d.ts +1 -1
  10. package/actors/dist/xstate-actors.development.esm.js +1 -1
  11. package/actors/dist/xstate-actors.esm.js +1 -1
  12. package/actors/dist/xstate-actors.umd.min.js.map +1 -1
  13. package/dev/dist/xstate-dev.cjs.d.ts +1 -1
  14. package/dev/dist/xstate-dev.umd.min.js.map +1 -1
  15. package/dist/StateMachine-1cda96d3.cjs.js +560 -0
  16. package/dist/StateMachine-38b5bb3f.development.cjs.js +566 -0
  17. package/dist/StateMachine-b4e94439.development.esm.js +563 -0
  18. package/dist/StateMachine-c88ea5dd.esm.js +557 -0
  19. package/dist/assign-6313ccb3.development.esm.js +133 -0
  20. package/dist/assign-c3259787.esm.js +127 -0
  21. package/dist/assign-c84786ab.development.cjs.js +135 -0
  22. package/dist/assign-e9c344ea.cjs.js +129 -0
  23. package/dist/declarations/src/StateMachine.d.ts +2 -2
  24. package/dist/declarations/src/createMachine.d.ts +1 -2
  25. package/dist/declarations/src/graph/TestModel.d.ts +70 -0
  26. package/dist/declarations/src/graph/adjacency.d.ts +8 -0
  27. package/dist/declarations/src/graph/graph.d.ts +14 -0
  28. package/dist/declarations/src/graph/index.d.ts +9 -0
  29. package/dist/declarations/src/graph/pathFromEvents.d.ts +3 -0
  30. package/dist/declarations/src/graph/pathGenerators.d.ts +4 -0
  31. package/dist/declarations/src/graph/shortestPaths.d.ts +3 -0
  32. package/dist/declarations/src/graph/simplePaths.d.ts +3 -0
  33. package/dist/declarations/src/graph/types.d.ts +159 -0
  34. package/dist/declarations/src/index.d.ts +1 -1
  35. package/dist/declarations/src/inspection.d.ts +1 -1
  36. package/dist/{log-655aa404.esm.js → log-1c257a58.esm.js} +3 -126
  37. package/dist/{log-fa2e731a.cjs.js → log-215998b6.cjs.js} +2 -126
  38. package/dist/{log-fadc8808.development.cjs.js → log-2c8d7f98.development.cjs.js} +2 -132
  39. package/dist/{log-5a7b5528.development.esm.js → log-ef959da6.development.esm.js} +3 -132
  40. package/dist/{raise-59549771.development.esm.js → raise-78b8dcb8.development.esm.js} +1 -1
  41. package/dist/{raise-3e01e82a.esm.js → raise-b0a4e862.esm.js} +1 -1
  42. package/dist/xstate.cjs.d.ts +1 -1
  43. package/dist/xstate.cjs.js +7 -558
  44. package/dist/xstate.development.cjs.js +7 -564
  45. package/dist/xstate.development.esm.js +7 -564
  46. package/dist/xstate.esm.js +7 -558
  47. package/dist/xstate.umd.min.js +1 -1
  48. package/dist/xstate.umd.min.js.map +1 -1
  49. package/graph/dist/xstate-graph.cjs.d.mts +2 -0
  50. package/graph/dist/xstate-graph.cjs.d.ts +2 -0
  51. package/graph/dist/xstate-graph.cjs.js +901 -0
  52. package/graph/dist/xstate-graph.cjs.mjs +15 -0
  53. package/graph/dist/xstate-graph.development.cjs.js +901 -0
  54. package/graph/dist/xstate-graph.development.cjs.mjs +15 -0
  55. package/graph/dist/xstate-graph.development.esm.js +885 -0
  56. package/graph/dist/xstate-graph.esm.js +885 -0
  57. package/graph/dist/xstate-graph.umd.min.js +2 -0
  58. package/graph/dist/xstate-graph.umd.min.js.map +1 -0
  59. package/graph/package.json +8 -0
  60. package/guards/dist/xstate-guards.cjs.d.ts +1 -1
  61. package/guards/dist/xstate-guards.development.esm.js +1 -1
  62. package/guards/dist/xstate-guards.esm.js +1 -1
  63. package/guards/dist/xstate-guards.umd.min.js +1 -1
  64. package/guards/dist/xstate-guards.umd.min.js.map +1 -1
  65. package/package.json +22 -6
@@ -0,0 +1,885 @@
1
+ import { createEmptyActor } from '../../actors/dist/xstate-actors.esm.js';
2
+ import { S as StateMachine } from '../../dist/StateMachine-c88ea5dd.esm.js';
3
+ import { b as isMachineSnapshot, d as getAllOwnEventDescriptors } from '../../dist/raise-b0a4e862.esm.js';
4
+ import '../../dev/dist/xstate-dev.esm.js';
5
+ import '../../dist/assign-c3259787.esm.js';
6
+
7
+ function simpleStringify(value) {
8
+ return JSON.stringify(value);
9
+ }
10
+ function formatPathTestResult(path, testPathResult, options) {
11
+ const resolvedOptions = {
12
+ formatColor: (_color, string) => string,
13
+ serializeState: simpleStringify,
14
+ serializeEvent: simpleStringify,
15
+ ...options
16
+ };
17
+ const {
18
+ formatColor,
19
+ serializeState,
20
+ serializeEvent
21
+ } = resolvedOptions;
22
+ const {
23
+ state
24
+ } = path;
25
+ const targetStateString = serializeState(state, path.steps.length ? path.steps[path.steps.length - 1].event : undefined);
26
+ let errMessage = '';
27
+ let hasFailed = false;
28
+ errMessage += '\nPath:\n' + testPathResult.steps.map((s, i, steps) => {
29
+ const stateString = serializeState(s.step.state, i > 0 ? steps[i - 1].step.event : undefined);
30
+ const eventString = serializeEvent(s.step.event);
31
+ const stateResult = `\tState: ${hasFailed ? formatColor('gray', stateString) : s.state.error ? (hasFailed = true, formatColor('redBright', stateString)) : formatColor('greenBright', stateString)}`;
32
+ const eventResult = `\tEvent: ${hasFailed ? formatColor('gray', eventString) : s.event.error ? (hasFailed = true, formatColor('red', eventString)) : formatColor('green', eventString)}`;
33
+ return [stateResult, eventResult].join('\n');
34
+ }).concat(`\tState: ${hasFailed ? formatColor('gray', targetStateString) : testPathResult.state.error ? formatColor('red', targetStateString) : formatColor('green', targetStateString)}`).join('\n\n');
35
+ return errMessage;
36
+ }
37
+ function getDescription(snapshot) {
38
+ const contextString = !Object.keys(snapshot.context).length ? '' : `(${JSON.stringify(snapshot.context)})`;
39
+ const stateStrings = snapshot._nodes.filter(sn => sn.type === 'atomic' || sn.type === 'final').map(({
40
+ id,
41
+ path
42
+ }) => {
43
+ const meta = snapshot.getMeta()[id];
44
+ if (!meta) {
45
+ return `"${path.join('.')}"`;
46
+ }
47
+ const {
48
+ description
49
+ } = meta;
50
+ if (typeof description === 'function') {
51
+ return description(snapshot);
52
+ }
53
+ return description ? `"${description}"` : JSON.stringify(snapshot.value);
54
+ });
55
+ return `state${stateStrings.length === 1 ? '' : 's'} ` + stateStrings.join(', ') + ` ${contextString}`.trim();
56
+ }
57
+
58
+ /**
59
+ * Deduplicates your paths so that A -> B is not executed separately to A -> B
60
+ * -> C
61
+ */
62
+ const deduplicatePaths = (paths, serializeEvent = simpleStringify) => {
63
+ /** Put all paths on the same level so we can dedup them */
64
+ const allPathsWithEventSequence = [];
65
+ paths.forEach(path => {
66
+ allPathsWithEventSequence.push({
67
+ path,
68
+ eventSequence: path.steps.map(step => serializeEvent(step.event))
69
+ });
70
+ });
71
+
72
+ // Sort by path length, descending
73
+ allPathsWithEventSequence.sort((a, z) => z.path.steps.length - a.path.steps.length);
74
+ const superpathsWithEventSequence = [];
75
+
76
+ /** Filter out the paths that are subpaths of superpaths */
77
+ pathLoop: for (const pathWithEventSequence of allPathsWithEventSequence) {
78
+ // Check each existing superpath to see if the path is a subpath of it
79
+ superpathLoop: for (const superpathWithEventSequence of superpathsWithEventSequence) {
80
+ // eslint-disable-next-line @typescript-eslint/no-for-in-array
81
+ for (const i in pathWithEventSequence.eventSequence) {
82
+ // Check event sequence to determine if path is subpath, e.g.:
83
+ //
84
+ // This will short-circuit the check
85
+ // ['a', 'b', 'c', 'd'] (superpath)
86
+ // ['a', 'b', 'x'] (path)
87
+ //
88
+ // This will not short-circuit; path is subpath
89
+ // ['a', 'b', 'c', 'd'] (superpath)
90
+ // ['a', 'b', 'c'] (path)
91
+ if (pathWithEventSequence.eventSequence[i] !== superpathWithEventSequence.eventSequence[i]) {
92
+ // If the path is different from the superpath,
93
+ // continue to the next superpath
94
+ continue superpathLoop;
95
+ }
96
+ }
97
+
98
+ // If we reached here, path is subpath of superpath
99
+ // Continue & do not add path to superpaths
100
+ continue pathLoop;
101
+ }
102
+
103
+ // If we reached here, path is not a subpath of any existing superpaths
104
+ // So add it to the superpaths
105
+ superpathsWithEventSequence.push(pathWithEventSequence);
106
+ }
107
+ return superpathsWithEventSequence.map(path => path.path);
108
+ };
109
+
110
+ const createShortestPathsGen = () => (logic, defaultOptions) => {
111
+ const paths = getShortestPaths(logic, defaultOptions);
112
+ return paths;
113
+ };
114
+ const createSimplePathsGen = () => (logic, defaultOptions) => {
115
+ const paths = getSimplePaths(logic, defaultOptions);
116
+ return paths;
117
+ };
118
+
119
+ const validateState = state => {
120
+ if (state.invoke.length > 0) {
121
+ throw new Error('Invocations on test machines are not supported');
122
+ }
123
+ if (state.after.length > 0) {
124
+ throw new Error('After events on test machines are not supported');
125
+ }
126
+ // TODO: this doesn't account for always transitions
127
+ [...state.entry, ...state.exit, ...[...state.transitions.values()].flatMap(t => t.flatMap(t => t.actions))].forEach(action => {
128
+ // TODO: this doesn't check referenced actions, only the inline ones
129
+ if (typeof action === 'function' && 'resolve' in action && typeof action.delay === 'number') {
130
+ throw new Error('Delayed actions on test machines are not supported');
131
+ }
132
+ });
133
+ for (const child of Object.values(state.states)) {
134
+ validateState(child);
135
+ }
136
+ };
137
+ const validateMachine = machine => {
138
+ validateState(machine.root);
139
+ };
140
+
141
+ /**
142
+ * Creates a test model that represents an abstract model of a system under test
143
+ * (SUT).
144
+ *
145
+ * The test model is used to generate test paths, which are used to verify that
146
+ * states in the model are reachable in the SUT.
147
+ */
148
+ class TestModel {
149
+ getDefaultOptions() {
150
+ return {
151
+ serializeState: state => simpleStringify(state),
152
+ serializeEvent: event => simpleStringify(event),
153
+ // For non-state-machine test models, we cannot identify
154
+ // separate transitions, so just use event type
155
+ serializeTransition: (state, event) => `${simpleStringify(state)}|${event?.type}`,
156
+ events: [],
157
+ stateMatcher: (_, stateKey) => stateKey === '*',
158
+ logger: {
159
+ log: console.log.bind(console),
160
+ error: console.error.bind(console)
161
+ }
162
+ };
163
+ }
164
+ constructor(testLogic, options) {
165
+ this.testLogic = testLogic;
166
+ this.options = void 0;
167
+ this.defaultTraversalOptions = void 0;
168
+ this._toTestPath = statePath => {
169
+ function formatEvent(event) {
170
+ const {
171
+ type,
172
+ ...other
173
+ } = event;
174
+ const propertyString = Object.keys(other).length ? ` (${JSON.stringify(other)})` : '';
175
+ return `${type}${propertyString}`;
176
+ }
177
+ const eventsString = statePath.steps.map(s => formatEvent(s.event)).join(' → ');
178
+ return {
179
+ ...statePath,
180
+ test: params => this.testPath(statePath, params),
181
+ description: isMachineSnapshot(statePath.state) ? `Reaches ${getDescription(statePath.state).trim()}: ${eventsString}` : JSON.stringify(statePath.state)
182
+ };
183
+ };
184
+ this.options = {
185
+ ...this.getDefaultOptions(),
186
+ ...options
187
+ };
188
+ }
189
+ getPaths(pathGenerator, options) {
190
+ const allowDuplicatePaths = options?.allowDuplicatePaths ?? false;
191
+ const paths = pathGenerator(this.testLogic, this._resolveOptions(options));
192
+ return (allowDuplicatePaths ? paths : deduplicatePaths(paths)).map(this._toTestPath);
193
+ }
194
+ getShortestPaths(options) {
195
+ return this.getPaths(createShortestPathsGen(), options);
196
+ }
197
+ getShortestPathsFrom(paths, options) {
198
+ const resultPaths = [];
199
+ for (const path of paths) {
200
+ const shortestPaths = this.getShortestPaths({
201
+ ...options,
202
+ fromState: path.state
203
+ });
204
+ for (const shortestPath of shortestPaths) {
205
+ resultPaths.push(this._toTestPath(joinPaths(path, shortestPath)));
206
+ }
207
+ }
208
+ return resultPaths;
209
+ }
210
+ getSimplePaths(options) {
211
+ return this.getPaths(createSimplePathsGen(), options);
212
+ }
213
+ getSimplePathsFrom(paths, options) {
214
+ const resultPaths = [];
215
+ for (const path of paths) {
216
+ const shortestPaths = this.getSimplePaths({
217
+ ...options,
218
+ fromState: path.state
219
+ });
220
+ for (const shortestPath of shortestPaths) {
221
+ resultPaths.push(this._toTestPath(joinPaths(path, shortestPath)));
222
+ }
223
+ }
224
+ return resultPaths;
225
+ }
226
+ getPathsFromEvents(events, options) {
227
+ const paths = getPathsFromEvents(this.testLogic, events, options);
228
+ return paths.map(this._toTestPath);
229
+ }
230
+
231
+ /**
232
+ * An array of adjacencies, which are objects that represent each `state` with
233
+ * the `nextState` given the `event`.
234
+ */
235
+ getAdjacencyMap() {
236
+ const adjMap = getAdjacencyMap(this.testLogic, this.options);
237
+ return adjMap;
238
+ }
239
+ async testPath(path, params, options) {
240
+ const testPathResult = {
241
+ steps: [],
242
+ state: {
243
+ error: null
244
+ }
245
+ };
246
+ try {
247
+ for (const step of path.steps) {
248
+ const testStepResult = {
249
+ step,
250
+ state: {
251
+ error: null
252
+ },
253
+ event: {
254
+ error: null
255
+ }
256
+ };
257
+ testPathResult.steps.push(testStepResult);
258
+ try {
259
+ await this.testTransition(params, step);
260
+ } catch (err) {
261
+ testStepResult.event.error = err;
262
+ throw err;
263
+ }
264
+ try {
265
+ await this.testState(params, step.state, options);
266
+ } catch (err) {
267
+ testStepResult.state.error = err;
268
+ throw err;
269
+ }
270
+ }
271
+ } catch (err) {
272
+ // TODO: make option
273
+ err.message += formatPathTestResult(path, testPathResult, this.options);
274
+ throw err;
275
+ }
276
+ return testPathResult;
277
+ }
278
+ async testState(params, state, options) {
279
+ const resolvedOptions = this._resolveOptions(options);
280
+ const stateTestKeys = this._getStateTestKeys(params, state, resolvedOptions);
281
+ for (const stateTestKey of stateTestKeys) {
282
+ await params.states?.[stateTestKey](state);
283
+ }
284
+ }
285
+ _getStateTestKeys(params, state, resolvedOptions) {
286
+ const states = params.states || {};
287
+ const stateTestKeys = Object.keys(states).filter(stateKey => {
288
+ return resolvedOptions.stateMatcher(state, stateKey);
289
+ });
290
+
291
+ // Fallthrough state tests
292
+ if (!stateTestKeys.length && '*' in states) {
293
+ stateTestKeys.push('*');
294
+ }
295
+ return stateTestKeys;
296
+ }
297
+ _getEventExec(params, step) {
298
+ const eventExec = params.events?.[step.event.type];
299
+ return eventExec;
300
+ }
301
+ async testTransition(params, step) {
302
+ const eventExec = this._getEventExec(params, step);
303
+ await eventExec?.(step);
304
+ }
305
+ _resolveOptions(options) {
306
+ return {
307
+ ...this.defaultTraversalOptions,
308
+ ...this.options,
309
+ ...options
310
+ };
311
+ }
312
+ }
313
+ function stateValuesEqual(a, b) {
314
+ if (a === b) {
315
+ return true;
316
+ }
317
+ if (a === undefined || b === undefined) {
318
+ return false;
319
+ }
320
+ if (typeof a === 'string' || typeof b === 'string') {
321
+ return a === b;
322
+ }
323
+ const aKeys = Object.keys(a);
324
+ const bKeys = Object.keys(b);
325
+ return aKeys.length === bKeys.length && aKeys.every(key => stateValuesEqual(a[key], b[key]));
326
+ }
327
+ function serializeMachineTransition(snapshot, event, previousSnapshot, {
328
+ serializeEvent
329
+ }) {
330
+ // TODO: the stateValuesEqual check here is very likely not exactly correct
331
+ // but I'm not sure what the correct check is and what this is trying to do
332
+ if (!event || previousSnapshot && stateValuesEqual(previousSnapshot.value, snapshot.value)) {
333
+ return '';
334
+ }
335
+ const prevStateString = previousSnapshot ? ` from ${simpleStringify(previousSnapshot.value)}` : '';
336
+ return ` via ${serializeEvent(event)}${prevStateString}`;
337
+ }
338
+
339
+ /**
340
+ * Creates a test model that represents an abstract model of a system under test
341
+ * (SUT).
342
+ *
343
+ * The test model is used to generate test paths, which are used to verify that
344
+ * states in the `machine` are reachable in the SUT.
345
+ *
346
+ * @example
347
+ *
348
+ * ```js
349
+ * const toggleModel = createModel(toggleMachine).withEvents({
350
+ * TOGGLE: {
351
+ * exec: async (page) => {
352
+ * await page.click('input');
353
+ * }
354
+ * }
355
+ * });
356
+ * ```
357
+ *
358
+ * @param machine The state machine used to represent the abstract model.
359
+ * @param options Options for the created test model:
360
+ *
361
+ * - `events`: an object mapping string event types (e.g., `SUBMIT`) to an event
362
+ * test config (e.g., `{exec: () => {...}, cases: [...]}`)
363
+ */
364
+ function createTestModel(machine, options) {
365
+ validateMachine(machine);
366
+ const serializeEvent = options?.serializeEvent ?? simpleStringify;
367
+ const serializeTransition = options?.serializeTransition ?? serializeMachineTransition;
368
+ const {
369
+ events: getEvents,
370
+ ...otherOptions
371
+ } = options ?? {};
372
+ const testModel = new TestModel(machine, {
373
+ serializeState: (state, event, prevState) => {
374
+ // Only consider the `state` if `serializeTransition()` is opted out (empty string)
375
+ return `${serializeSnapshot(state)}${serializeTransition(state, event, prevState, {
376
+ serializeEvent
377
+ })}`;
378
+ },
379
+ stateMatcher: (state, key) => {
380
+ return key.startsWith('#') ? state._nodes.includes(machine.getStateNodeById(key)) : state.matches(key);
381
+ },
382
+ events: state => {
383
+ const events = typeof getEvents === 'function' ? getEvents(state) : getEvents ?? [];
384
+ return getAllOwnEventDescriptors(state).flatMap(eventType => {
385
+ if (events.some(e => e.type === eventType)) {
386
+ return events.filter(e => e.type === eventType);
387
+ }
388
+ return [{
389
+ type: eventType
390
+ }]; // TODO: fix types
391
+ });
392
+ },
393
+ ...otherOptions
394
+ });
395
+ return testModel;
396
+ }
397
+
398
+ function createMockActorScope() {
399
+ const emptyActor = createEmptyActor();
400
+ return {
401
+ self: emptyActor,
402
+ logger: console.log,
403
+ id: '',
404
+ sessionId: Math.random().toString(32).slice(2),
405
+ defer: () => {},
406
+ system: emptyActor.system,
407
+ // TODO: mock system?
408
+ stopChild: () => {},
409
+ emit: () => {},
410
+ actionExecutor: () => {}
411
+ };
412
+ }
413
+
414
+ /**
415
+ * Returns all state nodes of the given `node`.
416
+ *
417
+ * @param stateNode State node to recursively get child state nodes from
418
+ */
419
+ function getStateNodes(stateNode) {
420
+ const {
421
+ states
422
+ } = stateNode;
423
+ const nodes = Object.keys(states).reduce((accNodes, stateKey) => {
424
+ const childStateNode = states[stateKey];
425
+ const childStateNodes = getStateNodes(childStateNode);
426
+ accNodes.push(childStateNode, ...childStateNodes);
427
+ return accNodes;
428
+ }, []);
429
+ return nodes;
430
+ }
431
+ function getChildren(stateNode) {
432
+ if (!stateNode.states) {
433
+ return [];
434
+ }
435
+ const children = Object.keys(stateNode.states).map(key => {
436
+ return stateNode.states[key];
437
+ });
438
+ return children;
439
+ }
440
+ function serializeSnapshot(snapshot) {
441
+ const {
442
+ value,
443
+ context
444
+ } = snapshot;
445
+ return JSON.stringify({
446
+ value,
447
+ context: Object.keys(context ?? {}).length ? context : undefined
448
+ });
449
+ }
450
+ function serializeEvent(event) {
451
+ return JSON.stringify(event);
452
+ }
453
+ function createDefaultMachineOptions(machine, options) {
454
+ const {
455
+ events: getEvents,
456
+ ...otherOptions
457
+ } = options ?? {};
458
+ const traversalOptions = {
459
+ serializeState: serializeSnapshot,
460
+ serializeEvent,
461
+ events: state => {
462
+ const events = typeof getEvents === 'function' ? getEvents(state) : getEvents ?? [];
463
+ return getAllOwnEventDescriptors(state).flatMap(type => {
464
+ const matchingEvents = events.filter(ev => ev.type === type);
465
+ if (matchingEvents.length) {
466
+ return matchingEvents;
467
+ }
468
+ return [{
469
+ type
470
+ }];
471
+ });
472
+ },
473
+ fromState: machine.getInitialSnapshot(createMockActorScope(), options?.input),
474
+ ...otherOptions
475
+ };
476
+ return traversalOptions;
477
+ }
478
+ function createDefaultLogicOptions() {
479
+ return {
480
+ serializeState: state => JSON.stringify(state),
481
+ serializeEvent
482
+ };
483
+ }
484
+ function toDirectedGraph(stateMachine) {
485
+ const stateNode = stateMachine instanceof StateMachine ? stateMachine.root : stateMachine; // TODO: accept only machines
486
+
487
+ const edges = [...stateNode.transitions.values()].flat().flatMap((t, transitionIndex) => {
488
+ const targets = t.target ? t.target : [stateNode];
489
+ return targets.map((target, targetIndex) => {
490
+ const edge = {
491
+ id: `${stateNode.id}:${transitionIndex}:${targetIndex}`,
492
+ source: stateNode,
493
+ target: target,
494
+ transition: t,
495
+ label: {
496
+ text: t.eventType,
497
+ toJSON: () => ({
498
+ text: t.eventType
499
+ })
500
+ },
501
+ toJSON: () => {
502
+ const {
503
+ label
504
+ } = edge;
505
+ return {
506
+ source: stateNode.id,
507
+ target: target.id,
508
+ label
509
+ };
510
+ }
511
+ };
512
+ return edge;
513
+ });
514
+ });
515
+ const graph = {
516
+ id: stateNode.id,
517
+ stateNode: stateNode,
518
+ children: getChildren(stateNode).map(toDirectedGraph),
519
+ edges,
520
+ toJSON: () => {
521
+ const {
522
+ id,
523
+ children,
524
+ edges: graphEdges
525
+ } = graph;
526
+ return {
527
+ id,
528
+ children,
529
+ edges: graphEdges
530
+ };
531
+ }
532
+ };
533
+ return graph;
534
+ }
535
+ function isMachineLogic(logic) {
536
+ return 'getStateNodeById' in logic;
537
+ }
538
+ function resolveTraversalOptions(logic, traversalOptions, defaultOptions) {
539
+ const resolvedDefaultOptions = defaultOptions ?? (isMachineLogic(logic) ? createDefaultMachineOptions(logic, traversalOptions) : undefined);
540
+ const serializeState = traversalOptions?.serializeState ?? resolvedDefaultOptions?.serializeState ?? (state => JSON.stringify(state));
541
+ const traversalConfig = {
542
+ serializeState,
543
+ serializeEvent,
544
+ events: [],
545
+ limit: Infinity,
546
+ fromState: undefined,
547
+ toState: undefined,
548
+ // Traversal should not continue past the `toState` predicate
549
+ // since the target state has already been reached at that point
550
+ stopWhen: traversalOptions?.toState,
551
+ ...resolvedDefaultOptions,
552
+ ...traversalOptions
553
+ };
554
+ return traversalConfig;
555
+ }
556
+ function joinPaths(headPath, tailPath) {
557
+ const secondPathSource = tailPath.steps[0].state;
558
+ if (secondPathSource !== headPath.state) {
559
+ throw new Error(`Paths cannot be joined`);
560
+ }
561
+ return {
562
+ state: tailPath.state,
563
+ // e.g. [A, B, C] + [C, D, E] = [A, B, C, D, E]
564
+ steps: headPath.steps.concat(tailPath.steps.slice(1)),
565
+ weight: headPath.weight + tailPath.weight
566
+ };
567
+ }
568
+
569
+ function getAdjacencyMap(logic, options) {
570
+ const {
571
+ transition
572
+ } = logic;
573
+ const {
574
+ serializeEvent,
575
+ serializeState,
576
+ events: getEvents,
577
+ limit,
578
+ fromState: customFromState,
579
+ stopWhen
580
+ } = resolveTraversalOptions(logic, options);
581
+ const actorScope = createMockActorScope();
582
+ const fromState = customFromState ?? logic.getInitialSnapshot(actorScope,
583
+ // TODO: fix this
584
+ options.input);
585
+ const adj = {};
586
+ let iterations = 0;
587
+ const queue = [{
588
+ nextState: fromState,
589
+ event: undefined,
590
+ prevState: undefined
591
+ }];
592
+ const stateMap = new Map();
593
+ while (queue.length) {
594
+ const {
595
+ nextState: state,
596
+ event,
597
+ prevState
598
+ } = queue.shift();
599
+ if (iterations++ > limit) {
600
+ throw new Error('Traversal limit exceeded');
601
+ }
602
+ const serializedState = serializeState(state, event, prevState);
603
+ if (adj[serializedState]) {
604
+ continue;
605
+ }
606
+ stateMap.set(serializedState, state);
607
+ adj[serializedState] = {
608
+ state,
609
+ transitions: {}
610
+ };
611
+ if (stopWhen && stopWhen(state)) {
612
+ continue;
613
+ }
614
+ const events = typeof getEvents === 'function' ? getEvents(state) : getEvents;
615
+ for (const nextEvent of events) {
616
+ const nextSnapshot = transition(state, nextEvent, actorScope);
617
+ adj[serializedState].transitions[serializeEvent(nextEvent)] = {
618
+ event: nextEvent,
619
+ state: nextSnapshot
620
+ };
621
+ queue.push({
622
+ nextState: nextSnapshot,
623
+ event: nextEvent,
624
+ prevState: state
625
+ });
626
+ }
627
+ }
628
+ return adj;
629
+ }
630
+ function adjacencyMapToArray(adjMap) {
631
+ const adjList = [];
632
+ for (const adjValue of Object.values(adjMap)) {
633
+ for (const transition of Object.values(adjValue.transitions)) {
634
+ adjList.push({
635
+ state: adjValue.state,
636
+ event: transition.event,
637
+ nextState: transition.state
638
+ });
639
+ }
640
+ }
641
+ return adjList;
642
+ }
643
+
644
+ // TODO: rewrite parts of the algorithm leading to this to make this function obsolete
645
+ function alterPath(path) {
646
+ let steps = [];
647
+ if (!path.steps.length) {
648
+ steps = [{
649
+ state: path.state,
650
+ event: {
651
+ type: 'xstate.init'
652
+ }
653
+ }];
654
+ } else {
655
+ for (let i = 0; i < path.steps.length; i++) {
656
+ const step = path.steps[i];
657
+ steps.push({
658
+ state: step.state,
659
+ event: i === 0 ? {
660
+ type: 'xstate.init'
661
+ } : path.steps[i - 1].event
662
+ });
663
+ }
664
+ steps.push({
665
+ state: path.state,
666
+ event: path.steps[path.steps.length - 1].event
667
+ });
668
+ }
669
+ return {
670
+ ...path,
671
+ steps
672
+ };
673
+ }
674
+
675
+ function isMachine(value) {
676
+ return !!value && '__xstatenode' in value;
677
+ }
678
+ function getPathsFromEvents(logic, events, options) {
679
+ const resolvedOptions = resolveTraversalOptions(logic, {
680
+ events,
681
+ ...options
682
+ }, isMachine(logic) ? createDefaultMachineOptions(logic) : createDefaultLogicOptions());
683
+ const actorScope = createMockActorScope();
684
+ const fromState = resolvedOptions.fromState ?? logic.getInitialSnapshot(actorScope,
685
+ // TODO: fix this
686
+ options?.input);
687
+ const {
688
+ serializeState,
689
+ serializeEvent
690
+ } = resolvedOptions;
691
+ const adjacency = getAdjacencyMap(logic, resolvedOptions);
692
+ const stateMap = new Map();
693
+ const steps = [];
694
+ const serializedFromState = serializeState(fromState, undefined, undefined);
695
+ stateMap.set(serializedFromState, fromState);
696
+ let stateSerial = serializedFromState;
697
+ let state = fromState;
698
+ for (const event of events) {
699
+ steps.push({
700
+ state: stateMap.get(stateSerial),
701
+ event
702
+ });
703
+ const eventSerial = serializeEvent(event);
704
+ const {
705
+ state: nextState,
706
+ event: _nextEvent
707
+ } = adjacency[stateSerial].transitions[eventSerial];
708
+ if (!nextState) {
709
+ throw new Error(`Invalid transition from ${stateSerial} with ${eventSerial}`);
710
+ }
711
+ const prevState = stateMap.get(stateSerial);
712
+ const nextStateSerial = serializeState(nextState, event, prevState);
713
+ stateMap.set(nextStateSerial, nextState);
714
+ stateSerial = nextStateSerial;
715
+ state = nextState;
716
+ }
717
+
718
+ // If it is expected to reach a specific state (`toState`) and that state
719
+ // isn't reached, there are no paths
720
+ if (resolvedOptions.toState && !resolvedOptions.toState(state)) {
721
+ return [];
722
+ }
723
+ return [alterPath({
724
+ state,
725
+ steps,
726
+ weight: steps.length
727
+ })];
728
+ }
729
+
730
+ function getShortestPaths(logic, options) {
731
+ const resolvedOptions = resolveTraversalOptions(logic, options);
732
+ const serializeState = resolvedOptions.serializeState;
733
+ const fromState = resolvedOptions.fromState ?? logic.getInitialSnapshot(createMockActorScope(), options?.input);
734
+ const adjacency = getAdjacencyMap(logic, resolvedOptions);
735
+
736
+ // weight, state, event
737
+ const weightMap = new Map();
738
+ const stateMap = new Map();
739
+ const serializedFromState = serializeState(fromState, undefined, undefined);
740
+ stateMap.set(serializedFromState, fromState);
741
+ weightMap.set(serializedFromState, {
742
+ weight: 0,
743
+ state: undefined,
744
+ event: undefined
745
+ });
746
+ const unvisited = new Set();
747
+ const visited = new Set();
748
+ unvisited.add(serializedFromState);
749
+ for (const serializedState of unvisited) {
750
+ const prevState = stateMap.get(serializedState);
751
+ const {
752
+ weight
753
+ } = weightMap.get(serializedState);
754
+ for (const event of Object.keys(adjacency[serializedState].transitions)) {
755
+ const {
756
+ state: nextState,
757
+ event: eventObject
758
+ } = adjacency[serializedState].transitions[event];
759
+ const nextSerializedState = serializeState(nextState, eventObject, prevState);
760
+ stateMap.set(nextSerializedState, nextState);
761
+ if (!weightMap.has(nextSerializedState)) {
762
+ weightMap.set(nextSerializedState, {
763
+ weight: weight + 1,
764
+ state: serializedState,
765
+ event: eventObject
766
+ });
767
+ } else {
768
+ const {
769
+ weight: nextWeight
770
+ } = weightMap.get(nextSerializedState);
771
+ if (nextWeight > weight + 1) {
772
+ weightMap.set(nextSerializedState, {
773
+ weight: weight + 1,
774
+ state: serializedState,
775
+ event: eventObject
776
+ });
777
+ }
778
+ }
779
+ if (!visited.has(nextSerializedState)) {
780
+ unvisited.add(nextSerializedState);
781
+ }
782
+ }
783
+ visited.add(serializedState);
784
+ unvisited.delete(serializedState);
785
+ }
786
+ const statePlanMap = {};
787
+ const paths = [];
788
+ weightMap.forEach(({
789
+ weight,
790
+ state: fromState,
791
+ event: fromEvent
792
+ }, stateSerial) => {
793
+ const state = stateMap.get(stateSerial);
794
+ const steps = !fromState ? [] : statePlanMap[fromState].paths[0].steps.concat({
795
+ state: stateMap.get(fromState),
796
+ event: fromEvent
797
+ });
798
+ paths.push({
799
+ state,
800
+ steps,
801
+ weight
802
+ });
803
+ statePlanMap[stateSerial] = {
804
+ state,
805
+ paths: [{
806
+ state,
807
+ steps,
808
+ weight
809
+ }]
810
+ };
811
+ });
812
+ if (resolvedOptions.toState) {
813
+ return paths.filter(path => resolvedOptions.toState(path.state)).map(alterPath);
814
+ }
815
+ return paths.map(alterPath);
816
+ }
817
+
818
+ function getSimplePaths(logic, options) {
819
+ const resolvedOptions = resolveTraversalOptions(logic, options);
820
+ const actorScope = createMockActorScope();
821
+ const fromState = resolvedOptions.fromState ?? logic.getInitialSnapshot(actorScope, options?.input);
822
+ const serializeState = resolvedOptions.serializeState;
823
+ const adjacency = getAdjacencyMap(logic, resolvedOptions);
824
+ const stateMap = new Map();
825
+ const visitCtx = {
826
+ vertices: new Set(),
827
+ edges: new Set()
828
+ };
829
+ const steps = [];
830
+ const pathMap = {};
831
+ function util(fromStateSerial, toStateSerial) {
832
+ const fromState = stateMap.get(fromStateSerial);
833
+ visitCtx.vertices.add(fromStateSerial);
834
+ if (fromStateSerial === toStateSerial) {
835
+ if (!pathMap[toStateSerial]) {
836
+ pathMap[toStateSerial] = {
837
+ state: stateMap.get(toStateSerial),
838
+ paths: []
839
+ };
840
+ }
841
+ const toStatePlan = pathMap[toStateSerial];
842
+ const path2 = {
843
+ state: fromState,
844
+ weight: steps.length,
845
+ steps: [...steps]
846
+ };
847
+ toStatePlan.paths.push(path2);
848
+ } else {
849
+ for (const serializedEvent of Object.keys(adjacency[fromStateSerial].transitions)) {
850
+ const {
851
+ state: nextState,
852
+ event: subEvent
853
+ } = adjacency[fromStateSerial].transitions[serializedEvent];
854
+ if (!(serializedEvent in adjacency[fromStateSerial].transitions)) {
855
+ continue;
856
+ }
857
+ const prevState = stateMap.get(fromStateSerial);
858
+ const nextStateSerial = serializeState(nextState, subEvent, prevState);
859
+ stateMap.set(nextStateSerial, nextState);
860
+ if (!visitCtx.vertices.has(nextStateSerial)) {
861
+ visitCtx.edges.add(serializedEvent);
862
+ steps.push({
863
+ state: stateMap.get(fromStateSerial),
864
+ event: subEvent
865
+ });
866
+ util(nextStateSerial, toStateSerial);
867
+ }
868
+ }
869
+ }
870
+ steps.pop();
871
+ visitCtx.vertices.delete(fromStateSerial);
872
+ }
873
+ const fromStateSerial = serializeState(fromState, undefined);
874
+ stateMap.set(fromStateSerial, fromState);
875
+ for (const nextStateSerial of Object.keys(adjacency)) {
876
+ util(fromStateSerial, nextStateSerial);
877
+ }
878
+ const simplePaths = Object.values(pathMap).flatMap(p => p.paths);
879
+ if (resolvedOptions.toState) {
880
+ return simplePaths.filter(path => resolvedOptions.toState(path.state)).map(alterPath);
881
+ }
882
+ return simplePaths.map(alterPath);
883
+ }
884
+
885
+ export { TestModel, adjacencyMapToArray, createShortestPathsGen, createSimplePathsGen, createTestModel, getAdjacencyMap, getPathsFromEvents, getShortestPaths, getSimplePaths, getStateNodes, joinPaths, serializeSnapshot, toDirectedGraph };