From 064511ec4b4773e2b1a57eebcf45e052bffb06ea Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Wed, 13 Feb 2019 13:30:28 +0100 Subject: [PATCH] events: add once method to use promises with EventEmitter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This change adds a EventEmitter.once() method that wraps ee.once in a promise. Co-authored-by: David Mark Clements PR-URL: https://github.com/nodejs/node/pull/26078 Reviewed-By: James M Snell Reviewed-By: Ruben Bridgewater Reviewed-By: Gus Caplan Reviewed-By: Anna Henningsen Reviewed-By: Michaël Zasso Reviewed-By: Anatoli Papirovski Reviewed-By: Benjamin Gruenbaum Reviewed-By: Сковорода Никита Андреевич --- doc/api/events.md | 41 ++++++++++++++ lib/events.js | 30 ++++++++++ test/parallel/test-events-once.js | 93 +++++++++++++++++++++++++++++++ 3 files changed, 164 insertions(+) create mode 100644 test/parallel/test-events-once.js diff --git a/doc/api/events.md b/doc/api/events.md index aafdbcf735bee5..a94df00b888f11 100644 --- a/doc/api/events.md +++ b/doc/api/events.md @@ -653,6 +653,47 @@ newListeners[0](); emitter.emit('log'); ``` +## events.once(emitter, name) + +* `emitter` {EventEmitter} +* `name` {string} +* Returns: {Promise} + +Creates a `Promise` that is resolved 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. + +```js +const { once, EventEmitter } = require('events'); + +async function run() { + const ee = new EventEmitter(); + + process.nextTick(() => { + ee.emit('myevent', 42); + }); + + const [value] = await once(ee, 'myevent'); + console.log(value); + + const err = new Error('kaboom'); + process.nextTick(() => { + ee.emit('error', err); + }); + + try { + await once(ee, 'myevent'); + } catch (err) { + console.log('error happened', err); + } +} + +run(); +``` + [`--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 8cd9cd9582725c..67febe9f00355d 100644 --- a/lib/events.js +++ b/lib/events.js @@ -27,6 +27,7 @@ function EventEmitter() { EventEmitter.init.call(this); } module.exports = EventEmitter; +module.exports.once = once; // Backwards-compat with node 0.10.x EventEmitter.EventEmitter = EventEmitter; @@ -474,3 +475,32 @@ function unwrapListeners(arr) { } return ret; } + +function once(emitter, name) { + return new Promise((resolve, reject) => { + const eventListener = (...args) => { + if (errorListener !== undefined) { + emitter.removeListener('error', errorListener); + } + resolve(args); + }; + let errorListener; + + // Adding an error listener is not optional because + // if an error is thrown on an event emitter we cannot + // guarantee that the actual event we are waiting will + // be fired. The result could be a silent way to create + // memory or file descriptor leaks, which is something + // we should avoid. + if (name !== 'error') { + errorListener = (err) => { + emitter.removeListener(name, eventListener); + reject(err); + }; + + emitter.once('error', errorListener); + } + + emitter.once(name, eventListener); + }); +} diff --git a/test/parallel/test-events-once.js b/test/parallel/test-events-once.js new file mode 100644 index 00000000000000..f99604018ad0af --- /dev/null +++ b/test/parallel/test-events-once.js @@ -0,0 +1,93 @@ +'use strict'; + +const common = require('../common'); +const { once, EventEmitter } = require('events'); +const { strictEqual, deepStrictEqual } = require('assert'); + +async function onceAnEvent() { + const ee = new EventEmitter(); + + process.nextTick(() => { + ee.emit('myevent', 42); + }); + + const [value] = await once(ee, 'myevent'); + strictEqual(value, 42); + strictEqual(ee.listenerCount('error'), 0); + strictEqual(ee.listenerCount('myevent'), 0); +} + +async function onceAnEventWithTwoArgs() { + const ee = new EventEmitter(); + + process.nextTick(() => { + ee.emit('myevent', 42, 24); + }); + + const value = await once(ee, 'myevent'); + deepStrictEqual(value, [42, 24]); +} + +async function catchesErrors() { + const ee = new EventEmitter(); + + const expected = new Error('kaboom'); + let err; + process.nextTick(() => { + ee.emit('error', expected); + }); + + try { + await once(ee, 'myevent'); + } catch (_e) { + err = _e; + } + strictEqual(err, expected); + strictEqual(ee.listenerCount('error'), 0); + strictEqual(ee.listenerCount('myevent'), 0); +} + +async function stopListeningAfterCatchingError() { + const ee = new EventEmitter(); + + const expected = new Error('kaboom'); + let err; + process.nextTick(() => { + ee.emit('error', expected); + ee.emit('myevent', 42, 24); + }); + + process.on('multipleResolves', common.mustNotCall()); + + try { + await once(ee, 'myevent'); + } catch (_e) { + err = _e; + } + process.removeAllListeners('multipleResolves'); + strictEqual(err, expected); + strictEqual(ee.listenerCount('error'), 0); + strictEqual(ee.listenerCount('myevent'), 0); +} + +async function onceError() { + const ee = new EventEmitter(); + + const expected = new Error('kaboom'); + process.nextTick(() => { + ee.emit('error', expected); + }); + + const [err] = await once(ee, 'error'); + strictEqual(err, expected); + strictEqual(ee.listenerCount('error'), 0); + strictEqual(ee.listenerCount('myevent'), 0); +} + +Promise.all([ + onceAnEvent(), + onceAnEventWithTwoArgs(), + catchesErrors(), + stopListeningAfterCatchingError(), + onceError() +]);