Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

events: support abortsignal in ee.on #35877

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 24 additions & 4 deletions doc/api/events.md
Original file line number Diff line number Diff line change
Expand Up @@ -310,16 +310,18 @@ changes:

The `'removeListener'` event is emitted _after_ the `listener` is removed.

### `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])`

Expand Down Expand Up @@ -453,14 +455,16 @@ 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 @@ -491,6 +495,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
Expand Down Expand Up @@ -526,14 +544,16 @@ 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 @@ -545,13 +545,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 && 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);
};
benjamingr marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -613,9 +630,10 @@ function _addListener(target, type, listener, prepend) {
* @param {Function} listener
* @returns {EventEmitter}
*/
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;

Expand All @@ -627,8 +645,8 @@ EventEmitter.prototype.on = EventEmitter.prototype.addListener;
* @returns {EventEmitter}
*/
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 @@ -730,7 +748,6 @@ EventEmitter.prototype.removeListener =
if (events.removeListener !== undefined)
this.emit('removeListener', type, listener);
}

return this;
};

Expand Down
94 changes: 94 additions & 0 deletions test/parallel/test-event-emitter-remove-listeners.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
const common = require('../common');
const assert = require('assert');
const EventEmitter = require('events');
const { strictEqual } = require('assert');

function listener1() {}

Expand Down Expand Up @@ -168,3 +169,96 @@ 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');
}
{
// AbortSignals - checking rawListeners
const ac = new AbortController();
const ee = new EventEmitter();
const fn = common.mustCall();
ee.on('foo', fn, { signal: ac.signal });
const listeners = ee.listeners('foo');
strictEqual(listeners.length, 1);
strictEqual(listeners[0], fn);
ee.emit('foo');
const rawListeners = ee.rawListeners('foo');
strictEqual(rawListeners.length, 1);
strictEqual(rawListeners[0].listener, fn);
}
{
// AbortSignals - already aborted
const ac = new AbortController();
const ee = new EventEmitter();
const fn = common.mustNotCall();
ac.abort();
ee.on('foo', fn, { signal: ac.signal });
const listeners = ee.listeners('foo');
strictEqual(listeners.length, 0);
ee.emit('foo');
}