101 lines
3.5 KiB
JavaScript
101 lines
3.5 KiB
JavaScript
/**
|
|
* @module finite_state_machine
|
|
*/
|
|
|
|
/**
|
|
* The states to be used with state machine definitions
|
|
* @typedef {Object} FiniteStateMachineStates
|
|
* @property {!Object} ANY_KEY - Any key that maps to a known state
|
|
* @property {!Object} ANY_KEY.on - A dictionary of transition events for the ANY_KEY state that map to a different state
|
|
* @property {!String} ANY_KEY.on.ANY_EVENT - The resulting state that the machine should end at
|
|
*/
|
|
|
|
/**
|
|
* An object whose minimum definition defined here can be used to guard UI state transitions
|
|
* @typedef {Object} StatelessFiniteStateMachineDefinition
|
|
* @property {FiniteStateMachineStates} states
|
|
*/
|
|
|
|
/**
|
|
* An object whose minimum definition defined here can be used to create a live finite state machine
|
|
* @typedef {Object} LiveFiniteStateMachineDefinition
|
|
* @property {String} initial - The initial state for this machine
|
|
* @property {FiniteStateMachineStates} states
|
|
*/
|
|
|
|
/**
|
|
* An object that allows interacting with a stateful, live finite state machine
|
|
* @typedef {Object} LiveStateMachine
|
|
* @property {String} value - The current state of this machine
|
|
* @property {Object} states - The states from when the machine definition was constructed
|
|
* @property {Function} is - {@link module:finite_state_machine~is LiveStateMachine.is}
|
|
* @property {Function} send - {@link module:finite_state_machine~send LiveStatemachine.send}
|
|
*/
|
|
|
|
// This is not user-facing functionality
|
|
/* eslint-disable @gitlab/require-i18n-strings */
|
|
|
|
function hasKeys(object, keys) {
|
|
return keys.every((key) => Object.keys(object).includes(key));
|
|
}
|
|
|
|
/**
|
|
* Get an updated state given a machine definition, a starting state, and a transition event
|
|
* @param {StatelessFiniteStateMachineDefinition} definition
|
|
* @param {String} current - The current known state
|
|
* @param {String} event - A transition event
|
|
* @returns {String} A state value
|
|
*/
|
|
export function transition(definition, current, event) {
|
|
return definition?.states?.[current]?.on[event] || current;
|
|
}
|
|
|
|
function startMachine({ states, initial } = {}) {
|
|
let current = initial;
|
|
|
|
return {
|
|
/**
|
|
* A convenience function to test arbitrary input against the machine's current state
|
|
* @param {String} testState - The value to test against the machine's current state
|
|
*/
|
|
is(testState) {
|
|
return current === testState;
|
|
},
|
|
/**
|
|
* A function to transition the live state machine using an arbitrary event
|
|
* @param {String} event - The event to send to the machine
|
|
* @returns {String} A string representing the current state. Note this may not have changed if the current state + transition event combination are not valid.
|
|
*/
|
|
send(event) {
|
|
current = transition({ states }, current, event);
|
|
|
|
return current;
|
|
},
|
|
get value() {
|
|
return current;
|
|
},
|
|
set value(forcedState) {
|
|
current = forcedState;
|
|
},
|
|
states,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Create a live state machine
|
|
* @param {LiveFiniteStateMachineDefinition} definition
|
|
* @returns {LiveStateMachine} A live state machine
|
|
*/
|
|
export function machine(definition) {
|
|
if (!hasKeys(definition, ['initial', 'states'])) {
|
|
throw new Error(
|
|
'A state machine must have an initial state (`.initial`) and a dictionary of possible states (`.states`)',
|
|
);
|
|
} else if (!hasKeys(definition.states, [definition.initial])) {
|
|
throw new Error(
|
|
`Cannot initialize the state machine to state '${definition.initial}'. Is that one of the machine's defined states?`,
|
|
);
|
|
} else {
|
|
return startMachine(definition);
|
|
}
|
|
}
|