diff --git a/doc/api/events.md b/doc/api/events.md index 3b92a04ca582a2..8d409582329c14 100644 --- a/doc/api/events.md +++ b/doc/api/events.md @@ -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'); @@ -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 diff --git a/lib/events.js b/lib/events.js index ebde85c6ddf990..f6e4996d20b40f 100644 --- a/lib/events.js +++ b/lib/events.js @@ -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); diff --git a/test/parallel/test-events-once.js b/test/parallel/test-events-once.js index 25ef4e9845422c..fea143f5877cc7 100644 --- a/test/parallel/test-events-once.js +++ b/test/parallel/test-events-once.js @@ -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(); @@ -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());