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 2 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 @@ -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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would throw if signal === undefined, btw, and it doesn’t look like that would be intentional

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 @@ -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
83 changes: 83 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,85 @@ 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);
}