xstate
Version:
Finite State Machines and Statecharts for the Modern Web.
843 lines (825 loc) • 22.9 kB
JavaScript
Object.defineProperty(exports, '__esModule', { value: true });
var guards_dist_xstateGuards = require('../../dist/raise-60cebf03.cjs.js');
require('../../dev/dist/xstate-dev.cjs.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 === guards_dist_xstateGuards.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 guards_dist_xstateGuards.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 guards_dist_xstateGuards.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 guards_dist_xstateGuards.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 guards_dist_xstateGuards.createActor(emptyLogic);
}
exports.createEmptyActor = createEmptyActor;
exports.fromCallback = fromCallback;
exports.fromEventObservable = fromEventObservable;
exports.fromObservable = fromObservable;
exports.fromPromise = fromPromise;
exports.fromTransition = fromTransition;
;