claude-code-templates
Version:
CLI tool to setup Claude Code configurations with framework-specific commands, automation hooks and MCP Servers for your projects
526 lines (462 loc) • 14.1 kB
JavaScript
/**
* WebSocketServer - Handles real-time communication between server and clients
* Part of the modular backend architecture for Phase 3
*/
const WebSocket = require('ws');
const chalk = require('chalk');
class WebSocketServer {
constructor(httpServer, options = {}, performanceMonitor = null) {
this.httpServer = httpServer;
this.performanceMonitor = performanceMonitor;
this.options = {
port: options.port || 3334,
path: options.path || '/ws',
heartbeatInterval: options.heartbeatInterval || 30000,
...options
};
this.wss = null;
this.clients = new Map();
this.heartbeatInterval = null;
this.isRunning = false;
this.messageQueue = [];
this.maxQueueSize = 100;
}
/**
* Initialize and start the WebSocket server
*/
async initialize() {
try {
console.log(chalk.blue('🔌 Initializing WebSocket server...'));
// Create WebSocket server
this.wss = new WebSocket.Server({
server: this.httpServer,
path: this.options.path,
clientTracking: true
});
this.setupEventHandlers();
this.startHeartbeat();
this.isRunning = true;
console.log(chalk.green(`✅ WebSocket server initialized on ${this.options.path}`));
} catch (error) {
console.error(chalk.red('❌ Failed to initialize WebSocket server:'), error);
throw error;
}
}
/**
* Setup WebSocket event handlers
*/
setupEventHandlers() {
this.wss.on('connection', (ws, request) => {
this.handleConnection(ws, request);
});
this.wss.on('error', (error) => {
console.error(chalk.red('WebSocket server error:'), error);
});
this.wss.on('close', () => {
console.log(chalk.yellow('🔌 WebSocket server closed'));
this.isRunning = false;
});
}
/**
* Handle new WebSocket connection
* @param {WebSocket} ws - WebSocket connection
* @param {Object} request - HTTP request object
*/
handleConnection(ws, request) {
const clientId = this.generateClientId();
const clientInfo = {
id: clientId,
ws: ws,
ip: request.socket.remoteAddress,
userAgent: request.headers['user-agent'],
connectedAt: new Date(),
isAlive: true,
subscriptions: new Set()
};
this.clients.set(clientId, clientInfo);
console.log(chalk.green(`🔗 WebSocket client connected: ${clientId} (${this.clients.size} total)`));
// Track WebSocket connection in performance monitor
if (this.performanceMonitor) {
this.performanceMonitor.recordWebSocket('connection', {
clientId,
totalClients: this.clients.size,
ip: request.socket.remoteAddress
});
}
// Send welcome message
this.sendToClient(clientId, {
type: 'connection',
data: {
clientId: clientId,
serverTime: new Date().toISOString(),
message: 'Connected to Claude Code Analytics WebSocket'
}
});
// Send any queued messages
this.sendQueuedMessages(clientId);
// Setup client event handlers
ws.on('message', (message) => {
this.handleClientMessage(clientId, message);
});
ws.on('close', (code, reason) => {
this.handleClientDisconnect(clientId, code, reason);
});
ws.on('error', (error) => {
console.error(chalk.red(`WebSocket client error (${clientId}):`), error);
});
ws.on('pong', () => {
this.handleClientPong(clientId);
});
}
/**
* Handle message from client
* @param {string} clientId - Client ID
* @param {Buffer} message - Message buffer
*/
handleClientMessage(clientId, message) {
try {
const data = JSON.parse(message.toString());
const client = this.clients.get(clientId);
if (!client) return;
console.log(chalk.cyan(`📨 Message from ${clientId}:`), data.type);
// Track message in performance monitor
if (this.performanceMonitor) {
this.performanceMonitor.recordWebSocket('message_received', {
clientId,
messageType: data.type,
messageSize: message.length
});
}
switch (data.type) {
case 'subscribe':
this.handleSubscription(clientId, data.channel);
break;
case 'unsubscribe':
this.handleUnsubscription(clientId, data.channel);
break;
case 'ping':
this.sendToClient(clientId, { type: 'pong', timestamp: Date.now() });
break;
case 'refresh_request':
this.handleRefreshRequest(clientId);
break;
default:
console.warn(chalk.yellow(`Unknown message type from ${clientId}: ${data.type}`));
}
} catch (error) {
console.error(chalk.red(`Error parsing message from ${clientId}:`), error);
}
}
/**
* Handle client subscription to a channel
* @param {string} clientId - Client ID
* @param {string} channel - Channel name
*/
handleSubscription(clientId, channel) {
const client = this.clients.get(clientId);
if (!client) return;
client.subscriptions.add(channel);
console.log(chalk.green(`📡 Client ${clientId} subscribed to ${channel}`));
this.sendToClient(clientId, {
type: 'subscription_confirmed',
data: { channel, subscriptions: Array.from(client.subscriptions) }
});
}
/**
* Handle client unsubscription from a channel
* @param {string} clientId - Client ID
* @param {string} channel - Channel name
*/
handleUnsubscription(clientId, channel) {
const client = this.clients.get(clientId);
if (!client) return;
client.subscriptions.delete(channel);
console.log(chalk.yellow(`📡 Client ${clientId} unsubscribed from ${channel}`));
this.sendToClient(clientId, {
type: 'unsubscription_confirmed',
data: { channel, subscriptions: Array.from(client.subscriptions) }
});
}
/**
* Handle refresh request from client
* @param {string} clientId - Client ID
*/
handleRefreshRequest(clientId) {
console.log(chalk.blue(`🔄 Refresh requested by ${clientId}`));
// Emit refresh event that the main analytics server can listen to
this.emit('refresh_requested', { clientId });
}
/**
* Handle client disconnection
* @param {string} clientId - Client ID
* @param {number} code - Close code
* @param {Buffer} reason - Close reason
*/
handleClientDisconnect(clientId, code, reason) {
this.clients.delete(clientId);
console.log(chalk.yellow(`🔗 WebSocket client disconnected: ${clientId} (${this.clients.size} remaining)`));
console.log(chalk.gray(` Close code: ${code}, Reason: ${reason || 'No reason provided'}`));
// Track disconnection in performance monitor
if (this.performanceMonitor) {
this.performanceMonitor.recordWebSocket('disconnection', {
clientId,
closeCode: code,
totalClients: this.clients.size,
reason: reason?.toString() || 'No reason provided'
});
}
}
/**
* Handle client pong response
* @param {string} clientId - Client ID
*/
handleClientPong(clientId) {
const client = this.clients.get(clientId);
if (client) {
client.isAlive = true;
}
}
/**
* Broadcast message to all connected clients
* @param {Object} message - Message to broadcast
* @param {string} channel - Optional channel filter
*/
broadcast(message, channel = null) {
const messageStr = JSON.stringify({
...message,
timestamp: Date.now(),
server: 'Claude Code Analytics'
});
let sentCount = 0;
this.clients.forEach((client, clientId) => {
// Filter by channel subscription if specified
if (channel && !client.subscriptions.has(channel)) {
return;
}
if (client.ws.readyState === WebSocket.OPEN) {
try {
client.ws.send(messageStr);
sentCount++;
} catch (error) {
console.error(chalk.red(`Error sending to client ${clientId}:`), error);
this.clients.delete(clientId);
}
}
});
if (sentCount > 0) {
//console.log(chalk.green(`📢 Broadcasted ${message.type} to ${sentCount} clients${channel ? ` on channel ${channel}` : ''}`));
}
// Queue message if no clients connected
if (sentCount === 0 && this.clients.size === 0) {
this.queueMessage(message);
}
}
/**
* Send message to specific client
* @param {string} clientId - Client ID
* @param {Object} message - Message to send
*/
sendToClient(clientId, message) {
const client = this.clients.get(clientId);
if (!client || client.ws.readyState !== WebSocket.OPEN) {
return false;
}
try {
const messageStr = JSON.stringify({
...message,
timestamp: Date.now(),
server: 'Claude Code Analytics'
});
client.ws.send(messageStr);
return true;
} catch (error) {
console.error(chalk.red(`Error sending to client ${clientId}:`), error);
this.clients.delete(clientId);
return false;
}
}
/**
* Queue message for future delivery
* @param {Object} message - Message to queue
*/
queueMessage(message) {
this.messageQueue.push({
...message,
queuedAt: Date.now()
});
// Keep queue size manageable
if (this.messageQueue.length > this.maxQueueSize) {
this.messageQueue.shift();
}
}
/**
* Send queued messages to newly connected client
* @param {string} clientId - Client ID
*/
sendQueuedMessages(clientId) {
if (this.messageQueue.length === 0) return;
console.log(chalk.blue(`📦 Sending ${this.messageQueue.length} queued messages to ${clientId}`));
this.messageQueue.forEach(message => {
this.sendToClient(clientId, {
...message,
type: 'queued_' + message.type,
wasQueued: true
});
});
}
/**
* Notify clients of conversation state change
* @param {string} conversationId - Conversation ID
* @param {string} newState - New state
* @param {Object} metadata - Additional metadata
*/
notifyConversationStateChange(conversationId, newState, metadata = {}) {
this.broadcast({
type: 'conversation_state_change',
data: {
conversationId,
newState,
...metadata
}
}, 'conversation_updates');
}
/**
* Notify clients of data refresh
* @param {Object} data - Updated data
*/
notifyDataRefresh(data) {
this.broadcast({
type: 'data_refresh',
data
}, 'data_updates');
}
/**
* Notify clients of system status change
* @param {Object} status - System status
*/
notifySystemStatus(status) {
this.broadcast({
type: 'system_status',
data: status
}, 'system_updates');
}
/**
* Start heartbeat mechanism
*/
startHeartbeat() {
this.heartbeatInterval = setInterval(() => {
this.clients.forEach((client, clientId) => {
if (!client.isAlive) {
console.log(chalk.yellow(`💔 Terminating unresponsive client: ${clientId}`));
client.ws.terminate();
this.clients.delete(clientId);
return;
}
client.isAlive = false;
if (client.ws.readyState === WebSocket.OPEN) {
client.ws.ping();
}
});
}, this.options.heartbeatInterval);
}
/**
* Stop heartbeat mechanism
*/
stopHeartbeat() {
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval);
this.heartbeatInterval = null;
}
}
/**
* Generate unique client ID
* @returns {string} Client ID
*/
generateClientId() {
return `client_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
/**
* Get server statistics
* @returns {Object} Server statistics
*/
getStats() {
const clientStats = Array.from(this.clients.values()).map(client => ({
id: client.id,
ip: client.ip,
connectedAt: client.connectedAt,
subscriptions: Array.from(client.subscriptions),
isAlive: client.isAlive
}));
return {
isRunning: this.isRunning,
clientCount: this.clients.size,
queuedMessages: this.messageQueue.length,
clients: clientStats,
uptime: this.isRunning ? Date.now() - this.startTime : 0
};
}
/**
* Gracefully close all connections and stop server
*/
async close() {
console.log(chalk.yellow('🔌 Closing WebSocket server...'));
this.stopHeartbeat();
// Close all client connections
this.clients.forEach((client, clientId) => {
if (client.ws.readyState === WebSocket.OPEN) {
client.ws.close(1000, 'Server shutting down');
}
});
this.clients.clear();
if (this.wss) {
await new Promise((resolve) => {
this.wss.close(resolve);
});
}
this.isRunning = false;
console.log(chalk.green('✅ WebSocket server closed'));
}
/**
* Event emitter functionality
*/
emit(event, data) {
// Simple event emitter implementation
if (this.listeners && this.listeners[event]) {
this.listeners[event].forEach(callback => {
try {
callback(data);
} catch (error) {
console.error(`Error in WebSocket event listener for ${event}:`, error);
}
});
}
}
/**
* Add event listener
* @param {string} event - Event name
* @param {Function} callback - Callback function
*/
on(event, callback) {
if (!this.listeners) {
this.listeners = {};
}
if (!this.listeners[event]) {
this.listeners[event] = [];
}
this.listeners[event].push(callback);
}
/**
* Remove event listener
* @param {string} event - Event name
* @param {Function} callback - Callback function
*/
off(event, callback) {
if (this.listeners && this.listeners[event]) {
const index = this.listeners[event].indexOf(callback);
if (index !== -1) {
this.listeners[event].splice(index, 1);
}
}
}
}
module.exports = WebSocketServer;