Files
cellxgene/client/src/util/statemachine/index.js
T

96 lines
3.0 KiB
JavaScript

/*
Very simple FSM for use in reducer, etc.
To create a state machine:
new StateMachine(initialState, transitions, onErrorCallback) -> statemachine
Where:
* initialState - a caller-specified value that represents the initial state of
the FSM.
* transitions - an array of objects, representing FSM transitions (graph edges),
having the form:
{
to: state_name_transitioning_to,
from: state_name_transitioning_from,
event: value_that_will_cause_transition,
action: optional_callback_upon_transition
}
The transition will be provided to the action callback, so other data
may be stored in the transition object for use by the action callback.
* onErrorCallback - a callback function called if the FSM receives an event
for which it has no defined transition.
Interface:
* states - property containing the state names. A Set(), contianing the
union of to: and from: values.
* events - property containing all of the accepted event values. Set().
* graph - a Map of Maps, organized as graph[eventValue][fromStateValue]
* clone() - clone the entire statemachine.
* next(eventValue) - drive the FSM to the next state. If the event
matches a transition with a defined action, the action callback is
called, and the action return value is returned by next(). If no
transition is defined, onErrorCallback is called.
Example:
const transitions = [
{ from: "A", to: "B", event: "yo", action: () => 42 }
];
const fsm = new StateMachine("A", transitions, () => { throw new Error("oops") });
fsm.next("yo"); // returns 42
*/
export default class StateMachine {
constructor(initState, transitions, onError) {
this.onError = onError || (() => undefined);
this.state = initState;
// all states
this.states = new Set(
transitions.reduce((names, tsn) => {
names.push(tsn.from);
names.push(tsn.to);
return names;
}, [])
);
// all transition names (aka events)
this.events = new Set(transitions.map((tsn) => tsn.event));
// the transition graph.
// graph[event][from] -> transition
this.graph = transitions.reduce((graph, tsn) => {
const { event, from } = tsn;
if (!graph.has(event)) graph.set(event, new Map());
const tsnMap = graph.get(event);
tsnMap.set(from, tsn);
return graph;
}, new Map());
}
clone(initState) {
const fsm = new StateMachine(initState, []);
fsm.onError = this.onError;
fsm.states = this.states;
fsm.events = this.events;
fsm.graph = this.graph;
return fsm;
}
next(event, data) {
const { graph, state } = this;
const tsnMap = graph.get(event);
if (!tsnMap) return this.onError(this, event, state, undefined);
const transition = tsnMap.get(state);
if (!transition) return this.onError(this, event, state, undefined);
this.state = transition.to;
return transition.action
? transition.action(this, transition, data)
: undefined;
}
}