| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent a79112b commit a68a67f
4 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1799,6 +1799,64 @@ const emitter = new EventEmitter(); | |||
| 1799 | 1799 | setMaxListeners(5, target, emitter); | |
| 1800 | 1800 | ``` | |
| 1801 | 1801 | ||
| 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 | + | ||
| 1802 | 1860 | ## Class: `events.EventEmitterAsyncResource extends EventEmitter` | |
| 1803 | 1861 | ||
| 1804 | 1862 | <!-- YAML | |
@@ -2520,6 +2578,7 @@ to the `EventTarget`. | |||
| 2520 | 2578 | [`EventTarget` error handling]: #eventtarget-error-handling | |
| 2521 | 2579 | [`Event` Web API]: https://dom.spec.whatwg.org/#event | |
| 2522 | 2580 | [`domain`]: domain.md | |
| 2581 | + [`e.stopImmediatePropagation()`]: #eventstopimmediatepropagation | ||
| 2523 | 2582 | [`emitter.listenerCount()`]: #emitterlistenercounteventname-listener | |
| 2524 | 2583 | [`emitter.removeListener()`]: #emitterremovelistenereventname-listener | |
| 2525 | 2584 | [`emitter.setMaxListeners(n)`]: #emittersetmaxlistenersn | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -48,6 +48,7 @@ const { | |||
| 48 | 48 | Symbol, | |
| 49 | 49 | SymbolFor, | |
| 50 | 50 | SymbolAsyncIterator, | |
| 51 | + SymbolDispose, | ||
| 51 | 52 | } = primordials; | |
| 52 | 53 | const kRejection = SymbolFor('nodejs.rejection'); | |
| 53 | 54 | ||
@@ -214,6 +215,7 @@ function EventEmitter(opts) { | |||
| 214 | 215 | EventEmitter.init.call(this, opts); | |
| 215 | 216 | } | |
| 216 | 217 | module.exports = EventEmitter; | |
| 218 | + module.exports.addAbortListener = addAbortListener; | ||
| 217 | 219 | module.exports.once = once; | |
| 218 | 220 | module.exports.on = on; | |
| 219 | 221 | module.exports.getEventListeners = getEventListeners; | |
@@ -1160,3 +1162,32 @@ function on(emitter, event, options = kEmptyObject) { | |||
| 1160 | 1162 | } | |
| 1161 | 1163 | return iterator; | |
| 1162 | 1164 | } | |
| 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 | + } | ||
| Original file line number | Diff line number | Diff 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 | + }); | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -267,6 +267,7 @@ const customTypesMap = { | |||
| 267 | 267 | 'Headers': 'https://developer.mozilla.org/en-US/docs/Web/API/Headers', | |
| 268 | 268 | 'Response': 'https://developer.mozilla.org/en-US/docs/Web/API/Response', | |
| 269 | 269 | 'Request': 'https://developer.mozilla.org/en-US/docs/Web/API/Request', | |
| 270 | + 'Disposable': 'https://tc39.es/proposal-explicit-resource-management/#sec-disposable-interface', | ||
| 270 | 271 | }; | |
| 271 | 272 | ||
| 272 | 273 | const arrayPart = /(?:\[])+$/; | |
| Back | FazBrowse Home | New Git URL |
0 commit comments