state-transducer
Version:
Extended Hierarchical State Transducer library
740 lines (647 loc) • 32.5 kB
JavaScript
import {
ACTION_FACTORY_DESC, ACTION_IDENTITY, AUTO_EVENT, DEEP, ENTRY_ACTION_FACTORY_DESC, history_symbol, INIT_EVENT,
INIT_STATE, NO_OUTPUT, PREDICATE_DESC, SHALLOW, STATE_PROTOTYPE_NAME, UPDATE_STATE_FN_DESC
} from "./properties";
import {
arrayizeOutput, assert, computeHistoryMaps, emptyConsole, findInitTransition, get_fn_name, getFsmStateList,
getFunctionName, handleFnExecError, initHistoryDataStructure, isActions, isBoolean, isEventStruct,
isHistoryControlState, keys, mapOverTransitionsActions, noop, notifyAndRethrow, throwIfInvalidActionResult,
throwIfInvalidEntryActionResult, throwIfInvalidGuardResult, tryCatchMachineFn, updateHistory, wrap
} from "./helpers";
import { fsmContractChecker } from "./contracts"
const alwaysTrue = () => true;
/**
* Takes a list of identifiers (strings), adds init to it, and returns a hash whose properties are
* the uppercased identifiers For instance :
* ('edit', 'delete') -> {EDIT: 'EDIT', DELETE : 'DELETE', INIT : 'INIT'}
* If there is an init in the list of identifiers, it is overwritten
* RESTRICTION : avoid having init as an identifier
* @param array_identifiers {Array | arguments}
* @returns {Object<String,String>}
*/
function build_event_enum(array_identifiers) {
array_identifiers = array_identifiers.reduce
? array_identifiers
: Array.prototype.slice.call(arguments);
// NOTE : That will overwrite any other event called init...
array_identifiers.push(INIT_EVENT);
return array_identifiers.reduce(function (acc, identifier) {
acc[identifier] = identifier;
return acc;
}, {});
}
/**
* Processes the hierarchically nested states and returns miscellaneous objects derived from it:
* `is_group_state` : {Object<String,Boolean>} Hash whose properties (state names) are matched with
* whether that state is a nested state
* `hash_states` : Hierarchically nested object whose properties are the nested states.
* - Nested states inherit (prototypal inheritance) from the containing state.
* - Holds a `history` property which holds a `last_seen_state` property which holds the latest
* state for that hierarchy group For instance, if A < B < C and the state machine leaves C for a
* state in another branch, then `last_seen_state` will be set to C for A, B and C
* - Tthe root state (NOK) is added to the whole hierarchy, i.e. all states inherit from the root
* state
* `states` {Object<String,Boolean>} : Hash which maps every state name with itself
* `states.history` {Object<String,Function>} : Hash which maps every state name with a function
* whose name is the state name
* @param states
* @returns {{hash_states: {}, is_group_state: {}}}
*/
function build_nested_state_structure(states) {
const root_name = "State";
let hash_states = {};
let is_group_state = {};
// Add the starting state
states = { nok: states };
////////
// Helper functions
function build_state_reducer(states, curr_constructor) {
keys(states).forEach(function (state_name) {
const state_config = states[state_name];
// The hierarchical state mechanism is implemented by reusing the standard Javascript
// prototypal inheritance If A < B < C, then C has a B as prototype which has an A as
// prototype So when an event handler (transition) is put on A, that event handler will be
// visible in B and C
hash_states[state_name] = new curr_constructor();
hash_states[state_name].name = state_name;
const parent_name = (hash_states[state_name].parent_name = get_fn_name(
curr_constructor
));
hash_states[state_name].root_name = root_name;
if (typeof state_config === "object") {
is_group_state[state_name] = true;
const curr_constructor_new = function () {};
curr_constructor_new.displayName = state_name;
curr_constructor_new.prototype = hash_states[state_name];
build_state_reducer(state_config, curr_constructor_new);
}
});
}
function State() {
}
State.prototype = {
current_state_name: INIT_STATE
};
hash_states[INIT_STATE] = new State();
hash_states[STATE_PROTOTYPE_NAME] = new State();
build_state_reducer(states, State);
return {
hash_states: hash_states,
is_group_state: is_group_state
};
}
/**
* Returns a hash which maps a state name to :
* - a string identifier which represents the standard state
* @param states A hash describing a hierarchy of nested states
* @returns {state_name: {String}}
*/
export function build_state_enum(states) {
let states_enum = { history: {} };
// Set initial state
states_enum.NOK = INIT_STATE;
function build_state_reducer(states) {
keys(states).forEach(function (state_name) {
const state_config = states[state_name];
states_enum[state_name] = state_name;
if (typeof state_config === "object") {
build_state_reducer(state_config);
}
});
}
build_state_reducer(states);
return states_enum;
}
export function normalizeTransitions(fsmDef) {
const { initialControlState, transitions } = fsmDef;
const initTransition = findInitTransition(transitions);
if (initialControlState) {
return transitions
.concat([{ from: INIT_STATE, event: INIT_EVENT, to: initialControlState, action: ACTION_IDENTITY }])
}
else if (initTransition) {
return transitions
}
}
export function normalizeFsmDef(fsmDef) {
return Object.assign({}, fsmDef, { transitions: normalizeTransitions(fsmDef) })
}
// Alias for compatibility before deprecating entirely create_state_machine
export function create_state_machine(fsmDef, settings) {
return createStateMachine(fsmDef, settings)
}
/**
* Creates an instance of state machine from a set of states, transitions, and accepted events. The initial
* extended state for the machine is included in the machine definition.
* @param {FSM_Def} fsmDef
* @param {FSM_Settings} settings
* @return {function(*=)}
*/
export function createStateMachine(fsmDef, settings) {
const {
states: control_states,
events,
// transitions ,
initialExtendedState,
updateState: userProvidedUpdateStateFn,
} = fsmDef;
const { debug } = settings || {};
const checkContracts = debug && debug.checkContracts || void 0;
let console = debug && debug.console ? debug.console : emptyConsole;
if (checkContracts) {
const { failingContracts } = fsmContractChecker(fsmDef, settings, checkContracts);
if (failingContracts.length > 0) throw new Error(`createStateMachine: called with wrong parameters! Cf. logs for failing contracts.`)
}
const wrappedUpdateState = tryCatchMachineFn(UPDATE_STATE_FN_DESC, userProvidedUpdateStateFn, ['extendedState,' +
' updates']);
const _events = build_event_enum(events);
const transitions = normalizeTransitions(fsmDef);
// Create the nested hierarchy
const hash_states_struct = build_nested_state_structure(control_states);
// This will be the extended state object which will be updated by all actions and on which conditions
// will be evaluated It is safely contained in a closure so it cannot be accessed in any way
// outside the state machine.
// Note the extended state is modified by the `settings.updateState` function, which should not modify
// the extended state object. There is hence no need to do any cloning.
let extendedState = initialExtendedState;
// history maps
const { stateList, stateAncestors } = computeHistoryMaps(control_states);
let history = initHistoryDataStructure(stateList);
// @type {Object<state_name,boolean>}, allows to know whether a state has a init transition defined
let is_init_state = {};
// @type {Object<state_name,boolean>}, allows to know whether a state has an automatic transition defined
let is_auto_state = {};
// @type {Object<state_name,boolean>}, allows to know whether a state is a group of state or not
const is_group_state = hash_states_struct.is_group_state;
let hash_states = hash_states_struct.hash_states;
transitions.forEach(function (transition) {
let { from, to, action, event, guards: arr_predicate } = transition;
// CASE : ZERO OR ONE condition set
if (!arr_predicate)
arr_predicate = [{ predicate: alwaysTrue, to: to, action: action }];
// CASE : transition has a init event
// NOTE : there should ever only be one, but we don't enforce it here
if (event === INIT_EVENT) {
is_init_state[from] = true;
}
let from_proto = hash_states[from];
// ERROR CASE : state found in transition but cannot be found in the events passed as parameter
// NOTE : this is probably all what we need the events variable for
if (event && !(event in _events))
throw `unknown event ${event} found in state machine definition!`;
// CASE : automatic transitions : no events - likely a transient state with only conditions
if (!event) {
event = AUTO_EVENT;
is_auto_state[from] = true;
}
// CASE : automatic transitions : init event automatically fired upon entering a grouping state
if (is_group_state[from] && is_init_state[from]) {
is_auto_state[from] = true;
}
// TODO : this seriously needs refactoring, that is one line in ramda
from_proto[event] = arr_predicate.reduce((acc, guard, index) => {
const action = guard.action || ACTION_IDENTITY;
const actionName = action.name || action.displayName;
const condition_checking_fn = (function (guard, settings) {
let condition_suffix = "";
// We add the `current_state` because the current control state might be different from
// the `from` field here This is the case for instance when we are in a substate, but
// through prototypal inheritance it is the handler of the prototype which is called
const condition_checking_fn = function (extendedState_, event_data, current_state) {
from = current_state || from;
const isGuardedTransition = guard.predicate;
const predicate = isGuardedTransition || alwaysTrue;
const shouldTransitionBeTaken =
!isGuardedTransition
|| tryCatchMachineFn(
PREDICATE_DESC, predicate, ['extendedState', 'eventData', 'settings']
)(extendedState_, event_data, settings);
const to = guard.to;
condition_suffix = predicate ? "_checking_condition_" + index : "";
handleFnExecError(
{ debug, console },
{ predicate: guard.predicate, extendedState: extendedState_, eventData: event_data, settings },
shouldTransitionBeTaken,
isBoolean,
notifyAndRethrow,
throwIfInvalidGuardResult
);
if (shouldTransitionBeTaken) {
// CASE : guard for transition is fulfilled so we can execute the actions...
console.info("IN STATE ", from);
isGuardedTransition && console.info(`CASE: guard ${predicate.name} for transition is fulfilled`);
!isGuardedTransition && console.info(`CASE: unguarded transition`);
console.info("THEN : we execute the action " + actionName);
const wrappedAction = tryCatchMachineFn(ACTION_FACTORY_DESC, action, ['extendedState', 'eventData', 'settings']);
const actionResultOrError = wrappedAction(extendedState_, event_data, settings);
handleFnExecError(
{ debug, console },
{ action, extendedState: extendedState_, eventData: event_data, settings },
actionResultOrError,
isActions,
notifyAndRethrow,
throwIfInvalidActionResult
);
const { updates, outputs } = actionResultOrError;
// Leave the current state
leave_state(from, extendedState_, hash_states);
// Update the extendedState before entering the next state
const extendedStateOrError = wrappedUpdateState(extendedState_, updates);
handleFnExecError(
{ debug, console },
{ updateStateFn: userProvidedUpdateStateFn, extendedState: extendedState_, updates },
extendedStateOrError,
alwaysTrue, // NOTE : could also be passed in settings with updateState, maybe in later version
notifyAndRethrow,
noop // NOTE : we do not test the return value of the update state function, cf. previous note
);
extendedState = extendedStateOrError;
// ...and enter the next state (can be different from `to` if we have nesting state group)
const next_state = enter_next_state(to, updates, hash_states);
console.info("ENTERING NEXT STATE : ", next_state);
// allows for chaining and stop chaining guard
return { stop: true, outputs };
}
else {
// CASE : guard for transition is not fulfilled
return { stop: false, outputs: NO_OUTPUT };
}
};
condition_checking_fn.displayName = from + condition_suffix;
return condition_checking_fn;
})(guard, settings);
return function arr_predicate_reduce_fn(extendedState_, event_data, current_state) {
const condition_checked = acc(extendedState_, event_data, current_state);
return condition_checked.stop
? condition_checked
: condition_checking_fn(extendedState_, event_data, current_state);
};
},
function dummy() {
return { stop: false, outputs: NO_OUTPUT };
}
);
});
function assertContract(contract, arrayParams) {
if (!checkContracts) return void 0
else return assert(contract, arrayParams)
}
function send_event(event_struct, isExternalEvent) {
console.debug("send event", event_struct);
assertContract(isEventStruct, [event_struct]);
const event_name = keys(event_struct)[0];
const event_data = event_struct[event_name];
const current_state = hash_states[INIT_STATE].current_state_name;
// Edge case : INIT_EVENT sent and the current state is not the initial state
// We have to do this separately, as by construction the INIT_STATE is a
// super state of all states in the machine. Hence sending an INIT_EVENT
// would always execute the INIT transition by prototypal delegation
if (isExternalEvent && event_name === INIT_EVENT && current_state !== INIT_STATE) {
console.warn(`The external event INIT_EVENT can only be sent when starting the machine!`)
return NO_OUTPUT
}
return process_event(
hash_states_struct.hash_states,
event_name,
event_data,
extendedState
);
}
function process_event(hash_states, event, event_data, extendedState) {
const current_state = hash_states[INIT_STATE].current_state_name;
const event_handler = hash_states[current_state][event];
if (event_handler) {
// CASE : There is a transition associated to that event
console.log("found event handler!");
console.info("WHEN EVENT ", event);
/* OUT : this event handler modifies the extendedState and possibly other data structures */
const { stop, outputs: rawOutputs } = event_handler(extendedState, event_data, current_state);
debug && !stop && console.warn("No guards have been fulfilled! We recommend to configure guards explicitly to" +
" cover the full state space!")
const outputs = arrayizeOutput(rawOutputs);
// we read it anew as the execution of the event handler may have changed it
const new_current_state = hash_states[INIT_STATE].current_state_name;
// Two cases here:
// 1. Init handlers, when present on the current state, must be acted on immediately
// This allows for sequence of init events in various state levels
// For instance, L1: init -> L2:init -> L3:init -> L4: stateX
// In this case event_data will carry on the data passed on from the last event (else we loose
// the extendedState?)
// 2. transitions with no events associated, only conditions (i.e. transient states)
// In this case, there is no need for event data
// NOTE : the guard is to defend against loops occuring when an AUTO transition fails to advance and stays
// in the same control state!! But by contract that should never happen : all AUTO transitions should advance!
if (is_auto_state[new_current_state] && new_current_state !== current_state) {
// CASE : transient state with no triggering event, just conditions
// automatic transitions = transitions without events
const auto_event = is_init_state[new_current_state]
? INIT_EVENT
: AUTO_EVENT;
return [].concat(outputs).concat(send_event({ [auto_event]: event_data }, false));
} else return outputs;
} else {
// CASE : There is no transition associated to that event from that state
console.warn(`There is no transition associated to the event |${event}| in state |${current_state}|!`);
return NO_OUTPUT;
}
}
function leave_state(from, extendedState, hash_states) {
// NOTE : extendedState is passed as a parameter for symetry reasons, no real use for it so far
const state_from = hash_states[from];
const state_from_name = state_from.name;
history = updateHistory(history, stateAncestors, state_from_name);
console.info("left state", wrap(from));
}
function enter_next_state(to, updatedExtendedState, hash_states) {
let state_to;
let state_to_name;
// CASE : history state (H)
if (isHistoryControlState(to)) {
const history_type = to.deep ? DEEP : to.shallow ? SHALLOW : void 0;
const history_target = to[history_type];
// Edge case : history state (H) && no history (i.e. first time state is entered), target state
// is the entered state
state_to_name = history[history_type][history_target] || history_target;
state_to = hash_states[state_to_name];
}
else if (to) {
// CASE : normal state
state_to = hash_states[to];
state_to_name = state_to.name;
} else {
throw "enter_state : unknown case! Not a state name, and not a history state to enter!";
}
hash_states[INIT_STATE].current_state_name = state_to_name;
debug && console.info("AND TRANSITION TO STATE", state_to_name);
return state_to_name;
}
function start() {
return send_event({ [INIT_EVENT]: initialExtendedState }, true);
}
start();
// NOTE : yield is a reserved JavaScript word so using yyield
return function yyield(x) { return send_event(x, true)}
}
/**
*
* @param {WebComponentName} name name for the web component. Must include at least one hyphen per custom
* components' specification
* @param {SubjectFactory} subjectFactory A factory function which returns a subject, i.e. an object which
* implements the `Observer` and `Observable` interface
* @param {FSM} fsm An executable machine, i.e. a function which accepts machine inputs
* @param {Object.<CommandName, CommandHandler>} commandHandlers
* @param {*} effectHandlers Typically anything necessary to perform effects. Usually this is a hashmap mapping an
* effect moniker to a function performing the corresponding effect.
* @param {{initialEvent, terminalEvent, NO_ACTION}} options
*/
export function makeWebComponentFromFsm({ name, eventHandler, fsm, commandHandlers, effectHandlers, options }) {
class FsmComponent extends HTMLElement {
constructor() {
if (name.split('-').length <= 1) throw `makeWebComponentFromFsm : web component's name MUST include a dash! Please review the name property passed as parameter to the function!`
super();
const el = this;
const { subjectFactory } = eventHandler;
this.eventSubject = subjectFactory();
this.options = Object.assign({}, options);
const NO_ACTION = this.options.NO_ACTION || NO_OUTPUT;
// Set up execution of commands
this.eventSubject.subscribe({
next: eventStruct => {
const actions = fsm(eventStruct);
if (actions === NO_ACTION) return;
actions.forEach(action => {
if (action === NO_ACTION) return;
const { command, params } = action;
commandHandlers[command](this.eventSubject.next, params, effectHandlers, el);
});
}
});
}
static get observedAttributes() {
return [];
}
connectedCallback() {
this.options.initialEvent && this.eventSubject.next(this.options.initialEvent);
}
disconnectedCallback() {
this.options.terminalEvent && this.eventSubject.next(this.options.terminalEvent);
this.eventSubject.complete();
}
attributeChangedCallback(name, oldValue, newValue) {
// simulate a new creation every time an attribute is changed
// i.e. they are not expected to change
this.constructor();
this.connectedCallback();
}
}
return customElements.define(name, FsmComponent);
}
/**
* Adds a `displayName` property corresponding to the action name to all given action factories. The idea is to use
* the action name in some specific useful contexts (debugging, tracing, visualizing)
* @param {Object.<string, function>} namedActionSpecs Maps an action name to an action factory
*/
export function makeNamedActionsFactory(namedActionSpecs) {
return Object.keys(namedActionSpecs).reduce((acc, actionName) => {
const actionFactory = namedActionSpecs[actionName];
actionFactory.displayName = actionName;
acc[actionName] = actionFactory;
return acc;
}, {});
}
/**
* This function works to merge outputs by simple concatenation and flattening
* Every action return T or [T], and we want in output [T] always
* mergeOutputsFn([a, [b]) = mergeOutputsFn([a,b]) = mergeOutputsFn([[a],b) = mergeOutputsFn([[a],[b]]) = [a,b]
* If we wanted to pass [a] as value we would have to do mergeOutputsFn([[[a]],[b]]) to get [[a],b]
* @param arrayOutputs
* @returns {*}
*/
export function mergeOutputsFn(arrayOutputs) {
// NOTE : here, this array of outputs could be array x non-array ^n
// The algorithm is to concat all elements
return arrayOutputs.reduce((acc, element) => acc.concat(element), [])
}
/**
* @param {FSM_Def} fsmDef
* @param {Object.<ControlState, function>} entryActions Adds an action to be processed when entering a given state
* @param {function (Array<MachineOutput>) : MachineOutput} mergeOutputs monoidal merge (pure) function
* to be provided to instruct how to combine machine outputs. Beware that the second output corresponds to the entry
* action output which must logically correspond to a processing as if it were posterior to the first output. In
* many cases, that will mean that the second machine output has to be 'last', whatever that means for the monoid
* and application in question
*/
export function decorateWithEntryActions(fsmDef, entryActions, mergeOutputs) {
if (!entryActions) return fsmDef
const { transitions, states, initialExtendedState, initialControlState, events, updateState, settings } = fsmDef;
const stateHashMap = getFsmStateList(states);
const isValidEntryActions = Object.keys(entryActions).every(controlState => {
return stateHashMap[controlState] != null;
});
const mergeOutputFn = mergeOutputs || mergeOutputsFn
if (!isValidEntryActions) {
throw `decorateWithEntryActions : found control states for which entry actions are defined, and yet do not exist in the state machine!`;
} else {
const decoratedTransitions = mapOverTransitionsActions((action, transition, guardIndex, transitionIndex) => {
const { to } = transition;
const entryAction = entryActions[to];
const decoratedAction = entryAction
? decorateWithExitAction(action, entryAction, mergeOutputFn, updateState)
: action;
return decoratedAction
}, transitions);
return {
initialExtendedState,
initialControlState,
states,
events,
transitions: decoratedTransitions,
updateState,
settings
}
}
}
/**
*
* @param {ActionFactory} action action factory which may be associated to a display name
* @param {ActionFactory} entryAction
* @param {function (Array<MachineOutput>) : MachineOutput} mergeOutputFn monoidal merge function. Cf.
* decorateWithEntryActions
* @param updateState
* @return {decoratedAction}
*/
function decorateWithExitAction(action, entryAction, mergeOutputFn, updateState) {
// NOTE : An entry action is modelized by an exit action, i.e. an action which will be processed last after any
// others which apply. Because in the transducer semantics there is nothing happening after the transition is
// processed, or to express it differently, transition and state entry are simultaneous, this modelization is
// accurate.
// DOC : entry actions for a control state will apply before any automatic event related to that state! In fact before
// anything. That means the automatic event should logically receive the state updated by the entry action
const decoratedAction = function (extendedState, eventData, settings) {
const { debug } = settings;
const wrappedEntryAction = tryCatchMachineFn(ENTRY_ACTION_FACTORY_DESC, entryAction, ['extendedState', 'eventData', 'settings']);
const actionResult = action(extendedState, eventData, settings);
const actionUpdate = actionResult.updates;
const wrappedUpdateState = tryCatchMachineFn(UPDATE_STATE_FN_DESC, updateState, ['extendedState, updates']);
const updatedExtendedStateOrError = wrappedUpdateState(extendedState, actionUpdate);
// TODO : test that case
handleFnExecError(
{ debug, console },
{ updateStateFn: updateState, extendedState, actionUpdate },
updatedExtendedStateOrError,
alwaysTrue, // NOTE : could also be passed in settings with updateState, maybe in later version
notifyAndRethrow,
noop // NOTE : we do not test the return value of the update state function, cf. previous note
);
const updatedExtendedState = updatedExtendedStateOrError;
const exitActionResultOrError = wrappedEntryAction(updatedExtendedState, eventData, settings);
const isExitActionResultError = handleFnExecError(
{ debug, console },
{ action: entryAction, extendedState: updatedExtendedState, eventData, settings },
exitActionResultOrError,
isActions,
notifyAndRethrow,
throwIfInvalidEntryActionResult
);
if (!isExitActionResultError) {
// NOTE : exitActionResult comes last as we want it to have priority over other actions.
// As a matter of fact, it is an exit action, so it must always happen on exiting, no matter what
//
// ADR : Beware of the fact that as a result it could overwrite previous actions. In principle exit actions
// should add to existing actions, not overwrite. Because exit actions are not represented on the machine
// visualization, having exit actions which overwrite other actions might make it hard to reason about the
// visualization. We choose however to not forbid the overwrite by contract. But beware. ROADMAP : the best is,
// according to semantics, to actually send both separately
return {
updates: [].concat(actionUpdate, exitActionResultOrError.updates),
outputs: mergeOutputFn([actionResult.outputs, exitActionResultOrError.outputs])
};
}
};
decoratedAction.displayName = `Entry_Action_After_${getFunctionName(action)}`;
return decoratedAction;
}
/**
* This function converts a state machine `A` into a traced state machine `T(A)`. The traced state machine, on
* receiving an input `I` outputs the following information :
* - `outputs` : the outputs `A.yield(I)`
* - `updates` : the update of the extended state of `A` to be performed as a consequence of receiving the
* input `I`
* - `extendedState` : the extended state of `A` prior to receiving the input `I`
* - `controlState` : the control state in which the machine is when receiving the input `I`
* - `event::{eventLabel, eventData}` : the event label and event data corresponding to `I`
* - `settings` : settings passed at construction time to `A`
* - `targetControlState` : the target control state the machine has transitioned to as a consequence of receiving
* the input `I`
* - `predicate` : the predicate (guard) corresponding to the transition that was taken to `targetControlState`, as
* a consequence of receiving the input `I`
* - `actionFactory` : the `actionFactory` which was executed as a consequence of receiving the input `I`
* Note that the trace functionality is obtained by wrapping over the action factories in `A`. As such, all action
* factories will see their output wrapped. However, transitions which do not lead to the execution of action
* factories are not traced.
* @param {*} env
* @param {FSM_Def} fsm
*/
export function traceFSM(env, fsm) {
const { initialExtendedState, initialControlState, events, states, transitions, updateState } = fsm;
return {
initialExtendedState,
initialControlState,
events,
states,
updateState,
transitions: mapOverTransitionsActions((action, transition, guardIndex, transitionIndex) => {
return function (extendedState, eventData, settings) {
const { from: controlState, event: eventLabel, to: targetControlState, predicate } = transition;
const wrappedAction = tryCatchMachineFn(ACTION_FACTORY_DESC, action, ['extendedState', 'eventData', 'settings']);
const actionResultOrError = wrappedAction(extendedState, eventData, settings);
const { outputs, updates } = actionResultOrError;
const wrappedUpdateState = tryCatchMachineFn(UPDATE_STATE_FN_DESC, updateState, ['extendedState, updates']);
return {
updates,
outputs: [{
outputs,
updates,
extendedState: extendedState,
// NOTE : I can do this because pure function!! This is the extended state after taking the transition
newExtendedState: wrappedUpdateState(extendedState, updates || []),
controlState,
event: { eventLabel, eventData },
settings: settings,
targetControlState,
predicate,
actionFactory: action,
guardIndex,
transitionIndex
}],
}
}
}, transitions)
}
}
/**
* Construct history states `hs` from a list of states for a given state machine. The history states for a given control
* state can then be referenced as follows :
* - `hs.shallow(state)` will be the shallow history state associated to the `state`
* - `hs.deep(state)` will be the deep history state associated to the `state`
* @param {FSM_States} states
* @return {HistoryStateFactory}
*/
export function makeHistoryStates(states) {
const stateList = Object.keys(getFsmStateList(states));
// used for referential equality comparison to discriminate history type
return (historyType, controlState) => {
if (!stateList.includes(controlState)) {
throw `makeHistoryStates: the state for which a history state must be constructed is not a configured state for the state machine under implementation!!`
}
return {
[historyType]: controlState,
type: history_symbol
}
}
}
export function historyState(historyType, controlState) {
return {
[historyType]: controlState
}
}