UNPKG

node-state-machine

Version:
315 lines (287 loc) 11.8 kB
// @flow import { Transition } from './Transition'; import { Transitions } from './Transitions'; /** * Class representing a state machine which holds states and transitions and * is able to process input while keeping track of the current state. * @author Jensen Bernard */ class Machine { _finalized: boolean; _current: ?string; _initial: string; _states: Map<string, Transitions>; _locked: boolean; _accepts: Set<string>; _onChange: ?(from: string, to: string, value: string) => void; /** * Creates a new instance with the given states. Note that at least one state * should be provided. It then creates a new machine with these states, which * will be unfinalized and not locked. * @param states {string} All the states of the machine. * @throws Error {error} If no states are provided. */ constructor(...states: Array<string>) { if (states.length < 1) throw new Error(`No states provided!`); this._initial = states[0]; this._states = new Map(); for(var state of states) this._states.set(state, new Transitions()); this._finalized = false; this._onChange = undefined; this._locked = false; this._accepts = new Set(); } /** * Locks the state machine so it ignores all the input from now on. */ lock() { this._locked = true; } /** * Unlocks the state machine so it starts listening again to input. */ unlock() { this._locked = false; } /** * Returns whether or not this state machine is locked. * @return {boolean} True if and only if this state machine is locked. */ locked(): boolean { return this._locked; } /** * Checks if this state machine is currently in een accepting state. * @return {boolean} True if and only if the current state is an accepted state. */ accepted(): boolean { if (this._current == undefined) return false; return this._accepts.has(this._current); } /** * Adds the given states to the list of accepting states. This means that if * the current state is one of these, this.accepted() will return true. The * given states should already exist in this machine or an error will be * thrown. You can't call this function if this machine is finalized. * @param states {string} The states that should accept. * @throws Error {error} If this machine is finalized! * @throws Error {error} If one of the given states does not exists in the machine. */ accepts(...states: Array<string>) { if (this.finalized()) throw new Error(`This machine is finalized!`); for(var state of states) { if (!this._states.has(state)) throw new Error(`State ${state} is not in this machine!`); this._accepts.add(state); } } /** * Processes the given input and will change the internal state if necessary. * This can only be called if the machine is finalized! If the state is locked, * this function won't do anything. It also returns the machine for function * chaining. Just before setting the state, all midllewares will be executed. * @param v {string} The value to process. * @throws Error {error} If the machine if finalized. * @throws Error {error} If the current state is undefined (should never happen). * @throws Error {error} If there is no transitions instance for the current state (should never happen). * @throws Error {error} If there is no transition. */ process(v: string) { if (this.locked()) return this; if (!this.finalized()) throw new Error(`Machine is not finalized!`); if (this._current == undefined) throw new Error(`Current state is undefined: FATAL!`); let transitions = this._states.get(this._current); if (transitions == undefined) throw new Error(`Transitions is undefined: FATAL!`); let transition = transitions.find(v); if (transition == undefined) throw new Error(`Transition is undefined: FATAL!`); let old = this._current; if (transition.hasMiddlewares()) for(var middleware of transition.middlewares()) middleware(v); this._current = transition.destination(); if (this._onChange != undefined && old != undefined) this._onChange(old, this._current, v); return this; } /** * Processes the given input and will change the internal state if necessary. * This can only be called if the machine is finalized! If the state is locked, * this function won't do anything. Just before setting the state, all * midllewares will be executed. * @param v {string} The value to process. * @throws Error {error} If the machine if finalized. * @throws Error {error} If the current state is undefined (should never happen). * @throws Error {error} If there is no transitions instance for the current state (should never happen). * @throws Error {error} If there is no transition. */ bulkProcess(vs: Array<string>) { for(var value of vs) this.process(value); } /** * This function makes it possible for the cool method chaining ie .from.to * or .from.to.middleware.when. More information about how to use these * functions can be found in the tutorial. This function can probably be * written in a more beautiful way. */ from(fromState: string) { return { to: (toState: string) => { return { default: () => { this._buildDefault(fromState, toState); }, when: (checker: (v: string) => boolean) => { this._buildTransition(fromState, toState, checker); }, on: (v: string) => { if (v != '*') { this._buildTransition(fromState, toState, (o: string) => o == v); } else { this._buildDefault(fromState, toState); } }, middleware: (...middlewares: Array<(v: string) => void>) => { return { default: () => { this._buildDefault(fromState, toState, middlewares); }, when: (checker: (v: string) => boolean) => { this._buildTransition(fromState, toState, checker, middlewares); }, on: (v: string) => { if (v != '*') { this._buildTransition(fromState, toState, (o: string) => o == v, middlewares); } else { this._buildDefault(fromState, toState, middlewares); } }, }; }, }; }, }; } /** * Will set default transitions from every state to itself. */ setDefaults() { for(var state of this._states) this._buildDefault(state[0], state[0]); } /** * Returns whether or not the machine is deterministic for the given alphabet. * @return {boolean} If and only if the machine is deterministic for the alphabet. */ isDeterministic(alphabet: Array<string>) { for(var element of alphabet) { for(var state of this._states) { try { let transition = state[1].find(element); if (transition == undefined) return false; } catch (error) { return false; } } } return true; } /** * Sets the onChange function callback to the given function. Note that this * will override previous callbacks! * @param change {function} Callback to be executed on state change. */ onChange(change: (from: string, to: string, value: string) => void) { this._onChange = change; } /** * Finalizes the state machine so it is ready to use. */ finalize(): void { this._current = this._initial; this._finalized = true; } /** * Checks if the current state machine is finalized and ready to use. * @return {boolean} True if and only if the state machine is ready to use. */ finalized(): boolean { return this._finalized; } /** * Resets the state machine. */ reset() { this.finalize(); } /** * Returns the initial state of this machine. This will be the first argument * of the constructor. * @return {string} The initial state. */ initial(): string { return this._initial; } /** * Returns the current state of the machine. If the machine is not finalized * yet, an error will be thrown because the current state is not yet initialized. * @return {string} The current state of the state machine. * @throws Error {error} IF the machine is not finalized yet. */ current(): string { if (this._current == undefined) throw new Error(`Machine not started yet!`); return this._current; } /** * Returns the number of states of this state machine. * @return {number} The number of state of this state machine. */ numberOfStates(): number { return this._states.size; } /** * Returns a string representation of this state machine. * @return {string} A string representation of this state machine. */ toString(): string { let str = `---------- StateMachine ----------\n`; str += `Finalized: ${this.finalized().toString()}\n`; str += `Accepted: ${this.accepted().toString()}\n`; if (this._current != undefined) str += `Current: ${this._current} \n`; for(var states of this._states) str += `- ${states[0]} \n ${states[1].toString()}\n`; return str; } /** * Adds a transition with the given start state, end state, checker and middlewares * to this state machine. Will throw an error when the machine is not finalized. * Will also throw an error if one of the states does not exists. * @param fromState {string} Start state of the transition. * @param toState {string} End state of the transition. * @param checker {function} Checker/trigger of the transition. * @param middlewares {array} The middlewares of the transition. * @throws Error {error} If the machine is not finalized. * @throws Error {error} If the start or end state does not exist. * @throws Error {error} If no transitions instance is found for fromState (this will never happen). */ _buildTransition(fromState: string, toState: string, checker: (v: string) => boolean, middlewares: Array<(v: string) => void> = []) { if (this.finalized()) throw new Error(`This machine is finalized!`); if (!this._states.has(fromState) || !this._states.has(toState)) throw new Error(`Start or end state not defined!`); let transitions = this._states.get(fromState); if (transitions != undefined) { transitions.add(new Transition(checker, toState, middlewares)); } else { throw new Error(`Transitions not defined: FATAL!`); } } /** * Adds a transition with the given start state, end state and middlewares * to this state machine and uses it as a default transition. Will throw an * error when the machine is not finalized. Will also throw an error if one * of the states does not exists. * @param fromState {string} Start state of the transition. * @param toState {string} End state of the transition. * @param middlewares {array} The middlewares of the transition. * @throws Error {error} If the machine is not finalized. * @throws Error {error} If the start or end state does not exist. * @throws Error {error} If no transitions instance is found for fromState (this will never happen). */ _buildDefault(fromState: string, toState: string, middlewares: Array<(v: string) => void> = []) { if (this.finalized()) throw new Error(`This machine is finalized!`); if (!this._states.has(fromState) || !this._states.has(toState)) throw new Error(`Start or end state not defined!`); let transitions = this._states.get(fromState); if (transitions != undefined) { transitions.default(toState, middlewares); } else { throw new Error(`Transitions not defined: FATAL!`); } } } module.exports.Machine = Machine;