Skip to content

Commit

Permalink
events: add once method to use promises with EventEmitter
Browse files Browse the repository at this point in the history
This change adds a EventEmitter.once() method that wraps ee.once in a
promise.

Co-authored-by: David Mark Clements <david.mark.clements@gmail.com>

PR-URL: nodejs#26078
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de>
Reviewed-By: Gus Caplan <me@gus.host>
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: Michaël Zasso <targos@protonmail.com>
Reviewed-By: Anatoli Papirovski <apapirovski@mac.com>
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
Reviewed-By: Сковорода Никита Андреевич <chalkerx@gmail.com>
  • Loading branch information
mcollina authored and targos committed Mar 27, 2019
1 parent 51d874c commit df55731
Show file tree
Hide file tree
Showing 3 changed files with 164 additions and 0 deletions.
41 changes: 41 additions & 0 deletions doc/api/events.md
Original file line number Diff line number Diff line change
Expand Up @@ -653,6 +653,47 @@ newListeners[0]();
emitter.emit('log');
```

## events.once(emitter, name)
<!-- YAML
added: REPLACEME
-->
* `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
Expand Down
30 changes: 30 additions & 0 deletions lib/events.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -482,3 +483,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);
});
}
93 changes: 93 additions & 0 deletions test/parallel/test-events-once.js
Original file line number Diff line number Diff line change
@@ -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()
]);

0 comments on commit df55731

Please sign in to comment.