events: allow safely adding listener to abortSignal · nodejs/node@a316808 · GitHub
Skip to content

Commit a316808

Browse files
atlowChemijuanarbol
authored andcommitted
events: allow safely adding listener to abortSignal
PR-URL: #48596 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Moshe Atlow <moshe@atlow.co.il> Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com> Reviewed-By: Robert Nagy <ronagy@icloud.com>
1 parent dfa0aee commit a316808

4 files changed

Lines changed: 146 additions & 0 deletions

File tree

doc/api/events.md

Lines changed: 59 additions & 0 deletions

lib/events.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ const {
4848
Symbol,
4949
SymbolFor,
5050
SymbolAsyncIterator,
51+
SymbolDispose,
5152
} = primordials;
5253
const kRejection = SymbolFor('nodejs.rejection');
5354

@@ -218,6 +219,7 @@ function EventEmitter(opts) {
218219
EventEmitter.init.call(this, opts);
219220
}
220221
module.exports = EventEmitter;
222+
module.exports.addAbortListener = addAbortListener;
221223
module.exports.once = once;
222224
module.exports.on = on;
223225
module.exports.getEventListeners = getEventListeners;
@@ -1212,3 +1214,32 @@ function listenersController() {
12121214
},
12131215
};
12141216
}
1217+
1218+
let queueMicrotask;
1219+
1220+
function addAbortListener(signal, listener) {
1221+
if (signal === undefined) {
1222+
throw new ERR_INVALID_ARG_TYPE('signal', 'AbortSignal', signal);
1223+
}
1224+
validateAbortSignal(signal, 'signal');
1225+
validateFunction(listener, 'listener');
1226+
1227+
let removeEventListener;
1228+
if (signal.aborted) {
1229+
queueMicrotask ??= require('internal/process/task_queues').queueMicrotask;
1230+
queueMicrotask(() => listener());
1231+
} else {
1232+
kResistStopPropagation ??= require('internal/event_target').kResistStopPropagation;
1233+
// TODO(atlowChemi) add { subscription: true } and return directly
1234+
signal.addEventListener('abort', listener, { __proto__: null, once: true, [kResistStopPropagation]: true });
1235+
removeEventListener = () => {
1236+
signal.removeEventListener('abort', listener);
1237+
};
1238+
}
1239+
return {
1240+
__proto__: null,
1241+
[SymbolDispose]() {
1242+
removeEventListener?.();
1243+
},
1244+
};
1245+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import * as common from '../common/index.mjs';
2+
import * as events from 'node:events';
3+
import * as assert from 'node:assert';
4+
import { describe, it } from 'node:test';
5+
6+
describe('events.addAbortListener', () => {
7+
it('should throw if signal not provided', () => {
8+
assert.throws(() => events.addAbortListener(), { code: 'ERR_INVALID_ARG_TYPE' });
9+
});
10+
11+
it('should throw if provided signal is invalid', () => {
12+
assert.throws(() => events.addAbortListener(undefined), { code: 'ERR_INVALID_ARG_TYPE' });
13+
assert.throws(() => events.addAbortListener(null), { code: 'ERR_INVALID_ARG_TYPE' });
14+
assert.throws(() => events.addAbortListener({}), { code: 'ERR_INVALID_ARG_TYPE' });
15+
});
16+
17+
it('should throw if listener is not a function', () => {
18+
const { signal } = new AbortController();
19+
assert.throws(() => events.addAbortListener(signal), { code: 'ERR_INVALID_ARG_TYPE' });
20+
assert.throws(() => events.addAbortListener(signal, {}), { code: 'ERR_INVALID_ARG_TYPE' });
21+
assert.throws(() => events.addAbortListener(signal, undefined), { code: 'ERR_INVALID_ARG_TYPE' });
22+
});
23+
24+
it('should return a Disposable', () => {
25+
const { signal } = new AbortController();
26+
const disposable = events.addAbortListener(signal, common.mustNotCall());
27+
28+
assert.strictEqual(typeof disposable[Symbol.dispose], 'function');
29+
});
30+
31+
it('should execute the listener immediately for aborted runners', () => {
32+
const disposable = events.addAbortListener(AbortSignal.abort(), common.mustCall());
33+
assert.strictEqual(typeof disposable[Symbol.dispose], 'function');
34+
});
35+
36+
it('should execute the listener even when event propagation stopped', () => {
37+
const controller = new AbortController();
38+
const { signal } = controller;
39+
40+
signal.addEventListener('abort', (e) => e.stopImmediatePropagation());
41+
events.addAbortListener(
42+
signal,
43+
common.mustCall((e) => assert.strictEqual(e.target, signal)),
44+
);
45+
46+
controller.abort();
47+
});
48+
49+
it('should remove event listeners when disposed', () => {
50+
const controller = new AbortController();
51+
const disposable = events.addAbortListener(controller.signal, common.mustNotCall());
52+
disposable[Symbol.dispose]();
53+
controller.abort();
54+
});
55+
});

tools/doc/type-parser.mjs

Lines changed: 1 addition & 0 deletions

0 commit comments

Comments
 (0)