Source: state-updater.js

/**
 * The state-updater-on-actions interface.
 * <br><br>
 * <b>NOTE this isn't a real class</b>, it's an interface because of JSDoc
 * limitations.
 * [Related StackOverflow thread]{@link https://stackoverflow.com/questions/23095975/jsdoc-object-methods-with-method-or-property}
 *
 * @name StateUpdaterOnActions
 * @class
 */

/**
 * Calls an action with the passed parameters.
 *
 * @method
 * @name StateUpdaterOnActions#call
 * @param {Action} action - The action to call.
 * @param {*} params - The parameters to pass to the action when it will be called.
 * @return {Promise} - The promise that the action returns, or if it returns a
 *    promise with the value returned by the <code>updateState</code>, it will
 *    call the <code>updateAction</code> function passed to the constructor by
 *    parameter and if it returns a rejected promise, then it will be returned
 *    such rejected promise.
 */

/**
 * Calls an action using the specified context (e.g obj.action()) and the
 * passed parameters.
 *
 * @method
 * @name StateUpdaterOnActions#callCtx
 * @param {Action} action - The action to call.
 * @param {*} ctx - The context to use for calling the action (i.e. `this`
 *    pointer).
 * @param {*} params - The parameters to pass to the action when it will be called.
 * @return {Promise} - The promise that the action returns, or if it returns a
 *    promise with the value returned by the <code>updateState</code> method, it
 *    will call the <code>updateAction</code> function passed to the constructor
 *    by parameter and if it returns a rejected promise then it will be returned
 *    such rejected promise.
 */

/**
 * Returns the value which the actions must resolve their returned promises when
 * they require an state update.
 *
 * @method
 * @name StateUpdaterOnActions#updateState
 * @return {*} - The value DOES NOT matter as it's used internally to identify
 *        that the update state action must be called.
 */

/**
 * Action is a function which returns a promise; in order to notify that the
 * state must be updated, it MUST return a resolved promise which the value
 * returned by the
 * [<code>updateState</code> method of the <code>StateUpdaterOnActions</code> instance which is used to call the action]{@link StateUpdaterOnActions#updateState}.
 *
 * @async
 * @typedef {Function} Action
 */

/**
 * Create an instance of state updater that uses an action function, which
 * doesn't require to be executed in any context (e.g. a method of an
 * object, or a function which use `this` pointer) and requires the passed
 * parameters.
 *
 * @param {Action} updateAction - The function which update the shared state
 *    require by the actions which will be executed by the returned instance.
 *    The function MUST return a promise.
 * @param {*} params - The parameters to pass to the <code>updateAction</code>
 *    each time that it will be executed.
 * @return {StateUpdaterOnActions} - The instance which will call the
 *    <code>updateAction</code> with the passed parameters when required.
 */
export function createStateUpdater (updateAction, ...params) {
  if (typeof updateAction !== 'function') {
    throw new TypeError('updateAction must be a function')
  }

  return create(updateAction, null, ...params)
}

/**
 * Create an instance of state updater that uses an action function, which
 * requires to be executed in any context (e.g. a method of an object, or a
 * function which use <code>this</code> pointer) and requires the passed
 * parameters.
 *
 * @param {Action} updateAction - The function which update the shared state
 *    require by the actions which will be executed by the returned instance.
 *    The function MUST return a promise.
 * @param {*} ctx - The context to use for calling the action (i.e.
 *    <code>this</code> pointer).
 * @param {*} params - The parameters to pass to the <code>updateAction</code>
 *    each time that will be executed.
 * @return {StateUpdaterOnActions} - The instance which will call the
 *    <code>updateAction</code> with the passed parameters when required.
 */
export function createStateUpdaterCtx (updateAction, ctx, ...params) {
  if (typeof updateAction !== 'function') {
    throw new TypeError('updateAction must be a function')
  }

  if (!ctx) {
    switch (typeof ctx) {
      case 'undefined':
        throw new TypeError('context is required')
      case 'object':
        throw new TypeError('context cannot be null')
    }
  }

  return create(updateAction, ctx, ...params)
}

function create (updateAction, ctx, ...params) {
  // Empty object to have a reference to compare and detect that a updateState
  // has been called
  const updateID = {}
  let updatingPromise = null
  const updater = updateAction.bind(ctx, ...params)

  async function caller (action, ctx, ...params) {
    if (updatingPromise) {
      try {
        await updatingPromise
      } catch (err) {
        throw err
      } finally {
        updatingPromise = null
      }
    }

    let actPromise
    if (ctx === null) {
      actPromise = action(...params)
    } else {
      actPromise = action.call(ctx, ...params)
    }

    const res = await actPromise
    if (res !== updateID) {
      return res
    }

    if (!updatingPromise) {
      updatingPromise = updater()
    }

    return caller(action, ctx, ...params)
  }

  return {
    call: function (action, ...params) {
      if (typeof action !== 'function') {
        throw new TypeError('action must be a function')
      }

      return caller(action, null, ...params)
    },
    callCtx: function (action, ctx, ...params) {
      if (typeof action !== 'function') {
        throw new TypeError('action must be a function')
      }

      if (!ctx) {
        switch (typeof ctx) {
          case 'undefined':
            throw new TypeError('context is required')
          case 'object':
            throw new TypeError('context cannot be null')
        }
      }

      return caller(action, ctx, ...params)
    },
    updateState: function () {
      return updateID
    }
  }
}