node-state-machine
Version:
State machine for NodeJS.
315 lines (287 loc) • 11.8 kB
JavaScript
// @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;