adapt-authoring-core/lib/Hook.js

import _ from 'lodash'
/**
 * Allows observers to tap into to a specific piece of code, and execute their own arbitrary code
 * @memberof core
 */
class Hook {
  /**
   * Types of supported Hook
   * @type {Object}
   * @property {String} Parallel
   * @property {String} Series
   */
  static get Types () {
    return {
      Parallel: 'parallel',
      Series: 'series'
    }
  }

  /** @constructor */
  constructor (opts) {
    /** @ignore */ this._hookObservers = []
    /** @ignore */ this._promiseObservers = []
    /** @ignore */ this._options = Object.assign({ // force series execution for mutable hooks
      type: opts?.mutable === true ? Hook.Types.Series : Hook.Types.Parallel,
      mutable: false
    }, opts)
  }

  /**
   * Whether this hook has any observer functions
   * @return {Boolean}
   */
  get hasObservers () {
    return this._hookObservers.length > 0
  }

  /**
   * Adds an observer to the hook
   * @param {Function} observer Callback to be called when the hook is invoked
   */
  tap (observer) {
    if (_.isFunction(observer)) this._hookObservers.push(observer)
  }

  /**
   * Removes an observer from the hook
   * @param {Function} observer
   */
  untap (observer) {
    const i = this._hookObservers.indexOf(observer)
    if (i > -1) this._hookObservers.splice(i, 1)
  }

  /**
   * Returns a promise which is resolved when the hook is successfully invoked. If hook fails, the promise is rejected with the error
   * @returns Promise
   */
  onInvoke () {
    return new Promise((resolve, reject) => this._hookObservers.push([resolve, reject]))
  }

  /**
   * Invokes all observers
   * @param {...*} args Arguments to be passed to observers
   * @return {Promise}
   */
  async invoke (...args) {
    let error, data
    try {
      if (this._options.type === Hook.Types.Parallel) {
        data = await Promise.all(this._hookObservers.map(o => o(...args)))
      } else {
        // if not mutable, send a deep copy of the args to avoid any meddling
        for (const o of this._hookObservers) data = await o(...this._options.mutable ? args : args.map(a => _.cloneDeep(a)))
      }
    } catch (e) {
      error = e
    }
    this._promiseObservers.forEach(([resolve, reject]) => error ? reject(error) : resolve(...args))
    if (error) throw error
    return data
  }
}

export default Hook