mirror of
https://github.com/nodejs/node.git
synced 2025-08-15 13:48:44 +02:00
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>
This commit is contained in:
parent
37a8179673
commit
883fc779b6
4 changed files with 179 additions and 4 deletions
|
@ -825,7 +825,7 @@ class MyClass extends EventEmitter {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## `events.once(emitter, name)`
|
## `events.once(emitter, name[, options])`
|
||||||
<!-- YAML
|
<!-- YAML
|
||||||
added:
|
added:
|
||||||
- v11.13.0
|
- v11.13.0
|
||||||
|
@ -834,6 +834,9 @@ added:
|
||||||
|
|
||||||
* `emitter` {EventEmitter}
|
* `emitter` {EventEmitter}
|
||||||
* `name` {string}
|
* `name` {string}
|
||||||
|
* `options` {Object}
|
||||||
|
* `signal` {AbortSignal} An {AbortSignal} that may be used to cancel waiting
|
||||||
|
for the event.
|
||||||
* Returns: {Promise}
|
* Returns: {Promise}
|
||||||
|
|
||||||
Creates a `Promise` that is fulfilled when the `EventEmitter` emits the given
|
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
|
// 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()`
|
### Awaiting multiple events emitted on `process.nextTick()`
|
||||||
|
|
||||||
There is an edge case worth noting when using the `events.once()` function
|
There is an edge case worth noting when using the `events.once()` function
|
||||||
|
|
|
@ -44,6 +44,7 @@ const kRejection = SymbolFor('nodejs.rejection');
|
||||||
let spliceOne;
|
let spliceOne;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
hideStackFrames,
|
||||||
kEnhanceStackBeforeInspector,
|
kEnhanceStackBeforeInspector,
|
||||||
codes
|
codes
|
||||||
} = require('internal/errors');
|
} = require('internal/errors');
|
||||||
|
@ -57,9 +58,20 @@ const {
|
||||||
inspect
|
inspect
|
||||||
} = require('internal/util/inspect');
|
} = require('internal/util/inspect');
|
||||||
|
|
||||||
|
const {
|
||||||
|
validateAbortSignal
|
||||||
|
} = require('internal/validators');
|
||||||
|
|
||||||
const kCapture = Symbol('kCapture');
|
const kCapture = Symbol('kCapture');
|
||||||
const kErrorMonitor = Symbol('events.errorMonitor');
|
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) {
|
function EventEmitter(opts) {
|
||||||
EventEmitter.init.call(this, opts);
|
EventEmitter.init.call(this, opts);
|
||||||
}
|
}
|
||||||
|
@ -621,22 +633,61 @@ function unwrapListeners(arr) {
|
||||||
return ret;
|
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) => {
|
return new Promise((resolve, reject) => {
|
||||||
const errorListener = (err) => {
|
const errorListener = (err) => {
|
||||||
emitter.removeListener(name, resolver);
|
emitter.removeListener(name, resolver);
|
||||||
|
if (signal != null) {
|
||||||
|
eventTargetAgnosticRemoveListener(
|
||||||
|
signal,
|
||||||
|
'abort',
|
||||||
|
abortListener,
|
||||||
|
{ once: true });
|
||||||
|
}
|
||||||
reject(err);
|
reject(err);
|
||||||
};
|
};
|
||||||
const resolver = (...args) => {
|
const resolver = (...args) => {
|
||||||
if (typeof emitter.removeListener === 'function') {
|
if (typeof emitter.removeListener === 'function') {
|
||||||
emitter.removeListener('error', errorListener);
|
emitter.removeListener('error', errorListener);
|
||||||
}
|
}
|
||||||
|
if (signal != null) {
|
||||||
|
eventTargetAgnosticRemoveListener(
|
||||||
|
signal,
|
||||||
|
'abort',
|
||||||
|
abortListener,
|
||||||
|
{ once: true });
|
||||||
|
}
|
||||||
resolve(args);
|
resolve(args);
|
||||||
};
|
};
|
||||||
eventTargetAgnosticAddListener(emitter, name, resolver, { once: true });
|
eventTargetAgnosticAddListener(emitter, name, resolver, { once: true });
|
||||||
if (name !== 'error') {
|
if (name !== 'error') {
|
||||||
addErrorHandlerIfEventEmitter(emitter, errorListener, { once: true });
|
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 });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -216,6 +216,15 @@ const validateCallback = hideStackFrames((callback) => {
|
||||||
throw new ERR_INVALID_CALLBACK(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 = {
|
module.exports = {
|
||||||
isInt32,
|
isInt32,
|
||||||
isUint32,
|
isUint32,
|
||||||
|
@ -234,4 +243,5 @@ module.exports = {
|
||||||
validateString,
|
validateString,
|
||||||
validateUint32,
|
validateUint32,
|
||||||
validateCallback,
|
validateCallback,
|
||||||
|
validateAbortSignal,
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,9 +1,14 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
// Flags: --expose-internals
|
// Flags: --expose-internals --no-warnings
|
||||||
|
|
||||||
const common = require('../common');
|
const common = require('../common');
|
||||||
const { once, EventEmitter } = require('events');
|
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');
|
const { EventTarget, Event } = require('internal/event_target');
|
||||||
|
|
||||||
async function onceAnEvent() {
|
async function onceAnEvent() {
|
||||||
|
@ -114,6 +119,81 @@ async function prioritizesEventEmitter() {
|
||||||
process.nextTick(() => ee.emit('foo'));
|
process.nextTick(() => ee.emit('foo'));
|
||||||
await once(ee, '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([
|
Promise.all([
|
||||||
onceAnEvent(),
|
onceAnEvent(),
|
||||||
onceAnEventWithTwoArgs(),
|
onceAnEventWithTwoArgs(),
|
||||||
|
@ -123,4 +203,10 @@ Promise.all([
|
||||||
onceWithEventTarget(),
|
onceWithEventTarget(),
|
||||||
onceWithEventTargetError(),
|
onceWithEventTargetError(),
|
||||||
prioritizesEventEmitter(),
|
prioritizesEventEmitter(),
|
||||||
|
abortSignalBefore(),
|
||||||
|
abortSignalAfter(),
|
||||||
|
abortSignalAfterEvent(),
|
||||||
|
eventTargetAbortSignalBefore(),
|
||||||
|
eventTargetAbortSignalAfter(),
|
||||||
|
eventTargetAbortSignalAfterEvent(),
|
||||||
]).then(common.mustCall());
|
]).then(common.mustCall());
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue