Major milestone: Frontend implementation complete for Shattered Void MMO FRONTEND IMPLEMENTATION: - React 18 + TypeScript + Vite development environment - Tailwind CSS with custom dark theme for sci-fi aesthetic - Zustand state management with authentication persistence - Socket.io WebSocket client with auto-reconnection - Protected routing with authentication guards - Responsive design with mobile-first approach AUTHENTICATION SYSTEM: - Login/register forms with comprehensive validation - JWT token management with localStorage persistence - Password strength validation and user feedback - Protected routes and authentication guards CORE GAME INTERFACE: - Colony management dashboard with real-time updates - Resource display with live production tracking - WebSocket integration for real-time game events - Navigation with connection status indicator - Toast notifications for user feedback BACKEND ENHANCEMENTS: - Complete Research System with technology tree (23 technologies) - Fleet Management System with ship designs and movement - Enhanced Authentication with email verification and password reset - Complete game tick integration for all systems - Advanced WebSocket events for real-time updates ARCHITECTURE FEATURES: - Type-safe TypeScript throughout - Component-based architecture with reusable UI elements - API client with request/response interceptors - Error handling and loading states - Performance optimized builds with code splitting Phase 2 Status: Frontend foundation complete (Week 1-2 objectives met) Ready for: Colony management, fleet operations, research interface Next: Enhanced gameplay features and admin interface 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
321 lines
8.3 KiB
JavaScript
321 lines
8.3 KiB
JavaScript
/**
|
|
* 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<Object>} 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<void>}
|
|
*/
|
|
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,
|
|
};
|