lib/base/events.js

// Events
// ---------------

'use strict';

const Promise = require('bluebird');
const events = require('events');
const _ = require('lodash');
const EventEmitter = events.EventEmitter;
const eventNames = (text) => text.split(/\s+/);

/**
 * @class Events
 * @description
 * Base Event class inherited by {@link Model} and {@link Collection}. It's not
 * meant to be used directly, and is only displayed here for completeness.
 */
class Events extends EventEmitter {
  /**
   * Registers an event listener. The callback will be invoked whenever the event is fired. The event string may also be
   * a space-delimited list of several event names.
   *
   * @method Events#on
   * @param {string} nameOrNames The name or space separated names of events to register a callback for.
   * @param {function} callback That callback to invoke whenever the event is fired.
   * @return {mixed} The object where this is called on is returned to allow chaining this method call.
   */
  on(nameOrNames, callback) {
    eventNames(nameOrNames).forEach((name) => {
      super.on(name, callback);
    });
    return this;
  }

  /**
   * @method Events#off
   * @description
   * Remove a previously-bound callback event listener from an object. If no
   * event name is specified, callbacks for all events will be removed.
   *
   * @param {string} nameOrNames
   *   The name of the event or space separated list of events to stop listening
   *   to.
   * @param {function} callback That callback to remove.
   */
  off(nameOrNames, callback) {
    if (nameOrNames == null) {
      return this.removeAllListeners();
    }

    eventNames(nameOrNames).forEach((name) => {
      if (callback === undefined) {
        return this.removeAllListeners(name);
      }
      return this.removeListener(name, callback);
    });
    return this;
  }

  /**
   * @method Events#trigger
   * @description
   * Trigger callbacks for the given event, or space-delimited list of events.
   * Subsequent arguments to `trigger` will be passed along to the event
   * callback.
   *
   * @param {string} nameOrNames
   *   The name of the event to trigger. Also accepts a space separated list of
   *   event names.
   * @param {...mixed} [args]
   *   Extra arguments to pass to the event listener callback function.
   */
  trigger(nameOrNames) {
    eventNames(nameOrNames).forEach((name) => {
      this.emit.apply(this, [name].concat(Array.from(arguments)));
    });
    return this;
  }

  /**
   * A promise version of {@link Events#trigger}, returning a promise which
   * resolves with all return values from triggered event handlers. If any of the
   * event handlers throw an `Error` or return a rejected promise, the promise
   * will be rejected. Used internally on the {@link Model#event:creating "creating"},
   * {@link Model#event:updating "updating"}, {@link Model#event:saving "saving"}, and
   * {@link Model@event:destroying "destroying"} events, and can be helpful when needing
   * async event handlers (e.g. for validations).
   *
   * @method Events#triggerThen
   * @param {string} name
   *   The event name or a whitespace-separated list of event names to be triggered.
   * @param {...mixed} [args] Arguments to be passed to any registered event handlers.
   * @returns {Promise}
   *   A promise resolving to the return values of any triggered handlers.
   */
  triggerThen(nameOrNames) {
    const names = eventNames(nameOrNames);
    const listeners = _.flatMap(names, (name) => this.listeners(name));
    const args = Array.from(arguments);

    return Promise.mapSeries(listeners, (listener) => listener.apply(this, args.slice(1)));
  }

  /**
   * @method Events#once
   * @description
   * Just like {@link Events#on}, but causes the bound callback to fire only
   * once before being removed. Handy for saying "the next time that X happens,
   * do this". When multiple events are passed in using the space separated
   * syntax, the event will fire once for every event you passed in, not once
   * for a combination of all events.
   *
   * @param {string} nameOrNames
   *   The name of the event or space separated list of events to register a
   *   callback for.
   * @param {function} callback
   *   That callback to invoke only once when the event is fired.
   */
  once(name, callback) {
    const wrapped = _.once(function() {
      this.off(name, wrapped);
      return callback.apply(this, arguments);
    });
    wrapped._callback = callback;
    return this.on(name, wrapped);
  }
}

module.exports = Events;