UNPKG

kingly

Version:

State machine library (Extended Hierarchical State Transducer)

1,408 lines (1,266 loc) 120 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : typeof define === 'function' && define.amd ? define(['exports'], factory) : (factory((global.Kingly = {}))); }(this, (function (exports) { 'use strict'; function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function (obj) { return typeof obj; }; } else { _typeof = function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a 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); } } function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; } function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; var ownKeys = Object.keys(source); if (typeof Object.getOwnPropertySymbols === 'function') { ownKeys = ownKeys.concat(Object.getOwnPropertySymbols(source).filter(function (sym) { return Object.getOwnPropertyDescriptor(source, sym).enumerable; })); } ownKeys.forEach(function (key) { _defineProperty(target, key, source[key]); }); } return target; } function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); } function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); } function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); } function isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Date.prototype.toString.call(Reflect.construct(Date, [], function () {})); return true; } catch (e) { return false; } } function _construct(Parent, args, Class) { if (isNativeReflectConstruct()) { _construct = Reflect.construct; } else { _construct = function _construct(Parent, args, Class) { var a = [null]; a.push.apply(a, args); var Constructor = Function.bind.apply(Parent, a); var instance = new Constructor(); if (Class) _setPrototypeOf(instance, Class.prototype); return instance; }; } return _construct.apply(null, arguments); } function _isNativeFunction(fn) { return Function.toString.call(fn).indexOf("[native code]") !== -1; } function _wrapNativeSuper(Class) { var _cache = typeof Map === "function" ? new Map() : undefined; _wrapNativeSuper = function _wrapNativeSuper(Class) { if (Class === null || !_isNativeFunction(Class)) return Class; if (typeof Class !== "function") { throw new TypeError("Super expression must either be null or a function"); } if (typeof _cache !== "undefined") { if (_cache.has(Class)) return _cache.get(Class); _cache.set(Class, Wrapper); } function Wrapper() { return _construct(Class, arguments, _getPrototypeOf(this).constructor); } Wrapper.prototype = Object.create(Class.prototype, { constructor: { value: Wrapper, enumerable: false, writable: true, configurable: true } }); return _setPrototypeOf(Wrapper, Class); }; return _wrapNativeSuper(Class); } function _objectDestructuringEmpty(obj) { if (obj == null) throw new TypeError("Cannot destructure undefined"); } function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; } function _possibleConstructorReturn(self, call) { if (call && (typeof call === "object" || typeof call === "function")) { return call; } return _assertThisInitialized(self); } function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _nonIterableRest(); } function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; } function _iterableToArrayLimit(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"] != null) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } var SEP = '.'; var TRANSITION_SYMBOL = "-->"; var TRANSITION_LABEL_START_SYMBOL = ":"; var HISTORY_STATE_NAME = "H"; var HISTORY_PREFIX = 'history.'; // CONSTANTS var INIT_STATE = 'nok'; var INIT_EVENT = 'init'; var AUTO_EVENT = 'auto'; var STATE_PROTOTYPE_NAME = 'State'; // !!must be the function name for the constructor State, // i.e. State var NO_STATE_UPDATE = []; // NOTE : this really cannot be anything else than a falsy value, beware var NO_OUTPUT = []; var ACTION_IDENTITY = function ACTION_IDENTITY() { return { outputs: NO_OUTPUT, updates: NO_STATE_UPDATE }; }; var SHALLOW = 'shallow'; var DEEP = 'deep'; var WRONG_EVENT_FORMAT_ERROR = "The machine received an event which does not have the proper format. Expecting an object whose unique key is the event name, and value is the event data."; var CONTRACTS_EVAL = "CONTRACTS_EVAL"; var OUTPUTS_MSG = "OUTPUTS_MSG"; var INPUT_MSG = "INPUT_MSG"; var WARN_MSG = 'WARN_MSG'; var MACHINE_CREATION_ERROR_MSG = 'MACHINE_CREATION_ERROR_MSG'; var ERROR_MSG = 'ERROR_MSG'; var INTERNAL_INPUT_MSG = 'INTERNAL_INPUT_MSG'; var INTERNAL_OUTPUTS_MSG = 'INTERNAL_OUTPUTS_MSG'; var DEBUG_MSG = 'DEBUG_MSG'; var INIT_INPUT_MSG = 'INIT_INPUT_MSG'; const PATH_ROOT = [0]; const PRE_ORDER = "PRE_ORDER"; ///// Utility functions // Cheap cloning, which is enough for our needs : we only clone seeds and empty values, which are generally simple // objects function clone(a) { return a === undefined ? undefined : JSON.parse(JSON.stringify(a)) } function merge(objA, objB) { return Object.assign({}, objA, objB); } /** * * @param {Map} traversalState * @param subTree * @param {Array} subTreeChildren * @modifies {traversalState} */ function updatePathInTraversalState(traversalState, subTree, subTreeChildren) { subTreeChildren.forEach((subTreeChild, index) => { const traversalStateParent = traversalState.get(subTree); // NOTE : if the path is already set we do not modify it. This allows for post-order traversal, which puts back // the parent node into the children nodes to keep the original path for the parent node. So at any time, the // `path` value can be trusted to be accurately describing the location of the node in the tree const traversalStateChild = traversalState.get(subTreeChild); const currentChildPath = traversalStateChild && traversalStateChild.path; traversalState.set( subTreeChild, merge(traversalStateChild, { isAdded: true, isVisited: false, path: currentChildPath || traversalStateParent.path.concat(index) }) ); }); } /** * * @param {Map} traversalState * @param tree * @modifies {traversalState} */ function updateVisitInTraversalState(traversalState, tree) { traversalState.set( tree, merge(traversalState.get(tree), { isVisited: true }) ); } ///// Core API function visitTree(traversalSpecs, tree) { const { store, lenses, traverse } = traversalSpecs; const { empty: emptyOrEmptyConstructor, add, takeAndRemoveOne, isEmpty } = store; const { getChildren } = lenses; const { visit, seed: seedOrSeedConstructor } = traverse; const traversalState = new Map(); // NOTE : This allows to have seeds which are non-JSON objects, such as new Map(). We force a new here to make // sure we have an object that cannot be modified out of the scope of visitTree and collaborators const seed = (typeof seedOrSeedConstructor === 'function') ? new (seedOrSeedConstructor()) : clone(seedOrSeedConstructor); const empty = (typeof emptyOrEmptyConstructor === 'function') ? new (emptyOrEmptyConstructor()) : clone(emptyOrEmptyConstructor); let currentStore = empty; let visitAcc = seed; add([tree], currentStore); traversalState.set(tree, { isAdded: true, isVisited: false, path: PATH_ROOT }); while ( !isEmpty(currentStore) ) { const subTree = takeAndRemoveOne(currentStore); const subTreeChildren = getChildren(traversalState, subTree); add(subTreeChildren, currentStore); updatePathInTraversalState(traversalState, subTree, subTreeChildren); visitAcc = visit(visitAcc, traversalState, subTree); updateVisitInTraversalState(traversalState, subTree); } // Free the references to the tree/subtrees traversalState.clear(); return visitAcc; } function breadthFirstTraverseTree(lenses, traverse, tree) { const { getChildren } = lenses; const traversalSpecs = { store: { empty: [], takeAndRemoveOne: store => store.shift(), isEmpty: store => store.length === 0, add: (subTrees, store) => store.push.apply(store, subTrees) }, lenses: { getChildren: (traversalState, subTree) => getChildren(subTree) }, traverse }; return visitTree(traversalSpecs, tree); } function preorderTraverseTree(lenses, traverse, tree) { const { getChildren } = lenses; const traversalSpecs = { store: { empty: [], takeAndRemoveOne: store => store.shift(), isEmpty: store => store.length === 0, // NOTE : vs. bfs, only `add` changes add: (subTrees, store) => store.unshift(...subTrees) }, lenses: { getChildren: (traversalState, subTree) => getChildren(subTree) }, traverse }; return visitTree(traversalSpecs, tree); } function postOrderTraverseTree(lenses, traverse, tree) { const { getChildren } = lenses; const isLeaf = (tree, traversalState) => getChildren(tree, traversalState).length === 0; const { seed, visit } = traverse; const predicate = (tree, traversalState) => traversalState.get(tree).isVisited || isLeaf(tree, traversalState); const decoratedLenses = { // For post-order, add the parent at the end of the children, that simulates the stack for the recursive function // call in the recursive post-order traversal algorithm // DOC : getChildren(tree, traversalState) also admit traversalState as argumnets but in second place getChildren: (traversalState, tree) => predicate(tree, traversalState) ? [] : getChildren(tree, traversalState).concat([tree]) }; const traversalSpecs = { store: { empty: [], takeAndRemoveOne: store => store.shift(), isEmpty: store => store.length === 0, add: (subTrees, store) => store.unshift(...subTrees) }, lenses: decoratedLenses, traverse: { seed: seed, visit: (result, traversalState, tree) => { // Cases : // 1. label has been visited already : visit // 2. label has not been visited, and there are no children : visit // 3. label has not been visited, and there are children : don't visit, will do it later return predicate(tree, traversalState) ? visit(result, traversalState, tree) : result } } }; return visitTree(traversalSpecs, tree); } // Object as a tree function isLeafLabel(label) { return objectTreeLenses.getChildren(label).length === 0 } const objectTreeLenses = { isLeafLabel, getLabel: tree => { if (typeof tree === 'object' && !Array.isArray(tree) && Object.keys(tree).length === 1) { return tree; } else { throw `getLabel > unexpected object tree value` } }, getChildren: tree => { if (typeof tree === 'object' && !Array.isArray(tree) && Object.keys(tree).length === 1) { let value = Object.values(tree)[0]; if (value && typeof value === 'object' && !Array.isArray(value)) { return Object.keys(value).map(prop => ({ [prop]: value[prop] })) } else { return [] } } else { throw `getChildren > unexpected value` } }, constructTree: (label, children) => { const labelKey = label && Object.keys(label) && Object.keys(label)[0]; return children.length === 0 ? label : { [labelKey]: Object.assign.apply(null, children) } }, }; function traverseObj(traverse, obj){ const treeObj = {root : obj}; const {strategy, seed, visit} = traverse; const traverseFn = { BFS : breadthFirstTraverseTree, PRE_ORDER : preorderTraverseTree, POST_ORDER: postOrderTraverseTree }[strategy] || preorderTraverseTree; const decoratedTraverse = { seed, visit : function visitAllButRoot(visitAcc, traversalState, tree){ const {path} = traversalState.get(tree); return JSON.stringify(path)=== JSON.stringify(PATH_ROOT) ? visitAcc : visit(visitAcc, traversalState, tree) } }; const traversedTreeObj = traverseFn(objectTreeLenses, decoratedTraverse, treeObj); return traversedTreeObj } // Arrays as trees const arrayTreeLenses = { getLabel: tree => { return Array.isArray(tree) ? tree[0] : tree }, getChildren: tree => { return Array.isArray(tree) ? tree[1] : [] }, constructTree: (label, children) => { return children && Array.isArray(children) && children.length > 0 ? [label, children] : label }, }; var noop = function noop() {}; var emptyConsole = { log: noop, warn: noop, info: noop, debug: noop, error: noop, trace: noop, group: noop, groupEnd: noop }; var emptyTracer = noop; function isFunction(x) { return typeof x === 'function'; } function isControlState(x) { return x && typeof x === 'string' || isHistoryControlState(x); } function isEvent(x) { return typeof x === 'undefined' || typeof x === 'string'; } function isActionFactory(x) { return x && typeof x === 'function'; } /** * Returns the name of the function as taken from its source definition. * For instance, function do_something(){} -> "do_something" * @param fn {Function} * @returns {String} */ function get_fn_name(fn) { var tokens = /^[\s\r\n]*function[\s\r\n]*([^\(\s\r\n]*?)[\s\r\n]*\([^\)\s\r\n]*\)[\s\r\n]*\{((?:[^}]*\}?)+)\}\s*$/.exec(fn.toString()); return tokens[1]; } function wrap(str) { return ['-', str, '-'].join(""); } function times$1(fn, n) { return Array.apply(null, { length: n }).map(Number.call, Number).map(fn); } function keys(obj) { return Object.keys(obj); } function is_history_transition(transition) { return transition.to.startsWith(HISTORY_PREFIX); } function is_entry_transition(transition) { return transition.event === INIT_EVENT; } function is_from_control_state(controlState) { return function (transition) { return transition.from === controlState; }; } function is_to_history_control_state_of(controlState) { return function (transition) { return is_history_control_state_of(controlState, transition.to); }; } function is_history_control_state_of(controlState, state) { return state.substring(HISTORY_PREFIX.length) === controlState; } function format_transition_label(_event, predicate, action) { var event = _event || ''; return predicate && action ? "".concat(event, " [").concat(predicate.name, "] / ").concat(action.name) : predicate ? "".concat(event, " [").concat(predicate.name, "]}") : action ? "".concat(event, " / ").concat(action.name) : "".concat(event); } function format_history_transition_state_name(_ref) { var from = _ref.from, to = _ref.to; return "".concat(from, ".").concat(to.substring(HISTORY_PREFIX.length), ".").concat(HISTORY_STATE_NAME); } function get_all_transitions(transition) { var from = transition.from, event = transition.event, guards = transition.guards; return guards ? guards.map(function (_ref2) { var predicate = _ref2.predicate, to = _ref2.to, action = _ref2.action; return { from: from, event: event, predicate: predicate, to: to, action: action }; }) : [transition]; } /** * 'this_name' => 'this name' * @param {String} str * @returns {String} */ function getDisplayName(str) { return str.replace(/_/g, ' '); } function getStatesType(statesTree) { var getLabel = objectTreeLenses.getLabel, isLeafLabel = objectTreeLenses.isLeafLabel; var traverse = { strategy: PRE_ORDER, seed: {}, visit: function visit(acc, traversalState, tree) { var treeLabel = getLabel(tree); var controlState = Object.keys(treeLabel)[0]; // true iff control state is a compound state return isLeafLabel(treeLabel) ? (acc[controlState] = false, acc) : (acc[controlState] = true, acc); } }; return traverseObj(traverse, statesTree); } function getStatesPath(statesTree) { var getLabel = objectTreeLenses.getLabel; var traverse = { strategy: PRE_ORDER, seed: {}, visit: function visit(acc, traversalState, tree) { var pathStr = traversalState.get(tree).path.join('.'); var treeLabel = getLabel(tree); var controlState = Object.keys(treeLabel)[0]; return acc[controlState] = pathStr, acc; } }; return traverseObj(traverse, statesTree); } function getStatesTransitionsMap(transitions) { // Map a control state to the transitions which it as origin return transitions.reduce(function (acc, transition) { var from = transition.from, event = transition.event; // NOTE: that should never be, but we need to be defensive here to keep semantics if (isHistoryControlState(from)) return acc; acc[from] = acc[from] || {}; acc[from][event] = transition; return acc; }, {}) || {}; } function getStateEventTransitionsMaps(transitions) { // Map a control state to the transitions which it as origin return transitions.reduce(function (acc, transition) { var from = transition.from, event = transition.event; // NOTE: that should never be, but we need to be defensive here to keep semantics if (isHistoryControlState(from)) return acc; acc[from] = acc[from] || {}; acc[from][event] = acc[from][event] ? acc[from][event].concat(transition) : [transition]; return acc; }, {}) || {}; } function getEventTransitionsMaps(transitions) { // Map an event to the origin control states of the transitions it triggers return transitions.reduce(function (acc, transition) { var from = transition.from, event = transition.event; // NOTE: that should never be, but we need to be defensive here to keep semantics if (isHistoryControlState(from)) return acc; acc[event] = acc[event] || {}; acc[event][from] = acc[event][from] ? acc[event][from].concat(transition) : [transition]; return acc; }, {}) || {}; } function getHistoryStatesMap(transitions) { return reduceTransitions(function (map, flatTransition, guardIndex, transitionIndex) { var from = flatTransition.from, event = flatTransition.event, to = flatTransition.to, action = flatTransition.action, predicate = flatTransition.predicate, gen = flatTransition.gen; if (isHistoryControlState(from)) { var underlyingControlState = getHistoryUnderlyingState(from); map.set(underlyingControlState, (map.get(underlyingControlState) || []).concat([flatTransition])); } else if (isHistoryControlState(to)) { var _underlyingControlState = getHistoryUnderlyingState(to); map.set(_underlyingControlState, (map.get(_underlyingControlState) || []).concat([flatTransition])); } return map; }, new Map(), transitions) || {}; } function getTargetStatesMap(transitions) { return reduceTransitions(function (map, flatTransition, guardIndex, transitionIndex) { var to = flatTransition.to; map.set(to, (map.get(to) || []).concat([flatTransition])); return map; }, new Map(), transitions) || {}; } function getAncestorMap(statesTree) { var getLabel = objectTreeLenses.getLabel, getChildren = objectTreeLenses.getChildren; var traverse = { strategy: PRE_ORDER, seed: {}, visit: function visit(acc, traversalState, tree) { var treeLabel = getLabel(tree); var controlState = Object.keys(treeLabel)[0]; var children = getChildren(tree); var childrenControlStates = children.map(function (tree) { return Object.keys(getLabel(tree))[0]; }); childrenControlStates.forEach(function (state) { acc[state] = acc[state] || []; acc[state] = acc[state].concat(controlState); }); return acc; } }; return traverseObj(traverse, statesTree); } function computeHistoryMaps(control_states) { if (Object.keys(control_states).length === 0) { throw "computeHistoryMaps : passed empty control states parameter?"; } var getLabel = objectTreeLenses.getLabel; var traverse = { strategy: PRE_ORDER, seed: { stateList: [], stateAncestors: {} }, visit: function visit(acc, traversalState, tree) { var treeLabel = getLabel(tree); var controlState = Object.keys(treeLabel)[0]; acc.stateList = acc.stateList.concat(controlState); // NOTE : we don't have to worry about path having only one element // that case correspond to the root of the tree which is excluded from visiting var _traversalState$get = traversalState.get(tree), path = _traversalState$get.path; traversalState.set(JSON.stringify(path), controlState); var parentPath = path.slice(0, -1); if (parentPath.length === 1) { // That's the root traversalState.set(JSON.stringify(parentPath), INIT_STATE); } else { var parentControlState = traversalState.get(JSON.stringify(parentPath)); acc.stateAncestors[controlState] = [parentControlState]; var _path$reduce = path.reduce(function (acc, _) { var parentPath = acc.path.slice(0, -1); acc.path = parentPath; if (parentPath.length > 1) { var _parentControlState = traversalState.get(JSON.stringify(parentPath)); acc.ancestors = acc.ancestors.concat(_parentControlState); } return acc; }, { ancestors: [], path: path }), ancestors = _path$reduce.ancestors; acc.stateAncestors[controlState] = ancestors; } return acc; } }; var _traverseObj = traverseObj(traverse, control_states), stateList = _traverseObj.stateList, stateAncestors = _traverseObj.stateAncestors; return { stateList: stateList, stateAncestors: stateAncestors }; } function reduceTransitions(reduceFn, seed, transitions) { var result = transitions.reduce(function (acc, transitionStruct, transitionIndex) { var from = transitionStruct.from, event = transitionStruct.event, to = transitionStruct.to, gen = transitionStruct.gen, action = transitionStruct.action, guards = transitionStruct.guards; // Edge case when no guards are defined if (!guards) { guards = gen ? [{ to: to, action: action, gen: gen, predicate: undefined }] : [{ to: to, action: action, predicate: undefined }]; } return guards.reduce(function (acc, guard, guardIndex) { var to = guard.to, action = guard.action, gen = guard.gen, predicate = guard.predicate; return gen ? reduceFn(acc, { from: from, event: event, to: to, action: action, predicate: predicate, gen: gen }, guardIndex, transitionIndex) : reduceFn(acc, { from: from, event: event, to: to, action: action, predicate: predicate }, guardIndex, transitionIndex); }, acc); }, seed); return result; } /** * @description takes an output and turns it into an array * @param {*} output * @returns {*[]|null} if the output is null: null, if output is an array: output, else: [output] */ function arrayizeOutput(output) { return output === NO_OUTPUT ? NO_OUTPUT : Array.isArray(output) ? output : [output]; } function isHistoryControlState(to) { return _typeof(to) === 'object' && (DEEP in to || SHALLOW in to); } function getHistoryType(history) { return history[DEEP] ? DEEP : SHALLOW; } function getHistoryUnderlyingState(history) { return history[getHistoryType(history)]; } /** * Creates a history object from a state list. The created history object represents the history states when no * control states have been entered or exited. * @param stateList * @returns {History} */ function initHistoryDataStructure(stateList) { var _ref4; // NOTE : we update history in place, so we need two different objects here, even // when they start with the same value var initHistory = function initHistory() { return stateList.reduce(function (acc, state) { return acc[state] = '', acc; }, {}); }; return _ref4 = {}, _defineProperty(_ref4, DEEP, initHistory()), _defineProperty(_ref4, SHALLOW, initHistory()), _ref4; } /** * Updates the history state (both deep and shallow) after `state_from_name` has been exited. Impacted states are the * `stateAncestors` which are the ancestors for the exited state. * @param {HistoryState} _history Contains deep history and shallow history for all * control states, except the INIT_STATE (not that the concept has no value for atomic state). The function * `updateHistory` allows to update the history as transitions occur in the state machine. * @param {Object.<DEEP|SHALLOW, Object.<ControlState, Array<ControlState>>>} stateAncestors * @returns {HistoryState} * @modifies history */ function updateHistory(_history, stateAncestors, state_from_name) { var _history2; // 27.08.2020: Now that I expose history state I have to make sure that it is not mutated!! // We have a fixed format here, so we use native `assign` at deepest level var history = (_history2 = {}, _defineProperty(_history2, DEEP, Object.assign({}, _history[DEEP])), _defineProperty(_history2, SHALLOW, Object.assign({}, _history[SHALLOW])), _history2); // Edge case, we start with INIT_STATE but that is not kept in the history (no transition to it!!) if (state_from_name === INIT_STATE) { return history; } else { // ancestors for the state which is exited var ancestors = stateAncestors[state_from_name] || []; ancestors.reduce(function (oldAncestor, newAncestor) { // set the exited state in the history of all ancestors history[DEEP][newAncestor] = state_from_name; history[SHALLOW][newAncestor] = oldAncestor; return newAncestor; }, state_from_name); return history; } } function findInitTransition(transitions) { return transitions.find(function (transition) { return transition.from === INIT_STATE && transition.event === INIT_EVENT; }); } /** * * @param {function(...*): True | Error} contract Contract that either fulfills or returns an error * @param {Array<*>} arrayParams Parameters to be passed to the contract * @returns {undefined|{when, location, info, message}|Object} either true (fulfilled contract) * or an object with optional properties for diagnostic and tracing purposes * about the cause of the error if the contract is not fulfilled */ function assert(contract, arrayParams) { var contractName = contract.name || ""; var isFulfilledOrError = contract.apply(null, arrayParams); if (isFulfilledOrError === true) return void 0;else { return _objectSpread({}, isFulfilledOrError, { when: "Checking contract", message: [isFulfilledOrError.message, "failed contract ".concat(contractName)].join("\n"), info: isFulfilledOrError.info }); } } function isActions(obj) { return obj && "updates" in obj && "outputs" in obj && Array.isArray(obj.outputs); // && Array.isArray(obj.updates) // !! does not have to be arrays. HAs to be anything that is accepted by updateState } /** * That is a Either contract, not a Boolean contract! * @param obj * @returns {boolean|Error} */ function isEventStruct(obj) { var trueOrError; if (!obj || _typeof(obj) !== 'object') { trueOrError = new Error(WRONG_EVENT_FORMAT_ERROR); trueOrError.info = { event: obj, cause: "not an object!" }; } else if (Object.keys(obj).length > 1) { trueOrError = new Error(WRONG_EVENT_FORMAT_ERROR); trueOrError.info = { event: obj, cause: "Event objects must have only one key which is the event name!" }; } else trueOrError = true; return trueOrError; } function destructureEvent(obj) { var eventName = Object.keys(obj)[0]; var eventData = obj[eventName]; return { eventName: eventName, eventData: eventData }; } function wrapUpdateStateFn(userProvidedUpdateStateFn, _ref9) { var throwKinglyError = _ref9.throwKinglyError, tracer = _ref9.tracer; return function (extendedState, updates) { var fnName = userProvidedUpdateStateFn.name || ""; try { return userProvidedUpdateStateFn(extendedState, updates); } catch (e) { throwKinglyError({ when: "Executing updateState function ".concat(fnName), location: "createStateMachine > wrappedUpdateState", info: { extendedState: extendedState, updates: updates }, message: e.message, stack: e.stack }); } }; } function throwKinglyErrorFactory(console, tracer) { return function (obj) { throw new KinglyError(obj, console, tracer); }; } var KinglyError = /*#__PURE__*/ function (_Error) { _inherits(KinglyError, _Error); function KinglyError(m, console, tracer) { var _this; _classCallCheck(this, KinglyError); _this = _possibleConstructorReturn(this, _getPrototypeOf(KinglyError).call(this, m && m.message || "")); _this.name = "KinglyError"; _this.stack = m && m.stack || _this.stack; _this.errors = m; var _ref10 = m || {}, when = _ref10.when, location = _ref10.location, info = _ref10.info, message = _ref10.message; var fm = "At ".concat(location, ": ").concat(when, " => ").concat(message); var infoMsg = info ? "See extra info in console" : ""; var fullMsg = [fm, infoMsg].join("\n"); // this.message = fullMsg; console && console.error(fullMsg); info && console && console.info(info); return _this; } return KinglyError; }(_wrapNativeSuper(Error)); // S2. State names must be unique var noDuplicatedStates = { name: 'noDuplicatedStates', shouldThrow: false, predicate: function predicate(fsmDef, settings) { var getLabel = objectTreeLenses.getLabel; var traverse = { strategy: PRE_ORDER, seed: { duplicatedStates: [], statesHashMap: {} }, visit: function visit(acc, traversalState, tree) { var duplicatedStates = acc.duplicatedStates, statesHashMap = acc.statesHashMap; var treeLabel = getLabel(tree); var controlState = Object.keys(treeLabel)[0]; if (controlState in statesHashMap) { return { duplicatedStates: duplicatedStates.concat(controlState), statesHashMap: statesHashMap }; } else { return { duplicatedStates: duplicatedStates, statesHashMap: (statesHashMap[controlState] = "", statesHashMap) }; } } }; var _traverseObj = traverseObj(traverse, fsmDef.states), duplicatedStates = _traverseObj.duplicatedStates; var isFulfilled = duplicatedStates.length === 0; return { isFulfilled: isFulfilled, blame: { message: "State names must be unique! Found duplicated state names. Cf. log", info: { duplicatedStates: duplicatedStates } } }; } }; // S1. State name cannot be a reserved state name (for now only INIT_STATE) var noReservedStates = { name: 'noReservedStates', shouldThrow: false, predicate: function predicate(fsmDef, settings, _ref) { var statesType = _ref.statesType; return { isFulfilled: Object.keys(statesType).indexOf(INIT_STATE) === -1, blame: { message: "You cannot use a reserved control state name for any of the configured control states for the machine! Cf. log", info: { reservedStates: [INIT_STATE], statesType: statesType } } }; } }; // S4. At least one control state (other than the initial state) muat be declared var atLeastOneState = { name: 'atLeastOneState', shouldThrow: false, predicate: function predicate(fsmDef, settings, _ref2) { var statesType = _ref2.statesType; return { isFulfilled: Object.keys(statesType).length > 0, blame: { message: "Machine configuration must define at least one control state! Cf. log", info: { statesType: statesType } } }; } }; // S5. check initial control state is a defined state in states var isInitialControlStateDeclared = { name: 'isInitialControlStateDeclared', shouldThrow: false, predicate: function predicate(fsmDef, settings, _ref3) { var initTransition = _ref3.initTransition, statesType = _ref3.statesType; var initialControlState = fsmDef.initialControlState, transitions = fsmDef.transitions; var stateList = Object.keys(statesType); if (initialControlState) { return { isFulfilled: stateList.indexOf(initialControlState) > -1, blame: { message: "Configured initial control state must be a declared state. Cf. log", info: { initialControlState: initialControlState, declaredStates: stateList } } }; } else { return { isFulfilled: true, blame: void 0 }; } } }; // E0. `fsmDef.events` msut be an array of strings var eventsAreStrings = { name: 'eventsAreStrings', shouldThrow: false, predicate: function predicate(fsmDef, settings) { return { isFulfilled: fsmDef.events.every(function (x) { return typeof x === 'string'; }), blame: { message: "Events must be an array of strings!", info: { events: fsmDef.events } } }; } }; var validInitialConfig = { name: 'validInitialConfig', shouldThrow: false, predicate: function predicate(fsmDef, settings, _ref4) { var initTransition = _ref4.initTransition; var initialControlState = fsmDef.initialControlState; if (initTransition && initialControlState) { return { isFulfilled: false, blame: { message: "Invalid machine configuration : defining an initial control state and an initial transition at the same time may lead to ambiguity and is forbidden!", info: { initialControlState: initialControlState, initTransition: initTransition } } }; } else if (!initTransition && !initialControlState) { return { isFulfilled: false, blame: { message: "Invalid machine configuration : you must define EITHER an initial control state OR an initial transition! Else in which state is the machine supposed to start?", info: { initialControlState: initialControlState, initTransition: initTransition } } }; } else return { isFulfilled: true, blame: void 0 }; } }; // T1. There must be configured at least one transition away from the initial state // T2. A transition away from the initial state can only be triggered by the initial event // T7b. The initial state must have a valid transition INIT_STATE -INIT-> defined which does not have a history // state as target // T23. We allow conditional initial transitions, but what about the action ? should it be always identity? We // can't run any actions. We can update internal state, but we can't trace it, so we loose tracing properties and // debugging!. So enforce ACTIONS to be identity var validInitialTransition = { name: 'validInitialTransition', shouldThrow: false, predicate: function predicate(fsmDef, settings, _ref5) { var initTransition = _ref5.initTransition; var initialControlState = fsmDef.initialControlState, transitions = fsmDef.transitions; var initTransitions = transitions.reduce(function (acc, transition) { transition.from === INIT_STATE && acc.push(transition); return acc; }, []); // DOC : or not, we allow conditional init transitions!! allow to set the initial state depending on settings! // NOTE: functional object reference, and decoration (trace, entry actions )do not work well together, so we don't // enforce the part of the contract which require to have no actions for initial transitions... var isFulfilled = initialControlState && !initTransition || !initialControlState && initTransition && initTransitions.length === 1 && initTransition.event === INIT_EVENT && (isInconditionalTransition(initTransition) // && initTransition.action === ACTION_IDENTITY || areCconditionalTransitions(initTransition) // && initTransition.guards.every(guard => guard.action === ACTION_IDENTITY) ); return { isFulfilled: isFulfilled, blame: { message: "Invalid configuration for initial transition! Cf. log", info: { initTransition: initTransition, initTransitions: initTransitions, initialControlState: initialControlState } } }; } }; // T15. Init transitions can only occur from compound states or the initial state, i.e. A -INIT-> B iff A is a compound // state or A is the initial state var initEventOnlyInCompoundStates = { name: 'initEventOnlyInCompoundStates', shouldThrow: false, predicate: function predicate(fsmDef, settings, _ref6) { var statesTransitionsMap = _ref6.statesTransitionsMap, statesType = _ref6.statesType, statesPath = _ref6.statesPath; // The compound states below does not include the initial state by construction var atomicStates = Object.keys(statesType).filter(function (controlState) { return !statesType[controlState]; }); var atomicInitTransitions = atomicStates.map(function (atomicState) { return _defineProperty({}, atomicState, statesTransitionsMap[atomicState] && statesTransitionsMap[atomicState][INIT_EVENT]); }).filter(function (obj) { return Object.values(obj)[0]; }); var hasInitEventOnlyInCompoundStates = atomicInitTransitions.length === 0; return { isFulfilled: hasInitEventOnlyInCompoundStates, blame: { message: "Found at least one atomic state with an entry transition! That is forbidden! Cf. log", info: { initTransitions: atomicInitTransitions } } }; } }; // T5. Every compound state NOT the initial state A must have a valid transition A -INIT-> defined // T7a. Every compound state NOT the initial state must have a valid INCONDITIONAL transition A -INIT-> defined which // does not have a history state as target // NOTE: actually we could limit it to history state of the containing compound state to avoid infinity loop // T8. Every compound state NOT the initial state must have a valid INCONDITIONAL transition A -INIT-> defined which // does not have the history state as target and has a target control state that is one of its substates (no // out-of-hierarchy INIT transitions) var validInitialTransitionForCompoundState = { name: 'validInitialTransitionForCompoundState', shouldThrow: false, predicate: function predicate(fsmDef, settings, _ref8) { var statesTransitionsMap = _ref8.statesTransitionsMap, statesType = _ref8.statesType, statesPath = _ref8.statesPath; // The compound states below does not include the initial state by construction var compoundStates = Object.keys(statesType).filter(function (controlState) { return statesType[controlState]; }); var compoundStatesInitTransitions = compoundStates.map(function (compoundState) { return statesTransitionsMap[compoundState] && statesTransitionsMap[compoundState][INIT_EVENT]; }); var allHaveInitTransitions = compoundStatesInitTransitions.every(Boolean); if (!allHaveInitTransitions) { return { isFulfilled: false, blame: { message: "Found at least one compound state without an entry transition! Cf. log", info: { hasEntryTransitions: compoundStates.map(function (state) { return _defineProperty({}, state, !!(statesTransitionsMap[state] && statesTransitionsMap[state][INIT_EVENT])); }) } } }; } var allHaveValidInitTransitions = allHaveInitTransitions && compoundStatesInitTransitions.every(function (initTransition) { var guards = initTransition.guards, to = initTransition.to; if (!guards) { // T7a return typeof to === 'string'; } else { var targetStates = guards.map(function (guard) { return guard.to; }); return targetStates.every(function (targetState) { return typeof targetState === 'string'; }); } }); if (!allHaveValidInitTransitions) { return { isFulfilled: false, blame: { message: "Found at least one compound state with an invalid entry transition! Entry transitions for compound states must have the associated target control states which are not a history pseudo-state. Cf. log", info: { entryTransitions: compoundStatesInitTransitions } } }; } var allHaveTargetStatesWithinHierarchy = allHaveValidInitTransitions && compoundStatesInitTransitions.every(function (initTransition) { var from = initTransition.from, guards = initTransition.guards, to = initTransition.to; // Don't forget to also eliminate the case when from = to // Also note that wwe check that `to` is in statesPath as one is derived from states in transitions, and the // other from declared states if (!guards) { return from !== to && statesPath[to] && statesPath[to].startsWith(statesPath[from]); } else { var targetStates = guards.map(function (guard) { return guard.to; }); return targetStates.every(function (to) { return from !== to && statesPath[to] && statesPath[to].startsWith(statesPath[from]); }); } }); if (!allHaveTargetStatesWithinHierarchy) { return { isFulfilled: false, blame: { message: "Found at least one compound state with an invalid entry transition! Entry transitions for compound states must have a target state which is strictly below the compound state in the state hierarchy! ", info: { states: fsmDef.states, statesPath: statesPath, entryTransitions: compoundStatesInitTransitions } } }; } return { isFulfilled: true, blame: void 0 }; } }; // T11. If there is an eventless transition A -eventless-> B, there cannot be a competing A -ev-> X // T24. Check that we have this implicitly : Compound states must not have eventless transitions // defined on them (would introduce ambiguity with the INIT transition). var validEventLessTransitions = { name: 'validEventLessTransitions', shouldThrow: false, predicate: function predicate(fsmDef, settings, _ref10) { var statesTransitionsMap = _ref10.statesTransitionsMap, statesType = _ref10.statesType, statesPath = _ref10.statesPath; // The compound states below does not include the initial state by construction var stateList = Object.keys(statesType); var failingOriginControlStates = stateList.map(function (state) { return _defineProperty({}, state, statesTransitionsMap[state] && "".concat(void 0) in statesTransitionsMap[state] && Object.keys(statesTransitionsMap[state]).length !== 1); }).filter(function (obj) { return Object.values(obj)[0] !== void 0 && Object.values(obj)[0]; }); var isFulfilled = failingOriginControlStates.length === 0; return { isFulfilled: isFulfilled, blame: { message: "Found at least one control state without both an eventless transition and a competing transition! Cf. log", info: { failingOriginControlStates: failingOriginControlStates } } }; } }; // T12. All transitions A -ev-> * must have the same transition index, i.e. all associated guards must be together // in a single array and there cannot be two transition rows showcasing A -ev-> * transitions var allStateTransitionsOnOneSingleRow = { name: 'allStateTransitionsOnOneSingleRow', shouldThrow: false, predicate: function predicate(fsmDef, settings, _ref12) { var stateEventTransitionsMaps = _ref12.stateEventTransitionsMaps; var originStateList = Object.keys(stateEventTransitionsMaps); var statesTransitionsInfo = originStateList.reduce(function (acc, state) { var events = Object.keys(stateEventTransitionsMaps[state]); var wrongEventConfig = events.filter(function (event) { return stateEventTransitionsMaps[state][event].length > 1; }); if (wrongEventConfig.length > 0) { acc[state] = wrongEventConfig; } return acc; }, {});