DEV Community

Cover image for How I Built a Pub/Sub Library Using Firebase and TypeScript
Francisco Inoque
Francisco Inoque

Posted on

How I Built a Pub/Sub Library Using Firebase and TypeScript

If you've ever worked with real-time messaging systems, you know how crucial it is to have a robust and reliable solution for communication between different parts of a system. In this post, I'll share how I created a Pub/Sub (Publish/Subscribe) library using Firebase and TypeScript, allowing for efficient communication between real-time applications. We'll explore the development process, key features, and how you can start using this library in your project.

Why I Chose Firebase and TypeScript?

Firebase is a powerful platform offering a range of services for web and mobile development, including Firestore, a real-time database ideal for messaging systems. TypeScript is an excellent choice for JavaScript projects because it offers static typing and autocompletion, which helps with code maintainability and scalability.

Overview of the Library

The library I developed, named @savanapoint/pub-sub, allows you to publish and subscribe to messages on specific channels. It provides a simple way to send messages to one or more subscribers and receive real-time notifications.

Key Features

  1. Publish Messages: Send messages to a channel, which will be stored in Firestore and delivered to subscribers.
  2. Subscribe to Channels: Subscribe to specific channels to receive messages in real-time.
  3. Message Management: Mark messages as read after they are processed.

How It Works

Library Structure

The library is composed of three main modules:

  1. index.ts: Firebase configuration and initialization.
  2. publish.ts: Functions for publishing messages.
  3. subscribe.ts: Functions for subscribing to channels and receiving messages.

Firebase Initialization

In the index.ts file, we initialize Firebase and export a function to get the Firestore instance:

import { initializeApp, FirebaseApp } from 'firebase/app';
import { getFirestore, Firestore } from 'firebase/firestore';

let firestoreInstance: Firestore | null = null;

export const initializePubSub = (firebaseConfig: object): void => {
  const app: FirebaseApp = initializeApp(firebaseConfig);
  firestoreInstance = getFirestore(app);
};

export const getFirestoreInstance = (): Firestore => {
  if (!firestoreInstance) {
    throw new Error('Firestore not initialized. Call initializePubSub first.');
  }
  return firestoreInstance;
};
Enter fullscreen mode Exit fullscreen mode

Publishing Messages

In the publish.ts file, we create the function to publish messages to a channel:

import { collection, addDoc, serverTimestamp, Firestore } from 'firebase/firestore';
import { getFirestoreInstance } from './index';

export const publishMessage = async (channel: string, message: string, subscribers: string[]): Promise<void> => {
  try {
    const firestore: Firestore = getFirestoreInstance();

    for (const subscriber of subscribers) {
      await addDoc(collection(firestore, 'channels', channel, 'messages'), {
        message,
        timestamp: serverTimestamp(),
        read: false,
        subscriber
      });
      console.log(`Message published to channel ${channel} for ${subscriber}: ${message}`);
    }
  } catch (err) {
    console.error(`Error publishing message to channel ${channel}:`, err);
  }
};
Enter fullscreen mode Exit fullscreen mode

Subscribing to Channels

In the subscribe.ts file, we create the function to subscribe and receive messages:

import { collection, query, orderBy, onSnapshot, where, updateDoc, doc, Firestore } from 'firebase/firestore';
import { getFirestoreInstance } from './index';
import { Message } from './types';

export const subscribeToChannel = (channel: string, subscriberIds: string[]): Promise<string> => {
  return new Promise((resolve, reject) => {
    const firestore: Firestore = getFirestoreInstance();

    if (!firestore) {
      reject('Firestore instance is not initialized.');
      return;
    }

    subscriberIds.forEach((subscriberId) => {
      const messagesRef = collection(firestore, 'channels', channel, 'messages');
      const q = query(
        messagesRef,
        orderBy('timestamp'),
        where('read', '==', false),
        where('subscriber', '==', subscriberId)
      );

      onSnapshot(q, (snapshot) => {
        snapshot.docChanges().forEach(async (change) => {
          if (change.type === 'added') {
            const messageData = change.doc.data();
            console.log(`Received message from channel ${channel} by ${subscriberId}: ${messageData.message}`);

            try {
              await updateDoc(doc(firestore, 'channels', channel, 'messages', change.doc.id), { read: true });
              resolve(messageData.message);
            } catch (error) {
              console.error(`Error processing message with ID ${change.doc.id}:`, error);
              reject(error);
            }
          }
        });
      }, (err) => {
        console.error('Error listening for messages:', err);
        reject(err);
      });
    });
  });
};
Enter fullscreen mode Exit fullscreen mode

Getting Started

  1. Install the Library:
   npm install @savanapoint/pub-sub
Enter fullscreen mode Exit fullscreen mode
  1. Initialize Firebase:
   import { initializePubSub } from '@savanapoint/pub-sub';

   const firebaseConfig = {
     apiKey: process.env.FIREBASE_API_KEY,
     authDomain: process.env.FIREBASE_AUTH_DOMAIN,
     databaseURL: process.env.FIREBASE_DATABASE_URL,
     projectId: process.env.FIREBASE_PROJECT_ID,
     storageBucket: process.env.FIREBASE_STORAGE_BUCKET,
     messagingSenderId: process.env.FIREBASE_MESSAGING_SENDER_ID,
     appId: process.env.FIREBASE_APP_ID,
   };

   initializePubSub(firebaseConfig);
Enter fullscreen mode Exit fullscreen mode
  1. Publish and Subscribe to Messages:
   import { publishMessage, subscribeToChannel } from '@savanapoint/pub-sub';

   // Publish a message
   await publishMessage('newsletter', 'Welcome to our channel!', ['subscriber1', 'subscriber2']);

   // Subscribe to receive messages
   subscribeToChannel('newsletter', ['subscriber1'])
     .then(message => console.log('Message received:', message))
     .catch(error => console.error('Error receiving message:', error));
Enter fullscreen mode Exit fullscreen mode

Conclusion

Building a Pub/Sub library with Firebase and TypeScript is an effective way to create real-time messaging systems. The @savanapoint/pub-sub library offers a simple and powerful solution for publishing and subscribing to messages, leveraging Firebase's flexibility and TypeScript's robustness. Feel free to explore, modify, and contribute to the library as needed!

If you have any questions or suggestions, feel free to reach out. Happy coding!

Top comments (0)