xstate
Version:
Finite State Machines and Statecharts for the Modern Web.
615 lines (606 loc) • 18 kB
JavaScript
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 };