typescript-fsm
Version:
finite state machine with async callbacks
155 lines • 5.76 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.SyncStateMachine = exports.StateMachine = void 0;
exports.t = t;
function t(fromState, event, toState, cb) {
return { fromState, event, toState, cb };
}
/**
* StateMachine
* TypeScript finite state machine class with async transformations.
*/
class StateMachine {
// initialize the state-machine
constructor(init, transitions = [], logger = console) {
this.init = init;
this.transitions = transitions;
this.logger = logger;
this._current = init;
}
addTransitions(transitions) {
// bind any unbound method
transitions.forEach((_tran) => {
const tran = Object.create(_tran);
if (tran.cb && !tran.cb.name?.startsWith("bound ")) {
tran.cb = tran.cb.bind(this);
}
this.transitions.push(tran);
});
}
getState() { return this._current; }
can(event) {
return this.transitions.some((trans) => (trans.fromState === this._current && trans.event === event));
}
getNextState(event) {
const transition = this.transitions.find((tran) => tran.fromState === this._current && tran.event === event);
return transition?.toState;
}
isFinal() {
// search for a transition that starts from current state.
// if none is found it's a terminal state.
return this.transitions.every((trans) => (trans.fromState !== this._current));
}
formatErr(fromState, event) {
return `No transition: from ${String(fromState)} event ${String(event)}`;
}
// post event async
async dispatch(event, ...args) {
return new Promise((resolve, reject) => {
// delay execution to make it async
setTimeout((me) => {
// find transition
const found = this.transitions.some((tran) => {
if (tran.fromState === me._current && tran.event === event) {
me._current = tran.toState;
if (tran.cb) {
try {
const p = tran.cb(...args);
if (p instanceof Promise) {
p.then(resolve).catch(reject);
}
else {
resolve();
}
}
catch (e) {
this.logger.error("Exception in callback", e);
reject(e);
}
}
else {
resolve();
}
return true;
}
return false;
});
// no such transition
if (!found) {
const errorMessage = this.formatErr(me._current, event);
this.logger.error(errorMessage);
reject(new Error(errorMessage));
}
}, 0, this);
});
}
/**
* Generate a Mermaid StateDiagram of the current machine.
*/
toMermaid(title) {
const diagram = [];
if (title) {
diagram.push("---");
diagram.push(`title: ${title}`);
diagram.push("---");
}
diagram.push("stateDiagram-v2");
diagram.push(` [*] --> ${String(this.init)}`);
this.transitions.forEach(({ event, fromState, toState }) => {
const from = String(fromState);
const to = String(toState);
const evt = String(event);
diagram.push(` ${from} --> ${to}: ${evt}`);
});
// find terminal states
const ts = new Set();
this.transitions.forEach(({ toState }) => ts.add(toState));
this.transitions.forEach(({ fromState }) => ts.delete(fromState));
ts.forEach((state) => diagram.push(` ${String(state)} --> [*]`));
return diagram.join("\n");
}
}
exports.StateMachine = StateMachine;
/**
* SyncStateMachine
* TypeScript finite state machine class with sync transformations.
*/
class SyncStateMachine extends StateMachine {
constructor(init, transitions = [], logger = console) {
super(init, transitions, logger);
}
dispatch(_event, ..._args) {
throw new Error("SyncStateMachine does not support async dispatch.");
}
// post sync event
// returns true if the event was handled, false otherwise
syncDispatch(event, ...args) {
// find transition
const found = this.transitions.some((tran) => {
if (tran.fromState === this._current && tran.event === event) {
const current = this._current;
this._current = tran.toState;
if (tran.cb) {
try {
tran.cb(...args);
}
catch (e) {
this._current = current; // revert to previous state
this.logger.error("Exception in callback", e);
throw e;
}
return true;
}
return false; // search for more transitions
}
});
// no such transition
if (!found) {
const errorMessage = this.formatErr(this._current, event);
this.logger.error(errorMessage);
}
return (!!found);
}
}
exports.SyncStateMachine = SyncStateMachine;
//# sourceMappingURL=stateMachine.js.map