Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

events: add once method to use promises with EventEmitter #26078

Closed
wants to merge 1 commit into from

Conversation

mcollina
Copy link
Member

This change adds a EventEmitter.once() method that wraps ee.once in a
promise. I've been using this model for some months now, and it works extremely well for me.

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();
Checklist
  • make -j4 test (UNIX), or vcbuild test (Windows) passes
  • tests and/or benchmarks are included
  • documentation is changed or added
  • commit message follows commit guidelines

@nodejs-github-bot nodejs-github-bot added the events Issues and PRs related to the events subsystem / EventEmitter. label Feb 13, 2019
@mcollina mcollina added the semver-minor PRs that contain new features and should be released in the next minor version. label Feb 13, 2019
@mcollina
Copy link
Member Author

Also @davidmarkclements that had the original idea: would you like to be included as "co-authored-by"?

doc/api/events.md Outdated Show resolved Hide resolved
doc/api/events.md Outdated Show resolved Hide resolved
@lpinca
Copy link
Member

lpinca commented Feb 13, 2019

Why in core? Can't this be an npm module?

Copy link
Member

@addaleax addaleax left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you! 💙

Copy link
Contributor

@Fishrock123 Fishrock123 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same question as @lpinca -

Why in core? Can't this be an npm module?

I mean, I understand why it would be in core, I think -- that is, to make yet more node.js core apis usable with await.

But I also think that since it doesn't appear to need to be in core, it should at very least have an npm module with adoption to show the weight of it's usefulness...

@cjihrig
Copy link
Contributor

cjihrig commented Feb 13, 2019

Fixes #20893 ?

@jasnell
Copy link
Member

jasnell commented Feb 13, 2019

While this could live outside of core, it's been frequently requested and aligns with our promises support. I'm +1 on adding it

@@ -653,6 +653,46 @@ newListeners[0]();
emitter.emit('log');
```

## EventEmitter.once(emitter, name)
Copy link
Contributor

@vsemozhetbyt vsemozhetbyt Feb 14, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should not this be events.once(emitter, name)? Otherwise, this seems to be a static EventEmitter method like EventEmitter.listenerCount() above. If I understand correctly, this would be called as events.once() in the REPL. I see that it can be used as EventEmitter static method, but in examples, it is imported alongside with the class and this can be confusing for users not aware of EventEmitter.EventEmitter = EventEmitter; core binding.

@lpinca
Copy link
Member

lpinca commented Feb 14, 2019

If I'm not wrong it even works with a third party EventEmitter assuming that it supports once() and removeListener() which kind of reinforces "why in core?". In my opinion this goes against the small core principle.

  • It is not more efficient than a userland implementation.
  • It doesn't add something that can't be done in userland.
  • As far as I know It is not needed in core itself. Is the plan to make core API use it?

@mcollina
Copy link
Member Author

I think this API (or something similar) belongs in core for three main reasons:

  1. it aligns with our strategic initiative of “promisifying core APIs”.
  2. it is extremely hard to interact with stream using async/await without an utility like this.
  3. making sure that 12.0.0 has this will shorten the adoption cycle by 1 year.

I think we can use this API in a lot of test code within core.

I’m using it from a userland module, but tiny utility modules are hard to market at this point.

I’m happy to provide more examples if needed.

@mcollina
Copy link
Member Author

@lpinca we have been using it for 1 year and a half in Pino test suite (and copy-paste in a lot of other places): https://github.com/pinojs/pino/blob/master/test/basic.test.js#L43 as an example.

@lpinca
Copy link
Member

lpinca commented Feb 14, 2019

Yes, ok but that does not answer "why in core?". A npm module would be better for this in my opinion. Anyway I'm not blocking this and it already has a few TSC approvals.

@mcollina mcollina added the tsc-agenda Issues and PRs to discuss during the meetings of the TSC. label Feb 14, 2019
@mcollina
Copy link
Member Author

cc @nodejs/tsc adding to tsc-agenda for visibility

Copy link
Member

@apapirovski apapirovski left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. I don't disagree with the comment about this possibly belonging in user-land but I'm also happy with it in Node. Implementation is super clean 👍

lib/events.js Outdated
}
resolve(args);
};
let secondListener;
Copy link
Member

@apapirovski apapirovski Feb 20, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just personal preference but having this at the start of the function body would be clearer for me. When I read the code, the secondListener is referenced before its defined which makes it somewhat confusing. I know it's kind of nitpicky tho so non-blocking...

(Maybe naming it errorRejectListener would also be clearer?)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

eventListener and errorListener (or something of the like) would be clearer than main and second.

@ChALkeR ChALkeR self-requested a review February 20, 2019 21:19
@ChALkeR
Copy link
Member

ChALkeR commented Feb 20, 2019

Related: #20909

@Trott
Copy link
Member

Trott commented Feb 20, 2019

No strong opinion, but if there's concerns especially around API design, maybe land as Experimental so we can change the API if we don't get it right the first time?

@Trott Trott removed the tsc-agenda Issues and PRs to discuss during the meetings of the TSC. label Feb 20, 2019
targos pushed a commit to targos/node that referenced this pull request Mar 27, 2019
This change adds a EventEmitter.once() method that wraps ee.once in a
promise.

Co-authored-by: David Mark Clements <[email protected]>

PR-URL: nodejs#26078
Reviewed-By: James M Snell <[email protected]>
Reviewed-By: Ruben Bridgewater <[email protected]>
Reviewed-By: Gus Caplan <[email protected]>
Reviewed-By: Anna Henningsen <[email protected]>
Reviewed-By: Michaël Zasso <[email protected]>
Reviewed-By: Anatoli Papirovski <[email protected]>
Reviewed-By: Benjamin Gruenbaum <[email protected]>
Reviewed-By: Сковорода Никита Андреевич <[email protected]>
targos added a commit that referenced this pull request Mar 27, 2019
Notable changes:

* events:
  * Added a `once` function to use `EventEmitter` with promises
    (#26078).
* tty:
  * Added a `hasColors` method to `WriteStream`
    (#26247).
  * Added NO_COLOR and FORCE_COLOR support
    (#26485).
* v8:
  * Added `v8.getHeapSnapshot` and `v8.writeHeapSnapshot` to generate snapshots
    in the format used by tools such as Chrome DevTools
    (#26501).
* meta:
  * Gireesh Punathil is now a member of the Technical Steering Committee
    (#26657).
  * Added ZYSzys to collaborators (#26730).

PR-URL: #26949
targos added a commit that referenced this pull request Mar 28, 2019
Notable changes:

* crypto
  * Allow deriving public from private keys (Tobias Nießen)
    [#26278](#26278).
* events
  * Added a `once` function to use `EventEmitter` with promises
    (Matteo Collina) [#26078](#26078).
* tty
  * Added a `hasColors` method to `WriteStream` (Ruben Bridgewater)
    [#26247](#26247).
  * Added NO_COLOR and FORCE_COLOR support (Ruben Bridgewater)
    [#26485](#26485).
* v8
  * Added `v8.getHeapSnapshot` and `v8.writeHeapSnapshot` to generate snapshots
    in the format used by tools such as Chrome DevTools (James M Snell)
    [#26501](#26501).
* worker
  * Added `worker.moveMessagePortToContext`. This enables using MessagePorts in
    different vm.Contexts, aiding with the isolation that the vm module seeks to
    provide (Anna Henningsen)
    [#26497](#26497).
* C++ API
  * `AddPromiseHook` is now deprecated. This API was added to fill an use case
    that is served by `async_hooks`, since that has `Promise` support
    (Anna Henningsen) [#26529](#26529).
  * Added a `Stop` API to shut down Node.js while it is running
    (Gireesh Punathil) [#21283](#21283).
* meta
  * [Gireesh Punathil](https://github.com/gireeshpunathil) is now a member of
    the Technical Steering Committee
    [#26657](#26657).
  * Added [Yongsheng Zhang](https://github.com/ZYSzys) to collaborators
    [#26730](#26730).

PR-URL: #26949
targos added a commit that referenced this pull request Mar 28, 2019
Notable changes:

* crypto
  * Allow deriving public from private keys (Tobias Nießen)
    [#26278](#26278).
* events
  * Added a `once` function to use `EventEmitter` with promises
    (Matteo Collina) [#26078](#26078).
* tty
  * Added a `hasColors` method to `WriteStream` (Ruben Bridgewater)
    [#26247](#26247).
  * Added NO_COLOR and FORCE_COLOR support (Ruben Bridgewater)
    [#26485](#26485).
* v8
  * Added `v8.getHeapSnapshot` and `v8.writeHeapSnapshot` to generate snapshots
    in the format used by tools such as Chrome DevTools (James M Snell)
    [#26501](#26501).
* worker
  * Added `worker.moveMessagePortToContext`. This enables using MessagePorts in
    different vm.Contexts, aiding with the isolation that the vm module seeks to
    provide (Anna Henningsen)
    [#26497](#26497).
* C++ API
  * `AddPromiseHook` is now deprecated. This API was added to fill an use case
    that is served by `async_hooks`, since that has `Promise` support
    (Anna Henningsen) [#26529](#26529).
  * Added a `Stop` API to shut down Node.js while it is running
    (Gireesh Punathil) [#21283](#21283).
* meta
  * [Gireesh Punathil](https://github.com/gireeshpunathil) is now a member of
    the Technical Steering Committee
    [#26657](#26657).
  * Added [Yongsheng Zhang](https://github.com/ZYSzys) to collaborators
    [#26730](#26730).

PR-URL: #26949
BethGriggs pushed a commit that referenced this pull request Apr 5, 2019
Notable changes:

* crypto
  * Allow deriving public from private keys (Tobias Nießen)
    [#26278](#26278).
* events
  * Added a `once` function to use `EventEmitter` with promises
    (Matteo Collina) [#26078](#26078).
* tty
  * Added a `hasColors` method to `WriteStream` (Ruben Bridgewater)
    [#26247](#26247).
  * Added NO_COLOR and FORCE_COLOR support (Ruben Bridgewater)
    [#26485](#26485).
* v8
  * Added `v8.getHeapSnapshot` and `v8.writeHeapSnapshot` to generate snapshots
    in the format used by tools such as Chrome DevTools (James M Snell)
    [#26501](#26501).
* worker
  * Added `worker.moveMessagePortToContext`. This enables using MessagePorts in
    different vm.Contexts, aiding with the isolation that the vm module seeks to
    provide (Anna Henningsen)
    [#26497](#26497).
* C++ API
  * `AddPromiseHook` is now deprecated. This API was added to fill an use case
    that is served by `async_hooks`, since that has `Promise` support
    (Anna Henningsen) [#26529](#26529).
  * Added a `Stop` API to shut down Node.js while it is running
    (Gireesh Punathil) [#21283](#21283).
* meta
  * [Gireesh Punathil](https://github.com/gireeshpunathil) is now a member of
    the Technical Steering Committee
    [#26657](#26657).
  * Added [Yongsheng Zhang](https://github.com/ZYSzys) to collaborators
    [#26730](#26730).

PR-URL: #26949
BethGriggs pushed a commit that referenced this pull request Apr 16, 2019
This change adds a EventEmitter.once() method that wraps ee.once in a
promise.

Co-authored-by: David Mark Clements <[email protected]>

PR-URL: #26078
Reviewed-By: James M Snell <[email protected]>
Reviewed-By: Ruben Bridgewater <[email protected]>
Reviewed-By: Gus Caplan <[email protected]>
Reviewed-By: Anna Henningsen <[email protected]>
Reviewed-By: Michaël Zasso <[email protected]>
Reviewed-By: Anatoli Papirovski <[email protected]>
Reviewed-By: Benjamin Gruenbaum <[email protected]>
Reviewed-By: Сковорода Никита Андреевич <[email protected]>
@BethGriggs BethGriggs mentioned this pull request May 1, 2019
@AAverin
Copy link

AAverin commented May 22, 2019

Why not do the same for on?
Then it would be possible to have a bus based on promises

@sam-github
Copy link
Contributor

.once() callbacks once, promises resolve once. .on() callbacks repeatedly, so how could that work with promises, which cannot resolve multiple times?

@mcollina
Copy link
Member Author

Why not do the same for on?

We could expose a on(ee, 'event') method that returns an async iterator. However there would be no "exit" condition from the loop.

for await (let event of on(ee, 'event')) {
  // do something with event
  // this loop will never end
}

Would you mind opening a new issue to discuss? Do you think this would be worthwhile even if the loop would never end?

@AAverin
Copy link

AAverin commented May 22, 2019

Well, problem I have so far, if I have an active subscription to events that can come forever until I unsubscribe, there is no way to have a nice handling of that with promises.
Basically I am looking for a "hot observable" kind of thing.

@mcollina
Copy link
Member Author

I would recommend using a stream instead. It’s async iterable already.

@jasnell
Copy link
Member

jasnell commented May 22, 2019

We could support an on function that returns an async iterator so long as we introduced a concept of a loop terminator.... That is, an object that would cause the loop to terminate...

E.g. something roughly like...

const foo = events.on(emitter, 'foo', term);
for await (const f of foo) {
  // ...
  term.exit()
}

@benjamingr
Copy link
Member

@jasnell @mcollina actually break and return would work:

const foo = events.on(emitter, 'foo');
for await (const f of foo) {
  break; // this calls `.return` on the async iterable and can unsubscribe.
}

This is also true for regular generators. I'm +1 on exploring this.

@jasnell
Copy link
Member

jasnell commented May 22, 2019

@benjamingr ... yes, those work within the actual for await but we would need to make sure we're able to clean up inside the async iterator itself (e.g. removing the event listener). It may be rather trivial to do, I just haven't tried it yet :-) ... Perhaps I'll take a break from QUIC implementation and give it a try :-)

@jasnell
Copy link
Member

jasnell commented May 23, 2019

Ok, quick investigation... it should be possible to implement an EventEmitter.on(emitter, event) function that is essentially a simplification of the code used for stream.Readable with a few notable differences:

  1. Unlike stream.Readable, there is no natural end to the event stream, and there is no destroy() method, so cleanup and termination of the for await() {} would be entirely dependent on using break and return as suggested.

  2. With stream.Readable, when there are multiple synchronous push()'s, there's still just a single 'readable', and just in case there are multiple read() operations performed. For an arbitrary EventEmitter, we would need a way of reliably handling synchronous emits.

Everything else looks like it should be fairly reasonable to implement if someone wants to take it on ;-)

@benjamingr
Copy link
Member

benjamingr commented May 24, 2019

moving to a separate issue

BethGriggs added a commit that referenced this pull request May 28, 2019
Notable changes:

- deps:
  - icu 63.1 bump (CLDR 34) (Steven R. Loomis)
    [#23715](#23715)
  - upgrade npm to 6.9.0 (Kat Marchán)
    [#26244](#26244)
  - upgrade openssl sources to 1.1.1a (Sam Roberts)
    [#25381](#25381)
  - upgrade to libuv 1.24.1 (cjihrig)
    [#25078](#25078)
- events: add once method to use promises with EventEmitter
  (Matteo Collina) [#26078](#26078)
- n-api: mark thread-safe function as stable (Gabriel Schulhof)
  [#25556](#25556)
- repl: support top-level for-await-of (Shelley Vohr)
  [#23841](#23841)
- zlib:
  - add brotli support (Anna Henningsen)
  [#24938](#24938)

PR-URL: #27514
BethGriggs added a commit that referenced this pull request May 28, 2019
Notable changes:

- **deps**:
  - update ICU to 64.2 (Ujjwal Sharma)
    [#27361](#27361)
  - upgrade npm to 6.9.0 (Kat Marchán)
    [#26244](#26244)
  - upgrade openssl sources to 1.1.1b (Sam Roberts)
    [#26327](#26327)
  - upgrade to libuv 1.28.0 (cjihrig)
    [#27241](#27241)
- **events**:
  - add once method to use promises with EventEmitter (Matteo Collina)
   [#26078](#26078)
- **n-api**:
  - mark thread-safe function as stable (Gabriel Schulhof)
    [#25556](#25556)
- **repl**:
  - support top-level for-await-of (Shelley Vohr)
    [#23841](#23841)
- **zlib**:
  - add brotli support (Anna Henningsen)
    [#24938](#24938)

PR-URL: #27514
BethGriggs added a commit that referenced this pull request May 28, 2019
Notable changes:

- **deps**:
  - update ICU to 64.2 (Ujjwal Sharma)
    [#27361](#27361)
  - upgrade npm to 6.9.0 (Kat Marchán)
    [#26244](#26244)
  - upgrade openssl sources to 1.1.1b (Sam Roberts)
    [#26327](#26327)
  - upgrade to libuv 1.28.0 (cjihrig)
    [#27241](#27241)
- **events**:
  - add once method to use promises with EventEmitter (Matteo Collina)
   [#26078](#26078)
- **n-api**:
  - mark thread-safe function as stable (Gabriel Schulhof)
    [#25556](#25556)
- **repl**:
  - support top-level for-await-of (Shelley Vohr)
    [#23841](#23841)
- **zlib**:
  - add brotli support (Anna Henningsen)
    [#24938](#24938)

PR-URL: #27514
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
author ready PRs that have at least one approval, no pending requests for changes, and a CI started. events Issues and PRs related to the events subsystem / EventEmitter. semver-minor PRs that contain new features and should be released in the next minor version.
Projects
None yet
Development

Successfully merging this pull request may close these issues.