SSE With Event Emitter: A Simple Guide

by Editorial Team 39 views
Iklan Headers

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

  1. 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.
  2. Scalability: By centralizing the event broadcasting logic in a single utility, you can easily scale your application to handle more clients and events.
  3. 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.
  4. 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.ts created: We need a dedicated file for our event emitter.
  • SimpleEventEmitter class with on/off/emit methods: 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 globalEventEmitter singleton 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 called listeners, 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 the listeners Map, 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 the listeners Map, 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 the listeners Map, 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!