This page is part of a static HTML representation of TriTarget.org at https://tritarget.org

Simple State Machine

Sukima5th December 2019 at 9:00pm

Super simple Finite State Machine. This attempts to follow the conventions of XState with the exceptions of multiple actions, interpreter, and the added concept of meta content.

You can optionally use the Simple Interpreter to manage these FSMs.

See also, CSS based view states.

function createMachine(
  { context = {}, id, initial, states = {}, on = {} } = {},
  { actions = {}, guards = {} } = {}
) {
  function stateFor(stateEvent, stateContext = context, changed) {
    let { target } = stateEvent;
    if (!target) { return; }
    let { entry, meta = {} } = states[target] || {};
    let newContext = { ...stateContext };
    let nextState = { value: target, meta, context: newContext, changed };
    nextState.actions = changed
      ? [].concat(entry || []).map(boundAction('entry', stateEvent, nextState))
      : [];
    return nextState;
  }
  function findStateEvent(transitionData, eventData, stateContext = context) {
    for (let transition of [].concat(transitionData)) {
      if (!transition) { continue; }
      let { target = transition, cond = () => true } = transition;
      if (typeof target !== 'string') { target = ''; }
      let stateEvent = { ...eventData, target };
      if (typeof cond === 'function') {
        if (cond(stateContext, stateEvent)) { return stateEvent; }
        continue;
      }
      if (guards[cond](stateContext, stateEvent)) { return stateEvent; }
    }
    return {};
  }
  function boundAction(type, event, state) {
    return function(action) {
      if (!action) { return; }
      if (!action.exec) { action = { exec: action }; }
      if (typeof action.exec !== 'function') { action.exec = actions[action.exec]; }
      let exec = action.exec && ((...args) => action.exec(state.context, event, { action, state }, ...args));
      return { type: (action.type || type), exec };
    };
  }
  return {
    id, initialState: stateFor({ target: initial, type: '#init' }, context, true),
    transition(state, transitionEvent) {
      let currentValue = (state && state.value) || state;
      let stateConfig = states[currentValue];
      if (!stateConfig) {
        throw new Error(`State '${currentValue}' not found on machine${id ? ` ${id}` : ''}`);
      }
      let { type = transitionEvent, value } = transitionEvent;
      let transitions = { ...on, ...(stateConfig.on || {}) };
      let transition = transitions[type] || {};
      let stateEvent = findStateEvent(transition, { type, value }, state.context);
      let nextState = stateEvent.target && stateEvent.target !== ''
        ? stateFor(stateEvent, state.context, true)
        : stateFor({ target: currentValue }, state.context, false);
      nextState.actions = [
        ...(nextState.changed
          ? [].concat(stateConfig.exit || []).map(boundAction('exit', stateEvent, nextState))
          : []),
        ...[].concat(transition.actions || []).map(boundAction('action', stateEvent, nextState)),
        ...nextState.actions,
      ];
      nextState.actions = nextState.actions.filter(action => !!action);
      return nextState;
    },
  };
}

Tests are available for this snippet.