gitlab-org--gitlab-foss/app/assets/javascripts/lib/utils/finite_state_machine.js

102 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);
}
}