Skip to content

Commit

Permalink
events: support abortsignal in ee.on
Browse files Browse the repository at this point in the history
  • Loading branch information
benjamingr committed Oct 31, 2020
1 parent c0af8bd commit 866e7af
Show file tree
Hide file tree
Showing 3 changed files with 116 additions and 11 deletions.
28 changes: 24 additions & 4 deletions doc/api/events.md
Original file line number Diff line number Diff line change
Expand Up @@ -383,15 +383,17 @@ Installing a listener using this symbol does not change the behavior once an
`'error'` event is emitted, therefore the process will still crash if no
regular `'error'` listener is installed.

### `emitter.addListener(eventName, listener)`
### `emitter.addListener(eventName, listener [,options])`
<!-- YAML
added: v0.1.26
-->

* `eventName` {string|symbol}
* `listener` {Function}
* `options` {Object}
* `signal` {AbortSignal} Can be used to unsubscribe from the event

Alias for `emitter.on(eventName, listener)`.
Alias for `emitter.on(eventName, listener, options)`.

### `emitter.emit(eventName[, ...args])`
<!-- YAML
Expand Down Expand Up @@ -519,13 +521,15 @@ added: v10.0.0

Alias for [`emitter.removeListener()`][].

### `emitter.on(eventName, listener)`
### `emitter.on(eventName, listener[, options])`
<!-- YAML
added: v0.1.101
-->

* `eventName` {string|symbol} The name of the event.
* `listener` {Function} The callback function
* `options` {Object}
* `signal` {AbortSignal} Can be used to unsubscrie from the event
* Returns: {EventEmitter}

Adds the `listener` function to the end of the listeners array for the
Expand Down Expand Up @@ -556,6 +560,20 @@ myEE.emit('foo');
// a
```

An AbortSignal can be passed in to facilitate removal of the listener without
keeping a reference to the listener function:

```js
const myEE = new EventEmitter();
const ac = new AbortController();
myEE.on('foo', () => console.log('a'), { signal: ac.signal });
myEE.emit('foo');
// Prints:
// a
ac.abort();
myEE.emit('foo');
// Prints nothing, the listener was removed
```
### `emitter.once(eventName, listener)`
<!-- YAML
added: v0.3.0
Expand Down Expand Up @@ -590,13 +608,15 @@ myEE.emit('foo');
// a
```

### `emitter.prependListener(eventName, listener)`
### `emitter.prependListener(eventName, listener [,options])`
<!-- YAML
added: v6.0.0
-->

* `eventName` {string|symbol} The name of the event.
* `listener` {Function} The callback function
* `options` {Object}
* `signal` {AbortSignal} Can be used to unsubscrie from the event
* Returns: {EventEmitter}

Adds the `listener` function to the *beginning* of the listeners array for the
Expand Down
31 changes: 24 additions & 7 deletions lib/events.js
Original file line number Diff line number Diff line change
Expand Up @@ -352,13 +352,30 @@ EventEmitter.prototype.emit = function emit(type, ...args) {
return true;
};

function _addListener(target, type, listener, prepend) {
function _addListener(target, type, listener, prepend, options) {
let m;
let events;
let existing;

checkListener(listener);

if (options !== undefined) {
const signal = options ? options.signal : undefined;
validateAbortSignal(signal, 'options.signal');
if (signal.aborted) {
return target;
}
// The same listener can be added/removed multiple times
// the signal needs to remove the correct instance - so we wrap it
const originalListener = listener;
listener = function listener(...args) {
return originalListener.apply(this, args);
};
listener.listener = originalListener;
signal.addEventListener('abort', () => {
target.removeListener(type, listener);
}, { once: true });
}
events = target._events;
if (events === undefined) {
events = target._events = ObjectCreate(null);
Expand Down Expand Up @@ -414,15 +431,16 @@ function _addListener(target, type, listener, prepend) {
return target;
}

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

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

EventEmitter.prototype.prependListener =
function prependListener(type, listener) {
return _addListener(this, type, listener, true);
function prependListener(type, listener, options) {
return _addListener(this, type, listener, true, options);
};

function onceWrapper() {
Expand Down Expand Up @@ -506,7 +524,6 @@ EventEmitter.prototype.removeListener =
if (events.removeListener !== undefined)
this.emit('removeListener', type, listener);
}

return this;
};

Expand Down
68 changes: 68 additions & 0 deletions test/parallel/test-event-emitter-remove-listeners.js
Original file line number Diff line number Diff line change
Expand Up @@ -168,3 +168,71 @@ function listener2() {}
ee.removeListener('foo', listener1);
assert.strictEqual(ee._events.foo, listener2);
}
{
// AbortSignals
const ac = new AbortController();
const ee = new EventEmitter();
ee.on('foo', common.mustNotCall(), { signal: ac.signal });
ee.prependListener('foo', common.mustNotCall(), { signal: ac.signal });
ac.abort();
ee.emit('foo');
}
{
// AbortSignals - aborting before adding the listener
const ac = new AbortController();
const ee = new EventEmitter();
ac.abort();
ee.on('foo', common.mustNotCall(), { signal: ac.signal });
ee.prependListener('foo', common.mustNotCall(), { signal: ac.signal });
ee.emit('foo');
}
{
// AbortSignals - removing the correct listener
const ac = new AbortController();
const ee = new EventEmitter();
const mustCall = common.mustCall();
const mustNotCall = common.mustNotCall();
ee.on('foo', mustNotCall, { signal: ac.signal });
ee.on('foo', mustCall);
ac.abort();
ee.emit('foo');
}
{
// AbortSignals - removing the correct same listener
const ac = new AbortController();
const ee = new EventEmitter();
const arr = [];
const fn1 = (arg) => arr.push(arg);
const pushA = fn1.bind(null, 'A');
const pushB = fn1.bind(null, 'B');
ee.on('foo', pushA, { signal: ac.signal });
ee.on('foo', pushB);
ee.on('foo', pushA);
ac.abort();
ee.emit('foo');
assert.deepStrictEqual(arr, ['B', 'A']);
}
{
// AbortSignals - removing the correct same listener
const ac = new AbortController();
const ee = new EventEmitter();
const arr = [];
const fn1 = (arg) => arr.push(arg);
const pushA = fn1.bind(null, 'A');
const pushB = fn1.bind(null, 'B');
ee.on('foo', pushA);
ee.on('foo', pushB);
ee.on('foo', pushA, { signal: ac.signal });
ac.abort();
ee.emit('foo');
assert.deepStrictEqual(arr, ['A', 'B']);
}
{
// AbortSignals - removing with removeListener
const ac = new AbortController();
const ee = new EventEmitter();
const mustNotCall = common.mustNotCall();
ee.on('foo', mustNotCall, { signal: ac.signal });
ee.removeListener('foo', mustNotCall);
ee.emit('foo');
}

0 comments on commit 866e7af

Please sign in to comment.