node-state-machine
Version:
State machine for NodeJS.
584 lines (510 loc) • 19.9 kB
JavaScript
'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;