UNPKG

node-state-machine

Version:
584 lines (510 loc) 19.9 kB
'use strict'; var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); var _Transition = require('./Transition'); var _Transitions = require('./Transitions'); function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } /** * 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 */ var Machine = function () { /** * 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. */ function Machine() { _classCallCheck(this, Machine); for (var _len = arguments.length, states = Array(_len), _key = 0; _key < _len; _key++) { states[_key] = arguments[_key]; } if (states.length < 1) throw new Error('No states provided!'); this._initial = states[0]; this._states = new Map(); var _iteratorNormalCompletion = true; var _didIteratorError = false; var _iteratorError = undefined; try { for (var _iterator = states[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { var state = _step.value; this._states.set(state, new _Transitions.Transitions()); } } catch (err) { _didIteratorError = true; _iteratorError = err; } finally { try { if (!_iteratorNormalCompletion && _iterator.return) { _iterator.return(); } } finally { if (_didIteratorError) { throw _iteratorError; } } } 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. */ _createClass(Machine, [{ key: 'lock', value: function lock() { this._locked = true; } /** * Unlocks the state machine so it starts listening again to input. */ }, { key: 'unlock', value: function 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. */ }, { key: 'locked', value: function locked() { 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. */ }, { key: 'accepted', value: function accepted() { 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. */ }, { key: 'accepts', value: function accepts() { if (this.finalized()) throw new Error('This machine is finalized!'); for (var _len2 = arguments.length, states = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { states[_key2] = arguments[_key2]; } var _iteratorNormalCompletion2 = true; var _didIteratorError2 = false; var _iteratorError2 = undefined; try { for (var _iterator2 = states[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { var state = _step2.value; if (!this._states.has(state)) throw new Error('State ' + state + ' is not in this machine!'); this._accepts.add(state); } } catch (err) { _didIteratorError2 = true; _iteratorError2 = err; } finally { try { if (!_iteratorNormalCompletion2 && _iterator2.return) { _iterator2.return(); } } finally { if (_didIteratorError2) { throw _iteratorError2; } } } } /** * 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. */ }, { key: 'process', value: function process(v) { 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!'); var transitions = this._states.get(this._current); if (transitions == undefined) throw new Error('Transitions is undefined: FATAL!'); var transition = transitions.find(v); if (transition == undefined) throw new Error('Transition is undefined: FATAL!'); var old = this._current; if (transition.hasMiddlewares()) { var _iteratorNormalCompletion3 = true; var _didIteratorError3 = false; var _iteratorError3 = undefined; try { for (var _iterator3 = transition.middlewares()[Symbol.iterator](), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) { var middleware = _step3.value; middleware(v); } } catch (err) { _didIteratorError3 = true; _iteratorError3 = err; } finally { try { if (!_iteratorNormalCompletion3 && _iterator3.return) { _iterator3.return(); } } finally { if (_didIteratorError3) { throw _iteratorError3; } } } }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. */ }, { key: 'bulkProcess', value: function bulkProcess(vs) { var _iteratorNormalCompletion4 = true; var _didIteratorError4 = false; var _iteratorError4 = undefined; try { for (var _iterator4 = vs[Symbol.iterator](), _step4; !(_iteratorNormalCompletion4 = (_step4 = _iterator4.next()).done); _iteratorNormalCompletion4 = true) { var value = _step4.value; this.process(value); } } catch (err) { _didIteratorError4 = true; _iteratorError4 = err; } finally { try { if (!_iteratorNormalCompletion4 && _iterator4.return) { _iterator4.return(); } } finally { if (_didIteratorError4) { throw _iteratorError4; } } } } /** * 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. */ }, { key: 'from', value: function from(fromState) { var _this = this; return { to: function to(toState) { return { default: function _default() { _this._buildDefault(fromState, toState); }, when: function when(checker) { _this._buildTransition(fromState, toState, checker); }, on: function on(v) { if (v != '*') { _this._buildTransition(fromState, toState, function (o) { return o == v; }); } else { _this._buildDefault(fromState, toState); } }, middleware: function middleware() { for (var _len3 = arguments.length, middlewares = Array(_len3), _key3 = 0; _key3 < _len3; _key3++) { middlewares[_key3] = arguments[_key3]; } return { default: function _default() { _this._buildDefault(fromState, toState, middlewares); }, when: function when(checker) { _this._buildTransition(fromState, toState, checker, middlewares); }, on: function on(v) { if (v != '*') { _this._buildTransition(fromState, toState, function (o) { return o == v; }, middlewares); } else { _this._buildDefault(fromState, toState, middlewares); } } }; } }; } }; } /** * Will set default transitions from every state to itself. */ }, { key: 'setDefaults', value: function setDefaults() { var _iteratorNormalCompletion5 = true; var _didIteratorError5 = false; var _iteratorError5 = undefined; try { for (var _iterator5 = this._states[Symbol.iterator](), _step5; !(_iteratorNormalCompletion5 = (_step5 = _iterator5.next()).done); _iteratorNormalCompletion5 = true) { var state = _step5.value; this._buildDefault(state[0], state[0]); } } catch (err) { _didIteratorError5 = true; _iteratorError5 = err; } finally { try { if (!_iteratorNormalCompletion5 && _iterator5.return) { _iterator5.return(); } } finally { if (_didIteratorError5) { throw _iteratorError5; } } } } /** * 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. */ }, { key: 'isDeterministic', value: function isDeterministic(alphabet) { var _iteratorNormalCompletion6 = true; var _didIteratorError6 = false; var _iteratorError6 = undefined; try { for (var _iterator6 = alphabet[Symbol.iterator](), _step6; !(_iteratorNormalCompletion6 = (_step6 = _iterator6.next()).done); _iteratorNormalCompletion6 = true) { var element = _step6.value; var _iteratorNormalCompletion7 = true; var _didIteratorError7 = false; var _iteratorError7 = undefined; try { for (var _iterator7 = this._states[Symbol.iterator](), _step7; !(_iteratorNormalCompletion7 = (_step7 = _iterator7.next()).done); _iteratorNormalCompletion7 = true) { var state = _step7.value; try { var transition = state[1].find(element); if (transition == undefined) return false; } catch (error) { return false; } } } catch (err) { _didIteratorError7 = true; _iteratorError7 = err; } finally { try { if (!_iteratorNormalCompletion7 && _iterator7.return) { _iterator7.return(); } } finally { if (_didIteratorError7) { throw _iteratorError7; } } } } } catch (err) { _didIteratorError6 = true; _iteratorError6 = err; } finally { try { if (!_iteratorNormalCompletion6 && _iterator6.return) { _iterator6.return(); } } finally { if (_didIteratorError6) { throw _iteratorError6; } } } 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. */ }, { key: 'onChange', value: function onChange(change) { this._onChange = change; } /** * Finalizes the state machine so it is ready to use. */ }, { key: 'finalize', value: function finalize() { 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. */ }, { key: 'finalized', value: function finalized() { return this._finalized; } /** * Resets the state machine. */ }, { key: 'reset', value: function reset() { this.finalize(); } /** * Returns the initial state of this machine. This will be the first argument * of the constructor. * @return {string} The initial state. */ }, { key: 'initial', value: function initial() { 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. */ }, { key: 'current', value: function current() { 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. */ }, { key: 'numberOfStates', value: function numberOfStates() { return this._states.size; } /** * Returns a string representation of this state machine. * @return {string} A string representation of this state machine. */ }, { key: 'toString', value: function toString() { var str = '---------- StateMachine ----------\n'; str += 'Finalized: ' + this.finalized().toString() + '\n'; str += 'Accepted: ' + this.accepted().toString() + '\n'; if (this._current != undefined) str += 'Current: ' + this._current + ' \n'; var _iteratorNormalCompletion8 = true; var _didIteratorError8 = false; var _iteratorError8 = undefined; try { for (var _iterator8 = this._states[Symbol.iterator](), _step8; !(_iteratorNormalCompletion8 = (_step8 = _iterator8.next()).done); _iteratorNormalCompletion8 = true) { var states = _step8.value; str += '- ' + states[0] + ' \n ' + states[1].toString() + '\n'; } } catch (err) { _didIteratorError8 = true; _iteratorError8 = err; } finally { try { if (!_iteratorNormalCompletion8 && _iterator8.return) { _iterator8.return(); } } finally { if (_didIteratorError8) { throw _iteratorError8; } } } 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). */ }, { key: '_buildTransition', value: function _buildTransition(fromState, toState, checker) { var middlewares = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : []; 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!'); var transitions = this._states.get(fromState); if (transitions != undefined) { transitions.add(new _Transition.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). */ }, { key: '_buildDefault', value: function _buildDefault(fromState, toState) { var middlewares = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : []; 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!'); var transitions = this._states.get(fromState); if (transitions != undefined) { transitions.default(toState, middlewares); } else { throw new Error('Transitions not defined: FATAL!'); } } }]); return Machine; }(); module.exports.Machine = Machine;