Source: syngen/utility/machine.js

/**
 * Provides an interface for finite-state machines.
 * Machines have defined finite states with actions that transition it to other states.
 * Implementations can leverage machines to handle state and subscribe to their events to respond to changes in state.
 * @augments syngen.utility.pubsub
 * @interface
 * @see syngen.utility.machine.create
 */
syngen.utility.machine = {}

/**
 * Instantiates a new finite-state machine.
 * @param {Object} options
 * @param {Object} [options={}]
 * @param {Object} [options.state=none]
 *   The initial state.
 * @param {Object} [options.transition={}]
 *   A hash of states and their actions.
 *   Each state is a hash of one or more actions.
 *   Each action is a function which _should_ call {@link syngen.utility.machine.change|this.change()} to change state.
 *   Actions _can_ have branching logic that results in multiple states.
 * @returns {syngen.utility.machine}
 * @static
 */
syngen.utility.machine.create = function (options = {}) {
  return Object.create(this.prototype).construct(options)
}

syngen.utility.machine.prototype = {
  /**
   * Changes to `state` with `data`.
   * @fires syngen.utility.machine#event:enter
   * @fires syngen.utility.machine#event:enter-{state}
   * @fires syngen.utility.machine#event:exit
   * @fires syngen.utility.machine#event:exit-{state}
   * @instance
   * @param {String} state
   * @param {Object} [data={}]
   */
  change: function (state, data = {}) {
    if (!(state in this.transition) || this.is(state)) {
      return this
    }

    const exitPayload = {
      currentState: this.state,
      nextState: state,
      ...data,
    }

    /**
     * Fired whenever states are exited.
     * @event syngen.utility.machine#event:exit
     * @type {Object}
     * @param {String} currentState
     * @param {String} nextState
     * @param {...*} ...data
     */
    this.pubsub.emit('exit', exitPayload)

    /**
     * Fired whenever a particular state is exited.
     * If the state is `foo`, then the event is named `exit-foo`.
     * @event syngen.utility.machine#event:exit-{state}
     * @type {Object}
     * @param {String} currentState
     * @param {String} nextState
     * @param {...*} ...data
     */
    this.pubsub.emit(`exit-${this.state}`, exitPayload)

    const enterPayload = {
      currentState: state,
      previousState: this.state,
      ...data,
    }

    this.setState(state)

    /**
     * Fired whenever states are entered.
     * @event syngen.utility.machine#event:enter
     * @type {Object}
     * @param {String} currentState
     * @param {String} previousState
     * @param {...*} ...data
     */
    this.pubsub.emit('enter', enterPayload)

    /**
     * Fired whenever a particular state is entered.
     * If the state is `foo`, then the event is named `enter-foo`.
     * @event syngen.utility.machine#event:enter-{state}
     * @type {Object}
     * @param {String} currentState
     * @param {String} previousState
     * @param {...*} ...data
     */
    this.pubsub.emit(`enter-${this.state}`, enterPayload)

    return this
  },
  /**
   * Initializes the instance with `options`.
   * @instance
   * @param {Object} options
   * @private
   */
  construct: function ({
    state = 'none',
    transition = {}
  } = {}) {
    this.transition = {...transition}
    this.setState(state)

    syngen.utility.pubsub.decorate(this)

    return this
  },
  /**
   * Prepares the instance for garbage collection.
   * @instance
   */
  destroy: function () {
    this.pubsub.destroy()
    return this
  },
  /**
   * Calls the function defined for `action` in the current state with `data`.
   * @fires syngen.utility.machine#event:after
   * @fires syngen.utility.machine#event:after-{event}
   * @fires syngen.utility.machine#event:after-{state}-{event}
   * @fires syngen.utility.machine#event:before
   * @fires syngen.utility.machine#event:before-{event}
   * @fires syngen.utility.machine#event:before-{state}-{event}
   * @instance
   */
  dispatch: function (event, data = {}) {
    const actions = this.transition[this.state]

    if (!actions) {
      return this
    }

    const action = actions[event]

    if (action) {
      const state = this.state

      const beforePayload = {
        event,
        state,
        ...data,
      }

      /**
       * Fired before an event is dispatched.
       * @event syngen.utility.machine#event:before
       * @type {Object}
       * @param {String} event
       * @param {Object} state
       * @param {...*} ...data
       */
      this.pubsub.emit('before', beforePayload)

      /**
       * Fired before a particular event is dispatched.
       * If the event is `foo`, then the event is named `before-foo`.
       * @event syngen.utility.machine#event:before-{event}
       * @type {Object}
       * @param {String} event
       * @param {Object} state
       * @param {...*} ...data
       */
      this.pubsub.emit(`before-${event}`, beforePayload)

      /**
       * Fired before a particular event is dispatched in a particular state.
       * If the state is `foo` and the event is `bar`, then the event is named `before-foo-bar`.
       * @event syngen.utility.machine#event:before-{state}-{event}
       * @type {Object}
       * @param {String} event
       * @param {Object} state
       * @param {...*} ...data
       */
      this.pubsub.emit(`before-${state}-${event}`, beforePayload)

      action.call(this, data)

      const afterPayload = {
        currentState: this.state,
        event,
        previousState: state,
        ...data,
      }

      /**
       * Fired after an event is dispatched.
       * @event syngen.utility.machine#event:after
       * @type {Object}
       * @param {String} currentState
       * @param {String} event
       * @param {String} previousState
       * @param {Object} state
       * @param {...*} ...data
       */
      this.pubsub.emit('after', afterPayload)

      /**
       * Fired after a particular event is dispatched.
       * If the event is `foo`, then the event is named `before-foo`.
       * @event syngen.utility.machine#event:after-{event}
       * @type {Object}
       * @param {String} currentState
       * @param {String} event
       * @param {String} previousState
       * @param {Object} state
       * @param {...*} ...data
       */
      this.pubsub.emit(`after-${event}`, afterPayload)

      /**
       * Fired after a particular event is dispatched in a particular state.
       * If the state is `foo` and the event is `bar`, then the event is named `before-foo-bar`.
       * @event syngen.utility.machine#event:after-{state}-{event}
       * @type {Object}
       * @param {String} currentState
       * @param {String} event
       * @param {String} previousState
       * @param {Object} state
       * @param {...*} ...data
       */
      this.pubsub.emit(`after-${state}-${event}`, afterPayload)
    }

    return this
  },
  /**
   * Returns the current state.
   * @instance
   * @returns {String}
   */
  getState: function () {
    return this.state
  },
  /**
   * Returns whether `state` is the current state.
   * @instance
   * @param {String} state
   * @returns {Boolean}
   */
  is: function (state) {
    return this.state == state
  },
  /**
   * Sets the current state to `state` immediately.
   * @instance
   * @param {String} state
   * @returns {String}
   */
  setState: function (state) {
    if (state in this.transition) {
      this.state = state
    }

    return this
  },
  /**
   * The current state.
   * @instance
   * @type {String}
   */
  state: undefined,
}