javascript 96 lines · 3 tabs

Server-Sent Events for Live Notifications with a Reconnectable EventSource Hook

Shared by codesnips Jul 2026
3 tabs
const { EventEmitter } = require('events');

class NotificationBus extends EventEmitter {
  constructor(bufferSize = 100) {
    super();
    this.setMaxListeners(0);
    this.seq = 0;
    this.recent = [];
    this.bufferSize = bufferSize;
  }

  publish(userId, type, payload) {
    const event = { id: ++this.seq, userId, type, payload, ts: Date.now() };
    this.recent.push(event);
    if (this.recent.length > this.bufferSize) this.recent.shift();
    this.emit(`user:${userId}`, event);
    return event;
  }

  subscribe(userId, listener) {
    const channel = `user:${userId}`;
    this.on(channel, listener);
    return () => this.off(channel, listener);
  }

  replaySince(userId, lastId) {
    if (!lastId) return [];
    return this.recent.filter((e) => e.userId === userId && e.id > lastId);
  }
}

module.exports = new NotificationBus();
3 files · javascript Explain with highlit

This snippet shows how to push live notifications from a Node/Express server to browsers using Server-Sent Events (SSE), a one-way streaming protocol built on a long-lived HTTP response. Unlike WebSockets, SSE runs over plain HTTP, carries a simple text/event-stream format, and gets automatic reconnection from the browser for free — making it ideal for a notification feed where only the server needs to push.

The notificationBus file is a tiny in-process pub/sub built on Node's EventEmitter. Each connected client registers a listener keyed by userId, and publish fans an event out to every listener for that user. It also keeps a small ring buffer of recent events in recent so a client that reconnects can replay anything it missed. Every event carries a monotonically increasing id, which is the key to reliable delivery.

The sseRouter file is where the protocol details live. GET /events sets the SSE headers — Content-Type: text/event-stream, no-cache, and keep-alive — and disables buffering with X-Accel-Buffering so proxies flush each chunk immediately. The write helper serializes each event into the wire format: an id: line, an event: line, and one or more data: lines terminated by a blank line. On connect it reads the standard Last-Event-ID header and calls bus.replaySince so no notification is dropped across a reconnect. A setInterval sends comment-only :ping lines to keep intermediaries from timing the connection out, and the req.on('close') handler unsubscribes and clears the timer to prevent leaks.

The useNotifications hook wraps the browser EventSource. It listens for the named notification event, parses the JSON payload, and prepends it to state. Because EventSource reconnects on its own and replays Last-Event-ID automatically, the hook stays small — it only tracks a connected flag via onopen/onerror and tears the stream down on unmount.

The trade-offs: SSE is unidirectional and, over HTTP/1.1, subject to the ~6-connection-per-origin limit, so it suits notifications far better than chat. The EventEmitter bus works only within a single process; scaling horizontally requires backing publish with Redis pub/sub or similar so every instance sees every event.


Related snips

Share this code

Here's the card — post it anywhere.

Server-Sent Events for Live Notifications with a Reconnectable EventSource Hook — share card
Link copied