claude-collab
Version:
Claude Collab - The AI collaboration framework that prevents echo chambers
249 lines (209 loc) • 6.64 kB
JavaScript
/**
* Connection Wrapper for CLI
* Wraps the ConnectionManager for use in the CLI with automatic reconnection
*
* Created by Alex - Connection Expert
*/
const { EventEmitter } = require('events');
const WebSocket = require('ws');
class ConnectionWrapper extends EventEmitter {
constructor(config) {
super();
this.config = {
url: config.url || 'ws://localhost:8765',
maxReconnectAttempts: config.maxReconnectAttempts || 10,
initialReconnectDelay: config.initialReconnectDelay || 1000,
maxReconnectDelay: config.maxReconnectDelay || 30000,
reconnectBackoffMultiplier: config.reconnectBackoffMultiplier || 1.5,
heartbeatInterval: config.heartbeatInterval || 30000,
connectionTimeout: config.connectionTimeout || 10000,
enableAutoReconnect: config.enableAutoReconnect !== false
};
this.ws = null;
this.isConnected = false;
this.isReconnecting = false;
this.reconnectAttempts = 0;
this.reconnectDelay = this.config.initialReconnectDelay;
this.messageQueue = [];
this.heartbeatTimer = null;
this.reconnectTimer = null;
}
async connect() {
return new Promise((resolve, reject) => {
if (this.isConnected) {
resolve();
return;
}
this.emit('connecting', { url: this.config.url });
try {
this.ws = new WebSocket(this.config.url);
// Connection timeout
const timeoutTimer = setTimeout(() => {
if (!this.isConnected) {
this.ws.terminate();
const error = new Error('Connection timeout - server may be unavailable');
this.handleError(error);
reject(error);
}
}, this.config.connectionTimeout);
this.ws.on('open', () => {
clearTimeout(timeoutTimer);
this.isConnected = true;
this.isReconnecting = false;
this.reconnectAttempts = 0;
this.reconnectDelay = this.config.initialReconnectDelay;
this.emit('connected', {
reconnected: this.reconnectAttempts > 0
});
this.startHeartbeat();
this.flushMessageQueue();
resolve();
});
this.ws.on('message', (data) => {
this.emit('message', data);
});
this.ws.on('error', (error) => {
this.handleError(error);
if (!this.isConnected) {
clearTimeout(timeoutTimer);
reject(error);
}
});
this.ws.on('close', (code, reason) => {
clearTimeout(timeoutTimer);
this.handleClose(code, reason);
});
this.ws.on('pong', () => {
this.emit('pong');
});
} catch (error) {
this.handleError(error);
reject(error);
}
});
}
send(data) {
const message = typeof data === 'string' ? data : JSON.stringify(data);
if (this.isConnected && this.ws && this.ws.readyState === WebSocket.OPEN) {
try {
this.ws.send(message);
} catch (error) {
this.messageQueue.push(data);
}
} else {
this.messageQueue.push(data);
this.emit('messageQueued', { queueLength: this.messageQueue.length });
}
}
disconnect() {
this.config.enableAutoReconnect = false;
this.clearTimers();
if (this.ws) {
this.ws.removeAllListeners();
if (this.ws.readyState === WebSocket.OPEN) {
this.ws.close(1000, 'Client disconnect');
} else {
this.ws.terminate();
}
}
this.isConnected = false;
this.isReconnecting = false;
}
handleError(error) {
this.emit('error', error);
// Provide helpful error messages
if (error.code === 'ECONNREFUSED') {
this.emit('serverUnavailable', {
message: 'Cannot connect to Claude-Collab server',
suggestion: 'Please ensure the server is running with: claude-collab server'
});
} else if (error.code === 'ETIMEDOUT') {
this.emit('timeout', {
message: 'Connection timed out',
suggestion: 'Check your network connection and server status'
});
}
}
handleClose(code, reason) {
this.clearTimers();
this.isConnected = false;
this.emit('disconnected', { code, reason: reason.toString() });
// Auto-reconnect if enabled and not a clean close
if (this.config.enableAutoReconnect &&
code !== 1000 &&
this.reconnectAttempts < this.config.maxReconnectAttempts) {
this.scheduleReconnect();
}
}
scheduleReconnect() {
if (this.isReconnecting) return;
this.isReconnecting = true;
this.reconnectAttempts++;
this.emit('reconnecting', {
attempt: this.reconnectAttempts,
maxAttempts: this.config.maxReconnectAttempts,
delay: this.reconnectDelay
});
this.reconnectTimer = setTimeout(async () => {
try {
await this.connect();
} catch (error) {
if (this.reconnectAttempts >= this.config.maxReconnectAttempts) {
this.emit('reconnectFailed', {
attempts: this.reconnectAttempts,
lastError: error
});
this.isReconnecting = false;
}
}
}, this.reconnectDelay);
// Exponential backoff
this.reconnectDelay = Math.min(
this.reconnectDelay * this.config.reconnectBackoffMultiplier,
this.config.maxReconnectDelay
);
}
startHeartbeat() {
this.heartbeatTimer = setInterval(() => {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.ws.ping();
}
}, this.config.heartbeatInterval);
}
flushMessageQueue() {
if (this.messageQueue.length === 0) return;
const queue = [...this.messageQueue];
this.messageQueue = [];
for (const message of queue) {
this.send(message);
}
}
clearTimers() {
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer);
this.heartbeatTimer = null;
}
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
this.reconnectTimer = null;
}
}
// WebSocket compatibility methods
get readyState() {
return this.ws ? this.ws.readyState : WebSocket.CLOSED;
}
close() {
this.disconnect();
}
// Forward event listeners to underlying WebSocket
on(event, listener) {
if (event === 'open') {
return super.on('connected', listener);
}
if (event === 'close') {
return super.on('disconnected', listener);
}
return super.on(event, listener);
}
}
module.exports = ConnectionWrapper;