Skip to content

Commit

Permalink
events: add support for EventTarget in once
Browse files Browse the repository at this point in the history
PR-URL: #29498
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Minwoo Jung <minwoo@nodesource.com>
  • Loading branch information
jeniabrook authored and Trott committed Sep 23, 2019
1 parent 54c4139 commit 34a61d5
Show file tree
Hide file tree
Showing 3 changed files with 103 additions and 3 deletions.
8 changes: 6 additions & 2 deletions doc/api/events.md
Original file line number Diff line number Diff line change
Expand Up @@ -703,11 +703,15 @@ added: v11.13.0
* `name` {string}
* Returns: {Promise}

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

This method is intentionally generic and works with the web platform
[EventTarget](WHATWG-EventTarget) interface, which has no special
`'error'` event semantics and does not listen to the `'error'` event.

```js
const { once, EventEmitter } = require('events');

Expand Down Expand Up @@ -735,7 +739,7 @@ async function run() {

run();
```

[WHATWG-EventTarget](https://dom.spec.whatwg.org/#interface-eventtarget)
[`--trace-warnings`]: cli.html#cli_trace_warnings
[`EventEmitter.defaultMaxListeners`]: #events_eventemitter_defaultmaxlisteners
[`domain`]: domain.html
Expand Down
11 changes: 11 additions & 0 deletions lib/events.js
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,17 @@ function unwrapListeners(arr) {

function once(emitter, name) {
return new Promise((resolve, reject) => {
if (typeof emitter.addEventListener === 'function') {
// EventTarget does not have `error` event semantics like Node
// EventEmitters, we do not listen to `error` events here.
emitter.addEventListener(
name,
(...args) => { resolve(args); },
{ once: true }
);
return;
}

const eventListener = (...args) => {
if (errorListener !== undefined) {
emitter.removeListener('error', errorListener);
Expand Down
87 changes: 86 additions & 1 deletion test/parallel/test-events-once.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,53 @@ const common = require('../common');
const { once, EventEmitter } = require('events');
const { strictEqual, deepStrictEqual } = require('assert');

class EventTargetMock {
constructor() {
this.events = {};
}

addEventListener = common.mustCall(function(name, listener, options) {
if (!(name in this.events)) {
this.events[name] = { listeners: [], options };
}
this.events[name].listeners.push(listener);
});

removeEventListener = common.mustCall(function(name, callback) {
if (!(name in this.events)) {
return;
}
const event = this.events[name];
const stack = event.listeners;

for (let i = 0, l = stack.length; i < l; i++) {
if (stack[i] === callback) {
stack.splice(i, 1);
if (stack.length === 0) {
Reflect.deleteProperty(this.events, name);
}
return;
}
}
});

dispatchEvent = function(name, ...arg) {
if (!(name in this.events)) {
return true;
}
const event = this.events[name];
const stack = event.listeners.slice();

for (let i = 0, l = stack.length; i < l; i++) {
stack[i].apply(this, arg);
if (event.options.once) {
this.removeEventListener(name, stack[i]);
}
}
return !name.defaultPrevented;
};
}

async function onceAnEvent() {
const ee = new EventEmitter();

Expand Down Expand Up @@ -84,10 +131,48 @@ async function onceError() {
strictEqual(ee.listenerCount('myevent'), 0);
}

async function onceWithEventTarget() {
const et = new EventTargetMock();

process.nextTick(() => {
et.dispatchEvent('myevent', 42);
});
const [ value ] = await once(et, 'myevent');
strictEqual(value, 42);
strictEqual(Reflect.has(et.events, 'myevent'), false);
}

async function onceWithEventTargetTwoArgs() {
const et = new EventTargetMock();

process.nextTick(() => {
et.dispatchEvent('myevent', 42, 24);
});

const value = await once(et, 'myevent');
deepStrictEqual(value, [42, 24]);
}

async function onceWithEventTargetError() {
const et = new EventTargetMock();

const expected = new Error('kaboom');
process.nextTick(() => {
et.dispatchEvent('error', expected);
});

const [err] = await once(et, 'error');
strictEqual(err, expected);
strictEqual(Reflect.has(et.events, 'error'), false);
}

Promise.all([
onceAnEvent(),
onceAnEventWithTwoArgs(),
catchesErrors(),
stopListeningAfterCatchingError(),
onceError()
onceError(),
onceWithEventTarget(),
onceWithEventTargetTwoArgs(),
onceWithEventTargetError(),
]).then(common.mustCall());

0 comments on commit 34a61d5

Please sign in to comment.