/*! * asyncemitter.js - event emitter which resolves promises. * Copyright (c) 2014-2017, Christopher Jeffrey (MIT License). * https://github.com/bcoin-org/bcoin */ 'use strict'; var assert = require('assert'); var co = require('./co'); /** * Represents a promise-resolving event emitter. * @alias module:utils.AsyncEmitter * @see EventEmitter * @constructor */ function AsyncEmitter() { if (!(this instanceof AsyncEmitter)) return new AsyncEmitter(); this._events = Object.create(null); } /** * Add a listener. * @param {String} type * @param {Function} handler */ AsyncEmitter.prototype.addListener = function addListener(type, handler) { return this._push(type, handler, false); }; /** * Add a listener. * @param {String} type * @param {Function} handler */ AsyncEmitter.prototype.on = function on(type, handler) { return this.addListener(type, handler); }; /** * Add a listener to execute once. * @param {String} type * @param {Function} handler */ AsyncEmitter.prototype.once = function once(type, handler) { return this._push(type, handler, true); }; /** * Prepend a listener. * @param {String} type * @param {Function} handler */ AsyncEmitter.prototype.prependListener = function prependListener(type, handler) { return this._unshift(type, handler, false); }; /** * Prepend a listener to execute once. * @param {String} type * @param {Function} handler */ AsyncEmitter.prototype.prependOnceListener = function prependOnceListener(type, handler) { return this._unshift(type, handler, true); }; /** * Push a listener. * @private * @param {String} type * @param {Function} handler * @param {Boolean} once */ AsyncEmitter.prototype._push = function _push(type, handler, once) { assert(typeof type === 'string', '`type` must be a string.'); if (!this._events[type]) this._events[type] = []; this._events[type].push(new Listener(handler, once)); this.emit('newListener', type, handler); }; /** * Unshift a listener. * @param {String} type * @param {Function} handler * @param {Boolean} once */ AsyncEmitter.prototype._unshift = function _unshift(type, handler, once) { assert(typeof type === 'string', '`type` must be a string.'); if (!this._events[type]) this._events[type] = []; this._events[type].unshift(new Listener(handler, once)); this.emit('newListener', type, handler); }; /** * Remove a listener. * @param {String} type * @param {Function} handler */ AsyncEmitter.prototype.removeListener = function removeListener(type, handler) { var i, listeners, listener; var index = -1; assert(typeof type === 'string', '`type` must be a string.'); listeners = this._events[type]; if (!listeners) return; for (i = 0; i < listeners.length; i++) { listener = listeners[i]; if (listener.handler === handler) { index = i; break; } } if (index === -1) return; listeners.splice(index, 1); if (listeners.length === 0) delete this._events[type]; this.emit('removeListener', type, handler); }; /** * Set max listeners. * @param {Number} max */ AsyncEmitter.prototype.setMaxListeners = function setMaxListeners(max) { assert(typeof max === 'number', '`max` must be a number.'); assert(max >= 0, '`max` must be non-negative.'); assert(max % 1 === 0, '`max` must be an integer.'); }; /** * Remove all listeners. * @param {String?} type */ AsyncEmitter.prototype.removeAllListeners = function removeAllListeners(type) { if (arguments.length === 0) { this._events = Object.create(null); return; } assert(typeof type === 'string', '`type` must be a string.'); delete this._events[type]; }; /** * Get listeners array. * @param {String} type * @returns {Function[]} */ AsyncEmitter.prototype.listeners = function listeners(type) { var i, listeners, listener; var result = []; assert(typeof type === 'string', '`type` must be a string.'); listeners = this._events[type]; if (!listeners) return result; for (i = 0; i < listeners.length; i++) { listener = listeners[i]; result.push(listener.handler); } return result; }; /** * Get listener count for an event. * @param {String} type */ AsyncEmitter.prototype.listenerCount = function listenerCount(type) { var listeners; assert(typeof type === 'string', '`type` must be a string.'); listeners = this._events[type]; if (!listeners) return 0; return listeners.length; }; /** * Emit an event synchronously. * @method * @param {String} type * @param {...Object} args * @returns {Promise} */ AsyncEmitter.prototype.emit = function emit(type) { var i, j, listeners, error, err, args, listener, handler; assert(typeof type === 'string', '`type` must be a string.'); listeners = this._events[type]; if (!listeners || listeners.length === 0) { if (type === 'error') { error = arguments[1]; if (error instanceof Error) throw error; err = new Error('Uncaught, unspecified "error" event. (' + error + ')'); err.context = error; throw err; } return; } for (i = 0; i < listeners.length; i++) { listener = listeners[i]; handler = listener.handler; if (listener.once) { listeners.splice(i, 1); i--; } switch (arguments.length) { case 1: handler(); break; case 2: handler(arguments[1]); break; case 3: handler(arguments[1], arguments[2]); break; case 4: handler(arguments[1], arguments[2], arguments[3]); break; default: if (!args) { args = new Array(arguments.length - 1); for (j = 1; j < arguments.length; j++) args[j - 1] = arguments[j]; } handler.apply(null, args); break; } } }; /** * Emit an event. Wait for promises to resolve. * @method * @param {String} type * @param {...Object} args * @returns {Promise} */ AsyncEmitter.prototype.fire = co(function* fire(type) { var i, j, listeners, error, err, args, listener, handler; assert(typeof type === 'string', '`type` must be a string.'); listeners = this._events[type]; if (!listeners || listeners.length === 0) { if (type === 'error') { error = arguments[1]; if (error instanceof Error) throw error; err = new Error('Uncaught, unspecified "error" event. (' + error + ')'); err.context = error; throw err; } return; } for (i = 0; i < listeners.length; i++) { listener = listeners[i]; handler = listener.handler; if (listener.once) { listeners.splice(i, 1); i--; } switch (arguments.length) { case 1: yield handler(); break; case 2: yield handler(arguments[1]); break; case 3: yield handler(arguments[1], arguments[2]); break; case 4: yield handler(arguments[1], arguments[2], arguments[3]); break; default: if (!args) { args = new Array(arguments.length - 1); for (j = 1; j < arguments.length; j++) args[j - 1] = arguments[j]; } yield handler.apply(null, args); break; } } }); /** * Emit an event. Ignore rejections. * @method * @param {String} type * @param {...Object} args * @returns {Promise} */ AsyncEmitter.prototype.tryFire = co(function* tryFire(type) { try { yield this.emit.apply(this, arguments); } catch (e) { if (type === 'error') return; try { yield this.emit('error', e); } catch (e) { ; } } }); /** * Event Listener * @constructor * @ignore * @param {Function} handler * @param {Boolean} once * @property {Function} handler * @property {Boolean} once */ function Listener(handler, once) { assert(typeof handler === 'function', '`handler` must be a function.'); assert(typeof once === 'boolean', '`once` must be a function.'); this.handler = handler; this.once = once; } /* * Expose */ module.exports = AsyncEmitter;