Skip to content

Instantly share code, notes, and snippets.

@intech
Created December 12, 2022 13:03
Show Gist options
  • Save intech/6273b75531f8ac8147108f170a74b07e to your computer and use it in GitHub Desktop.
Save intech/6273b75531f8ac8147108f170a74b07e to your computer and use it in GitHub Desktop.
import FSM from "./fsm";
const instance = new FSM([
// Start state, default state
{ state: "Green", from: "Red", to: ["Yellow", "Error"] },
// from is allowed array list states
{ state: "Yellow", from: "Green", to: ["Red", "Error"] },
// to is single, auto change state to Green
{ state: "Red", from: "Yellow", to: ["Green", "Error"] },
// End state
{ state: "Error", from: "*", end: true }
], "Green"); // Default state is Green
instance.preGreen = async (from, state) => { console.log(`Event 'preGreen' from '${from}' to ${state}`); return Promise.resolve(); };
instance.postGreen = async (to, state) => { console.log(`Event 'postGreen' from '${state}' to ${to}`); return Promise.resolve(); };
instance.preYellow = async (from, state) => { console.log(`Event 'preYellow' from '${from}' to ${state}`); return Promise.resolve(); };
instance.postYellow = async (to, state) => { console.log(`Event 'postYellow' from '${state}' to ${to}`); return Promise.resolve(); };
instance.preRed = async (from, state) => { console.log(`Event 'preRed' from '${from}' to ${state}`); return Promise.resolve(); };
instance.postRed = async (to, state) => { console.log(`Event 'postRed' from '${state}' to ${to}`); return Promise.resolve(); };
// example usages and results:
console.log(await instance.state()); // Yellow
console.log(await instance.state()); // Red
console.log(await instance.state()); // Green
try {
console.log(await instance.state("Red")); // throw StateError("Change state: Green -> Red");
} catch (error) {
console.log(error.message);
}
try {
console.log(await instance.state("Error")); // change to end state Error
} catch (error) {
console.log(error.message);
}
try {
console.log(await instance.state("Green")); // throw StateError("Change end state: Error");
} catch (error) {
console.log(error.message);
}
export default class FSM {
private currentState: string;
constructor(private states: FSMState[], init: string) {
const currentState = states.find(state => state.state === init)?.state;
if(!currentState) {
throw new StateError(`Cannot change init state to ${init}`);
}
this.currentState = currentState;
}
async state(toState?: string): Promise<string> {
const currentStateData = this.states.find(state => state.state === this.currentState);
const prevState = this.currentState;
if (this[`pre${this.currentState}`]) {
await this[`pre${this.currentState}`](toState, this.currentState);
}
if (currentStateData.end) {
throw new StateError(`End state reached: ${this.currentState}`);
}
if (toState) {
const toStateData = this.states.find(state => state.state === toState);
if (!currentStateData || !toStateData) {
throw new StateError("Invalid state provided");
}
// Check if transition from the current state to the requested state is allowed
if (!currentStateData.to.includes("*") && !currentStateData.to.includes(toState)) {
throw new StateError(`Cannot change state from ${this.currentState} to ${toState}`);
}
// Check if transition from the requested state to the current state is allowed
if (!toStateData.from.includes("*") && !toStateData.from.includes(this.currentState)) {
throw new StateError(`Cannot change state from ${toState} to ${this.currentState}`);
}
this.currentState = toState;
} else {
// Transition to a new state according to the current state and the `to` property of the state data
this.currentState = Array.isArray(currentStateData.to) ? currentStateData.to[0] : currentStateData.to;
}
if (this[`post${prevState}`]) {
this[`post${prevState}`](this.currentState, prevState);
}
return this.currentState;
}
}
export interface FSMState {
state: string;
from: string | string[];
to?: string | string[];
end?: boolean;
}
export class StateError extends Error {}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment