Files
adopt-a-street/backend/SSE_INFRASTRUCTURE.md
William Valentin bb9c8ec1c3 feat: Migrate from Socket.IO to Server-Sent Events (SSE)
- Replace Socket.IO with SSE for real-time server-to-client communication
- Add SSE service with client management and topic-based subscriptions
- Implement SSE authentication middleware and streaming endpoints
- Update all backend routes to emit SSE events instead of Socket.IO
- Create SSE context provider for frontend with EventSource API
- Update all frontend components to use SSE instead of Socket.IO
- Add comprehensive SSE tests for both backend and frontend
- Remove Socket.IO dependencies and legacy files
- Update documentation to reflect SSE architecture

Benefits:
- Simpler architecture using native browser EventSource API
- Lower bundle size (removed socket.io-client dependency)
- Better compatibility with reverse proxies and load balancers
- Reduced resource usage for Raspberry Pi deployment
- Standard HTTP-based real-time communication

🤖 Generated with [AI Assistant]

Co-Authored-By: AI Assistant <noreply@ai-assistant.com>
2025-12-05 22:49:22 -08:00

8.8 KiB

Server-Sent Events (SSE) Infrastructure

This document describes the SSE implementation for real-time server-to-client communication in the Adopt-a-Street backend.

Overview

Server-Sent Events (SSE) provides a unidirectional real-time communication channel from server to client over HTTP. Unlike WebSockets, SSE is built on standard HTTP and automatically handles reconnection.

Architecture

Components

  1. SSE Service (services/sseService.js)

    • Manages client connections and topic subscriptions
    • Provides methods for broadcasting and targeted messaging
    • Handles automatic cleanup on disconnect
  2. SSE Auth Middleware (middleware/sseAuth.js)

    • JWT authentication for SSE connections
    • Supports token from query string, Authorization header, or x-auth-token header
  3. SSE Routes (routes/sse.js)

    • /api/sse/stream - SSE stream endpoint
    • /api/sse/subscribe - Subscribe to topics
    • /api/sse/unsubscribe - Unsubscribe from topics

API Endpoints

GET /api/sse/stream

Opens an SSE connection for the authenticated user.

Authentication: Required (JWT via query parameter ?token=xxx or Authorization header)

Response: SSE stream with events

Example:

const token = localStorage.getItem('token');
const eventSource = new EventSource(`/api/sse/stream?token=${token}`);

eventSource.addEventListener('connected', (e) => {
  console.log('Connected:', JSON.parse(e.data));
});

eventSource.addEventListener('notification', (e) => {
  const data = JSON.parse(e.data);
  console.log('Notification:', data);
});

eventSource.onerror = (error) => {
  console.error('SSE Error:', error);
};

POST /api/sse/subscribe

Subscribe to one or more topics.

Authentication: Required

Body:

{
  "topics": ["events", "posts", "notifications"]
}

Response:

{
  "success": true,
  "msg": "Subscribed to topics",
  "topics": ["events", "posts", "notifications"]
}

POST /api/sse/unsubscribe

Unsubscribe from topics.

Authentication: Required

Body:

{
  "topics": ["events"]
}

Response:

{
  "success": true,
  "msg": "Unsubscribed from topics",
  "topics": ["events"]
}

SSE Service Methods

Client Management

const sseService = require('./services/sseService');

// Add a client (called automatically by /stream endpoint)
sseService.addClient(userId, res);

// Remove a client (called automatically on disconnect)
sseService.removeClient(userId);

Topic Subscriptions

// Subscribe to topics
sseService.subscribe(userId, ['events', 'posts']);

// Unsubscribe from topics
sseService.unsubscribe(userId, ['events']);

Broadcasting Messages

// Broadcast to all connected clients
sseService.broadcast('announcement', {
  message: 'System maintenance in 10 minutes'
});

// Broadcast to topic subscribers
sseService.broadcastToTopic('events', 'eventUpdate', {
  eventId: 123,
  status: 'started'
});

// Send to specific user
sseService.sendToUser(userId, 'notification', {
  text: 'You have a new message',
  priority: 'high'
});

Statistics

// Get service statistics
const stats = sseService.getStats();
// Returns: { totalClients: 5, totalTopics: 3, topics: { events: 3, posts: 2, notifications: 5 } }

Usage in Routes

You can access the SSE service from any route handler:

router.post('/events', auth, async (req, res) => {
  try {
    // Create event...
    const event = await Event.create(req.body);
    
    // Notify subscribers via SSE
    const sse = req.app.get('sse');
    sse.broadcastToTopic('events', 'newEvent', {
      eventId: event._id,
      title: event.title,
      date: event.date
    });
    
    res.json({ success: true, event });
  } catch (error) {
    res.status(500).json({ success: false, msg: 'Server error' });
  }
});

Event Types

SSE messages are sent with specific event types. Clients can listen for specific event types:

eventSource.addEventListener('newEvent', (e) => {
  // Handle new event
});

eventSource.addEventListener('eventUpdate', (e) => {
  // Handle event update
});

eventSource.addEventListener('notification', (e) => {
  // Handle notification
});

Common Topics

Recommended topic names for consistency:

  • events - Event creation, updates, deletions
  • posts - Social feed posts
  • tasks - Task updates
  • notifications - User notifications
  • rewards - Badge and reward notifications
  • leaderboard - Leaderboard changes

Message Format

SSE messages follow this format:

event: eventType
data: {"key": "value"}

The SSE service automatically formats messages correctly.

Connection Management

  • Heartbeat: The server sends a heartbeat comment (:heartbeat) every 30 seconds to keep the connection alive
  • Auto-reconnect: Browsers automatically reconnect if the connection is lost
  • Cleanup: Clients are automatically removed when the connection closes

Health Monitoring

SSE statistics are included in the /api/health endpoint:

{
  "status": "healthy",
  "services": {
    "sse": {
      "totalClients": 5,
      "totalTopics": 3
    }
  }
}

Testing

Run SSE tests:

npm test -- __tests__/routes/sse.test.js

Demo script:

node test-sse-demo.js

Security

  • All SSE connections require JWT authentication
  • Tokens can be passed via query string (for EventSource compatibility) or headers
  • Connections are validated on connect; invalid tokens receive 401 Unauthorized
  • Each user can only have one active SSE connection (new connections replace old ones)

Performance Considerations

  • SSE uses HTTP/1.1 long-polling, so each connection uses one HTTP connection
  • Browser limit: ~6 connections per domain (HTTP/1.1)
  • For high-concurrency scenarios, consider using HTTP/2 (multiplexing) or WebSockets
  • The service is designed for low-memory usage with Map-based storage

Comparison with Socket.IO

The backend supports both SSE and Socket.IO:

Feature SSE Socket.IO
Direction Server → Client Bidirectional
Protocol HTTP WebSocket + HTTP fallback
Browser Support All modern browsers All browsers
Auto-reconnect Built-in Built-in
Use Case Server push notifications Real-time chat, collaboration

When to use SSE:

  • Server needs to push updates to clients
  • Unidirectional communication is sufficient
  • Simpler setup and infrastructure

When to use Socket.IO:

  • Bidirectional real-time communication needed
  • Complex event patterns
  • Existing Socket.IO infrastructure

Example: Complete Client Implementation

class SSEClient {
  constructor(token) {
    this.token = token;
    this.eventSource = null;
    this.connected = false;
  }

  connect() {
    this.eventSource = new EventSource(`/api/sse/stream?token=${this.token}`);
    
    this.eventSource.addEventListener('connected', (e) => {
      console.log('SSE Connected:', JSON.parse(e.data));
      this.connected = true;
      this.subscribeToTopics(['events', 'notifications']);
    });

    this.eventSource.addEventListener('newEvent', (e) => {
      const event = JSON.parse(e.data);
      this.handleNewEvent(event);
    });

    this.eventSource.addEventListener('notification', (e) => {
      const notification = JSON.parse(e.data);
      this.handleNotification(notification);
    });

    this.eventSource.onerror = (error) => {
      console.error('SSE Error:', error);
      this.connected = false;
    };
  }

  async subscribeToTopics(topics) {
    const response = await fetch('/api/sse/subscribe', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'x-auth-token': this.token
      },
      body: JSON.stringify({ topics })
    });
    const data = await response.json();
    console.log('Subscribed:', data);
  }

  handleNewEvent(event) {
    console.log('New event:', event);
    // Update UI with new event
  }

  handleNotification(notification) {
    console.log('Notification:', notification);
    // Show notification to user
  }

  disconnect() {
    if (this.eventSource) {
      this.eventSource.close();
      this.connected = false;
    }
  }
}

// Usage
const sseClient = new SSEClient(localStorage.getItem('token'));
sseClient.connect();

Troubleshooting

Connection not established

  • Check that JWT token is valid and not expired
  • Verify token is passed correctly (query param or header)
  • Check browser console for CORS errors

Not receiving messages

  • Verify client is subscribed to the correct topics
  • Check that server is broadcasting to the correct topic
  • Ensure client is still connected (check eventSource.readyState)

High memory usage

  • Review the number of connected clients
  • Check for memory leaks in client connection handlers
  • Monitor SSE stats via /api/health endpoint