/**
* gRPC Client Service - Real-time communication with backend
* Reemplaza REST polling con gRPC streaming para datos en tiempo real
*/
class GrpcClient {
constructor() {
this.server_url = this.getServerUrl();
this.isConnected = false;
this.reconnectAttempts = 0;
this.maxReconnectAttempts = 5;
this.reconnectInterval = 3000;
this.listeners = new Map();
// gRPC streams management
this.streams = new Map();
this.streamHandlers = new Map();
// Client configuration
this.clientId = this.generateClientId();
this.heartbeatInterval = null;
this.connectionTimeout = 30000; // 30 seconds
}
/**
* Get gRPC server URL
*/
getServerUrl() {
if (typeof window !== 'undefined') {
const host = window.location.hostname;
// Use port 50051 for gRPC server
return `${host}:50051`;
}
return 'localhost:50051';
}
/**
* Generate unique client ID
*/
generateClientId() {
return `dashboard_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
/**
* Initialize gRPC connection
*/
async connect() {
try {
console.log(`🔗 Connecting to gRPC server at ${this.server_url}`);
// Simulate gRPC connection initialization
// In a real implementation, you would use @grpc/grpc-js here
// For browser compatibility, we're using WebSocket with gRPC-like protocol
await this.initializeWebSocketConnection();
this.isConnected = true;
this.reconnectAttempts = 0;
this.startHeartbeat();
this.emit('connection', { status: 'connected', clientId: this.clientId });
console.log(`✅ gRPC Client connected successfully (ID: ${this.clientId})`);
return true;
} catch (error) {
console.error('❌ gRPC connection failed:', error);
this.handleConnectionError();
return false;
}
}
/**
* Initialize WebSocket connection for browser compatibility
* Uses gRPC-like message format
*/
async initializeWebSocketConnection() {
return new Promise((resolve, reject) => {
try {
// Use WebSocket for browser compatibility with gRPC-like protocol
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${wsProtocol}//${this.server_url.replace(':50051', ':8080')}/grpc-ws`;
this.ws = new WebSocket(wsUrl);
this.ws.onopen = () => {
console.log('🔗 WebSocket connection established for gRPC streaming');
// Send connection handshake
this.sendMessage({
type: 'handshake',
clientId: this.clientId,
timestamp: Date.now()
});
resolve();
};
this.ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
this.handleIncomingMessage(data);
} catch (error) {
console.warn('⚠️ Failed to parse gRPC message:', error);
}
};
this.ws.onerror = (error) => {
console.error('❌ WebSocket error:', error);
reject(error);
};
this.ws.onclose = () => {
console.log('🔌 WebSocket connection closed');
this.handleConnectionLoss();
};
// Connection timeout
setTimeout(() => {
if (this.ws.readyState !== WebSocket.OPEN) {
reject(new Error('Connection timeout'));
}
}, this.connectionTimeout);
} catch (error) {
reject(error);
}
});
}
/**
* Send message through WebSocket with gRPC-like format
*/
sendMessage(message) {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
const grpcMessage = {
...message,
clientId: this.clientId,
timestamp: Date.now()
};
this.ws.send(JSON.stringify(grpcMessage));
} else {
console.warn('⚠️ Cannot send message: WebSocket not connected');
}
}
/**
* Handle incoming messages from server
*/
handleIncomingMessage(data) {
const { type, payload, timestamp } = data;
switch (type) {
case 'handshake_ack':
console.log('✅ gRPC handshake acknowledged');
break;
case 'live_stats':
this.emit('live_stats', payload);
break;
case 'active_sessions':
this.emit('active_sessions_update', payload);
break;
case 'conversations_update':
this.emit('conversations_update', payload);
break;
case 'heartbeat':
// Server heartbeat response
this.lastHeartbeat = timestamp;
break;
case 'server_shutdown':
console.log('🛑 Server shutting down');
this.handleConnectionLoss();
break;
default:
console.log(`📡 Received message type: ${type}`, payload);
this.emit(type, payload);
}
}
/**
* Disconnect from server
*/
disconnect() {
this.isConnected = false;
this.stopHeartbeat();
// Close all active streams
for (const [name, stream] of this.streams) {
try {
stream.cancel();
console.log(`🛑 Closed stream: ${name}`);
} catch (error) {
console.warn(`⚠️ Error closing stream ${name}:`, error);
}
}
this.streams.clear();
// Close WebSocket connection
if (this.ws) {
this.ws.close();
this.ws = null;
}
this.emit('connection', { status: 'disconnected' });
console.log('🔌 gRPC Client disconnected');
}
/**
* Start streaming live statistics
*/
streamLiveStats() {
if (!this.isConnected) {
console.warn('⚠️ Not connected, cannot start stats stream');
return;
}
console.log('📊 Starting live stats stream');
this.sendMessage({
type: 'subscribe',
stream: 'live_stats',
filters: {
include_project_stats: true,
include_recent_activity: true
}
});
this.streams.set('live_stats', { active: true });
}
/**
* Stream active sessions in real-time
*/
streamActiveSessions() {
if (!this.isConnected) {
console.warn('⚠️ Not connected, cannot start active sessions stream');
return;
}
console.log('👥 Starting active sessions stream');
this.sendMessage({
type: 'subscribe',
stream: 'active_sessions',
filters: {
include_details: true,
activity_threshold_minutes: 30
}
});
this.streams.set('active_sessions', { active: true });
}
/**
* Stream conversations updates
*/
streamConversations(projectFilter = null) {
if (!this.isConnected) {
console.warn('⚠️ Not connected, cannot start conversations stream');
return;
}
console.log('💬 Starting conversations stream');
this.sendMessage({
type: 'subscribe',
stream: 'conversations',
filters: {
project_filter: projectFilter,
include_recent_messages: true,
max_sessions_per_project: 10
}
});
this.streams.set('conversations', { active: true });
}
/**
* Get conversation tree (unary call)
*/
async getConversationTree(filters = {}) {
return new Promise((resolve, reject) => {
if (!this.isConnected) {
reject(new Error('Not connected to gRPC server'));
return;
}
const requestId = `tree_${Date.now()}`;
// Set up response listener
const responseHandler = (data) => {
if (data.requestId === requestId) {
this.off('conversation_tree_response', responseHandler);
resolve(data.payload);
}
};
this.on('conversation_tree_response', responseHandler);
// Send request
this.sendMessage({
type: 'get_conversation_tree',
requestId,
filters
});
// Timeout handling
setTimeout(() => {
this.off('conversation_tree_response', responseHandler);
reject(new Error('Request timeout'));
}, 10000);
});
}
/**
* Search conversations (unary call)
*/
async searchConversations(query, filters = {}) {
return new Promise((resolve, reject) => {
if (!this.isConnected) {
reject(new Error('Not connected to gRPC server'));
return;
}
const requestId = `search_${Date.now()}`;
// Set up response listener
const responseHandler = (data) => {
if (data.requestId === requestId) {
this.off('search_response', responseHandler);
resolve(data.payload);
}
};
this.on('search_response', responseHandler);
// Send request
this.sendMessage({
type: 'search_conversations',
requestId,
query,
filters
});
// Timeout handling
setTimeout(() => {
this.off('search_response', responseHandler);
reject(new Error('Search timeout'));
}, 10000);
});
}
/**
* Start heartbeat mechanism
*/
startHeartbeat() {
this.heartbeatInterval = setInterval(() => {
if (this.isConnected) {
this.sendMessage({
type: 'heartbeat',
timestamp: Date.now()
});
}
}, 30000); // 30 seconds heartbeat
}
/**
* Stop heartbeat mechanism
*/
stopHeartbeat() {
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval);
this.heartbeatInterval = null;
}
}
/**
* Handle connection loss
*/
handleConnectionLoss() {
this.isConnected = false;
this.stopHeartbeat();
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnectAttempts++;
const delay = this.reconnectInterval * Math.pow(2, this.reconnectAttempts - 1);
console.log(`🔄 Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`);
setTimeout(() => {
this.connect();
}, delay);
} else {
console.error('❌ Max reconnection attempts reached');
this.emit('connection', { status: 'error' });
}
}
/**
* Handle connection errors
*/
handleConnectionError() {
this.isConnected = false;
this.stopHeartbeat();
this.handleConnectionLoss();
}
// === Event System ===
/**
* Register event listener
*/
on(event, callback) {
if (!this.listeners.has(event)) {
this.listeners.set(event, new Set());
}
this.listeners.get(event).add(callback);
}
/**
* Remove event listener
*/
off(event, callback) {
if (this.listeners.has(event)) {
this.listeners.get(event).delete(callback);
}
}
/**
* Emit event to listeners
*/
emit(event, data) {
if (this.listeners.has(event)) {
this.listeners.get(event).forEach(callback => {
try {
callback(data);
} catch (error) {
console.error(`❌ Error in ${event} listener:`, error);
}
});
}
}
/**
* Get connection status
*/
getConnectionStatus() {
return this.isConnected ? 'connected' : 'disconnected';
}
/**
* Get client statistics
*/
getClientStats() {
return {
clientId: this.clientId,
isConnected: this.isConnected,
reconnectAttempts: this.reconnectAttempts,
activeStreams: Array.from(this.streams.keys()),
lastHeartbeat: this.lastHeartbeat || null
};
}
}
// Create and export singleton instance for browser
let grpcClient;
if (typeof window !== 'undefined') {
if (!window.grpcClientInstance) {
window.grpcClientInstance = new GrpcClient();
}
grpcClient = window.grpcClientInstance;
} else {
grpcClient = new GrpcClient();
}
// Auto-initialize in browser
if (typeof window !== 'undefined' && !window.grpcClientInitialized) {
window.grpcClientInitialized = true;
// Auto-connect when page loads
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
grpcClient.connect().catch(error => {
console.error('❌ Failed to auto-connect gRPC client:', error);
});
});
} else {
grpcClient.connect().catch(error => {
console.error('❌ Failed to auto-connect gRPC client:', error);
});
}
// Handle page visibility changes
document.addEventListener('visibilitychange', () => {
if (!document.hidden && grpcClient.getConnectionStatus() === 'disconnected') {
grpcClient.connect();
}
});
// Handle online/offline events
window.addEventListener('online', () => {
console.log('📶 Network online, reconnecting gRPC...');
grpcClient.connect();
});
window.addEventListener('offline', () => {
console.log('📵 Network offline, disconnecting gRPC');
grpcClient.disconnect();
});
// Cleanup before unload
window.addEventListener('beforeunload', () => {
grpcClient.disconnect();
});
}
export default grpcClient;