xstate
Version:
Finite State Machines and Statecharts for the Modern Web.
561 lines (541 loc) • 18.5 kB
JavaScript
'use strict';
var guards_dist_xstateGuards = require('./raise-5872b9e8.cjs.js');
var assign = require('./assign-e9c344ea.cjs.js');
const cache = new WeakMap();
function memo(object, key, fn) {
let memoizedData = cache.get(object);
if (!memoizedData) {
memoizedData = {
[key]: fn()
};
cache.set(object, memoizedData);
} else if (!(key in memoizedData)) {
memoizedData[key] = fn();
}
return memoizedData[key];
}
const EMPTY_OBJECT = {};
const toSerializableAction = action => {
if (typeof action === 'string') {
return {
type: action
};
}
if (typeof action === 'function') {
if ('resolve' in action) {
return {
type: action.type
};
}
return {
type: action.name
};
}
return action;
};
class StateNode {
constructor(/** The raw config used to create the machine. */
config, options) {
this.config = config;
/**
* The relative key of the state node, which represents its location in the
* overall state value.
*/
this.key = void 0;
/** The unique ID of the state node. */
this.id = void 0;
/**
* The type of this state node:
*
* - `'atomic'` - no child state nodes
* - `'compound'` - nested child state nodes (XOR)
* - `'parallel'` - orthogonal nested child state nodes (AND)
* - `'history'` - history state node
* - `'final'` - final state node
*/
this.type = void 0;
/** The string path from the root machine node to this node. */
this.path = void 0;
/** The child state nodes. */
this.states = void 0;
/**
* The type of history on this state node. Can be:
*
* - `'shallow'` - recalls only top-level historical state value
* - `'deep'` - recalls historical state value at all levels
*/
this.history = void 0;
/** The action(s) to be executed upon entering the state node. */
this.entry = void 0;
/** The action(s) to be executed upon exiting the state node. */
this.exit = void 0;
/** The parent state node. */
this.parent = void 0;
/** The root machine node. */
this.machine = void 0;
/**
* The meta data associated with this state node, which will be returned in
* State instances.
*/
this.meta = void 0;
/**
* The output data sent with the "xstate.done.state._id_" event if this is a
* final state node.
*/
this.output = void 0;
/**
* The order this state node appears. Corresponds to the implicit document
* order.
*/
this.order = -1;
this.description = void 0;
this.tags = [];
this.transitions = void 0;
this.always = void 0;
this.parent = options._parent;
this.key = options._key;
this.machine = options._machine;
this.path = this.parent ? this.parent.path.concat(this.key) : [];
this.id = this.config.id || [this.machine.id, ...this.path].join(guards_dist_xstateGuards.STATE_DELIMITER);
this.type = this.config.type || (this.config.states && Object.keys(this.config.states).length ? 'compound' : this.config.history ? 'history' : 'atomic');
this.description = this.config.description;
this.order = this.machine.idMap.size;
this.machine.idMap.set(this.id, this);
this.states = this.config.states ? guards_dist_xstateGuards.mapValues(this.config.states, (stateConfig, key) => {
const stateNode = new StateNode(stateConfig, {
_parent: this,
_key: key,
_machine: this.machine
});
return stateNode;
}) : EMPTY_OBJECT;
if (this.type === 'compound' && !this.config.initial) {
throw new Error(`No initial state specified for compound state node "#${this.id}". Try adding { initial: "${Object.keys(this.states)[0]}" } to the state config.`);
}
// History config
this.history = this.config.history === true ? 'shallow' : this.config.history || false;
this.entry = guards_dist_xstateGuards.toArray(this.config.entry).slice();
this.exit = guards_dist_xstateGuards.toArray(this.config.exit).slice();
this.meta = this.config.meta;
this.output = this.type === 'final' || !this.parent ? this.config.output : undefined;
this.tags = guards_dist_xstateGuards.toArray(config.tags).slice();
}
/** @internal */
_initialize() {
this.transitions = guards_dist_xstateGuards.formatTransitions(this);
if (this.config.always) {
this.always = guards_dist_xstateGuards.toTransitionConfigArray(this.config.always).map(t => guards_dist_xstateGuards.formatTransition(this, guards_dist_xstateGuards.NULL_EVENT, t));
}
Object.keys(this.states).forEach(key => {
this.states[key]._initialize();
});
}
/** The well-structured state node definition. */
get definition() {
return {
id: this.id,
key: this.key,
version: this.machine.version,
type: this.type,
initial: this.initial ? {
target: this.initial.target,
source: this,
actions: this.initial.actions.map(toSerializableAction),
eventType: null,
reenter: false,
toJSON: () => ({
target: this.initial.target.map(t => `#${t.id}`),
source: `#${this.id}`,
actions: this.initial.actions.map(toSerializableAction),
eventType: null
})
} : undefined,
history: this.history,
states: guards_dist_xstateGuards.mapValues(this.states, state => {
return state.definition;
}),
on: this.on,
transitions: [...this.transitions.values()].flat().map(t => ({
...t,
actions: t.actions.map(toSerializableAction)
})),
entry: this.entry.map(toSerializableAction),
exit: this.exit.map(toSerializableAction),
meta: this.meta,
order: this.order || -1,
output: this.output,
invoke: this.invoke,
description: this.description,
tags: this.tags
};
}
/** @internal */
toJSON() {
return this.definition;
}
/** The logic invoked as actors by this state node. */
get invoke() {
return memo(this, 'invoke', () => guards_dist_xstateGuards.toArray(this.config.invoke).map((invokeConfig, i) => {
const {
src,
systemId
} = invokeConfig;
const resolvedId = invokeConfig.id ?? guards_dist_xstateGuards.createInvokeId(this.id, i);
const sourceName = typeof src === 'string' ? src : `xstate.invoke.${guards_dist_xstateGuards.createInvokeId(this.id, i)}`;
return {
...invokeConfig,
src: sourceName,
id: resolvedId,
systemId: systemId,
toJSON() {
const {
onDone,
onError,
...invokeDefValues
} = invokeConfig;
return {
...invokeDefValues,
type: 'xstate.invoke',
src: sourceName,
id: resolvedId
};
}
};
}));
}
/** The mapping of events to transitions. */
get on() {
return memo(this, 'on', () => {
const transitions = this.transitions;
return [...transitions].flatMap(([descriptor, t]) => t.map(t => [descriptor, t])).reduce((map, [descriptor, transition]) => {
map[descriptor] = map[descriptor] || [];
map[descriptor].push(transition);
return map;
}, {});
});
}
get after() {
return memo(this, 'delayedTransitions', () => guards_dist_xstateGuards.getDelayedTransitions(this));
}
get initial() {
return memo(this, 'initial', () => guards_dist_xstateGuards.formatInitialTransition(this, this.config.initial));
}
/** @internal */
next(snapshot, event) {
const eventType = event.type;
const actions = [];
let selectedTransition;
const candidates = memo(this, `candidates-${eventType}`, () => guards_dist_xstateGuards.getCandidates(this, eventType));
for (const candidate of candidates) {
const {
guard
} = candidate;
const resolvedContext = snapshot.context;
let guardPassed = false;
try {
guardPassed = !guard || guards_dist_xstateGuards.evaluateGuard(guard, resolvedContext, event, snapshot);
} catch (err) {
const guardType = typeof guard === 'string' ? guard : typeof guard === 'object' ? guard.type : undefined;
throw new Error(`Unable to evaluate guard ${guardType ? `'${guardType}' ` : ''}in transition for event '${eventType}' in state node '${this.id}':\n${err.message}`);
}
if (guardPassed) {
actions.push(...candidate.actions);
selectedTransition = candidate;
break;
}
}
return selectedTransition ? [selectedTransition] : undefined;
}
/** All the event types accepted by this state node and its descendants. */
get events() {
return memo(this, 'events', () => {
const {
states
} = this;
const events = new Set(this.ownEvents);
if (states) {
for (const stateId of Object.keys(states)) {
const state = states[stateId];
if (state.states) {
for (const event of state.events) {
events.add(`${event}`);
}
}
}
}
return Array.from(events);
});
}
/**
* All the events that have transitions directly from this state node.
*
* Excludes any inert events.
*/
get ownEvents() {
const events = new Set([...this.transitions.keys()].filter(descriptor => {
return this.transitions.get(descriptor).some(transition => !(!transition.target && !transition.actions.length && !transition.reenter));
}));
return Array.from(events);
}
}
const STATE_IDENTIFIER = '#';
class StateMachine {
constructor(/** The raw config used to create the machine. */
config, implementations) {
this.config = config;
/** The machine's own version. */
this.version = void 0;
this.schemas = void 0;
this.implementations = void 0;
/** @internal */
this.__xstatenode = true;
/** @internal */
this.idMap = new Map();
this.root = void 0;
this.id = void 0;
this.states = void 0;
this.events = void 0;
this.id = config.id || '(machine)';
this.implementations = {
actors: implementations?.actors ?? {},
actions: implementations?.actions ?? {},
delays: implementations?.delays ?? {},
guards: implementations?.guards ?? {}
};
this.version = this.config.version;
this.schemas = this.config.schemas;
this.transition = this.transition.bind(this);
this.getInitialSnapshot = this.getInitialSnapshot.bind(this);
this.getPersistedSnapshot = this.getPersistedSnapshot.bind(this);
this.restoreSnapshot = this.restoreSnapshot.bind(this);
this.start = this.start.bind(this);
this.root = new StateNode(config, {
_key: this.id,
_machine: this
});
this.root._initialize();
this.states = this.root.states; // TODO: remove!
this.events = this.root.events;
}
/**
* Clones this state machine with the provided implementations.
*
* @param implementations Options (`actions`, `guards`, `actors`, `delays`) to
* recursively merge with the existing options.
* @returns A new `StateMachine` instance with the provided implementations.
*/
provide(implementations) {
const {
actions,
guards,
actors,
delays
} = this.implementations;
return new StateMachine(this.config, {
actions: {
...actions,
...implementations.actions
},
guards: {
...guards,
...implementations.guards
},
actors: {
...actors,
...implementations.actors
},
delays: {
...delays,
...implementations.delays
}
});
}
resolveState(config) {
const resolvedStateValue = guards_dist_xstateGuards.resolveStateValue(this.root, config.value);
const nodeSet = guards_dist_xstateGuards.getAllStateNodes(guards_dist_xstateGuards.getStateNodes(this.root, resolvedStateValue));
return guards_dist_xstateGuards.createMachineSnapshot({
_nodes: [...nodeSet],
context: config.context || {},
children: {},
status: guards_dist_xstateGuards.isInFinalState(nodeSet, this.root) ? 'done' : config.status || 'active',
output: config.output,
error: config.error,
historyValue: config.historyValue
}, this);
}
/**
* Determines the next snapshot given the current `snapshot` and received
* `event`. Calculates a full macrostep from all microsteps.
*
* @param snapshot The current snapshot
* @param event The received event
*/
transition(snapshot, event, actorScope) {
return guards_dist_xstateGuards.macrostep(snapshot, event, actorScope, []).snapshot;
}
/**
* Determines the next state given the current `state` and `event`. Calculates
* a microstep.
*
* @param state The current state
* @param event The received event
*/
microstep(snapshot, event, actorScope) {
return guards_dist_xstateGuards.macrostep(snapshot, event, actorScope, []).microstates;
}
getTransitionData(snapshot, event) {
return guards_dist_xstateGuards.transitionNode(this.root, snapshot.value, snapshot, event) || [];
}
/**
* The initial state _before_ evaluating any microsteps. This "pre-initial"
* state is provided to initial actions executed in the initial state.
*/
getPreInitialState(actorScope, initEvent, internalQueue) {
const {
context
} = this.config;
const preInitial = guards_dist_xstateGuards.createMachineSnapshot({
context: typeof context !== 'function' && context ? context : {},
_nodes: [this.root],
children: {},
status: 'active'
}, this);
if (typeof context === 'function') {
const assignment = ({
spawn,
event,
self
}) => context({
spawn,
input: event.input,
self
});
return guards_dist_xstateGuards.resolveActionsAndContext(preInitial, initEvent, actorScope, [assign.assign(assignment)], internalQueue, undefined);
}
return preInitial;
}
/**
* Returns the initial `State` instance, with reference to `self` as an
* `ActorRef`.
*/
getInitialSnapshot(actorScope, input) {
const initEvent = guards_dist_xstateGuards.createInitEvent(input); // TODO: fix;
const internalQueue = [];
const preInitialState = this.getPreInitialState(actorScope, initEvent, internalQueue);
const nextState = guards_dist_xstateGuards.microstep([{
target: [...guards_dist_xstateGuards.getInitialStateNodes(this.root)],
source: this.root,
reenter: true,
actions: [],
eventType: null,
toJSON: null // TODO: fix
}], preInitialState, actorScope, initEvent, true, internalQueue);
const {
snapshot: macroState
} = guards_dist_xstateGuards.macrostep(nextState, initEvent, actorScope, internalQueue);
return macroState;
}
start(snapshot) {
Object.values(snapshot.children).forEach(child => {
if (child.getSnapshot().status === 'active') {
child.start();
}
});
}
getStateNodeById(stateId) {
const fullPath = guards_dist_xstateGuards.toStatePath(stateId);
const relativePath = fullPath.slice(1);
const resolvedStateId = guards_dist_xstateGuards.isStateId(fullPath[0]) ? fullPath[0].slice(STATE_IDENTIFIER.length) : fullPath[0];
const stateNode = this.idMap.get(resolvedStateId);
if (!stateNode) {
throw new Error(`Child state node '#${resolvedStateId}' does not exist on machine '${this.id}'`);
}
return guards_dist_xstateGuards.getStateNodeByPath(stateNode, relativePath);
}
get definition() {
return this.root.definition;
}
toJSON() {
return this.definition;
}
getPersistedSnapshot(snapshot, options) {
return guards_dist_xstateGuards.getPersistedSnapshot(snapshot, options);
}
restoreSnapshot(snapshot, _actorScope) {
const children = {};
const snapshotChildren = snapshot.children;
Object.keys(snapshotChildren).forEach(actorId => {
const actorData = snapshotChildren[actorId];
const childState = actorData.snapshot;
const src = actorData.src;
const logic = typeof src === 'string' ? guards_dist_xstateGuards.resolveReferencedActor(this, src) : src;
if (!logic) {
return;
}
const actorRef = guards_dist_xstateGuards.createActor(logic, {
id: actorId,
parent: _actorScope.self,
syncSnapshot: actorData.syncSnapshot,
snapshot: childState,
src,
systemId: actorData.systemId
});
children[actorId] = actorRef;
});
function resolveHistoryReferencedState(root, referenced) {
if (referenced instanceof StateNode) {
return referenced;
}
try {
return root.machine.getStateNodeById(referenced.id);
} catch {
}
}
function reviveHistoryValue(root, historyValue) {
if (!historyValue || typeof historyValue !== 'object') {
return {};
}
const revived = {};
for (const key in historyValue) {
const arr = historyValue[key];
for (const item of arr) {
const resolved = resolveHistoryReferencedState(root, item);
if (!resolved) {
continue;
}
revived[key] ??= [];
revived[key].push(resolved);
}
}
return revived;
}
const revivedHistoryValue = reviveHistoryValue(this.root, snapshot.historyValue);
const restoredSnapshot = guards_dist_xstateGuards.createMachineSnapshot({
...snapshot,
children,
_nodes: Array.from(guards_dist_xstateGuards.getAllStateNodes(guards_dist_xstateGuards.getStateNodes(this.root, snapshot.value))),
historyValue: revivedHistoryValue
}, this);
const seen = new Set();
function reviveContext(contextPart, children) {
if (seen.has(contextPart)) {
return;
}
seen.add(contextPart);
for (const key in contextPart) {
const value = contextPart[key];
if (value && typeof value === 'object') {
if ('xstate$$type' in value && value.xstate$$type === guards_dist_xstateGuards.$$ACTOR_TYPE) {
contextPart[key] = children[value.id];
continue;
}
reviveContext(value, children);
}
}
}
reviveContext(restoredSnapshot.context, children);
return restoredSnapshot;
}
}
exports.StateMachine = StateMachine;
exports.StateNode = StateNode;