emitter-listener
Version:
(Meta)observability for EventEmitters.
173 lines (152 loc) • 5.11 kB
JavaScript
;
var shimmer = require('shimmer');
var wrap = shimmer.wrap;
var unwrap = shimmer.unwrap;
// Default to complaining loudly when things don't go according to plan.
// dunderscores are boring
var SYMBOL = 'wrap@before';
// Sets a property on an object, preserving its enumerability.
// This function assumes that the property is already writable.
function defineProperty(obj, name, value) {
var enumerable = !!obj[name] && obj.propertyIsEnumerable(name);
Object.defineProperty(obj, name, {
configurable: true,
enumerable: enumerable,
writable: true,
value: value
});
}
function _process(self, listeners) {
var l = listeners.length;
for (var p = 0; p < l; p++) {
var listener = listeners[p];
// set up the listener so that onEmit can do whatever it needs
var before = self[SYMBOL];
if (typeof before === 'function') {
before(listener);
}
else if (Array.isArray(before)) {
var length = before.length;
for (var i = 0; i < length; i++) before[i](listener);
}
}
}
function _listeners(self, event) {
var listeners;
listeners = self._events && self._events[event];
if (!Array.isArray(listeners)) {
if (listeners) {
listeners = [listeners];
}
else {
listeners = [];
}
}
return listeners;
}
function _findAndProcess(self, event, before) {
var after = _listeners(self, event);
var unprocessed = after.filter(function(fn) { return before.indexOf(fn) === -1; });
if (unprocessed.length > 0) _process(self, unprocessed);
}
function _wrap(unwrapped, visit) {
if (!unwrapped) return;
var wrapped = unwrapped;
if (typeof unwrapped === 'function') {
wrapped = visit(unwrapped);
}
else if (Array.isArray(unwrapped)) {
wrapped = [];
for (var i = 0; i < unwrapped.length; i++) {
wrapped[i] = visit(unwrapped[i]);
}
}
return wrapped;
}
module.exports = function wrapEmitter(emitter, onAddListener, onEmit) {
if (!emitter || !emitter.on || !emitter.addListener ||
!emitter.removeListener || !emitter.emit) {
throw new Error("can only wrap real EEs");
}
if (!onAddListener) throw new Error("must have function to run on listener addition");
if (!onEmit) throw new Error("must have function to wrap listeners when emitting");
/* Attach a context to a listener, and make sure that this hook stays
* attached to the emitter forevermore.
*/
function adding(on) {
return function added(event, listener) {
var existing = _listeners(this, event).slice();
try {
var returned = on.call(this, event, listener);
_findAndProcess(this, event, existing);
return returned;
}
finally {
// old-style streaming overwrites .on and .addListener, so rewrap
if (!this.on.__wrapped) wrap(this, 'on', adding);
if (!this.addListener.__wrapped) wrap(this, 'addListener', adding);
}
};
}
function emitting(emit) {
return function emitted(event) {
if (!this._events || !this._events[event]) return emit.apply(this, arguments);
var unwrapped = this._events[event];
/* Ensure that if removeListener gets called, it's working with the
* unwrapped listeners.
*/
function remover(removeListener) {
return function removed() {
this._events[event] = unwrapped;
try {
return removeListener.apply(this, arguments);
}
finally {
unwrapped = this._events[event];
this._events[event] = _wrap(unwrapped, onEmit);
}
};
}
wrap(this, 'removeListener', remover);
try {
/* At emit time, ensure that whatever else is going on, removeListener will
* still work while at the same time running whatever hooks are necessary to
* make sure the listener is run in the correct context.
*/
this._events[event] = _wrap(unwrapped, onEmit);
return emit.apply(this, arguments);
}
finally {
/* Ensure that regardless of what happens when preparing and running the
* listeners, the status quo ante is restored before continuing.
*/
unwrap(this, 'removeListener');
this._events[event] = unwrapped;
}
};
}
// support multiple onAddListeners
if (!emitter[SYMBOL]) {
defineProperty(emitter, SYMBOL, onAddListener);
}
else if (typeof emitter[SYMBOL] === 'function') {
defineProperty(emitter, SYMBOL, [emitter[SYMBOL], onAddListener]);
}
else if (Array.isArray(emitter[SYMBOL])) {
emitter[SYMBOL].push(onAddListener);
}
// only wrap the core functions once
if (!emitter.__wrapped) {
wrap(emitter, 'addListener', adding);
wrap(emitter, 'on', adding);
wrap(emitter, 'emit', emitting);
defineProperty(emitter, '__unwrap', function () {
unwrap(emitter, 'addListener');
unwrap(emitter, 'on');
unwrap(emitter, 'emit');
delete emitter[SYMBOL];
delete emitter.__wrapped;
});
defineProperty(emitter, '__wrapped', true);
}
};