xstate 3.3.2 → 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.
- package/README.md +21 -1
- package/dist/xstate.js +1 -1
- package/dist/xstate.utils.js +1 -1
- package/es/State.d.ts +8 -7
- package/es/State.js +3 -2
- package/es/StateNode.d.ts +35 -20
- package/es/StateNode.js +119 -46
- package/es/graph.d.ts +4 -1
- package/es/graph.js +5 -4
- package/es/patterns.js +1 -1
- package/es/scxml.d.ts +2 -1
- package/es/scxml.js +9 -8
- package/es/types.d.ts +4 -7
- package/es/utils.d.ts +6 -1
- package/es/utils.js +17 -1
- package/lib/State.d.ts +8 -7
- package/lib/State.js +3 -2
- package/lib/StateNode.d.ts +35 -20
- package/lib/StateNode.js +118 -45
- package/lib/graph.d.ts +4 -1
- package/lib/graph.js +5 -3
- package/lib/patterns.js +1 -1
- package/lib/scxml.d.ts +2 -1
- package/lib/scxml.js +9 -8
- package/lib/types.d.ts +4 -7
- package/lib/utils.d.ts +6 -1
- package/lib/utils.js +18 -1
- package/package.json +3 -3
- package/src/State.ts +4 -1
- package/src/StateNode.ts +221 -89
- package/src/graph.ts +14 -12
- package/src/scxml.ts +52 -49
- package/src/types.ts +6 -9
- package/src/utils.ts +25 -4
- package/test/activities.test.ts +28 -1
- package/test/deterministic.test.ts +1 -1
- package/test/history.test.ts +139 -0
- package/test/invalid.test.ts +48 -0
- package/test/parallel.test.ts +229 -1
package/src/scxml.ts
CHANGED
|
@@ -62,26 +62,54 @@ function stateNodeToSCXML(stateNode: StateNode) {
|
|
|
62
62
|
return stateNodeToSCXML(subStateNode);
|
|
63
63
|
}),
|
|
64
64
|
...Object.keys(stateNode.on)
|
|
65
|
-
.map(
|
|
66
|
-
|
|
65
|
+
.map(
|
|
66
|
+
(event): XMLElement[] => {
|
|
67
|
+
const transition = stateNode.on![event];
|
|
67
68
|
|
|
68
|
-
|
|
69
|
-
|
|
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!.getRelativeStateNodes(
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
...targetTransition.cond
|
|
107
|
+
...(event ? { event } : undefined),
|
|
108
|
+
target: stateNode.parent!.getRelativeStateNodes(target)![0]
|
|
109
|
+
.id, // TODO: fixme
|
|
110
|
+
...(targetTransition.cond
|
|
83
111
|
? { cond: targetTransition.cond.toString() }
|
|
84
|
-
: undefined
|
|
112
|
+
: undefined)
|
|
85
113
|
},
|
|
86
114
|
elements: targetTransition.actions
|
|
87
115
|
? targetTransition.actions.map(action => ({
|
|
@@ -95,32 +123,7 @@ function stateNodeToSCXML(stateNode: StateNode) {
|
|
|
95
123
|
};
|
|
96
124
|
});
|
|
97
125
|
}
|
|
98
|
-
|
|
99
|
-
return Object.keys(transition).map(target => {
|
|
100
|
-
const targetTransition = transition[target];
|
|
101
|
-
|
|
102
|
-
return {
|
|
103
|
-
type: 'element',
|
|
104
|
-
name: 'transition',
|
|
105
|
-
attributes: {
|
|
106
|
-
...event ? { event } : undefined,
|
|
107
|
-
target: stateNode.parent!.getRelativeStateNodes(target)![0].id, // TODO: fixme
|
|
108
|
-
...targetTransition.cond
|
|
109
|
-
? { cond: targetTransition.cond.toString() }
|
|
110
|
-
: undefined
|
|
111
|
-
},
|
|
112
|
-
elements: targetTransition.actions
|
|
113
|
-
? targetTransition.actions.map(action => ({
|
|
114
|
-
type: 'element',
|
|
115
|
-
name: 'send',
|
|
116
|
-
attributes: {
|
|
117
|
-
event: getActionType(action)
|
|
118
|
-
}
|
|
119
|
-
}))
|
|
120
|
-
: undefined
|
|
121
|
-
};
|
|
122
|
-
});
|
|
123
|
-
})
|
|
126
|
+
)
|
|
124
127
|
.reduce((a, b) => a.concat(b))
|
|
125
128
|
].filter(Boolean) as XMLElement[]
|
|
126
129
|
};
|
|
@@ -276,12 +279,12 @@ function toConfig(
|
|
|
276
279
|
(values: XMLElement[]) => {
|
|
277
280
|
return values.map(value => ({
|
|
278
281
|
target: `#${value.attributes!.target}`,
|
|
279
|
-
...value.elements ? executableContent(value.elements) : undefined,
|
|
280
|
-
...value.attributes!.cond
|
|
282
|
+
...(value.elements ? executableContent(value.elements) : undefined),
|
|
283
|
+
...(value.attributes!.cond
|
|
281
284
|
? {
|
|
282
285
|
cond: evalCond(value.attributes!.cond as string)
|
|
283
286
|
}
|
|
284
|
-
: undefined
|
|
287
|
+
: undefined)
|
|
285
288
|
}));
|
|
286
289
|
}
|
|
287
290
|
);
|
|
@@ -311,18 +314,18 @@ function toConfig(
|
|
|
311
314
|
return {
|
|
312
315
|
id,
|
|
313
316
|
delimiter: options.delimiter,
|
|
314
|
-
...initial ? { initial } : undefined,
|
|
315
|
-
...parallel ? { parallel } : undefined,
|
|
316
|
-
...stateElements.length
|
|
317
|
+
...(initial ? { initial } : undefined),
|
|
318
|
+
...(parallel ? { parallel } : undefined),
|
|
319
|
+
...(stateElements.length
|
|
317
320
|
? {
|
|
318
321
|
states: mapValues(states, (state, key) =>
|
|
319
322
|
toConfig(state, key, options)
|
|
320
323
|
)
|
|
321
324
|
}
|
|
322
|
-
: undefined,
|
|
323
|
-
...transitionElements.length ? { on } : undefined,
|
|
324
|
-
...onEntry ? { onEntry } : undefined,
|
|
325
|
-
...onExit ? { onExit } : undefined
|
|
325
|
+
: undefined),
|
|
326
|
+
...(transitionElements.length ? { on } : undefined),
|
|
327
|
+
...(onEntry ? { onEntry } : undefined),
|
|
328
|
+
...(onExit ? { onExit } : undefined)
|
|
326
329
|
};
|
|
327
330
|
}
|
|
328
331
|
|
package/src/types.ts
CHANGED
|
@@ -25,6 +25,11 @@ export interface StateValueMap {
|
|
|
25
25
|
|
|
26
26
|
export type StateValue = string | StateValueMap;
|
|
27
27
|
|
|
28
|
+
export interface HistoryValue {
|
|
29
|
+
states: Record<string, HistoryValue | undefined>;
|
|
30
|
+
current: StateValue | undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
28
33
|
export type ConditionPredicate = (
|
|
29
34
|
extendedState: any,
|
|
30
35
|
event: EventObject,
|
|
@@ -112,7 +117,6 @@ export interface StandardMachineConfig extends MachineConfig {
|
|
|
112
117
|
export interface ParallelMachineConfig extends MachineConfig {
|
|
113
118
|
initial?: undefined;
|
|
114
119
|
parallel: true;
|
|
115
|
-
states: Record<string, CompoundStateNodeConfig>;
|
|
116
120
|
}
|
|
117
121
|
|
|
118
122
|
export interface EntryExitEffectMap {
|
|
@@ -190,15 +194,8 @@ export type MaybeStateValueActionsTuple = [
|
|
|
190
194
|
ActivityMap | undefined
|
|
191
195
|
];
|
|
192
196
|
|
|
193
|
-
export interface StateTransition {
|
|
194
|
-
statePaths: string[][];
|
|
195
|
-
actions: ActionMap;
|
|
196
|
-
activities: ActivityMap | undefined;
|
|
197
|
-
events: EventObject[];
|
|
198
|
-
}
|
|
199
|
-
|
|
200
197
|
// tslint:disable-next-line:class-name
|
|
201
|
-
export interface
|
|
198
|
+
export interface StateTransition {
|
|
202
199
|
value: StateValue | undefined;
|
|
203
200
|
entryExitStates: EntryExitStates | undefined;
|
|
204
201
|
actions: Action[];
|
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'
|
|
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.'
|
|
@@ -113,18 +115,37 @@ export function mapFilterValues<T, P>(
|
|
|
113
115
|
* Retrieves a value at the given path.
|
|
114
116
|
* @param props The deep path to the prop of the desired value
|
|
115
117
|
*/
|
|
116
|
-
export const path = (props: string[]): any =>
|
|
118
|
+
export const path = <T extends Record<string, any>>(props: string[]): any => (
|
|
117
119
|
object: T
|
|
118
120
|
): any => {
|
|
119
|
-
let result:
|
|
121
|
+
let result: T = object;
|
|
120
122
|
|
|
121
123
|
for (const prop of props) {
|
|
122
|
-
result = result[prop];
|
|
124
|
+
result = result[prop as keyof typeof result];
|
|
123
125
|
}
|
|
124
126
|
|
|
125
127
|
return result;
|
|
126
128
|
};
|
|
127
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
|
+
|
|
128
149
|
export const toStatePaths = (stateValue: StateValue): string[][] => {
|
|
129
150
|
if (typeof stateValue === 'string') {
|
|
130
151
|
return [[stateValue]];
|
package/test/activities.test.ts
CHANGED
|
@@ -203,6 +203,21 @@ describe('transient activities', () => {
|
|
|
203
203
|
B1: '.B1',
|
|
204
204
|
B2: '.B2'
|
|
205
205
|
}
|
|
206
|
+
},
|
|
207
|
+
C: {
|
|
208
|
+
initial: 'C1',
|
|
209
|
+
states: {
|
|
210
|
+
C1: {
|
|
211
|
+
activities: ['C1'],
|
|
212
|
+
on: {
|
|
213
|
+
C: 'C1',
|
|
214
|
+
C_SIMILAR: 'C2'
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
C2: {
|
|
218
|
+
activities: ['C1'],
|
|
219
|
+
},
|
|
220
|
+
}
|
|
206
221
|
}
|
|
207
222
|
}
|
|
208
223
|
});
|
|
@@ -223,10 +238,22 @@ describe('transient activities', () => {
|
|
|
223
238
|
assert.deepEqual(state.activities.A, true);
|
|
224
239
|
});
|
|
225
240
|
|
|
241
|
+
it('should have kept same activities', () => {
|
|
242
|
+
let state = machine.initialState;
|
|
243
|
+
state = machine.transition(state, 'C_SIMILAR');
|
|
244
|
+
assert.deepEqual(state.activities.C1, true);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('should have kept same activities after self transition', () => {
|
|
248
|
+
let state = machine.initialState;
|
|
249
|
+
state = machine.transition(state, 'C');
|
|
250
|
+
assert.deepEqual(state.activities.C1, true);
|
|
251
|
+
});
|
|
252
|
+
|
|
226
253
|
it('should have stopped after automatic transitions', () => {
|
|
227
254
|
let state = machine.initialState;
|
|
228
255
|
state = machine.transition(state, 'A');
|
|
229
|
-
assert.deepEqual(state.value, { A: 'A2', B: 'B2' });
|
|
256
|
+
assert.deepEqual(state.value, { A: 'A2', B: 'B2', C: 'C1' });
|
|
230
257
|
assert.deepEqual(state.activities.B2, true);
|
|
231
258
|
});
|
|
232
259
|
});
|
package/test/history.test.ts
CHANGED
|
@@ -301,3 +301,142 @@ describe('parallel history states', () => {
|
|
|
301
301
|
});
|
|
302
302
|
});
|
|
303
303
|
});
|
|
304
|
+
|
|
305
|
+
describe('transient history', () => {
|
|
306
|
+
const transientMachine = Machine({
|
|
307
|
+
initial: 'A',
|
|
308
|
+
parallel: false,
|
|
309
|
+
states: {
|
|
310
|
+
A: {
|
|
311
|
+
on: { EVENT: 'B' }
|
|
312
|
+
},
|
|
313
|
+
B: {
|
|
314
|
+
on: {
|
|
315
|
+
// eventless transition
|
|
316
|
+
'': 'C'
|
|
317
|
+
}
|
|
318
|
+
},
|
|
319
|
+
C: {}
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
it('should have history on transient transitions', () => {
|
|
323
|
+
const nextState = transientMachine.transition('A', 'EVENT');
|
|
324
|
+
assert.equal(nextState.value, 'C');
|
|
325
|
+
assert.isDefined(nextState.history);
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
describe('internal transition with history', () => {
|
|
330
|
+
const machine = Machine({
|
|
331
|
+
key: 'test',
|
|
332
|
+
initial: 'first',
|
|
333
|
+
states: {
|
|
334
|
+
first: {
|
|
335
|
+
initial: 'foo',
|
|
336
|
+
states: {
|
|
337
|
+
foo: {}
|
|
338
|
+
},
|
|
339
|
+
on: {
|
|
340
|
+
NEXT: 'second.other'
|
|
341
|
+
}
|
|
342
|
+
},
|
|
343
|
+
second: {
|
|
344
|
+
initial: 'nested',
|
|
345
|
+
states: {
|
|
346
|
+
nested: {},
|
|
347
|
+
other: {},
|
|
348
|
+
hist: {
|
|
349
|
+
history: true
|
|
350
|
+
}
|
|
351
|
+
},
|
|
352
|
+
on: {
|
|
353
|
+
NEXT: [
|
|
354
|
+
{
|
|
355
|
+
target: '.hist'
|
|
356
|
+
}
|
|
357
|
+
]
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it('should transition internally to the most recently visited state', () => {
|
|
364
|
+
// {
|
|
365
|
+
// $current: 'first',
|
|
366
|
+
// first: undefined,
|
|
367
|
+
// second: {
|
|
368
|
+
// $current: 'nested',
|
|
369
|
+
// nested: undefined,
|
|
370
|
+
// other: undefined
|
|
371
|
+
// }
|
|
372
|
+
// }
|
|
373
|
+
const state2 = machine.transition(machine.initialState, 'NEXT');
|
|
374
|
+
// {
|
|
375
|
+
// $current: 'second',
|
|
376
|
+
// first: undefined,
|
|
377
|
+
// second: {
|
|
378
|
+
// $current: 'other',
|
|
379
|
+
// nested: undefined,
|
|
380
|
+
// other: undefined
|
|
381
|
+
// }
|
|
382
|
+
// }
|
|
383
|
+
const state3 = machine.transition(state2, 'NEXT');
|
|
384
|
+
// {
|
|
385
|
+
// $current: 'second',
|
|
386
|
+
// first: undefined,
|
|
387
|
+
// second: {
|
|
388
|
+
// $current: 'other',
|
|
389
|
+
// nested: undefined,
|
|
390
|
+
// other: undefined
|
|
391
|
+
// }
|
|
392
|
+
// }
|
|
393
|
+
|
|
394
|
+
assert.deepEqual(state3.value, { second: 'other' });
|
|
395
|
+
});
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
describe('multistage history states', () => {
|
|
399
|
+
const pcWithTurboButtonMachine = Machine({
|
|
400
|
+
key: 'pc-with-turbo-button',
|
|
401
|
+
initial: 'off',
|
|
402
|
+
states: {
|
|
403
|
+
off: {
|
|
404
|
+
on: { POWER: 'starting' }
|
|
405
|
+
},
|
|
406
|
+
starting: {
|
|
407
|
+
on: { STARTED: 'running.H' }
|
|
408
|
+
},
|
|
409
|
+
running: {
|
|
410
|
+
initial: 'normal',
|
|
411
|
+
states: {
|
|
412
|
+
normal: {
|
|
413
|
+
on: { SWITCH_TURBO: 'turbo' }
|
|
414
|
+
},
|
|
415
|
+
turbo: {
|
|
416
|
+
on: { SWITCH_TURBO: 'normal' }
|
|
417
|
+
},
|
|
418
|
+
H: {
|
|
419
|
+
history: true
|
|
420
|
+
}
|
|
421
|
+
},
|
|
422
|
+
on: {
|
|
423
|
+
POWER: 'off'
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
it('should go to the most recently visited state', () => {
|
|
430
|
+
const onTurboState = pcWithTurboButtonMachine.transition(
|
|
431
|
+
'running',
|
|
432
|
+
'SWITCH_TURBO'
|
|
433
|
+
);
|
|
434
|
+
const offState = pcWithTurboButtonMachine.transition(onTurboState, 'POWER');
|
|
435
|
+
const loadingState = pcWithTurboButtonMachine.transition(offState, 'POWER');
|
|
436
|
+
|
|
437
|
+
assert.equal(
|
|
438
|
+
pcWithTurboButtonMachine.transition(loadingState, 'STARTED').toString(),
|
|
439
|
+
'running.turbo'
|
|
440
|
+
);
|
|
441
|
+
});
|
|
442
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { assert } from 'chai';
|
|
2
|
+
import { Machine } from '../src/index';
|
|
3
|
+
|
|
4
|
+
const machine = Machine({
|
|
5
|
+
parallel: true,
|
|
6
|
+
states: {
|
|
7
|
+
A: {
|
|
8
|
+
initial: 'A1',
|
|
9
|
+
states: {
|
|
10
|
+
A1: {},
|
|
11
|
+
A2: {}
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
B: {
|
|
15
|
+
initial: 'B1',
|
|
16
|
+
states: {
|
|
17
|
+
B1: {},
|
|
18
|
+
B2: {}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe('invalid states', () => {
|
|
25
|
+
xit('should reject transitioning from a String state', () => {
|
|
26
|
+
assert.throws(() => machine.transition('A', 'E'));
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
xit('should reject transitioning from empty states', () => {
|
|
30
|
+
assert.throws(() => machine.transition({ A: {}, B: {} }, 'E'));
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should allow transitioning from valid states', () => {
|
|
34
|
+
machine.transition({ A: 'A1', B: 'B1' }, 'E');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should reject transitioning from bad state configs', () => {
|
|
38
|
+
assert.throws(() => machine.transition({ A: 'A3', B: 'B3' }, 'E'));
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
xit('should reject transitioning from partially valid states', () => {
|
|
42
|
+
assert.throws(() => machine.transition({ A: 'A1' }, 'E'));
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
xit("should reject transitioning from regions that don't exist", () => {
|
|
46
|
+
assert.throws(() => machine.transition({ A: 'A1', B: 'B1', Z: 'Z1' }, 'E'));
|
|
47
|
+
});
|
|
48
|
+
});
|