Files
basicswap/basicswap/static/js/modules/websocket-manager.js
T
Gerlof van Ek 748dd388cb Extra refactor + Various bug/fixes. (#293)
* Refactor + Various Fixes.

* WS / LINT

* Show also failed status.

* Fix sorting market +/-

* Simplified swaps in progress

* Black

* Update basicswap/static/js/modules/coin-manager.js

Co-authored-by: nahuhh <50635951+nahuhh@users.noreply.github.com>

* Update basicswap/static/js/modules/coin-manager.js

Co-authored-by: nahuhh <50635951+nahuhh@users.noreply.github.com>

* Fixes + GUI: v3.2.1

* Fixes + AutoRefreshEnabled true as default.

* Fix small memory issue since new features added,

---------

Co-authored-by: nahuhh <50635951+nahuhh@users.noreply.github.com>
2025-04-10 21:18:03 +02:00

447 lines
12 KiB
JavaScript

const WebSocketManager = (function() {
let ws = null;
const config = {
reconnectAttempts: 0,
maxReconnectAttempts: 5,
reconnectDelay: 5000,
debug: false
};
const state = {
isConnecting: false,
isIntentionallyClosed: false,
lastConnectAttempt: null,
connectTimeout: null,
lastHealthCheck: null,
healthCheckInterval: null,
isPageHidden: document.hidden,
messageHandlers: {},
listeners: {},
reconnectTimeout: null
};
function log(message, ...args) {
if (config.debug) {
console.log(`[WebSocketManager] ${message}`, ...args);
}
}
function generateHandlerId() {
return `handler_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
}
function determineWebSocketPort() {
let wsPort;
if (window.config && window.config.wsPort) {
wsPort = window.config.wsPort;
return wsPort;
}
if (window.ws_port) {
wsPort = window.ws_port.toString();
return wsPort;
}
if (typeof getWebSocketConfig === 'function') {
const wsConfig = getWebSocketConfig();
wsPort = (wsConfig.port || wsConfig.fallbackPort || '11700').toString();
return wsPort;
}
wsPort = '11700';
return wsPort;
}
const publicAPI = {
initialize: function(options = {}) {
Object.assign(config, options);
setupPageVisibilityHandler();
this.connect();
startHealthCheck();
log('WebSocketManager initialized with options:', options);
if (window.CleanupManager) {
window.CleanupManager.registerResource('webSocketManager', this, (mgr) => mgr.dispose());
}
return this;
},
connect: function() {
if (state.isConnecting || state.isIntentionallyClosed) {
log('Connection attempt blocked - already connecting or intentionally closed');
return false;
}
if (state.reconnectTimeout) {
clearTimeout(state.reconnectTimeout);
state.reconnectTimeout = null;
}
cleanup();
state.isConnecting = true;
state.lastConnectAttempt = Date.now();
try {
const wsPort = determineWebSocketPort();
if (!wsPort) {
state.isConnecting = false;
return false;
}
ws = new WebSocket(`ws://${window.location.hostname}:${wsPort}`);
setupEventHandlers();
state.connectTimeout = setTimeout(() => {
if (state.isConnecting) {
log('Connection timeout, cleaning up');
cleanup();
handleReconnect();
}
}, 5000);
return true;
} catch (error) {
log('Error during connection attempt:', error);
state.isConnecting = false;
handleReconnect();
return false;
}
},
disconnect: function() {
log('Disconnecting WebSocket');
state.isIntentionallyClosed = true;
cleanup();
stopHealthCheck();
},
isConnected: function() {
return ws && ws.readyState === WebSocket.OPEN;
},
sendMessage: function(message) {
if (!this.isConnected()) {
log('Cannot send message - not connected');
return false;
}
try {
ws.send(JSON.stringify(message));
return true;
} catch (error) {
log('Error sending message:', error);
return false;
}
},
addMessageHandler: function(type, handler) {
if (!state.messageHandlers[type]) {
state.messageHandlers[type] = {};
}
const handlerId = generateHandlerId();
state.messageHandlers[type][handlerId] = handler;
return handlerId;
},
removeMessageHandler: function(type, handlerId) {
if (state.messageHandlers[type] && state.messageHandlers[type][handlerId]) {
delete state.messageHandlers[type][handlerId];
}
},
cleanup: function() {
log('Cleaning up WebSocket resources');
clearTimeout(state.connectTimeout);
stopHealthCheck();
if (state.reconnectTimeout) {
clearTimeout(state.reconnectTimeout);
state.reconnectTimeout = null;
}
state.isConnecting = false;
state.messageHandlers = {};
if (ws) {
ws.onopen = null;
ws.onmessage = null;
ws.onerror = null;
ws.onclose = null;
if (ws.readyState === WebSocket.OPEN) {
ws.close(1000, 'Cleanup');
}
ws = null;
window.ws = null;
}
},
dispose: function() {
log('Disposing WebSocketManager');
this.disconnect();
if (state.listeners.visibilityChange) {
document.removeEventListener('visibilitychange', state.listeners.visibilityChange);
}
state.messageHandlers = {};
state.listeners = {};
},
pause: function() {
log('WebSocketManager paused');
state.isIntentionallyClosed = true;
if (ws && ws.readyState === WebSocket.OPEN) {
ws.close(1000, 'WebSocketManager paused');
}
stopHealthCheck();
},
resume: function() {
log('WebSocketManager resumed');
state.isIntentionallyClosed = false;
if (!this.isConnected()) {
this.connect();
}
startHealthCheck();
}
};
function setupEventHandlers() {
if (!ws) return;
ws.onopen = () => {
state.isConnecting = false;
config.reconnectAttempts = 0;
clearTimeout(state.connectTimeout);
state.lastHealthCheck = Date.now();
window.ws = ws;
log('WebSocket connection established');
notifyHandlers('connect', { isConnected: true });
if (typeof updateConnectionStatus === 'function') {
updateConnectionStatus('connected');
}
};
ws.onmessage = (event) => {
try {
const message = JSON.parse(event.data);
log('WebSocket message received:', message);
notifyHandlers('message', message);
} catch (error) {
log('Error processing message:', error);
if (typeof updateConnectionStatus === 'function') {
updateConnectionStatus('error');
}
}
};
ws.onerror = (error) => {
log('WebSocket error:', error);
if (typeof updateConnectionStatus === 'function') {
updateConnectionStatus('error');
}
notifyHandlers('error', error);
};
ws.onclose = (event) => {
log('WebSocket closed:', event);
state.isConnecting = false;
window.ws = null;
if (typeof updateConnectionStatus === 'function') {
updateConnectionStatus('disconnected');
}
notifyHandlers('disconnect', {
code: event.code,
reason: event.reason
});
if (!state.isIntentionallyClosed) {
handleReconnect();
}
};
}
function setupPageVisibilityHandler() {
const visibilityChangeHandler = () => {
if (document.hidden) {
handlePageHidden();
} else {
handlePageVisible();
}
};
document.addEventListener('visibilitychange', visibilityChangeHandler);
state.listeners.visibilityChange = visibilityChangeHandler;
}
function handlePageHidden() {
log('Page hidden');
state.isPageHidden = true;
stopHealthCheck();
if (ws && ws.readyState === WebSocket.OPEN) {
state.isIntentionallyClosed = true;
ws.close(1000, 'Page hidden');
}
}
function handlePageVisible() {
log('Page visible');
state.isPageHidden = false;
state.isIntentionallyClosed = false;
setTimeout(() => {
if (!publicAPI.isConnected()) {
publicAPI.connect();
}
startHealthCheck();
}, 0);
}
function startHealthCheck() {
stopHealthCheck();
state.healthCheckInterval = setInterval(() => {
performHealthCheck();
}, 30000);
}
function stopHealthCheck() {
if (state.healthCheckInterval) {
clearInterval(state.healthCheckInterval);
state.healthCheckInterval = null;
}
}
function performHealthCheck() {
if (!publicAPI.isConnected()) {
log('Health check failed - not connected');
handleReconnect();
return;
}
const now = Date.now();
const lastCheck = state.lastHealthCheck;
if (lastCheck && (now - lastCheck) > 60000) {
log('Health check failed - too long since last check');
handleReconnect();
return;
}
state.lastHealthCheck = now;
log('Health check passed');
}
function handleReconnect() {
if (state.reconnectTimeout) {
clearTimeout(state.reconnectTimeout);
state.reconnectTimeout = null;
}
config.reconnectAttempts++;
if (config.reconnectAttempts <= config.maxReconnectAttempts) {
const delay = Math.min(
config.reconnectDelay * Math.pow(1.5, config.reconnectAttempts - 1),
30000
);
log(`Scheduling reconnect in ${delay}ms (attempt ${config.reconnectAttempts})`);
state.reconnectTimeout = setTimeout(() => {
state.reconnectTimeout = null;
if (!state.isIntentionallyClosed) {
publicAPI.connect();
}
}, delay);
} else {
log('Max reconnect attempts reached');
if (typeof updateConnectionStatus === 'function') {
updateConnectionStatus('error');
}
state.reconnectTimeout = setTimeout(() => {
state.reconnectTimeout = null;
config.reconnectAttempts = 0;
publicAPI.connect();
}, 60000);
}
}
function notifyHandlers(type, data) {
if (state.messageHandlers[type]) {
Object.values(state.messageHandlers[type]).forEach(handler => {
try {
handler(data);
} catch (error) {
log(`Error in ${type} handler:`, error);
}
});
}
}
function cleanup() {
log('Cleaning up WebSocket resources');
clearTimeout(state.connectTimeout);
stopHealthCheck();
if (state.reconnectTimeout) {
clearTimeout(state.reconnectTimeout);
state.reconnectTimeout = null;
}
state.isConnecting = false;
if (ws) {
ws.onopen = null;
ws.onmessage = null;
ws.onerror = null;
ws.onclose = null;
if (ws.readyState === WebSocket.OPEN) {
ws.close(1000, 'Cleanup');
}
ws = null;
window.ws = null;
}
}
return publicAPI;
})();
window.WebSocketManager = WebSocketManager;
document.addEventListener('DOMContentLoaded', function() {
if (!window.webSocketManagerInitialized) {
window.WebSocketManager.initialize();
window.webSocketManagerInitialized = true;
}
});
//console.log('WebSocketManager initialized with methods:', Object.keys(WebSocketManager));
console.log('WebSocketManager initialized');