diff options
Diffstat (limited to 'app/assets/javascripts/lib/utils/finite_state_machine.js')
-rw-r--r-- | app/assets/javascripts/lib/utils/finite_state_machine.js | 101 |
1 files changed, 101 insertions, 0 deletions
diff --git a/app/assets/javascripts/lib/utils/finite_state_machine.js b/app/assets/javascripts/lib/utils/finite_state_machine.js new file mode 100644 index 00000000000..99eeb7cb947 --- /dev/null +++ b/app/assets/javascripts/lib/utils/finite_state_machine.js @@ -0,0 +1,101 @@ +/** + * @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); + } +} |