kingly
Version:
State machine library (Extended Hierarchical State Transducer)
1,408 lines (1,266 loc) • 120 kB
JavaScript
(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;
}, {});