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/StateNode.ts CHANGED
@@ -6,7 +6,11 @@ import {
6
6
  path,
7
7
  toStatePaths,
8
8
  pathsToStateValue,
9
- pathToStateValue
9
+ pathToStateValue,
10
+ getActionType,
11
+ flatMap,
12
+ mapFilterValues,
13
+ nestedPath
10
14
  } from './utils';
11
15
  import {
12
16
  Event,
@@ -17,18 +21,24 @@ import {
17
21
  StandardMachine,
18
22
  ParallelMachine,
19
23
  SimpleOrCompoundStateNodeConfig,
20
- MachineConfig,
21
24
  ParallelMachineConfig,
22
25
  EventType,
23
- ActionMap,
24
26
  StandardMachineConfig,
25
27
  TransitionConfig,
26
28
  ActivityMap,
27
- StateNodeConfig,
28
29
  Activity,
30
+ ConditionalTransitionConfig,
31
+ EntryExitStates,
32
+ TargetTransitionConfig,
29
33
  StateTransition,
34
+ ActionObject,
35
+ StateValueMap,
36
+ MachineOptions,
37
+ Condition,
38
+ ConditionPredicate,
30
39
  EventObject,
31
- ConditionalTransitionConfig
40
+ HistoryStateNodeConfig,
41
+ HistoryValue
32
42
  } from './types';
33
43
  import { matchesState } from './matchesState';
34
44
  import { State } from './State';
@@ -39,85 +49,22 @@ const HISTORY_KEY = '$history';
39
49
  const NULL_EVENT = '';
40
50
  const STATE_IDENTIFIER = '#';
41
51
  const isStateId = (str: string) => str[0] === STATE_IDENTIFIER;
42
- const emptyActions: ActionMap = Object.freeze({
43
- onEntry: [],
44
- onExit: [],
45
- actions: []
46
- });
47
-
48
- /**
49
- * Given a StateNode, walk up the parent chain until we find an
50
- * orthogonal region of a parallel state, or the top level machine
51
- * itself
52
- */
53
- const regionOf = (node: StateNode): StateNode => {
54
- // If we reach the top of the state machine, we're a "region".
55
- // If our parent is a parallel state, we're a region.
56
- while (node.parent && !node.parent.parallel) {
57
- node = node.parent;
58
- }
59
- return node;
52
+ const defaultOptions: MachineOptions = {
53
+ guards: {}
60
54
  };
61
55
 
62
- /**
63
- * Ensure that the passed in StateNode instance belongs to a region
64
- * that previously had not been used, or that matches the existing
65
- * StateNode for the orthogonal regions. This function is used to
66
- * verify that a transition that has multiple targets ends doesn't try
67
- * to target several states in the same orthogonal region. The passed
68
- * state is added to the regions data structure using the state's
69
- * _region_ (see regionOf), and the region's parent. If there is
70
- * already an object in the structure which is not already the state
71
- * in question, an Error is thrown, otherwise the state is added to
72
- * the structure, and the _region_ is returned.
73
- *
74
- * @param sourceState the state in which the event was triggered (used
75
- * to report error messages)
76
- * @param event the event that triggered the transition (used to
77
- * report error messages)
78
- * @param regions A data structure that retains the current set of
79
- * orthogonal regions (their IDs), grouped by their parallel state
80
- * (their IDs), with the values being the chosen states
81
- * @param state A state to add to the structure if possible.
82
- * @returns The region of the state, in order for the caller to repeat the process for the parent.
83
- * @throws Error if the region found already exists in the regions
84
- */
85
-
86
- const ensureTargetStateIsInCorrectRegion = (
87
- sourceState: StateNode,
88
- event: Event,
89
- regions: Record<string, Record<string, StateNode>>,
90
- stateToCheck: StateNode
91
- ): StateNode => {
92
- const region = regionOf(stateToCheck);
93
- const parent = region.parent;
94
- const parentId = parent ? parent.id : ''; // '' == machine
95
-
96
- regions[parentId] = regions[parentId] || {};
97
- if (
98
- regions[parentId][region.id] &&
99
- regions[parentId][region.id] !== stateToCheck
100
- ) {
101
- throw new Error(
102
- `Event '${event}' on state '${sourceState.id}' leads to an invalid configuration: ` +
103
- `Two or more states in the orthogonal region '${region.id}'.`
104
- );
105
- }
106
- // Keep track of which state was chosen in a particular region.
107
- regions[parentId][region.id] = stateToCheck;
108
- return region;
109
- };
110
-
111
- class StateNode implements StateNodeConfig {
56
+ class StateNode {
112
57
  public key: string;
113
58
  public id: string;
114
59
  public path: string[];
115
60
  public initial?: string;
116
61
  public parallel?: boolean;
62
+ public transient: boolean;
117
63
  public states: Record<string, StateNode>;
64
+ public history: false | 'shallow' | 'deep';
118
65
  public on: Record<string, ConditionalTransitionConfig>;
119
- public onEntry?: Action[];
120
- public onExit?: Action[];
66
+ public onEntry: Action[];
67
+ public onExit: Action[];
121
68
  public activities?: Activity[];
122
69
  public strict: boolean;
123
70
  public parent?: StateNode;
@@ -137,7 +84,8 @@ class StateNode implements StateNodeConfig {
137
84
  public config:
138
85
  | SimpleOrCompoundStateNodeConfig
139
86
  | StandardMachineConfig
140
- | ParallelMachineConfig
87
+ | ParallelMachineConfig,
88
+ public options: MachineOptions = defaultOptions
141
89
  ) {
142
90
  this.key = config.key || '(machine)';
143
91
  this.parent = config.parent;
@@ -154,35 +102,41 @@ class StateNode implements StateNodeConfig {
154
102
  this.initial = config.initial;
155
103
  this.parallel = !!config.parallel;
156
104
  this.states = (config.states
157
- ? mapValues<
158
- SimpleOrCompoundStateNodeConfig,
159
- StateNode
160
- >(config.states, (stateConfig, key) => {
161
- const stateNode = new StateNode({
162
- ...stateConfig,
163
- key,
164
- parent: this
165
- });
166
- Object.assign(this.idMap, {
167
- [stateNode.id]: stateNode,
168
- ...stateNode.idMap
169
- });
170
- return stateNode;
171
- })
105
+ ? mapValues<SimpleOrCompoundStateNodeConfig, StateNode>(
106
+ config.states,
107
+ (stateConfig, key) => {
108
+ const stateNode = new StateNode({
109
+ ...stateConfig,
110
+ key,
111
+ parent: this
112
+ });
113
+ Object.assign(this.idMap, {
114
+ [stateNode.id]: stateNode,
115
+ ...stateNode.idMap
116
+ });
117
+ return stateNode;
118
+ }
119
+ )
172
120
  : {}) as Record<string, StateNode>;
173
121
 
122
+ // History config
123
+ this.history =
124
+ config.history === true ? 'shallow' : config.history || false;
125
+
174
126
  this.on = config.on ? this.formatTransitions(config.on) : {};
127
+ this.transient = !!this.on[NULL_EVENT];
175
128
  this.strict = !!config.strict;
176
129
  this.onEntry = config.onEntry
177
130
  ? ([] as Action[]).concat(config.onEntry)
178
- : undefined;
179
- this.onExit = config.onExit
180
- ? ([] as Action[]).concat(config.onExit)
181
- : undefined;
131
+ : [];
132
+ this.onExit = config.onExit ? ([] as Action[]).concat(config.onExit) : [];
182
133
  this.data = config.data;
183
134
  this.activities = config.activities;
184
135
  }
185
136
  public getStateNodes(state: StateValue | State): StateNode[] {
137
+ if (!state) {
138
+ return [];
139
+ }
186
140
  const stateValue =
187
141
  state instanceof State
188
142
  ? state.value
@@ -219,411 +173,285 @@ class StateNode implements StateNodeConfig {
219
173
 
220
174
  return this.events.indexOf(eventType) !== -1;
221
175
  }
222
- public transition(
223
- state: StateValue | State,
176
+ private _transitionLeafNode(
177
+ stateValue: string,
178
+ state: State,
224
179
  event: Event,
225
180
  extendedState?: any
226
- ): State {
227
- const resolvedStateValue =
228
- typeof state === 'string'
229
- ? this.resolve(pathToStateValue(this.getResolvedPath(state)))
230
- : state instanceof State ? state : this.resolve(state);
231
-
232
- if (this.strict) {
233
- const eventType = getEventType(event);
234
- if (this.events.indexOf(eventType) === -1) {
235
- throw new Error(
236
- `Machine '${this.id}' does not accept event '${eventType}'`
237
- );
238
- }
239
- }
240
-
241
- const currentState = State.from(resolvedStateValue);
242
-
243
- const stateTransition = this.transitionStateValue(
244
- currentState,
245
- event,
246
- currentState,
247
- extendedState
248
- );
249
- let nextState = this.stateTransitionToState(stateTransition, currentState);
250
-
251
- if (!nextState) {
252
- return State.inert(currentState);
253
- }
254
-
255
- let maybeNextState: State | undefined = nextState;
256
-
257
- const raisedEvents = nextState.actions.filter(
258
- action => typeof action === 'object' && action.type === actionTypes.raise
259
- );
260
-
261
- if (raisedEvents.length) {
262
- const raisedEvent = (raisedEvents[0] as EventObject).event!;
263
-
264
- nextState = this.transition(nextState, raisedEvent, extendedState);
265
- nextState.actions.unshift(...nextState.actions);
266
- return nextState;
267
- }
181
+ ): StateTransition {
182
+ const stateNode = this.getStateNode(stateValue);
183
+ const next = stateNode._next(state, event, extendedState);
268
184
 
269
- if (stateTransition.events.length) {
270
- const raised =
271
- stateTransition.events[0].type === actionTypes.raise
272
- ? stateTransition.events[0].event!
273
- : undefined;
274
- const nullEvent = stateTransition.events[0].type === actionTypes.null;
185
+ if (!next.value) {
186
+ const { value, entryExitStates, actions, paths } = this._next(
187
+ state,
188
+ event,
189
+ extendedState
190
+ );
275
191
 
276
- if (raised || nullEvent) {
277
- maybeNextState = this.transition(
278
- nextState,
279
- nullEvent ? NULL_EVENT : raised,
280
- extendedState
281
- );
282
- maybeNextState.actions.unshift(...nextState.actions);
283
- return maybeNextState;
284
- }
192
+ return {
193
+ value,
194
+ entryExitStates: {
195
+ entry: entryExitStates ? entryExitStates.entry : new Set(),
196
+ exit: new Set<StateNode>([
197
+ stateNode,
198
+ ...(entryExitStates
199
+ ? Array.from(entryExitStates.exit)
200
+ : ([] as StateNode[]))
201
+ ])
202
+ },
203
+ actions,
204
+ paths
205
+ };
285
206
  }
286
207
 
287
- return nextState;
208
+ return next;
288
209
  }
289
- private stateTransitionToState(
290
- stateTransition: StateTransition,
291
- prevState: State
292
- ): State | undefined {
293
- const {
294
- statePaths: nextStatePaths,
295
- actions: nextActions,
296
- activities: nextActivities,
297
- events
298
- } = stateTransition;
299
-
300
- if (!nextStatePaths.length) {
301
- return undefined;
302
- }
303
-
304
- const prevActivities =
305
- prevState instanceof State ? prevState.activities : undefined;
306
-
307
- const activities = { ...prevActivities, ...nextActivities };
308
-
309
- const nextStateValue = this.resolve(pathsToStateValue(nextStatePaths));
310
- return new State(
311
- // next state value
312
- nextStateValue,
313
- // history
314
- State.from(prevState),
315
- // effects
316
- nextActions
317
- ? nextActions.onExit
318
- .concat(nextActions.actions)
319
- .concat(nextActions.onEntry)
320
- : [],
321
- // activities
322
- activities,
323
- // data
324
- this.getStateNodes(nextStateValue).reduce(
325
- (data, stateNode) => {
326
- if (stateNode.data !== undefined) {
327
- data[stateNode.id] = stateNode.data;
328
- }
210
+ private _transitionHierarchicalNode(
211
+ stateValue: StateValueMap,
212
+ state: State,
213
+ event: Event,
214
+ extendedState?: any
215
+ ): StateTransition {
216
+ const subStateKeys = Object.keys(stateValue);
329
217
 
330
- return data;
331
- },
332
- {} as Record<string, any>
333
- ),
334
- events
218
+ const stateNode = this.getStateNode(subStateKeys[0]);
219
+ const next = stateNode._transition(
220
+ stateValue[subStateKeys[0]],
221
+ state,
222
+ event,
223
+ extendedState
335
224
  );
336
- }
337
- public getStateNode(stateKey: string): StateNode {
338
- if (isStateId(stateKey)) {
339
- return this.machine.getStateNodeById(stateKey);
340
- }
341
225
 
342
- if (!this.states) {
343
- throw new Error(
344
- `Unable to retrieve child state '${stateKey}' from '${this
345
- .id}'; no child states exist.`
346
- );
347
- }
348
-
349
- const result = this.states[stateKey];
350
- if (!result) {
351
- throw new Error(
352
- `Child state '${stateKey}' does not exist on '${this.id}'`
226
+ if (!next.value) {
227
+ const { value, entryExitStates, actions, paths } = this._next(
228
+ state,
229
+ event,
230
+ extendedState
353
231
  );
354
- }
355
-
356
- return result;
357
- }
358
- public getStateNodeById(stateId: string): StateNode {
359
- const resolvedStateId = isStateId(stateId)
360
- ? stateId.slice(STATE_IDENTIFIER.length)
361
- : stateId;
362
- const stateNode = this.idMap[resolvedStateId];
363
232
 
364
- if (!stateNode) {
365
- throw new Error(
366
- `Substate '#${resolvedStateId}' does not exist on '${this.id}'`
367
- );
233
+ return {
234
+ value,
235
+ entryExitStates: {
236
+ entry: entryExitStates ? entryExitStates.entry : new Set(),
237
+ exit: new Set<StateNode>([
238
+ ...(next.entryExitStates
239
+ ? Array.from(next.entryExitStates.exit)
240
+ : []),
241
+ stateNode,
242
+ ...(entryExitStates
243
+ ? Array.from(entryExitStates.exit)
244
+ : ([] as StateNode[]))
245
+ ])
246
+ },
247
+ actions,
248
+ paths
249
+ };
368
250
  }
369
251
 
370
- return stateNode;
252
+ return next;
371
253
  }
372
- private resolve(stateValue: StateValue): StateValue {
373
- if (typeof stateValue === 'string') {
374
- const subStateNode = this.getStateNode(stateValue);
375
- return subStateNode.initial
376
- ? { [stateValue]: subStateNode.initialStateValue! }
377
- : stateValue;
378
- }
379
-
380
- if (this.parallel) {
381
- return mapValues(
382
- this.initialStateValue as Record<string, StateValue>,
383
- (subStateValue, subStateKey) => {
384
- return this.getStateNode(subStateKey).resolve(
385
- stateValue[subStateKey] || subStateValue
386
- );
387
- }
388
- );
389
- }
390
-
391
- return mapValues(stateValue, (subStateValue, subStateKey) => {
392
- return this.getStateNode(subStateKey).resolve(subStateValue);
393
- });
394
- }
395
- private transitionStateValue(
254
+ private _transitionOrthogonalNode(
255
+ stateValue: StateValueMap,
396
256
  state: State,
397
257
  event: Event,
398
- fullState: State,
399
258
  extendedState?: any
400
259
  ): StateTransition {
401
- const { history } = state;
402
- const stateValue = state.value;
260
+ const noTransitionKeys: string[] = [];
261
+ const transitionMap: Record<string, StateTransition> = {};
403
262
 
404
- if (typeof stateValue === 'string') {
405
- const subStateNode = this.getStateNode(stateValue);
263
+ Object.keys(stateValue).forEach(subStateKey => {
264
+ const subStateValue = stateValue[subStateKey];
406
265
 
407
- const result = subStateNode.next(
266
+ if (!subStateValue) {
267
+ return;
268
+ }
269
+
270
+ const next = this.getStateNode(subStateKey)._transition(
271
+ subStateValue,
272
+ state,
408
273
  event,
409
- fullState,
410
- history ? history.value : undefined,
411
274
  extendedState
412
275
  );
413
276
 
414
- // If a machine substate returns no potential transitions,
415
- // check on the machine itself.
416
- if (!result.statePaths.length && !this.parent) {
417
- return this.next(
418
- event,
419
- fullState,
420
- history ? history.value : undefined,
421
- extendedState
422
- );
277
+ if (!next.value) {
278
+ noTransitionKeys.push(subStateKey);
423
279
  }
424
280
 
425
- return result;
426
- }
427
-
428
- // Potential transition tuples from parent state nodes
429
- const potentialStateTransitions: StateTransition[] = [];
430
- let willTransition = false;
431
-
432
- let nextStateTransitionMap = mapValues(
433
- stateValue,
434
- (subStateValue, subStateKey) => {
435
- const subStateNode = this.getStateNode(subStateKey);
436
- const subHistory = history ? history.value[subStateKey] : undefined;
437
- const subState = new State(
438
- subStateValue,
439
- subHistory ? State.from(subHistory) : undefined
440
- );
441
- const subStateTransition = subStateNode.transitionStateValue(
442
- subState,
443
- event,
444
- fullState,
445
- extendedState
446
- );
447
-
448
- if (!subStateTransition.statePaths.length) {
449
- potentialStateTransitions.push(
450
- subStateNode.next(
451
- event,
452
- fullState,
453
- history ? history.value : undefined,
454
- extendedState
455
- )
456
- );
457
- } else {
458
- willTransition = true;
459
- }
281
+ transitionMap[subStateKey] = next;
282
+ });
460
283
 
461
- return subStateTransition;
462
- }
284
+ const willTransition = Object.keys(transitionMap).some(
285
+ key => transitionMap[key].value !== undefined
463
286
  );
464
287
 
465
288
  if (!willTransition) {
466
- if (this.parallel) {
467
- if (potentialStateTransitions.length) {
468
- // Select the first potential state transition to take
469
- return potentialStateTransitions[0];
470
- }
471
-
472
- return {
473
- statePaths: [],
474
- actions: emptyActions,
475
- activities: undefined,
476
- events: []
477
- };
478
- }
479
-
480
- const [subStateKey] = Object.keys(nextStateTransitionMap);
481
-
482
- // try with parent
483
- const {
484
- statePaths: parentStatePaths,
485
- actions: parentNextActions,
486
- activities: parentActivities
487
- } = this.getStateNode(subStateKey).next(
289
+ const { value, entryExitStates, actions, paths } = this._next(
290
+ state,
488
291
  event,
489
- fullState,
490
- history ? history.value : undefined,
491
292
  extendedState
492
293
  );
493
294
 
494
- const nextActions = nextStateTransitionMap[subStateKey].actions;
495
- const activities = nextStateTransitionMap[subStateKey].activities;
496
-
497
- const allActivities = {
498
- ...activities,
499
- ...parentActivities
295
+ return {
296
+ value,
297
+ entryExitStates: {
298
+ entry: entryExitStates ? entryExitStates.entry : new Set(),
299
+ exit: new Set([
300
+ ...Object.keys(this.states).map(key => this.states[key]),
301
+ ...(entryExitStates ? Array.from(entryExitStates.exit) : [])
302
+ ])
303
+ },
304
+ actions,
305
+ paths
500
306
  };
307
+ }
501
308
 
502
- const allActions = parentNextActions
503
- ? nextActions
504
- ? {
505
- onEntry: [...nextActions.onEntry, ...parentNextActions.onEntry],
506
- actions: [...nextActions.actions, ...parentNextActions.actions],
507
- onExit: [...nextActions.onExit, ...parentNextActions.onExit]
508
- }
509
- : parentNextActions
510
- : nextActions;
309
+ const allPaths = flatMap(
310
+ Object.keys(transitionMap).map(key => transitionMap[key].paths)
311
+ );
511
312
 
313
+ // External transition that escapes orthogonal region
314
+ if (
315
+ allPaths.length === 1 &&
316
+ !matchesState(pathToStateValue(this.path), pathToStateValue(allPaths[0]))
317
+ ) {
512
318
  return {
513
- statePaths: parentStatePaths,
514
- actions: allActions,
515
- activities: allActivities,
516
- events: []
319
+ value: this.machine.resolve(pathsToStateValue(allPaths)),
320
+ entryExitStates: Object.keys(transitionMap)
321
+ .map(key => transitionMap[key].entryExitStates)
322
+ .reduce(
323
+ (allEntryExitStates, entryExitStates) => {
324
+ const { entry, exit } = entryExitStates!;
325
+
326
+ return {
327
+ entry: new Set([
328
+ ...Array.from(allEntryExitStates!.entry),
329
+ ...Array.from(entry)
330
+ ]),
331
+ exit: new Set([
332
+ ...Array.from(allEntryExitStates!.exit),
333
+ ...Array.from(exit)
334
+ ])
335
+ };
336
+ },
337
+ { entry: new Set(), exit: new Set() } as EntryExitStates
338
+ ),
339
+ actions: flatMap(
340
+ Object.keys(transitionMap).map(key => {
341
+ return transitionMap[key].actions;
342
+ })
343
+ ),
344
+ paths: allPaths
517
345
  };
518
346
  }
519
347
 
520
- if (this.parallel) {
521
- nextStateTransitionMap = {
522
- ...mapValues(
523
- this.initialState.value as Record<string, StateValue>,
524
- (subStateValue, key) => {
525
- const subStateTransition = nextStateTransitionMap[key];
526
- return {
527
- statePaths:
528
- subStateTransition && subStateTransition.statePaths.length
529
- ? subStateTransition.statePaths
530
- : toStatePaths(
531
- stateValue[key] || subStateValue
532
- ).map(subPath => [
533
- ...this.getStateNode(key).path,
534
- ...subPath
535
- ]),
536
- actions:
537
- subStateTransition && subStateTransition.actions
538
- ? subStateTransition.actions
539
- : {
540
- onEntry: [],
541
- onExit: [],
542
- actions: []
543
- },
544
- activities: undefined,
545
- events: []
546
- };
547
- }
548
- )
549
- };
550
- }
348
+ const allResolvedPaths = flatMap(
349
+ Object.keys(transitionMap).map(key => {
350
+ const transition = transitionMap[key];
351
+ const value = transition.value || state.value;
551
352
 
552
- const finalActions: ActionMap = {
553
- onEntry: [],
554
- actions: [],
555
- onExit: []
556
- };
557
- const finalActivities: ActivityMap = {};
558
- mapValues(nextStateTransitionMap, subStateTransition => {
559
- const {
560
- // statePaths: nextSubStatePaths,
561
- actions: nextSubActions,
562
- activities: nextSubActivities
563
- } = subStateTransition;
564
- if (nextSubActions) {
565
- if (nextSubActions.onEntry) {
566
- finalActions.onEntry.push(...nextSubActions.onEntry);
567
- }
568
- if (nextSubActions.actions) {
569
- finalActions.actions.push(...nextSubActions.actions);
570
- }
571
- if (nextSubActions.onExit) {
572
- finalActions.onExit.push(...nextSubActions.onExit);
573
- }
574
- }
575
- if (nextSubActivities) {
576
- Object.assign(finalActivities, nextSubActivities);
577
- }
578
- });
353
+ return toStatePaths(path(this.path)(value)[key]).map(statePath =>
354
+ this.path.concat(key, statePath)
355
+ );
356
+ })
357
+ );
358
+
359
+ const nextStateValue = this.machine.resolve(
360
+ pathsToStateValue(allResolvedPaths)
361
+ );
579
362
 
580
363
  return {
581
- statePaths: Object.keys(nextStateTransitionMap)
582
- .map(stateKey => nextStateTransitionMap[stateKey].statePaths)
583
- .reduce((a, b) => a.concat(b), [] as string[][]),
584
- actions: finalActions,
585
- activities: finalActivities,
586
- events: []
364
+ value: nextStateValue,
365
+ entryExitStates: Object.keys(transitionMap).reduce(
366
+ (allEntryExitStates, key) => {
367
+ const { value: subStateValue, entryExitStates } = transitionMap[key];
368
+
369
+ // If the event was not handled (no subStateValue),
370
+ // machine should still be in state without reentry/exit.
371
+ if (!subStateValue || !entryExitStates) {
372
+ return allEntryExitStates;
373
+ }
374
+
375
+ const { entry, exit } = entryExitStates;
376
+
377
+ return {
378
+ entry: new Set([
379
+ ...Array.from(allEntryExitStates.entry),
380
+ ...Array.from(entry)
381
+ ]),
382
+ exit: new Set([
383
+ ...Array.from(allEntryExitStates.exit),
384
+ ...Array.from(exit)
385
+ ])
386
+ };
387
+ },
388
+ { entry: new Set(), exit: new Set() } as EntryExitStates
389
+ ),
390
+ actions: flatMap(
391
+ Object.keys(transitionMap).map(key => {
392
+ return transitionMap[key].actions;
393
+ })
394
+ ),
395
+ paths: toStatePaths(nextStateValue)
587
396
  };
588
397
  }
398
+ public _transition(
399
+ stateValue: StateValue,
400
+ state: State,
401
+ event: Event,
402
+ extendedState?: any
403
+ ): StateTransition {
404
+ // leaf node
405
+ if (typeof stateValue === 'string') {
406
+ return this._transitionLeafNode(stateValue, state, event, extendedState);
407
+ }
408
+
409
+ // hierarchical node
410
+ if (Object.keys(stateValue).length === 1) {
411
+ return this._transitionHierarchicalNode(
412
+ stateValue,
413
+ state,
414
+ event,
415
+ extendedState
416
+ );
417
+ }
589
418
 
590
- private next(
419
+ // orthogonal node
420
+ return this._transitionOrthogonalNode(
421
+ stateValue,
422
+ state,
423
+ event,
424
+ extendedState
425
+ );
426
+ }
427
+ private _next(
428
+ state: State,
591
429
  event: Event,
592
- fullState: State,
593
- history?: StateValue,
594
430
  extendedState?: any
595
431
  ): StateTransition {
596
432
  const eventType = getEventType(event);
597
- const actionMap: ActionMap = { onEntry: [], onExit: [], actions: [] };
598
- const activityMap: ActivityMap = {};
599
433
  const candidates = this.on[eventType];
434
+ const actions: Action[] = this.transient
435
+ ? [{ type: actionTypes.null }]
436
+ : [];
600
437
 
601
- if (this.onExit) {
602
- actionMap.onExit = this.onExit;
603
- }
604
- if (this.activities) {
605
- this.activities.forEach(activity => {
606
- activityMap[getEventType(activity)] = false;
607
- actionMap.onExit = actionMap.onExit.concat(stop(activity));
608
- });
609
- }
610
-
611
- if (!candidates) {
438
+ if (!candidates || !candidates.length) {
612
439
  return {
613
- statePaths: [],
614
- actions: actionMap,
615
- activities: activityMap,
616
- events: []
440
+ value: undefined,
441
+ entryExitStates: undefined,
442
+ actions,
443
+ paths: []
617
444
  };
618
445
  }
619
446
 
620
447
  let nextStateStrings: string[] = [];
448
+ let selectedTransition: TargetTransitionConfig;
621
449
 
622
450
  for (const candidate of candidates) {
623
451
  const {
624
452
  cond,
625
- in: stateIn,
626
- actions: transitionActions
453
+ in: stateIn
454
+ // actions: transitionActions
627
455
  } = candidate as TransitionConfig;
628
456
  const extendedStateObject = extendedState || {};
629
457
  const eventObject = toEventObject(event);
@@ -631,172 +459,450 @@ class StateNode implements StateNodeConfig {
631
459
  const isInState = stateIn
632
460
  ? matchesState(
633
461
  toStateValue(stateIn, this.delimiter),
634
- path(this.path.slice(0, -2))(fullState.value)
462
+ path(this.path.slice(0, -2))(state.value)
635
463
  )
636
464
  : true;
637
465
 
638
466
  if (
639
- (!cond || cond(extendedStateObject, eventObject)) &&
467
+ (!cond ||
468
+ this._evaluateCond(
469
+ cond,
470
+ extendedStateObject,
471
+ eventObject,
472
+ state.value
473
+ )) &&
640
474
  (!stateIn || isInState)
641
475
  ) {
642
476
  nextStateStrings = Array.isArray(candidate.target)
643
477
  ? candidate.target
644
478
  : [candidate.target];
645
- if (transitionActions) {
646
- actionMap.actions = actionMap.actions.concat(transitionActions);
647
- }
479
+ actions.push(...(candidate.actions ? candidate.actions : [])); // TODO: fixme;
480
+ selectedTransition = candidate;
648
481
  break;
649
482
  }
650
483
  }
651
484
 
652
485
  if (nextStateStrings.length === 0) {
653
486
  return {
654
- statePaths: [],
655
- actions: actionMap,
656
- activities: activityMap,
657
- events: []
487
+ value: undefined,
488
+ entryExitStates: undefined,
489
+ actions,
490
+ paths: []
658
491
  };
659
492
  }
660
493
 
661
- const finalPaths: string[][] = [];
662
- const raisedEvents: Action[] = [];
663
- const usedRegions: Record<string, Record<string, StateNode>> = {};
494
+ const nextStateNodes = flatMap(
495
+ nextStateStrings.map(str =>
496
+ this.getRelativeStateNodes(str, state.historyValue)
497
+ )
498
+ );
664
499
 
665
- nextStateStrings.forEach(nextStateString => {
666
- const nextStatePath = this.getResolvedPath(nextStateString);
667
- let currentState = isStateId(nextStateString)
668
- ? this.machine
669
- : this.parent;
670
- let currentHistory = history;
671
- let currentPath = this.key;
500
+ const nextStatePaths = nextStateNodes.map(stateNode => stateNode.path);
672
501
 
673
- nextStatePath.forEach(subPath => {
674
- if (subPath === '') {
675
- actionMap.onExit = [];
676
- currentState = this;
677
- return;
678
- }
502
+ const entryExitStates = nextStateNodes.reduce(
503
+ (allEntryExitStates, nextStateNode) => {
504
+ const { entry, exit } = this._getEntryExitStates(
505
+ nextStateNode,
506
+ !!selectedTransition.internal
507
+ );
679
508
 
680
- if (!currentState || !currentState.states) {
681
- throw new Error(`Unable to read '${subPath}' from '${this.id}'`);
682
- }
509
+ return {
510
+ entry: new Set([
511
+ ...Array.from(allEntryExitStates.entry),
512
+ ...Array.from(entry)
513
+ ]),
514
+ exit: new Set([
515
+ ...Array.from(allEntryExitStates.exit),
516
+ ...Array.from(exit)
517
+ ])
518
+ };
519
+ },
520
+ { entry: new Set(), exit: new Set() } as EntryExitStates
521
+ );
683
522
 
684
- if (subPath === HISTORY_KEY) {
685
- if (!Object.keys(currentState.states).length) {
686
- subPath = '';
687
- } else if (currentHistory) {
688
- subPath =
689
- typeof currentHistory === 'object'
690
- ? Object.keys(currentHistory)[0]
691
- : currentHistory;
692
- } else if (currentState.initial) {
693
- subPath = currentState.initial;
694
- } else {
695
- throw new Error(
696
- `Cannot read '${HISTORY_KEY}' from state '${currentState.id}': missing 'initial'`
697
- );
698
- }
699
- }
523
+ return {
524
+ value: this.machine.resolve(
525
+ pathsToStateValue(
526
+ flatMap(
527
+ nextStateStrings.map(str =>
528
+ this.getRelativeStateNodes(str, state.historyValue).map(
529
+ s => s.path
530
+ )
531
+ )
532
+ )
533
+ )
534
+ ),
535
+ entryExitStates,
536
+ actions,
537
+ paths: nextStatePaths
538
+ };
539
+ }
540
+ private _getEntryExitStates(
541
+ nextStateNode: StateNode,
542
+ internal: boolean
543
+ ): EntryExitStates {
544
+ const entryExitStates = {
545
+ entry: [] as StateNode[],
546
+ exit: [] as StateNode[]
547
+ };
700
548
 
701
- try {
702
- if (subPath !== '') {
703
- currentState = currentState.getStateNode(subPath);
704
- }
705
- } catch (e) {
706
- throw new Error(
707
- `Event '${event}' on state '${currentPath}' leads to undefined state '${nextStatePath.join(
708
- this.delimiter
709
- )}'.`
710
- );
711
- }
549
+ const fromPath = this.path;
550
+ const toPath = nextStateNode.path;
712
551
 
713
- if (currentState.onEntry) {
714
- actionMap.onEntry = actionMap.onEntry.concat(currentState.onEntry);
715
- }
716
- if (currentState.activities) {
717
- currentState.activities.forEach(activity => {
718
- activityMap[getEventType(activity)] = true;
719
- actionMap.onEntry = actionMap.onEntry.concat(start(activity));
720
- });
721
- }
552
+ let parent = this.machine;
722
553
 
723
- currentPath = subPath;
554
+ for (let i = 0; i < Math.min(fromPath.length, toPath.length); i++) {
555
+ const fromPathSegment = fromPath[i];
556
+ const toPathSegment = toPath[i];
724
557
 
725
- if (currentHistory) {
726
- currentHistory = currentHistory[subPath];
727
- }
728
- });
558
+ if (fromPathSegment === toPathSegment) {
559
+ parent = parent.getStateNode(fromPathSegment);
560
+ } else {
561
+ break;
562
+ }
563
+ }
564
+
565
+ const commonAncestorPath = parent.path;
729
566
 
730
- if (!currentState) {
731
- throw new Error('no state');
567
+ let marker: StateNode = parent;
568
+ for (const segment of fromPath.slice(commonAncestorPath.length)) {
569
+ marker = marker.getStateNode(segment);
570
+ entryExitStates.exit.unshift(marker);
571
+ }
572
+
573
+ // Child node
574
+ if (parent === this) {
575
+ if (!internal) {
576
+ entryExitStates.exit.push(this);
577
+ entryExitStates.entry.push(this);
732
578
  }
579
+ }
733
580
 
734
- let region = ensureTargetStateIsInCorrectRegion(
735
- this,
736
- event,
737
- usedRegions,
738
- currentState
739
- );
581
+ marker = parent;
582
+ for (const segment of toPath.slice(commonAncestorPath.length)) {
583
+ marker = marker.getStateNode(segment);
584
+ entryExitStates.entry.push(marker);
585
+ }
740
586
 
741
- while (region.parent) {
742
- region = ensureTargetStateIsInCorrectRegion(
743
- this,
744
- event,
745
- usedRegions,
746
- region.parent
587
+ return {
588
+ entry: new Set(entryExitStates.entry),
589
+ exit: new Set(entryExitStates.exit)
590
+ };
591
+ }
592
+ private _evaluateCond(
593
+ condition: Condition,
594
+ extendedState: any,
595
+ eventObject: EventObject,
596
+ interimState: StateValue
597
+ ): boolean {
598
+ let condFn: ConditionPredicate;
599
+
600
+ if (typeof condition === 'string') {
601
+ if (!this.machine.options.guards[condition]) {
602
+ throw new Error(
603
+ `String condition '${condition}' is not defined on machine '${
604
+ this.machine.id
605
+ }'`
747
606
  );
748
607
  }
749
608
 
750
- let paths = [currentState.path];
609
+ condFn = this.machine.options.guards[condition];
610
+ } else {
611
+ condFn = condition;
612
+ }
613
+
614
+ return condFn(extendedState, eventObject, interimState);
615
+ }
616
+ private _getActions(transition: StateTransition): Action[] {
617
+ const entryExitActions = {
618
+ entry: transition.entryExitStates
619
+ ? flatMap(
620
+ Array.from(transition.entryExitStates.entry).map(n => [
621
+ ...n.onEntry,
622
+ ...(n.activities
623
+ ? n.activities.map(activity => start(activity))
624
+ : [])
625
+ ])
626
+ )
627
+ : [],
628
+ exit: transition.entryExitStates
629
+ ? flatMap(
630
+ Array.from(transition.entryExitStates.exit).map(n => [
631
+ ...n.onExit,
632
+ ...(n.activities
633
+ ? n.activities.map(activity => stop(activity))
634
+ : [])
635
+ ])
636
+ )
637
+ : []
638
+ };
639
+
640
+ const actions = (entryExitActions.exit || [])
641
+ .concat(transition.actions || [])
642
+ .concat(entryExitActions.entry || []);
643
+
644
+ return actions;
645
+ }
646
+ private _getActivities(
647
+ state: State,
648
+ transition: StateTransition
649
+ ): ActivityMap {
650
+ if (!transition.entryExitStates) {
651
+ return {};
652
+ }
653
+
654
+ const activityMap = { ...state.activities };
655
+
656
+ Array.from(transition.entryExitStates.exit).forEach(stateNode => {
657
+ if (!stateNode.activities) {
658
+ return; // TODO: fixme
659
+ }
660
+
661
+ stateNode.activities.forEach(activity => {
662
+ activityMap[getActionType(activity)] = false;
663
+ });
664
+ });
665
+
666
+ Array.from(transition.entryExitStates.entry).forEach(stateNode => {
667
+ if (!stateNode.activities) {
668
+ return; // TODO: fixme
669
+ }
751
670
 
752
- if (currentState.initial || currentState.parallel) {
753
- const { initialState } = currentState;
754
- actionMap.onEntry = actionMap.onEntry.concat(initialState.actions);
755
- paths = toStatePaths(initialState.value).map(subPath =>
756
- currentState!.path.concat(subPath)
671
+ stateNode.activities.forEach(activity => {
672
+ activityMap[getActionType(activity)] = true;
673
+ });
674
+ });
675
+
676
+ return activityMap;
677
+ }
678
+ public transition(
679
+ state: StateValue | State,
680
+ event: Event,
681
+ extendedState?: any
682
+ ): State {
683
+ const resolvedStateValue =
684
+ typeof state === 'string'
685
+ ? this.resolve(pathToStateValue(this.getResolvedPath(state)))
686
+ : state instanceof State
687
+ ? state
688
+ : this.resolve(state);
689
+
690
+ const eventType = getEventType(event);
691
+
692
+ if (this.strict) {
693
+ if (this.events.indexOf(eventType) === -1) {
694
+ throw new Error(
695
+ `Machine '${this.id}' does not accept event '${eventType}'`
757
696
  );
758
697
  }
698
+ }
699
+
700
+ const currentState = State.from(resolvedStateValue);
701
+
702
+ const historyValue =
703
+ resolvedStateValue instanceof State
704
+ ? resolvedStateValue.historyValue
705
+ ? resolvedStateValue.historyValue
706
+ : (this.machine.historyValue(
707
+ resolvedStateValue.value
708
+ ) as HistoryValue)
709
+ : (this.machine.historyValue(resolvedStateValue) as HistoryValue);
710
+
711
+ const stateTransition = this._transition(
712
+ currentState.value,
713
+ currentState,
714
+ event,
715
+ extendedState
716
+ );
717
+
718
+ try {
719
+ this.ensureValidPaths(stateTransition.paths);
720
+ } catch (e) {
721
+ throw new Error(
722
+ `Event '${eventType}' leads to an invalid configuration: ` + e.message
723
+ );
724
+ }
725
+
726
+ const actions = this._getActions(stateTransition);
727
+ const activities = this._getActivities(currentState, stateTransition);
759
728
 
760
- finalPaths.push(...paths);
729
+ const raisedEvents = actions.filter(
730
+ action =>
731
+ typeof action === 'object' &&
732
+ (action.type === actionTypes.raise || action.type === actionTypes.null)
733
+ ) as ActionObject[];
761
734
 
762
- while (currentState.initial) {
763
- if (!currentState || !currentState.states) {
764
- throw new Error(`Invalid initial state`);
735
+ const nonEventActions = actions.filter(
736
+ action =>
737
+ typeof action !== 'object' ||
738
+ (action.type !== actionTypes.raise && action.type !== actionTypes.null)
739
+ );
740
+ const stateNodes = stateTransition.value
741
+ ? this.getStateNodes(stateTransition.value)
742
+ : [];
743
+
744
+ const isTransient = stateNodes.some(stateNode => stateNode.transient);
745
+ if (isTransient) {
746
+ raisedEvents.push({ type: actionTypes.null });
747
+ }
748
+
749
+ const data = {};
750
+ stateNodes.forEach(stateNode => {
751
+ data[stateNode.id] = stateNode.data;
752
+ });
753
+
754
+ const nextState = stateTransition.value
755
+ ? new State(
756
+ stateTransition.value,
757
+ StateNode.updateHistoryValue(historyValue, stateTransition.value),
758
+ currentState,
759
+ nonEventActions,
760
+ activities,
761
+ data,
762
+ raisedEvents
763
+ )
764
+ : undefined;
765
+
766
+ if (!nextState) {
767
+ // Unchanged state should be returned with no actions
768
+ return State.inert(currentState);
769
+ }
770
+
771
+ // Dispose of previous histories to prevent memory leaks
772
+ delete currentState.history;
773
+
774
+ let maybeNextState = nextState;
775
+ while (raisedEvents.length) {
776
+ const currentActions = maybeNextState.actions;
777
+ const raisedEvent = raisedEvents.shift()!;
778
+ maybeNextState = this.transition(
779
+ maybeNextState,
780
+ raisedEvent.type === actionTypes.null ? NULL_EVENT : raisedEvent.event,
781
+ extendedState
782
+ );
783
+ maybeNextState.actions.unshift(...currentActions);
784
+ }
785
+
786
+ return maybeNextState;
787
+ }
788
+ private ensureValidPaths(paths: string[][]): void {
789
+ const visitedParents = new Map<StateNode, StateNode[]>();
790
+
791
+ const stateNodes = flatMap(
792
+ paths.map(_path => this.getRelativeStateNodes(_path))
793
+ );
794
+
795
+ outer: for (const stateNode of stateNodes) {
796
+ let marker = stateNode;
797
+
798
+ while (marker.parent) {
799
+ if (visitedParents.has(marker.parent)) {
800
+ if (marker.parent.parallel) {
801
+ continue outer;
802
+ }
803
+
804
+ throw new Error(
805
+ `State node '${stateNode.id}' shares parent '${
806
+ marker.parent.id
807
+ }' with state node '${visitedParents
808
+ .get(marker.parent)!
809
+ .map(a => a.id)}'`
810
+ );
765
811
  }
766
- currentState = currentState.states[currentState.initial];
767
812
 
768
- if (currentState.activities) {
769
- currentState.activities.forEach(activity => {
770
- activityMap[getEventType(activity)] = true;
771
- actionMap.onEntry = actionMap.onEntry.concat(start(activity));
772
- });
813
+ if (!visitedParents.get(marker.parent)) {
814
+ visitedParents.set(marker.parent, [stateNode]);
815
+ } else {
816
+ visitedParents.get(marker.parent)!.push(stateNode);
773
817
  }
818
+
819
+ marker = marker.parent;
774
820
  }
775
- const myActions = (currentState.onEntry
776
- ? currentState.onEntry.filter(
777
- action =>
778
- typeof action === 'object' && action.type === actionTypes.raise
779
- )
780
- : []
781
- ).concat(currentState.on[NULL_EVENT] ? { type: actionTypes.null } : []);
782
- myActions.forEach(action => raisedEvents.push(action));
783
- });
821
+ }
822
+ }
823
+ public getStateNode(stateKey: string): StateNode {
824
+ if (isStateId(stateKey)) {
825
+ return this.machine.getStateNodeById(stateKey);
826
+ }
784
827
 
785
- return {
786
- statePaths: finalPaths,
787
- actions: actionMap,
788
- activities: activityMap,
789
- events: raisedEvents as EventObject[]
790
- };
828
+ if (!this.states) {
829
+ throw new Error(
830
+ `Unable to retrieve child state '${stateKey}' from '${
831
+ this.id
832
+ }'; no child states exist.`
833
+ );
834
+ }
835
+
836
+ const result = this.states[stateKey];
837
+ if (!result) {
838
+ throw new Error(
839
+ `Child state '${stateKey}' does not exist on '${this.id}'`
840
+ );
841
+ }
842
+
843
+ return result;
791
844
  }
845
+ public getStateNodeById(stateId: string): StateNode {
846
+ const resolvedStateId = isStateId(stateId)
847
+ ? stateId.slice(STATE_IDENTIFIER.length)
848
+ : stateId;
849
+ const stateNode = this.machine.idMap[resolvedStateId];
850
+
851
+ if (!stateNode) {
852
+ throw new Error(
853
+ `Substate '#${resolvedStateId}' does not exist on '${this.id}'`
854
+ );
855
+ }
856
+
857
+ return stateNode;
858
+ }
859
+ public getStateNodeByPath(statePath: string | string[]): StateNode {
860
+ const arrayStatePath = toStatePath(statePath, this.delimiter);
861
+ let currentStateNode: StateNode = this;
862
+ while (arrayStatePath.length) {
863
+ const key = arrayStatePath.shift()!;
864
+ currentStateNode = currentStateNode.getStateNode(key);
865
+ }
866
+
867
+ return currentStateNode;
868
+ }
869
+ private resolve(stateValue: StateValue): StateValue {
870
+ if (typeof stateValue === 'string') {
871
+ const subStateNode = this.getStateNode(stateValue);
872
+ return subStateNode.initial
873
+ ? { [stateValue]: subStateNode.initialStateValue! }
874
+ : stateValue;
875
+ }
876
+
877
+ if (this.parallel) {
878
+ return mapValues(
879
+ this.initialStateValue as Record<string, StateValue>,
880
+ (subStateValue, subStateKey) => {
881
+ return subStateValue
882
+ ? this.getStateNode(subStateKey).resolve(
883
+ stateValue[subStateKey] || subStateValue
884
+ )
885
+ : {};
886
+ }
887
+ );
888
+ }
889
+
890
+ return mapValues(stateValue, (subStateValue, subStateKey) => {
891
+ return subStateValue
892
+ ? this.getStateNode(subStateKey).resolve(subStateValue)
893
+ : {};
894
+ });
895
+ }
896
+
792
897
  private get resolvedStateValue(): StateValue {
793
898
  const { key } = this;
794
899
 
795
900
  if (this.parallel) {
796
901
  return {
797
- [key]: mapValues(
902
+ [key]: mapFilterValues(
798
903
  this.states,
799
- stateNode => stateNode.resolvedStateValue[stateNode.key]
904
+ stateNode => stateNode.resolvedStateValue[stateNode.key],
905
+ stateNode => !stateNode.history
800
906
  )
801
907
  };
802
908
  }
@@ -826,16 +932,19 @@ class StateNode implements StateNodeConfig {
826
932
  return toStatePath(stateIdentifier, this.delimiter);
827
933
  }
828
934
  private get initialStateValue(): StateValue | undefined {
829
- const initialStateValue =
830
- this.__cache.initialState ||
831
- ((this.parallel
832
- ? mapValues(
833
- this.states as Record<string, StateNode>,
834
- state => state.initialStateValue
835
- )
836
- : typeof this.resolvedStateValue === 'string'
837
- ? undefined
838
- : this.resolvedStateValue[this.key]) as StateValue);
935
+ if (this.__cache.initialState) {
936
+ return this.__cache.initialState;
937
+ }
938
+
939
+ const initialStateValue = (this.parallel
940
+ ? mapFilterValues(
941
+ this.states as Record<string, StateNode>,
942
+ state => state.initialStateValue || {},
943
+ stateNode => !stateNode.history
944
+ )
945
+ : typeof this.resolvedStateValue === 'string'
946
+ ? undefined
947
+ : this.resolvedStateValue[this.key]) as StateValue;
839
948
 
840
949
  this.__cache.initialState = initialStateValue;
841
950
 
@@ -865,7 +974,53 @@ class StateNode implements StateNodeConfig {
865
974
  }
866
975
  });
867
976
 
868
- return new State(initialStateValue, undefined, actions, activityMap);
977
+ // TODO: deduplicate - DRY (from this.transition())
978
+ const raisedEvents = actions.filter(
979
+ action =>
980
+ typeof action === 'object' &&
981
+ (action.type === actionTypes.raise || action.type === actionTypes.null)
982
+ ) as ActionObject[];
983
+
984
+ const initialState = new State(
985
+ initialStateValue,
986
+ undefined,
987
+ undefined,
988
+ actions,
989
+ activityMap
990
+ );
991
+
992
+ let maybeNextState = initialState;
993
+ while (raisedEvents.length) {
994
+ const currentActions = maybeNextState.actions;
995
+ const raisedEvent = raisedEvents.shift()!;
996
+ maybeNextState = this.transition(
997
+ maybeNextState,
998
+ raisedEvent.type === actionTypes.null ? NULL_EVENT : raisedEvent.event,
999
+ undefined // TODO: consider initial state given external state
1000
+ );
1001
+ maybeNextState.actions.unshift(...currentActions);
1002
+ }
1003
+
1004
+ return maybeNextState;
1005
+ }
1006
+ public get target(): StateValue | undefined {
1007
+ let target;
1008
+ if (this.history) {
1009
+ const historyConfig = this.config as HistoryStateNodeConfig;
1010
+ if (historyConfig.target && typeof historyConfig.target === 'string') {
1011
+ target = isStateId(historyConfig.target)
1012
+ ? pathToStateValue(
1013
+ this.machine
1014
+ .getStateNodeById(historyConfig.target)
1015
+ .path.slice(this.path.length - 1)
1016
+ )
1017
+ : historyConfig.target;
1018
+ } else {
1019
+ target = historyConfig.target;
1020
+ }
1021
+ }
1022
+
1023
+ return target;
869
1024
  }
870
1025
  public getStates(stateValue: StateValue): StateNode[] {
871
1026
  if (typeof stateValue === 'string') {
@@ -880,30 +1035,215 @@ class StateNode implements StateNodeConfig {
880
1035
 
881
1036
  return stateNodes;
882
1037
  }
883
- public getState(relativeStateId: string | string[]): StateNode | undefined {
1038
+
1039
+ /**
1040
+ * Returns the leaf nodes from a state path relative to this state node.
1041
+ *
1042
+ * @param relativeStateId The relative state path to retrieve the state nodes
1043
+ * @param history The previous state to retrieve history
1044
+ * @param resolve Whether state nodes should resolve to initial child state nodes
1045
+ */
1046
+ public getRelativeStateNodes(
1047
+ relativeStateId: string | string[],
1048
+ historyValue?: HistoryValue,
1049
+ resolve: boolean = true
1050
+ ): StateNode[] {
884
1051
  if (typeof relativeStateId === 'string' && isStateId(relativeStateId)) {
885
- return this.getStateNodeById(relativeStateId);
1052
+ const unresolvedStateNode = this.getStateNodeById(relativeStateId);
1053
+
1054
+ return resolve
1055
+ ? unresolvedStateNode.history
1056
+ ? unresolvedStateNode.resolveHistory(historyValue)
1057
+ : unresolvedStateNode.initialStateNodes
1058
+ : [unresolvedStateNode];
886
1059
  }
887
1060
 
888
1061
  const statePath = toStatePath(relativeStateId, this.delimiter);
889
1062
 
890
- try {
891
- return statePath.reduce(
892
- (subState, subPath) => {
893
- if (!subState.states) {
894
- throw new Error(
895
- `Cannot retrieve subPath '${subPath}' from node with no states`
896
- );
897
- }
898
- return subState.states[subPath];
899
- },
900
- this as StateNode
901
- );
902
- } catch (e) {
1063
+ const rootStateNode = this.parent || this;
1064
+
1065
+ const unresolvedStateNodes = rootStateNode.getFromRelativePath(
1066
+ statePath,
1067
+ historyValue
1068
+ );
1069
+
1070
+ if (!resolve) {
1071
+ return unresolvedStateNodes;
1072
+ }
1073
+ return flatMap(
1074
+ unresolvedStateNodes.map(stateNode => stateNode.initialStateNodes)
1075
+ );
1076
+ }
1077
+ public get initialStateNodes(): StateNode[] {
1078
+ // todo - isLeafNode or something
1079
+ if (!this.parallel && !this.initial) {
1080
+ return [this];
1081
+ }
1082
+
1083
+ const { initialState } = this;
1084
+ const initialStateNodePaths = toStatePaths(initialState.value);
1085
+ return flatMap(
1086
+ initialStateNodePaths.map(initialPath =>
1087
+ this.getFromRelativePath(initialPath)
1088
+ )
1089
+ );
1090
+ }
1091
+ /**
1092
+ * Retrieves state nodes from a relative path to this state node.
1093
+ *
1094
+ * @param relativePath The relative path from this state node
1095
+ * @param historyValue
1096
+ */
1097
+ public getFromRelativePath(
1098
+ relativePath: string[],
1099
+ historyValue?: HistoryValue
1100
+ ): StateNode[] {
1101
+ if (!relativePath.length) {
1102
+ return [this];
1103
+ }
1104
+
1105
+ const [x, ...xs] = relativePath;
1106
+
1107
+ if (!this.states) {
903
1108
  throw new Error(
904
- `State '${relativeStateId} does not exist on machine '${this.id}'`
1109
+ `Cannot retrieve subPath '${x}' from node with no states`
1110
+ );
1111
+ }
1112
+
1113
+ // TODO: remove (4.0)
1114
+ if (x === HISTORY_KEY) {
1115
+ if (!historyValue) {
1116
+ return [this];
1117
+ }
1118
+
1119
+ const subHistoryValue = nestedPath<HistoryValue>(this.path, 'states')(
1120
+ historyValue
1121
+ ).current;
1122
+
1123
+ if (typeof subHistoryValue === 'string') {
1124
+ return this.states[subHistoryValue].getFromRelativePath(
1125
+ xs,
1126
+ historyValue
1127
+ );
1128
+ }
1129
+
1130
+ return flatMap(
1131
+ Object.keys(subHistoryValue!).map(key => {
1132
+ return this.states[key].getFromRelativePath(xs, historyValue);
1133
+ })
905
1134
  );
906
1135
  }
1136
+
1137
+ const childStateNode = this.getStateNode(x);
1138
+
1139
+ if (childStateNode.history) {
1140
+ return childStateNode.resolveHistory(historyValue);
1141
+ }
1142
+
1143
+ if (!this.states[x]) {
1144
+ throw new Error(`Child state '${x}' does not exist on '${this.id}'`);
1145
+ }
1146
+
1147
+ return this.states[x].getFromRelativePath(xs, historyValue);
1148
+ }
1149
+ public static updateHistoryValue(
1150
+ hist: HistoryValue,
1151
+ stateValue: StateValue
1152
+ ): HistoryValue {
1153
+ function update(
1154
+ _hist: HistoryValue,
1155
+ _sv: StateValue
1156
+ ): Record<string, HistoryValue | undefined> {
1157
+ return mapValues(_hist.states, (subHist, key) => {
1158
+ if (!subHist) {
1159
+ return undefined;
1160
+ }
1161
+ const subStateValue =
1162
+ (typeof _sv === 'string' ? undefined : _sv[key]) ||
1163
+ (subHist ? subHist.current : undefined);
1164
+
1165
+ if (!subStateValue) {
1166
+ return undefined;
1167
+ }
1168
+
1169
+ return {
1170
+ current: subStateValue,
1171
+ states: update(subHist, subStateValue)
1172
+ };
1173
+ });
1174
+ }
1175
+ return {
1176
+ current: stateValue,
1177
+ states: update(hist, stateValue)
1178
+ };
1179
+ }
1180
+ public historyValue(
1181
+ relativeStateValue?: StateValue | undefined
1182
+ ): HistoryValue | undefined {
1183
+ if (!Object.keys(this.states).length) {
1184
+ return undefined;
1185
+ }
1186
+
1187
+ return {
1188
+ current: relativeStateValue || this.initialStateValue,
1189
+ states: mapFilterValues(
1190
+ this.states,
1191
+ (stateNode, key) => {
1192
+ if (!relativeStateValue) {
1193
+ return stateNode.historyValue();
1194
+ }
1195
+
1196
+ const subStateValue =
1197
+ typeof relativeStateValue === 'string'
1198
+ ? undefined
1199
+ : relativeStateValue[key];
1200
+
1201
+ return stateNode.historyValue(
1202
+ subStateValue || stateNode.initialStateValue
1203
+ );
1204
+ },
1205
+ stateNode => !stateNode.history
1206
+ )
1207
+ };
1208
+ }
1209
+ /**
1210
+ * Resolves to the historical value(s) of the parent state node,
1211
+ * represented by state nodes.
1212
+ *
1213
+ * @param historyValue
1214
+ */
1215
+ private resolveHistory(historyValue?: HistoryValue): StateNode[] {
1216
+ if (!this.history) {
1217
+ return [this];
1218
+ }
1219
+
1220
+ const parent = this.parent!;
1221
+
1222
+ if (!historyValue) {
1223
+ return this.target
1224
+ ? flatMap(
1225
+ toStatePaths(this.target).map(relativeChildPath =>
1226
+ parent.getFromRelativePath(relativeChildPath)
1227
+ )
1228
+ )
1229
+ : this.parent!.initialStateNodes;
1230
+ }
1231
+
1232
+ const subHistoryValue = nestedPath<HistoryValue>(parent.path, 'states')(
1233
+ historyValue
1234
+ ).current;
1235
+
1236
+ if (typeof subHistoryValue === 'string') {
1237
+ return [parent.getStateNode(subHistoryValue)];
1238
+ }
1239
+
1240
+ return flatMap(
1241
+ toStatePaths(subHistoryValue!).map(subStatePath => {
1242
+ return this.history === 'deep'
1243
+ ? parent.getFromRelativePath(subStatePath)
1244
+ : [parent.states[subStatePath[0]]];
1245
+ })
1246
+ );
907
1247
  }
908
1248
  get events(): EventType[] {
909
1249
  if (this.__cache.events) {
@@ -925,6 +1265,33 @@ class StateNode implements StateNodeConfig {
925
1265
 
926
1266
  return (this.__cache.events = Array.from(events));
927
1267
  }
1268
+ private formatTransition(
1269
+ targets: string[],
1270
+ transitionConfig?: TransitionConfig
1271
+ ): TargetTransitionConfig {
1272
+ let internal = transitionConfig ? transitionConfig.internal : false;
1273
+
1274
+ // Format targets to their full string path
1275
+ const formattedTargets = targets.map(target => {
1276
+ const internalTarget =
1277
+ typeof target === 'string' && target[0] === this.delimiter;
1278
+ internal = internal || internalTarget;
1279
+
1280
+ // If internal target is defined on machine,
1281
+ // do not include machine key on target
1282
+ if (internalTarget && !this.parent) {
1283
+ return target.slice(1);
1284
+ }
1285
+
1286
+ return internalTarget ? this.key + target : target;
1287
+ });
1288
+
1289
+ return {
1290
+ ...transitionConfig,
1291
+ target: formattedTargets,
1292
+ internal
1293
+ };
1294
+ }
928
1295
  private formatTransitions(
929
1296
  onConfig: Record<string, Transition | undefined>
930
1297
  ): Record<string, ConditionalTransitionConfig> {
@@ -934,27 +1301,36 @@ class StateNode implements StateNodeConfig {
934
1301
  }
935
1302
 
936
1303
  if (Array.isArray(value)) {
937
- return value;
1304
+ return value.map(targetTransitionConfig =>
1305
+ this.formatTransition(
1306
+ ([] as string[]).concat(targetTransitionConfig.target),
1307
+ targetTransitionConfig
1308
+ )
1309
+ );
938
1310
  }
939
1311
 
940
1312
  if (typeof value === 'string') {
941
- return [{ target: value }];
1313
+ return [this.formatTransition([value])];
942
1314
  }
943
1315
 
944
1316
  return Object.keys(value).map(target => {
945
- return {
946
- target,
947
- ...value[target]
948
- };
1317
+ return this.formatTransition([target], value[target]);
949
1318
  });
950
1319
  });
951
1320
  }
952
1321
  }
953
1322
 
954
- export function Machine(
955
- config: MachineConfig | ParallelMachineConfig
956
- ): StandardMachine | ParallelMachine {
957
- return new StateNode(config) as StandardMachine | ParallelMachine;
1323
+ export function Machine<
1324
+ T extends StandardMachineConfig | ParallelMachineConfig
1325
+ >(
1326
+ config: T,
1327
+ options?: MachineOptions
1328
+ ): T extends ParallelMachineConfig
1329
+ ? ParallelMachine
1330
+ : T extends StandardMachineConfig ? StandardMachine : never {
1331
+ return new StateNode(config, options) as T extends ParallelMachineConfig
1332
+ ? ParallelMachine
1333
+ : T extends StandardMachineConfig ? StandardMachine : never;
958
1334
  }
959
1335
 
960
1336
  export { StateNode };