typestar

event-bus.js in JavaScript

A typed-ish event bus: subscribe, emit, unsubscribe, and await one event.

#!/usr/bin/env node

class EventBus {
  #handlers = new Map();

  on(type, handler) {
    if (!this.#handlers.has(type)) this.#handlers.set(type, new Set());
    this.#handlers.get(type).add(handler);
    return () => this.off(type, handler);
  }

  off(type, handler) {
    const handlers = this.#handlers.get(type);
    if (!handlers) return false;
    const removed = handlers.delete(handler);
    if (handlers.size === 0) this.#handlers.delete(type);
    return removed;
  }

  once(type, { signal } = {}) {
    const { promise, resolve, reject } = Promise.withResolvers();
    const stop = this.on(type, (payload) => {
      stop();
      resolve(payload);
    });
    signal?.addEventListener("abort", () => {
      stop();
      reject(signal.reason ?? new Error("aborted"));
    }, { once: true });
    return promise;
  }

  emit(type, payload) {
    const handlers = this.#handlers.get(type);
    if (!handlers) return 0;

    let delivered = 0;
    for (const handler of [...handlers]) {
      try {
        handler(payload);
        delivered += 1;
      } catch (error) {
        this.emit("error", new Error(`handler for ${type} threw`,
                                     { cause: error }));
      }
    }
    return delivered;
  }

  get types() {
    return [...this.#handlers.keys()];
  }
}

const bus = new EventBus();
const seen = [];

const stopRuns = bus.on("run", (payload) => seen.push(payload.tpm));
bus.on("run", () => { throw new Error("bad handler"); });
bus.on("error", (error) => seen.push(`error: ${error.cause.message}`));

console.log("delivered:", bus.emit("run", { tpm: 104 }));
console.log("types:", bus.types.sort());

stopRuns();
console.log("after unsubscribe:", bus.emit("run", { tpm: 98 }));

setTimeout(() => bus.emit("cleared", { set: "Async" }), 5);
const cleared = await bus.once("cleared");
console.log("awaited:", cleared.set);

const controller = new AbortController();
setTimeout(() => controller.abort(new Error("gave up")), 5);
const failed = await bus.once("never", { signal: controller.signal })
  .catch((error) => error.message);

console.log(failed, seen);

How it works

  1. Handlers live in a Map of Sets, so removal is cheap.
  2. once resolves a promise, which composes with await.
  3. A handler that throws must not stop the others.

Keywords and builtins used here

The run, in numbers

Lines
77
Characters to type
1902
Tokens
586
Three-star pace
110 tpm

At the three-star pace of 110 tokens a minute, this run takes about 320 seconds.

Type this snippet

Step 1 of 1 in Encore, step 13 of 13 in The browser.

← Previous