index.js
'use strict';
/**
* Nice EventEmitter
* - more object-friendly: listeners can be objects (i.e. no need to "bind" on listening methods)
* - easier to find leaks: debug counting allows each "class" of listeners to set its own maximum number
* - easier to stop listening: per listener (i.e. no need to keep track of each "on" so you can "remove" later)
* - easier to avoid mistake of listening to inexistant event: emitter MUST declare which events it can emit
* - often faster than many existing event emitters
*/
function EventEmitter () {
this._listenersPerEventId = {};
if (debugLevel > 0) {
this._maxCountPerListenerKey = {};
}
}
module.exports = EventEmitter;
// Debug levels
/**
* Debug checks & counting. Errors are thrown. Helps you debug your code by crashing early.
* This is the default level, except if code is minified.
*/
EventEmitter.DEBUG_THROW = 2;
/**
* Debug checks & counting. Errors go to `console.error` but execution continues as normally as possible.
*/
EventEmitter.DEBUG_ERROR = 1;
/**
* No counting, no checks except those that avoid crashes.
* The fastest level, with minimum memory usage.
* If your code is minified, NO_DEBUG is automatically the default (no need to call `setDebugLevel`).
* NB: counting based on class names would have issues when 2 minified classes end-up with the same name.
*/
EventEmitter.NO_DEBUG = 0;
// Set default. NO_DEBUG if minified, DEBUG_THROW otherwise. (see above)
var debugLevel = getObjectClassname(new EventEmitter()) !== 'EventEmitter'
? EventEmitter.NO_DEBUG
: EventEmitter.DEBUG_THROW;
/**
* Sets debug level. See comments about debug levels.
*
* @param {number} level - EventEmitter.NO_DEBUG, EventEmitter.DEBUG_ERROR, or EventEmitter.DEBUG_THROW
*/
EventEmitter.setDebugLevel = function (level) {
debugLevel = level;
};
var respectSubscriberOrder = false;
/**
* Forces listener to be notified in the order in which they subscribed, at the expense of a bit more CPU and memory.
* By default his order is not guaranteed.
*
* @param {boolean} shouldRespect
*/
EventEmitter.respectSubscriberOrder = function (shouldRespect) {
respectSubscriberOrder = shouldRespect;
};
//--- Emitter side
/**
* Declares an event for this emitter.
* Event must be declared before `emit`, `on`, or other method is called for this event ID.
*
* @param {string} eventId
*/
EventEmitter.prototype.declareEvent = function (eventId) {
if (this._listenersPerEventId[eventId] !== undefined) {
return throwOrConsole('Event ID declared twice: ', getAsText(this, eventId));
}
this._listenersPerEventId[eventId] = new ListenerList(this, eventId);
};
/**
* Notifies each listener which subscribed to given eventId.
* Optional parameters are passed.
*
* @param {string} eventId
* @param {*} [p1]
* @param {*} [p2]
* @param {*} [p3] - there can be more than 3 parameters
* @returns {boolean} false if no listeners are registered.
*/
EventEmitter.prototype.emit = function (eventId, p1, p2, p3) {
var listenerList = this._listenersPerEventId[eventId];
if (listenerList === undefined) {
throwOrConsole('Undeclared event ID for ' + getObjectClassname(this) + ': ', eventId);
return false;
}
switch (arguments.length) {
case 1: return listenerList.emit0();
case 2: return listenerList.emit1(p1);
case 3: return listenerList.emit2(p1, p2);
case 4: return listenerList.emit3(p1, p2, p3);
default: return listenerList.emitN.apply(listenerList, [].slice.call(arguments, 1));
}
};
/**
* Returns a "quick emitter" for a given event ID of this EventEmitter.
* Using a quick emitter to emit is quite faster (if you are chasing fractions of milliseconds).
*
* @param {string} eventId - declared event ID for which you want to "quick emit"
* @returns {QuickEmitter} - an object with methods emit0, emit1, emit2, emit3 and emitN
*/
EventEmitter.prototype.getQuickEmitter = function (eventId) {
var listenerList = this._listenersPerEventId[eventId];
if (listenerList === undefined) {
return throwOrConsole('Undeclared event ID for ' + getObjectClassname(this) + ': ', eventId);
}
return listenerList;
};
/**
* Tells how many listeners are currently subscribed to given event ID.
*
* @param {string} eventId
* @returns {number} number of listeners on this specific event
*/
EventEmitter.prototype.listenerCount = function (eventId) {
var listenerList = this._listenersPerEventId[eventId];
if (listenerList === undefined) {
return throwOrConsole('Undeclared event ID for ' + getObjectClassname(this) + ': ', eventId);
}
return listenerList._count;
};
//--- Listener side
/**
* Subscribes to an event.
*
* @param {string} eventId
* @param {function} method - can be a simple function too
* @param {object|string|undefined} listener - if not passed, emitter will be passed as context when event occurs
* @returns {EventEmitter} this
*/
EventEmitter.prototype.on = function (eventId, method, listener) {
var listenerList = this._listenersPerEventId[eventId];
if (!listenerList) return throwOrConsole('Invalid event ID: ', getAsText(this, eventId, listener)), this;
if (debugLevel > 0) {
if (arguments.length >= 3 && (!listener || typeof listener === 'function')) {
return throwOrConsole('Invalid listener parameter to emitter.on \'' + eventId + '\': ', typeof listener), this;
}
if (typeof method !== 'function') {
return throwOrConsole('Invalid function parameter to emitter.on \'' + eventId + '\': ', typeof method), this;
}
listenerList._countListener(listener || this, listener || null);
}
listenerList._addListener(listener || this, method);
return this;
};
EventEmitter.prototype.addListener = EventEmitter.prototype.on;
/**
* Unsubscribes from an event.
* Specifying your listeners when calling "on" is often much easier
* than having to track/store which functions you passed when subscribing.
*
* @param {string} eventId
* @param {object|string|function} listener - same "listener" you passed when you called "on"
*/
EventEmitter.prototype.off = function (eventId, listener) {
var listenerList = this._listenersPerEventId[eventId];
if (!listenerList) return throwOrConsole('Invalid event ID: ', getAsText(this, eventId, listener));
if (typeof listener === 'function') {
// Old API compatibility
var indexFn = listenerList._findMethod(listener);
if (indexFn !== -1) {
listenerList._removeListener(indexFn, null);
} else if (listener._hasNiceEmitterOnce) {
this._removeOnceListener(listenerList, listener);
}
} else {
var index = listenerList._findListener(listener);
if (index !== -1) {
listenerList._removeListener(index, listener);
} else if (debugLevel > 0 && !listener) {
return throwOrConsole('Invalid parameter to emitter.off \'' + eventId + '\': ', listener);
}
}
};
EventEmitter.prototype.removeListener = EventEmitter.prototype.off;
/**
* Old API compatibility
* @deprecated `once` is not your friend.
* @param {string} eventId
* @param {function} method - can be a simple function too
* @param {object|string|undefined} listener - if not passed, emitter will be passed as context when event occurs
* @param {number} [timeoutMs] - if not passed, a "human-debugging" value of 5 seconds will be used.
* If timeout expires before the event occurs, a simple console.error is logged.
*/
EventEmitter.prototype.once = function (eventId, method, listener, timeoutMs) {
method._hasNiceEmitterOnce = true;
timeoutMs = timeoutMs || 5000;
var timeout = setTimeout(function () {
console.error('emitter.once \'' + eventId + '\' was not called before ' + timeoutMs + 'ms timeout');
}, timeoutMs);
var emitter = this;
function hopFunc () {
if (listener) {
emitter.off(eventId, listener);
} else {
emitter.off(eventId, hopFunc);
}
clearTimeout(timeout);
method.apply(this, arguments); // "this" and arguments will be passed correctly by emitter
}
hopFunc._timeout = timeout;
hopFunc._method = method;
if (listener) {
this.on(eventId, hopFunc, listener);
} else {
this.on(eventId, hopFunc);
}
};
// Only used when caller does `emitter.off(eventId, func)` of a previously `emitter.once(eventId, func)`
EventEmitter.prototype._removeOnceListener = function (listenerList, method) {
var hopFunc, indexFn = -1;
var methods = listenerList._methods;
if (methods === null) return; // not listening; safe to ignore
if (typeof methods === 'function') {
if (methods._method === method) hopFunc = methods; // indexFn not needed here
} else {
for (var i = 0; i < methods.length; i++) {
var m = methods[i];
if (m && m._method === method) {
hopFunc = m;
indexFn = i;
break;
}
}
}
if (!hopFunc) return; // happens if "once" has already fired before being removed; safe to ignore
listenerList._removeListener(indexFn, null);
clearTimeout(hopFunc._timeout);
}
/**
* Unsubscribes the given listener (context) from all events of this emitter
*
* @param {object|string} listener
*/
EventEmitter.prototype.forgetListener = function (listener) {
for (var eventId in this._listenersPerEventId) {
this.off(eventId, listener);
}
};
/**
* Sets the limit per event ID of listeners for this emitter and listener class objects.
* Default is 1 for all classes when this API is not called.
* If the maximum is reached, an error is thrown or logged.
* Does nothing if debug level is NO_DEBUG.
*
* @param {number} maxCount
* @param {object|string} listener
*/
EventEmitter.prototype.setListenerMaxCount = function (maxCount, listener) {
if (debugLevel === 0) return;
if (!(maxCount > 0) || !listener) {
return throwOrConsole('Invalid parameters to emitter.setListenerMaxCount: ', maxCount + ', ' + listener);
}
this._maxCountPerListenerKey[getObjectClassname(listener)] = maxCount;
};
/**
* Old API compatibility.
* Sets a maximum count (default is 1) of listeners that can subscribe to 1 event ID.
* This limit applies when `on` or `addListener` are called without `listener` parameter.
* If the maximum is reached, an error is thrown or logged.
* Does nothing if debug level is NO_DEBUG.
*
* @param {number} maxCount
*/
EventEmitter.prototype.setMaxListeners = function (maxCount) {
if (debugLevel === 0) return;
if (!(maxCount > 0) || arguments.length > 1) {
return throwOrConsole('Invalid parameters to emitter.setMaxListeners: ', maxCount + (arguments[1] !== undefined ? ', ' + arguments[1] : ''));
}
this._maxCountPerListenerKey[DEFAULT_LISTENER] = maxCount;
};
//--- Private helpers
/**
* Internal implementation of a "single eventID" emitter to a list of listeners.
* A ListenerList can be returned to outside world for "quick emit" purpose.
*
* @param {EventEmitter} emitter
* @param {string} eventId
*/
function ListenerList (emitter, eventId) {
this._count = 0; // count of "listeners"
this._methods = null; // null, function, or array of functions
this._objects = null; // null, context, or array of contexts
if (debugLevel > 0) {
this._emitter = emitter; // our parent EventEmitter
this._eventId = eventId;
this._counterMap = null; // key: listenerKey, value: count of listeners with same listenerKey
}
}
ListenerList.prototype._addListener = function (context, method) {
if (this._methods === null) {
// 0 -> 1
this._count = 1;
this._methods = method;
this._objects = context;
} else if (typeof this._methods !== 'function') {
// n -> n+1 (n >= 0) # Array already exists
var index = this._methods.length;
if (index !== this._count) {
if (index > 5 && index > this._count * 2) {
this._compactList(this._count + 1);
index = this._count;
} else if (respectSubscriberOrder) {
while (index >= 1 && !this._methods[index - 1]) index--;
} else {
for (index = 0; this._methods[index]; index++) ; // this._methods.indexOf(null) seems slower
}
}
this._methods[index] = method;
this._objects[index] = context;
this._count++;
} else {
// 1 -> 2 # Array creation
this._count = 2;
this._methods = [this._methods, method];
this._objects = [this._objects, context];
}
};
ListenerList.prototype._compactList = function (size) {
var methods = new Array(size), objects = new Array(size);
for (var i = 0, j = 0; j < this._count; i++) {
if (this._methods[i]) {
methods[j] = this._methods[i];
objects[j++] = this._objects[i];
}
}
this._methods = methods;
this._objects = objects;
};
ListenerList.prototype._findListener = function (listener) {
if (typeof this._methods === 'function') {
return this._objects === listener ? 0 : -1;
} else {
return this._objects !== null ? this._objects.indexOf(listener) : -1;
}
};
ListenerList.prototype._findMethod = function (method) {
if (typeof this._methods === 'function') {
return this._methods === method ? 0 : -1;
} else {
return this._methods !== null ? this._methods.indexOf(method) : -1;
}
};
ListenerList.prototype._removeListener = function (index, listener) {
this._count--;
if (typeof this._methods === 'function') {
this._methods = null;
this._objects = null;
} else {
this._methods[index] = null;
this._objects[index] = null;
}
if (debugLevel > 0) {
var listenerKey = getObjectClassname(listener);
this._counterMap[listenerKey]--;
}
};
// Only used if debugLevel > 0
ListenerList.prototype._countListener = function (context, listener) {
var listenerKey = getObjectClassname(listener);
var currentCount;
if (!this._counterMap) {
this._counterMap = { 0: 0 };
currentCount = 1;
} else {
currentCount = (this._counterMap[listenerKey] || 0) + 1
}
var maxListenerCount = this._emitter._maxCountPerListenerKey[listenerKey] || 1;
if (currentCount > maxListenerCount) {
var msg = 'Too many listeners: ' + getAsText(this._emitter, this._eventId, listener) + '. ';
var advice = listener
? 'Use ' + getObjectClassname(this._emitter) + '.setListenerMaxCount(n, ' + listenerKey + ') with n >= ' + currentCount
: 'Use ' + getObjectClassname(this._emitter) + '.setMaxListeners(n) with n >= ' + currentCount + '. Even better: specify your listeners when calling "on"';
throwOrConsole(msg, advice); // if console we can continue below
}
this._counterMap[listenerKey] = currentCount; // not done if exception above
// Same listener should not listen twice to same event ID (does not apply to "undefined" listener)
// NB: this._count is not yet updated at this point, hence this._count >= 1 below (instead of 2)
if (currentCount >= 2 && this._count >= 1 && context === listener && this._findListener(listener) !== -1) {
throwOrConsole('Listener listens twice: ', getAsText(this._emitter, this._eventId, listener));
}
};
ListenerList.prototype.emit0 = function () {
if (this._count === 0) return false; // 0 listeners
if (typeof this._methods === 'function') {
this._methods.call(this._objects);
} else {
var methods = this._methods, objects = this._objects, len = this._methods.length;
for (var i = 0; i < len; i++) { var m = methods[i]; m && m.call(objects[i]); }
}
return true;
};
ListenerList.prototype.emit1 = function (arg1) {
if (this._count === 0) return false; // 0 listeners
if (typeof this._methods === 'function') {
this._methods.call(this._objects, arg1);
} else {
var methods = this._methods, objects = this._objects, len = this._methods.length;
for (var i = 0; i < len; i++) { var m = methods[i]; m && m.call(objects[i], arg1); }
}
return true;
};
ListenerList.prototype.emit2 = function (arg1, arg2) {
if (this._count === 0) return false; // 0 listeners
if (typeof this._methods === 'function') {
this._methods.call(this._objects, arg1, arg2);
} else {
var methods = this._methods, objects = this._objects, len = this._methods.length;
for (var i = 0; i < len; i++) { var m = methods[i]; m && m.call(objects[i], arg1, arg2); }
}
return true;
};
ListenerList.prototype.emit3 = function (arg1, arg2, arg3) {
if (this._count === 0) return false; // 0 listeners
if (typeof this._methods === 'function') {
this._methods.call(this._objects, arg1, arg2, arg3);
} else {
var methods = this._methods, objects = this._objects, len = this._methods.length;
for (var i = 0; i < len; i++) { var m = methods[i]; m && m.call(objects[i], arg1, arg2, arg3); }
}
return true;
};
ListenerList.prototype.emitN = function () {
if (this._count === 0) return false; // 0 listeners
if (typeof this._methods === 'function') {
this._methods.apply(this._objects, arguments);
} else {
var methods = this._methods, objects = this._objects, len = this._methods.length;
for (var i = 0; i < len; i++) { var m = methods[i]; m && m.apply(objects[i], arguments); }
}
return true;
};
//---
/* eslint no-console: 0 */
function throwOrConsole (msg, info) {
if (debugLevel >= EventEmitter.DEBUG_THROW) throw new Error(msg + info);
console.error(msg + info);
}
var DEFAULT_LISTENER = 0; // Using 0 is a bit faster for old API when in debug mode
function getObjectClassname (listener) {
if (!listener) return DEFAULT_LISTENER;
if (typeof listener === 'string') return listener;
var constr = listener.constructor;
return constr.name || constr.toString().split(/ |\(/, 2)[1];
}
function getAsText (emitter, eventId, listener) {
if (!listener || typeof listener === 'function') {
return getObjectClassname(emitter) + '.on(\'' + eventId + '\', fn)';
} else {
return getObjectClassname(emitter) + '.on(\'' + eventId + '\', fn, ' + getObjectClassname(listener) + ')';
}
}