xstate
Version:
Finite State Machines and Statecharts for the Modern Web.
1,723 lines (1,672 loc) • 82.4 kB
JavaScript
'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