SSE With Event Emitter: A Simple Guide
Hey guys! Today, we're diving into creating a global event emitter utility to enable Server-Sent Events (SSE) broadcasting across your API routes. This is super useful for real-time updates and keeping your clients in sync without constant polling. Let's get started!
What is an Event Emitter?
An event emitter is a design pattern that allows you to create objects that can emit events, which other parts of your application can listen for. Think of it like a radio station (the emitter) broadcasting signals, and various radios (the listeners) tuning in to receive those signals. In our case, we'll use it to broadcast events from our API routes to connected SSE clients.
Why Use an Event Emitter with SSE?
Using an event emitter with SSE provides a clean and efficient way to push updates to clients in real-time. Instead of having each API route manage its own connections and updates, you can simply emit an event, and the event emitter will take care of notifying all connected clients. This simplifies your code, reduces redundancy, and makes your application more scalable.
For instance, consider scenarios where you have events like classification_complete, item_accepted, item_corrected, or queue_updated. Without an event emitter, each API endpoint responsible for these events would need to directly manage sending updates to all connected clients. This quickly becomes cumbersome and hard to maintain. With an event emitter, you can simply emit these events from the relevant API routes, and the event emitter will handle the broadcasting to all SSE clients.
Benefits of Using an Event Emitter
- Decoupling: Event emitters decouple the components that emit events from the components that handle them. This means that you can change the way events are handled without affecting the code that emits them, and vice versa.
- Scalability: By centralizing the event broadcasting logic in a single utility, you can easily scale your application to handle more clients and events.
- Maintainability: Event emitters make your code easier to maintain by reducing redundancy and complexity. You only need to define the event handling logic once, and then reuse it throughout your application.
- Real-Time Updates: SSE with event emitters ensures that clients receive updates in real-time, providing a better user experience.
Acceptance Criteria
Before we start coding, let's make sure we know what we're aiming for. Here’s what we need to achieve:
lib/event-emitter.tscreated: We need a dedicated file for our event emitter.SimpleEventEmitterclass withon/off/emitmethods: This class will handle the core logic of our event emitter.- Manages listeners
Map<string, Set<Function>>: We'll use a Map to store listeners for each event. - Exports
globalEventEmittersingleton instance: We need a single, global instance of our event emitter that can be used throughout the application.
Implementation: lib/event-emitter.ts
Okay, let's dive into the code! We'll create a file named lib/event-emitter.ts and implement our SimpleEventEmitter class.
Step 1: Define the SimpleEventEmitter Class
First, we'll define the SimpleEventEmitter class with the necessary methods (on, off, and emit). This class will manage the listeners and handle event broadcasting.
class SimpleEventEmitter {
private listeners: Map<string, Set<Function>> = new Map();
on(event: string, listener: Function): void {
if (!this.listeners.has(event)) {
this.listeners.set(event, new Set());
}
this.listeners.get(event)!.add(listener);
}
off(event: string, listener: Function): void {
if (this.listeners.has(event)) {
this.listeners.get(event)!.delete(listener);
if (this.listeners.get(event)!.size === 0) {
this.listeners.delete(event);
}
}
}
emit(event: string, ...args: any[]): void {
if (this.listeners.has(event)) {
this.listeners.get(event)!.forEach(listener => {
listener(...args);
});
}
}
}
Step 2: Explanation of the Code
Let's break down what each part of the code does:
private listeners: Map<string, Set<Function>> = new Map();: This line declares a private property calledlisteners, which is a Map. The keys of the Map are strings (the event names), and the values are Sets of Functions (the listeners for each event).on(event: string, listener: Function): void: This method is used to add a new listener for a specific event. It takes two arguments: the name of the event and the listener function. If the event doesn't already exist in thelistenersMap, it creates a new Set for that event. Then, it adds the listener function to the Set.off(event: string, listener: Function): void: This method is used to remove a listener for a specific event. It takes two arguments: the name of the event and the listener function. If the event exists in thelistenersMap, it removes the listener function from the Set. If the Set becomes empty after removing the listener, it also removes the event from the Map.emit(event: string, ...args: any[]): void: This method is used to emit an event. It takes one required argument: the name of the event, and then any number of additional arguments (...args). If the event exists in thelistenersMap, it iterates over the Set of listeners for that event and calls each listener function with the provided arguments.
Step 3: Create and Export the globalEventEmitter Instance
Now, we'll create a single instance of our SimpleEventEmitter and export it so it can be used throughout the application.
const globalEventEmitter = new SimpleEventEmitter();
export default globalEventEmitter;
Step 4: Complete lib/event-emitter.ts
Here’s the complete code for lib/event-emitter.ts:
class SimpleEventEmitter {
private listeners: Map<string, Set<Function>> = new Map();
on(event: string, listener: Function): void {
if (!this.listeners.has(event)) {
this.listeners.set(event, new Set());
}
this.listeners.get(event)!.add(listener);
}
off(event: string, listener: Function): void {
if (this.listeners.has(event)) {
this.listeners.get(event)!.delete(listener);
if (this.listeners.get(event)!.size === 0) {
this.listeners.delete(event);
}
}
}
emit(event: string, ...args: any[]): void {
if (this.listeners.has(event)) {
this.listeners.get(event)!.forEach(listener => {
listener(...args);
});
}
}
}
const globalEventEmitter = new SimpleEventEmitter();
export default globalEventEmitter;
How to Use the Event Emitter
Now that we have our event emitter, let's see how to use it in practice.
Step 1: Import the globalEventEmitter
First, you need to import the globalEventEmitter in any file where you want to emit or listen for events.
import globalEventEmitter from './lib/event-emitter';
Step 2: Emit an Event
To emit an event, simply call the emit method on the globalEventEmitter instance, passing the event name and any data you want to send.
globalEventEmitter.emit('item_accepted', { itemId: 123, status: 'accepted' });
Step 3: Listen for an Event
To listen for an event, use the on method, providing the event name and a callback function to be executed when the event is emitted.
globalEventEmitter.on('item_accepted', (data: any) => {
console.log('Item accepted:', data);
// Perform actions based on the event data
});
Step 4: Stop Listening to an Event
If you no longer want to listen for an event, you can use the off method to remove the listener.
const listener = (data: any) => {
console.log('Item accepted:', data);
// Perform actions based on the event data
};
globalEventEmitter.on('item_accepted', listener);
// Later, when you want to stop listening:
globalEventEmitter.off('item_accepted', listener);
Implementation Notes
This utility enables all API routes to broadcast events (e.g., classification_complete, item_accepted, item_corrected, queue_updated) to connected SSE clients. By using a global event emitter, we ensure that any part of our application can easily communicate with the SSE clients without needing to manage the connections directly.
The estimated LOC (Lines of Code) for this utility is approximately 30, which is included in the overall 60 LOC SSE estimate. This keeps our codebase lean and manageable.
Dependencies
Good news – this utility is foundational and doesn't rely on any external dependencies. It's pure TypeScript, making it lightweight and easy to integrate into any project.
Conclusion
And there you have it! We've successfully created a global event emitter utility for SSE. This will help you build real-time applications with ease, keeping your clients updated without unnecessary overhead. Feel free to experiment with different events and data structures to fit your specific needs. Happy coding, and let me know if you have any questions!