cntx-ui
Version:
Autonomous Repository Intelligence engine with web UI and MCP server. Unified semantic code understanding, local RAG, and agent working memory.
284 lines (283 loc) • 9.34 kB
JavaScript
/**
* WebSocket Manager for cntx-ui
* Handles real-time communication between server and dashboard
*/
import { WebSocketServer, WebSocket } from 'ws';
export default class WebSocketManager {
bundleManager;
configManager;
verbose;
isMcp;
clients;
wss;
constructor(bundleManager, configManager, options = {}) {
this.bundleManager = bundleManager;
this.configManager = configManager;
this.verbose = options.verbose || false;
this.isMcp = options.isMcp || false;
this.clients = new Set();
this.wss = null;
}
log(message) {
if (this.isMcp) {
process.stderr.write(message + '\n');
}
else {
console.log(message);
}
}
initialize(httpServer) {
this.wss = new WebSocketServer({ server: httpServer });
this.wss.on('connection', (ws) => {
this.handleConnection(ws);
});
this.wss.on('error', (error) => {
if (this.verbose) {
console.error('🔌 WebSocket server error:', error.message);
}
});
if (this.verbose) {
this.log('🔌 WebSocket server initialized');
}
}
handleConnection(ws) {
this.clients.add(ws);
if (this.verbose) {
this.log(`📱 WebSocket client connected (${this.clients.size} total clients)`);
}
// Send initial status
this.sendUpdate(ws);
ws.on('close', () => {
this.clients.delete(ws);
if (this.verbose) {
this.log(`📱 WebSocket client disconnected (${this.clients.size} total clients)`);
}
});
ws.on('error', (error) => {
if (this.verbose) {
console.error('WebSocket client error:', error.message);
}
this.clients.delete(ws);
});
ws.on('message', (message) => {
try {
const data = JSON.parse(message.toString());
this.handleClientMessage(ws, data);
}
catch (error) {
if (this.verbose) {
console.error('Invalid WebSocket message:', error.message);
}
}
});
}
handleClientMessage(ws, data) {
if (this.verbose) {
this.log('📩 Received client message: ' + data.type);
}
switch (data.type) {
case 'ping':
ws.send(JSON.stringify({ type: 'pong', timestamp: Date.now() }));
break;
case 'get_status':
this.sendUpdate(ws);
break;
default:
if (this.verbose) {
console.warn('Unknown message type:', data.type);
}
}
}
get connectedClientsCount() {
return this.clients.size;
}
broadcast(type, payload) {
const data = JSON.stringify({ type, payload, timestamp: Date.now() });
this.clients.forEach(client => {
if (client.readyState === WebSocket.OPEN) {
try {
client.send(data);
}
catch (error) {
this.clients.delete(client);
}
}
});
}
/**
* Broadcast a general status update
*/
broadcastUpdate() {
if (this.verbose) {
this.log('📢 Broadcasting status update to all clients');
}
this.clients.forEach(client => {
this.sendUpdate(client);
});
}
sendUpdate(client) {
if (client.readyState !== WebSocket.OPEN)
return;
try {
const bundles = this.configManager.getBundles();
const bundleData = Array.from(bundles.entries()).map(([name, bundle]) => ({
name,
changed: bundle.changed,
fileCount: bundle.files.length,
content: bundle.content ? bundle.content.substring(0, 200) + '...' : '',
files: bundle.files,
lastGenerated: bundle.generated,
size: bundle.size,
patterns: bundle.patterns
}));
const updateData = {
type: 'status_update',
payload: {
bundles: bundleData,
scanning: this.bundleManager.isScanning || false,
totalFiles: this.bundleManager.fileSystemManager?.getAllFiles()?.length || 0
},
timestamp: Date.now()
};
client.send(JSON.stringify(updateData));
}
catch (error) {
if (this.verbose) {
console.error('Failed to send WebSocket update:', error.message);
}
this.clients.delete(client);
}
}
broadcastBundleUpdate(bundleName) {
if (this.verbose) {
this.log(`📢 Broadcasting update for bundle: ${bundleName}`);
}
const bundle = this.configManager.getBundles().get(bundleName);
if (!bundle)
return;
const updateData = {
type: 'bundle_update',
payload: {
name: bundleName,
changed: bundle.changed,
fileCount: bundle.files.length,
size: bundle.size,
generated: bundle.generated
},
timestamp: Date.now()
};
this.broadcastToActiveClients(updateData);
}
broadcastFileChange(filename, eventType) {
const updateData = {
type: 'file_change',
payload: {
filename,
eventType
},
timestamp: Date.now()
};
this.broadcastToActiveClients(updateData);
}
broadcastStatusUpdate(status) {
const updateData = {
type: 'status',
payload: status,
timestamp: Date.now()
};
this.broadcastToActiveClients(updateData);
}
ping() {
const pingData = { type: 'ping', timestamp: Date.now() };
this.clients.forEach(client => {
if (client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify(pingData));
}
else {
this.clients.delete(client);
}
});
}
close() {
if (this.wss) {
if (this.verbose) {
this.log('🔌 Closing WebSocket server');
}
this.clients.forEach(client => {
try {
client.terminate();
}
catch (error) {
if (this.verbose) {
console.error('Error closing WebSocket client:', error.message);
}
}
});
this.wss.close(() => {
if (this.verbose) {
this.log('🔌 WebSocket server closed');
}
});
this.clients.clear();
}
}
getStatus() {
return {
connected: this.clients.size,
server: this.wss ? 'running' : 'stopped'
};
}
onBundleGenerated(bundleName) {
this.broadcastBundleUpdate(bundleName);
}
onFileChanged(filename, eventType) {
this.broadcastFileChange(filename, eventType);
// Also broadcast bundle changes if needed
const affectedBundles = this.getAffectedBundles(filename);
affectedBundles.forEach(bundleName => {
this.broadcastBundleUpdate(bundleName);
});
}
onBundleSyncStarted(bundleName) {
this.broadcast('bundle_sync_started', { name: bundleName });
}
onBundleSyncCompleted(bundleName) {
this.broadcast('bundle_sync_completed', { name: bundleName });
}
onBundleSyncFailed(bundleName, error) {
this.broadcast('bundle_sync_failed', { name: bundleName, error: error.message });
}
broadcastBundleFileChanged(bundleName, filename) {
this.broadcast('bundle_file_changed', { bundleName, filename });
}
broadcastBundleSyncCompleted(bundleName) {
this.broadcast('bundle_sync_completed', { bundleName });
}
broadcastToActiveClients(data) {
const message = JSON.stringify(data);
this.clients.forEach(client => {
if (client.readyState === WebSocket.OPEN) {
try {
client.send(message);
}
catch (error) {
if (this.verbose) {
console.error('Failed to send WebSocket update:', error.message);
}
this.clients.delete(client);
}
}
});
}
getAffectedBundles(filename) {
const bundles = this.configManager.getBundles();
const affectedBundles = [];
bundles.forEach((bundle, name) => {
const matchesBundle = bundle.patterns.some(pattern => this.bundleManager.fileSystemManager.matchesPattern(filename, pattern));
if (matchesBundle) {
affectedBundles.push(name);
}
});
return affectedBundles;
}
}