UNPKG

xstate

Version:

Finite State Machines and Statecharts for the Modern Web.

561 lines (541 loc) 18.5 kB
'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;