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:
William Valentin
2025-12-05 22:49:22 -08:00
parent b5ee7571c9
commit bb9c8ec1c3
571 changed files with 156739 additions and 1350 deletions
+13 -85
View File
@@ -1,15 +1,14 @@
require("dotenv").config();
const express = require("express");
const couchdbService = require("./services/couchdbService");
const sseService = require("./services/sseService");
const cors = require("cors");
const http = require("http");
const socketio = require("socket.io");
const helmet = require("helmet");
const rateLimit = require("express-rate-limit");
const mongoSanitize = require("express-mongo-sanitize");
const xss = require("xss-clean");
const { errorHandler } = require("./middleware/errorHandler");
const socketAuth = require("./middleware/socketAuth");
const requestLogger = require("./middleware/requestLogger");
const logger = require("./utils/logger");
const { validateEnv, logEnvConfig } = require("./utils/validateEnv");
@@ -26,13 +25,6 @@ try {
const app = express();
const server = http.createServer(app);
const io = socketio(server, {
cors: {
origin: process.env.FRONTEND_URL || "http://localhost:3000",
methods: ["GET", "POST"],
credentials: true,
},
});
const port = process.env.PORT || 5000;
// Trust proxy - required when behind ingress/reverse proxy
@@ -101,34 +93,8 @@ if (process.env.NODE_ENV !== 'test') {
});
}
// Socket.IO Authentication Middleware
io.use(socketAuth);
// Socket.IO Setup with Authentication
io.on("connection", (socket) => {
logger.info(`Socket.IO client connected`, { userId: socket.user.id });
socket.on("joinEvent", (eventId) => {
socket.join(`event_${eventId}`);
logger.debug(`User joined event`, { userId: socket.user.id, eventId });
});
socket.on("joinPost", (postId) => {
socket.join(`post_${postId}`);
logger.debug(`User joined post`, { userId: socket.user.id, postId });
});
socket.on("eventUpdate", (data) => {
io.to(`event_${data.eventId}`).emit("update", data.message);
});
socket.on("disconnect", () => {
logger.info(`Socket.IO client disconnected`, { userId: socket.user.id });
});
});
// Make io available to routes
app.set("io", io);
// Make sse available to routes
app.set("sse", sseService);
// Routes
const authRoutes = require("./routes/auth");
@@ -147,6 +113,7 @@ const cacheRoutes = require("./routes/cache");
const profileRoutes = require("./routes/profile");
const analyticsRoutes = require("./routes/analytics");
const leaderboardRoutes = require("./routes/leaderboard");
const sseRoutes = require("./routes/sse");
// Apply rate limiters
app.use("/api/auth/register", authLimiter);
@@ -158,15 +125,10 @@ app.get("/api/health", async (req, res) => {
try {
const couchdbStatus = await couchdbService.checkConnection();
// Check Socket.IO status
const socketIOStatus = {
engine: io.engine ? "running" : "stopped",
connectedClients: io.engine ? io.engine.clientsCount : 0,
// Get number of connected sockets
sockets: io.sockets ? io.sockets.sockets.size : 0
};
// Get SSE stats
const sseStats = sseService.getStats();
const isHealthy = couchdbStatus && io.engine;
const isHealthy = couchdbStatus;
res.status(isHealthy ? 200 : 503).json({
status: isHealthy ? "healthy" : "degraded",
@@ -174,10 +136,9 @@ app.get("/api/health", async (req, res) => {
uptime: process.uptime(),
services: {
couchdb: couchdbStatus ? "connected" : "disconnected",
socketIO: {
status: socketIOStatus.engine,
connectedClients: socketIOStatus.connectedClients,
activeSockets: socketIOStatus.sockets
sse: {
totalClients: sseStats.totalClients,
totalTopics: sseStats.totalTopics
}
},
memory: {
@@ -193,47 +154,13 @@ app.get("/api/health", async (req, res) => {
uptime: process.uptime(),
services: {
couchdb: "disconnected",
socketIO: "unknown"
sse: "unknown"
},
error: error.message,
});
}
});
// Detailed Socket.IO health check endpoint
app.get("/api/health/socketio", (req, res) => {
try {
const socketIOInfo = {
status: io.engine ? "running" : "stopped",
connectedClients: io.engine ? io.engine.clientsCount : 0,
activeSockets: io.sockets ? io.sockets.sockets.size : 0,
rooms: [],
timestamp: new Date().toISOString()
};
// Get list of active rooms (excluding auto-generated socket ID rooms)
if (io.sockets && io.sockets.adapter && io.sockets.adapter.rooms) {
const rooms = Array.from(io.sockets.adapter.rooms.keys()).filter(room => {
// Filter out socket ID rooms (they start with socket ID pattern)
return room.startsWith('event_') || room.startsWith('post_');
});
socketIOInfo.rooms = rooms.map(room => {
const roomSize = io.sockets.adapter.rooms.get(room)?.size || 0;
return { name: room, members: roomSize };
});
}
res.status(200).json(socketIOInfo);
} catch (error) {
res.status(500).json({
status: "error",
error: error.message,
timestamp: new Date().toISOString()
});
}
});
// Routes
app.use("/api/auth", authRoutes);
app.use("/api/streets", streetRoutes);
@@ -251,6 +178,7 @@ app.use("/api/cache", cacheRoutes);
app.use("/api/profile", profileRoutes);
app.use("/api/analytics", analyticsRoutes);
app.use("/api/leaderboard", leaderboardRoutes);
app.use("/api/sse", sseRoutes);
app.get("/", (req, res) => {
res.send("Street Adoption App Backend");
@@ -267,7 +195,7 @@ if (require.main === module) {
}
// Export app and server for testing
module.exports = { app, server, io };
module.exports = { app, server };
// Graceful shutdown
process.on("SIGTERM", async () => {