Skip to content

Event to Async Iterator ​

Convert any EventEmitter (Node.js) or EventTarget (DOM) into an async iterable using Repeater. Listeners are automatically cleaned up when iteration stops.

When to Use ​

  • Consuming DOM events with for await...of instead of callbacks
  • Processing Node.js EventEmitter events as a stream
  • Bridging any event source to async iteration for composition with debounce, chunksAsync, or Repeater.merge

The Pattern ​

Watch DOM events ​

javascript
const clicks = fromEvent(button, "click");

for await (const event of clicks) {
  handleClick(event);
  if (shouldStop) break; // listener is removed
}

Stream Node.js events ​

javascript
const emitter = new EventEmitter();
const values = fromEvent(emitter, "data");

setTimeout(() => {
  emitter.emit("data", 1);
  emitter.emit("data", 2);
  emitter.emit("data", 3);
}, 10);

for await (const value of values) {
  console.log(value); // 1, 2, 3
  if (value === 3) break;
}

Compose with other utilities ​

javascript
// Debounced resize events
const resizes = fromEvent(window, "resize");
const debounced = debounce(200)(resizes);
for await (const event of debounced) {
  recalculateLayout();
}

// Merge multiple event sources
const merged = Repeater.merge([
  fromEvent(window, "online"),
  fromEvent(window, "offline"),
]);
for await (const event of merged) {
  updateConnectionStatus();
}

Stop iteration explicitly ​

javascript
const iterator = fromEvent(emitter, "value");
const collected = [];

for await (const value of iterator) {
  collected.push(value);
  if (collected.length === 3) {
    iterator.return(); // triggers cleanup
  }
}

Implementing fromEvent() ​

fromEvent() creates a Repeater that attaches an event listener on construction and removes it when iteration stops. It auto-detects Node.js EventEmitter (.on/.off) or DOM EventTarget (.addEventListener/.removeEventListener):

javascript
import { Repeater } from "@repeaterjs/repeater";

export function fromEvent(emitter, event) {
  return new Repeater(async (push, stop) => {
    const on =
      (emitter.on ?? emitter.addEventListener).bind(emitter);
    const off =
      (emitter.off ?? emitter.removeEventListener)
        .bind(emitter);
    
    on(event, listener);
    await stop;
    off(event, listener);
    
    function listener() {
      const [first, ...rest] = arguments;
      if (rest.length > 0) {
        push([first, ...rest]);
      } else {
        push(first);
      }
    }
  });
}

How it works:

  1. Auto-detect API — checks for on/off (Node.js EventEmitter) or addEventListener/removeEventListener (DOM EventTarget).
  2. Register listener — attaches the listener immediately.
  3. Push events — each event emission calls push() to enqueue the value for the consumer.
  4. Multi-arg handling — if the event emits multiple arguments, they are wrapped in an array. Single arguments are pushed directly.
  5. Cleanup on stop — when the consumer breaks out of the for await loop or calls iterator.return(), stop resolves and the listener is removed.

Trade-offs ​

ApproachProsCons
fromEvent + RepeaterAuto cleanup, composableNeeds Repeater
Node.js on(emitter, event)Built-inNode.js only
Manual listener + queueNo dependencyVerbose, leak-prone
RxJS fromEventRich operatorsLarge bundle

Backpressure consideration: push() without await means events queue unboundedly. For high-frequency events (e.g., mousemove), consider using SlidingBuffer(1) to keep only the latest value:

javascript
import {
  Repeater,
  SlidingBuffer,
} from "@repeaterjs/repeater";

function fromEventLatest(emitter, event) {
  return new Repeater(async (push, stop) => {
    const on =
      (emitter.on ?? emitter.addEventListener)
        .bind(emitter);
    const off =
      (emitter.off ?? emitter.removeEventListener)
        .bind(emitter);
    
    on(event, push);
    await stop;
    off(event, push);
  }, new SlidingBuffer(1));
}