UNPKG

typescript-fsm

Version:

finite state machine with async callbacks

155 lines 5.76 kB
"use strict"; 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