Skip to the code
cf.

Event emitter

Write a class EventEmitter with four methods.

on(event, listener) adds a listener for event and returns a function that removes it. off(event, listener) removes a listener, including one added with once. once(event, listener) adds a listener that runs on the next emit of that event only.

emit(event, ...args) calls the event's listeners with args, in the order they were added, with this set to the emitter. It returns true if the event had listeners and false if it had none. Listeners added or removed while an emit is running don't change who runs in that emit: the list is fixed when it starts.

const emitter = new EventEmitter();
const stop = emitter.on("message", (text) => console.log("got", text));
emitter.once("message", () => console.log("first message only"));
 
emitter.emit("message", "hi"); // logs "got hi", then "first message only"
emitter.emit("message", "yo"); // logs "got yo"
stop();
emitter.emit("message", "?");  // false: no listeners left

What the tests check

  1. calls listeners with the arguments, in the order they were added
    const emitter = new EventEmitter();
    const log = [];
    emitter.on("x", (a, b) => log.push(["first", a, b]));
    emitter.on("x", (a) => log.push(["second", a]));
    emitter.emit("x", 1, 2);
    expect(log).toEqual([["first", 1, 2], ["second", 1]]);
    
  2. emit returns whether the event had listeners
    const emitter = new EventEmitter();
    emitter.on("a", () => {});
    expect(emitter.emit("a")).toBe(true);
    expect(emitter.emit("b")).toBe(false);
    const stop = emitter.on("c", () => {});
    stop();
    expect(emitter.emit("c")).toBe(false);
    
  3. calls listeners, once listeners included, with this set to the emitter
    const emitter = new EventEmitter();
    const listener = spy();
    const onceListener = spy();
    emitter.on("x", listener);
    emitter.once("y", onceListener);
    emitter.emit("x");
    emitter.emit("y");
    expect(listener).toHaveBeenCalledTimes(1);
    expect(listener.calls[0].this).toBe(emitter);
    expect(onceListener).toHaveBeenCalledTimes(1);
    expect(onceListener.calls[0].this).toBe(emitter);
    
  4. off removes a listener
    const emitter = new EventEmitter();
    const removed = spy();
    const kept = spy();
    emitter.on("x", removed);
    emitter.on("x", kept);
    emitter.off("x", removed);
    emitter.emit("x");
    expect(removed).not.toHaveBeenCalled();
    expect(kept).toHaveBeenCalledTimes(1);
    
  5. on returns a function that removes the listener
    const emitter = new EventEmitter();
    const listener = spy();
    const stop = emitter.on("x", listener);
    emitter.emit("x");
    stop();
    emitter.emit("x");
    expect(listener).toHaveBeenCalledTimes(1);
    
  6. a once listener runs on the next emit only, even if it emits again
    const emitter = new EventEmitter();
    const listener = spy();
    emitter.once("x", listener);
    emitter.emit("x", 1);
    emitter.emit("x", 2);
    expect(listener).toHaveBeenCalledTimes(1);
    expect(listener).toHaveBeenCalledWith(1);
    expect(emitter.emit("x")).toBe(false);
    let runs = 0;
    emitter.once("y", () => {
      runs++;
      emitter.emit("y");
    });
    emitter.emit("y");
    expect(runs).toBe(1);
    
  7. off removes a once listener before it runs
    const emitter = new EventEmitter();
    const listener = spy();
    emitter.once("x", listener);
    emitter.off("x", listener);
    expect(emitter.emit("x")).toBe(false);
    expect(listener).not.toHaveBeenCalled();
    
  8. removing a listener during an emit doesn't skip anyone in that emit
    const emitter = new EventEmitter();
    const log = [];
    const b = () => log.push("b");
    emitter.on("x", () => {
      log.push("a");
      emitter.off("x", b);
    });
    emitter.on("x", b);
    emitter.on("x", () => log.push("c"));
    emitter.emit("x");
    emitter.emit("x");
    expect(log).toEqual(["a", "b", "c", "a", "c"]);
    
  9. a listener added during an emit waits for the next one
    const emitter = new EventEmitter();
    const late = spy();
    emitter.on("x", () => emitter.on("x", late));
    emitter.emit("x");
    expect(late).not.toHaveBeenCalled();
    emitter.emit("x");
    expect(late).toHaveBeenCalledTimes(1);
    

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).