/**
* @deprecated This service is deprecated and should not be used for new code.
* Use `unifiedWebSocketManager` from `./unified-websocket-manager.ts` instead.
*
* This service maintained multiple WebSocket connections which caused:
* - Connection overhead and resource waste
* - Inconsistent behavior across views
* - Difficulty tracking user activity
*
* The unified WebSocket manager provides:
* - Single persistent connection with auto-reconnect
* - Pub/sub pattern for message routing
* - Activity tracking for authenticated and anonymous users
* - Better connection state management
*
* Migration example:
* ```typescript
* // Old:
* import { webSocketService } from './services/websocket-service';
* webSocketService.connectToFlowUpdates(callback, onOpen, onClose);
* webSocketService.disconnectFromFlowUpdates();
*
* // New:
* import { unifiedWebSocketManager } from './services/unified-websocket-manager';
* const unsubscribe = unifiedWebSocketManager.subscribe('flow_executions', callback);
* unsubscribe(); // Clean up when done
* ```
*/
class WebSocketService {
private connections: Map<string, WebSocket> = new Map();
private heartbeatIntervals: Map<string, number> = new Map();
/**
* Connect to a WebSocket endpoint
* @param key Unique identifier for this connection
* @param url WebSocket URL
* @param onMessageCallback Callback for incoming messages
* @param onOpenCallback Optional callback when connection opens
* @param onCloseCallback Optional callback when connection closes
*/
connect(
key: string,
url: string,
onMessageCallback: (message: any) => void,
onOpenCallback?: () => void,
onCloseCallback?: () => void
) {
// Disconnect existing connection if any
this.disconnect(key);
const ws = new WebSocket(url);
this.connections.set(key, ws);
ws.onopen = () => {
console.log(`WebSocket connected: ${key}`);
this.startHeartbeat(key);
if (onOpenCallback) {
onOpenCallback();
}
};
ws.onmessage = (event) => {
// Handle ping/pong - backend expects JSON format
try {
const message = JSON.parse(event.data);
if (message.type === 'ping') {
ws.send(JSON.stringify({ type: 'pong' }));
return;
}
onMessageCallback(message);
} catch (e) {
console.error('Failed to parse WebSocket message:', e);
}
};
ws.onclose = () => {
console.log(`WebSocket disconnected: ${key}`);
this.stopHeartbeat(key);
this.connections.delete(key);
if (onCloseCallback) {
onCloseCallback();
}
};
ws.onerror = (error) => {
console.error(`WebSocket error (${key}):`, error);
};
}
/**
* Connect to a flow execution WebSocket for real-time updates
*/
connectToExecution(
executionId: string,
onMessageCallback: (message: any) => void,
onOpenCallback?: () => void,
onCloseCallback?: () => void
) {
const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws';
const host = window.location.host;
// Get access token from localStorage for authentication
const token = localStorage.getItem('accessToken');
if (!token) {
console.error('No access token found for WebSocket connection');
if (onCloseCallback) {
onCloseCallback();
}
return;
}
// Include token as query parameter
const url = `${protocol}://${host}/api/v1/ws/flow-executions/${executionId}?token=${encodeURIComponent(token)}`;
this.connect(
`execution-${executionId}`,
url,
onMessageCallback,
onOpenCallback,
onCloseCallback
);
}
/**
* Connect to general WebSocket for all flow updates
*/
connectToFlowUpdates(
onMessageCallback: (message: any) => void,
onOpenCallback?: () => void,
onCloseCallback?: () => void
) {
const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws';
const host = window.location.host;
// Get access token from localStorage for authentication
const token = localStorage.getItem('accessToken');
if (!token) {
console.error('No access token found for WebSocket connection');
if (onCloseCallback) {
onCloseCallback();
}
return;
}
// Include token as query parameter
const url = `${protocol}://${host}/api/v1/ws?token=${encodeURIComponent(token)}`;
this.connect(
'flow-updates',
url,
onMessageCallback,
onOpenCallback,
onCloseCallback
);
}
/**
* Connect to general WebSocket to receive approval updates
* This uses the same WebSocket endpoint as flow updates since
* the backend broadcasts both types of events to authenticated users
*/
connectToApprovalUpdates(
onMessageCallback: (message: any) => void,
onOpenCallback?: () => void,
onCloseCallback?: () => void
) {
// Approval updates come through the same general WebSocket as flow updates
// The backend filters messages by account_id, so we just need to connect
// and filter approval-related messages on the client side
this.connectToFlowUpdates(
(message) => {
// Only pass through approval-related messages
if (message.type?.startsWith('approval_')) {
onMessageCallback(message);
}
},
onOpenCallback,
onCloseCallback
);
}
private startHeartbeat(key: string) {
const interval = window.setInterval(() => {
const ws = this.connections.get(key);
if (ws?.readyState === WebSocket.OPEN) {
// Send JSON ping as keepalive - backend will respond with pong
ws.send(JSON.stringify({ type: 'ping' }));
}
}, 30000); // 30 seconds
this.heartbeatIntervals.set(key, interval);
}
private stopHeartbeat(key: string) {
const interval = this.heartbeatIntervals.get(key);
if (interval) {
clearInterval(interval);
this.heartbeatIntervals.delete(key);
}
}
disconnect(key: string) {
const ws = this.connections.get(key);
if (ws) {
ws.close();
this.connections.delete(key);
}
this.stopHeartbeat(key);
}
/**
* Disconnect from execution WebSocket
*/
disconnectFromExecution(executionId: string) {
this.disconnect(`execution-${executionId}`);
}
/**
* Disconnect from general flow updates WebSocket
*/
disconnectFromFlowUpdates() {
this.disconnect('flow-updates');
}
/**
* Disconnect all WebSocket connections
*/
disconnectAll() {
for (const key of this.connections.keys()) {
this.disconnect(key);
}
}
/**
* Send data to a specific WebSocket connection
*/
send(key: string, data: any) {
const ws = this.connections.get(key);
if (ws?.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(data));
} else {
console.warn(`WebSocket not connected (${key}). Cannot send data.`);
}
}
/**
* Send command to execution
*/
sendToExecution(executionId: string, data: any) {
this.send(`execution-${executionId}`, data);
}
}
export const webSocketService = new WebSocketService();