Files
adopt-a-street/backend/routes/sse.js
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

138 lines
3.5 KiB
JavaScript

const express = require("express");
const router = express.Router();
const sseAuth = require("../middleware/sseAuth");
const sseService = require("../services/sseService");
const logger = require("../utils/logger");
/**
* @route GET /api/sse/stream
* @desc SSE stream endpoint
* @access Private
*/
router.get("/stream", sseAuth, (req, res) => {
const userId = req.user.id;
// Set SSE headers
res.setHeader("Content-Type", "text/event-stream");
res.setHeader("Cache-Control", "no-cache");
res.setHeader("Connection", "keep-alive");
res.setHeader("X-Accel-Buffering", "no"); // Disable nginx buffering
// Send initial connection success message
res.write(`event: connected\ndata: ${JSON.stringify({ userId, timestamp: new Date().toISOString() })}\n\n`);
// Register client
sseService.addClient(userId, res);
// Send heartbeat every 30 seconds to keep connection alive
const heartbeatInterval = setInterval(() => {
try {
res.write(`:heartbeat\n\n`);
} catch (error) {
logger.error(`Heartbeat failed for user`, { userId, error: error.message });
clearInterval(heartbeatInterval);
sseService.removeClient(userId);
}
}, 30000);
// Handle client disconnect
req.on("close", () => {
clearInterval(heartbeatInterval);
sseService.removeClient(userId);
logger.info(`SSE stream closed`, { userId });
});
// Handle connection errors
req.on("error", (error) => {
logger.error(`SSE stream error`, { userId, error: error.message });
clearInterval(heartbeatInterval);
sseService.removeClient(userId);
});
});
/**
* @route POST /api/sse/subscribe
* @desc Subscribe to SSE topics
* @access Private
*/
router.post("/subscribe", sseAuth, (req, res) => {
try {
const userId = req.user.id;
const { topics } = req.body;
// Validate topics
if (!topics || !Array.isArray(topics) || topics.length === 0) {
return res.status(400).json({
success: false,
msg: "Topics must be a non-empty array"
});
}
// Subscribe to topics
const success = sseService.subscribe(userId, topics);
if (!success) {
return res.status(400).json({
success: false,
msg: "User not connected to SSE stream"
});
}
res.json({
success: true,
msg: "Subscribed to topics",
topics
});
} catch (error) {
logger.error(`Subscribe error`, { error: error.message });
res.status(500).json({
success: false,
msg: "Server error"
});
}
});
/**
* @route POST /api/sse/unsubscribe
* @desc Unsubscribe from SSE topics
* @access Private
*/
router.post("/unsubscribe", sseAuth, (req, res) => {
try {
const userId = req.user.id;
const { topics } = req.body;
// Validate topics
if (!topics || !Array.isArray(topics) || topics.length === 0) {
return res.status(400).json({
success: false,
msg: "Topics must be a non-empty array"
});
}
// Unsubscribe from topics
const success = sseService.unsubscribe(userId, topics);
if (!success) {
return res.status(400).json({
success: false,
msg: "User not connected to SSE stream"
});
}
res.json({
success: true,
msg: "Unsubscribed from topics",
topics
});
} catch (error) {
logger.error(`Unsubscribe error`, { error: error.message });
res.status(500).json({
success: false,
msg: "Server error"
});
}
});
module.exports = router;