An event emitter is an object that passes news from one part of a program to any other parts that want it. Code that wants to hear about something registers a function, called a listener, under an event name such as "message". Code that has news emits the event by that name, with any data, and the emitter calls every listener registered under it, passing the data along. Neither side needs to know about the other: the code that emits doesn't know who's listening, or whether anyone is, and listeners can be added and removed without touching it.
Think of a radio station. It broadcasts on its frequency whether anyone has tuned in or not, and every radio tuned to it hears the same thing. Tuning in is on, broadcasting is emit, and switching off is off.
Node has one built in, EventEmitter from the node:events module, and its four main methods are the ones most emitters share:
| Method | What it does |
|---|---|
on(name, listener) | Adds listener for the event name. An event can have any number of listeners. |
once(name, listener) | Adds a listener that runs the next time name is emitted, and is then removed. |
off(name, listener) | Removes a listener. You pass the same function you added, so keep a reference to it: an arrow function written inline can't be removed later. Some emitters also have on return a function that removes the listener, so you don't need to keep it. |
emit(name, ...args) | Calls each of the event's listeners with args, one after another in the order they were added, before emit returns. In Node, a listener written as a regular function gets the emitter as this, and emit returns true if the event had listeners and false if it had none. |
const { EventEmitter } = require("node:events");
const chat = new EventEmitter();
const greet = (name) => console.log("Hello, " + name);
chat.on("join", greet);
chat.once("join", (name) => console.log(name + " was first"));
chat.emit("join", "Ada"); // logs "Hello, Ada", then "Ada was first"; returns true
chat.emit("join", "Alan"); // logs "Hello, Alan" only: the once listener is gone
chat.off("join", greet);
chat.emit("join", "Grace"); // logs nothing and returns false: no listeners are left
In the browser, the same idea is EventTarget, which every element, document, and window inherit from. addEventListener(type, listener) is its on, removeEventListener(type, listener) its off, and addEventListener(type, listener, { once: true }) its once. The browser emits events such as click and input itself, and your code can emit its own with dispatchEvent. Node's streams and servers are EventEmitters too, which is why you write stream.on("data", ...).
A publish/subscribe hub, pub/sub for short, is the same idea under other names. Event names are called topics, often strings such as "user.created"; adding a listener is subscribing, and emitting is publishing. Some hubs also let one subscription match a whole group of topics with a wildcard pattern. An event bus is one emitter or hub that many parts of an app share, so they can talk to each other without importing each other.
Write a class PubSub. Topics are strings of segments separated by dots, such as user.created or order.item.added.
subscribe(pattern, handler) registers handler for every topic that pattern matches. It returns a function that cancels this subscription and no other; calling that function again does nothing.
A pattern is a topic in which any segment may also be *, which matches exactly one segment. The last segment may also be **, which matches one or more segments. Every other segment matches only the same text.
So user.* matches user.created but not user or user.profile.updated, and user.** matches user.created and user.profile.updated but not user.
publish(topic, data) calls the handler of every matching subscription with (data, topic), in the order the subscriptions were made, and returns how many handlers it called. Published topics never contain wildcards.
const bus = new PubSub();
bus.subscribe("user.created", (data) => console.log("welcome", data.name));
bus.subscribe("user.*", (data, topic) => console.log("user event:", topic));
bus.subscribe("**", (data, topic) => console.log("audit:", topic));
bus.publish("user.created", { name: "Ada" }); // 3: all three handlers run
bus.publish("user.profile.updated", {}); // 1: only "**" matches
const bus = new PubSub();
const handler = spy();
bus.subscribe("user.created", handler);
bus.publish("user.created", { id: 1 });
bus.publish("user.deleted", { id: 2 });
expect(handler).toHaveBeenCalledTimes(1);
expect(handler).toHaveBeenCalledWith({ id: 1 }, "user.created");
const bus = new PubSub();
const handler = spy();
bus.subscribe("user.*", handler);
bus.publish("user.created");
bus.publish("user");
bus.publish("user.profile.updated");
expect(handler.calls.map((call) => call.args[1])).toEqual(["user.created"]);
const bus = new PubSub();
const handler = spy();
bus.subscribe("*.created", handler);
bus.subscribe("order.*.added", handler);
bus.publish("user.created");
bus.publish("order.created");
bus.publish("order.item.added");
bus.publish("order.item.removed");
expect(handler.calls.map((call) => call.args[1])).toEqual(["user.created", "order.created", "order.item.added"]);
const bus = new PubSub();
const handler = spy();
bus.subscribe("user.**", handler);
bus.publish("user.created");
bus.publish("user.profile.updated");
bus.publish("user");
bus.publish("order.created");
expect(handler.calls.map((call) => call.args[1])).toEqual(["user.created", "user.profile.updated"]);
const bus = new PubSub();
const handler = spy();
bus.subscribe("**", handler);
bus.publish("a");
bus.publish("a.b.c");
expect(handler).toHaveBeenCalledTimes(2);
const bus = new PubSub();
const handler = spy();
bus.subscribe("user.*", handler);
bus.subscribe("a.b", handler);
bus.publish("users.created");
bus.publish("username");
bus.publish("aXb");
bus.publish("a.b.c");
expect(handler).not.toHaveBeenCalled();
const bus = new PubSub();
const order = [];
bus.subscribe("**", () => order.push("all"));
bus.subscribe("a.b", () => order.push("exact"));
bus.subscribe("a.*", () => order.push("star"));
bus.subscribe("x.y", () => order.push("other"));
expect(bus.publish("a.b")).toBe(3);
expect(order).toEqual(["all", "exact", "star"]);
expect(bus.publish("nothing.here")).toBe(1);
const bus = new PubSub();
const handler = spy();
const cancel = bus.subscribe("a.*", handler);
bus.subscribe("**", handler);
cancel();
expect(bus.publish("a.b")).toBe(1);
expect(handler).toHaveBeenCalledTimes(1);
cancel();
expect(bus.publish("a.b")).toBe(1);
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).
I'm here to help with PubSub with wildcard topics. I give hints, never the solution, so you get to solve it yourself. Ask for a hint, select code to ask about it, or press Ask Tuto why on a failing test.
Your first 3 answers need no account. for free to keep asking.