Socket Manager - Drowbe/coffee-pub-blacksmith GitHub Wiki
SocketManager - Internal Architecture
Overview
This document details the internal architecture and implementation of Blacksmith's SocketManager system. This is for developers who want to understand how the socket system works internally, not for module consumers.
Core Architecture
Design Philosophy
The SocketManager is designed as a facade pattern that provides a unified interface for socket communication while automatically detecting and integrating with the best available transport layer.
Transport Layer Priority
- SocketLib (Primary) - Professional-grade socket communication
- Native FoundryVTT (Fallback) - Built-in socket system
- Local Mode (Development) - Single-client testing
Class Structure
SocketManager Class
Location: scripts/manager-sockets.js
Core Responsibilities:
- Transport layer detection and initialization
- Socket event registration and management
- Cross-client communication orchestration
- Fallback system management
- Performance monitoring and optimization
Internal Data Structures
class SocketManager {
static instance = null;
static transport = null; // 'socketlib', 'native', or 'local'
static isReady = false;
static eventHandlers = new Map(); // eventName -> Set(handlers)
static pendingMessages = new Map(); // clientId -> Array(messages)
static performanceMetrics = {
messagesSent: 0,
messagesReceived: 0,
averageLatency: 0,
lastUpdate: Date.now()
};
}
Transport Layer Detection
SocketLib Detection
static _detectSocketLib() {
try {
// Check if SocketLib is available and active
if (game.modules.get('socketlib')?.active &&
typeof socketlib !== 'undefined' &&
socketlib.module) {
this.transport = 'socketlib';
this._initializeSocketLib();
return true;
}
} catch (error) {
console.warn('SocketLib detection failed:', error);
}
return false;
}
Native FoundryVTT Fallback
static _initializeNative() {
this.transport = 'native';
// Use FoundryVTT's built-in socket system
this.socket = game.socket;
// Set up event handlers for native system
this.socket.on('module.coffee-pub-blacksmith', this._handleNativeMessage.bind(this));
postConsoleAndNotification(
MODULE.NAME,
'SocketManager: Using native FoundryVTT sockets',
'Fallback system active',
true,
false
);
}
Local Mode for Development
static _initializeLocal() {
this.transport = 'local';
// Simulate socket behavior in single-client mode
this._simulateCrossClient = true;
postConsoleAndNotification(
MODULE.NAME,
'SocketManager: Local mode active',
'Single-client development mode',
true,
false
);
}
Event System Architecture
Event Registration Pattern
static registerEvent(eventName, handler, options = {}) {
if (!this.eventHandlers.has(eventName)) {
this.eventHandlers.set(eventName, new Set());
}
const handlerSet = this.eventHandlers.get(eventName);
const handlerId = this._generateHandlerId();
const handlerRecord = {
id: handlerId,
handler,
options,
registeredAt: Date.now(),
transport: this.transport
};
handlerSet.add(handlerRecord);
// Register with transport layer
this._registerWithTransport(eventName, handlerRecord);
return handlerId;
}
Transport-Specific Registration
static _registerWithTransport(eventName, handlerRecord) {
switch (this.transport) {
case 'socketlib':
this._registerSocketLib(eventName, handlerRecord);
break;
case 'native':
this._registerNative(eventName, handlerRecord);
break;
case 'local':
this._registerLocal(eventName, handlerRecord);
break;
}
}
Message Routing System
Outgoing Message Flow
static emit(eventName, data, options = {}) {
const message = {
event: eventName,
data,
timestamp: Date.now(),
source: game.user.id,
target: options.target || 'all',
priority: options.priority || 'normal'
};
// Route through appropriate transport
switch (this.transport) {
case 'socketlib':
return this._emitSocketLib(message);
case 'native':
return this._emitNative(message);
case 'local':
return this._emitLocal(message);
}
}
Incoming Message Flow
static _handleIncomingMessage(message) {
const { event, data, timestamp, source } = message;
// Update performance metrics
this._updatePerformanceMetrics(message);
// Find registered handlers for this event
const handlers = this.eventHandlers.get(event);
if (!handlers) {
return; // No handlers registered
}
// Execute handlers in registration order
for (const handlerRecord of handlers) {
try {
handlerRecord.handler(data, {
source,
timestamp,
transport: this.transport,
originalMessage: message
});
} catch (error) {
console.error(`Socket event handler error for ${event}:`, error);
}
}
}
Performance Optimization
Message Batching
static _batchMessages() {
// Group messages by target client
for (const [clientId, messages] of this.pendingMessages.entries()) {
if (messages.length >= this.batchSize ||
Date.now() - messages[0].timestamp > this.batchTimeout) {
this._flushBatch(clientId, messages);
}
}
}
static _flushBatch(clientId, messages) {
const batchMessage = {
type: 'batch',
messages,
timestamp: Date.now()
};
this._sendToClient(clientId, batchMessage);
this.pendingMessages.delete(clientId);
}
Latency Monitoring
static _updatePerformanceMetrics(message) {
const now = Date.now();
const latency = now - message.timestamp;
this.performanceMetrics.messagesReceived++;
this.performanceMetrics.averageLatency =
(this.performanceMetrics.averageLatency * (this.performanceMetrics.messagesReceived - 1) + latency) /
this.performanceMetrics.messagesReceived;
this.performanceMetrics.lastUpdate = now;
// Log high latency for debugging
if (latency > 1000) { // 1 second threshold
console.warn(`High socket latency detected: ${latency}ms for event ${message.event}`);
}
}
Error Handling and Recovery
Connection Failure Recovery
static _handleConnectionFailure(error) {
console.error('Socket connection failure:', error);
// Attempt to reconnect
this._scheduleReconnection();
// Notify event handlers
this._notifyConnectionStatus('disconnected', error);
}
static _scheduleReconnection() {
if (this.reconnectionAttempts >= this.maxReconnectionAttempts) {
this._fallbackToLocalMode();
return;
}
const delay = Math.min(1000 * Math.pow(2, this.reconnectionAttempts), 30000);
setTimeout(() => {
this._attemptReconnection();
}, delay);
this.reconnectionAttempts++;
}
Transport Fallback Strategy
static _fallbackToLocalMode() {
postConsoleAndNotification(
MODULE.NAME,
'SocketManager: Falling back to local mode',
'Cross-client features disabled',
false,
true
);
// Switch to local mode
this.transport = 'local';
this._initializeLocal();
// Notify all handlers of mode change
this._notifyTransportChange('local');
}
Security and Validation
Message Validation
static _validateMessage(message) {
// Required fields
if (!message.event || !message.data || !message.timestamp) {
throw new Error('Invalid message format: missing required fields');
}
// Timestamp validation (prevent replay attacks)
const now = Date.now();
const messageAge = now - message.timestamp;
if (messageAge > this.maxMessageAge) {
throw new Error(`Message too old: ${messageAge}ms`);
}
// Source validation
if (message.source && !this._isValidUser(message.source)) {
throw new Error(`Invalid source user: ${message.source}`);
}
return true;
}
User Permission Checking
static _isValidUser(userId) {
// Check if user exists and is active
const user = game.users.get(userId);
if (!user || !user.active) {
return false;
}
// Check if user has permission to send messages
if (!user.hasRole('PLAYER') && !user.hasRole('ASSISTANT') && !user.hasRole('GAMEMASTER')) {
return false;
}
return true;
}
Debugging and Monitoring
Console Commands
// Add debugging commands to window object
window.socketStatus = () => this._showStatus();
window.socketMetrics = () => this._showMetrics();
window.socketEvents = () => this._showEvents();
window.socketTest = () => this._runTestMessage();
Status Display
static _showStatus() {
console.group('COFFEE PUB • BLACKSMITH | SOCKET MANAGER STATUS');
console.log('==========================================================');
console.log(`Transport: ${this.transport.toUpperCase()}`);
console.log(`Status: ${this.isReady ? 'READY' : 'INITIALIZING'}`);
console.log(`Active Events: ${this.eventHandlers.size}`);
console.log(`Pending Messages: ${this._getTotalPendingMessages()}`);
console.log('==========================================================');
console.groupEnd();
}
Performance Metrics Display
static _showMetrics() {
const metrics = this.performanceMetrics;
console.group('COFFEE PUB • BLACKSMITH | SOCKET PERFORMANCE');
console.log('==========================================================');
console.log(`Messages Sent: ${metrics.messagesSent}`);
console.log(`Messages Received: ${metrics.messagesReceived}`);
console.log(`Average Latency: ${metrics.averageLatency.toFixed(2)}ms`);
console.log(`Last Update: ${new Date(metrics.lastUpdate).toLocaleTimeString()}`);
console.log('==========================================================');
console.groupEnd();
}
Configuration and Settings
Configurable Parameters
static config = {
// Performance tuning
batchSize: 10, // Messages per batch
batchTimeout: 100, // Max wait time for batching (ms)
maxMessageAge: 30000, // Max message age (30 seconds)
// Reconnection settings
maxReconnectionAttempts: 5,
initialReconnectionDelay: 1000,
maxReconnectionDelay: 30000,
// Transport preferences
preferSocketLib: true, // Prefer SocketLib over native
fallbackToLocal: true, // Allow local mode fallback
// Debug settings
enableDebugLogging: false,
logAllMessages: false,
performanceMonitoring: true
};
Runtime Configuration
static configure(newConfig) {
// Validate configuration
this._validateConfig(newConfig);
// Apply new configuration
Object.assign(this.config, newConfig);
// Reinitialize if transport needs to change
if (newConfig.preferSocketLib !== undefined) {
this._reinitializeTransport();
}
postConsoleAndNotification(
MODULE.NAME,
'SocketManager: Configuration updated',
newConfig,
true,
false
);
}
Testing and Development
Local Testing Mode
static enableLocalTesting() {
this.config.preferSocketLib = false;
this.config.fallbackToLocal = true;
this._reinitializeTransport();
postConsoleAndNotification(
MODULE.NAME,
'SocketManager: Local testing mode enabled',
'Cross-client features simulated locally',
true,
false
);
}
Message Simulation
static _simulateCrossClientMessage(eventName, data) {
if (this.transport !== 'local') {
console.warn('Cross-client simulation only available in local mode');
return;
}
// Simulate message from another client
const simulatedMessage = {
event: eventName,
data,
timestamp: Date.now(),
source: 'simulated-client',
transport: 'local'
};
// Process as if it came from network
this._handleIncomingMessage(simulatedMessage);
}
Integration Points
HookManager Integration
// SocketManager registers hooks for socket events
static _registerSocketHooks() {
// Hook into FoundryVTT socket events
Hooks.on('socketlib.ready', () => {
this._onSocketLibReady();
});
Hooks.on('socketlib.error', (error) => {
this._onSocketLibError(error);
});
}
Module System Integration
// Other modules can register socket event handlers
static registerModuleHandler(moduleId, eventName, handler) {
const handlerId = this.registerEvent(eventName, handler, {
moduleId,
priority: 'normal'
});
// Track module handlers for cleanup
if (!this.moduleHandlers.has(moduleId)) {
this.moduleHandlers.set(moduleId, new Set());
}
this.moduleHandlers.get(moduleId).add(handlerId);
return handlerId;
}
Future Enhancements
Planned Features
- Message Encryption - End-to-end encryption for sensitive data
- Compression - Message compression for large payloads
- Reliability - Guaranteed message delivery with acknowledgments
- Scalability - Support for large numbers of connected clients
- Analytics - Detailed usage analytics and performance insights
Architecture Evolution
- Plugin System - Allow modules to extend socket functionality
- Protocol Versioning - Support for multiple message protocol versions
- Load Balancing - Distribute socket load across multiple servers
- Real-time Monitoring - Live performance and health monitoring
Last Updated: Current session - Socket system fully functional Status: Production ready with comprehensive architecture Next Milestone: Enhanced security and performance features