node:events exports two functions that turn an emitter into something you can await. once(emitter, name) resolves with an array of the emitted arguments the next time that event fires; on(emitter, name) returns an async iterator over every occurrence.
import { EventEmitter, once, on } from 'node:events';
const meter = new EventEmitter();
setTimeout(() => meter.emit('ready', 'v2', 3), 10);
console.log('ready ', await once(meter, 'ready'));
let n = 0;
const tick = setInterval(() => {
meter.emit('sample', 100 + n * 5);
if (++n === 3) { clearInterval(tick); meter.emit('done'); }
}, 5);
for await (const [mv] of on(meter, 'sample', { close: ['done'] })) console.log('sample ', mv);
console.log('closed ', n, 'samples');ready [ 'v2', 3 ] sample 100 sample 105 sample 110 closed 3 samples
close names the events that end an on() iteration; without it the loop never finishes, because the iterator cannot know the emitter is done. A signal option cancels either helper (AbortController), and once() watches error and rejects with it rather than hanging — unless the event you await is error.
The iterator buffers: events arriving while the loop body is busy queue up in memory, and an emitter faster than its consumer grows that queue without limit. Pass highWaterMark and lowWaterMark to make on() call emitter.pause() when the queue fills (Backpressure).