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:
+13
-85
@@ -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 () => {
|
||||
|
||||
Reference in New Issue
Block a user