state-machine
Version:
An expressive, feature-rich, event-driven JavaScript finite-state machine
1,869 lines (1,543 loc) • 73.2 kB
JavaScript
(function webpackUniversalModuleDefinition(root, factory) {
if(typeof exports === 'object' && typeof module === 'object')
module.exports = factory();
else if(typeof define === 'function' && define.amd)
define([], factory);
else {
var a = factory();
for(var i in a) (typeof exports === 'object' ? exports : root)[i] = a[i];
}
})(this, function() {
return /******/ (function(modules) { // webpackBootstrap
/******/ // The module cache
/******/ var installedModules = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/
/******/ // Check if module is in cache
/******/ if(installedModules[moduleId]) {
/******/ return installedModules[moduleId].exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = installedModules[moduleId] = {
/******/ i: moduleId,
/******/ l: false,
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ // Flag the module as loaded
/******/ module.l = true;
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/******/
/******/ // expose the modules object (__webpack_modules__)
/******/ __webpack_require__.m = modules;
/******/
/******/ // expose the module cache
/******/ __webpack_require__.c = installedModules;
/******/
/******/ // define getter function for harmony exports
/******/ __webpack_require__.d = function(exports, name, getter) {
/******/ if(!__webpack_require__.o(exports, name)) {
/******/ Object.defineProperty(exports, name, {
/******/ configurable: false,
/******/ enumerable: true,
/******/ get: getter
/******/ });
/******/ }
/******/ };
/******/
/******/ // getDefaultExport function for compatibility with non-harmony modules
/******/ __webpack_require__.n = function(module) {
/******/ var getter = module && module.__esModule ?
/******/ function getDefault() { return module['default']; } :
/******/ function getModuleExports() { return module; };
/******/ __webpack_require__.d(getter, 'a', getter);
/******/ return getter;
/******/ };
/******/
/******/ // Object.prototype.hasOwnProperty.call
/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
/******/
/******/ // __webpack_public_path__
/******/ __webpack_require__.p = "";
/******/
/******/ // Load entry module and return exports
/******/ return __webpack_require__(__webpack_require__.s = 5);
/******/ })
/************************************************************************/
/******/ ([
/* 0 */
/***/ (function(module, exports, __webpack_require__) {
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.isObject = isObject;
exports.isArray = isArray;
exports.isString = isString;
exports.isFunction = isFunction;
exports.isDefined = isDefined;
exports.isUndefined = isUndefined;
exports.trim = trim;
exports.diff = diff;
exports.toHash = toHash;
function isObject(value) {
return Object.prototype.toString.call(value) === '[object Object]';
}
function isArray(value) {
return value instanceof Array;
}
function isString(value) {
return typeof value === 'string';
}
function isFunction(value) {
return value instanceof Function;
}
function isDefined(value) {
return typeof value !== 'undefined';
}
function isUndefined(value) {
return typeof value === 'undefined';
}
function trim(value) {
return String(value || '').replace(/^\s+|\s+$/g, '');
}
function diff(a, b) {
var da = b.filter(function (v) {
return a.indexOf(v) < 0;
});
var db = a.filter(function (v) {
return b.indexOf(v) < 0;
});
return db.concat(da);
}
function toHash(values) {
return values.reduce(function (obj, value) {
obj[value] = true;
return obj;
}, {});
}
/***/ }),
/* 1 */
/***/ (function(module, exports, __webpack_require__) {
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }();
var _Config = __webpack_require__(6);
var _Config2 = _interopRequireDefault(_Config);
var _HandlerMap = __webpack_require__(7);
var _HandlerMap2 = _interopRequireDefault(_HandlerMap);
var _TransitionMap = __webpack_require__(13);
var _TransitionMap2 = _interopRequireDefault(_TransitionMap);
var _Transition = __webpack_require__(16);
var _Transition2 = _interopRequireDefault(_Transition);
var _utils = __webpack_require__(0);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
/**
* StateMachine constructor
*
* @param {Object|null} options
* @constructor
*/
function StateMachine(options) {
this.transitions = new _TransitionMap2.default();
this.handlers = new _HandlerMap2.default(this);
this.initialize(options);
}
/**
* StateMachine prototype
*
* The property examples below illustrate a 4-state machine, with states:
*
* - intro > settings > summary > end
*
* And actions:
*
* - back | next | restart | finish
*/
StateMachine.prototype = {
// -----------------------------------------------------------------------------------------------------------------
// properties
/**
* Configuration object
*
* @var {Config}
*/
config: null,
/**
* Map of all transitions
*
* @var {TransitionMap}
*/
transitions: null,
/**
* Map of all handlers
*
* @var {HandlerMap}
*/
handlers: null,
/**
* Any active Transition object that is driving the state change
*
* @var {Transition}
*/
transition: null,
/**
* The current state
*
* @var {string}
*/
state: '',
// -----------------------------------------------------------------------------------------------------------------
// private methods
/**
* Initialize the FSM with options
*
* @private
* @param options
*/
initialize: function initialize(options) {
var _this = this;
// state
this.state = '';
// build config
var config = new _Config2.default(options);
this.config = config;
// pre-process all transitions
var transitions = [];
if (Array.isArray(options.transitions)) {
options.transitions.forEach(function (tx) {
transitions = transitions.concat(_this.transitions.parse(tx));
});
}
// add transitions
transitions.forEach(function (transition) {
_this.transitions.add(transition.action, transition.from, transition.to);
});
// get initial state (must be done after state collation)
if (!config.initial) {
config.initial = this.transitions.getStates()[0];
}
// add handlers
if (options.handlers) {
for (var _name in options.handlers) {
if (options.handlers.hasOwnProperty(_name)) {
this.on(_name, options.handlers[_name]);
}
}
}
// add methods
if (options.methods) {
if (!this.config.scope) {
this.config.scope = this;
}
for (var name in options.methods) {
if (options.methods.hasOwnProperty(name) && !this.hasOwnProperty(name)) {
this[name] = options.methods[name];
}
}
}
// start
if (this.config.start) {
this.start();
}
// return
return this;
},
start: function start() {
this.state = this.config.initial;
this.handlers.trigger('system.start');
if (this.state) {
this.handlers.trigger('system.change', this.state);
}
return this;
},
/**
* Reset the FSM to the initial, or supplied, state
*
* @returns {StateMachine}
*/
reset: function reset() {
var initial = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
var state = initial || this.config.initial;
this.handlers.trigger('system.reset');
if (this.transition) {
this.transition.cancel();
delete this.transition;
}
if (this.state !== state) {
this.state = state;
this.handlers.trigger('system.change', this.state);
}
return this;
},
// -----------------------------------------------------------------------------------------------------------------
// api
/**
* Attempt to run an action, resulting in a transition to a state
*
* @param {string} action
* @param {*[]} rest
* @returns {boolean}
*/
do: function _do(action) {
if (this.canDo(action) && !this.isPaused()) {
for (var _len = arguments.length, rest = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
rest[_key - 1] = arguments[_key];
}
this.transition = _Transition2.default.create(this, action, rest);
this.transition.exec();
return true;
}
return false;
},
/**
* Attempt to go to a state
*
* Queries TransitionMap instance to see if a transition exists, then calls the related action if it does
*
* @param {string} state
* @param {boolean} [force]
* @returns {boolean}
*/
go: function go(state) {
var force = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
if (this.has(state)) {
if (force) {
if (this.transition) {
this.transition.clear();
}
this.transition = _Transition2.default.force(this, state);
return this.end();
}
var action = this.transitions.getActionFor(this.state, state);
if (action) {
return this.do(action);
}
this.config.errors > 0 && console.warn('No transition exists from "%s" to "%s"', this.state, state);
return false;
}
this.config.errors > 0 && console.warn('No such state "%s"', state);
return false;
},
/**
* Query transition map to see if a named action is available
*
* @param {string} action
* @returns {boolean}
*/
canDo: function canDo(action) {
return this.transitions.getActionsFrom(this.state).indexOf(action) !== -1;
},
/**
* Query transition map to see if a state is available to go to
*
* @param to
* @return {boolean}
*/
canGo: function canGo(to) {
return this.transitions.getActionFor(this.state, to) !== null;
},
/**
* Test if a state exists
*
* @param {string} state
* @return {boolean}
*/
has: function has(state) {
return this.transitions.hasState(state);
},
/**
* Test if the current state is the same as the supplied one
*
* @param {string} state A state name to compare against the current state
* @returns {boolean}
*/
is: function is(state) {
return state === this.state;
},
// -----------------------------------------------------------------------------------------------------------------
// flags
/**
* Test if the FSM has started
*
* @returns {boolean}
*/
isStarted: function isStarted() {
return this.state !== '';
},
/**
* Test if the FSM is transitioning
*
* @returns {boolean}
*/
isTransitioning: function isTransitioning() {
return !!this.transition;
},
/**
* Test if the FSM is paused (whilst transitioning)
*
* @returns {boolean}
*/
isPaused: function isPaused() {
return this.transition ? this.transition.paused : false;
},
/**
* Test if the FSM is on the "final" state
*
* @returns {boolean}
*/
isComplete: function isComplete() {
return this.state === this.config.final;
},
// -----------------------------------------------------------------------------------------------------------------
// transitions
/**
* Pause any current transition
*
* @returns {StateMachine}
*/
pause: function pause() {
if (this.transition && !this.isPaused()) {
this.transition.pause();
}
return this;
},
/**
* Resume any current transition
*
* @returns {StateMachine}
*/
resume: function resume() {
if (this.transition && this.isPaused()) {
this.transition.resume();
}
return this;
},
/**
* Cancel any current transition
*
* @returns {StateMachine}
*/
cancel: function cancel() {
if (this.transition) {
this.state = this.transition.from;
this.transition.cancel();
delete this.transition;
}
return this;
},
/**
* End any current transition, skipping remaining handlers
*
* @returns {StateMachine}
*/
end: function end() {
if (this.transition) {
this.state = this.transition.to;
this.transition.clear();
delete this.transition;
this.handlers.trigger('system.change', this.state);
if (this.isComplete()) {
this.handlers.trigger('system.complete');
}
}
return this;
},
// -----------------------------------------------------------------------------------------------------------------
// actions
/**
* Add a transition
*
* @param {string} action
* @param {string} from
* @param {string} to
* @return {StateMachine}
*/
add: function add(action, from, to) {
var _this2 = this;
// 1 argument: shorthand transition, i.e 'next : a > b'
if (arguments.length === 1) {
var transitions = this.transitions.parse(action);
transitions.forEach(function (tx) {
return _this2.add(tx.action, tx.from, tx.to);
});
return this;
}
// 3 arguments: longhand transition
updateTransitions(this, 'add', function () {
return _this2.transitions.add(action, from, to);
});
return this;
},
/**
* Remove a state
*
* @param {string} state
* @return {StateMachine}
*/
remove: function remove(state) {
var _this3 = this;
this.handlers.remove('state.' + state);
updateTransitions(this, 'remove', function () {
return _this3.transitions.remove(state);
});
return this;
},
// -----------------------------------------------------------------------------------------------------------------
// handlers
/**
* Add an event handler
*
* Event handler signatures are built from the following grammar:
*
* - token foo
* - property .foo
* - event :foo
* - action @foo
* - targets (foo bar baz)
*
* For example:
*
* - change
* - transition.pause
* - next:end
* - (a b)@next
* - a@next
*
* The main event types are unique, so can be used without the namespace:
*
* - change
* - pause
* - complete
* - ...
*
* If your states and events are unique, they can also be used without qualification.
*
* See docs and demo for more information
*
* @param {string} id
* @param {Function} fn
* @return {StateMachine}
*/
on: function on(id, fn) {
var _this4 = this;
this.parse(id, this.config.invalid, this.config.errors).forEach(function (meta) {
return _this4.handlers.add(meta.path, fn);
});
return this;
},
off: function off(id, fn) {
var _this5 = this;
this.parse(id, this.config.invalid, this.config.errors).forEach(function (meta) {
return _this5.handlers.remove(meta.path, fn);
});
return this;
},
// -----------------------------------------------------------------------------------------------------------------
// utilities
/**
* Parses a handler id string into HandlerMeta objects
*
* @param {string} id
* @param {boolean} invalid
* @param {number} errors
* @returns {HandlerMeta[]}
*/
parse: function parse(id) {
var _this6 = this;
var invalid = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
var errors = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 0;
return this.handlers.parse(id).filter(function (result) {
// picks up unrecognised handlers, namespaces, etc
if (result instanceof Error) {
if (errors == 2) {
throw result;
}
errors == 1 && console.warn(result.message);
return false;
}
// picks up unrecognised states and actions
if (result.target !== '*') {
var error = '';
if (result.namespace === 'state') {
if (!_this6.transitions.hasState(result.target)) {
error = 'Unrecognised state "' + result.target + '" in handler "' + result.id + '"';
}
} else if (result.namespace === 'action') {
if (!_this6.transitions.hasAction(result.target)) {
error = 'Unrecognised action "' + result.target + '" in handler "' + result.id + '"';
}
} else if (result.namespace === 'state/action') {
// variables
var _result$target$split = result.target.split('@'),
_result$target$split2 = _slicedToArray(_result$target$split, 2),
state = _result$target$split2[0],
action = _result$target$split2[1];
// test for state and action
if (!_this6.transitions.hasState(state)) {
error = 'Unrecognised state "' + state + '" in handler "' + result.id + '"';
}
if (!_this6.transitions.hasAction(action)) {
error = 'Unrecognised action "' + action + '" in handler "' + result.id + '"';
}
}
// if we have an error, the result was not an existing state or action
if (error) {
if (errors == 2) {
throw new Error(error);
}
errors == 1 && console.warn(error);
return !!invalid;
}
}
// must be valid
return true;
});
},
trigger: function trigger(id) {
var _this7 = this;
for (var _len2 = arguments.length, rest = Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) {
rest[_key2 - 1] = arguments[_key2];
}
this.handlers.parse(id).forEach(function (meta) {
return _this7.handlers.trigger.apply(_this7.handlers, [meta.path].concat(rest));
});
return this;
}
};
StateMachine.prototype.constructor = StateMachine;
exports.default = StateMachine;
// ---------------------------------------------------------------------------------------------------------------------
// static methods
/**
* Factory method
*
* @param options
* @returns {StateMachine}
*/
StateMachine.create = function (options) {
return new StateMachine(options);
};
/**
* Gets the default order events should be called in
* @returns {string[]}
*/
StateMachine.getDefaultOrder = function () {
return ['action.*.start', 'action.{action}.start', 'state.*.{action}', 'state.{from}.{action}', 'state.{from}.leave', 'state.*.leave', 'state.*.enter', 'state.{to}.enter', 'action.{action}.end', 'action.*.end'];
};
// ---------------------------------------------------------------------------------------------------------------------
// helper functions
/**
* Utility method to update transitions and dispatch events
*
* Saves duplicating the following code in both add() and remove() methods
*
* @param {StateMachine} fsm
* @param {string} method
* @param {Function} callback
*/
function updateTransitions(fsm, method, callback) {
var statesBefore = fsm.transitions.getStates();
var actionsBefore = fsm.transitions.getActions();
callback();
var statesAfter = fsm.transitions.getStates();
var actionsAfter = fsm.transitions.getActions();
// calculate differences
var states = (0, _utils.diff)(statesBefore, statesAfter);
var actions = (0, _utils.diff)(actionsBefore, actionsAfter);
// dispatch events
states.forEach(function (state) {
return fsm.handlers.trigger('system.state.' + method, state);
});
actions.forEach(function (action) {
return fsm.handlers.trigger('system.action.' + method, action);
});
}
/***/ }),
/* 2 */
/***/ (function(module, exports, __webpack_require__) {
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.values = exports.remove = exports.indexOf = exports.has = exports.get = exports.set = undefined;
exports.default = ValueMap;
var _utils = __webpack_require__(0);
/**
* Utility class to create, modify and delete nested hashes and values
*
* @constructor
*/
function ValueMap(data) {
this.data = data || {};
}
ValueMap.prototype = {
data: null,
set: function set(path, value) {
_set(this.data, path, value);
return this;
},
add: function add(path, value) {
_add(this.data, path, value);
return this;
},
insert: function insert(path, value) {
_insert(this.data, path, value);
return this;
},
get: function get(path) {
return _get(this.data, path);
},
has: function has(path) {
var value = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : undefined;
return _has(this.data, path, value);
},
indexOf: function indexOf(path, value) {
return _indexOf(this.data, path, value);
},
remove: function remove(path) {
var value = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : undefined;
_remove(this.data, path, value);
return this;
},
keys: function keys(path) {
return Object.keys(_get(this.data, path));
},
values: function values(path) {
return _values(this.data, path);
}
};
function create(obj, keys) {
var key = void 0;
while (keys.length) {
key = keys.shift();
if (!(0, _utils.isObject)(obj[key])) {
obj[key] = {};
}
obj = obj[key];
}
return obj;
}
function _set(obj, path, value) {
var index = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : -1;
var keys = String(path).split('.'),
key = keys.pop();
obj = create(obj, keys);
obj[key] = value;
}
exports.set = _set;
function _add(obj, path, value) {
var keys = String(path).split('.'),
key = keys.pop();
obj = create(obj, keys);
if (!(0, _utils.isArray)(obj[key])) {
obj[key] = [];
}
obj[key].push(value);
}
function _insert(obj, path, value) {
var keys = String(path).split('.'),
key = keys.pop();
obj = create(obj, keys);
if (!(0, _utils.isArray)(obj[key])) {
obj[key] = [];
}
var parent = obj[key],
index = parent.indexOf(value);
if (index === -1) {
parent.push(value);
} else {
parent[index] = value;
}
}
function _get(obj, path) {
if ((0, _utils.isUndefined)(path) || path == '') {
return obj;
}
var key = void 0,
keys = String(path).split('.');
while (keys.length > 1) {
key = keys.shift();
if (!obj.hasOwnProperty(key)) {
return;
}
obj = obj[key];
}
key = keys.shift();
return obj[key];
}
exports.get = _get;
function _has(obj, path, value) {
var parent = _get(obj, path);
return !!((0, _utils.isArray)(parent) && (0, _utils.isDefined)(value) ? parent.indexOf(value) !== -1 : (0, _utils.isUndefined)(value) ? (0, _utils.isDefined)(parent) : parent === value);
}
exports.has = _has;
function _indexOf(obj, path, value) {
var arr = _get(obj, path);
if ((0, _utils.isArray)(arr)) {
return arr.indexOf(value);
}
return -1;
}
exports.indexOf = _indexOf;
function _remove(obj, path, value) {
var parent = obj,
keys = String(path || '').split('.'),
key = keys.pop();
if (keys.length) {
parent = _get(obj, keys.join('.'));
}
if ((0, _utils.isDefined)(value) && (0, _utils.isArray)(parent[key])) {
var target = parent[key];
var index = target.indexOf(value);
if (index > -1) {
target.splice(index, 1);
if (target.length === 0) {
delete parent[key];
}
return true;
}
return false;
} else {
if ((0, _utils.isObject)(parent) && obj.hasOwnProperty(key)) {
delete parent[key];
return true;
}
}
return false;
}
exports.remove = _remove;
function _values(obj, path) {
var values = [];
var target = _get(obj, path);
if ((0, _utils.isObject)(target)) {
for (var name in target) {
if (target.hasOwnProperty(name)) {
values.push(target[name]);
}
}
}
return values;
}
exports.values = _values;
/***/ }),
/* 3 */
/***/ (function(module, exports, __webpack_require__) {
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.ActionEvent = ActionEvent;
exports.StateEvent = StateEvent;
exports.SystemEvent = SystemEvent;
exports.TransitionEvent = TransitionEvent;
// ------------------------------------------------------------------------------------------------
// setup
/**
* @prop {string} namespace The Event namespace; i.e. state or action
* @prop {string} type The Event type; i.e. leave/enter (state) or start/end (action)
* @prop {string} target The Event target; i.e. intro (state), next (action), or * (all states or types)
* @prop {Transition} transition The transition which generated the event
*/
var event = {
// properties
namespace: null,
type: null,
target: null,
transition: null
};
function initialize(event, namespace, type, target, transition) {
event.namespace = namespace;
event.type = type;
event.target = target;
event.transition = transition;
}
// ------------------------------------------------------------------------------------------------
// ActionEvent
function ActionEvent(type, target, transition) {
initialize(this, 'action', type, target, transition);
}
ActionEvent.prototype = event;
// ------------------------------------------------------------------------------------------------
// StateEvent
function StateEvent(type, target, transition) {
initialize(this, 'state', type, target, transition);
}
StateEvent.prototype = event;
// ------------------------------------------------------------------------------------------------
// SystemEvent
function SystemEvent(type, value) {
this.type = type;
this.value = value;
}
SystemEvent.prototype = {
namespace: 'system',
type: '',
value: null
};
// ------------------------------------------------------------------------------------------------
// TransitionEvent
function TransitionEvent(type) {
this.type = type;
}
TransitionEvent.prototype = {
namespace: 'transition',
type: ''
};
/***/ }),
/* 4 */
/***/ (function(module, exports, __webpack_require__) {
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.ParseError = ParseError;
function ParseError(message, path, id) {
this.message = message;
this.path = path;
this.id = id;
}
ParseError.prototype = Error.prototype;
ParseError.prototype.name = 'ParseError';
ParseError.prototype.constructor = ParseError;
/***/ }),
/* 5 */
/***/ (function(module, exports, __webpack_require__) {
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.StateHelper = exports.StateMachine = undefined;
var _StateMachine = __webpack_require__(1);
var _StateMachine2 = _interopRequireDefault(_StateMachine);
var _StateHelper = __webpack_require__(17);
var _StateHelper2 = _interopRequireDefault(_StateHelper);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
exports.default = _StateMachine2.default;
exports.StateMachine = _StateMachine2.default;
exports.StateHelper = _StateHelper2.default;
/***/ }),
/* 6 */
/***/ (function(module, exports, __webpack_require__) {
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = Config;
var _StateMachine = __webpack_require__(1);
var _StateMachine2 = _interopRequireDefault(_StateMachine);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function Config(options) {
var _this = this;
'scope start initial final invalid errors'.match(/\w+/g).forEach(function (name) {
if (options.hasOwnProperty(name)) {
_this[name] = options[name];
}
});
// order
this.order = options.order || _StateMachine2.default.getDefaultOrder();
// defaults
this.defaults = Object.assign({
// allow user to specify alternate triggers for event and action ids
action: 'start',
state: 'enter'
}, options.defaults);
}
Config.prototype = {
/**
* An optional scope to run handler functions in
*
* @var object
*/
scope: null,
/**
* A boolean to automatically start the state machine in the initial state
*
* @var boolean
*/
start: true,
/**
* A string to indicate which state to start on; defaults to ''
*
* @var string
*/
initial: '',
/**
* A string indicating the state to trigger a complete event; defaults to ''
*
* @var string
*/
final: '',
/**
* A boolean to allow non-existent states and actions to be added to the handlers object; defaults to false (disallow)
*
* @var boolean
*/
invalid: false,
/**
* A number indicating how to handle invalid or erroneous actions; defaults to 1 (warn)
*
* - 0 : quiet
* - 1 : console.warn()
* - 2 : throw an error
*
* @var number
*/
errors: 1,
/**
* The order to run transition callbacks in
*
* @type {string[]} type.target
*/
order: null,
/**
* Sets defaults for various declarations
*
* Available options are:
*
* - action: (start|end)
* - state: (enter|leave)
*
* @type {Object}
*/
defaults: null
};
/***/ }),
/* 7 */
/***/ (function(module, exports, __webpack_require__) {
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }();
var _ValueMap = __webpack_require__(2);
var _ValueMap2 = _interopRequireDefault(_ValueMap);
var _events = __webpack_require__(3);
var _utils = __webpack_require__(0);
var _HandlerParser = __webpack_require__(8);
var _HandlerParser2 = _interopRequireDefault(_HandlerParser);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function HandlerMap(fsm) {
this.fsm = fsm;
this.map = new _ValueMap2.default();
}
HandlerMap.prototype = {
fsm: null,
map: null,
/**
* Parse event handler grammar into a HandlerMeta structure
*
* @param {string} id The handler id to parse, i.e. '@next', 'intro:end', 'change', etc
* @returns {HandlerMeta[]}
*/
parse: function parse(id) {
return (0, _HandlerParser2.default)(id, this.fsm.config.defaults);
},
/**
* Directly add a new handler
*
* @param {string} path A 'namespace.target.type' target path to add a handler to
* @param {Function} fn A callback function
* @returns {HandlerMap}
*/
add: function add(path, fn) {
// check handler is a function
if (!(0, _utils.isFunction)(fn)) {
throw new Error('Error assigning "' + path + '" handler; callback is not a function', fn);
}
this.map.insert(path, fn);
return this;
},
/**
* Directly remove a handler target
*
* @param {string} path A 'namespace.target.type' parent to a handler removed from
* @param {Function} fn The instance of the callback function
* @returns {HandlerMap}
*/
remove: function remove(path, fn) {
this.map.remove(path, fn);
return this;
},
/**
* Get all handlers for a valid target path
*
* @param {string} path A 'namespace.target.type' target path
* @returns {Function[]} An array of callback functions
*/
get: function get(path) {
return this.map.get(path);
},
/**
* Dispatch an event
*
* @param {string} path
* @param {*} value
* @returns {StateMachine}
*/
trigger: function trigger(path) {
var _this = this;
var value = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
// create lookup path
var _path$match = path.match(/\w+/g),
_path$match2 = _slicedToArray(_path$match, 3),
namespace = _path$match2[0],
type = _path$match2[1],
method = _path$match2[2];
// build event
var event = void 0;
if (/^system\.(state|action)\./.test(path)) {
event = type === 'state' ? new _events.StateEvent(method, value) : new _events.ActionEvent(method, value);
} else {
event = namespace === 'system' ? new _events.SystemEvent(type, value) : new _events.TransitionEvent(type);
}
// dispatch
var handlers = this.map.get(path);
if (handlers) {
handlers.forEach(function (fn) {
return fn(event, _this.fsm);
});
}
}
};
exports.default = HandlerMap;
/***/ }),
/* 8 */
/***/ (function(module, exports, __webpack_require__) {
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = parse;
var _HandlerMeta = __webpack_require__(9);
var _HandlerMeta2 = _interopRequireDefault(_HandlerMeta);
var _Lexer = __webpack_require__(10);
var _Lexer2 = _interopRequireDefault(_Lexer);
var _utils = __webpack_require__(0);
var _errors = __webpack_require__(4);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
// ------------------------------------------------------------------------------------------------
// functions
function isSystem(token) {
return (/^(start|change|complete|reset)$/.test(token)
);
}
function isTransition(token) {
return (/^(pause|resume|cancel)$/.test(token)
);
}
function expandGroups(input) {
var rx = /\((.+?)\)/;
var matches = input.match(rx);
if (matches) {
var group = matches[0];
var items = matches[1].match(/\S+/g);
if (items) {
items = items.map(function (item) {
return input.replace(group, item);
});
if (rx.test(items[0])) {
return items.reduce(function (output, input) {
return output.concat(expandGroups(input));
}, []);
}
return items;
}
}
return [input];
}
function addPath(path, namespace, target) {
results.push(new _HandlerMeta2.default(_id, path, namespace, target));
return true;
}
function addError(message, path) {
var error = new _errors.ParseError(message, path, _id);
results.push(error);
return false;
}
// ------------------------------------------------------------------------------------------------
// export
/**
* Parses event handler id into a HandlerMeta results containing handler paths
*
* @param {string} id The handler id to parse, i.e. '@next', 'intro:end', 'change', etc
* @param {Object} defaults A StateMachine instance to test for states and actions
* @return {HandlerMeta[]}
*/
function parse(id, defaults) {
// pre-parse handler
id = (0, _utils.trim)(id);
// objects
_id = id;
_defaults = defaults;
results = [];
// parse
parser.parse(id, defaults);
// return
return results;
}
// ------------------------------------------------------------------------------------------------
// objects
var results = void 0,
_defaults = void 0,
_id = void 0;
var patterns = {
// start pause intro
alias: /^(\w+)$/,
// system.start state.add
namespaced: /^(system|transition|state|action)\.(\w+)$/,
// @next @quit
oneAction: /^@(\w+)$/,
// @next:start @next:end
oneActionEvent: /^@(\w+):(start|end)$/,
// :start :end
anyActionEvent: /^:(start|end)$/,
// intro form
oneState: /^#(\w+)$/,
// intro:enter intro:leave
oneStateEvent: /^#?(\w+):(leave|enter)$/,
// :enter :leave
anyStateEvent: /^:(enter|leave)$/,
// intro@next
oneStateAction: /^#?(\w+)@(\w+)$/
};
var lexer = new _Lexer2.default(patterns);
var parser = {
/**
* Parses event handler id into HandlerMeta instance
*
* Resolving namespace, type and target properties
*
* @param {string} id
* @param {Object} defaults
*/
parse: function parse(id, defaults) {
var _this = this;
// expand groups
var paths = expandGroups(id);
// process paths
paths.forEach(function (path) {
return _this.parsePath(path);
});
},
parsePath: function parsePath(path) {
var tokens = void 0;
try {
tokens = lexer.process(path);
} catch (error) {
return addError('Unrecognised pattern "' + path + '"', path);
}
if (tokens && tokens.length) {
// variables
var token = tokens.shift();
var fn = this[token.type];
// process
if (fn) {
return fn.apply(this, token.values);
}
return addError('Unknown token type "' + token.type + '"', path);
}
},
alias: function alias(value) {
if (isSystem(value)) {
return addPath('system.' + value, 'system');
}
if (isTransition(value)) {
return addPath('transition.' + value, 'transition');
}
return this.oneState(value);
},
namespaced: function namespaced(namespace, type) {
var path = namespace + '.' + type;
if (namespace === 'system' && isSystem(type) || namespace === 'transition' && isTransition(type)) {
return addPath(path, namespace);
}
if (/^(state|action)$/.test(namespace) && /^(add|remove)$/.test(type)) {
return addPath('system.' + path, 'system');
}
addError('Unrecognised type "' + type + '" for namespace "' + namespace + '"', _id);
},
oneState: function oneState(state) {
return addPath('state.' + state + '.' + _defaults.state, 'state', state);
},
oneAction: function oneAction(action) {
return addPath('action.' + action + '.' + _defaults.action, 'action', action);
},
anyActionEvent: function anyActionEvent(event) {
return addPath('action.*.' + event, 'action', '*');
},
oneActionEvent: function oneActionEvent(action, event) {
return addPath('action.' + action + '.' + event, 'action', action);
},
anyStateEvent: function anyStateEvent(event) {
return addPath('state.*.' + event, 'state', '*');
},
oneStateEvent: function oneStateEvent(state, event) {
return addPath('state.' + state + '.' + event, 'state', state);
},
oneStateAction: function oneStateAction(state, action) {
return addPath('state.' + state + '.' + action, 'state/action', state + '@' + action);
}
};
/***/ }),
/* 9 */
/***/ (function(module, exports, __webpack_require__) {
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
function HandlerMeta(id, path) {
var namespace = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : '';
var target = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : '';
this.id = id;
this.path = path;
if (namespace) {
this.namespace = namespace;
}
if (target) {
this.target = target;
}
}
HandlerMeta.prototype = {
id: '',
path: '',
namespace: '',
target: ''
};
exports.default = HandlerMeta;
/***/ }),
/* 10 */
/***/ (function(module, exports, __webpack_require__) {
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = Lexer;
var _Rule = __webpack_require__(11);
var _Rule2 = _interopRequireDefault(_Rule);
var _Token = __webpack_require__(12);
var _Token2 = _interopRequireDefault(_Token);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
/**
* Simple Lexer class
*
* @param {Object} rules A hash of id:RegExp values
* @constructor
*/
function Lexer(rules) {
var _this = this;
this.rules = [];
if (rules) {
Object.keys(rules).forEach(function (name) {
return _this.addRule(name, rules[name]);
});
}
}
Lexer.prototype = {
/** @var {String} */
source: null,
/** @var {Rule[]} */
rules: null,
/** @var {Token[]} */
tokens: null,
/** @var {Number} */
index: 0,
/**
* Process a source string into an array of Tokens based on Rules
*
* @param source
* @returns {Token[]}
*/
process: function process(source) {
this.source = source;
this.tokens = [];
this.index = 0;
this.next();
return this.tokens;
},
/**
* Adds a new rule
*
* @protected
* @param name
* @param rx
*/
addRule: function addRule(name, rx) {
this.rules.push(new _Rule2.default(name, rx));
},
next: function next() {
var _this2 = this;
if (this.index < this.source.length) {
var source = this.source.substr(this.index);
var state = this.rules.some(function (rule) {
var matches = source.match(rule.rx);
if (matches) {
_this2.tokens.push(new _Token2.default(rule.name, matches));
_this2.index += matches[0].length;
return true;
}
return false;
});
// not matched
if (!state) {
throw new LexerError('Unable to match source at position ' + this.index + ': "' + source + '"', this.source, this.index);
}
// match
this.next();
}
}
};
function LexerError(message, source, index) {
this.message = message;
this.source = source;
this.index = index;
}
LexerError.prototype = new Error();
LexerError.prototype.constructor = LexerError;
/***/ }),
/* 11 */
/***/ (function(module, exports, __webpack_require__) {
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
/**
* A parsing rule, designed to match part of a string
*
* @param {string} name
* @param {RegExp} rx
*/
function Rule(name, rx) {
this.name = name;
this.rx = rx;
}
exports.default = Rule;
/***/ }),
/* 12 */
/***/ (function(module, exports, __webpack_require__) {
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
/**
* Token class, representing the type and value of part of a source string
*
* @param {string} type
* @param {string[]} matches
*
* @property {string} type
* @property {string} match
* @property {string[]} values
*/
function Token(type, matches) {
this.type = type;
this.match = matches[0];
this.values = matches.slice(1);
}
exports.default = Token;
/***/ }),
/* 13 */
/***/ (function(module, exports, __webpack_require__) {
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
var _ValueMap = __webpack_require__(2);
var _ValueMap2 = _interopRequireDefault(_ValueMap);
var _TransitionParser = __webpack_require__(14);
var _TransitionParser2 = _interopRequireDefault(_TransitionParser);
var _utils = __webpack_require__(0);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } }
function TransitionMap() {
this.map = new _ValueMap2.default();
this.states = [];
this.actions = [];
}
TransitionMap.prototype = {
// ------------------------------------------------------------------------------------------------
// properties
map: null,
states: null,
actions: null,
// ------------------------------------------------------------------------------------------------
// add and remove states
/**
* Add event handler parsing
*
* @param {string} tx
* @returns {TransitionMeta[]}
*/
parse: function parse(tx) {
return (0, _TransitionParser2.default)(tx);
},
/**
* Adds a new transition
*
* @param {string} action
* @param {string} from
* @param {string} to
* @returns {TransitionMap}
*/
add: function add(action, from, to) {
// procss variables
action = (0, _utils.trim)(action);
from = (0, _utils.trim)(from);
to = typeof to === 'string' ? (0, _utils.trim)(to) : to;
// check for wildcards
if (to === '*') {
throw new Error('Transitioning to a wildcard doesn\'t make sense');
}
// add transition
this.map.set(from + '.' + action, to);
return update(this);
},
/**
* Removes an existing state
*
* @param {string} state
* @returns {TransitionMap}
*/
remove: function remove(state) {
// remove "from" state
this.map.remove(state);
// remove "to" states
var data = this.map.data;
for (var name in data) {
var from = data[name];
for (var action in from) {
if (from[action] === state) {
delete from[action];
}
}
}
// update and return
return update(this);
},
// ------------------------------------------------------------------------------------------------
// accessors
/**
* Get all available actions (or action => states map) for a given state
*
* @param {string} from Name of a state to get actions for
* @param {boolean} [asMap] Optional boolean to return a Object of action:state properties. Defaults to false
* @returns {string[]|Object} An array of string actions, or a hash of action:states
*/
getActionsFrom: function getActionsFrom(from) {
var asMap = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
if (this.has(from) || this.has('*')) {
// get all available actions
var actions = this.map.get(from) || {};
var wildcard = this.map.get('*');
var outp