events: allow use of AbortController with on
Signed-off-by: James M Snell <jasnell@gmail.com> PR-URL: https://github.com/nodejs/node/pull/34912 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com> Reviewed-By: Denys Otrishko <shishugi@gmail.com>
This commit is contained in:
parent
42a793eba3
commit
df1023bb22
@ -1000,7 +1000,7 @@ Value: `Symbol.for('nodejs.rejection')`
|
|||||||
|
|
||||||
See how to write a custom [rejection handler][rejection].
|
See how to write a custom [rejection handler][rejection].
|
||||||
|
|
||||||
## `events.on(emitter, eventName)`
|
## `events.on(emitter, eventName[, options])`
|
||||||
<!-- YAML
|
<!-- YAML
|
||||||
added:
|
added:
|
||||||
- v13.6.0
|
- v13.6.0
|
||||||
@ -1009,6 +1009,9 @@ added:
|
|||||||
|
|
||||||
* `emitter` {EventEmitter}
|
* `emitter` {EventEmitter}
|
||||||
* `eventName` {string|symbol} The name of the event being listened for
|
* `eventName` {string|symbol} The name of the event being listened for
|
||||||
|
* `options` {Object}
|
||||||
|
* `signal` {AbortSignal} An {AbortSignal} that can be used to cancel awaiting
|
||||||
|
events.
|
||||||
* Returns: {AsyncIterator} that iterates `eventName` events emitted by the `emitter`
|
* Returns: {AsyncIterator} that iterates `eventName` events emitted by the `emitter`
|
||||||
|
|
||||||
```js
|
```js
|
||||||
@ -1038,6 +1041,33 @@ if the `EventEmitter` emits `'error'`. It removes all listeners when
|
|||||||
exiting the loop. The `value` returned by each iteration is an array
|
exiting the loop. The `value` returned by each iteration is an array
|
||||||
composed of the emitted event arguments.
|
composed of the emitted event arguments.
|
||||||
|
|
||||||
|
An {AbortSignal} may be used to cancel waiting on events:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const { on, EventEmitter } = require('events');
|
||||||
|
const ac = new AbortController();
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
const ee = new EventEmitter();
|
||||||
|
|
||||||
|
// Emit later on
|
||||||
|
process.nextTick(() => {
|
||||||
|
ee.emit('foo', 'bar');
|
||||||
|
ee.emit('foo', 42);
|
||||||
|
});
|
||||||
|
|
||||||
|
for await (const event of on(ee, 'foo', { signal: ac.signal })) {
|
||||||
|
// The execution of this inner block is synchronous and it
|
||||||
|
// processes one event at a time (even with await). Do not use
|
||||||
|
// if concurrent execution is required.
|
||||||
|
console.log(event); // prints ['bar'] [42]
|
||||||
|
}
|
||||||
|
// Unreachable here
|
||||||
|
})();
|
||||||
|
|
||||||
|
process.nextTick(() => ac.abort());
|
||||||
|
```
|
||||||
|
|
||||||
## `EventTarget` and `Event` API
|
## `EventTarget` and `Event` API
|
||||||
<!-- YAML
|
<!-- YAML
|
||||||
added: v14.5.0
|
added: v14.5.0
|
||||||
|
@ -730,7 +730,13 @@ function eventTargetAgnosticAddListener(emitter, name, listener, flags) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function on(emitter, event) {
|
function on(emitter, event, options) {
|
||||||
|
const { signal } = { ...options };
|
||||||
|
validateAbortSignal(signal, 'options.signal');
|
||||||
|
if (signal && signal.aborted) {
|
||||||
|
throw lazyDOMException('The operation was aborted', 'AbortError');
|
||||||
|
}
|
||||||
|
|
||||||
const unconsumedEvents = [];
|
const unconsumedEvents = [];
|
||||||
const unconsumedPromises = [];
|
const unconsumedPromises = [];
|
||||||
let error = null;
|
let error = null;
|
||||||
@ -768,6 +774,15 @@ function on(emitter, event) {
|
|||||||
return() {
|
return() {
|
||||||
eventTargetAgnosticRemoveListener(emitter, event, eventHandler);
|
eventTargetAgnosticRemoveListener(emitter, event, eventHandler);
|
||||||
eventTargetAgnosticRemoveListener(emitter, 'error', errorHandler);
|
eventTargetAgnosticRemoveListener(emitter, 'error', errorHandler);
|
||||||
|
|
||||||
|
if (signal) {
|
||||||
|
eventTargetAgnosticRemoveListener(
|
||||||
|
signal,
|
||||||
|
'abort',
|
||||||
|
abortListener,
|
||||||
|
{ once: true });
|
||||||
|
}
|
||||||
|
|
||||||
finished = true;
|
finished = true;
|
||||||
|
|
||||||
for (const promise of unconsumedPromises) {
|
for (const promise of unconsumedPromises) {
|
||||||
@ -797,9 +812,20 @@ function on(emitter, event) {
|
|||||||
addErrorHandlerIfEventEmitter(emitter, errorHandler);
|
addErrorHandlerIfEventEmitter(emitter, errorHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (signal) {
|
||||||
|
eventTargetAgnosticAddListener(
|
||||||
|
signal,
|
||||||
|
'abort',
|
||||||
|
abortListener,
|
||||||
|
{ once: true });
|
||||||
|
}
|
||||||
|
|
||||||
return iterator;
|
return iterator;
|
||||||
|
|
||||||
|
function abortListener() {
|
||||||
|
errorHandler(lazyDOMException('The operation was aborted', 'AbortError'));
|
||||||
|
}
|
||||||
|
|
||||||
function eventHandler(...args) {
|
function eventHandler(...args) {
|
||||||
const promise = unconsumedPromises.shift();
|
const promise = unconsumedPromises.shift();
|
||||||
if (promise) {
|
if (promise) {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
// Flags: --expose-internals
|
// Flags: --expose-internals --no-warnings
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const common = require('../common');
|
const common = require('../common');
|
||||||
@ -248,6 +248,117 @@ async function nodeEventTarget() {
|
|||||||
clearInterval(interval);
|
clearInterval(interval);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function abortableOnBefore() {
|
||||||
|
const ee = new EventEmitter();
|
||||||
|
const ac = new AbortController();
|
||||||
|
ac.abort();
|
||||||
|
[1, {}, null, false, 'hi'].forEach((signal) => {
|
||||||
|
assert.throws(() => on(ee, 'foo', { signal }), {
|
||||||
|
code: 'ERR_INVALID_ARG_TYPE'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
assert.throws(() => on(ee, 'foo', { signal: ac.signal }), {
|
||||||
|
name: 'AbortError'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function eventTargetAbortableOnBefore() {
|
||||||
|
const et = new EventTarget();
|
||||||
|
const ac = new AbortController();
|
||||||
|
ac.abort();
|
||||||
|
[1, {}, null, false, 'hi'].forEach((signal) => {
|
||||||
|
assert.throws(() => on(et, 'foo', { signal }), {
|
||||||
|
code: 'ERR_INVALID_ARG_TYPE'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
assert.throws(() => on(et, 'foo', { signal: ac.signal }), {
|
||||||
|
name: 'AbortError'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function abortableOnAfter() {
|
||||||
|
const ee = new EventEmitter();
|
||||||
|
const ac = new AbortController();
|
||||||
|
|
||||||
|
const i = setInterval(() => ee.emit('foo', 'foo'), 10);
|
||||||
|
|
||||||
|
async function foo() {
|
||||||
|
for await (const f of on(ee, 'foo', { signal: ac.signal })) {
|
||||||
|
assert.strictEqual(f, 'foo');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foo().catch(common.mustCall((error) => {
|
||||||
|
assert.strictEqual(error.name, 'AbortError');
|
||||||
|
})).finally(() => {
|
||||||
|
clearInterval(i);
|
||||||
|
});
|
||||||
|
|
||||||
|
process.nextTick(() => ac.abort());
|
||||||
|
}
|
||||||
|
|
||||||
|
async function eventTargetAbortableOnAfter() {
|
||||||
|
const et = new EventTarget();
|
||||||
|
const ac = new AbortController();
|
||||||
|
|
||||||
|
const i = setInterval(() => et.dispatchEvent(new Event('foo')), 10);
|
||||||
|
|
||||||
|
async function foo() {
|
||||||
|
for await (const f of on(et, 'foo', { signal: ac.signal })) {
|
||||||
|
assert(f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foo().catch(common.mustCall((error) => {
|
||||||
|
assert.strictEqual(error.name, 'AbortError');
|
||||||
|
})).finally(() => {
|
||||||
|
clearInterval(i);
|
||||||
|
});
|
||||||
|
|
||||||
|
process.nextTick(() => ac.abort());
|
||||||
|
}
|
||||||
|
|
||||||
|
async function eventTargetAbortableOnAfter2() {
|
||||||
|
const et = new EventTarget();
|
||||||
|
const ac = new AbortController();
|
||||||
|
|
||||||
|
const i = setInterval(() => et.dispatchEvent(new Event('foo')), 10);
|
||||||
|
|
||||||
|
async function foo() {
|
||||||
|
for await (const f of on(et, 'foo', { signal: ac.signal })) {
|
||||||
|
assert(f);
|
||||||
|
// Cancel after a single event has been triggered.
|
||||||
|
ac.abort();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foo().catch(common.mustCall((error) => {
|
||||||
|
assert.strictEqual(error.name, 'AbortError');
|
||||||
|
})).finally(() => {
|
||||||
|
clearInterval(i);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function abortableOnAfterDone() {
|
||||||
|
const ee = new EventEmitter();
|
||||||
|
const ac = new AbortController();
|
||||||
|
|
||||||
|
const i = setInterval(() => ee.emit('foo', 'foo'), 1);
|
||||||
|
let count = 0;
|
||||||
|
|
||||||
|
async function foo() {
|
||||||
|
for await (const f of on(ee, 'foo', { signal: ac.signal })) {
|
||||||
|
assert.strictEqual(f[0], 'foo');
|
||||||
|
if (++count === 5)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
ac.abort(); // No error will occur
|
||||||
|
}
|
||||||
|
|
||||||
|
foo().finally(() => {
|
||||||
|
clearInterval(i);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function run() {
|
async function run() {
|
||||||
const funcs = [
|
const funcs = [
|
||||||
@ -260,7 +371,13 @@ async function run() {
|
|||||||
iterableThrow,
|
iterableThrow,
|
||||||
eventTarget,
|
eventTarget,
|
||||||
errorListenerCount,
|
errorListenerCount,
|
||||||
nodeEventTarget
|
nodeEventTarget,
|
||||||
|
abortableOnBefore,
|
||||||
|
abortableOnAfter,
|
||||||
|
eventTargetAbortableOnBefore,
|
||||||
|
eventTargetAbortableOnAfter,
|
||||||
|
eventTargetAbortableOnAfter2,
|
||||||
|
abortableOnAfterDone
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const fn of funcs) {
|
for (const fn of funcs) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user