raveljs/ravel

View on GitHub
lib/util/event_emitter.js

Summary

Maintainability
A
0 mins
Test Coverage
A
100%
'use strict';

const $err = require('./application_error');

const sEventMap = Symbol('_eventMap');

/**
 * @private
 * @class Listener
 */
class Listener {
  constructor (listener, once = false) {
    this.listener = listener;
    this.once = once;
  }
}

/**
 * A more-or-less API-compatible implementation of EventEmitter which supports
 * async functions as listeners, where emit() returns a `Promise` which resolves
 * when each of the listeners have resolved (or rejects when one rejects).
 * Listeners are run in parallel (no order is guaranteed).
 *
 * @private
 * @class AsyncEventEmitter
 */
class AsyncEventEmitter {
  constructor () {
    this[sEventMap] = new Map();
  }

  /**
   * AsyncEventEmitter does not support defaultMaxListeners.
   */
  static get defaultMaxListeners () {
    throw new $err.NotImplemented('AsyncEventEmitter does not support defaultMaxListeners');
  }

  /**
   * AsyncEventEmitter does not support getMaxListeners.
   */
  getMaxListeners () {
    throw new $err.NotImplemented('AsyncEventEmitter does not support getMaxListeners');
  }

  /**
   * AsyncEventEmitter does not support setMaxListeners.
   */
  setMaxListeners () {
    throw new $err.NotImplemented('AsyncEventEmitter does not support setMaxListeners');
  }

  /**
   * Adds the listener function to the end of the listeners array for the event named eventName.
   * No checks are made to see if the listener has already been added. Multiple calls passing
   * the same combination of eventName and listener will result in the listener being added,
   * and called, multiple times.
   *
   * @param {any} eventName - The name of the event to attach to.
   * @param {Function} listener - The listener callback (potentially an async function).
   * @returns {AsyncEventEmitter} - A Reference this AsyncEventEmitter so that calls can be chained.
   * @memberof AsyncEventEmitter
   */
  on (eventName, listener) {
    if (!this[sEventMap].has(eventName)) {
      this[sEventMap].set(eventName, []);
    }
    this[sEventMap].get(eventName).push(new Listener(listener));
    return this;
  }

  /**
   * Alias for `on(eventName, listener)`.
   *
   * @param {any} eventName - The name of the event to attach to.
   * @param {Function} listener - The listener callback (potentially an async function).
   * @returns {AsyncEventEmitter} - A Reference this AsyncEventEmitter so that calls can be chained.
   * @memberof AsyncEventEmitter
   */
  addListener (eventName, listener) {
    return this.on(eventName, listener);
  }

  /**
   * Adds a one time listener function for the event named eventName. The next time eventName
   * is triggered, this listener is removed and then invoked.
   *
   * @param {any} eventName - The name of the event to attach to.
   * @param {Function} listener - The listener callback (potentially an async function).
   * @returns {AsyncEventEmitter} - A Reference this AsyncEventEmitter so that calls can be chained.
   * @memberof AsyncEventEmitter
   */
  once (eventName, listener) {
    if (!this[sEventMap].has(eventName)) {
      this[sEventMap].set(eventName, []);
    }
    this[sEventMap].get(eventName).push(new Listener(listener, true));
    return this;
  }

  /**
   * Calls each of the listeners registered for the event named eventName,
   * in the order they were registered, passing the supplied arguments to each.
   * Awaits on the successful result of ALL the listeners, returning a Promise.
   *
   * @param {any} eventName - The name of the event to emit.
   * @param {...any} args - Arguments to pass to the listeners.
   * @returns {Promise} - Resolves when all of the listeners resolve. Rejects otherwise.
   * @memberof AsyncEventEmitter
   */
  emit (eventName, ...args) {
    if (!this[sEventMap].has(eventName)) {
      return Promise.resolve();
    } else {
      const toExecute = this[sEventMap].get(eventName);
      // remove onces immediately, before invoking
      this[sEventMap].set(eventName, toExecute.filter(l => !l.once));
      // invoke (mapping to promises)
      return Promise.all(toExecute.map(l => l.listener.apply(l.listener, args)));
    }
  }

  /**
   * Returns an array listing the events for which the emitter has registered listeners.
   * The values in the array will be strings or Symbols.
   *
   * @returns {Array[any]} - The events for which the emitter has registered listeners.
   */
  eventNames () {
    return [...this[sEventMap].keys()];
  }

  /**
   * Returns the number of listeners listening to the event named eventName.
   *
   * @param {any} eventName - The name of the event to emit.
   * @returns {number} The number of listeners for the given event.
   * @memberof AsyncEventEmitter
   */
  listenerCount (eventName) {
    return !this[sEventMap].has(eventName) ? 0 : this[sEventMap].get(eventName).length;
  }

  /**
   * AsyncEventEmitter does not support prependListener.
   */
  prependListener () {
    throw new $err.NotImplemented('AsyncEventEmitter does not support prependListener');
  }

  /**
   * AsyncEventEmitter does not support prependOnceListener..
   */
  prependOnceListener () {
    throw new $err.NotImplemented('AsyncEventEmitter does not support prependOnceListener');
  }

  /**
   * Removes all listeners, or those of the specified eventName.
   *
   * Note that it is bad practice to remove listeners added elsewhere in the code,
   * particularly when the EventEmitter instance was created by some other component
   * or module (e.g. Sockets or file streams).
   *
   * Returns a reference to the EventEmitter, so that calls can be chained.
   *
   * @param {any | undefined} eventName - The name of the event to remove all ilsteners for (optional).
   * @returns {AsyncEventEmitter} - A Reference this AsyncEventEmitter so that calls can be chained.
   * @memberof AsyncEventEmitter
   */
  removeAllListeners (eventName = null) {
    if (eventName === null) {
      this[sEventMap].clear();
    } else if (this[sEventMap].has(eventName)) {
      this[sEventMap].set(eventName, []);
    }
    return this;
  }

  /**
   * Removes the specified listener from the listener array for the event named eventName.
   *
   * @param {any} eventName - The name of the event to remove the listener from.
   * @param {Function} listener - The listener to remove.
   * @returns {AsyncEventEmitter} - A Reference this AsyncEventEmitter so that calls can be chained.
   * @memberof AsyncEventEmitter
   */
  removeListener (eventName, listener) {
    if (this[sEventMap].has(eventName)) {
      const idx = this[sEventMap].get(eventName).findIndex(e => e.listener === listener);
      if (idx >= 0) {
        this[sEventMap].get(eventName).splice(idx, 1);
      }
    }
    return this;
  }
}

module.exports = AsyncEventEmitter;