Skip to content

events: add addDisposableListener method to EventEmitter #58453

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

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

jasnell
Copy link
Member

@jasnell jasnell commented May 25, 2025

Marking this draft as I'm not 100% sure it's something we want. Looking for feedback and opinions.

This adds new use(...) and useOnce(...) methods addDisposableListener method to EventEmitter that returns a disposable object that will unregister the event listeners when disposed, along with two changes that use the new apis to demonstrate how it can be used to simplify some cleanup.

const ee = new EventEmitter();
{
  using ds = new DisposableStack();
  ds.use(ee.addDisposableListener('foo', () => {});
  ds.use(ee.addDisposableListener('bar', () => {});
  if (something) ds.use(ee.addDisposableListener('baz', () => {});
}
// The event listeners will all be removed automatically when ds is disposed.
// We don't need to separately remember to call removeListener/off to cleanup the event listeners

@jasnell jasnell requested review from mcollina, addaleax and anonrig May 25, 2025 19:01
@nodejs-github-bot
Copy link
Collaborator

Review requested:

  • @nodejs/streams
  • @nodejs/test_runner

@nodejs-github-bot nodejs-github-bot added events Issues and PRs related to the events subsystem / EventEmitter. needs-ci PRs that need a full CI run. stream Issues and PRs related to the stream subsystem. test_runner Issues and PRs related to the test runner subsystem. labels May 25, 2025
@jasnell
Copy link
Member Author

jasnell commented May 31, 2025

Relevant to advancing on this: #58526

@mcollina
Copy link
Member

I'm ok with the addition. I'm a bit worried about the potential change in behavior, as I didn't have time to explore yet if there is a change in the order of operations.

@jasnell
Copy link
Member Author

jasnell commented May 31, 2025

What change in behavior are you concerned about? This should not change any existing behavior of the EventEmitter

@jasnell jasnell force-pushed the jasnell/eventemitter-using branch from 718fe84 to 2657ecb Compare June 1, 2025 17:52
@jasnell jasnell changed the title events: Add use/useOnce methods to EventEmitter events: add addDisposableListener method to EventEmitter Jun 1, 2025
@jasnell
Copy link
Member Author

jasnell commented Jun 1, 2025

I've updated the implementation to rename the use method to a single addDisposableListener method with a once option. It returns a dispose function that has the Symbol.dispose property attached to call itself. PTAL

@jasnell jasnell requested a review from LiviaMedeiros June 1, 2025 17:55
Copy link
Member

@LiviaMedeiros LiviaMedeiros left a comment

Choose a reason for hiding this comment

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

LGTM with removal of obsolete no-undefs and assuming consensus on this in #58526.
Perhaps using dispose === dispose[Symbol.dispose] can be made into recommended pattern (whenever we don't have more meaningful return values) in the ERM guideline.

const onrequest = () => {
stream.req.on('finish', onfinish);
disposableStack.use(stream.req.addDisposableListener('finish', onfinish));
Copy link
Member

Choose a reason for hiding this comment

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

This would need a stream benchmark run before landing

Copy link
Member Author

Choose a reason for hiding this comment

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

The results are mixed and it's just not clear if the difference is worth being concerned about... some benchmarks are faster, others are slower.

streams/compose.js n=1000                                                                       ***    -19.41 %       ±1.17%  ±1.57%  ±2.06%
streams/creation.js kind='duplex' n=50000000                                                    ***      4.39 %       ±1.62%  ±2.16%  ±2.81%
streams/creation.js kind='readable' n=50000000                                                           2.46 %       ±4.12%  ±5.48%  ±7.14%
streams/creation.js kind='transform' n=50000000                                                   *      2.21 %       ±1.69%  ±2.25%  ±2.94%
streams/creation.js kind='writable' n=50000000                                                  ***      6.72 %       ±2.15%  ±2.85%  ±3.71%
streams/destroy.js kind='duplex' n=1000000                                                              -0.50 %       ±3.36%  ±4.48%  ±5.84%
streams/destroy.js kind='readable' n=1000000                                                             1.07 %       ±2.43%  ±3.24%  ±4.22%
streams/destroy.js kind='transform' n=1000000                                                           -0.55 %       ±2.33%  ±3.10%  ±4.03%
streams/destroy.js kind='writable' n=1000000                                                             0.16 %       ±2.75%  ±3.66%  ±4.77%
streams/pipe-object-mode.js n=5000000                                                           ***      4.68 %       ±2.00%  ±2.67%  ±3.50%
streams/pipe.js n=5000000                                                                       ***     10.30 %       ±1.19%  ±1.59%  ±2.07%
streams/readable-async-iterator.js sync='no' n=100000                                                    1.07 %       ±1.92%  ±2.55%  ±3.32%
streams/readable-async-iterator.js sync='yes' n=100000                                          ***      8.45 %       ±3.80%  ±5.06%  ±6.59%
streams/readable-bigread.js n=1000                                                                       0.55 %       ±2.27%  ±3.02%  ±3.94%
streams/readable-bigunevenread.js n=1000                                                        ***     -2.12 %       ±1.13%  ±1.52%  ±1.99%
streams/readable-boundaryread.js type='buffer' n=2000                                             *      1.03 %       ±0.84%  ±1.12%  ±1.46%
streams/readable-boundaryread.js type='string' n=2000                                             *      2.08 %       ±1.69%  ±2.26%  ±2.97%
streams/readable-from.js type='array' n=10000000                                                ***     -5.20 %       ±2.30%  ±3.06%  ±3.99%
streams/readable-from.js type='async-generator' n=10000000                                               1.73 %       ±2.22%  ±2.96%  ±3.86%
streams/readable-from.js type='sync-generator-with-async-values' n=10000000                              1.88 %       ±2.05%  ±2.73%  ±3.56%
streams/readable-from.js type='sync-generator-with-sync-values' n=10000000                              -1.75 %       ±2.41%  ±3.24%  ±4.28%
streams/readable-readall.js n=5000                                                               **     -4.53 %       ±3.18%  ±4.24%  ±5.51%
streams/readable-uint8array.js kind='encoding' n=1000000                                         **      2.40 %       ±1.52%  ±2.02%  ±2.63%
streams/readable-uint8array.js kind='read' n=1000000                                            ***     -2.98 %       ±1.59%  ±2.11%  ±2.75%
streams/readable-unevenread.js n=1000                                                             *     -1.81 %       ±1.45%  ±1.93%  ±2.51%
streams/writable-manywrites.js len=1024 callback='no' writev='no' sync='no' n=100000                     1.10 %       ±3.76%  ±5.00%  ±6.51%
streams/writable-manywrites.js len=1024 callback='no' writev='no' sync='yes' n=100000           ***     27.55 %       ±9.28% ±12.36% ±16.09%
streams/writable-manywrites.js len=1024 callback='no' writev='yes' sync='no' n=100000                    2.88 %       ±5.18%  ±6.90%  ±8.98%
streams/writable-manywrites.js len=1024 callback='no' writev='yes' sync='yes' n=100000            *     11.47 %       ±9.08% ±12.08% ±15.73%
streams/writable-manywrites.js len=1024 callback='yes' writev='no' sync='no' n=100000                   -1.45 %       ±3.28%  ±4.36%  ±5.68%
streams/writable-manywrites.js len=1024 callback='yes' writev='no' sync='yes' n=100000          ***     23.32 %       ±8.66% ±11.53% ±15.01%
streams/writable-manywrites.js len=1024 callback='yes' writev='yes' sync='no' n=100000                   3.83 %       ±4.48%  ±5.96%  ±7.76%
streams/writable-manywrites.js len=1024 callback='yes' writev='yes' sync='yes' n=100000                  1.32 %       ±6.89%  ±9.17% ±11.94%
streams/writable-manywrites.js len=32768 callback='no' writev='no' sync='no' n=100000                   -1.30 %       ±3.51%  ±4.68%  ±6.09%
streams/writable-manywrites.js len=32768 callback='no' writev='no' sync='yes' n=100000          ***     30.32 %      ±10.19% ±13.57% ±17.67%
streams/writable-manywrites.js len=32768 callback='no' writev='yes' sync='no' n=100000                   0.12 %       ±3.88%  ±5.17%  ±6.73%
streams/writable-manywrites.js len=32768 callback='no' writev='yes' sync='yes' n=100000          **     13.40 %       ±9.34% ±12.47% ±16.32%
streams/writable-manywrites.js len=32768 callback='yes' writev='no' sync='no' n=100000                  -1.46 %       ±4.38%  ±5.83%  ±7.61%
streams/writable-manywrites.js len=32768 callback='yes' writev='no' sync='yes' n=100000         ***     16.67 %       ±8.21% ±10.94% ±14.28%
streams/writable-manywrites.js len=32768 callback='yes' writev='yes' sync='no' n=100000                  1.19 %       ±4.05%  ±5.39%  ±7.01%
streams/writable-manywrites.js len=32768 callback='yes' writev='yes' sync='yes' n=100000        ***     21.43 %       ±9.84% ±13.14% ±17.21%
streams/writable-uint8array.js kind='object-mode' n=50000000                                             0.64 %       ±3.66%  ±4.87%  ±6.34%
streams/writable-uint8array.js kind='write' n=50000000                                            *      3.85 %       ±3.32%  ±4.42%  ±5.76%
streams/writable-uint8array.js kind='writev' n=50000000                                           *      3.15 %       ±2.99%  ±3.98%  ±5.18%

@benjamingr
Copy link
Member

Is there any way we can avoid adding yet another way to register events? i.e. can this be an option and not a method?

I think this was blocked previously in AbortSignal due to a V8 optimization that happened since.

@jasnell
Copy link
Member Author

jasnell commented Jun 1, 2025

Is there any way we can avoid adding yet another way to register events? i.e. can this be an option and not a method?

Not without adding too much complexity. As it is now, addListener(...) returns the EventEmitter itself. Adding an option would mean making the return value polymorphic which is far worse than adding a new separate API. See the discussion around #58526

Copy link
Contributor

Choose a reason for hiding this comment

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

Experimental?

@Renegade334
Copy link
Contributor

Renegade334 commented Jun 2, 2025

Regarding the single-use disposable that's just a disposer, with no associated object data: is

function dispose() { ... }
dispose[SymbolDispose] = dispose
return dispose

OK, or should we be keeping things consistent with the draft guidance for disposers in general?

function dispose() { ... }
return {
  dispose,
  [SymbolDispose]: dispose,
  // or
  [SymbolDispose]() { dispose() },
}

My vote would be for the latter – it would be an inconsistent pattern for disposables to sometimes be directly callable, and other times not.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
events Issues and PRs related to the events subsystem / EventEmitter. needs-ci PRs that need a full CI run. stream Issues and PRs related to the stream subsystem. test_runner Issues and PRs related to the test runner subsystem.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants