Skip to the code
cf.

PubSub with wildcard topics

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

What the tests check

  1. calls a handler for its exact topic with the data and the topic
    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");
    
  2. * matches exactly one segment
    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"]);
    
  3. * can stand for any segment, not only the last
    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"]);
    
  4. ** at the end matches one or more segments
    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"]);
    
  5. ** on its own matches every topic
    const bus = new PubSub();
    const handler = spy();
    bus.subscribe("**", handler);
    bus.publish("a");
    bus.publish("a.b.c");
    expect(handler).toHaveBeenCalledTimes(2);
    
  6. segments match whole, and dots aren't wildcards
    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();
    
  7. publish calls handlers in subscription order and returns how many ran
    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);
    
  8. the returned function cancels only its own subscription
    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);
    

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