Skip to content

Commit 9150c4d

Browse files
jeniabrookBridgeAR
authored andcommitted
events: add support for EventTarget in once
PR-URL: #29498 Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: Benjamin Gruenbaum <[email protected]> Reviewed-By: James M Snell <[email protected]> Reviewed-By: Matteo Collina <[email protected]> Reviewed-By: Minwoo Jung <[email protected]>
1 parent 6f8ef2c commit 9150c4d

File tree

3 files changed

+103
-3
lines changed

3 files changed

+103
-3
lines changed

doc/api/events.md

+6-2
Original file line numberDiff line numberDiff line change
@@ -703,11 +703,15 @@ added: v11.13.0
703703
* `name` {string}
704704
* Returns: {Promise}
705705

706-
Creates a `Promise` that is resolved when the `EventEmitter` emits the given
706+
Creates a `Promise` that is fulfilled when the `EventEmitter` emits the given
707707
event or that is rejected when the `EventEmitter` emits `'error'`.
708708
The `Promise` will resolve with an array of all the arguments emitted to the
709709
given event.
710710

711+
This method is intentionally generic and works with the web platform
712+
[EventTarget](WHATWG-EventTarget) interface, which has no special
713+
`'error'` event semantics and does not listen to the `'error'` event.
714+
711715
```js
712716
const { once, EventEmitter } = require('events');
713717

@@ -735,7 +739,7 @@ async function run() {
735739

736740
run();
737741
```
738-
742+
[WHATWG-EventTarget](https://dom.spec.whatwg.org/#interface-eventtarget)
739743
[`--trace-warnings`]: cli.html#cli_trace_warnings
740744
[`EventEmitter.defaultMaxListeners`]: #events_eventemitter_defaultmaxlisteners
741745
[`domain`]: domain.html

lib/events.js

+11
Original file line numberDiff line numberDiff line change
@@ -497,6 +497,17 @@ function unwrapListeners(arr) {
497497

498498
function once(emitter, name) {
499499
return new Promise((resolve, reject) => {
500+
if (typeof emitter.addEventListener === 'function') {
501+
// EventTarget does not have `error` event semantics like Node
502+
// EventEmitters, we do not listen to `error` events here.
503+
emitter.addEventListener(
504+
name,
505+
(...args) => { resolve(args); },
506+
{ once: true }
507+
);
508+
return;
509+
}
510+
500511
const eventListener = (...args) => {
501512
if (errorListener !== undefined) {
502513
emitter.removeListener('error', errorListener);

test/parallel/test-events-once.js

+86-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,53 @@ const common = require('../common');
44
const { once, EventEmitter } = require('events');
55
const { strictEqual, deepStrictEqual } = require('assert');
66

7+
class EventTargetMock {
8+
constructor() {
9+
this.events = {};
10+
}
11+
12+
addEventListener = common.mustCall(function(name, listener, options) {
13+
if (!(name in this.events)) {
14+
this.events[name] = { listeners: [], options };
15+
}
16+
this.events[name].listeners.push(listener);
17+
});
18+
19+
removeEventListener = common.mustCall(function(name, callback) {
20+
if (!(name in this.events)) {
21+
return;
22+
}
23+
const event = this.events[name];
24+
const stack = event.listeners;
25+
26+
for (let i = 0, l = stack.length; i < l; i++) {
27+
if (stack[i] === callback) {
28+
stack.splice(i, 1);
29+
if (stack.length === 0) {
30+
Reflect.deleteProperty(this.events, name);
31+
}
32+
return;
33+
}
34+
}
35+
});
36+
37+
dispatchEvent = function(name, ...arg) {
38+
if (!(name in this.events)) {
39+
return true;
40+
}
41+
const event = this.events[name];
42+
const stack = event.listeners.slice();
43+
44+
for (let i = 0, l = stack.length; i < l; i++) {
45+
stack[i].apply(this, arg);
46+
if (event.options.once) {
47+
this.removeEventListener(name, stack[i]);
48+
}
49+
}
50+
return !name.defaultPrevented;
51+
};
52+
}
53+
754
async function onceAnEvent() {
855
const ee = new EventEmitter();
956

@@ -84,10 +131,48 @@ async function onceError() {
84131
strictEqual(ee.listenerCount('myevent'), 0);
85132
}
86133

134+
async function onceWithEventTarget() {
135+
const et = new EventTargetMock();
136+
137+
process.nextTick(() => {
138+
et.dispatchEvent('myevent', 42);
139+
});
140+
const [ value ] = await once(et, 'myevent');
141+
strictEqual(value, 42);
142+
strictEqual(Reflect.has(et.events, 'myevent'), false);
143+
}
144+
145+
async function onceWithEventTargetTwoArgs() {
146+
const et = new EventTargetMock();
147+
148+
process.nextTick(() => {
149+
et.dispatchEvent('myevent', 42, 24);
150+
});
151+
152+
const value = await once(et, 'myevent');
153+
deepStrictEqual(value, [42, 24]);
154+
}
155+
156+
async function onceWithEventTargetError() {
157+
const et = new EventTargetMock();
158+
159+
const expected = new Error('kaboom');
160+
process.nextTick(() => {
161+
et.dispatchEvent('error', expected);
162+
});
163+
164+
const [err] = await once(et, 'error');
165+
strictEqual(err, expected);
166+
strictEqual(Reflect.has(et.events, 'error'), false);
167+
}
168+
87169
Promise.all([
88170
onceAnEvent(),
89171
onceAnEventWithTwoArgs(),
90172
catchesErrors(),
91173
stopListeningAfterCatchingError(),
92-
onceError()
174+
onceError(),
175+
onceWithEventTarget(),
176+
onceWithEventTargetTwoArgs(),
177+
onceWithEventTargetError(),
93178
]).then(common.mustCall());

0 commit comments

Comments
 (0)