- 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>
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
-
SSE Service (
services/sseService.js)- Manages client connections and topic subscriptions
- Provides methods for broadcasting and targeted messaging
- Handles automatic cleanup on disconnect
-
SSE Auth Middleware (
middleware/sseAuth.js)- JWT authentication for SSE connections
- Supports token from query string, Authorization header, or x-auth-token header
-
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, deletionsposts- Social feed poststasks- Task updatesnotifications- User notificationsrewards- Badge and reward notificationsleaderboard- 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/healthendpoint