/** * WebSocket Configuration and Connection Management * Handles Socket.IO server initialization and connection management */ const { Server } = require('socket.io'); const logger = require('../utils/logger'); // Configuration const WEBSOCKET_CONFIG = { cors: { origin: process.env.WEBSOCKET_CORS_ORIGIN?.split(',') || ['http://localhost:3000', 'http://localhost:3001'], methods: ['GET', 'POST'], credentials: true, }, pingTimeout: parseInt(process.env.WEBSOCKET_PING_TIMEOUT) || 20000, pingInterval: parseInt(process.env.WEBSOCKET_PING_INTERVAL) || 25000, maxHttpBufferSize: parseInt(process.env.WEBSOCKET_MAX_BUFFER_SIZE) || 1e6, // 1MB transports: ['websocket', 'polling'], allowEIO3: true, compression: true, httpCompression: true, }; let io = null; let connectionCount = 0; const connectedClients = new Map(); /** * Initialize WebSocket server * @param {Object} server - HTTP server instance * @returns {Promise} Socket.IO server instance */ async function initializeWebSocket(server) { try { if (io) { logger.info('WebSocket server already initialized'); return io; } // Create Socket.IO server io = new Server(server, WEBSOCKET_CONFIG); // Set up middleware for authentication and logging io.use(async (socket, next) => { const correlationId = socket.handshake.query.correlationId || require('uuid').v4(); socket.correlationId = correlationId; logger.info('WebSocket connection attempt', { correlationId, socketId: socket.id, ip: socket.handshake.address, userAgent: socket.handshake.headers['user-agent'], }); next(); }); // Connection event handler io.on('connection', (socket) => { connectionCount++; connectedClients.set(socket.id, { connectedAt: new Date(), ip: socket.handshake.address, userAgent: socket.handshake.headers['user-agent'], playerId: null, // Will be set after authentication rooms: new Set(), }); logger.info('WebSocket client connected', { correlationId: socket.correlationId, socketId: socket.id, totalConnections: connectionCount, ip: socket.handshake.address, }); // Set up event handlers setupSocketEventHandlers(socket); // Handle disconnection socket.on('disconnect', (reason) => { connectionCount--; const clientInfo = connectedClients.get(socket.id); connectedClients.delete(socket.id); logger.info('WebSocket client disconnected', { correlationId: socket.correlationId, socketId: socket.id, reason, totalConnections: connectionCount, playerId: clientInfo?.playerId, connectionDuration: clientInfo ? Date.now() - clientInfo.connectedAt : 0, }); }); // Handle connection errors socket.on('error', (error) => { logger.error('WebSocket connection error', { correlationId: socket.correlationId, socketId: socket.id, error: error.message, stack: error.stack, }); }); }); // Server-level error handling io.engine.on('connection_error', (error) => { logger.error('WebSocket connection error:', { message: error.message, code: error.code, context: error.context, }); }); logger.info('WebSocket server initialized successfully', { maxConnections: process.env.WEBSOCKET_MAX_CONNECTIONS || 'unlimited', pingTimeout: WEBSOCKET_CONFIG.pingTimeout, pingInterval: WEBSOCKET_CONFIG.pingInterval, }); return io; } catch (error) { logger.error('Failed to initialize WebSocket server:', error); throw error; } } /** * Set up event handlers for individual socket connections * @param {Object} socket - Socket.IO socket instance */ function setupSocketEventHandlers(socket) { // Player authentication socket.on('authenticate', async (data) => { try { logger.info('WebSocket authentication attempt', { correlationId: socket.correlationId, socketId: socket.id, playerId: data?.playerId, }); // TODO: Implement JWT token validation // For now, just acknowledge socket.emit('authenticated', { success: true, message: 'Authentication successful', }); // Update client information if (connectedClients.has(socket.id)) { connectedClients.get(socket.id).playerId = data?.playerId; } } catch (error) { logger.error('WebSocket authentication error', { correlationId: socket.correlationId, socketId: socket.id, error: error.message, }); socket.emit('authentication_error', { success: false, message: 'Authentication failed', }); } }); // Join room (for game features like galaxy regions, player groups, etc.) socket.on('join_room', (roomName) => { if (typeof roomName !== 'string' || roomName.length > 50) { socket.emit('error', { message: 'Invalid room name' }); return; } socket.join(roomName); const clientInfo = connectedClients.get(socket.id); if (clientInfo) { clientInfo.rooms.add(roomName); } logger.info('Client joined room', { correlationId: socket.correlationId, socketId: socket.id, room: roomName, playerId: clientInfo?.playerId, }); socket.emit('room_joined', { room: roomName }); }); // Leave room socket.on('leave_room', (roomName) => { socket.leave(roomName); const clientInfo = connectedClients.get(socket.id); if (clientInfo) { clientInfo.rooms.delete(roomName); } logger.info('Client left room', { correlationId: socket.correlationId, socketId: socket.id, room: roomName, playerId: clientInfo?.playerId, }); socket.emit('room_left', { room: roomName }); }); // Ping/pong for connection testing socket.on('ping', () => { socket.emit('pong', { timestamp: Date.now() }); }); // Generic message handler (for debugging) socket.on('message', (data) => { logger.debug('WebSocket message received', { correlationId: socket.correlationId, socketId: socket.id, data: typeof data === 'object' ? JSON.stringify(data) : data, }); }); } /** * Get WebSocket server instance * @returns {Object|null} Socket.IO server instance */ function getWebSocketServer() { return io; } /** * Get connection statistics * @returns {Object} Connection statistics */ function getConnectionStats() { return { totalConnections: connectionCount, authenticatedConnections: Array.from(connectedClients.values()) .filter(client => client.playerId).length, anonymousConnections: Array.from(connectedClients.values()) .filter(client => !client.playerId).length, rooms: io ? Array.from(io.sockets.adapter.rooms.keys()) : [], }; } /** * Broadcast message to all connected clients * @param {string} event - Event name * @param {Object} data - Data to broadcast */ function broadcastToAll(event, data) { if (!io) { logger.warn('Attempted to broadcast but WebSocket server not initialized'); return; } io.emit(event, data); logger.info('Broadcast sent to all clients', { event, recipientCount: connectionCount, }); } /** * Broadcast message to specific room * @param {string} room - Room name * @param {string} event - Event name * @param {Object} data - Data to broadcast */ function broadcastToRoom(room, event, data) { if (!io) { logger.warn('Attempted to broadcast to room but WebSocket server not initialized'); return; } io.to(room).emit(event, data); logger.info('Broadcast sent to room', { room, event, recipientCount: io.sockets.adapter.rooms.get(room)?.size || 0, }); } /** * Close WebSocket server gracefully * @returns {Promise} */ async function closeWebSocket() { if (!io) return; try { // Disconnect all clients io.disconnectSockets(); // Close server io.close(); io = null; connectionCount = 0; connectedClients.clear(); logger.info('WebSocket server closed gracefully'); } catch (error) { logger.error('Error closing WebSocket server:', error); throw error; } } module.exports = { initializeWebSocket, getWebSocketServer, getConnectionStats, broadcastToAll, broadcastToRoom, closeWebSocket, };