UNPKG

xstate

Version:

Finite State Machines and Statecharts for the Modern Web.

1,723 lines (1,672 loc) 82.4 kB
'use strict'; var dev_dist_xstateDev = require('../dev/dist/xstate-dev.cjs.js'); class Mailbox { constructor(_process) { this._process = _process; this._active = false; this._current = null; this._last = null; } start() { this._active = true; this.flush(); } clear() { // we can't set _current to null because we might be currently processing // and enqueue following clear shouldnt start processing the enqueued item immediately if (this._current) { this._current.next = null; this._last = this._current; } } enqueue(event) { const enqueued = { value: event, next: null }; if (this._current) { this._last.next = enqueued; this._last = enqueued; return; } this._current = enqueued; this._last = enqueued; if (this._active) { this.flush(); } } flush() { while (this._current) { // atm the given _process is responsible for implementing proper try/catch handling // we assume here that this won't throw in a way that can affect this mailbox const consumed = this._current; this._process(consumed.value); this._current = consumed.next; } this._last = null; } } const STATE_DELIMITER = '.'; const TARGETLESS_KEY = ''; const NULL_EVENT = ''; const STATE_IDENTIFIER = '#'; const WILDCARD = '*'; const XSTATE_INIT = 'xstate.init'; const XSTATE_ERROR = 'xstate.error'; const XSTATE_STOP = 'xstate.stop'; /** * Returns an event that represents an implicit event that * is sent after the specified `delay`. * * @param delayRef The delay in milliseconds * @param id The state node ID where this event is handled */ function createAfterEvent(delayRef, id) { return { type: `xstate.after.${delayRef}.${id}` }; } /** * Returns an event that represents that a final state node * has been reached in the parent state node. * * @param id The final state node's parent state node `id` * @param output The data to pass into the event */ function createDoneStateEvent(id, output) { return { type: `xstate.done.state.${id}`, output }; } /** * Returns an event that represents that an invoked service has terminated. * * An invoked service is terminated when it has reached a top-level final state node, * but not when it is canceled. * * @param invokeId The invoked service ID * @param output The data to pass into the event */ function createDoneActorEvent(invokeId, output) { return { type: `xstate.done.actor.${invokeId}`, output }; } function createErrorActorEvent(id, error) { return { type: `xstate.error.actor.${id}`, error }; } function createInitEvent(input) { return { type: XSTATE_INIT, input }; } /** * This function makes sure that unhandled errors are thrown in a separate macrotask. * It allows those errors to be detected by global error handlers and reported to bug tracking services * without interrupting our own stack of execution. * * @param err error to be thrown */ function reportUnhandledError(err) { setTimeout(() => { throw err; }); } const symbolObservable = (() => typeof Symbol === 'function' && Symbol.observable || '@@observable')(); function createScheduledEventId(actorRef, id) { return `${actorRef.sessionId}.${id}`; } let idCounter = 0; function createSystem(rootActor, options) { const children = new Map(); const keyedActors = new Map(); const reverseKeyedActors = new WeakMap(); const inspectionObservers = new Set(); const timerMap = {}; const { clock, logger } = options; const scheduler = { schedule: (source, target, event, delay, id = Math.random().toString(36).slice(2)) => { const scheduledEvent = { source, target, event, delay, id, startedAt: Date.now() }; const scheduledEventId = createScheduledEventId(source, id); system._snapshot._scheduledEvents[scheduledEventId] = scheduledEvent; const timeout = clock.setTimeout(() => { delete timerMap[scheduledEventId]; delete system._snapshot._scheduledEvents[scheduledEventId]; system._relay(source, target, event); }, delay); timerMap[scheduledEventId] = timeout; }, cancel: (source, id) => { const scheduledEventId = createScheduledEventId(source, id); const timeout = timerMap[scheduledEventId]; delete timerMap[scheduledEventId]; delete system._snapshot._scheduledEvents[scheduledEventId]; clock.clearTimeout(timeout); }, cancelAll: actorRef => { for (const scheduledEventId in system._snapshot._scheduledEvents) { const scheduledEvent = system._snapshot._scheduledEvents[scheduledEventId]; if (scheduledEvent.source === actorRef) { scheduler.cancel(actorRef, scheduledEvent.id); } } } }; const sendInspectionEvent = event => { if (!inspectionObservers.size) { return; } const resolvedInspectionEvent = { ...event, rootId: rootActor.sessionId }; inspectionObservers.forEach(observer => observer.next?.(resolvedInspectionEvent)); }; const system = { _snapshot: { _scheduledEvents: (options?.snapshot && options.snapshot.scheduler) ?? {} }, _bookId: () => `x:${idCounter++}`, _register: (sessionId, actorRef) => { children.set(sessionId, actorRef); return sessionId; }, _unregister: actorRef => { children.delete(actorRef.sessionId); const systemId = reverseKeyedActors.get(actorRef); if (systemId !== undefined) { keyedActors.delete(systemId); reverseKeyedActors.delete(actorRef); } }, get: systemId => { return keyedActors.get(systemId); }, _set: (systemId, actorRef) => { const existing = keyedActors.get(systemId); if (existing && existing !== actorRef) { throw new Error(`Actor with system ID '${systemId}' already exists.`); } keyedActors.set(systemId, actorRef); reverseKeyedActors.set(actorRef, systemId); }, inspect: observer => { inspectionObservers.add(observer); }, _sendInspectionEvent: sendInspectionEvent, _relay: (source, target, event) => { system._sendInspectionEvent({ type: '@xstate.event', sourceRef: source, actorRef: target, event }); target._send(event); }, scheduler, getSnapshot: () => { return { _scheduledEvents: { ...system._snapshot._scheduledEvents } }; }, start: () => { const scheduledEvents = system._snapshot._scheduledEvents; system._snapshot._scheduledEvents = {}; for (const scheduledId in scheduledEvents) { const { source, target, event, delay, id } = scheduledEvents[scheduledId]; scheduler.schedule(source, target, event, delay, id); } }, _clock: clock, _logger: logger }; return system; } function matchesState(parentStateId, childStateId) { const parentStateValue = toStateValue(parentStateId); const childStateValue = toStateValue(childStateId); if (typeof childStateValue === 'string') { if (typeof parentStateValue === 'string') { return childStateValue === parentStateValue; } // Parent more specific than child return false; } if (typeof parentStateValue === 'string') { return parentStateValue in childStateValue; } return Object.keys(parentStateValue).every(key => { if (!(key in childStateValue)) { return false; } return matchesState(parentStateValue[key], childStateValue[key]); }); } function toStatePath(stateId) { if (isArray(stateId)) { return stateId; } let result = []; let segment = ''; for (let i = 0; i < stateId.length; i++) { const char = stateId.charCodeAt(i); switch (char) { // \ case 92: // consume the next character segment += stateId[i + 1]; // and skip over it i++; continue; // . case 46: result.push(segment); segment = ''; continue; } segment += stateId[i]; } result.push(segment); return result; } function toStateValue(stateValue) { if (isMachineSnapshot(stateValue)) { return stateValue.value; } if (typeof stateValue !== 'string') { return stateValue; } const statePath = toStatePath(stateValue); return pathToStateValue(statePath); } function pathToStateValue(statePath) { if (statePath.length === 1) { return statePath[0]; } const value = {}; let marker = value; for (let i = 0; i < statePath.length - 1; i++) { if (i === statePath.length - 2) { marker[statePath[i]] = statePath[i + 1]; } else { const previous = marker; marker = {}; previous[statePath[i]] = marker; } } return value; } function mapValues(collection, iteratee) { const result = {}; const collectionKeys = Object.keys(collection); for (let i = 0; i < collectionKeys.length; i++) { const key = collectionKeys[i]; result[key] = iteratee(collection[key], key, collection, i); } return result; } function toArrayStrict(value) { if (isArray(value)) { return value; } return [value]; } function toArray(value) { if (value === undefined) { return []; } return toArrayStrict(value); } function resolveOutput(mapper, context, event, self) { if (typeof mapper === 'function') { return mapper({ context, event, self }); } return mapper; } function isArray(value) { return Array.isArray(value); } function isErrorActorEvent(event) { return event.type.startsWith('xstate.error.actor'); } function toTransitionConfigArray(configLike) { return toArrayStrict(configLike).map(transitionLike => { if (typeof transitionLike === 'undefined' || typeof transitionLike === 'string') { return { target: transitionLike }; } return transitionLike; }); } function normalizeTarget(target) { if (target === undefined || target === TARGETLESS_KEY) { return undefined; } return toArray(target); } function toObserver(nextHandler, errorHandler, completionHandler) { const isObserver = typeof nextHandler === 'object'; const self = isObserver ? nextHandler : undefined; return { next: (isObserver ? nextHandler.next : nextHandler)?.bind(self), error: (isObserver ? nextHandler.error : errorHandler)?.bind(self), complete: (isObserver ? nextHandler.complete : completionHandler)?.bind(self) }; } function createInvokeId(stateNodeId, index) { return `${index}.${stateNodeId}`; } function resolveReferencedActor(machine, src) { const match = src.match(/^xstate\.invoke\.(\d+)\.(.*)/); if (!match) { return machine.implementations.actors[src]; } const [, indexStr, nodeId] = match; const node = machine.getStateNodeById(nodeId); const invokeConfig = node.config.invoke; return (Array.isArray(invokeConfig) ? invokeConfig[indexStr] : invokeConfig).src; } function getAllOwnEventDescriptors(snapshot) { return [...new Set([...snapshot._nodes.flatMap(sn => sn.ownEvents)])]; } const $$ACTOR_TYPE = 1; // those values are currently used by @xstate/react directly so it's important to keep the assigned values in sync let ProcessingStatus = /*#__PURE__*/function (ProcessingStatus) { ProcessingStatus[ProcessingStatus["NotStarted"] = 0] = "NotStarted"; ProcessingStatus[ProcessingStatus["Running"] = 1] = "Running"; ProcessingStatus[ProcessingStatus["Stopped"] = 2] = "Stopped"; return ProcessingStatus; }({}); const defaultOptions = { clock: { setTimeout: (fn, ms) => { return setTimeout(fn, ms); }, clearTimeout: id => { return clearTimeout(id); } }, logger: console.log.bind(console), devTools: false }; /** * An Actor is a running process that can receive events, send events and change its behavior based on the events it receives, which can cause effects outside of the actor. When you run a state machine, it becomes an actor. */ class Actor { /** * Creates a new actor instance for the given logic with the provided options, if any. * * @param logic The logic to create an actor from * @param options Actor options */ constructor(logic, options) { this.logic = logic; /** * The current internal state of the actor. */ this._snapshot = void 0; /** * The clock that is responsible for setting and clearing timeouts, such as delayed events and transitions. */ this.clock = void 0; this.options = void 0; /** * The unique identifier for this actor relative to its parent. */ this.id = void 0; this.mailbox = new Mailbox(this._process.bind(this)); this.observers = new Set(); this.eventListeners = new Map(); this.logger = void 0; /** @internal */ this._processingStatus = ProcessingStatus.NotStarted; // Actor Ref this._parent = void 0; /** @internal */ this._syncSnapshot = void 0; this.ref = void 0; // TODO: add typings for system this._actorScope = void 0; this._systemId = void 0; /** * The globally unique process ID for this invocation. */ this.sessionId = void 0; /** * The system to which this actor belongs. */ this.system = void 0; this._doneEvent = void 0; this.src = void 0; // array of functions to defer this._deferred = []; const resolvedOptions = { ...defaultOptions, ...options }; const { clock, logger, parent, syncSnapshot, id, systemId, inspect } = resolvedOptions; this.system = parent ? parent.system : createSystem(this, { clock, logger }); if (inspect && !parent) { // Always inspect at the system-level this.system.inspect(toObserver(inspect)); } this.sessionId = this.system._bookId(); this.id = id ?? this.sessionId; this.logger = options?.logger ?? this.system._logger; this.clock = options?.clock ?? this.system._clock; this._parent = parent; this._syncSnapshot = syncSnapshot; this.options = resolvedOptions; this.src = resolvedOptions.src ?? logic; this.ref = this; this._actorScope = { self: this, id: this.id, sessionId: this.sessionId, logger: this.logger, defer: fn => { this._deferred.push(fn); }, system: this.system, stopChild: child => { if (child._parent !== this) { throw new Error(`Cannot stop child actor ${child.id} of ${this.id} because it is not a child`); } child._stop(); }, emit: emittedEvent => { const listeners = this.eventListeners.get(emittedEvent.type); if (!listeners) { return; } for (const handler of Array.from(listeners)) { handler(emittedEvent); } } }; // Ensure that the send method is bound to this Actor instance // if destructured this.send = this.send.bind(this); this.system._sendInspectionEvent({ type: '@xstate.actor', actorRef: this }); if (systemId) { this._systemId = systemId; this.system._set(systemId, this); } this._initState(options?.snapshot ?? options?.state); if (systemId && this._snapshot.status !== 'active') { this.system._unregister(this); } } _initState(persistedState) { try { this._snapshot = persistedState ? this.logic.restoreSnapshot ? this.logic.restoreSnapshot(persistedState, this._actorScope) : persistedState : this.logic.getInitialSnapshot(this._actorScope, this.options?.input); } catch (err) { // if we get here then it means that we assign a value to this._snapshot that is not of the correct type // we can't get the true `TSnapshot & { status: 'error'; }`, it's impossible // so right now this is a lie of sorts this._snapshot = { status: 'error', output: undefined, error: err }; } } update(snapshot, event) { // Update state this._snapshot = snapshot; // Execute deferred effects let deferredFn; while (deferredFn = this._deferred.shift()) { try { deferredFn(); } catch (err) { // this error can only be caught when executing *initial* actions // it's the only time when we call actions provided by the user through those deferreds // when the actor is already running we always execute them synchronously while transitioning // no "builtin deferred" should actually throw an error since they are either safe // or the control flow is passed through the mailbox and errors should be caught by the `_process` used by the mailbox this._deferred.length = 0; this._snapshot = { ...snapshot, status: 'error', error: err }; } } switch (this._snapshot.status) { case 'active': for (const observer of this.observers) { try { observer.next?.(snapshot); } catch (err) { reportUnhandledError(err); } } break; case 'done': // next observers are meant to be notified about done snapshots // this can be seen as something that is different from how observable work // but with observables `complete` callback is called without any arguments // it's more ergonomic for XState to treat a done snapshot as a "next" value // and the completion event as something that is separate, // something that merely follows emitting that done snapshot for (const observer of this.observers) { try { observer.next?.(snapshot); } catch (err) { reportUnhandledError(err); } } this._stopProcedure(); this._complete(); this._doneEvent = createDoneActorEvent(this.id, this._snapshot.output); if (this._parent) { this.system._relay(this, this._parent, this._doneEvent); } break; case 'error': this._error(this._snapshot.error); break; } this.system._sendInspectionEvent({ type: '@xstate.snapshot', actorRef: this, event, snapshot }); } /** * Subscribe an observer to an actor’s snapshot values. * * @remarks * The observer will receive the actor’s snapshot value when it is emitted. The observer can be: * - A plain function that receives the latest snapshot, or * - An observer object whose `.next(snapshot)` method receives the latest snapshot * * @example * ```ts * // Observer as a plain function * const subscription = actor.subscribe((snapshot) => { * console.log(snapshot); * }); * ``` * * @example * ```ts * // Observer as an object * const subscription = actor.subscribe({ * next(snapshot) { * console.log(snapshot); * }, * error(err) { * // ... * }, * complete() { * // ... * }, * }); * ``` * * The return value of `actor.subscribe(observer)` is a subscription object that has an `.unsubscribe()` method. You can call `subscription.unsubscribe()` to unsubscribe the observer: * * @example * ```ts * const subscription = actor.subscribe((snapshot) => { * // ... * }); * * // Unsubscribe the observer * subscription.unsubscribe(); * ``` * * When the actor is stopped, all of its observers will automatically be unsubscribed. * * @param observer - Either a plain function that receives the latest snapshot, or an observer object whose `.next(snapshot)` method receives the latest snapshot */ subscribe(nextListenerOrObserver, errorListener, completeListener) { const observer = toObserver(nextListenerOrObserver, errorListener, completeListener); if (this._processingStatus !== ProcessingStatus.Stopped) { this.observers.add(observer); } else { switch (this._snapshot.status) { case 'done': try { observer.complete?.(); } catch (err) { reportUnhandledError(err); } break; case 'error': { const err = this._snapshot.error; if (!observer.error) { reportUnhandledError(err); } else { try { observer.error(err); } catch (err) { reportUnhandledError(err); } } break; } } } return { unsubscribe: () => { this.observers.delete(observer); } }; } on(type, handler) { let listeners = this.eventListeners.get(type); if (!listeners) { listeners = new Set(); this.eventListeners.set(type, listeners); } const wrappedHandler = handler.bind(undefined); listeners.add(wrappedHandler); return { unsubscribe: () => { listeners.delete(wrappedHandler); } }; } /** * Starts the Actor from the initial state */ start() { if (this._processingStatus === ProcessingStatus.Running) { // Do not restart the service if it is already started return this; } if (this._syncSnapshot) { this.subscribe({ next: snapshot => { if (snapshot.status === 'active') { this.system._relay(this, this._parent, { type: `xstate.snapshot.${this.id}`, snapshot }); } }, error: () => {} }); } this.system._register(this.sessionId, this); if (this._systemId) { this.system._set(this._systemId, this); } this._processingStatus = ProcessingStatus.Running; // TODO: this isn't correct when rehydrating const initEvent = createInitEvent(this.options.input); this.system._sendInspectionEvent({ type: '@xstate.event', sourceRef: this._parent, actorRef: this, event: initEvent }); const status = this._snapshot.status; switch (status) { case 'done': // a state machine can be "done" upon initialization (it could reach a final state using initial microsteps) // we still need to complete observers, flush deferreds etc this.update(this._snapshot, initEvent); // TODO: rethink cleanup of observers, mailbox, etc return this; case 'error': this._error(this._snapshot.error); return this; } if (!this._parent) { this.system.start(); } if (this.logic.start) { try { this.logic.start(this._snapshot, this._actorScope); } catch (err) { this._snapshot = { ...this._snapshot, status: 'error', error: err }; this._error(err); return this; } } // TODO: this notifies all subscribers but usually this is redundant // there is no real change happening here // we need to rethink if this needs to be refactored this.update(this._snapshot, initEvent); if (this.options.devTools) { this.attachDevTools(); } this.mailbox.start(); return this; } _process(event) { let nextState; let caughtError; try { nextState = this.logic.transition(this._snapshot, event, this._actorScope); } catch (err) { // we wrap it in a box so we can rethrow it later even if falsy value gets caught here caughtError = { err }; } if (caughtError) { const { err } = caughtError; this._snapshot = { ...this._snapshot, status: 'error', error: err }; this._error(err); return; } this.update(nextState, event); if (event.type === XSTATE_STOP) { this._stopProcedure(); this._complete(); } } _stop() { if (this._processingStatus === ProcessingStatus.Stopped) { return this; } this.mailbox.clear(); if (this._processingStatus === ProcessingStatus.NotStarted) { this._processingStatus = ProcessingStatus.Stopped; return this; } this.mailbox.enqueue({ type: XSTATE_STOP }); return this; } /** * Stops the Actor and unsubscribe all listeners. */ stop() { if (this._parent) { throw new Error('A non-root actor cannot be stopped directly.'); } return this._stop(); } _complete() { for (const observer of this.observers) { try { observer.complete?.(); } catch (err) { reportUnhandledError(err); } } this.observers.clear(); } _reportError(err) { if (!this.observers.size) { if (!this._parent) { reportUnhandledError(err); } return; } let reportError = false; for (const observer of this.observers) { const errorListener = observer.error; reportError ||= !errorListener; try { errorListener?.(err); } catch (err2) { reportUnhandledError(err2); } } this.observers.clear(); if (reportError) { reportUnhandledError(err); } } _error(err) { this._stopProcedure(); this._reportError(err); if (this._parent) { this.system._relay(this, this._parent, createErrorActorEvent(this.id, err)); } } // TODO: atm children don't belong entirely to the actor so // in a way - it's not even super aware of them // so we can't stop them from here but we really should! // right now, they are being stopped within the machine's transition // but that could throw and leave us with "orphaned" active actors _stopProcedure() { if (this._processingStatus !== ProcessingStatus.Running) { // Actor already stopped; do nothing return this; } // Cancel all delayed events this.system.scheduler.cancelAll(this); // TODO: mailbox.reset this.mailbox.clear(); // TODO: after `stop` we must prepare ourselves for receiving events again // events sent *after* stop signal must be queued // it seems like this should be the common behavior for all of our consumers // so perhaps this should be unified somehow for all of them this.mailbox = new Mailbox(this._process.bind(this)); this._processingStatus = ProcessingStatus.Stopped; this.system._unregister(this); return this; } /** * @internal */ _send(event) { if (this._processingStatus === ProcessingStatus.Stopped) { return; } this.mailbox.enqueue(event); } /** * Sends an event to the running Actor to trigger a transition. * * @param event The event to send */ send(event) { this.system._relay(undefined, this, event); } attachDevTools() { const { devTools } = this.options; if (devTools) { const resolvedDevToolsAdapter = typeof devTools === 'function' ? devTools : dev_dist_xstateDev.devToolsAdapter; resolvedDevToolsAdapter(this); } } toJSON() { return { xstate$$type: $$ACTOR_TYPE, id: this.id }; } /** * Obtain the internal state of the actor, which can be persisted. * * @remarks * The internal state can be persisted from any actor, not only machines. * * Note that the persisted state is not the same as the snapshot from {@link Actor.getSnapshot}. Persisted state represents the internal state of the actor, while snapshots represent the actor's last emitted value. * * Can be restored with {@link ActorOptions.state} * * @see https://stately.ai/docs/persistence */ getPersistedSnapshot(options) { return this.logic.getPersistedSnapshot(this._snapshot, options); } [symbolObservable]() { return this; } /** * Read an actor’s snapshot synchronously. * * @remarks * The snapshot represent an actor's last emitted value. * * When an actor receives an event, its internal state may change. * An actor may emit a snapshot when a state transition occurs. * * Note that some actors, such as callback actors generated with `fromCallback`, will not emit snapshots. * * @see {@link Actor.subscribe} to subscribe to an actor’s snapshot values. * @see {@link Actor.getPersistedSnapshot} to persist the internal state of an actor (which is more than just a snapshot). */ getSnapshot() { return this._snapshot; } } /** * Creates a new actor instance for the given actor logic with the provided options, if any. * * @remarks * When you create an actor from actor logic via `createActor(logic)`, you implicitly create an actor system where the created actor is the root actor. * Any actors spawned from this root actor and its descendants are part of that actor system. * * @example * ```ts * import { createActor } from 'xstate'; * import { someActorLogic } from './someActorLogic.ts'; * * // Creating the actor, which implicitly creates an actor system with itself as the root actor * const actor = createActor(someActorLogic); * * actor.subscribe((snapshot) => { * console.log(snapshot); * }); * * // Actors must be started by calling `actor.start()`, which will also start the actor system. * actor.start(); * * // Actors can receive events * actor.send({ type: 'someEvent' }); * * // You can stop root actors by calling `actor.stop()`, which will also stop the actor system and all actors in that system. * actor.stop(); * ``` * * @param logic - The actor logic to create an actor from. For a state machine actor logic creator, see {@link createMachine}. Other actor logic creators include {@link fromCallback}, {@link fromEventObservable}, {@link fromObservable}, {@link fromPromise}, and {@link fromTransition}. * @param options - Actor options */ function createActor(logic, ...[options]) { return new Actor(logic, options); } /** * Creates a new Interpreter instance for the given machine with the provided options, if any. * * @deprecated Use `createActor` instead */ const interpret = createActor; /** * @deprecated Use `Actor` instead. */ function resolveCancel(_, snapshot, actionArgs, actionParams, { sendId }) { const resolvedSendId = typeof sendId === 'function' ? sendId(actionArgs, actionParams) : sendId; return [snapshot, resolvedSendId]; } function executeCancel(actorScope, resolvedSendId) { actorScope.defer(() => { actorScope.system.scheduler.cancel(actorScope.self, resolvedSendId); }); } /** * Cancels a delayed `sendTo(...)` action that is waiting to be executed. The canceled `sendTo(...)` action * will not send its event or execute, unless the `delay` has already elapsed before `cancel(...)` is called. * * @param sendId The `id` of the `sendTo(...)` action to cancel. * * @example ```ts import { createMachine, sendTo, cancel } from 'xstate'; const machine = createMachine({ // ... on: { sendEvent: { actions: sendTo('some-actor', { type: 'someEvent' }, { id: 'some-id', delay: 1000 }) }, cancelEvent: { actions: cancel('some-id') } } }); ``` */ function cancel(sendId) { function cancel(args, params) { } cancel.type = 'xstate.cancel'; cancel.sendId = sendId; cancel.resolve = resolveCancel; cancel.execute = executeCancel; return cancel; } function resolveSpawn(actorScope, snapshot, actionArgs, _actionParams, { id, systemId, src, input, syncSnapshot }) { const logic = typeof src === 'string' ? resolveReferencedActor(snapshot.machine, src) : src; const resolvedId = typeof id === 'function' ? id(actionArgs) : id; let actorRef; if (logic) { actorRef = createActor(logic, { id: resolvedId, src, parent: actorScope.self, syncSnapshot, systemId, input: typeof input === 'function' ? input({ context: snapshot.context, event: actionArgs.event, self: actorScope.self }) : input }); } return [cloneMachineSnapshot(snapshot, { children: { ...snapshot.children, [resolvedId]: actorRef } }), { id, actorRef }]; } function executeSpawn(actorScope, { id, actorRef }) { if (!actorRef) { return; } actorScope.defer(() => { if (actorRef._processingStatus === ProcessingStatus.Stopped) { return; } actorRef.start(); }); } function spawnChild(...[src, { id, systemId, input, syncSnapshot = false } = {}]) { function spawnChild(args, params) { } spawnChild.type = 'snapshot.spawnChild'; spawnChild.id = id; spawnChild.systemId = systemId; spawnChild.src = src; spawnChild.input = input; spawnChild.syncSnapshot = syncSnapshot; spawnChild.resolve = resolveSpawn; spawnChild.execute = executeSpawn; return spawnChild; } function resolveStop(_, snapshot, args, actionParams, { actorRef }) { const actorRefOrString = typeof actorRef === 'function' ? actorRef(args, actionParams) : actorRef; const resolvedActorRef = typeof actorRefOrString === 'string' ? snapshot.children[actorRefOrString] : actorRefOrString; let children = snapshot.children; if (resolvedActorRef) { children = { ...children }; delete children[resolvedActorRef.id]; } return [cloneMachineSnapshot(snapshot, { children }), resolvedActorRef]; } function executeStop(actorScope, actorRef) { if (!actorRef) { return; } // we need to eagerly unregister it here so a new actor with the same systemId can be registered immediately // since we defer actual stopping of the actor but we don't defer actor creations (and we can't do that) // this could throw on `systemId` collision, for example, when dealing with reentering transitions actorScope.system._unregister(actorRef); // this allows us to prevent an actor from being started if it gets stopped within the same macrostep // this can happen, for example, when the invoking state is being exited immediately by an always transition if (actorRef._processingStatus !== ProcessingStatus.Running) { actorScope.stopChild(actorRef); return; } // stopping a child enqueues a stop event in the child actor's mailbox // we need for all of the already enqueued events to be processed before we stop the child // the parent itself might want to send some events to a child (for example from exit actions on the invoking state) // and we don't want to ignore those events actorScope.defer(() => { actorScope.stopChild(actorRef); }); } /** * Stops a child actor. * * @param actorRef The actor to stop. */ function stopChild(actorRef) { function stop(args, params) { } stop.type = 'xstate.stopChild'; stop.actorRef = actorRef; stop.resolve = resolveStop; stop.execute = executeStop; return stop; } /** * Stops a child actor. * * @deprecated Use `stopChild(...)` instead */ const stop = stopChild; function checkStateIn(snapshot, _, { stateValue }) { if (typeof stateValue === 'string' && isStateId(stateValue)) { const target = snapshot.machine.getStateNodeById(stateValue); return snapshot._nodes.some(sn => sn === target); } return snapshot.matches(stateValue); } function stateIn(stateValue) { function stateIn(args, params) { return false; } stateIn.check = checkStateIn; stateIn.stateValue = stateValue; return stateIn; } function checkNot(snapshot, { context, event }, { guards }) { return !evaluateGuard(guards[0], context, event, snapshot); } /** * Higher-order guard that evaluates to `true` if the `guard` passed to it evaluates to `false`. * * @category Guards * @example ```ts import { setup, not } from 'xstate'; const machine = setup({ guards: { someNamedGuard: () => false } }).createMachine({ on: { someEvent: { guard: not('someNamedGuard'), actions: () => { // will be executed if guard in `not(...)` // evaluates to `false` } } } }); ``` * @returns A guard */ function not(guard) { function not(args, params) { return false; } not.check = checkNot; not.guards = [guard]; return not; } function checkAnd(snapshot, { context, event }, { guards }) { return guards.every(guard => evaluateGuard(guard, context, event, snapshot)); } /** * Higher-order guard that evaluates to `true` if all `guards` passed to it * evaluate to `true`. * * @category Guards * @example ```ts import { setup, and } from 'xstate'; const machine = setup({ guards: { someNamedGuard: () => true } }).createMachine({ on: { someEvent: { guard: and([ ({ context }) => context.value > 0, 'someNamedGuard' ]), actions: () => { // will be executed if all guards in `and(...)` // evaluate to true } } } }); ``` * @returns A guard action object */ function and(guards) { function and(args, params) { return false; } and.check = checkAnd; and.guards = guards; return and; } function checkOr(snapshot, { context, event }, { guards }) { return guards.some(guard => evaluateGuard(guard, context, event, snapshot)); } /** * Higher-order guard that evaluates to `true` if any of the `guards` passed to it * evaluate to `true`. * * @category Guards * @example ```ts import { setup, or } from 'xstate'; const machine = setup({ guards: { someNamedGuard: () => true } }).createMachine({ on: { someEvent: { guard: or([ ({ context }) => context.value > 0, 'someNamedGuard' ]), actions: () => { // will be executed if any of the guards in `or(...)` // evaluate to true } } } }); ``` * @returns A guard action object */ function or(guards) { function or(args, params) { return false; } or.check = checkOr; or.guards = guards; return or; } // TODO: throw on cycles (depth check should be enough) function evaluateGuard(guard, context, event, snapshot) { const { machine } = snapshot; const isInline = typeof guard === 'function'; const resolved = isInline ? guard : machine.implementations.guards[typeof guard === 'string' ? guard : guard.type]; if (!isInline && !resolved) { throw new Error(`Guard '${typeof guard === 'string' ? guard : guard.type}' is not implemented.'.`); } if (typeof resolved !== 'function') { return evaluateGuard(resolved, context, event, snapshot); } const guardArgs = { context, event }; const guardParams = isInline || typeof guard === 'string' ? undefined : 'params' in guard ? typeof guard.params === 'function' ? guard.params({ context, event }) : guard.params : undefined; if (!('check' in resolved)) { // the existing type of `.guards` assumes non-nullable `TExpressionGuard` // inline guards expect `TExpressionGuard` to be set to `undefined` // it's fine to cast this here, our logic makes sure that we call those 2 "variants" correctly return resolved(guardArgs, guardParams); } const builtinGuard = resolved; return builtinGuard.check(snapshot, guardArgs, resolved // this holds all params ); } const isAtomicStateNode = stateNode => stateNode.type === 'atomic' || stateNode.type === 'final'; function getChildren(stateNode) { return Object.values(stateNode.states).filter(sn => sn.type !== 'history'); } function getProperAncestors(stateNode, toStateNode) { const ancestors = []; if (toStateNode === stateNode) { return ancestors; } // add all ancestors let m = stateNode.parent; while (m && m !== toStateNode) { ancestors.push(m); m = m.parent; } return ancestors; } function getAllStateNodes(stateNodes) { const nodeSet = new Set(stateNodes); const adjList = getAdjList(nodeSet); // add descendants for (const s of nodeSet) { // if previously active, add existing child nodes if (s.type === 'compound' && (!adjList.get(s) || !adjList.get(s).length)) { getInitialStateNodesWithTheirAncestors(s).forEach(sn => nodeSet.add(sn)); } else { if (s.type === 'parallel') { for (const child of getChildren(s)) { if (child.type === 'history') { continue; } if (!nodeSet.has(child)) { const initialStates = getInitialStateNodesWithTheirAncestors(child); for (const initialStateNode of initialStates) { nodeSet.add(initialStateNode); } } } } } } // add all ancestors for (const s of nodeSet) { let m = s.parent; while (m) { nodeSet.add(m); m = m.parent; } } return nodeSet; } function getValueFromAdj(baseNode, adjList) { const childStateNodes = adjList.get(baseNode); if (!childStateNodes) { return {}; // todo: fix? } if (baseNode.type === 'compound') { const childStateNode = childStateNodes[0]; if (childStateNode) { if (isAtomicStateNode(childStateNode)) { return childStateNode.key; } } else { return {}; } } const stateValue = {}; for (const childStateNode of childStateNodes) { stateValue[childStateNode.key] = getValueFromAdj(childStateNode, adjList); } return stateValue; } function getAdjList(stateNodes) { const adjList = new Map(); for (const s of stateNodes) { if (!adjList.has(s)) { adjList.set(s, []); } if (s.parent) { if (!adjList.has(s.parent)) { adjList.set(s.parent, []); } adjList.get(s.parent).push(s); } } return adjList; } function getStateValue(rootNode, stateNodes) { const config = getAllStateNodes(stateNodes); return getValueFromAdj(rootNode, getAdjList(config)); } function isInFinalState(stateNodeSet, stateNode) { if (stateNode.type === 'compound') { return getChildren(stateNode).some(s => s.type === 'final' && stateNodeSet.has(s)); } if (stateNode.type === 'parallel') { return getChildren(stateNode).every(sn => isInFinalState(stateNodeSet, sn)); } return stateNode.type === 'final'; } const isStateId = str => str[0] === STATE_IDENTIFIER; function getCandidates(stateNode, receivedEventType) { const candidates = stateNode.transitions.get(receivedEventType) || [...stateNode.transitions.keys()].filter(eventDescriptor => { // check if transition is a wildcard transition, // which matches any non-transient events if (eventDescriptor === WILDCARD) { return true; } if (!eventDescriptor.endsWith('.*')) { return false; } const partialEventTokens = eventDescriptor.split('.'); const eventTokens = receivedEventType.split('.'); for (let tokenIndex = 0; tokenIndex < partialEventTokens.length; tokenIndex++) { const partialEventToken = partialEventTokens[tokenIndex]; const eventToken = eventTokens[tokenIndex]; if (partialEventToken === '*') { const isLastToken = tokenIndex === partialEventTokens.length - 1; return isLastToken; } if (partialEventToken !== eventToken) { return false; } } return true; }).sort((a, b) => b.length - a.length).flatMap(key => stateNode.transitions.get(key)); return candidates; } /** * All delayed transitions from the config. */ function getDelayedTransitions(stateNode) { const afterConfig = stateNode.config.after; if (!afterConfig) { return []; } const mutateEntryExit = (delay, i) => { const afterEvent = createAfterEvent(delay, stateNode.id); const eventType = afterEvent.type; stateNode.entry.push(raise(afterEvent, { id: eventType, delay })); stateNode.exit.push(cancel(eventType)); return eventType; }; const delayedTransitions = Object.keys(afterConfig).flatMap((delay, i) => { const configTransition = afterConfig[delay]; const resolvedTransition = typeof configTransition === 'string' ? { target: configTransition } : configTransition; const resolvedDelay = Number.isNaN(+delay) ? delay : +delay; const eventType = mutateEntryExit(resolvedDelay); return toArray(resolvedTransition).map(transition => ({ ...transition, event: eventType, delay: resolvedDelay })); }); return delayedTransitions.map(delayedTransition => { const { delay } = delayedTransition; return { ...formatTransition(stateNode, delayedTransition.event, delayedTransition), delay }; }); } function formatTransition(stateNode, descriptor, transitionConfig) { const normalizedTarget = normalizeTarget(transitionConfig.target); const reenter = transitionConfig.reenter ?? false; const target = resolveTarget(stateNode, normalizedTarget); const transition = { ...transitionConfig, actions: toArray(transitionConfig.actions), guard: transitionConfig.guard, target, source: stateNode, reenter, eventType: descriptor, toJSON: () => ({ ...transition, source: `#${stateNode.id}`, target: target ? target.map(t => `#${t.id}`) : undefined }) }; return transition; } function formatTransitions(stateNode) { const transitions = new Map(); if (stateNode.config.on) { for (const descriptor of Object.keys(stateNode.config.on)) { if (descriptor === NULL_EVENT) { throw new Error('Null events ("") cannot be specified as a transition key. Use `always: { ... }` instead.'); } const transitionsConfig = stateNode.config.on[descriptor]; transitions.set(descriptor, toTransitionConfigArray(transitionsConfig).map(t => formatTransition(stateNode, descriptor, t))); } } if (stateNode.config.onDone) { const descriptor = `xstate.done.state.${stateNode.id}`; transitions.set(descriptor, toTransitionConfigArray(stateNode.config.onDone).map(t => formatTransition(stateNode, descriptor, t))); } for (const invokeDef of stateNode.invoke) { if (invokeDef.onDone) { const descriptor = `xstate.done.actor.${invokeDef.id}`; transitions.set(descriptor, toTransitionConfigArray(invokeDef.onDone).map(t => formatTransition(stateNode, descriptor, t))); } if (invokeDef.onError) { const descriptor = `xstate.error.actor.${invokeDef.id}`; transitions.set(descriptor, toTransitionConfigArray(invokeDef.onError).map(t => formatTransition(stateNode, descriptor, t))); } if (invokeDef.onSnapshot) { const descriptor = `xstate.snapshot.${invokeDef.id}`; transitions.set(descriptor, toTransitionConfigArray(invokeDef.onSnapshot).map(t => formatTransition(stateNode, descriptor, t))); } } for (const delayedTransition of stateNode.after) { let existing = transitions.get(delayedTransition.eventType); if (!existing) { existing = []; transitions.set(delayedTransition.eventType, existing); } existing.push(delayedTransition); } return transitions; } function formatInitialTransition(stateNode, _target) { const resolvedTarget = typeof _target === 'string' ? stateNode.states[_target] : _target ? stateNode.states[_target.target] : undefined; if (!resolvedTarget && _target) { throw new Error(`Initial state node "${_target}" not found on parent state node #${stateNode.id}`); } const transition = { source: stateNode, actions: !_target || typeof _target === 'string' ? [] : toArray(_target.actions), eventType: null, reenter: false, target: resolvedTarget ? [resolvedTarget] : [], toJSON: () => ({ ...transition, source: `#${stateNode.id}`, target: resolvedTarget ? [`#${resolvedTarget.id}`] : [] }) }; return transition; } function resolveTarget(stateNode, targets) { if (targets === undefined) { // an undefined target signals that the state node should not transition from that state when receiving that event return undefined; } return targets.map(target => { if (typeof target !== 'string') { return target; } if (isStateId(target)) { return stateNode.machine.getStateNodeById(target); } const isInternalTarget = target[0] === STATE_DELIMITER; // If internal target is defined on machine, // do not include machine key on target if (isInternalTarget && !stateNode.parent) { return getStateNodeByPath(stateNode, target.slice(1)); } const resolvedTarget = isInternalTarget ? stateNode.key + target : target; if (stateNode.parent) { try { const targetStateNode = getStateNodeByPath(stateNode.parent, resolvedTarget); return targetStateNode; } catch (err) { throw new Error(`Invalid transition definition for state node '${stateNode.id}':\n${err.message}`); } } else { throw new Error(`Invalid target: "${target}" is not a valid target from the root node. Did you mean ".${target}"?`); } }); } function resolveHistoryDefaultTransition(stateNode) { const normalizedTarget = normalizeTarget(stateNode.config.target); if (!normalizedTarget) { return stateNode.parent.initial; } return { target: normalizedTarget.map(t => typeof t === 'string' ? getStateNodeByPath(stateNode.parent, t) : t) }; } function isHistoryNode(stateNode) { return stateNode.type === 'history'; } function getInitialStateNodesWithTheirAncestors(stateNode) { const states = getInitialStateNodes(stateNode); for (const initialState of states) { for (const ancestor of getProperAncestors(initialState, stateNode)) { states.add(ancestor); } } return states; } function getInitialStateNodes(stateNode) { const set = new Set(); function iter(descStateNode) { if (set.has(descStateNode)) { return; } set.add(descStateNode); if (descStateNode.type === 'compound') { iter(descStateNode.initial.target[0]); } else if (descStateNo