JavaScript implementation of the Publish-Subscribe Pattern using ES6

A detailed guide to creating a message broker with the PubSub class

Publish-subscribe is a messaging pattern where publishers send messages to a message broker, which then delivers the message to subscribed consumers. This pattern allows for decoupling between the publisher and consumer, as the publisher does not need to know the specifics of who is consuming its messages.

In this post, we will walk through an implementation of the publish-subscribe pattern in JavaScript using ES6 syntax. We will be creating a PubSub class that will handle the message broker functionality.

class PubSub {
  constructor() {
    this.subscribers = {};
  }

  subscribe(topic, callback) {
    if (!this.subscribers[topic]) {
      this.subscribers[topic] = [];
    }
    this.subscribers[topic].push(callback);
  }
}

Next, let's define a publish method that will be used by publishers to send messages to a particular topic. This method should loop through all the subscribers for that topic and execute their callback functions with the provided data:

class PubSub {
  // ...

  publish(topic, data) {
    if (this.subscribers[topic]) {
      this.subscribers[topic].forEach((callback) => {
        callback(data);
      });
    }
  }
}

Now we have the basic structure of our PubSub class set up. Let's see how we can use it in practice.

First, we will create a new instance of our PubSub class and define a few subscribers:

const pubsub = new PubSub();

const subscriber1 = (data) => {
  console.log(`Received data: ${data}`);
};

const subscriber2 = (data) => {
  console.log(`Received data: ${data}`);
};

pubsub.subscribe('topic1', subscriber1);
pubsub.subscribe('topic1', subscriber2);

Now we have two subscribers for the topic 'topic1'. We can publish a message to this topic by using the publish method:

pubsub.publish('topic1', 'Hello, world!');
Received data: Hello, world!
Received data: Hello, world!

We can also define a third subscriber for a different topic:

const subscriber3 = (data) => {
  console.log(`Received data: ${data}`);
};

pubsub.subscribe('topic2', subscriber3);

If we publish a message to 'topic2', only subscriber3 will receive it:

Output:Received data: Hello, again!

To finish off our PubSub class, let's add an unsubscribe method that allows subscribers to unsubscribe from a particular topic. This method should remove the specified callback from the list of subscribers for that topic:

class PubSub {
  // ...

  unsubscribe(topic, callback) {
    if (this.subscribers[topic]) {
      this.subscribers[topic] = this.subscribers[topic].filter(
        (cb) => cb !== callback
      );
    }
  }
}

Now we have a fully functional PubSub class that implements the publish-subscribe pattern in JavaScript. Here is the complete code:

class PubSub {
  constructor() {
    this.subscribers = {};
  }

  subscribe(topic, callback) {
    if (!this.subscribers[topic]) {
      this.subscribers[topic] = [];
    }
    this.subscribers[topic].push(callback);
  }

  publish(topic, data) {
    if (this.subscribers[topic]) {
      this.subscribers[topic].forEach((callback) => {
        callback(data);
      });
    }
  }

  unsubscribe(topic, callback) {
    if (this.subscribers[topic]) {
      this.subscribers[topic] = this.subscribers[topic].filter(
        (cb) => cb !== callback
      );
    }
  }
}

With this implementation, publishers can send messages to a message broker (our PubSub instance) and subscribed consumers will receive those messages. This allows for decoupling between the publisher and consumer and makes it easy to add or remove subscribers without affecting the publisher.

Scenario: Social media

Here is an example of how the PubSub class could be used in a real-world scenario.
Imagine that we have a social media application with a feed that displays posts from the users that a user is following. We could use the publish-subscribe pattern to decouple the feed component from the logic for retrieving and displaying the posts.

First, let's define a Feed class that subscribes to the 'new-post' topic and adds the received post to the feed:

class Feed {
  constructor() {
    this.posts = [];
  }

  addPost(post) {
    this.posts.unshift(post);
  }

  render() {
    const feedContainer = document.getElementById('feed');
    this.posts.forEach((post) => {
      const postElement = document.createElement('div');
      postElement.innerHTML = post.content;
      feedContainer.appendChild(postElement);
    });
  }
}

Next, let's create a PostService class that retrieves new posts from the server and publishes them to the 'new-post' topic:

class PostService {
  constructor() {
    this.pubsub = new PubSub();
  }

  getNewPosts() {
    // retrieve new posts from the server
    const newPosts = [
      {
        id: 1,
        content: 'Hello, world!',
      },
      {
        id: 2,
        content: 'This is my first post',
      },
    ];

    // publish each new post to the 'new-post' topic
    newPosts.forEach((post) => {
      this.pubsub.publish('new-post', post);
    });
  }
}

Finally, we can create a new Feed instance and a new PostService instance, and subscribe the feed to the 'new-post' topic:

const feed = new Feed();
const postService = new PostService();

postService.pubsub.subscribe('new-post', (post) => {
  feed.addPost(post);
  feed.render();
});

postService.getNewPosts();

This will retrieve the new posts from the server and publish them to the 'new-post' topic. The feed will receive the posts and add them to its list of posts, and then re-render the feed to display the new posts.

This allows for a separation of concerns between the Feed class and the PostService class, as the Feed class does not need to know how the posts are retrieved, and the PostService class does not need to know how the posts are displayed. The publish-subscribe pattern allows them to communicate through the message broker (our PubSub instance) and remain decoupled.

Scenario: Task management

Imagine that we have a task management application with a calendar that displays the due dates for tasks. We could use the publish-subscribe pattern to decouple the calendar component from the logic for retrieving and displaying the tasks.

First, let's define a Calendar class that subscribes to the 'new-task' topic and adds the received task to the calendar:

class Calendar {
  constructor() {
    this.tasks = [];
  }

  addTask(task) {
    this.tasks.push(task);
  }

  render() {
    const calendarContainer = document.getElementById('calendar');
    this.tasks.forEach((task) => {
      const taskElement = document.createElement('div');
      taskElement.innerHTML = `${task.name} - ${task.dueDate}`;
      calendarContainer.appendChild(taskElement);
    });
  }
}

Next, let's create a TaskService class that retrieves new tasks from the server and publishes them to the 'new-task' topic:

class TaskService {
  constructor() {
    this.pubsub = new PubSub();
  }

  getNewTasks() {
    // retrieve new tasks from the server
    const newTasks = [
      {
        id: 1,
        name: 'Task 1',
        dueDate: '01/01/2024',
      },
      {
        id: 2,
        name: 'Task 2',
        dueDate: '01/02/2024',
      },
    ];

    // publish each new task to the 'new-task' topic
    newTasks.forEach((task) => {
      this.pubsub.publish('new-task', task);
    });
  }
}

Finally, we can create a new Calendar instance and a new TaskService instance, and subscribe the calendar to the 'new-task' topic:

const calendar = new Calendar();
const taskService = new TaskService();

taskService.pubsub.subscribe('new-task', (task) => {
  calendar.addTask(task);
  calendar.render();
});

taskService.getNewTasks();

This will retrieve the new tasks from the server and publish them to the 'new-task' topic. The calendar will receive the tasks and add them to its list of tasks, and then re-render the calendar to display the new tasks.

This allows for a separation of concerns between the Calendar class and the TaskService class, as the Calendar class does not need to know how the tasks are retrieved, and the TaskService class does not need to know how the tasks are displayed. The publish-subscribe pattern allows them to communicate through the message broker (our PubSub instance) and remain decoupled.

This covers the basics of implementing the publish-subscribe pattern in JavaScript with the PubSub class. The publish-subscribe pattern is a powerful way to decouple components in a system and can be used in a variety of real-world scenarios.

I hope this post has helped understand how the publish-subscribe pattern works and how it can be implemented in JavaScript. Let's conclude the post here. If you have any further questions, feel free to ask.

Reach me out
Twitter
GitHub