Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit a68a67f

Browse filesBrowse files
atlowChemiruyadorno
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 a79112b commit a68a67f
Copy full SHA for a68a67f

File tree

Expand file treeCollapse file tree

4 files changed

+146
-0
lines changed
Open diff view settings
Filter options
Expand file treeCollapse file tree

4 files changed

+146
-0
lines changed
Open diff view settings
Collapse file

‎doc/api/events.md‎

Copy file name to clipboardExpand all lines: doc/api/events.md
+59Lines changed: 59 additions & 0 deletions
  • Display the source diff
  • Display the rich diff
Original file line numberDiff line numberDiff line change
@@ -1799,6 +1799,64 @@ const emitter = new EventEmitter();
17991799
setMaxListeners(5, target, emitter);
18001800
```
18011801

1802+
## `events.addAbortListener(signal, resource)`
1803+
1804+
<!-- YAML
1805+
added: REPLACEME
1806+
-->
1807+
1808+
> Stability: 1 - Experimental
1809+
1810+
* `signal` {AbortSignal}
1811+
* `listener` {Function|EventListener}
1812+
* Returns: {Disposable} that removes the `abort` listener.
1813+
1814+
Listens once to the `abort` event on the provided `signal`.
1815+
1816+
Listening to the `abort` event on abort signals is unsafe and may
1817+
lead to resource leaks since another third party with the signal can
1818+
call [`e.stopImmediatePropagation()`][]. Unfortunately Node.js cannot change
1819+
this since it would violate the web standard. Additionally, the original
1820+
API makes it easy to forget to remove listeners.
1821+
1822+
This API allows safely using `AbortSignal`s in Node.js APIs by solving these
1823+
two issues by listening to the event such that `stopImmediatePropagation` does
1824+
not prevent the listener from running.
1825+
1826+
Returns a disposable so that it may be unsubscribed from more easily.
1827+
1828+
```cjs
1829+
const { addAbortListener } = require('node:events');
1830+
1831+
function example(signal) {
1832+
let disposable;
1833+
try {
1834+
signal.addEventListener('abort', (e) => e.stopImmediatePropagation());
1835+
disposable = addAbortListener(signal, (e) => {
1836+
// Do something when signal is aborted.
1837+
});
1838+
} finally {
1839+
disposable?.[Symbol.dispose]();
1840+
}
1841+
}
1842+
```
1843+
1844+
```mjs
1845+
import { addAbortListener } from 'node:events';
1846+
1847+
function example(signal) {
1848+
let disposable;
1849+
try {
1850+
signal.addEventListener('abort', (e) => e.stopImmediatePropagation());
1851+
disposable = addAbortListener(signal, (e) => {
1852+
// Do something when signal is aborted.
1853+
});
1854+
} finally {
1855+
disposable?.[Symbol.dispose]();
1856+
}
1857+
}
1858+
```
1859+
18021860
## Class: `events.EventEmitterAsyncResource extends EventEmitter`
18031861
18041862
<!-- YAML
@@ -2520,6 +2578,7 @@ to the `EventTarget`.
25202578
[`EventTarget` error handling]: #eventtarget-error-handling
25212579
[`Event` Web API]: https://dom.spec.whatwg.org/#event
25222580
[`domain`]: domain.md
2581+
[`e.stopImmediatePropagation()`]: #eventstopimmediatepropagation
25232582
[`emitter.listenerCount()`]: #emitterlistenercounteventname-listener
25242583
[`emitter.removeListener()`]: #emitterremovelistenereventname-listener
25252584
[`emitter.setMaxListeners(n)`]: #emittersetmaxlistenersn
Collapse file

‎lib/events.js‎

Copy file name to clipboardExpand all lines: lib/events.js
+31Lines 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

@@ -214,6 +215,7 @@ function EventEmitter(opts) {
214215
EventEmitter.init.call(this, opts);
215216
}
216217
module.exports = EventEmitter;
218+
module.exports.addAbortListener = addAbortListener;
217219
module.exports.once = once;
218220
module.exports.on = on;
219221
module.exports.getEventListeners = getEventListeners;
@@ -1160,3 +1162,32 @@ function on(emitter, event, options = kEmptyObject) {
11601162
}
11611163
return iterator;
11621164
}
1165+
1166+
let queueMicrotask;
1167+
1168+
function addAbortListener(signal, listener) {
1169+
if (signal === undefined) {
1170+
throw new ERR_INVALID_ARG_TYPE('signal', 'AbortSignal', signal);
1171+
}
1172+
validateAbortSignal(signal, 'signal');
1173+
validateFunction(listener, 'listener');
1174+
1175+
let removeEventListener;
1176+
if (signal.aborted) {
1177+
queueMicrotask ??= require('internal/process/task_queues').queueMicrotask;
1178+
queueMicrotask(() => listener());
1179+
} else {
1180+
kResistStopPropagation ??= require('internal/event_target').kResistStopPropagation;
1181+
// TODO(atlowChemi) add { subscription: true } and return directly
1182+
signal.addEventListener('abort', listener, { __proto__: null, once: true, [kResistStopPropagation]: true });
1183+
removeEventListener = () => {
1184+
signal.removeEventListener('abort', listener);
1185+
};
1186+
}
1187+
return {
1188+
__proto__: null,
1189+
[SymbolDispose]() {
1190+
removeEventListener?.();
1191+
},
1192+
};
1193+
}
Collapse file
+55Lines 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+
});
Collapse file

‎tools/doc/type-parser.mjs‎

Copy file name to clipboardExpand all lines: tools/doc/type-parser.mjs
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,7 @@ const customTypesMap = {
267267
'Headers': 'https://developer.mozilla.org/en-US/docs/Web/API/Headers',
268268
'Response': 'https://developer.mozilla.org/en-US/docs/Web/API/Response',
269269
'Request': 'https://developer.mozilla.org/en-US/docs/Web/API/Request',
270+
'Disposable': 'https://tc39.es/proposal-explicit-resource-management/#sec-disposable-interface',
270271
};
271272

272273
const arrayPart = /(?:\[])+$/;

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.