xstate 3.2.1 → 3.3.3

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 (55) hide show
  1. package/.vscode/launch.json +15 -13
  2. package/README.md +37 -9
  3. package/dist/xstate.js +1 -1
  4. package/dist/xstate.utils.js +1 -1
  5. package/es/Machine.d.ts +2 -2
  6. package/es/Machine.js +2 -2
  7. package/es/State.d.ts +8 -7
  8. package/es/State.js +3 -2
  9. package/es/StateNode.d.ts +50 -13
  10. package/es/StateNode.js +617 -412
  11. package/es/graph.d.ts +9 -6
  12. package/es/graph.js +31 -24
  13. package/es/patterns.js +1 -1
  14. package/es/scxml.d.ts +2 -1
  15. package/es/scxml.js +33 -10
  16. package/es/types.d.ts +38 -7
  17. package/es/utils.d.ts +14 -1
  18. package/es/utils.js +33 -5
  19. package/lib/Machine.d.ts +2 -2
  20. package/lib/Machine.js +2 -2
  21. package/lib/State.d.ts +8 -7
  22. package/lib/State.js +3 -2
  23. package/lib/StateNode.d.ts +50 -13
  24. package/lib/StateNode.js +616 -411
  25. package/lib/graph.d.ts +9 -6
  26. package/lib/graph.js +30 -22
  27. package/lib/patterns.js +1 -1
  28. package/lib/scxml.d.ts +2 -1
  29. package/lib/scxml.js +33 -10
  30. package/lib/types.d.ts +38 -7
  31. package/lib/utils.d.ts +14 -1
  32. package/lib/utils.js +35 -5
  33. package/package.json +3 -3
  34. package/src/Machine.ts +5 -3
  35. package/src/State.ts +10 -2
  36. package/src/StateNode.ts +966 -590
  37. package/src/graph.ts +60 -31
  38. package/src/scxml.ts +80 -49
  39. package/src/types.ts +48 -7
  40. package/src/utils.ts +52 -7
  41. package/test/actions.test.ts +24 -1
  42. package/test/activities.test.ts +165 -0
  43. package/test/deep.test.ts +14 -16
  44. package/test/deterministic.test.ts +26 -5
  45. package/test/examples/6.17.test.ts +64 -0
  46. package/test/fixtures/id.ts +1 -1
  47. package/test/graph.test.ts +39 -16
  48. package/test/guards.test.ts +172 -15
  49. package/test/history.test.ts +193 -58
  50. package/test/invalid.test.ts +48 -0
  51. package/test/multiple.test.ts +12 -18
  52. package/test/parallel.test.ts +472 -1
  53. package/test/scxml.test.ts +13 -4
  54. package/test/stateIn.test.ts +1 -1
  55. package/test/transient.test.ts +183 -1
package/src/graph.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { StateNode } from './index';
2
- import { toStateValue, getActionType } from './utils';
2
+ import { toStateValue, getActionType, flatMap } from './utils';
3
3
  import {
4
4
  StateValue,
5
5
  Machine,
@@ -16,37 +16,51 @@ const EMPTY_MAP = {};
16
16
 
17
17
  export function getNodes(node: StateNode): StateNode[] {
18
18
  const { states } = node;
19
- const nodes = Object.keys(
20
- states
21
- ).reduce((accNodes: StateNode[], stateKey) => {
22
- const subState = states[stateKey];
23
- const subNodes = getNodes(states[stateKey]);
19
+ const nodes = Object.keys(states).reduce(
20
+ (accNodes: StateNode[], stateKey) => {
21
+ const subState = states[stateKey];
22
+ const subNodes = getNodes(states[stateKey]);
24
23
 
25
- accNodes.push(subState, ...subNodes);
26
- return accNodes;
27
- }, []);
24
+ accNodes.push(subState, ...subNodes);
25
+ return accNodes;
26
+ },
27
+ []
28
+ );
28
29
 
29
30
  return nodes;
30
31
  }
31
32
 
32
- function getEventEdges(node: StateNode, event: string): Edge[] {
33
+ export function getEventEdges(node: StateNode, event: string): Edge[] {
33
34
  const transitions = node.on[event]!;
34
35
 
35
- return transitions.map(transition => {
36
- return {
37
- source: node,
38
- target: node.parent!.getState(transition.target)!,
39
- event,
40
- actions: transition.actions ? transition.actions.map(getActionType) : [],
41
- cond: transition.cond
42
- };
43
- });
36
+ return flatMap(
37
+ transitions.map(transition => {
38
+ const targets = ([] as string[]).concat(transition.target);
39
+ return targets.map(target => {
40
+ const targetNode = node.getRelativeStateNodes(
41
+ target,
42
+ undefined,
43
+ false
44
+ )[0];
45
+ return {
46
+ source: node,
47
+ target: targetNode,
48
+ event,
49
+ actions: transition.actions
50
+ ? transition.actions.map(getActionType)
51
+ : [],
52
+ cond: transition.cond
53
+ };
54
+ });
55
+ })
56
+ );
44
57
  }
45
58
 
46
- export function getEdges(node: StateNode): Edge[] {
59
+ export function getEdges(node: StateNode, options?: { deep: boolean }): Edge[] {
60
+ const { deep = true } = options || {};
47
61
  const edges: Edge[] = [];
48
62
 
49
- if (node.states) {
63
+ if (node.states && deep) {
50
64
  Object.keys(node.states).forEach(stateKey => {
51
65
  edges.push(...getEdges(node.states[stateKey]));
52
66
  });
@@ -59,7 +73,10 @@ export function getEdges(node: StateNode): Edge[] {
59
73
  return edges;
60
74
  }
61
75
 
62
- export function getAdjacencyMap(node: Machine): AdjacencyMap {
76
+ export function getAdjacencyMap(
77
+ node: Machine,
78
+ extendedState?: any
79
+ ): AdjacencyMap {
63
80
  const adjacency: AdjacencyMap = {};
64
81
 
65
82
  const events = node.events;
@@ -74,7 +91,7 @@ export function getAdjacencyMap(node: Machine): AdjacencyMap {
74
91
  adjacency[stateKey] = {};
75
92
 
76
93
  for (const event of events) {
77
- const nextState = node.transition(stateValue, event);
94
+ const nextState = node.transition(stateValue, event, extendedState);
78
95
  adjacency[stateKey][event] = { state: nextState.value };
79
96
 
80
97
  findAdjacencies(nextState.value);
@@ -86,11 +103,14 @@ export function getAdjacencyMap(node: Machine): AdjacencyMap {
86
103
  return adjacency;
87
104
  }
88
105
 
89
- export function getShortestPaths(machine: Machine): PathMap {
106
+ export function getShortestPaths(
107
+ machine: Machine,
108
+ extendedState?: any
109
+ ): PathMap {
90
110
  if (!machine.states) {
91
111
  return EMPTY_MAP;
92
112
  }
93
- const adjacency = getAdjacencyMap(machine);
113
+ const adjacency = getAdjacencyMap(machine, extendedState);
94
114
  const initialStateId = JSON.stringify(machine.initialState.value);
95
115
  const pathMap: PathMap = {
96
116
  [initialStateId]: []
@@ -148,20 +168,26 @@ export function getShortestPaths(machine: Machine): PathMap {
148
168
  return pathMap;
149
169
  }
150
170
 
151
- export function getShortestPathsAsArray(machine: Machine): PathItem[] {
152
- const result = getShortestPaths(machine);
171
+ export function getShortestPathsAsArray(
172
+ machine: Machine,
173
+ extendedState?: any
174
+ ): PathItem[] {
175
+ const result = getShortestPaths(machine, extendedState);
153
176
  return Object.keys(result).map(key => ({
154
177
  state: JSON.parse(key),
155
178
  path: result[key]
156
179
  }));
157
180
  }
158
181
 
159
- export function getSimplePaths(machine: Machine): PathsMap {
182
+ export function getSimplePaths(
183
+ machine: Machine,
184
+ extendedState?: any
185
+ ): PathsMap {
160
186
  if (!machine.states) {
161
187
  return EMPTY_MAP;
162
188
  }
163
189
 
164
- const adjacency = getAdjacencyMap(machine);
190
+ const adjacency = getAdjacencyMap(machine, extendedState);
165
191
  const visited = new Set();
166
192
  const path: Segment[] = [];
167
193
  const paths: PathsMap = {};
@@ -202,8 +228,11 @@ export function getSimplePaths(machine: Machine): PathsMap {
202
228
  return paths;
203
229
  }
204
230
 
205
- export function getSimplePathsAsArray(machine: Machine): PathsItem[] {
206
- const result = getSimplePaths(machine);
231
+ export function getSimplePathsAsArray(
232
+ machine: Machine,
233
+ extendedState?: any
234
+ ): PathsItem[] {
235
+ const result = getSimplePaths(machine, extendedState);
207
236
  return Object.keys(result).map(key => ({
208
237
  state: JSON.parse(key),
209
238
  paths: result[key]
package/src/scxml.ts CHANGED
@@ -62,25 +62,54 @@ function stateNodeToSCXML(stateNode: StateNode) {
62
62
  return stateNodeToSCXML(subStateNode);
63
63
  }),
64
64
  ...Object.keys(stateNode.on)
65
- .map((event): XMLElement[] => {
66
- const transition = stateNode.on![event];
65
+ .map(
66
+ (event): XMLElement[] => {
67
+ const transition = stateNode.on![event];
67
68
 
68
- if (!transition) {
69
- return [];
70
- }
69
+ if (!transition) {
70
+ return [];
71
+ }
72
+
73
+ if (Array.isArray(transition)) {
74
+ return transition.map(targetTransition => {
75
+ return {
76
+ type: 'element',
77
+ name: 'transition',
78
+ attributes: {
79
+ ...(event ? { event } : undefined),
80
+ target: stateNode.parent!.getRelativeStateNodes(
81
+ targetTransition.target
82
+ )[0]!.id, // TODO: fixme
83
+ ...(targetTransition.cond
84
+ ? { cond: targetTransition.cond.toString() }
85
+ : undefined)
86
+ },
87
+ elements: targetTransition.actions
88
+ ? targetTransition.actions.map(action => ({
89
+ type: 'element',
90
+ name: 'send',
91
+ attributes: {
92
+ event: getActionType(action)
93
+ }
94
+ }))
95
+ : undefined
96
+ };
97
+ });
98
+ }
99
+
100
+ return Object.keys(transition).map(target => {
101
+ const targetTransition = transition[target];
71
102
 
72
- if (Array.isArray(transition)) {
73
- return transition.map(targetTransition => {
74
103
  return {
75
104
  type: 'element',
76
105
  name: 'transition',
77
106
  attributes: {
78
- ...event ? { event } : undefined,
79
- target: stateNode.parent!.getState(targetTransition.target)!
80
- .id,
81
- ...targetTransition.cond
107
+ ...(event ? { event } : undefined),
108
+ target: stateNode.parent!.getRelativeStateNodes(target)![0]
109
+ .id, // TODO: fixme
110
+ ...(targetTransition.cond
82
111
  ? { cond: targetTransition.cond.toString() }
83
- : undefined
112
+ : undefined)
84
113
  },
85
114
  elements: targetTransition.actions
86
115
  ? targetTransition.actions.map(action => ({
@@ -94,32 +123,7 @@ function stateNodeToSCXML(stateNode: StateNode) {
94
123
  };
95
124
  });
96
125
  }
97
-
98
- return Object.keys(transition).map(target => {
99
- const targetTransition = transition[target];
100
-
101
- return {
102
- type: 'element',
103
- name: 'transition',
104
- attributes: {
105
- ...event ? { event } : undefined,
106
- target: stateNode.parent!.getState(target)!.id,
107
- ...targetTransition.cond
108
- ? { cond: targetTransition.cond.toString() }
109
- : undefined
110
- },
111
- elements: targetTransition.actions
112
- ? targetTransition.actions.map(action => ({
113
- type: 'element',
114
- name: 'send',
115
- attributes: {
116
- event: getActionType(action)
117
- }
118
- }))
119
- : undefined
120
- };
121
- });
122
- })
126
+ )
123
127
  .reduce((a, b) => a.concat(b))
124
128
  ].filter(Boolean) as XMLElement[]
125
129
  };
@@ -210,10 +214,37 @@ function toConfig(
210
214
  let initial = parallel ? undefined : nodeJson.attributes!.initial;
211
215
  let states: Record<string, any>;
212
216
  let on: Record<string, any>;
217
+ const { elements } = nodeJson;
218
+
219
+ switch (nodeJson.name) {
220
+ case 'history': {
221
+ if (!elements) {
222
+ return {
223
+ id,
224
+ history: nodeJson.attributes!.type || 'shallow'
225
+ };
226
+ }
227
+
228
+ const [transitionElement] = elements.filter(
229
+ element => element.name === 'transition'
230
+ );
231
+
232
+ return {
233
+ id,
234
+ history: nodeJson.attributes!.type || 'shallow',
235
+ target: `#${transitionElement.attributes!.target}`
236
+ };
237
+ }
238
+ default:
239
+ break;
240
+ }
213
241
 
214
242
  if (nodeJson.elements) {
215
243
  const stateElements = nodeJson.elements.filter(
216
- element => element.name === 'state' || element.name === 'parallel'
244
+ element =>
245
+ element.name === 'state' ||
246
+ element.name === 'parallel' ||
247
+ element.name === 'history'
217
248
  );
218
249
 
219
250
  const transitionElements = nodeJson.elements.filter(
@@ -248,12 +279,12 @@ function toConfig(
248
279
  (values: XMLElement[]) => {
249
280
  return values.map(value => ({
250
281
  target: `#${value.attributes!.target}`,
251
- ...value.elements ? executableContent(value.elements) : undefined,
252
- ...value.attributes!.cond
282
+ ...(value.elements ? executableContent(value.elements) : undefined),
283
+ ...(value.attributes!.cond
253
284
  ? {
254
285
  cond: evalCond(value.attributes!.cond as string)
255
286
  }
256
- : undefined
287
+ : undefined)
257
288
  }));
258
289
  }
259
290
  );
@@ -283,18 +314,18 @@ function toConfig(
283
314
  return {
284
315
  id,
285
316
  delimiter: options.delimiter,
286
- ...initial ? { initial } : undefined,
287
- ...parallel ? { parallel } : undefined,
288
- ...stateElements.length
317
+ ...(initial ? { initial } : undefined),
318
+ ...(parallel ? { parallel } : undefined),
319
+ ...(stateElements.length
289
320
  ? {
290
321
  states: mapValues(states, (state, key) =>
291
322
  toConfig(state, key, options)
292
323
  )
293
324
  }
294
- : undefined,
295
- ...transitionElements.length ? { on } : undefined,
296
- ...onEntry ? { onEntry } : undefined,
297
- ...onExit ? { onExit } : undefined
325
+ : undefined),
326
+ ...(transitionElements.length ? { on } : undefined),
327
+ ...(onEntry ? { onEntry } : undefined),
328
+ ...(onExit ? { onExit } : undefined)
298
329
  };
299
330
  }
300
331
 
package/src/types.ts CHANGED
@@ -25,12 +25,24 @@ export interface StateValueMap {
25
25
 
26
26
  export type StateValue = string | StateValueMap;
27
27
 
28
- export type Condition = (extendedState: any, event?: EventObject) => boolean;
28
+ export interface HistoryValue {
29
+ states: Record<string, HistoryValue | undefined>;
30
+ current: StateValue | undefined;
31
+ }
32
+
33
+ export type ConditionPredicate = (
34
+ extendedState: any,
35
+ event: EventObject,
36
+ microstepState: StateValue
37
+ ) => boolean;
38
+
39
+ export type Condition = string | ConditionPredicate;
29
40
 
30
41
  export interface TransitionConfig {
31
- cond?: (extendedState: any, event: EventObject) => boolean;
42
+ cond?: Condition;
32
43
  actions?: Action[];
33
44
  in?: StateValue;
45
+ internal?: boolean;
34
46
  }
35
47
 
36
48
  export interface TargetTransitionConfig extends TransitionConfig {
@@ -50,6 +62,12 @@ export interface StateNodeConfig {
50
62
  key?: string;
51
63
  initial?: string | undefined;
52
64
  parallel?: boolean | undefined;
65
+ /**
66
+ * Indicates whether the state node is a history state node, and what
67
+ * type of history:
68
+ * shallow, deep, true (shallow), false (none), undefined (none)
69
+ */
70
+ history?: 'shallow' | 'deep' | boolean | undefined;
53
71
  states?: Record<string, SimpleOrCompoundStateNodeConfig> | undefined;
54
72
  on?: Record<string, Transition | undefined>;
55
73
  onEntry?: Action | Action[];
@@ -67,19 +85,29 @@ export interface SimpleStateNodeConfig extends StateNodeConfig {
67
85
  states?: undefined;
68
86
  }
69
87
 
88
+ export interface HistoryStateNodeConfig extends SimpleStateNodeConfig {
89
+ history: 'shallow' | 'deep' | true;
90
+ target: StateValue | undefined;
91
+ }
92
+
70
93
  export interface CompoundStateNodeConfig extends StateNodeConfig {
71
94
  initial?: string;
72
95
  parallel?: boolean;
73
96
  states: Record<string, SimpleOrCompoundStateNodeConfig>;
97
+ history?: false | undefined;
74
98
  }
75
99
 
76
100
  export type SimpleOrCompoundStateNodeConfig =
77
101
  | CompoundStateNodeConfig
78
102
  | SimpleStateNodeConfig;
79
103
 
104
+ export interface MachineOptions {
105
+ guards: Record<string, ConditionPredicate>;
106
+ }
80
107
  export interface MachineConfig extends CompoundStateNodeConfig {
81
108
  key?: string;
82
109
  strict?: boolean;
110
+ history?: false | undefined;
83
111
  }
84
112
  export interface StandardMachineConfig extends MachineConfig {
85
113
  initial: string;
@@ -89,7 +117,6 @@ export interface StandardMachineConfig extends MachineConfig {
89
117
  export interface ParallelMachineConfig extends MachineConfig {
90
118
  initial?: undefined;
91
119
  parallel: true;
92
- states: Record<string, CompoundStateNodeConfig>;
93
120
  }
94
121
 
95
122
  export interface EntryExitEffectMap {
@@ -102,6 +129,8 @@ export interface StateNode {
102
129
  id: string;
103
130
  initial: string | undefined;
104
131
  parallel: boolean;
132
+ transient: boolean;
133
+ history: false | 'shallow' | 'deep';
105
134
  states: Record<string, StateNode>;
106
135
  on?: Record<string, Transition>;
107
136
  onEntry?: Action | Action[];
@@ -112,6 +141,7 @@ export interface StateNode {
112
141
 
113
142
  export interface ComplexStateNode extends StateNode {
114
143
  initial: string;
144
+ history: false;
115
145
  }
116
146
 
117
147
  export interface LeafStateNode extends StateNode {
@@ -121,6 +151,11 @@ export interface LeafStateNode extends StateNode {
121
151
  parent: StateNode;
122
152
  }
123
153
 
154
+ export interface HistoryStateNode extends StateNode {
155
+ history: 'shallow' | 'deep';
156
+ target: StateValue | undefined;
157
+ }
158
+
124
159
  export interface Machine extends StateNode {
125
160
  id: string;
126
161
  initial: string | undefined;
@@ -145,6 +180,11 @@ export interface ActionMap {
145
180
  onExit: Action[];
146
181
  }
147
182
 
183
+ export interface EntryExitStates {
184
+ entry: Set<StateNode>;
185
+ exit: Set<StateNode>;
186
+ }
187
+
148
188
  export interface ActivityMap {
149
189
  [activityKey: string]: boolean;
150
190
  }
@@ -154,11 +194,12 @@ export type MaybeStateValueActionsTuple = [
154
194
  ActivityMap | undefined
155
195
  ];
156
196
 
197
+ // tslint:disable-next-line:class-name
157
198
  export interface StateTransition {
158
- statePaths: string[][];
159
- actions: ActionMap;
160
- activities: ActivityMap | undefined;
161
- events: EventObject[];
199
+ value: StateValue | undefined;
200
+ entryExitStates: EntryExitStates | undefined;
201
+ actions: Action[];
202
+ paths: string[][];
162
203
  }
163
204
 
164
205
  export interface TransitionData {
package/src/utils.ts CHANGED
@@ -16,7 +16,9 @@ export function getActionType(action: Action): ActionType {
16
16
  try {
17
17
  return typeof action === 'string' || typeof action === 'number'
18
18
  ? `${action}`
19
- : typeof action === 'function' ? action.name : action.type;
19
+ : typeof action === 'function'
20
+ ? action.name
21
+ : action.type;
20
22
  } catch (e) {
21
23
  throw new Error(
22
24
  'Events must be strings or objects with a string event.type property.'
@@ -89,34 +91,73 @@ export function mapValues<T, P>(
89
91
  return result;
90
92
  }
91
93
 
94
+ export function mapFilterValues<T, P>(
95
+ collection: { [key: string]: T },
96
+ iteratee: (item: T, key: string, collection: { [key: string]: T }) => P,
97
+ predicate: (item: T) => boolean
98
+ ): { [key: string]: P } {
99
+ const result = {};
100
+
101
+ Object.keys(collection).forEach(key => {
102
+ const item = collection[key];
103
+
104
+ if (!predicate(item)) {
105
+ return;
106
+ }
107
+
108
+ result[key] = iteratee(item, key, collection);
109
+ });
110
+
111
+ return result;
112
+ }
113
+
92
114
  /**
93
115
  * Retrieves a value at the given path.
94
116
  * @param props The deep path to the prop of the desired value
95
117
  */
96
- export const path = (props: string[]): any => <T extends Record<string, any>>(
118
+ export const path = <T extends Record<string, any>>(props: string[]): any => (
97
119
  object: T
98
120
  ): any => {
99
- let result: Record<string, any> = object;
121
+ let result: T = object;
100
122
 
101
123
  for (const prop of props) {
102
- result = result[prop];
124
+ result = result[prop as keyof typeof result];
103
125
  }
104
126
 
105
127
  return result;
106
128
  };
107
129
 
130
+ /**
131
+ * Retrieves a value at the given path via the nested accessor prop.
132
+ * @param props The deep path to the prop of the desired value
133
+ */
134
+ export function nestedPath<T extends Record<string, any>>(
135
+ props: string[],
136
+ accessorProp: keyof T
137
+ ): (object: T) => T {
138
+ return object => {
139
+ let result: T = object;
140
+
141
+ for (const prop of props) {
142
+ result = result[accessorProp][prop];
143
+ }
144
+
145
+ return result;
146
+ };
147
+ }
148
+
108
149
  export const toStatePaths = (stateValue: StateValue): string[][] => {
109
150
  if (typeof stateValue === 'string') {
110
151
  return [[stateValue]];
111
152
  }
112
153
 
113
- const result = Object.keys(stateValue)
114
- .map(key => {
154
+ const result = flatMap(
155
+ Object.keys(stateValue).map(key => {
115
156
  return toStatePaths(stateValue[key]).map(subPath => {
116
157
  return [key].concat(subPath);
117
158
  });
118
159
  })
119
- .reduce((a, b) => a.concat(b), []);
160
+ );
120
161
 
121
162
  return result;
122
163
  };
@@ -145,3 +186,7 @@ export const pathsToStateValue = (paths: string[][]): StateValue => {
145
186
 
146
187
  return result;
147
188
  };
189
+
190
+ export const flatMap = <T>(array: T[][]): T[] => {
191
+ return array.reduce((a, b) => a.concat(b), []);
192
+ };
@@ -311,7 +311,7 @@ describe('onEntry/onExit actions', () => {
311
311
  assert.isEmpty(pingPong.transition('ping.foo', 'TACK').actions);
312
312
  });
313
313
 
314
- xit('with an absolute transition', () => {
314
+ it('with an absolute transition', () => {
315
315
  assert.isEmpty(
316
316
  pingPong.transition('ping.foo', 'ABSOLUTE_TACK').actions
317
317
  );
@@ -319,3 +319,26 @@ describe('onEntry/onExit actions', () => {
319
319
  });
320
320
  });
321
321
  });
322
+
323
+ describe('actions on invalid transition', () => {
324
+ const stopMachine = Machine({
325
+ initial: 'idle',
326
+ states: {
327
+ idle: {
328
+ on: {
329
+ STOP: {
330
+ stop: {
331
+ actions: ['action1']
332
+ }
333
+ }
334
+ }
335
+ },
336
+ stop: {}
337
+ }
338
+ });
339
+
340
+ it('should not recall previous actions', () => {
341
+ const nextState = stopMachine.transition('idle', 'STOP');
342
+ assert.isEmpty(stopMachine.transition(nextState, 'INVALID').actions);
343
+ });
344
+ });