/** * CE Relay Server - Hugging Face Edition * Optimized for 10k users via Room-based architecture. */ const WebSocket = require('ws'); const http = require('http'); const PORT = process.env.PORT || 7860; const PROJECT_TTL = 3 * 60 * 1000; // 3 minutes const server = http.createServer((req, res) => { res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end('CE Relay Server is running.'); }); const wss = new WebSocket.Server({ server }); // Room-based storage // rooms: Map> const rooms = new Map(); // Project storage: Map const activeProjects = new Map(); // Client info: Map const clients = new Map(); wss.on('connection', (ws) => { ws.on('message', (message) => { try { const payload = JSON.parse(message); handleMessage(ws, payload); } catch (e) { console.error('[Server] Invalid message format'); } }); ws.on('close', () => { const clientInfo = clients.get(ws); if (clientInfo) { leaveRoom(ws, clientInfo.code); broadcast(clientInfo.code, { type: 'USER_DISCONNECTED', id: clientInfo.id }, ws); clients.delete(ws); } }); }); function handleMessage(ws, payload) { const { type, code, id, data } = payload; switch (type) { case 'HOST_PROJECT': joinRoom(ws, code); // Cleanup existing TTL for this code if host re-hosts const existing = activeProjects.get(code); if (existing && existing.timeout) clearTimeout(existing.timeout); const timeout = setTimeout(() => { console.log(`[Server] Project ${code} TTL expired. Wiping data.`); const p = activeProjects.get(code); if (p) p.data = null; // We keep the entry so we know who the host is, but wipe the heavy 'data' }, PROJECT_TTL); activeProjects.set(code, { hostId: id, data, timeout }); clients.set(ws, { code, id }); ws.send(JSON.stringify({ type: 'HOST_CONFIRMED', code })); break; case 'JOIN_PROJECT': const project = activeProjects.get(code); if (project) { joinRoom(ws, code); clients.set(ws, { code, id }); ws.send(JSON.stringify({ type: 'JOIN_SUCCESS', code, projectData: project.data })); broadcast(code, { type: 'USER_JOINED', id }, ws); } else { ws.send(JSON.stringify({ type: 'JOIN_ERROR', message: 'Proyecto expirado o no existe' })); } break; case 'SYNC_UPDATE': broadcast(code, { type: 'SYNC_UPDATE', origin: id, data }, ws); break; case 'RE_UPLOAD_PROJECT': const p = activeProjects.get(code); if (p && p.hostId === id) { if (p.timeout) clearTimeout(p.timeout); p.data = data; p.timeout = setTimeout(() => { p.data = null; }, PROJECT_TTL); ws.send(JSON.stringify({ type: 'RE_UPLOAD_CONFIRMED' })); } break; case 'PING': ws.send(JSON.stringify({ type: 'PONG' })); break; } } function joinRoom(ws, code) { if (!rooms.has(code)) rooms.set(code, new Set()); rooms.get(code).add(ws); } function leaveRoom(ws, code) { const room = rooms.get(code); if (room) { room.delete(ws); if (room.size === 0) { rooms.delete(code); // If room is empty, we can also wipe the project immediately if we want const p = activeProjects.get(code); if (p) { if (p.timeout) clearTimeout(p.timeout); activeProjects.delete(code); } } } } function broadcast(code, payload, excludeWs = null) { const room = rooms.get(code); if (!room) return; const message = JSON.stringify(payload); for (const ws of room) { if (ws !== excludeWs && ws.readyState === WebSocket.OPEN) { ws.send(message); } } } server.listen(PORT, () => { console.log(`Relay server listening on port ${PORT}`); });