something
This commit is contained in:
177
web/src/utils/websocket.js
Normal file
177
web/src/utils/websocket.js
Normal file
@@ -0,0 +1,177 @@
|
||||
// Shared WebSocket connection manager
|
||||
// All components should use this instead of creating their own connections
|
||||
|
||||
class WebSocketManager {
|
||||
constructor() {
|
||||
this.ws = null;
|
||||
this.listeners = new Map(); // Map of listener IDs to callback functions
|
||||
this.reconnectTimeout = null;
|
||||
this.reconnectDelay = 2000;
|
||||
this.isConnecting = false;
|
||||
this.listenerIdCounter = 0;
|
||||
this.verboseLogging = false; // Set to true to enable verbose WebSocket logging
|
||||
}
|
||||
|
||||
connect() {
|
||||
// If already connected or connecting, don't create a new connection
|
||||
if (this.ws && (this.ws.readyState === WebSocket.CONNECTING || this.ws.readyState === WebSocket.OPEN)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isConnecting) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isConnecting = true;
|
||||
|
||||
try {
|
||||
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const wsHost = window.location.host;
|
||||
const API_BASE = '/api';
|
||||
const url = `${wsProtocol}//${wsHost}${API_BASE}/jobs/ws`;
|
||||
|
||||
this.ws = new WebSocket(url);
|
||||
|
||||
this.ws.onopen = () => {
|
||||
if (this.verboseLogging) {
|
||||
console.log('Shared WebSocket connected');
|
||||
}
|
||||
this.isConnecting = false;
|
||||
this.notifyListeners('open', {});
|
||||
};
|
||||
|
||||
this.ws.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
if (this.verboseLogging) {
|
||||
console.log('WebSocketManager: Message received:', data.type, data.channel || 'no channel', data);
|
||||
}
|
||||
this.notifyListeners('message', data);
|
||||
} catch (error) {
|
||||
console.error('WebSocketManager: Failed to parse message:', error, 'Raw data:', event.data);
|
||||
}
|
||||
};
|
||||
|
||||
this.ws.onerror = (error) => {
|
||||
console.error('Shared WebSocket error:', error);
|
||||
this.isConnecting = false;
|
||||
this.notifyListeners('error', error);
|
||||
};
|
||||
|
||||
this.ws.onclose = (event) => {
|
||||
if (this.verboseLogging) {
|
||||
console.log('Shared WebSocket closed:', {
|
||||
code: event.code,
|
||||
reason: event.reason,
|
||||
wasClean: event.wasClean
|
||||
});
|
||||
}
|
||||
this.ws = null;
|
||||
this.isConnecting = false;
|
||||
this.notifyListeners('close', event);
|
||||
|
||||
// Always retry connection
|
||||
if (this.reconnectTimeout) {
|
||||
clearTimeout(this.reconnectTimeout);
|
||||
}
|
||||
this.reconnectTimeout = setTimeout(() => {
|
||||
if (!this.ws || this.ws.readyState === WebSocket.CLOSED) {
|
||||
this.connect();
|
||||
}
|
||||
}, this.reconnectDelay);
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Failed to create WebSocket:', error);
|
||||
this.isConnecting = false;
|
||||
// Retry after delay
|
||||
this.reconnectTimeout = setTimeout(() => {
|
||||
this.connect();
|
||||
}, this.reconnectDelay);
|
||||
}
|
||||
}
|
||||
|
||||
subscribe(listenerId, callbacks) {
|
||||
// Generate ID if not provided
|
||||
if (!listenerId) {
|
||||
listenerId = `listener_${this.listenerIdCounter++}`;
|
||||
}
|
||||
|
||||
if (this.verboseLogging) {
|
||||
console.log('WebSocketManager: Subscribing listener:', listenerId, 'WebSocket state:', this.ws ? this.ws.readyState : 'no connection');
|
||||
}
|
||||
this.listeners.set(listenerId, callbacks);
|
||||
|
||||
// Connect if not already connected
|
||||
if (!this.ws || this.ws.readyState === WebSocket.CLOSED) {
|
||||
if (this.verboseLogging) {
|
||||
console.log('WebSocketManager: WebSocket not connected, connecting...');
|
||||
}
|
||||
this.connect();
|
||||
}
|
||||
|
||||
// If already open, notify immediately
|
||||
if (this.ws && this.ws.readyState === WebSocket.OPEN && callbacks.open) {
|
||||
if (this.verboseLogging) {
|
||||
console.log('WebSocketManager: WebSocket already open, calling open callback for listener:', listenerId);
|
||||
}
|
||||
// Use setTimeout to ensure this happens after the listener is registered
|
||||
setTimeout(() => {
|
||||
if (callbacks.open) {
|
||||
callbacks.open();
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
return listenerId;
|
||||
}
|
||||
|
||||
unsubscribe(listenerId) {
|
||||
this.listeners.delete(listenerId);
|
||||
|
||||
// If no more listeners, we could close the connection, but let's keep it open
|
||||
// in case other components need it
|
||||
}
|
||||
|
||||
send(data) {
|
||||
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
||||
if (this.verboseLogging) {
|
||||
console.log('WebSocketManager: Sending message:', data);
|
||||
}
|
||||
this.ws.send(JSON.stringify(data));
|
||||
} else {
|
||||
console.warn('WebSocketManager: Cannot send message - connection not open. State:', this.ws ? this.ws.readyState : 'no connection', 'Message:', data);
|
||||
}
|
||||
}
|
||||
|
||||
notifyListeners(eventType, data) {
|
||||
this.listeners.forEach((callbacks) => {
|
||||
if (callbacks[eventType]) {
|
||||
try {
|
||||
callbacks[eventType](data);
|
||||
} catch (error) {
|
||||
console.error('Error in WebSocket listener:', error);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getReadyState() {
|
||||
return this.ws ? this.ws.readyState : WebSocket.CLOSED;
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
if (this.reconnectTimeout) {
|
||||
clearTimeout(this.reconnectTimeout);
|
||||
this.reconnectTimeout = null;
|
||||
}
|
||||
if (this.ws) {
|
||||
this.ws.close();
|
||||
this.ws = null;
|
||||
}
|
||||
this.listeners.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const wsManager = new WebSocketManager();
|
||||
|
||||
Reference in New Issue
Block a user