UNPKG

xstate

Version:

Finite State Machines and Statecharts for the Modern Web.

615 lines (606 loc) 18 kB
import { X as XSTATE_STOP, A as createActor } from '../../dist/raise-040ba012.esm.js'; import '../../dev/dist/xstate-dev.esm.js'; /** * Returns actor logic given a transition function and its initial state. * * A “transition function” is a function that takes the current `state` and received `event` object as arguments, and returns the next state, similar to a reducer. * * Actors created from transition logic (“transition actors”) can: * * - Receive events * - Emit snapshots of its state * * The transition function’s `state` is used as its transition actor’s `context`. * * Note that the "state" for a transition function is provided by the initial state argument, and is not the same as the State object of an actor or a state within a machine configuration. * * @param transition The transition function used to describe the transition logic. It should return the next state given the current state and event. It receives the following arguments: * - `state` - the current state. * - `event` - the received event. * - `actorScope` - the actor scope object, with properties like `self` and `system`. * @param initialContext The initial state of the transition function, either an object representing the state, or a function which returns a state object. If a function, it will receive as its only argument an object with the following properties: * - `input` - the `input` provided to its parent transition actor. * - `self` - a reference to its parent transition actor. * @see {@link https://stately.ai/docs/input | Input docs} for more information about how input is passed * @returns Actor logic * * @example * ```ts * const transitionLogic = fromTransition( * (state, event) => { * if (event.type === 'increment') { * return { * ...state, * count: state.count + 1, * }; * } * return state; * }, * { count: 0 }, * ); * * const transitionActor = createActor(transitionLogic); * transitionActor.subscribe((snapshot) => { * console.log(snapshot); * }); * transitionActor.start(); * // => { * // status: 'active', * // context: { count: 0 }, * // ... * // } * * transitionActor.send({ type: 'increment' }); * // => { * // status: 'active', * // context: { count: 1 }, * // ... * // } * ``` */ function fromTransition(transition, initialContext) { return { config: transition, transition: (snapshot, event, actorScope) => { return { ...snapshot, context: transition(snapshot.context, event, actorScope) }; }, getInitialSnapshot: (_, input) => { return { status: 'active', output: undefined, error: undefined, context: typeof initialContext === 'function' ? initialContext({ input }) : initialContext }; }, getPersistedSnapshot: snapshot => snapshot, restoreSnapshot: snapshot => snapshot }; } const instanceStates = /* #__PURE__ */new WeakMap(); /** * An actor logic creator which returns callback logic as defined by a callback function. * * @remarks * Useful for subscription-based or other free-form logic that can send events back to the parent actor. * * Actors created from callback logic (“callback actors”) can: * - Receive events via the `receive` function * - Send events to the parent actor via the `sendBack` function * * Callback actors are a bit different from other actors in that they: * - Do not work with `onDone` * - Do not produce a snapshot using `.getSnapshot()` * - Do not emit values when used with `.subscribe()` * - Can not be stopped with `.stop()` * * @param invokeCallback - The callback function used to describe the callback logic * The callback function is passed an object with the following properties: * - `receive` - A function that can send events back to the parent actor; the listener is then called whenever events are received by the callback actor * - `sendBack` - A function that can send events back to the parent actor * - `input` - Data that was provided to the callback actor * - `self` - The parent actor of the callback actor * - `system` - The actor system to which the callback actor belongs * The callback function can (optionally) return a cleanup function, which is called when the actor is stopped. * @see {@link InvokeCallback} for more information about the callback function and its object argument * @see {@link https://stately.ai/docs/input | Input docs} for more information about how input is passed * @returns Callback logic * * @example * ```typescript * const callbackLogic = fromCallback(({ sendBack, receive }) => { * let lockStatus = 'unlocked'; * * const handler = (event) => { * if (lockStatus === 'locked') { * return; * } * sendBack(event); * }; * * receive((event) => { * if (event.type === 'lock') { * lockStatus = 'locked'; * } else if (event.type === 'unlock') { * lockStatus = 'unlocked'; * } * }); * * document.body.addEventListener('click', handler); * * return () => { * document.body.removeEventListener('click', handler); * }; * }); * ``` */ function fromCallback(invokeCallback) { const logic = { config: invokeCallback, start: (state, actorScope) => { const { self, system } = actorScope; const callbackState = { receivers: undefined, dispose: undefined }; instanceStates.set(self, callbackState); callbackState.dispose = invokeCallback({ input: state.input, system, self, sendBack: event => { if (self.getSnapshot().status === 'stopped') { return; } if (self._parent) { system._relay(self, self._parent, event); } }, receive: listener => { callbackState.receivers ??= new Set(); callbackState.receivers.add(listener); } }); }, transition: (state, event, actorScope) => { const callbackState = instanceStates.get(actorScope.self); if (event.type === XSTATE_STOP) { state = { ...state, status: 'stopped', error: undefined }; callbackState.dispose?.(); return state; } callbackState.receivers?.forEach(receiver => receiver(event)); return state; }, getInitialSnapshot: (_, input) => { return { status: 'active', output: undefined, error: undefined, input }; }, getPersistedSnapshot: snapshot => snapshot, restoreSnapshot: snapshot => snapshot }; return logic; } const XSTATE_OBSERVABLE_NEXT = 'xstate.observable.next'; const XSTATE_OBSERVABLE_ERROR = 'xstate.observable.error'; const XSTATE_OBSERVABLE_COMPLETE = 'xstate.observable.complete'; /** * Observable actor logic is described by an observable stream of values. Actors created from observable logic (“observable actors”) can: * * - Emit snapshots of the observable’s emitted value * * The observable’s emitted value is used as its observable actor’s `context`. * * Sending events to observable actors will have no effect. * * @param observableCreator A function that creates an observable. It receives one argument, an object with the following properties: * - `input` - Data that was provided to the observable actor * - `self` - The parent actor * - `system` - The actor system to which the observable actor belongs * * It should return a {@link Subscribable}, which is compatible with an RxJS Observable, although RxJS is not required to create them. * * @example * ```ts * import { fromObservable, createActor } from 'xstate' * import { interval } from 'rxjs'; * * const logic = fromObservable((obj) => interval(1000)); * * const actor = createActor(logic); * * actor.subscribe((snapshot) => { * console.log(snapshot.context); * }); * * actor.start(); * // At every second: * // Logs 0 * // Logs 1 * // Logs 2 * // ... * ``` * * @see {@link https://rxjs.dev} for documentation on RxJS Observable and observable creators. * @see {@link Subscribable} interface in XState, which is based on and compatible with RxJS Observable. */ function fromObservable(observableCreator) { // TODO: add event types const logic = { config: observableCreator, transition: (snapshot, event, { self, id, defer, system }) => { if (snapshot.status !== 'active') { return snapshot; } switch (event.type) { case XSTATE_OBSERVABLE_NEXT: { const newSnapshot = { ...snapshot, context: event.data }; return newSnapshot; } case XSTATE_OBSERVABLE_ERROR: return { ...snapshot, status: 'error', error: event.data, input: undefined, _subscription: undefined }; case XSTATE_OBSERVABLE_COMPLETE: return { ...snapshot, status: 'done', input: undefined, _subscription: undefined }; case XSTATE_STOP: snapshot._subscription.unsubscribe(); return { ...snapshot, status: 'stopped', input: undefined, _subscription: undefined }; default: return snapshot; } }, getInitialSnapshot: (_, input) => { return { status: 'active', output: undefined, error: undefined, context: undefined, input, _subscription: undefined }; }, start: (state, { self, system }) => { if (state.status === 'done') { // Do not restart a completed observable return; } state._subscription = observableCreator({ input: state.input, system, self }).subscribe({ next: value => { system._relay(self, self, { type: XSTATE_OBSERVABLE_NEXT, data: value }); }, error: err => { system._relay(self, self, { type: XSTATE_OBSERVABLE_ERROR, data: err }); }, complete: () => { system._relay(self, self, { type: XSTATE_OBSERVABLE_COMPLETE }); } }); }, getPersistedSnapshot: ({ _subscription, ...state }) => state, restoreSnapshot: state => ({ ...state, _subscription: undefined }) }; return logic; } /** * Creates event observable logic that listens to an observable that delivers event objects. * * Event observable actor logic is described by an observable stream of {@link https://stately.ai/docs/transitions#event-objects | event objects}. Actors created from event observable logic (“event observable actors”) can: * * - Implicitly send events to its parent actor * - Emit snapshots of its emitted event objects * * Sending events to event observable actors will have no effect. * * @param lazyObservable A function that creates an observable that delivers event objects. It receives one argument, an object with the following properties: * * - `input` - Data that was provided to the event observable actor * - `self` - The parent actor * - `system` - The actor system to which the event observable actor belongs. * * It should return a {@link Subscribable}, which is compatible with an RxJS Observable, although RxJS is not required to create them. * * @example * ```ts * import { * fromEventObservable, * Subscribable, * EventObject, * createMachine, * createActor * } from 'xstate'; * import { fromEvent } from 'rxjs'; * * const mouseClickLogic = fromEventObservable(() => * fromEvent(document.body, 'click') as Subscribable<EventObject> * ); * * const canvasMachine = createMachine({ * invoke: { * // Will send mouse `click` events to the canvas actor * src: mouseClickLogic, * } * }); * * const canvasActor = createActor(canvasMachine); * canvasActor.start(); * ``` */ function fromEventObservable(lazyObservable) { // TODO: event types const logic = { config: lazyObservable, transition: (state, event) => { if (state.status !== 'active') { return state; } switch (event.type) { case XSTATE_OBSERVABLE_ERROR: return { ...state, status: 'error', error: event.data, input: undefined, _subscription: undefined }; case XSTATE_OBSERVABLE_COMPLETE: return { ...state, status: 'done', input: undefined, _subscription: undefined }; case XSTATE_STOP: state._subscription.unsubscribe(); return { ...state, status: 'stopped', input: undefined, _subscription: undefined }; default: return state; } }, getInitialSnapshot: (_, input) => { return { status: 'active', output: undefined, error: undefined, context: undefined, input, _subscription: undefined }; }, start: (state, { self, system }) => { if (state.status === 'done') { // Do not restart a completed observable return; } state._subscription = lazyObservable({ input: state.input, system, self }).subscribe({ next: value => { if (self._parent) { system._relay(self, self._parent, value); } }, error: err => { system._relay(self, self, { type: XSTATE_OBSERVABLE_ERROR, data: err }); }, complete: () => { system._relay(self, self, { type: XSTATE_OBSERVABLE_COMPLETE }); } }); }, getPersistedSnapshot: ({ _subscription, ...snapshot }) => snapshot, restoreSnapshot: snapshot => ({ ...snapshot, _subscription: undefined }) }; return logic; } const XSTATE_PROMISE_RESOLVE = 'xstate.promise.resolve'; const XSTATE_PROMISE_REJECT = 'xstate.promise.reject'; /** * An actor logic creator which returns promise logic as defined by an async process that resolves or rejects after some time. * * Actors created from promise actor logic (“promise actors”) can: * - Emit the resolved value of the promise * - Output the resolved value of the promise * * Sending events to promise actors will have no effect. * * @param promiseCreator * A function which returns a Promise, and accepts an object with the following properties: * - `input` - Data that was provided to the promise actor * - `self` - The parent actor of the promise actor * - `system` - The actor system to which the promise actor belongs * @see {@link https://stately.ai/docs/input | Input docs} for more information about how input is passed * * @example * ```ts * const promiseLogic = fromPromise(async () => { * const result = await fetch('https://example.com/...') * .then((data) => data.json()); * * return result; * }); * * const promiseActor = createActor(promiseLogic); * promiseActor.subscribe((snapshot) => { * console.log(snapshot); * }); * promiseActor.start(); * // => { * // output: undefined, * // status: 'active' * // ... * // } * * // After promise resolves * // => { * // output: { ... }, * // status: 'done', * // ... * // } * ``` */ function fromPromise(promiseCreator) { const logic = { config: promiseCreator, transition: (state, event) => { if (state.status !== 'active') { return state; } switch (event.type) { case XSTATE_PROMISE_RESOLVE: { const resolvedValue = event.data; return { ...state, status: 'done', output: resolvedValue, input: undefined }; } case XSTATE_PROMISE_REJECT: return { ...state, status: 'error', error: event.data, input: undefined }; case XSTATE_STOP: return { ...state, status: 'stopped', input: undefined }; default: return state; } }, start: (state, { self, system }) => { // TODO: determine how to allow customizing this so that promises // can be restarted if necessary if (state.status !== 'active') { return; } const resolvedPromise = Promise.resolve(promiseCreator({ input: state.input, system, self })); resolvedPromise.then(response => { if (self.getSnapshot().status !== 'active') { return; } system._relay(self, self, { type: XSTATE_PROMISE_RESOLVE, data: response }); }, errorData => { if (self.getSnapshot().status !== 'active') { return; } system._relay(self, self, { type: XSTATE_PROMISE_REJECT, data: errorData }); }); }, getInitialSnapshot: (_, input) => { return { status: 'active', output: undefined, error: undefined, input }; }, getPersistedSnapshot: snapshot => snapshot, restoreSnapshot: snapshot => snapshot }; return logic; } const emptyLogic = fromTransition(_ => undefined, undefined); function createEmptyActor() { return createActor(emptyLogic); } export { createEmptyActor, fromCallback, fromEventObservable, fromObservable, fromPromise, fromTransition };