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
- Publish Messages: Send messages to a channel, which will be stored in Firestore and delivered to subscribers.
- Subscribe to Channels: Subscribe to specific channels to receive messages in real-time.
- Message Management: Mark messages as read after they are processed.
How It Works
Library Structure
The library is composed of three main modules:
-
index.ts
: Firebase configuration and initialization. -
publish.ts
: Functions for publishing messages. -
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;
};
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);
}
};
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);
});
});
});
};
Getting Started
- Install the Library:
npm install @savanapoint/pub-sub
- 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);
- 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));
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)