feat: Add WebSocket service with typed message handling, auto-reconnect, and subscription management

- Add WebSocketMessage, ServiceUpdate, SystemMetrics, AlertUpdate interfaces for typed message handling
- Add WebSocketService class with connection management, 5 max reconnect attempts, exponential backoff (1s-30s delay)
- Add subscribe/unsubscribe pattern with type-based message routing and listener cleanup
- Add getWebSocketService singleton factory with auto-detect ws/wss protocol and disconn
This commit is contained in:
nutcas3
2026-04-19 05:11:55 +03:00
parent a895a3ab18
commit 20bd86cf11
+171
View File
@@ -0,0 +1,171 @@
export interface WebSocketMessage {
type: string;
data: any;
timestamp: string;
}
export interface ServiceUpdate {
service: string;
status: string;
replicas: number;
readyReplicas: number;
timestamp: string;
}
export interface SystemMetrics {
cpu: number;
memory: number;
disk: number;
network: number;
timestamp: string;
}
export interface AlertUpdate {
id: string;
severity: string;
message: string;
status: string;
timestamp: string;
}
export class WebSocketService {
private ws: WebSocket | null = null;
private reconnectAttempts = 0;
private maxReconnectAttempts = 5;
private reconnectDelay = 1000;
private listeners: Map<string, ((data: any) => void)[]> = new Map();
private isConnecting = false;
constructor(private url: string) {
this.connect();
}
private connect() {
if (this.isConnecting || (this.ws && this.ws.readyState === WebSocket.OPEN)) {
return;
}
this.isConnecting = true;
try {
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
console.log('WebSocket connected');
this.isConnecting = false;
this.reconnectAttempts = 0;
this.reconnectDelay = 1000;
};
this.ws.onmessage = (event) => {
try {
const message: WebSocketMessage = JSON.parse(event.data);
this.handleMessage(message);
} catch (error) {
console.error('Failed to parse WebSocket message:', error);
}
};
this.ws.onclose = (event) => {
console.log('WebSocket disconnected:', event.code, event.reason);
this.isConnecting = false;
this.handleReconnect();
};
this.ws.onerror = (error) => {
console.error('WebSocket error:', error);
this.isConnecting = false;
};
} catch (error) {
console.error('Failed to create WebSocket connection:', error);
this.isConnecting = false;
this.handleReconnect();
}
}
private handleReconnect() {
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnectAttempts++;
console.log(`Attempting to reconnect (${this.reconnectAttempts}/${this.maxReconnectAttempts})...`);
setTimeout(() => {
this.connect();
}, this.reconnectDelay);
// Exponential backoff
this.reconnectDelay = Math.min(this.reconnectDelay * 2, 30000);
} else {
console.error('Max reconnection attempts reached');
}
}
private handleMessage(message: WebSocketMessage) {
const listeners = this.listeners.get(message.type);
if (listeners) {
listeners.forEach(listener => {
try {
listener(message.data);
} catch (error) {
console.error('Error in WebSocket listener:', error);
}
});
}
}
public subscribe(type: string, callback: (data: any) => void) {
if (!this.listeners.has(type)) {
this.listeners.set(type, []);
}
this.listeners.get(type)!.push(callback);
// Return unsubscribe function
return () => {
const listeners = this.listeners.get(type);
if (listeners) {
const index = listeners.indexOf(callback);
if (index > -1) {
listeners.splice(index, 1);
}
}
};
}
public send(message: WebSocketMessage) {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(message));
} else {
console.warn('WebSocket not connected, message not sent:', message);
}
}
public disconnect() {
if (this.ws) {
this.ws.close();
this.ws = null;
}
this.listeners.clear();
}
public isConnected(): boolean {
return this.ws !== null && this.ws.readyState === WebSocket.OPEN;
}
}
// Global WebSocket service instance
let wsService: WebSocketService | null = null;
export function getWebSocketService(url?: string): WebSocketService {
if (!wsService) {
const wsUrl = url || `${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.host}/ws`;
wsService = new WebSocketService(wsUrl);
}
return wsService;
}
export function disconnectWebSocket() {
if (wsService) {
wsService.disconnect();
wsService = null;
}
}