Skip to the code
cf.

Wait for an event, with a timeout

Write once(emitter, event, options). emitter is an object with on(name, listener) and off(name, listener) methods, like Node's EventEmitter. When it emits an event with emitter.emit(name, ...args), it calls every listener for name with args.

once returns a promise that fulfills the next time emitter emits event, with an array of the arguments that event came with. Other events don't affect it.

If emitter emits "error" first, the promise rejects with that event's first argument, the error. When event is "error" itself, it's the event you're waiting for, so the promise fulfills with [error].

options is optional, and so is its timeout, in milliseconds. If timeout ms pass before the event, the promise rejects with an Error whose name is "TimeoutError" (the message is up to you). Without a timeout, it waits for as long as it takes.

However the promise settles, remove every listener once added and clear its timer, so nothing is left behind.

const server = new EventEmitter(); // Node's, or any object with on, off, and emit
 
const ready = once(server, "listening", { timeout: 1000 });
server.emit("listening", 8080, "localhost");
await ready; // [8080, "localhost"]
 
const started = once(server, "listening");
server.emit("error", new Error("port in use"));
await started; // rejects with Error: port in use
 
const slow = once(server, "listening", { timeout: 1000 });
await slow; // after 1000 ms, rejects with an Error named "TimeoutError"

What the tests check

  1. fulfills with an array of the event's arguments
    let listeners = [];
    const emitter = {
      on: (name, fn) => listeners.push({ name, fn }),
      off: (name, fn) => (listeners = listeners.filter((l) => l.name !== name || l.fn !== fn)),
      emit: (name, ...args) => listeners.filter((l) => l.name === name).forEach((l) => l.fn(...args)),
    };
    const promise = once(emitter, "data");
    emitter.emit("data", "chunk", 2);
    await expect(promise).resolves.toEqual(["chunk", 2]);
    
  2. waits for that event and ignores others
    let listeners = [];
    const emitter = {
      on: (name, fn) => listeners.push({ name, fn }),
      off: (name, fn) => (listeners = listeners.filter((l) => l.name !== name || l.fn !== fn)),
      emit: (name, ...args) => listeners.filter((l) => l.name === name).forEach((l) => l.fn(...args)),
    };
    let result;
    once(emitter, "data").then((value) => {
      result = value;
    });
    emitter.emit("progress", 50);
    emitter.emit("end");
    await flush();
    expect(result).toBeUndefined();
    emitter.emit("data", "chunk");
    await flush();
    expect(result).toEqual(["chunk"]);
    
  3. rejects with the error when "error" is emitted first
    let listeners = [];
    const emitter = {
      on: (name, fn) => listeners.push({ name, fn }),
      off: (name, fn) => (listeners = listeners.filter((l) => l.name !== name || l.fn !== fn)),
      emit: (name, ...args) => listeners.filter((l) => l.name === name).forEach((l) => l.fn(...args)),
    };
    const error = new Error("connection reset");
    let outcome = "still pending";
    once(emitter, "data").then(
      (value) => (outcome = value),
      (reason) => (outcome = reason),
    );
    emitter.emit("error", error);
    await flush();
    expect(outcome).toBe(error);
    
  4. fulfills with [error] when it's waiting for "error" itself
    let listeners = [];
    const emitter = {
      on: (name, fn) => listeners.push({ name, fn }),
      off: (name, fn) => (listeners = listeners.filter((l) => l.name !== name || l.fn !== fn)),
      emit: (name, ...args) => listeners.filter((l) => l.name === name).forEach((l) => l.fn(...args)),
    };
    const error = new Error("connection reset");
    const promise = once(emitter, "error");
    emitter.emit("error", error);
    await expect(promise).resolves.toHaveLength(1);
    expect((await promise)[0]).toBe(error);
    
  5. rejects with a TimeoutError once timeout ms pass
    const clock = useFakeTimers();
    let listeners = [];
    const emitter = {
      on: (name, fn) => listeners.push({ name, fn }),
      off: (name, fn) => (listeners = listeners.filter((l) => l.name !== name || l.fn !== fn)),
      emit: (name, ...args) => listeners.filter((l) => l.name === name).forEach((l) => l.fn(...args)),
    };
    let reason;
    once(emitter, "data", { timeout: 100 }).catch((e) => {
      reason = e;
    });
    await clock.tickAsync(99);
    expect(reason).toBeUndefined();
    await clock.tickAsync(1);
    expect(reason).toBeInstanceOf(Error);
    expect(reason.name).toBe("TimeoutError");
    
  6. waits as long as it takes when there's no timeout
    const clock = useFakeTimers();
    let listeners = [];
    const emitter = {
      on: (name, fn) => listeners.push({ name, fn }),
      off: (name, fn) => (listeners = listeners.filter((l) => l.name !== name || l.fn !== fn)),
      emit: (name, ...args) => listeners.filter((l) => l.name === name).forEach((l) => l.fn(...args)),
    };
    let settled = false;
    const promise = once(emitter, "data");
    promise.then(
      () => (settled = true),
      () => (settled = true),
    );
    await clock.tickAsync(60000);
    expect(settled).toBe(false);
    emitter.emit("data", 1);
    await expect(promise).resolves.toEqual([1]);
    
  7. removes its listeners and clears its timer once the event comes
    const clock = useFakeTimers();
    let listeners = [];
    const emitter = {
      on: (name, fn) => listeners.push({ name, fn }),
      off: (name, fn) => (listeners = listeners.filter((l) => l.name !== name || l.fn !== fn)),
      emit: (name, ...args) => listeners.filter((l) => l.name === name).forEach((l) => l.fn(...args)),
    };
    const promise = once(emitter, "data", { timeout: 1000 });
    emitter.emit("data");
    await promise;
    expect(listeners).toHaveLength(0);
    expect(clock.pending()).toBe(0);
    
  8. cleans up after an "error" event too
    const clock = useFakeTimers();
    let listeners = [];
    const emitter = {
      on: (name, fn) => listeners.push({ name, fn }),
      off: (name, fn) => (listeners = listeners.filter((l) => l.name !== name || l.fn !== fn)),
      emit: (name, ...args) => listeners.filter((l) => l.name === name).forEach((l) => l.fn(...args)),
    };
    const promise = once(emitter, "data", { timeout: 1000 });
    promise.catch(() => {}); // it rejects; this test only checks the cleanup
    emitter.emit("error", new Error("closed"));
    await flush();
    expect(listeners).toHaveLength(0);
    expect(clock.pending()).toBe(0);
    
  9. cleans up after a timeout too
    const clock = useFakeTimers();
    let listeners = [];
    const emitter = {
      on: (name, fn) => listeners.push({ name, fn }),
      off: (name, fn) => (listeners = listeners.filter((l) => l.name !== name || l.fn !== fn)),
      emit: (name, ...args) => listeners.filter((l) => l.name === name).forEach((l) => l.fn(...args)),
    };
    const promise = once(emitter, "data", { timeout: 100 });
    promise.catch(() => {}); // it rejects; this test only checks the cleanup
    await clock.tickAsync(100);
    expect(listeners).toHaveLength(0);
    

Learn more

solution.jsSaved in this browser

Run tests runs every test on your code, here in your browser. The problem is solved when they all pass.

Ctrl + Enter runs the tests (⌘ + Enter on a Mac).