diff options
author | James M Snell <jasnell@gmail.com> | 2020-08-24 13:11:23 -0700 |
---|---|---|
committer | Node.js GitHub Bot <github-bot@iojs.org> | 2020-08-31 15:09:57 +0000 |
commit | 883fc779b637732b18e2d0e6b1f386cebb37e93c (patch) | |
tree | 3451371d49699a9ce78550851a535c4bce435f42 | |
parent | 37a8179673590af10b9e8e413388adffc21ba713 (diff) | |
download | node-new-883fc779b637732b18e2d0e6b1f386cebb37e93c.tar.gz |
events: allow use of AbortController with once
Allows an AbortSignal to be passed in to events.once() to cancel
waiting on an event.
Signed-off-by: James M Snell <jasnell@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/34911
Reviewed-By: Denys Otrishko <shishugi@gmail.com>
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
-rw-r--r-- | doc/api/events.md | 30 | ||||
-rw-r--r-- | lib/events.js | 53 | ||||
-rw-r--r-- | lib/internal/validators.js | 10 | ||||
-rw-r--r-- | test/parallel/test-events-once.js | 90 |
4 files changed, 179 insertions, 4 deletions
diff --git a/doc/api/events.md b/doc/api/events.md index 3004d5bbb3..7c756e6df9 100644 --- a/doc/api/events.md +++ b/doc/api/events.md @@ -825,7 +825,7 @@ class MyClass extends EventEmitter { } ``` -## `events.once(emitter, name)` +## `events.once(emitter, name[, options])` <!-- YAML added: - v11.13.0 @@ -834,6 +834,9 @@ added: * `emitter` {EventEmitter} * `name` {string} +* `options` {Object} + * `signal` {AbortSignal} An {AbortSignal} that may be used to cancel waiting + for the event. * Returns: {Promise} Creates a `Promise` that is fulfilled when the `EventEmitter` emits the given @@ -892,6 +895,31 @@ ee.emit('error', new Error('boom')); // Prints: ok boom ``` +An {AbortSignal} may be used to cancel waiting for the event early: + +```js +const { EventEmitter, once } = require('events'); + +const ee = new EventEmitter(); +const ac = new AbortController(); + +async function foo(emitter, event, signal) { + try { + await once(emitter, event, { signal }); + console.log('event emitted!'); + } catch (error) { + if (error.name === 'AbortError') { + console.error('Waiting for the event was canceled!'); + } else { + console.error('There was an error', error.message); + } + } +} + +foo(ee, 'foo', ac.signal); +ac.abort(); // Abort waiting for the event +``` + ### Awaiting multiple events emitted on `process.nextTick()` There is an edge case worth noting when using the `events.once()` function diff --git a/lib/events.js b/lib/events.js index 48341c0b20..270588fcbc 100644 --- a/lib/events.js +++ b/lib/events.js @@ -44,6 +44,7 @@ const kRejection = SymbolFor('nodejs.rejection'); let spliceOne; const { + hideStackFrames, kEnhanceStackBeforeInspector, codes } = require('internal/errors'); @@ -57,9 +58,20 @@ const { inspect } = require('internal/util/inspect'); +const { + validateAbortSignal +} = require('internal/validators'); + const kCapture = Symbol('kCapture'); const kErrorMonitor = Symbol('events.errorMonitor'); +let DOMException; +const lazyDOMException = hideStackFrames((message, name) => { + if (DOMException === undefined) + DOMException = internalBinding('messaging').DOMException; + return new DOMException(message, name); +}); + function EventEmitter(opts) { EventEmitter.init.call(this, opts); } @@ -621,22 +633,61 @@ function unwrapListeners(arr) { return ret; } -function once(emitter, name) { +async function once(emitter, name, options = {}) { + const signal = options ? options.signal : undefined; + validateAbortSignal(signal, 'options.signal'); + if (signal && signal.aborted) + throw lazyDOMException('The operation was aborted', 'AbortError'); return new Promise((resolve, reject) => { const errorListener = (err) => { emitter.removeListener(name, resolver); + if (signal != null) { + eventTargetAgnosticRemoveListener( + signal, + 'abort', + abortListener, + { once: true }); + } reject(err); }; const resolver = (...args) => { if (typeof emitter.removeListener === 'function') { emitter.removeListener('error', errorListener); } + if (signal != null) { + eventTargetAgnosticRemoveListener( + signal, + 'abort', + abortListener, + { once: true }); + } resolve(args); }; eventTargetAgnosticAddListener(emitter, name, resolver, { once: true }); if (name !== 'error') { addErrorHandlerIfEventEmitter(emitter, errorListener, { once: true }); } + function abortListener() { + if (typeof emitter.removeListener === 'function') { + emitter.removeListener(name, resolver); + emitter.removeListener('error', errorListener); + } else { + eventTargetAgnosticRemoveListener( + emitter, + name, + resolver, + { once: true }); + eventTargetAgnosticRemoveListener( + emitter, + 'error', + errorListener, + { once: true }); + } + reject(lazyDOMException('The operation was aborted', 'AbortError')); + } + if (signal != null) { + signal.addEventListener('abort', abortListener, { once: true }); + } }); } diff --git a/lib/internal/validators.js b/lib/internal/validators.js index 71726f7005..49bec77b3a 100644 --- a/lib/internal/validators.js +++ b/lib/internal/validators.js @@ -216,6 +216,15 @@ const validateCallback = hideStackFrames((callback) => { throw new ERR_INVALID_CALLBACK(callback); }); +const validateAbortSignal = hideStackFrames((signal, name) => { + if (signal !== undefined && + (signal === null || + typeof signal !== 'object' || + !('aborted' in signal))) { + throw new ERR_INVALID_ARG_TYPE(name, 'AbortSignal', signal); + } +}); + module.exports = { isInt32, isUint32, @@ -234,4 +243,5 @@ module.exports = { validateString, validateUint32, validateCallback, + validateAbortSignal, }; diff --git a/test/parallel/test-events-once.js b/test/parallel/test-events-once.js index 658a9964be..be3ed794e1 100644 --- a/test/parallel/test-events-once.js +++ b/test/parallel/test-events-once.js @@ -1,9 +1,14 @@ 'use strict'; -// Flags: --expose-internals +// Flags: --expose-internals --no-warnings const common = require('../common'); const { once, EventEmitter } = require('events'); -const { strictEqual, deepStrictEqual, fail } = require('assert'); +const { + strictEqual, + deepStrictEqual, + fail, + rejects, +} = require('assert'); const { EventTarget, Event } = require('internal/event_target'); async function onceAnEvent() { @@ -114,6 +119,81 @@ async function prioritizesEventEmitter() { process.nextTick(() => ee.emit('foo')); await once(ee, 'foo'); } + +async function abortSignalBefore() { + const ee = new EventEmitter(); + const ac = new AbortController(); + ee.on('error', common.mustNotCall()); + ac.abort(); + + await Promise.all([1, {}, 'hi', null, false].map((signal) => { + return rejects(once(ee, 'foo', { signal }), { + code: 'ERR_INVALID_ARG_TYPE' + }); + })); + + return rejects(once(ee, 'foo', { signal: ac.signal }), { + name: 'AbortError' + }); +} + +async function abortSignalAfter() { + const ee = new EventEmitter(); + const ac = new AbortController(); + ee.on('error', common.mustNotCall()); + const r = rejects(once(ee, 'foo', { signal: ac.signal }), { + name: 'AbortError' + }); + process.nextTick(() => ac.abort()); + return r; +} + +async function abortSignalAfterEvent() { + const ee = new EventEmitter(); + const ac = new AbortController(); + process.nextTick(() => { + ee.emit('foo'); + ac.abort(); + }); + await once(ee, 'foo', { signal: ac.signal }); +} + +async function eventTargetAbortSignalBefore() { + const et = new EventTarget(); + const ac = new AbortController(); + ac.abort(); + + await Promise.all([1, {}, 'hi', null, false].map((signal) => { + return rejects(once(et, 'foo', { signal }), { + code: 'ERR_INVALID_ARG_TYPE' + }); + })); + + return rejects(once(et, 'foo', { signal: ac.signal }), { + name: 'AbortError' + }); +} + +async function eventTargetAbortSignalAfter() { + const et = new EventTarget(); + const ac = new AbortController(); + const r = rejects(once(et, 'foo', { signal: ac.signal }), { + name: 'AbortError' + }); + process.nextTick(() => ac.abort()); + return r; +} + +async function eventTargetAbortSignalAfterEvent() { + const et = new EventTarget(); + const ac = new AbortController(); + process.nextTick(() => { + et.dispatchEvent(new Event('foo')); + ac.abort(); + }); + await once(et, 'foo', { signal: ac.signal }); +} + Promise.all([ onceAnEvent(), onceAnEventWithTwoArgs(), @@ -123,4 +203,10 @@ Promise.all([ onceWithEventTarget(), onceWithEventTargetError(), prioritizesEventEmitter(), + abortSignalBefore(), + abortSignalAfter(), + abortSignalAfterEvent(), + eventTargetAbortSignalBefore(), + eventTargetAbortSignalAfter(), + eventTargetAbortSignalAfterEvent(), ]).then(common.mustCall()); |