Initial commit: Shattered Void MMO foundation
- Complete PostgreSQL database schema with 21+ tables - Express.js server with dual authentication (player/admin) - WebSocket support for real-time features - Comprehensive middleware (auth, validation, logging, security) - Game systems: colonies, resources, fleets, research, factions - Plugin-based combat architecture - Admin panel foundation - Production-ready logging and error handling - Docker support and CI/CD ready - Complete project structure following CLAUDE.md patterns 🤖 Generated with Claude Code (https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
commit
1a60cf55a3
69 changed files with 24471 additions and 0 deletions
321
src/config/websocket.js
Normal file
321
src/config/websocket.js
Normal file
|
|
@ -0,0 +1,321 @@
|
|||
/**
|
||||
* 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
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue