← Publish / subscribe lesson
Lab

Publish / subscribe

// A message on a topic: what happened, and to which draft.
type Message = { topic: string; draft: string };

// A subscription: the topics it wants, and its own inbox of unread copies.
type Subscription = { id: number; topics: string[]; inbox: Message[] };

// The broker: publishers and subscribers know it, never each other.
class Broker {
  nextId = 1;
  subscriptions: Subscription[] = [];

  subscribe(topics: string[]): number {
    const id = this.nextId++;
    this.subscriptions.push({ id, topics, inbox: [] });
    return id;
  }

  // Leaves a copy in every subscription that wants this topic.
  publish(topic: string, draft: string): number {
    let copies = 0;
    for (const subscription of this.subscriptions) {
      if (subscription.topics.includes(topic)) {
        subscription.inbox.push({ topic, draft });
        copies++;
      }
    }
    return copies;
  }

  // Hands one subscriber its unread copies, oldest first, and empties its inbox.
  drain(id: number): Message[] {
    const subscription = this.subscriptions.find((s) => s.id === id);
    if (!subscription) return [];
    const messages = subscription.inbox;
    subscription.inbox = [];
    return messages;
  }
}

// The publisher: it knows the broker and the topic names, nothing else.
class Editor {
  broker: Broker;
  constructor(broker: Broker) {
    this.broker = broker;
  }
  save(draft: string) {
    this.broker.publish('draft.saved', draft);
  }
  remove(draft: string) {
    this.broker.publish('draft.deleted', draft);
  }
}

const broker = new Broker();
const editor = new Editor(broker);

// Two subscribers. Each knows the broker and its topics, not the editor.
const activity = broker.subscribe(['draft.saved']);
const search = broker.subscribe(['draft.saved', 'draft.deleted']);

// The writer saves the pricing page; the activity panel reads at once.
editor.save('pricing');
const feed = broker.drain(activity);

// Then deletes the page, and restores it from the trash.
editor.remove('pricing');
editor.save('pricing');

// The search index catches up later, in one batch.
const indexed = new Set<string>();
for (const message of broker.drain(search)) {
  if (message.topic === 'draft.saved') indexed.add(message.draft);
  else indexed.delete(message.draft);
}
const found = indexed.has('pricing');
const later = broker.drain(activity);

console.log(`activity: read ${feed.length}, then ${later.length}`);
console.log(`search index has pricing: ${found}`);