claude-flow-multilang
Version:
Revolutionary multilingual AI orchestration framework with cultural awareness and DDD architecture
481 lines (421 loc) • 11.7 kB
JavaScript
/**
* WebSocket Client for Claude Code Console
* Handles real-time communication with the backend MCP server
*/
export class WebSocketClient {
constructor() {
this.ws = null;
this.url = '';
this.authToken = '';
this.isConnected = false;
this.isConnecting = false;
this.reconnectAttempts = 0;
this.maxReconnectAttempts = 5;
this.reconnectDelay = 1000;
this.messageQueue = [];
this.requestHandlers = new Map();
this.eventListeners = new Map();
this.messageId = 1;
// Heartbeat configuration
this.heartbeatInterval = 30000; // 30 seconds
this.heartbeatTimer = null;
this.lastPongReceived = Date.now();
this.connectionTimeout = 10000; // 10 seconds
this.setupEventListeners();
}
/**
* Connect to WebSocket server
*/
async connect(url, authToken = '') {
if (this.isConnecting || this.isConnected) {
console.warn('Already connected or connecting');
return;
}
this.url = url;
this.authToken = authToken;
this.isConnecting = true;
try {
await this.establishConnection();
} catch (error) {
this.isConnecting = false;
throw error;
}
}
/**
* Establish WebSocket connection
*/
async establishConnection() {
return new Promise((resolve, reject) => {
try {
// Create WebSocket connection
this.ws = new WebSocket(this.url);
// Set up connection timeout
const connectionTimer = setTimeout(() => {
if (this.ws && this.ws.readyState !== WebSocket.OPEN) {
this.ws.close();
this.isConnecting = false;
reject(new Error('Connection timeout'));
}
}, this.connectionTimeout);
this.ws.onopen = () => {
clearTimeout(connectionTimer);
this.isConnected = true;
this.isConnecting = false;
this.reconnectAttempts = 0;
this.lastPongReceived = Date.now();
this.emit('connected');
this.startHeartbeat();
this.processMessageQueue();
console.log('WebSocket connected to:', this.url);
resolve();
};
this.ws.onclose = (event) => {
clearTimeout(connectionTimer);
this.handleDisconnection(event);
};
this.ws.onerror = (error) => {
clearTimeout(connectionTimer);
console.error('WebSocket error:', error);
this.isConnecting = false;
this.emit('error', error);
if (!this.isConnected) {
reject(error);
}
};
this.ws.onmessage = (event) => {
this.handleMessage(event);
};
} catch (error) {
this.isConnecting = false;
reject(error);
}
});
}
/**
* Disconnect from WebSocket server
*/
disconnect() {
if (this.ws) {
this.stopHeartbeat();
this.ws.close(1000, 'User initiated disconnect');
this.ws = null;
}
this.isConnected = false;
this.isConnecting = false;
this.reconnectAttempts = 0;
this.messageQueue = [];
this.requestHandlers.clear();
this.emit('disconnected');
}
/**
* Send a request and wait for response
*/
async sendRequest(method, params = {}) {
const id = this.generateMessageId();
const request = {
jsonrpc: '2.0',
id,
method,
params,
};
return new Promise((resolve, reject) => {
// Store request handler
this.requestHandlers.set(id, { resolve, reject });
// Send request
this.sendMessage(request);
// Set timeout for request
setTimeout(() => {
if (this.requestHandlers.has(id)) {
this.requestHandlers.delete(id);
reject(new Error(`Request timeout for method: ${method}`));
}
}, 30000); // 30 second timeout
});
}
/**
* Send a notification (no response expected)
*/
sendNotification(method, params = {}) {
const notification = {
jsonrpc: '2.0',
method,
params,
};
this.sendMessage(notification);
}
/**
* Send raw message
*/
sendMessage(message) {
if (!this.isConnected) {
// Queue message for later
this.messageQueue.push(message);
this.emit('message_queued', message);
return;
}
try {
const messageStr = JSON.stringify(message);
this.ws.send(messageStr);
this.emit('message_sent', message);
} catch (error) {
console.error('Failed to send message:', error);
this.emit('send_error', error);
}
}
/**
* Handle incoming messages
*/
handleMessage(event) {
try {
const message = JSON.parse(event.data);
// Handle pong response
if (message.method === 'pong') {
this.lastPongReceived = Date.now();
return;
}
// Handle responses to requests
if (message.id !== undefined && this.requestHandlers.has(message.id)) {
const handler = this.requestHandlers.get(message.id);
this.requestHandlers.delete(message.id);
if (message.error) {
handler.reject(new Error(message.error.message || 'Request failed'));
} else {
handler.resolve(message.result);
}
return;
}
// Handle notifications and other messages
if (message.method) {
this.emit('notification', message);
this.emit(`notification_${message.method}`, message.params);
}
this.emit('message_received', message);
} catch (error) {
console.error('Failed to parse WebSocket message:', error);
this.emit('parse_error', error);
}
}
/**
* Handle disconnection
*/
handleDisconnection(event) {
const wasConnected = this.isConnected;
this.isConnected = false;
this.isConnecting = false;
this.stopHeartbeat();
console.log('WebSocket disconnected:', event.code, event.reason);
if (wasConnected) {
this.emit('disconnected', { code: event.code, reason: event.reason });
// Attempt reconnection if not a clean close
if (event.code !== 1000 && this.reconnectAttempts < this.maxReconnectAttempts) {
this.attemptReconnection();
}
}
}
/**
* Attempt to reconnect
*/
async attemptReconnection() {
this.reconnectAttempts++;
const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1);
console.log(
`Attempting reconnection ${this.reconnectAttempts}/${this.maxReconnectAttempts} in ${delay}ms`,
);
this.emit('reconnecting', { attempt: this.reconnectAttempts, delay });
setTimeout(async () => {
try {
await this.establishConnection();
} catch (error) {
console.error('Reconnection failed:', error);
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
this.emit('reconnection_failed');
} else {
this.attemptReconnection();
}
}
}, delay);
}
/**
* Start heartbeat mechanism
*/
startHeartbeat() {
this.stopHeartbeat();
this.heartbeatTimer = setInterval(() => {
if (this.isConnected) {
// Check if we received a recent pong
const timeSinceLastPong = Date.now() - this.lastPongReceived;
if (timeSinceLastPong > this.heartbeatInterval * 2) {
console.warn('Heartbeat timeout - connection may be dead');
this.ws.close(1006, 'Heartbeat timeout');
return;
}
// Send ping
this.sendNotification('ping', { timestamp: Date.now() });
}
}, this.heartbeatInterval);
}
/**
* Stop heartbeat mechanism
*/
stopHeartbeat() {
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer);
this.heartbeatTimer = null;
}
}
/**
* Process queued messages
*/
processMessageQueue() {
while (this.messageQueue.length > 0 && this.isConnected) {
const message = this.messageQueue.shift();
this.sendMessage(message);
}
}
/**
* Generate unique message ID
*/
generateMessageId() {
return this.messageId++;
}
/**
* Set up internal event listeners
*/
setupEventListeners() {
// Handle page visibility changes
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
// Page is hidden - reduce heartbeat frequency
this.heartbeatInterval = 60000; // 1 minute
} else {
// Page is visible - restore normal heartbeat
this.heartbeatInterval = 30000; // 30 seconds
if (this.isConnected) {
this.startHeartbeat();
}
}
});
// Handle page unload
window.addEventListener('beforeunload', () => {
if (this.isConnected) {
this.disconnect();
}
});
}
/**
* Add event listener
*/
on(event, callback) {
if (!this.eventListeners.has(event)) {
this.eventListeners.set(event, []);
}
this.eventListeners.get(event).push(callback);
}
/**
* Remove event listener
*/
off(event, callback) {
if (this.eventListeners.has(event)) {
const listeners = this.eventListeners.get(event);
const index = listeners.indexOf(callback);
if (index > -1) {
listeners.splice(index, 1);
}
}
}
/**
* Emit event
*/
emit(event, data = null) {
if (this.eventListeners.has(event)) {
this.eventListeners.get(event).forEach((callback) => {
try {
callback(data);
} catch (error) {
console.error('Error in event listener:', error);
}
});
}
}
/**
* Get connection status
*/
getStatus() {
return {
connected: this.isConnected,
connecting: this.isConnecting,
url: this.url,
reconnectAttempts: this.reconnectAttempts,
queuedMessages: this.messageQueue.length,
pendingRequests: this.requestHandlers.size,
};
}
/**
* Initialize Claude Code session
*/
async initializeSession(clientInfo = {}) {
const params = {
protocolVersion: { major: 2024, minor: 11, patch: 5 },
clientInfo: {
name: 'Claude Flow v2',
version: '2.0.0',
...clientInfo,
},
capabilities: {
logging: { level: 'info' },
tools: { listChanged: true },
resources: { listChanged: false, subscribe: false },
prompts: { listChanged: false },
},
};
try {
const result = await this.sendRequest('initialize', params);
this.emit('session_initialized', result);
return result;
} catch (error) {
this.emit('session_error', error);
throw error;
}
}
/**
* Execute Claude Flow command
*/
async executeCommand(command, args = {}) {
try {
const result = await this.sendRequest('tools/call', {
name: 'claude-flow/execute',
arguments: { command, args },
});
return result;
} catch (error) {
console.error('Command execution failed:', error);
throw error;
}
}
/**
* Get available tools
*/
async getAvailableTools() {
try {
const result = await this.sendRequest('tools/list');
// The server returns { tools: [...] }, so we need to extract the tools array
return result && result.tools ? result.tools : [];
} catch (error) {
console.error('Failed to get tools:', error);
return []; // Return empty array on error instead of throwing
}
}
/**
* Get server health status
*/
async getHealthStatus() {
try {
return await this.sendRequest('tools/call', {
name: 'system/health',
});
} catch (error) {
console.error('Failed to get health status:', error);
throw error;
}
}
}