Skip to content

Commit

Permalink
events: add prependListener() and prependOnceListener()
Browse files Browse the repository at this point in the history
A handful of modules (including readable-streams) make
inappropriate use of the internal _events property. One
such use is to prepend an event listener to the front
of the array of listeners.

This adds EE.prototype.prependListener() and
EE.prototype.prependOnceListener() methods to add handlers
to the *front* of the listener array.

Doc update and test case is included.

Fixes: #1817
PR-URL: #6032
Reviewed-By: Сковорода Никита Андреевич <chalkerx@gmail.com>
Reviewed-By: Brian White <mscdex@mscdex.net>
  • Loading branch information
jasnell committed Apr 22, 2016
1 parent f85412d commit 0e7d57a
Show file tree
Hide file tree
Showing 4 changed files with 174 additions and 29 deletions.
70 changes: 70 additions & 0 deletions doc/api/events.md
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,9 @@ console.log(util.inspect(server.listeners('connection')));

### emitter.on(eventName, listener)

* `eventName` {string|Symbol} The name of the event.
* `listener` {Function} The callback function

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`
Expand All @@ -354,8 +357,25 @@ server.on('connection', (stream) => {

Returns a reference to the `EventEmitter` so calls can be chained.

By default, event listeners are invoked in the order they are added. The
`emitter.prependListener()` method can be used as an alternative to add the
event listener to the beginning of the listeners array.

```js
const myEE = new EventEmitter();
myEE.on('foo', () => console.log('a'));
myEE.prependListener('foo', () => console.log('b'));
myEE.emit('foo');
// Prints:
// b
// a
```

### emitter.once(eventName, listener)

* `eventName` {string|Symbol} The name of the event.
* `listener` {Function} The callback function

Adds a **one time** `listener` function for the event named `eventName`. This
listener is invoked only the next time `eventName` is triggered, after which
it is removed.
Expand All @@ -368,6 +388,56 @@ server.once('connection', (stream) => {

Returns a reference to the `EventEmitter` so calls can be chained.

By default, event listeners are invoked in the order they are added. The
`emitter.prependOnceListener()` method can be used as an alternative to add the
event listener to the beginning of the listeners array.

```js
const myEE = new EventEmitter();
myEE.once('foo', () => console.log('a'));
myEE.prependOnceListener('foo', () => console.log('b'));
myEE.emit('foo');
// Prints:
// b
// a
```

### emitter.prependListener(eventName, listener)

* `eventName` {string|Symbol} The name of the event.
* `listener` {Function} The callback function

Adds the `listener` function to the *beginning* 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.

```js
server.prependListener('connection', (stream) => {
console.log('someone connected!');
});
```

Returns a reference to the `EventEmitter` so calls can be chained.

### emitter.prependOnceListener(eventName, listener)

* `eventName` {string|Symbol} The name of the event.
* `listener` {Function} The callback function

Adds a **one time** `listener` function for the event named `eventName` to the
*beginning* of the listeners array. This listener is invoked only the next time
`eventName` is triggered, after which it is removed.

```js
server.prependOnceListener('connection', (stream) => {
console.log('Ah, we have our first user!');
});
```

Returns a reference to the `EventEmitter` so calls can be chained.

### emitter.removeAllListeners([eventName])

Removes all listeners, or those of the specified `eventName`.
Expand Down
29 changes: 21 additions & 8 deletions lib/_stream_readable.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,25 @@ var StringDecoder;

util.inherits(Readable, Stream);

const hasPrependListener = typeof EE.prototype.prependListener === 'function';

function prependListener(emitter, event, fn) {
if (hasPrependListener)
return emitter.prependListener(event, fn);

// This is a brutally ugly hack to make sure that our error handler
// is attached before any userland ones. NEVER DO THIS. This is here
// only because this code needs to continue to work with older versions
// of Node.js that do not include the prependListener() method. The goal
// is to eventually remove this hack.
if (!emitter._events || !emitter._events[event])
emitter.on(event, fn);
else if (Array.isArray(emitter._events[event]))
emitter._events[event].unshift(fn);
else
emitter._events[event] = [fn, emitter._events[event]];
}

function ReadableState(options, stream) {
options = options || {};

Expand Down Expand Up @@ -558,15 +577,9 @@ Readable.prototype.pipe = function(dest, pipeOpts) {
if (EE.listenerCount(dest, 'error') === 0)
dest.emit('error', er);
}
// This is a brutally ugly hack to make sure that our error handler
// is attached before any userland ones. NEVER DO THIS.
if (!dest._events || !dest._events.error)
dest.on('error', onerror);
else if (Array.isArray(dest._events.error))
dest._events.error.unshift(onerror);
else
dest._events.error = [onerror, dest._events.error];

// Make sure our error handler is attached before userland ones.
prependListener(dest, 'error', onerror);

// Both close and finish should trigger unpipe, but only once.
function onclose() {
Expand Down
63 changes: 42 additions & 21 deletions lib/events.js
Original file line number Diff line number Diff line change
Expand Up @@ -207,48 +207,53 @@ EventEmitter.prototype.emit = function emit(type) {
return true;
};

EventEmitter.prototype.addListener = function addListener(type, listener) {
function _addListener(target, type, listener, prepend) {
var m;
var events;
var existing;

if (typeof listener !== 'function')
throw new TypeError('"listener" argument must be a function');

events = this._events;
events = target._events;
if (!events) {
events = this._events = new EventHandlers();
this._eventsCount = 0;
events = target._events = new EventHandlers();
target._eventsCount = 0;
} else {
// To avoid recursion in the case that type === "newListener"! Before
// adding it to the listeners, first emit "newListener".
if (events.newListener) {
this.emit('newListener', type,
listener.listener ? listener.listener : listener);
target.emit('newListener', type,
listener.listener ? listener.listener : listener);

// Re-assign `events` because a newListener handler could have caused the
// this._events to be assigned to a new object
events = this._events;
events = target._events;
}
existing = events[type];
}

if (!existing) {
// Optimize the case of one listener. Don't need the extra array object.
existing = events[type] = listener;
++this._eventsCount;
++target._eventsCount;
} else {
if (typeof existing === 'function') {
// Adding the second element, need to change to array.
existing = events[type] = [existing, listener];
existing = events[type] = prepend ? [listener, existing] :
[existing, listener];
} else {
// If we've already got an array, just append.
existing.push(listener);
if (prepend) {
existing.unshift(listener);
} else {
existing.push(listener);
}
}

// Check for listener leak
if (!existing.warned) {
m = $getMaxListeners(this);
m = $getMaxListeners(target);
if (m && m > 0 && existing.length > m) {
existing.warned = true;
process.emitWarning('Possible EventEmitter memory leak detected. ' +
Expand All @@ -258,32 +263,48 @@ EventEmitter.prototype.addListener = function addListener(type, listener) {
}
}

return this;
return target;
}

EventEmitter.prototype.addListener = function addListener(type, listener) {
return _addListener(this, type, listener, false);
};

EventEmitter.prototype.on = EventEmitter.prototype.addListener;

EventEmitter.prototype.once = function once(type, listener) {
if (typeof listener !== 'function')
throw new TypeError('"listener" argument must be a function');
EventEmitter.prototype.prependListener =
function prependListener(type, listener) {
return _addListener(this, type, listener, true);
};

function _onceWrap(target, type, listener) {
var fired = false;

function g() {
this.removeListener(type, g);

target.removeListener(type, g);
if (!fired) {
fired = true;
listener.apply(this, arguments);
listener.apply(target, arguments);
}
}

g.listener = listener;
this.on(type, g);
return g;
}

EventEmitter.prototype.once = function once(type, listener) {
if (typeof listener !== 'function')
throw new TypeError('"listener" argument must be a function');
this.on(type, _onceWrap(this, type, listener));
return this;
};

EventEmitter.prototype.prependOnceListener =
function prependOnceListener(type, listener) {
if (typeof listener !== 'function')
throw new TypeError('"listener" argument must be a function');
this.prependListener(type, _onceWrap(this, type, listener));
return this;
};

// emits a 'removeListener' event iff the listener was removed
EventEmitter.prototype.removeListener =
function removeListener(type, listener) {
Expand Down
41 changes: 41 additions & 0 deletions test/parallel/test-event-emitter-prepend.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
'use strict';

const common = require('../common');
const EventEmitter = require('events');
const assert = require('assert');

const myEE = new EventEmitter();
var m = 0;
// This one comes last.
myEE.on('foo', common.mustCall(() => assert.equal(m, 2)));

// This one comes second.
myEE.prependListener('foo', common.mustCall(() => assert.equal(m++, 1)));

// This one comes first.
myEE.prependOnceListener('foo', common.mustCall(() => assert.equal(m++, 0)));

myEE.emit('foo');


// Test fallback if prependListener is undefined.
const stream = require('stream');
const util = require('util');

delete EventEmitter.prototype.prependListener;

function Writable() {
this.writable = true;
stream.Stream.call(this);
}
util.inherits(Writable, stream.Stream);

function Readable() {
this.readable = true;
stream.Stream.call(this);
}
util.inherits(Readable, stream.Stream);

const w = new Writable();
const r = new Readable();
r.pipe(w);

0 comments on commit 0e7d57a

Please sign in to comment.