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>
This commit is contained in:
@@ -0,0 +1,377 @@
|
||||
# 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:**
|
||||
```javascript
|
||||
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:**
|
||||
```json
|
||||
{
|
||||
"topics": ["events", "posts", "notifications"]
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"msg": "Subscribed to topics",
|
||||
"topics": ["events", "posts", "notifications"]
|
||||
}
|
||||
```
|
||||
|
||||
### POST /api/sse/unsubscribe
|
||||
|
||||
Unsubscribe from topics.
|
||||
|
||||
**Authentication:** Required
|
||||
|
||||
**Body:**
|
||||
```json
|
||||
{
|
||||
"topics": ["events"]
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"msg": "Unsubscribed from topics",
|
||||
"topics": ["events"]
|
||||
}
|
||||
```
|
||||
|
||||
## SSE Service Methods
|
||||
|
||||
### Client Management
|
||||
|
||||
```javascript
|
||||
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
|
||||
|
||||
```javascript
|
||||
// Subscribe to topics
|
||||
sseService.subscribe(userId, ['events', 'posts']);
|
||||
|
||||
// Unsubscribe from topics
|
||||
sseService.unsubscribe(userId, ['events']);
|
||||
```
|
||||
|
||||
### Broadcasting Messages
|
||||
|
||||
```javascript
|
||||
// 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
|
||||
|
||||
```javascript
|
||||
// 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:
|
||||
|
||||
```javascript
|
||||
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:
|
||||
|
||||
```javascript
|
||||
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:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "healthy",
|
||||
"services": {
|
||||
"sse": {
|
||||
"totalClients": 5,
|
||||
"totalTopics": 3
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
Run SSE tests:
|
||||
|
||||
```bash
|
||||
npm test -- __tests__/routes/sse.test.js
|
||||
```
|
||||
|
||||
Demo script:
|
||||
|
||||
```bash
|
||||
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
|
||||
|
||||
```javascript
|
||||
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
|
||||
Reference in New Issue
Block a user