Skip to content

Commit

Permalink
events: add ability to prepend event listeners
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.

To address part of the issue, this adds a new optional
bitwise flag to the addListener/on/once methods that,
when set, causes the listener to be prepended.

Doc update and test case is included.

Fixes: nodejs#1817
  • Loading branch information
jasnell committed Apr 7, 2016
1 parent 7c9a691 commit 209ed51
Show file tree
Hide file tree
Showing 5 changed files with 90 additions and 21 deletions.
46 changes: 42 additions & 4 deletions doc/api/events.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -284,9 +284,9 @@ emitter.once('event', () => {
});
```

### emitter.addListener(eventName, listener)
### emitter.addListener(eventName, listener[, flags])

Alias for `emitter.on(eventName, listener)`.
Alias for `emitter.on(eventName, listener[, flags])`.

### emitter.emit(eventName[, arg1][, arg2][, ...])

Expand Down Expand Up @@ -338,7 +338,12 @@ console.log(util.inspect(server.listeners('connection')));
// Prints: [ [Function] ]
```

### emitter.on(eventName, listener)
### emitter.on(eventName, listener[, flags])

* `eventName` {string|Symbol} The name of the event.
* `listener` {Function} The callback function
* `flags` {Bitwise field} Optional flags
* `EventEmitter.F_PREPEND` = `0x1`

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

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

### emitter.once(eventName, listener)
By default, event listeners are invoked in the order they are added. The
`EventEmitter.F_PREPEND` flag can be used to add the event listener to the
beginning of the list of registered listeners.

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

### emitter.once(eventName, listener, flags)

* `eventName` {string|Symbol} The name of the event.
* `listener` {Function} The callback function
* `flags` {Bitwise field} Optional flags
* `EventEmitter.F_PREPEND` = `0x1`

Adds a **one time** `listener` function for the event named `eventName`. This
listener is invoked only the next time `eventName` is triggered, after which
Expand All @@ -368,6 +392,20 @@ 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
`EventEmitter.F_PREPEND` flag can be used to add the event listener to the
beginning of the list of registered listeners.

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

### emitter.removeAllListeners([eventName])

Removes all listeners, or those of the specified `eventName`.
Expand Down
4 changes: 2 additions & 2 deletions lib/_http_server.js
Original file line number Diff line number Diff line change
Expand Up @@ -577,8 +577,8 @@ function unconsume(parser, socket) {
}
}

function socketOnWrap(ev, fn) {
var res = net.Socket.prototype.on.call(this, ev, fn);
function socketOnWrap(ev, fn, flags) {
var res = net.Socket.prototype.on.call(this, ev, fn, flags);
if (!this.parser) {
this.on = net.Socket.prototype.on;
return res;
Expand Down
14 changes: 4 additions & 10 deletions lib/_stream_readable.js
Original file line number Diff line number Diff line change
Expand Up @@ -558,15 +558,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.
dest.on('error', onerror, EE.F_PREPEND);

// Both close and finish should trigger unpipe, but only once.
function onclose() {
Expand Down Expand Up @@ -669,8 +663,8 @@ Readable.prototype.unpipe = function(dest) {

// set up data events if they are asked for
// Ensure readable listeners eventually get something
Readable.prototype.on = function(ev, fn) {
var res = Stream.prototype.on.call(this, ev, fn);
Readable.prototype.on = function(ev, fn, flags) {
var res = Stream.prototype.on.call(this, ev, fn, flags);

// If listening to data, and it has not explicitly been paused,
// then call resume to start the flow of data on the next tick.
Expand Down
26 changes: 21 additions & 5 deletions lib/events.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ function EventEmitter() {
}
module.exports = EventEmitter;

Object.defineProperty(EventEmitter, 'F_PREPEND', {
configurable: false,
enumerable: true,
value: 0x1
});

// Backwards-compat with node 0.10.x
EventEmitter.EventEmitter = EventEmitter;

Expand Down Expand Up @@ -201,11 +207,16 @@ EventEmitter.prototype.emit = function emit(type) {
return true;
};

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

flags >>>= 0;
const prepend = (flags & EventEmitter.F_PREPEND) === EventEmitter.F_PREPEND;

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

Expand Down Expand Up @@ -234,10 +245,15 @@ EventEmitter.prototype.addListener = function addListener(type, listener) {
} 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
Expand All @@ -257,7 +273,7 @@ EventEmitter.prototype.addListener = function addListener(type, listener) {

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

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

Expand All @@ -273,7 +289,7 @@ EventEmitter.prototype.once = function once(type, listener) {
}

g.listener = listener;
this.on(type, g);
this.on(type, g, flags);

return this;
};
Expand Down
21 changes: 21 additions & 0 deletions test/parallel/test-event-emitter-prepend.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
'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.on('foo', () => {
assert.equal(m++, 1);
}, EventEmitter.F_PREPEND);
// This one comes first.
myEE.once('foo', () => {
assert.equal(m++, 0);
}, EventEmitter.F_PREPEND);
myEE.emit('foo');

0 comments on commit 209ed51

Please sign in to comment.