deno.land / x / xstate@xstate@4.33.6 / src / interpreter.ts
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629import { StateMachine, Event, EventObject, CancelAction, DefaultContext, ActionObject, StateSchema, ActivityActionObject, SpecialTargets, ActionTypes, InvokeDefinition, SendActionObject, InvokeCallback, DisposeActivityFunction, StateValue, InterpreterOptions, ActivityDefinition, SingleOrArray, Subscribable, DoneEvent, MachineOptions, SCXML, EventData, Observer, Spawnable, Typestate, AnyEventObject, AnyInterpreter, ActorRef, ActorRefFrom, Behavior, StopActionObject, Subscription, AnyState, StateConfig, InteropSubscribable} from './types';import { State, bindActionToState, isStateConfig } from './State';import * as actionTypes from './actionTypes';import { doneInvoke, error, getActionFunction, initEvent, resolveActions, toActionObjects} from './actions';import { IS_PRODUCTION } from './environment';import { isPromiseLike, mapContext, warn, isArray, isFunction, isString, isObservable, uniqueId, isMachine, toEventObject, toSCXMLEvent, reportUnhandledExceptionOnInvocation, toInvokeSource, toObserver, isActor, isBehavior, symbolObservable, flatten} from './utils';import { Scheduler } from './scheduler';import { Actor, isSpawnedActor, createDeferredActor } from './Actor';import { registry } from './registry';import { getGlobal, registerService } from './devTools';import * as serviceScope from './serviceScope';import { spawnBehavior } from './behaviors';import { AreAllImplementationsAssumedToBeProvided, TypegenDisabled} from './typegenTypes';
export type StateListener< TContext, TEvent extends EventObject, TStateSchema extends StateSchema<TContext> = any, TTypestate extends Typestate<TContext> = { value: any; context: TContext }, TResolvedTypesMeta = TypegenDisabled> = ( state: State<TContext, TEvent, TStateSchema, TTypestate, TResolvedTypesMeta>, event: TEvent) => void;
export type ContextListener<TContext = DefaultContext> = ( context: TContext, prevContext: TContext | undefined) => void;
export type EventListener<TEvent extends EventObject = EventObject> = ( event: TEvent) => void;
export type Listener = () => void;
export interface Clock { setTimeout(fn: (...args: any[]) => void, timeout: number): any; clearTimeout(id: any): void;}
interface SpawnOptions { name?: string; autoForward?: boolean; sync?: boolean;}
const DEFAULT_SPAWN_OPTIONS = { sync: false, autoForward: false };
export enum InterpreterStatus { NotStarted, Running, Stopped}
export class Interpreter< // tslint:disable-next-line:max-classes-per-file TContext, TStateSchema extends StateSchema = any, TEvent extends EventObject = EventObject, TTypestate extends Typestate<TContext> = { value: any; context: TContext }, TResolvedTypesMeta = TypegenDisabled> implements ActorRef< TEvent, State<TContext, TEvent, TStateSchema, TTypestate, TResolvedTypesMeta> > { /** * The default interpreter options: * * - `clock` uses the global `setTimeout` and `clearTimeout` functions * - `logger` uses the global `console.log()` method */ public static defaultOptions = { execute: true, deferEvents: true, clock: { setTimeout: (fn, ms) => { return setTimeout(fn, ms); }, clearTimeout: (id) => { return clearTimeout(id); } } as Clock, logger: console.log.bind(console), devTools: false }; /** * The current state of the interpreted machine. */ private _state?: State< TContext, TEvent, TStateSchema, TTypestate, TResolvedTypesMeta >; private _initialState?: State< TContext, TEvent, TStateSchema, TTypestate, TResolvedTypesMeta >; /** * The clock that is responsible for setting and clearing timeouts, such as delayed events and transitions. */ public clock: Clock; public options: Readonly<InterpreterOptions>;
private scheduler: Scheduler; private delayedEventsMap: Record<string, unknown> = {}; private listeners: Set< StateListener< TContext, TEvent, TStateSchema, TTypestate, TResolvedTypesMeta > > = new Set(); private contextListeners: Set<ContextListener<TContext>> = new Set(); private stopListeners: Set<Listener> = new Set(); private doneListeners: Set<EventListener> = new Set(); private eventListeners: Set<EventListener> = new Set(); private sendListeners: Set<EventListener> = new Set(); private logger: (...args: any[]) => void; /** * Whether the service is started. */ public initialized = false; public status: InterpreterStatus = InterpreterStatus.NotStarted;
// Actor public parent?: Interpreter<any>; public id: string;
/** * The globally unique process ID for this invocation. */ public sessionId: string; public children: Map<string | number, ActorRef<any>> = new Map(); private forwardTo: Set<string> = new Set();
private _outgoingQueue: Array< [{ send: (ev: unknown) => void }, unknown] > = [];
// Dev Tools private devTools?: any;
/** * Creates a new Interpreter instance (i.e., service) for the given machine with the provided options, if any. * * @param machine The machine to be interpreted * @param options Interpreter options */ constructor( public machine: StateMachine< TContext, TStateSchema, TEvent, TTypestate, any, any, TResolvedTypesMeta >, options: InterpreterOptions = Interpreter.defaultOptions ) { const resolvedOptions = { ...Interpreter.defaultOptions, ...options };
const { clock, logger, parent, id } = resolvedOptions;
const resolvedId = id !== undefined ? id : machine.id;
this.id = resolvedId; this.logger = logger; this.clock = clock; this.parent = parent;
this.options = resolvedOptions;
this.scheduler = new Scheduler({ deferEvents: this.options.deferEvents });
this.sessionId = registry.bookId(); } public get initialState(): State< TContext, TEvent, TStateSchema, TTypestate, TResolvedTypesMeta > { if (this._initialState) { return this._initialState; }
return serviceScope.provide(this, () => { this._initialState = this.machine.initialState; return this._initialState; }); } /** * @deprecated Use `.getSnapshot()` instead. */ public get state(): State< TContext, TEvent, TStateSchema, TTypestate, TResolvedTypesMeta > { if (!IS_PRODUCTION) { warn( this.status !== InterpreterStatus.NotStarted, `Attempted to read state from uninitialized service '${this.id}'. Make sure the service is started first.` ); }
return this._state!; } public static interpret = interpret; /** * Executes the actions of the given state, with that state's `context` and `event`. * * @param state The state whose actions will be executed * @param actionsConfig The action implementations to use */ public execute( state: State< TContext, TEvent, TStateSchema, TTypestate, TResolvedTypesMeta >, actionsConfig?: MachineOptions<TContext, TEvent>['actions'] ): void { for (const action of state.actions) { this.exec(action, state, actionsConfig); } }
private update( state: State<TContext, TEvent, TStateSchema, TTypestate, any>, _event: SCXML.Event<TEvent> ): void { // Attach session ID to state state._sessionid = this.sessionId;
// Update state this._state = state;
// Execute actions if ( (!this.machine.config.predictableActionArguments || // this is currently required to execute initial actions as the `initialState` gets cached // we can't just recompute it (and execute actions while doing so) because we try to preserve identity of actors created within initial assigns _event === initEvent) && this.options.execute ) { this.execute(this.state); } else { let item: typeof this._outgoingQueue[number] | undefined; while ((item = this._outgoingQueue.shift())) { item[0].send(item[1]); } }
// Update children this.children.forEach((child) => { this.state.children[child.id] = child; });
// Dev tools if (this.devTools) { this.devTools.send(_event.data, state); }
// Execute listeners if (state.event) { for (const listener of this.eventListeners) { listener(state.event); } }
for (const listener of this.listeners) { listener(state, state.event); }
for (const contextListener of this.contextListeners) { contextListener( this.state.context, this.state.history ? this.state.history.context : undefined ); }
if (this.state.done) { // get final child state node const finalChildStateNode = state.configuration.find( (sn) => sn.type === 'final' && sn.parent === (this.machine as any) );
const doneData = finalChildStateNode && finalChildStateNode.doneData ? mapContext(finalChildStateNode.doneData, state.context, _event) : undefined;
for (const listener of this.doneListeners) { listener(doneInvoke(this.id, doneData)); } this._stop(); this._stopChildren(); } } /* * Adds a listener that is notified whenever a state transition happens. The listener is called with * the next state and the event object that caused the state transition. * * @param listener The state listener */ public onTransition( listener: StateListener< TContext, TEvent, TStateSchema, TTypestate, TResolvedTypesMeta > ): this { this.listeners.add(listener);
// Send current state to listener if (this.status === InterpreterStatus.Running) { listener(this.state, this.state.event); }
return this; } public subscribe( observer: Partial< Observer<State<TContext, TEvent, any, TTypestate, TResolvedTypesMeta>> > ): Subscription; public subscribe( nextListener?: ( state: State<TContext, TEvent, any, TTypestate, TResolvedTypesMeta> ) => void, errorListener?: (error: any) => void, completeListener?: () => void ): Subscription; public subscribe( nextListenerOrObserver?: | (( state: State<TContext, TEvent, any, TTypestate, TResolvedTypesMeta> ) => void) | Partial< Observer<State<TContext, TEvent, any, TTypestate, TResolvedTypesMeta>> >, _?: (error: any) => void, // TODO: error listener completeListener?: () => void ): Subscription { const observer = toObserver(nextListenerOrObserver, _, completeListener);
this.listeners.add(observer.next);
// Send current state to listener if (this.status !== InterpreterStatus.NotStarted) { observer.next(this.state); }
const completeOnce = () => { this.doneListeners.delete(completeOnce); this.stopListeners.delete(completeOnce); observer.complete(); };
if (this.status === InterpreterStatus.Stopped) { observer.complete(); } else { this.onDone(completeOnce); this.onStop(completeOnce); }
return { unsubscribe: () => { this.listeners.delete(observer.next); this.doneListeners.delete(completeOnce); this.stopListeners.delete(completeOnce); } }; }
/** * Adds an event listener that is notified whenever an event is sent to the running interpreter. * @param listener The event listener */ public onEvent(listener: EventListener): this { this.eventListeners.add(listener); return this; } /** * Adds an event listener that is notified whenever a `send` event occurs. * @param listener The event listener */ public onSend(listener: EventListener): this { this.sendListeners.add(listener); return this; } /** * Adds a context listener that is notified whenever the state context changes. * @param listener The context listener */ public onChange(listener: ContextListener<TContext>): this { this.contextListeners.add(listener); return this; } /** * Adds a listener that is notified when the machine is stopped. * @param listener The listener */ public onStop(listener: Listener): this { this.stopListeners.add(listener); return this; } /** * Adds a state listener that is notified when the statechart has reached its final state. * @param listener The state listener */ public onDone(listener: EventListener<DoneEvent>): this { this.doneListeners.add(listener); return this; } /** * Removes a listener. * @param listener The listener to remove */ public off(listener: (...args: any[]) => void): this { this.listeners.delete(listener); this.eventListeners.delete(listener); this.sendListeners.delete(listener); this.stopListeners.delete(listener); this.doneListeners.delete(listener); this.contextListeners.delete(listener); return this; } /** * Alias for Interpreter.prototype.start */ public init = this.start; /** * Starts the interpreter from the given state, or the initial state. * @param initialState The state to start the statechart from */ public start( initialState?: | State<TContext, TEvent, TStateSchema, TTypestate, TResolvedTypesMeta> | StateConfig<TContext, TEvent> | StateValue ): this { if (this.status === InterpreterStatus.Running) { // Do not restart the service if it is already started return this; }
// yes, it's a hack but we need the related cache to be populated for some things to work (like delayed transitions) // this is usually called by `machine.getInitialState` but if we rehydrate from a state we might bypass this call // we also don't want to call this method here as it resolves the full initial state which might involve calling assign actions // and that could potentially lead to some unwanted side-effects (even such as creating some rogue actors) (this.machine as any)._init();
registry.register(this.sessionId, this as Actor); this.initialized = true; this.status = InterpreterStatus.Running;
const resolvedState = initialState === undefined ? this.initialState : serviceScope.provide(this, () => { return isStateConfig<TContext, TEvent>(initialState) ? this.machine.resolveState(initialState) : this.machine.resolveState( State.from(initialState, this.machine.context) ); });
if (this.options.devTools) { this.attachDev(); } this.scheduler.initialize(() => { this.update(resolvedState, initEvent as SCXML.Event<TEvent>); }); return this; } private _stopChildren() { // TODO: think about converting those to actions this.children.forEach((child) => { if (isFunction(child.stop)) { child.stop(); } }); this.children.clear(); } private _stop() { for (const listener of this.listeners) { this.listeners.delete(listener); } for (const listener of this.stopListeners) { // call listener, then remove listener(); this.stopListeners.delete(listener); } for (const listener of this.contextListeners) { this.contextListeners.delete(listener); } for (const listener of this.doneListeners) { this.doneListeners.delete(listener); }
if (!this.initialized) { // Interpreter already stopped; do nothing return this; }
this.initialized = false; this.status = InterpreterStatus.Stopped; this._initialState = undefined;
// we are going to stop within the current sync frame // so we can safely just cancel this here as nothing async should be fired anyway for (const key of Object.keys(this.delayedEventsMap)) { this.clock.clearTimeout(this.delayedEventsMap[key]); }
// clear everything that might be enqueued this.scheduler.clear();
this.scheduler = new Scheduler({ deferEvents: this.options.deferEvents }); } /** * Stops the interpreter and unsubscribe all listeners. * * This will also notify the `onStop` listeners. */ public stop(): this { // TODO: add warning for stopping non-root interpreters
// grab the current scheduler as it will be replaced in _stop const scheduler = this.scheduler;
this._stop();
// let what is currently processed to be finished scheduler.schedule(() => { // it feels weird to handle this here but we need to handle this even slightly "out of band" const _event = toSCXMLEvent({ type: 'xstate.stop' }) as any;
const nextState = serviceScope.provide(this, () => { const exitActions = flatten( [...this.state.configuration] .sort((a, b) => b.order - a.order) .map((stateNode) => toActionObjects( stateNode.onExit, this.machine.options.actions as any ) ) );
const [resolvedActions, updatedContext] = resolveActions( this.machine as any, this.state, this.state.context, _event, [exitActions], this.machine.config.predictableActionArguments ? this._exec : undefined, this.machine.config.predictableActionArguments || this.machine.config.preserveActionOrder );
const newState = new State<TContext, TEvent, TStateSchema, TTypestate>({ value: this.state.value, context: updatedContext, _event, _sessionid: this.sessionId, historyValue: undefined, history: this.state, actions: resolvedActions.filter( (action) => action.type !== actionTypes.raise && (action.type !== actionTypes.send || (!!action.to && action.to !== SpecialTargets.Internal)) ), activities: {}, events: [], configuration: [], transitions: [], children: {}, done: this.state.done, tags: this.state.tags, machine: this.machine }); newState.changed = true; return newState; });
this.update(nextState, _event); this._stopChildren();
registry.free(this.sessionId); });
return this; } /** * Sends an event to the running interpreter to trigger a transition. * * An array of events (batched) can be sent as well, which will send all * batched events to the running interpreter. The listeners will be * notified only **once** when all events are processed. * * @param event The event(s) to send */ public send = ( event: SingleOrArray<Event<TEvent>> | SCXML.Event<TEvent>, payload?: EventData ): State<TContext, TEvent, TStateSchema, TTypestate, TResolvedTypesMeta> => { if (isArray(event)) { this.batch(event); return this.state; }
const _event = toSCXMLEvent(toEventObject(event as Event<TEvent>, payload));
if (this.status === InterpreterStatus.Stopped) { // do nothing if (!IS_PRODUCTION) { warn( false, `Event "${_event.name}" was sent to stopped service "${ this.machine.id }". This service has already reached its final state, and will not transition.\nEvent: ${JSON.stringify( _event.data )}` ); } return this.state; }
if ( this.status !== InterpreterStatus.Running && !this.options.deferEvents ) { throw new Error( `Event "${_event.name}" was sent to uninitialized service "${ this.machine.id // tslint:disable-next-line:max-line-length }". Make sure .start() is called for this service, or set { deferEvents: true } in the service options.\nEvent: ${JSON.stringify( _event.data )}` ); }
this.scheduler.schedule(() => { // Forward copy of event to child actors this.forward(_event);
const nextState = this._nextState(_event);
this.update(nextState, _event); });
return this._state!; // TODO: deprecate (should return void) // tslint:disable-next-line:semicolon };
private batch(events: Array<TEvent | TEvent['type']>): void { if ( this.status === InterpreterStatus.NotStarted && this.options.deferEvents ) { // tslint:disable-next-line:no-console if (!IS_PRODUCTION) { warn( false, `${events.length} event(s) were sent to uninitialized service "${ this.machine.id }" and are deferred. Make sure .start() is called for this service.\nEvent: ${JSON.stringify( event )}` ); } } else if (this.status !== InterpreterStatus.Running) { throw new Error( // tslint:disable-next-line:max-line-length `${events.length} event(s) were sent to uninitialized service "${this.machine.id}". Make sure .start() is called for this service, or set { deferEvents: true } in the service options.` ); }
if (!events.length) { return; }
const exec = !!this.machine.config.predictableActionArguments && this._exec;
this.scheduler.schedule(() => { let nextState = this.state; let batchChanged = false; const batchedActions: Array<ActionObject<TContext, TEvent>> = []; for (const event of events) { const _event = toSCXMLEvent(event);
this.forward(_event);
nextState = serviceScope.provide(this, () => { return this.machine.transition( nextState, _event, undefined, exec || undefined ); });
batchedActions.push( ...(this.machine.config.predictableActionArguments ? nextState.actions : (nextState.actions.map((a) => bindActionToState(a, nextState) ) as Array<ActionObject<TContext, TEvent>>)) );
batchChanged = batchChanged || !!nextState.changed; }
nextState.changed = batchChanged; nextState.actions = batchedActions; this.update(nextState, toSCXMLEvent(events[events.length - 1])); }); }
/** * Returns a send function bound to this interpreter instance. * * @param event The event to be sent by the sender. */ public sender( event: Event<TEvent> ): () => State<TContext, TEvent, TStateSchema, TTypestate> { return this.send.bind(this, event); }
private sendTo = ( event: SCXML.Event<AnyEventObject>, to: string | number | ActorRef<any>, immediate: boolean ) => { const isParent = this.parent && (to === SpecialTargets.Parent || this.parent.id === to); const target = isParent ? this.parent : isString(to) ? this.children.get(to as string) || registry.get(to as string) : isActor(to) ? to : undefined;
if (!target) { if (!isParent) { throw new Error( `Unable to send event to child '${to}' from service '${this.id}'.` ); }
// tslint:disable-next-line:no-console if (!IS_PRODUCTION) { warn( false, `Service '${this.id}' has no parent: unable to send event ${event.type}` ); } return; }
if ('machine' in target) { // perhaps those events should be rejected in the parent // but atm it doesn't have easy access to all of the information that is required to do it reliably if ( this.status !== InterpreterStatus.Stopped || this.parent !== target || // we need to send events to the parent from exit handlers of a machine that reached its final state this.state.done ) { // Send SCXML events to machines const scxmlEvent = { ...event, name: event.name === actionTypes.error ? `${error(this.id)}` : event.name, origin: this.sessionId }; if (!immediate && this.machine.config.predictableActionArguments) { this._outgoingQueue.push([target, scxmlEvent]); } else { (target as AnyInterpreter).send(scxmlEvent); } } } else { // Send normal events to other targets if (!immediate && this.machine.config.predictableActionArguments) { this._outgoingQueue.push([target, event.data]); } else { target.send(event.data); } } };
private _nextState( event: Event<TEvent> | SCXML.Event<TEvent>, exec = !!this.machine.config.predictableActionArguments && this._exec ) { const _event = toSCXMLEvent(event);
if ( _event.name.indexOf(actionTypes.errorPlatform) === 0 && !this.state.nextEvents.some( (nextEvent) => nextEvent.indexOf(actionTypes.errorPlatform) === 0 ) ) { throw (_event.data as any).data; }
const nextState = serviceScope.provide(this, () => { return this.machine.transition( this.state, _event, undefined, exec || undefined ); });
return nextState; }
/** * Returns the next state given the interpreter's current state and the event. * * This is a pure method that does _not_ update the interpreter's state. * * @param event The event to determine the next state */ public nextState( event: Event<TEvent> | SCXML.Event<TEvent> ): State<TContext, TEvent, TStateSchema, TTypestate, TResolvedTypesMeta> { return this._nextState(event, false); }
private forward(event: SCXML.Event<TEvent>): void { for (const id of this.forwardTo) { const child = this.children.get(id);
if (!child) { throw new Error( `Unable to forward event '${event}' from interpreter '${this.id}' to nonexistant child '${id}'.` ); }
child.send(event); } } private defer(sendAction: SendActionObject<TContext, TEvent>): void { this.delayedEventsMap[sendAction.id] = this.clock.setTimeout(() => { if (sendAction.to) { this.sendTo(sendAction._event, sendAction.to, true); } else { this.send( (sendAction as SendActionObject<TContext, TEvent, TEvent>)._event ); } }, sendAction.delay as number); } private cancel(sendId: string | number): void { this.clock.clearTimeout(this.delayedEventsMap[sendId]); delete this.delayedEventsMap[sendId]; }
private _exec = ( action: ActionObject<TContext, TEvent>, context: TContext, _event: SCXML.Event<TEvent>, actionFunctionMap = this.machine.options.actions ): void => { const actionOrExec = action.exec || getActionFunction(action.type, actionFunctionMap); const exec = isFunction(actionOrExec) ? actionOrExec : actionOrExec ? actionOrExec.exec : action.exec;
if (exec) { try { return (exec as any)( context, _event.data, !this.machine.config.predictableActionArguments ? { action, state: this.state, _event } : { action, _event } ); } catch (err) { if (this.parent) { this.parent.send({ type: 'xstate.error', data: err } as EventObject); }
throw err; } }
switch (action.type) { case actionTypes.send: const sendAction = action as SendActionObject<TContext, TEvent>;
if (typeof sendAction.delay === 'number') { this.defer(sendAction); return; } else { if (sendAction.to) { this.sendTo(sendAction._event, sendAction.to, _event === initEvent); } else { this.send( (sendAction as SendActionObject<TContext, TEvent, TEvent>)._event ); } } break;
case actionTypes.cancel: this.cancel((action as CancelAction).sendId);
break; case actionTypes.start: { if (this.status !== InterpreterStatus.Running) { return; } const activity = (action as ActivityActionObject<TContext, TEvent>) .activity as InvokeDefinition<TContext, TEvent>;
// If the activity will be stopped right after it's started // (such as in transient states) // don't bother starting the activity. if ( // in v4 with `predictableActionArguments` invokes are called eagerly when the `this.state` still points to the previous state !this.machine.config.predictableActionArguments && !this.state.activities[activity.id || activity.type] ) { break; }
// Invoked services if (activity.type === ActionTypes.Invoke) { const invokeSource = toInvokeSource(activity.src); const serviceCreator = this.machine.options.services ? this.machine.options.services[invokeSource.type] : undefined;
const { id, data } = activity;
if (!IS_PRODUCTION) { warn( !('forward' in activity), // tslint:disable-next-line:max-line-length `\`forward\` property is deprecated (found in invocation of '${activity.src}' in in machine '${this.machine.id}'). ` + `Please use \`autoForward\` instead.` ); }
const autoForward = 'autoForward' in activity ? activity.autoForward : !!activity.forward;
if (!serviceCreator) { // tslint:disable-next-line:no-console if (!IS_PRODUCTION) { warn( false, `No service found for invocation '${activity.src}' in machine '${this.machine.id}'.` ); } return; }
const resolvedData = data ? mapContext(data, context, _event) : undefined;
if (typeof serviceCreator === 'string') { // TODO: warn return; }
let source: Spawnable = isFunction(serviceCreator) ? (serviceCreator as any)(context, _event.data, { data: resolvedData, src: invokeSource, meta: activity.meta }) : serviceCreator;
if (!source) { // TODO: warn? return; }
let options: SpawnOptions | undefined;
if (isMachine(source)) { source = resolvedData ? source.withContext(resolvedData) : source; options = { autoForward }; }
this.spawn(source, id, options); } else { this.spawnActivity(activity); }
break; } case actionTypes.stop: { this.stopChild((action as StopActionObject).activity.id); break; }
case actionTypes.log: const { label, value } = action;
if (label) { this.logger(label, value); } else { this.logger(value); } break; default: if (!IS_PRODUCTION) { warn( false, `No implementation found for action type '${action.type}'` ); } break; } };
private exec( action: ActionObject<TContext, TEvent>, state: State< TContext, TEvent, TStateSchema, TTypestate, TResolvedTypesMeta >, actionFunctionMap = this.machine.options.actions ) { this._exec(action, state.context, state._event, actionFunctionMap); }
private removeChild(childId: string): void { this.children.delete(childId); this.forwardTo.delete(childId);
// this.state might not exist at the time this is called, // such as when a child is added then removed while initializing the state delete this.state?.children[childId]; }
private stopChild(childId: string): void { const child = this.children.get(childId); if (!child) { return; }
this.removeChild(childId);
if (isFunction(child.stop)) { child.stop(); } } public spawn( entity: Spawnable, name: string, options?: SpawnOptions ): ActorRef<any> { if (this.status !== InterpreterStatus.Running) { return createDeferredActor(entity, name); } if (isPromiseLike(entity)) { return this.spawnPromise(Promise.resolve(entity), name); } else if (isFunction(entity)) { return this.spawnCallback(entity as InvokeCallback, name); } else if (isSpawnedActor(entity)) { return this.spawnActor(entity, name); } else if (isObservable<TEvent>(entity)) { return this.spawnObservable(entity, name); } else if (isMachine(entity)) { return this.spawnMachine(entity, { ...options, id: name }); } else if (isBehavior(entity)) { return this.spawnBehavior(entity, name); } else { throw new Error( `Unable to spawn entity "${name}" of type "${typeof entity}".` ); } } public spawnMachine< TChildContext, TChildStateSchema extends StateSchema, TChildEvent extends EventObject >( machine: StateMachine<TChildContext, TChildStateSchema, TChildEvent>, options: { id?: string; autoForward?: boolean; sync?: boolean } = {} ): ActorRef<TChildEvent, State<TChildContext, TChildEvent>> { const childService = new Interpreter(machine, { ...this.options, // inherit options from this interpreter parent: this, id: options.id || machine.id });
const resolvedOptions = { ...DEFAULT_SPAWN_OPTIONS, ...options };
if (resolvedOptions.sync) { childService.onTransition((state) => { this.send(actionTypes.update as any, { state, id: childService.id }); }); }
const actor = childService;
this.children.set(childService.id, actor);
if (resolvedOptions.autoForward) { this.forwardTo.add(childService.id); }
childService .onDone((doneEvent) => { this.removeChild(childService.id); this.send(toSCXMLEvent(doneEvent as any, { origin: childService.id })); }) .start();
return actor as ActorRef<TChildEvent, State<TChildContext, TChildEvent>>; } private spawnBehavior<TActorEvent extends EventObject, TEmitted>( behavior: Behavior<TActorEvent, TEmitted>, id: string ): ActorRef<TActorEvent, TEmitted> { const actorRef = spawnBehavior(behavior, { id, parent: this });
this.children.set(id, actorRef);
return actorRef; } private spawnPromise<T>(promise: Promise<T>, id: string): ActorRef<never, T> { let canceled = false; let resolvedData: T | undefined;
promise.then( (response) => { if (!canceled) { resolvedData = response; this.removeChild(id); this.send( toSCXMLEvent(doneInvoke(id, response) as any, { origin: id }) ); } }, (errorData) => { if (!canceled) { this.removeChild(id); const errorEvent = error(id, errorData); try { // Send "error.platform.id" to this (parent). this.send(toSCXMLEvent(errorEvent as any, { origin: id })); } catch (error) { reportUnhandledExceptionOnInvocation(errorData, error, id); if (this.devTools) { this.devTools.send(errorEvent, this.state); } if (this.machine.strict) { // it would be better to always stop the state machine if unhandled // exception/promise rejection happens but because we don't want to // break existing code so enforce it on strict mode only especially so // because documentation says that onError is optional this.stop(); } } } } );
const actor: ActorRef<never, T> = { id, send: () => void 0, subscribe: (next, handleError?, complete?) => { const observer = toObserver(next, handleError, complete);
let unsubscribed = false; promise.then( (response) => { if (unsubscribed) { return; } observer.next(response); if (unsubscribed) { return; } observer.complete(); }, (err) => { if (unsubscribed) { return; } observer.error(err); } );
return { unsubscribe: () => (unsubscribed = true) }; }, stop: () => { canceled = true; }, toJSON() { return { id }; }, getSnapshot: () => resolvedData, [symbolObservable]: function () { return this; } };
this.children.set(id, actor);
return actor; } private spawnCallback(callback: InvokeCallback, id: string): ActorRef<any> { let canceled = false; const receivers = new Set<(e: EventObject) => void>(); const listeners = new Set<(e: EventObject) => void>(); let emitted: TEvent | undefined;
const receive = (e: TEvent) => { emitted = e; listeners.forEach((listener) => listener(e)); if (canceled) { return; } this.send(toSCXMLEvent(e, { origin: id })); };
let callbackStop;
try { callbackStop = callback(receive, (newListener) => { receivers.add(newListener); }); } catch (err) { this.send(error(id, err) as any); }
if (isPromiseLike(callbackStop)) { // it turned out to be an async function, can't reliably check this before calling `callback` // because transpiled async functions are not recognizable return this.spawnPromise(callbackStop as Promise<any>, id); }
const actor = { id, send: (event) => receivers.forEach((receiver) => receiver(event)), subscribe: (next) => { const observer = toObserver(next); listeners.add(observer.next);
return { unsubscribe: () => { listeners.delete(observer.next); } }; }, stop: () => { canceled = true; if (isFunction(callbackStop)) { callbackStop(); } }, toJSON() { return { id }; }, getSnapshot: () => emitted, [symbolObservable]: function () { return this; } };
this.children.set(id, actor);
return actor; } private spawnObservable<T extends TEvent>( source: Subscribable<T>, id: string ): ActorRef<any, T> { let emitted: T | undefined;
const subscription = source.subscribe( (value) => { emitted = value; this.send(toSCXMLEvent(value, { origin: id })); }, (err) => { this.removeChild(id); this.send(toSCXMLEvent(error(id, err) as any, { origin: id })); }, () => { this.removeChild(id); this.send(toSCXMLEvent(doneInvoke(id) as any, { origin: id })); } );
const actor: ActorRef<any, T> = { id, send: () => void 0, subscribe: (next, handleError?, complete?) => { return source.subscribe(next, handleError, complete); }, stop: () => subscription.unsubscribe(), getSnapshot: () => emitted, toJSON() { return { id }; }, [symbolObservable]: function () { return this; } };
this.children.set(id, actor);
return actor; } private spawnActor<T extends ActorRef<any>>(actor: T, name: string): T { this.children.set(name, actor);
return actor; } private spawnActivity(activity: ActivityDefinition<TContext, TEvent>): void { const implementation = this.machine.options && this.machine.options.activities ? this.machine.options.activities[activity.type] : undefined;
if (!implementation) { if (!IS_PRODUCTION) { warn(false, `No implementation found for activity '${activity.type}'`); } // tslint:disable-next-line:no-console return; }
// Start implementation const dispose = implementation(this.state.context, activity); this.spawnEffect(activity.id, dispose); } private spawnEffect( id: string, dispose?: DisposeActivityFunction | void ): void { this.children.set(id, { id, send: () => void 0, subscribe: () => { return { unsubscribe: () => void 0 }; }, stop: dispose || undefined, getSnapshot: () => undefined, toJSON() { return { id }; }, [symbolObservable]: function () { return this; } }); }
private attachDev(): void { const global = getGlobal(); if (this.options.devTools && global) { if ((global as any).__REDUX_DEVTOOLS_EXTENSION__) { const devToolsOptions = typeof this.options.devTools === 'object' ? this.options.devTools : undefined; this.devTools = (global as any).__REDUX_DEVTOOLS_EXTENSION__.connect( { name: this.id, autoPause: true, stateSanitizer: (state: AnyState): object => { return { value: state.value, context: state.context, actions: state.actions }; }, ...devToolsOptions, features: { jump: false, skip: false, ...(devToolsOptions ? (devToolsOptions as any).features : undefined) } }, this.machine ); this.devTools.init(this.state); }
// add XState-specific dev tooling hook registerService(this); } } public toJSON() { return { id: this.id }; }
public [symbolObservable](): InteropSubscribable< State<TContext, TEvent, TStateSchema, TTypestate, TResolvedTypesMeta> > { return this; }
public getSnapshot() { if (this.status === InterpreterStatus.NotStarted) { return this.initialState; } return this._state!; }}
const resolveSpawnOptions = (nameOrOptions?: string | SpawnOptions) => { if (isString(nameOrOptions)) { return { ...DEFAULT_SPAWN_OPTIONS, name: nameOrOptions }; }
return { ...DEFAULT_SPAWN_OPTIONS, name: uniqueId(), ...nameOrOptions };};
export function spawn<T extends Behavior<any, any>>( entity: T, nameOrOptions?: string | SpawnOptions): ActorRefFrom<T>;export function spawn<TC, TE extends EventObject>( entity: StateMachine<TC, any, TE, any, any, any, any>, nameOrOptions?: string | SpawnOptions): ActorRefFrom<StateMachine<TC, any, TE, any, any, any, any>>;export function spawn( entity: Spawnable, nameOrOptions?: string | SpawnOptions): ActorRef<any>;export function spawn( entity: Spawnable, nameOrOptions?: string | SpawnOptions): ActorRef<any> { const resolvedOptions = resolveSpawnOptions(nameOrOptions);
return serviceScope.consume((service) => { if (!IS_PRODUCTION) { const isLazyEntity = isMachine(entity) || isFunction(entity); warn( !!service || isLazyEntity, `Attempted to spawn an Actor (ID: "${ isMachine(entity) ? entity.id : 'undefined' }") outside of a service. This will have no effect.` ); }
if (service) { return service.spawn(entity, resolvedOptions.name, resolvedOptions); } else { return createDeferredActor(entity, resolvedOptions.name); } });}
/** * Creates a new Interpreter instance for the given machine with the provided options, if any. * * @param machine The machine to interpret * @param options Interpreter options */export function interpret< TContext = DefaultContext, TStateSchema extends StateSchema = any, TEvent extends EventObject = EventObject, TTypestate extends Typestate<TContext> = { value: any; context: TContext }, TResolvedTypesMeta = TypegenDisabled>( machine: AreAllImplementationsAssumedToBeProvided<TResolvedTypesMeta> extends true ? StateMachine< TContext, TStateSchema, TEvent, TTypestate, any, any, TResolvedTypesMeta > : 'Some implementations missing', options?: InterpreterOptions) { const interpreter = new Interpreter< TContext, TStateSchema, TEvent, TTypestate, TResolvedTypesMeta >(machine as any, options);
return interpreter;}
Version Info