Source: pattern/automaton/Automaton.js

/**
 * @author    Wolfgang Kowarschick <kowa@hs-augsburg.de>
 * @copyright 2017 - 2018
 * @license   CC-BY-NC-SA-4.0
 */

import EventDispatcher from "../observer/EventDispatcher.js";

const C_EMPTY = {};

/**
 * Implements a class the objects of which are finite automatons
 * (state machines).
 *
 * @class
 */
class Automaton extends EventDispatcher
{ /**
   * A finite automaton is in exactly one of a finite number of states
   * at any given time. Initially it is in the initial state.
   * <p>
   * Whenever an action is performed, it changes its state due to
   * the transition table given. The new state depends on the
   * current state and the action performed.
   * <p>
   * On exiting a state the automaton dispatches the exit event of that
   * state (if that event exists). Then it dispatches the do event of the
   * current transition (if that event exits). On entering a state,
   * it dispatches the entry event and then the do event of that state
   * (if those events exist). If a transition does not lead to a new state,
   * i.e., if the automaton remains in the current state, neither the exit
   * event nor the entry event but only the do event of the transition
   * an the do event of that state are dispatched.
   * <p>
   * In the transition table there can be noted a default action
   * for every state. Whenever the automaton is in a specific state
   * and there is no entry found for the current action, the default
   * action is performed instead. If no default action is found, too,
   * the automaton remains in the current state and an error event
   * is dispatched (if it exists).
   *
   * @param {{initialState:  string,
   *          defaultAction: ?string,
   *          errorEvent:    ?string,
   *          states:        ?Object.<string,
   *                                  {entry: ?(string|function),
   *                                   do:    ?(string|function),
   *                                   exit:  ?(string|function)
   *                                  }
   *                                >,
   *          transitions:   Object.<string,
   *                                 Object.<string,
   *                                         string
   *                                         |
   *                                         {state: string,
   *                                          do:    ?(string|function)
   *                                         }
   *                                        >
   *                                >
   *         }
   *        } automaton
   *
   * @param {string} automaton.initialState
   *        The initial state of the automaton.
   *
   * @param {?string} automaton.defaultAction
   *        An action that is performed, if the action to be performed
   *        does not exists.
   *
   * @param {?string} automaton.errorEvent
   *        This event is dispatched, if no transition is found (neither
   *        for the action passed to the automaton nor for the default action).
   *
   * @param {Object.<string,
   *                 {entry: ?(string|function),
   *                  do:    ?(string|function),
   *                  exit:  ?(string|function)
   *                 }
   *                >
   *        } [automaton.states = {}]
   *        An object that defines the entry, do, and exit actions (functions)
   *        or events (strings) of states of the automaton. Those actions are
   *        optional. If a state has no dedicated actions, it may or may not
   *        be stated here.
   *
   * @param {Object.<string,
   *                 Object.<string,
   *                         string
   *                         |
   *                         {state: string,
   *                          do:    ?(string|function)
   *                         }
   *                        >
   *                >
   *        } [automaton.transitions = {}]
   *        The transitions of the automaton. For each possible state of the
   *        automaton an object containing all possible actions that can be
   *        performed in that state is to be declared. For each action
   *        the new state has to be declared. Additionally an do action
   *        (Function) or event (string) may may be added.
   *
   * @param {Object} automaton.transitions.*
   *        The states of the automaton.
   *
   * @param {Object} automaton.transitions.*.*
   *        The action associated with the corresponding state.
   *
   * @param {string} automaton.transitions.*.*.state
   *        The new state the automaton switches into after the action
   *        has been performed.
   *
   * @param {?string} automaton.transitions.*.*.do
   *        An optional reaction (do action) of the automaton when an
   *        action is passed to it (while it is in the corresponding state).
   */
  constructor({initialState:  p_initial_state,
               states:        p_states         = C_EMPTY,
               transitions:   p_transitions    = C_EMPTY,
               defaultAction: p_default_action = null,
               errorEvent:    p_error_event    = null
             })
  { super();
    
    this.v_initial_state_name  = p_initial_state;
    this.v_states              = p_states;
    this.v_transitions         = p_transitions;
    this.v_default_action      = p_default_action;
    this.v_error_event         = p_error_event;
    
    this.reset();
  }
  
  /** @private */
  m_dispatch_event(p_type, p_action = null)
  { if (typeof p_type === 'string')
    { this.dispatchEvent(p_type,
                         {action:        p_action,
                          previousState: this.v_state_name_previous,
                          state:         this.v_state
                         }
                        )
    }
    else if (typeof p_type === 'function')
    { p_type({action:        p_action,
               previousState: this.v_state_name_previous,
               state:         this.v_state
              }
             )
    }
  }
  
  /**
   *  Resets the automaton to its initial state;
   */
  reset()
  { this.v_state_name          = this.v_initial_state_name;
    this.v_state_name_previous = null;
    
    this.v_state               = this.v_states[this.v_state_name]      || C_EMPTY;
    this.v_transitions_state   = this.v_transitions[this.v_state_name] || C_EMPTY;
    this.v_last_action         = null;
    
    this.m_dispatch_event(this.v_state.entry);
    this.m_dispatch_event(this.v_state.do);
  }
  
  /**
   * Performs an action on the automation. It reacts by changing its state
   * an dispatching appropriate events.
   *
   * @param {string} p_action
   */
  action(p_action)
  { let
      l_transition = this.v_transitions_state[p_action],
      l_state_name_previous,
      l_state_name,
      l_state;
  
    if (l_transition == null)
    { l_transition =  this.v_transitions_state[this.v_default_action]; }
    
    if (l_transition == null)
    { if (this.v_error_event != null)
      { this.m_dispatch_event(this.v_error_event, p_action); }
    }
    else
    { this.v_last_action = p_action;
      l_state_name_previous = this.v_state_name_previous = this.v_state_name;
    
      let l_do_event = null;
      
      if (typeof l_transition === 'string')
      { l_state_name = this.v_state_name = l_transition; }
      else
      { l_state_name = this.v_state_name = l_transition.state;
        l_do_event        = l_transition.do;
      }
      
      if (l_state_name !== l_state_name_previous)
      { this.m_dispatch_event(this.v_state.exit, p_action); }
      
      l_state = this.v_state   = this.v_states[this.v_state_name]      || C_EMPTY;
      this.v_transitions_state = this.v_transitions[this.v_state_name] || C_EMPTY;
      
      this.m_dispatch_event(l_do_event, p_action);
      if (l_state_name !== l_state_name_previous)
      { this.m_dispatch_event(l_state.entry, p_action); }
      this.m_dispatch_event(l_state.do, p_action);
    }
  }
  
  /**
   * @returns {string} The current state of the automaton;
   */
  get state()
  { return this.v_state_name; }
  
  /**
   * @returns {string} The previous state of the automaton;
   */
  get previousState()
  { return this.v_state_name_previous; }
  
  /**
   * @returns {string} The last action that was performed by the automaton;
   */
  get lastAction()
  { return this.v_last_action; }
}

export {Automaton};
export default Automaton;