UNPKG

xstate

Version:

Finite State Machines and Statecharts for the Modern Web.

834 lines (818 loc) 22.6 kB
import { X as XSTATE_STOP, A as createActor } from '../../dist/raise-1db27a82.development.esm.js'; import '../../dev/dist/xstate-dev.development.esm.js'; /** * Represents an actor created by `fromTransition`. * * The type of `self` within the actor's logic. * * @example * * ```ts * import { * fromTransition, * createActor, * type AnyActorSystem * } from 'xstate'; * * //* The actor's stored context. * type Context = { * // The current count. * count: number; * // The amount to increase `count` by. * step: number; * }; * // The events the actor receives. * type Event = { type: 'increment' }; * // The actor's input. * type Input = { step?: number }; * * // Actor logic that increments `count` by `step` when it receives an event of * // type `increment`. * const logic = fromTransition<Context, Event, AnyActorSystem, Input>( * (state, event, actorScope) => { * actorScope.self; * // ^? TransitionActorRef<Context, Event> * * if (event.type === 'increment') { * return { * ...state, * count: state.count + state.step * }; * } * return state; * }, * ({ input, self }) => { * self; * // ^? TransitionActorRef<Context, Event> * * return { * count: 0, * step: input.step ?? 1 * }; * } * ); * * const actor = createActor(logic, { input: { step: 10 } }); * // ^? TransitionActorRef<Context, Event> * ``` * * @see {@link fromTransition} */ /** * 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. * * @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 }, * // ... * // } * ``` * * @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. * * @returns Actor logic * @see {@link https://stately.ai/docs/input | Input docs} for more information about how input is passed */ 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(); /** * Represents an actor created by `fromCallback`. * * The type of `self` within the actor's logic. * * @example * * ```ts * import { fromCallback, createActor } from 'xstate'; * * // The events the actor receives. * type Event = { type: 'someEvent' }; * // The actor's input. * type Input = { name: string }; * * // Actor logic that logs whenever it receives an event of type `someEvent`. * const logic = fromCallback<Event, Input>(({ self, input, receive }) => { * self; * // ^? CallbackActorRef<Event, Input> * * receive((event) => { * if (event.type === 'someEvent') { * console.log(`${input.name}: received "someEvent" event`); * // logs 'myActor: received "someEvent" event' * } * }); * }); * * const actor = createActor(logic, { input: { name: 'myActor' } }); * // ^? CallbackActorRef<Event, Input> * ``` * * @see {@link fromCallback} */ /** * 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()` * * @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); * }; * }); * ``` * * @param callback - 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. * * @returns Callback logic * @see {@link CallbackLogicFunction} 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 */ function fromCallback(callback) { const logic = { config: callback, start: (state, actorScope) => { const { self, system, emit } = actorScope; const callbackState = { receivers: undefined, dispose: undefined }; instanceStates.set(self, callbackState); callbackState.dispose = callback({ 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); }, emit }); }, 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'; /** * Represents an actor created by `fromObservable` or `fromEventObservable`. * * The type of `self` within the actor's logic. * * @example * * ```ts * import { fromObservable, createActor } from 'xstate'; * import { interval } from 'rxjs'; * * // The type of the value observed by the actor's logic. * type Context = number; * // The actor's input. * type Input = { period?: number }; * * // Actor logic that observes a number incremented every `input.period` * // milliseconds (default: 1_000). * const logic = fromObservable<Context, Input>(({ input, self }) => { * self; * // ^? ObservableActorRef<Event, Input> * * return interval(input.period ?? 1_000); * }); * * const actor = createActor(logic, { input: { period: 2_000 } }); * // ^? ObservableActorRef<Event, Input> * ``` * * @see {@link fromObservable} * @see {@link fromEventObservable} */ /** * 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. * * @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 * // ... * ``` * * @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. * @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) => { 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, emit }) => { if (state.status === 'done') { // Do not restart a completed observable return; } state._subscription = observableCreator({ input: state.input, system, self, emit }).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. * * @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(); * ``` * * @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. */ 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, emit }) => { if (state.status === 'done') { // Do not restart a completed observable return; } state._subscription = lazyObservable({ input: state.input, system, self, emit }).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'; /** * Represents an actor created by `fromPromise`. * * The type of `self` within the actor's logic. * * @example * * ```ts * import { fromPromise, createActor } from 'xstate'; * * // The actor's resolved output * type Output = string; * // The actor's input. * type Input = { message: string }; * * // Actor logic that fetches the url of an image of a cat saying `input.message`. * const logic = fromPromise<Output, Input>(async ({ input, self }) => { * self; * // ^? PromiseActorRef<Output, Input> * * const data = await fetch( * `https://cataas.com/cat/says/${input.message}` * ); * const url = await data.json(); * return url; * }); * * const actor = createActor(logic, { input: { message: 'hello world' } }); * // ^? PromiseActorRef<Output, Input> * ``` * * @see {@link fromPromise} */ const controllerMap = new WeakMap(); /** * 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. * * @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', * // ... * // } * ``` * * @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 */ function fromPromise(promiseCreator) { const logic = { config: promiseCreator, transition: (state, event, scope) => { 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: { controllerMap.get(scope.self)?.abort(); return { ...state, status: 'stopped', input: undefined }; } default: return state; } }, start: (state, { self, system, emit }) => { // TODO: determine how to allow customizing this so that promises // can be restarted if necessary if (state.status !== 'active') { return; } const controller = new AbortController(); controllerMap.set(self, controller); const resolvedPromise = Promise.resolve(promiseCreator({ input: state.input, system, self, signal: controller.signal, emit })); resolvedPromise.then(response => { if (self.getSnapshot().status !== 'active') { return; } controllerMap.delete(self); system._relay(self, self, { type: XSTATE_PROMISE_RESOLVE, data: response }); }, errorData => { if (self.getSnapshot().status !== 'active') { return; } controllerMap.delete(self); 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 };