summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJames M Snell <jasnell@gmail.com>2020-08-24 13:11:23 -0700
committerNode.js GitHub Bot <github-bot@iojs.org>2020-08-31 15:09:57 +0000
commit883fc779b637732b18e2d0e6b1f386cebb37e93c (patch)
tree3451371d49699a9ce78550851a535c4bce435f42
parent37a8179673590af10b9e8e413388adffc21ba713 (diff)
downloadnode-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.md30
-rw-r--r--lib/events.js53
-rw-r--r--lib/internal/validators.js10
-rw-r--r--test/parallel/test-events-once.js90
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());