diff --git a/basicswap/static/js/active.js b/basicswap/static/js/active.js index d6d1767..5c4b82f 100644 --- a/basicswap/static/js/active.js +++ b/basicswap/static/js/active.js @@ -266,18 +266,33 @@ const WebSocketManager = { }, connect() { - if (this.ws?.readyState === WebSocket.OPEN) return; + if (this.ws?.readyState === WebSocket.OPEN) return; - try { - const wsPort = window.ws_port || '11700'; - this.ws = new WebSocket(`ws://${window.location.hostname}:${wsPort}`); - this.setupEventHandlers(); - } catch (error) { - console.error('WebSocket connection error:', error); - this.handleReconnect(); + try { + + let wsPort; + + if (typeof getWebSocketConfig === 'function') { + const wsConfig = getWebSocketConfig(); + wsPort = wsConfig?.port || wsConfig?.fallbackPort; } - }, + if (!wsPort && window.config?.port) { + wsPort = window.config.port; + } + + if (!wsPort) { + wsPort = window.ws_port || '11700'; + } + + console.log("Using WebSocket port:", wsPort); + this.ws = new WebSocket(`ws://${window.location.hostname}:${wsPort}`); + this.setupEventHandlers(); + } catch (error) { + console.error('WebSocket connection error:', error); + this.handleReconnect(); + } +}, setupEventHandlers() { this.ws.onopen = () => { state.wsConnected = true; diff --git a/basicswap/static/js/bids_available.js b/basicswap/static/js/bids_available.js index a183fd2..7e31cca 100644 --- a/basicswap/static/js/bids_available.js +++ b/basicswap/static/js/bids_available.js @@ -357,17 +357,29 @@ const WebSocketManager = { }, connect() { - if (this.ws?.readyState === WebSocket.OPEN) return; + if (this.ws?.readyState === WebSocket.OPEN) return; - try { - const wsPort = window.ws_port || '11700'; - this.ws = new WebSocket(`ws://${window.location.hostname}:${wsPort}`); - this.setupEventHandlers(); - } catch (error) { - console.error('WebSocket connection error:', error); - this.handleReconnect(); + try { + let wsPort; + + if (typeof getWebSocketConfig === 'function') { + const wsConfig = getWebSocketConfig(); + wsPort = wsConfig?.port || wsConfig?.fallbackPort; } - }, + if (!wsPort && window.config?.port) { + wsPort = window.config.port; + } + if (!wsPort) { + wsPort = window.ws_port || '11700'; + } + console.log("Using WebSocket port:", wsPort); + this.ws = new WebSocket(`ws://${window.location.hostname}:${wsPort}`); + this.setupEventHandlers(); + } catch (error) { + console.error('WebSocket connection error:', error); + this.handleReconnect(); + } +}, setupEventHandlers() { this.ws.onopen = () => { diff --git a/basicswap/static/js/bids_sentreceived.js b/basicswap/static/js/bids_sentreceived.js index c3321a9..2ea8d3b 100644 --- a/basicswap/static/js/bids_sentreceived.js +++ b/basicswap/static/js/bids_sentreceived.js @@ -249,21 +249,37 @@ const WebSocketManager = { }, connect() { - if (this.isConnected() || this.isPaused) return; + if (this.isConnected() || this.isPaused) return; - if (this.ws) { - this.cleanupConnection(); + if (this.ws) { + this.cleanupConnection(); + } + + try { + + let wsPort; + + if (typeof getWebSocketConfig === 'function') { + const wsConfig = getWebSocketConfig(); + wsPort = wsConfig?.port || wsConfig?.fallbackPort; } - try { - const wsPort = window.ws_port || '11700'; - this.ws = new WebSocket(`ws://${window.location.hostname}:${wsPort}`); - this.setupEventHandlers(); - } catch (error) { - console.error('WebSocket connection error:', error); - this.handleReconnect(); + if (!wsPort && window.config?.port) { + wsPort = window.config.port; } - }, + + if (!wsPort) { + wsPort = window.ws_port || '11700'; + } + + console.log("Using WebSocket port:", wsPort); + this.ws = new WebSocket(`ws://${window.location.hostname}:${wsPort}`); + this.setupEventHandlers(); + } catch (error) { + console.error('WebSocket connection error:', error); + this.handleReconnect(); + } +}, setupEventHandlers() { if (!this.ws) return; @@ -917,7 +933,6 @@ const forceTooltipDOMCleanup = () => { foundCount += allTooltipElements.length; allTooltipElements.forEach(element => { - const isDetached = !document.body.contains(element) || element.classList.contains('hidden') || element.style.display === 'none'; @@ -947,7 +962,6 @@ const forceTooltipDOMCleanup = () => { const tippyRoots = document.querySelectorAll('[data-tippy-root]'); foundCount += tippyRoots.length; - tippyRoots.forEach(element => { const isOrphan = !element.children.length || element.children[0].classList.contains('hidden') || @@ -975,13 +989,10 @@ const forceTooltipDOMCleanup = () => { } } }); - - // Handle legacy tooltip elements document.querySelectorAll('.tooltip').forEach(element => { const isTrulyDetached = !element.parentElement || !document.body.contains(element.parentElement) || element.classList.contains('hidden'); - if (isTrulyDetached) { try { element.remove(); @@ -992,14 +1003,11 @@ const forceTooltipDOMCleanup = () => { } }); - if (window.TooltipManager && window.TooltipManager.activeTooltips) { - window.TooltipManager.activeTooltips.forEach((instance, id) => { - const tooltipElement = document.getElementById(id.split('tooltip-trigger-')[1]); - const triggerElement = document.querySelector(`[data-tooltip-trigger-id="${id}"]`); - - if (!tooltipElement || !triggerElement || - !document.body.contains(tooltipElement) || - !document.body.contains(triggerElement)) { + if (window.TooltipManager && typeof window.TooltipManager.getActiveTooltipInstances === 'function') { + const activeTooltips = window.TooltipManager.getActiveTooltipInstances(); + activeTooltips.forEach(([element, instance]) => { + const tooltipId = element.getAttribute('data-tooltip-trigger-id'); + if (!document.body.contains(element)) { if (instance?.[0]) { try { instance[0].destroy(); @@ -1007,14 +1015,13 @@ const forceTooltipDOMCleanup = () => { console.warn('Error destroying tooltip instance:', e); } } - window.TooltipManager.activeTooltips.delete(id); } }); } if (removedCount > 0) { // console.log(`Tooltip cleanup: found ${foundCount}, removed ${removedCount} detached tooltips`); } -}; +} const createTableRow = async (bid) => { const identity = await IdentityManager.getIdentityData(bid.addr_from); diff --git a/basicswap/static/js/offers.js b/basicswap/static/js/offers.js index 8a472e4..83b5864 100644 --- a/basicswap/static/js/offers.js +++ b/basicswap/static/js/offers.js @@ -241,40 +241,47 @@ const WebSocketManager = { }, connect() { - if (this.connectionState.isConnecting || this.isIntentionallyClosed) { - return false; + if (this.connectionState.isConnecting || this.isIntentionallyClosed) { + return false; + } + + this.cleanup(); + this.connectionState.isConnecting = true; + this.connectionState.lastConnectAttempt = Date.now(); + + try { + let wsPort; + + if (typeof getWebSocketConfig === 'function') { + const wsConfig = getWebSocketConfig(); + wsPort = wsConfig.port || wsConfig.fallbackPort; + console.log("Using WebSocket port:", wsPort); + } else { + wsPort = config?.port || window.ws_port || '11700'; } - this.cleanup(); - this.connectionState.isConnecting = true; - this.connectionState.lastConnectAttempt = Date.now(); - - try { - const wsPort = config.port || window.ws_port || '11700'; - - if (!wsPort) { - this.connectionState.isConnecting = false; - return false; - } - - this.ws = new WebSocket(`ws://${window.location.hostname}:${wsPort}`); - this.setupEventHandlers(); - - this.connectionState.connectTimeout = setTimeout(() => { - if (this.connectionState.isConnecting) { - this.cleanup(); - this.handleReconnect(); - } - }, 5000); - - return true; - } catch (error) { + if (!wsPort) { this.connectionState.isConnecting = false; - this.handleReconnect(); return false; } - }, + this.ws = new WebSocket(`ws://${window.location.hostname}:${wsPort}`); + this.setupEventHandlers(); + + this.connectionState.connectTimeout = setTimeout(() => { + if (this.connectionState.isConnecting) { + this.cleanup(); + this.handleReconnect(); + } + }, 5000); + + return true; + } catch (error) { + this.connectionState.isConnecting = false; + this.handleReconnect(); + return false; + } +}, setupEventHandlers() { if (!this.ws) return; diff --git a/basicswap/static/js/pricechart.js b/basicswap/static/js/pricechart.js index ce4c4ba..010ab5e 100644 --- a/basicswap/static/js/pricechart.js +++ b/basicswap/static/js/pricechart.js @@ -1,3 +1,128 @@ +// CLEANUP +const cleanupManager = { + eventListeners: [], + timeouts: [], + intervals: [], + animationFrames: [], + + addListener: function(element, type, handler, options) { + if (!element) return null; + element.addEventListener(type, handler, options); + this.eventListeners.push({ element, type, handler, options }); + return handler; + }, + + setTimeout: function(callback, delay) { + const id = setTimeout(callback, delay); + this.timeouts.push(id); + return id; + }, + + setInterval: function(callback, delay) { + const id = setInterval(callback, delay); + this.intervals.push(id); + return id; + }, + + requestAnimationFrame: function(callback) { + const id = requestAnimationFrame(callback); + this.animationFrames.push(id); + return id; + }, + + clearAll: function() { + this.eventListeners.forEach(({ element, type, handler, options }) => { + if (element) { + try { + element.removeEventListener(type, handler, options); + } catch (e) { + console.warn('Error removing event listener:', e); + } + } + }); + this.eventListeners = []; + + this.timeouts.forEach(id => clearTimeout(id)); + this.timeouts = []; + + this.intervals.forEach(id => clearInterval(id)); + this.intervals = []; + + this.animationFrames.forEach(id => cancelAnimationFrame(id)); + this.animationFrames = []; + + console.log('All resources cleaned up'); + }, + + clearTimeouts: function() { + this.timeouts.forEach(id => clearTimeout(id)); + this.timeouts = []; + }, + + clearIntervals: function() { + this.intervals.forEach(id => clearInterval(id)); + this.intervals = []; + }, + + removeListenersByElement: function(element) { + if (!element) return; + + const listenersToRemove = this.eventListeners.filter( + listener => listener.element === element + ); + + listenersToRemove.forEach(({ element, type, handler, options }) => { + try { + element.removeEventListener(type, handler, options); + } catch (e) { + console.warn('Error removing event listener:', e); + } + }); + + this.eventListeners = this.eventListeners.filter( + listener => listener.element !== element + ); + } +}; + +// MEMORY +const memoryMonitor = { + isEnabled: true, + lastLogTime: 0, + logInterval: 5 * 60 * 1000, + monitorInterval: null, + + startMonitoring: function() { + console.log('Starting memory monitoring'); + if (!this.isEnabled) return; + + if (this.monitorInterval) { + clearInterval(this.monitorInterval); + } + + this.monitorInterval = setInterval(() => { + this.logMemoryUsage(); + }, this.logInterval); + + this.logMemoryUsage(); + }, + + logMemoryUsage: function() { + console.log('Logging memory usage'); + if (window.performance && window.performance.memory) { + const memory = window.performance.memory; + console.log(`Memory Usage: ${Math.round(memory.usedJSHeapSize / (1024 * 1024))}MB / ${Math.round(memory.jsHeapSizeLimit / (1024 * 1024))}MB`); + } + }, + + stopMonitoring: function() { + if (this.monitorInterval) { + clearInterval(this.monitorInterval); + this.monitorInterval = null; + } + } +}; + // CONFIG const config = { apiKeys: getAPIKeys(), @@ -393,49 +518,138 @@ const rateLimiter = { // CACHE const cache = { - set: (key, value, customTtl = null) => { + maxSizeBytes: 10 * 1024 * 1024, + maxItems: 200, + cacheTTL: 5 * 60 * 1000, + + set: function(key, value, customTtl = null) { + this.cleanup(); + const item = { value: value, timestamp: Date.now(), - expiresAt: Date.now() + (customTtl || app.cacheTTL) + expiresAt: Date.now() + (customTtl || this.cacheTTL) }; - localStorage.setItem(key, JSON.stringify(item)); - //console.log(`Cache set for ${key}, expires in ${(customTtl || app.cacheTTL) / 1000} seconds`); + + try { + const serialized = JSON.stringify(item); + localStorage.setItem(key, serialized); + } catch (e) { + console.warn('Cache set error:', e); + this.clear(); + try { + const serialized = JSON.stringify(item); + localStorage.setItem(key, serialized); + } catch (e2) { + console.error('Failed to store in cache even after cleanup:', e2); + } + } }, - get: (key) => { + + get: function(key) { const itemStr = localStorage.getItem(key); if (!itemStr) { return null; } + try { const item = JSON.parse(itemStr); const now = Date.now(); + if (now < item.expiresAt) { - //console.log(`Cache hit for ${key}, ${(item.expiresAt - now) / 1000} seconds remaining`); return { value: item.value, remainingTime: item.expiresAt - now }; } else { - //console.log(`Cache expired for ${key}`); localStorage.removeItem(key); } } catch (error) { - //console.error('Error parsing cache item:', error.message); + console.error('Error parsing cache item:', error.message); localStorage.removeItem(key); } + return null; }, - isValid: (key) => { - return cache.get(key) !== null; + + isValid: function(key) { + return this.get(key) !== null; }, - clear: () => { - Object.keys(localStorage).forEach(key => { + + clear: function() { + const keysToRemove = []; + + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i); if (key.startsWith('coinData_') || key.startsWith('chartData_') || key === 'coinGeckoOneLiner') { - localStorage.removeItem(key); + keysToRemove.push(key); } + } + + keysToRemove.forEach(key => { + localStorage.removeItem(key); }); - //console.log('Cache cleared'); + + console.log(`Cache cleared: removed ${keysToRemove.length} items`); + }, + + cleanup: function() { + let totalSize = 0; + const items = []; + const keysToRemove = []; + const now = Date.now(); + + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i); + if (key.startsWith('coinData_') || key.startsWith('chartData_') || key === 'coinGeckoOneLiner') { + try { + const value = localStorage.getItem(key); + const size = new Blob([value]).size; + + const item = JSON.parse(value); + + if (item.expiresAt && item.expiresAt < now) { + keysToRemove.push(key); + continue; + } + + totalSize += size; + items.push({ + key, + size, + timestamp: item.timestamp || 0, + expiresAt: item.expiresAt || 0 + }); + } catch (e) { + keysToRemove.push(key); + } + } + } + + keysToRemove.forEach(key => { + localStorage.removeItem(key); + }); + + if (totalSize > this.maxSizeBytes || items.length > this.maxItems) { + items.sort((a, b) => a.timestamp - b.timestamp); + + const itemsToRemove = Math.max( + Math.ceil(items.length * 0.2), + items.length - this.maxItems + ); + + items.slice(0, itemsToRemove).forEach(item => { + localStorage.removeItem(item.key); + }); + + console.log(`Cache cleanup: removed ${itemsToRemove} items, freed ${Math.round((totalSize - this.maxSizeBytes) / 1024)}KB`); + } + + return { + totalSize, + itemCount: items.length, + removedCount: keysToRemove.length + }; } }; @@ -631,6 +845,8 @@ const chartModule = { chart: null, currentCoin: 'BTC', loadStartTime: 0, + chartRefs: new WeakMap(), + verticalLinePlugin: { id: 'verticalLine', beforeDraw: (chart, args, options) => { @@ -652,15 +868,44 @@ const chartModule = { } }, - initChart: () => { - const ctx = document.getElementById('coin-chart')?.getContext('2d'); + getChartByElement: function(element) { + return this.chartRefs.get(element); + }, + + setChartReference: function(element, chart) { + this.chartRefs.set(element, chart); + }, + + destroyChart: function() { + if (chartModule.chart) { + try { + chartModule.chart.destroy(); + } catch (e) { + console.error('Error destroying chart:', e); + } + chartModule.chart = null; + } + }, + + initChart: function() { + this.destroyChart(); + + const canvas = document.getElementById('coin-chart'); + if (!canvas) { + logger.error('Chart canvas element not found'); + return; + } + + const ctx = canvas.getContext('2d'); if (!ctx) { logger.error('Failed to get chart context. Make sure the canvas element exists.'); return; } + const gradient = ctx.createLinearGradient(0, 0, 0, 400); gradient.addColorStop(0, 'rgba(77, 132, 240, 0.2)'); gradient.addColorStop(1, 'rgba(77, 132, 240, 0)'); + chartModule.chart = new Chart(ctx, { type: 'line', data: { @@ -811,11 +1056,15 @@ const chartModule = { }, plugins: [chartModule.verticalLinePlugin] }); + + this.setChartReference(canvas, chartModule.chart); }, - prepareChartData: (coinSymbol, data) => { + + prepareChartData: function(coinSymbol, data) { if (!data) { return []; } + try { let preparedData; @@ -825,9 +1074,11 @@ const chartModule = { const endUnix = endTime.getTime(); const startUnix = endUnix - (24 * 3600000); const hourlyPoints = []; + for (let hourUnix = startUnix; hourUnix <= endUnix; hourUnix += 3600000) { const targetHour = new Date(hourUnix); targetHour.setUTCMinutes(0, 0, 0); + const closestPoint = data.reduce((prev, curr) => { const prevTime = new Date(prev[0]); const currTime = new Date(curr[0]); @@ -868,6 +1119,7 @@ const chartModule = { y: price })); } else { + console.warn('Unknown data format for chartData:', data); return []; } return preparedData.map(point => ({ @@ -880,7 +1132,7 @@ const chartModule = { } }, - ensureHourlyData: (data) => { + ensureHourlyData: function(data) { const now = new Date(); now.setUTCMinutes(0, 0, 0); const twentyFourHoursAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000); @@ -888,136 +1140,176 @@ const chartModule = { for (let i = 0; i < 24; i++) { const targetTime = new Date(twentyFourHoursAgo.getTime() + i * 60 * 60 * 1000); - const closestDataPoint = data.reduce((prev, curr) => - Math.abs(new Date(curr.x).getTime() - targetTime.getTime()) < - Math.abs(new Date(prev.x).getTime() - targetTime.getTime()) ? curr : prev - ); - hourlyData.push({ - x: targetTime.getTime(), - y: closestDataPoint.y - }); + + if (data.length > 0) { + const closestDataPoint = data.reduce((prev, curr) => + Math.abs(new Date(curr.x).getTime() - targetTime.getTime()) < + Math.abs(new Date(prev.x).getTime() - targetTime.getTime()) ? curr : prev +); + hourlyData.push({ + x: targetTime.getTime(), + y: closestDataPoint.y + }); + } } + return hourlyData; }, - updateChart: async (coinSymbol, forceRefresh = false) => { + updateChart: async function(coinSymbol, forceRefresh = false) { + try { + if (!chartModule.chart) { + chartModule.initChart(); + } + const currentChartData = chartModule.chart?.data?.datasets[0]?.data || []; + if (currentChartData.length === 0) { + chartModule.showChartLoader(); + } + chartModule.loadStartTime = Date.now(); + const cacheKey = `chartData_${coinSymbol}_${config.currentResolution}`; + let cachedData = !forceRefresh ? cache.get(cacheKey) : null; + let data; + if (cachedData && Object.keys(cachedData.value).length > 0) { + data = cachedData.value; + } else { try { - const currentChartData = chartModule.chart?.data.datasets[0].data || []; - if (currentChartData.length === 0) { - chartModule.showChartLoader(); - } - chartModule.loadStartTime = Date.now(); - const cacheKey = `chartData_${coinSymbol}_${config.currentResolution}`; - let cachedData = !forceRefresh ? cache.get(cacheKey) : null; - let data; - if (cachedData && Object.keys(cachedData.value).length > 0) { - data = cachedData.value; - //console.log(`Using cached data for ${coinSymbol}`); - } else { - try { - const allData = await api.fetchHistoricalDataXHR([coinSymbol]); - data = allData[coinSymbol]; - if (!data || Object.keys(data).length === 0) { - throw new Error(`No data returned for ${coinSymbol}`); - } - cache.set(cacheKey, data, config.cacheTTL); - } catch (error) { - if (error.message.includes('429') && currentChartData.length > 0) { - //console.warn(`Rate limit hit for ${coinSymbol}, maintaining current chart`); - return; - } - const expiredCache = localStorage.getItem(cacheKey); - if (expiredCache) { - try { - const parsedCache = JSON.parse(expiredCache); - data = parsedCache.value; - //console.log(`Using expired cache data for ${coinSymbol}`); - } catch (cacheError) { - throw error; - } - } else { - throw error; - } - } - } - const chartData = chartModule.prepareChartData(coinSymbol, data); - if (chartData.length > 0 && chartModule.chart) { - chartModule.chart.data.datasets[0].data = chartData; - chartModule.chart.data.datasets[0].label = `${coinSymbol} Price (USD)`; - if (coinSymbol === 'WOW') { - chartModule.chart.options.scales.x.time.unit = 'hour'; - } else { - const resolution = config.resolutions[config.currentResolution]; - chartModule.chart.options.scales.x.time.unit = - resolution.interval === 'hourly' ? 'hour' : - config.currentResolution === 'year' ? 'month' : 'day'; - } - chartModule.chart.update('active'); - chartModule.currentCoin = coinSymbol; - const loadTime = Date.now() - chartModule.loadStartTime; - ui.updateLoadTimeAndCache(loadTime, cachedData); - } - } catch (error) { - console.error(`Error updating chart for ${coinSymbol}:`, error); - if (!(chartModule.chart?.data.datasets[0].data.length > 0)) { - chartModule.chart.data.datasets[0].data = []; - chartModule.chart.update('active'); - } - } finally { - chartModule.hideChartLoader(); - } - }, + const allData = await api.fetchHistoricalDataXHR([coinSymbol]); + data = allData[coinSymbol]; + + if (!data || Object.keys(data).length === 0) { + throw new Error(`No data returned for ${coinSymbol}`); + } - showChartLoader: () => { + cache.set(cacheKey, data, config.cacheTTL); + } catch (error) { + if (error.message.includes('429') && currentChartData.length > 0) { + console.warn(`Rate limit hit for ${coinSymbol}, maintaining current chart`); + chartModule.hideChartLoader(); + return; + } + const expiredCache = localStorage.getItem(cacheKey); + if (expiredCache) { + try { + const parsedCache = JSON.parse(expiredCache); + data = parsedCache.value; + } catch (cacheError) { + throw error; + } + } else { + throw error; + } + } + } + if (chartModule.currentCoin !== coinSymbol) { + chartModule.destroyChart(); + chartModule.initChart(); + } + + const chartData = chartModule.prepareChartData(coinSymbol, data); + if (chartData.length > 0 && chartModule.chart) { + chartModule.chart.data.datasets[0].data = chartData; + chartModule.chart.data.datasets[0].label = `${coinSymbol} Price (USD)`; + if (coinSymbol === 'WOW') { + chartModule.chart.options.scales.x.time.unit = 'hour'; + } else { + const resolution = config.resolutions[config.currentResolution]; + chartModule.chart.options.scales.x.time.unit = + resolution.interval === 'hourly' ? 'hour' : + config.currentResolution === 'year' ? 'month' : 'day'; + } + chartModule.chart.update('active'); + chartModule.currentCoin = coinSymbol; + const loadTime = Date.now() - chartModule.loadStartTime; + ui.updateLoadTimeAndCache(loadTime, cachedData); + } + } catch (error) { + console.error(`Error updating chart for ${coinSymbol}:`, error); + + // Keep existing chart data if possible /todo + if (!(chartModule.chart?.data?.datasets[0]?.data?.length > 0)) { + if (!chartModule.chart) { + chartModule.initChart(); + } + if (chartModule.chart) { + chartModule.chart.data.datasets[0].data = []; + chartModule.chart.update('active'); + } + } + } finally { + chartModule.hideChartLoader(); + } + }, + + showChartLoader: function() { const loader = document.getElementById('chart-loader'); const chart = document.getElementById('coin-chart'); if (!loader || !chart) { - //console.warn('Chart loader or chart container elements not found'); return; } loader.classList.remove('hidden'); chart.classList.add('hidden'); }, - hideChartLoader: () => { + hideChartLoader: function() { const loader = document.getElementById('chart-loader'); const chart = document.getElementById('coin-chart'); if (!loader || !chart) { - //console.warn('Chart loader or chart container elements not found'); return; } loader.classList.add('hidden'); chart.classList.remove('hidden'); }, + cleanup: function() { + this.destroyChart(); + this.currentCoin = null; + this.loadStartTime = 0; + console.log('Chart module cleaned up'); + } }; Chart.register(chartModule.verticalLinePlugin); - const volumeToggle = { - isVisible: localStorage.getItem('volumeToggleState') === 'true', - init: () => { - const toggleButton = document.getElementById('toggle-volume'); - if (toggleButton) { +const volumeToggle = { + isVisible: localStorage.getItem('volumeToggleState') === 'true', + init: function() { + const toggleButton = document.getElementById('toggle-volume'); + if (toggleButton) { + if (typeof cleanupManager !== 'undefined') { + cleanupManager.addListener(toggleButton, 'click', volumeToggle.toggle); + } else { toggleButton.addEventListener('click', volumeToggle.toggle); - volumeToggle.updateVolumeDisplay(); } - }, - toggle: () => { - volumeToggle.isVisible = !volumeToggle.isVisible; - localStorage.setItem('volumeToggleState', volumeToggle.isVisible.toString()); volumeToggle.updateVolumeDisplay(); - }, - updateVolumeDisplay: () => { - const volumeDivs = document.querySelectorAll('[id$="-volume-div"]'); - volumeDivs.forEach(div => { - div.style.display = volumeToggle.isVisible ? 'flex' : 'none'; - }); - const toggleButton = document.getElementById('toggle-volume'); - if (toggleButton) { - updateButtonStyles(toggleButton, volumeToggle.isVisible, 'green'); - } } - }; + }, + + toggle: function() { + volumeToggle.isVisible = !volumeToggle.isVisible; + localStorage.setItem('volumeToggleState', volumeToggle.isVisible.toString()); + volumeToggle.updateVolumeDisplay(); + }, + + updateVolumeDisplay: function() { + const volumeDivs = document.querySelectorAll('[id$="-volume-div"]'); + volumeDivs.forEach(div => { + if (div) { + div.style.display = volumeToggle.isVisible ? 'flex' : 'none'; + } + }); + + const toggleButton = document.getElementById('toggle-volume'); + if (toggleButton) { + updateButtonStyles(toggleButton, volumeToggle.isVisible, 'green'); + } + }, + + cleanup: function() { + const toggleButton = document.getElementById('toggle-volume'); + if (toggleButton) { + toggleButton.removeEventListener('click', volumeToggle.toggle); + } + } +}; function updateButtonStyles(button, isActive, color) { button.classList.toggle('text-' + color + '-500', isActive); @@ -1042,7 +1334,7 @@ const app = { minimumRefreshInterval: 60 * 1000, // 1 min init: () => { - //console.log('Init'); + console.log('Init'); window.addEventListener('load', app.onLoad); app.loadLastRefreshedTime(); app.updateAutoRefreshButton(); @@ -1689,5 +1981,48 @@ resolutionButtons.forEach(button => { }); }); +// LOAD +const appCleanup = { + init: function() { + memoryMonitor.startMonitoring(); + window.addEventListener('beforeunload', this.globalCleanup); + }, + + globalCleanup: function() { + try { + if (app.autoRefreshInterval) { + clearTimeout(app.autoRefreshInterval); + } + if (chartModule) { + chartModule.cleanup(); + } + if (volumeToggle) { + volumeToggle.cleanup(); + } + cleanupManager.clearAll(); + memoryMonitor.stopMonitoring(); + cache.clear(); + + console.log('Global application cleanup completed'); + } catch (error) { + console.error('Error during global cleanup:', error); + } + }, + manualCleanup: function() { + this.globalCleanup(); + window.location.reload(); + } +}; + +app.init = () => { + //console.log('Init'); + window.addEventListener('load', app.onLoad); + appCleanup.init(); + app.loadLastRefreshedTime(); + app.updateAutoRefreshButton(); + memoryMonitor.startMonitoring(); + //console.log('App initialized'); +}; + // LOAD app.init(); diff --git a/basicswap/static/js/tooltips.js b/basicswap/static/js/tooltips.js index 3a7b8e6..9249c56 100644 --- a/basicswap/static/js/tooltips.js +++ b/basicswap/static/js/tooltips.js @@ -1,9 +1,11 @@ class TooltipManager { constructor() { - this.activeTooltips = new Map(); - this.sizeCheckIntervals = new Map(); + this.activeTooltips = new WeakMap(); + this.sizeCheckIntervals = new WeakMap(); + this.tooltipIdCounter = 0; this.setupStyles(); this.setupCleanupEvents(); + this.initializeMutationObserver(); } static initialize() { @@ -19,16 +21,26 @@ class TooltipManager { this.destroy(element); const checkSize = () => { + if (!document.body.contains(element)) { + return; + } + const rect = element.getBoundingClientRect(); if (rect.width && rect.height) { - clearInterval(this.sizeCheckIntervals.get(element)); - this.sizeCheckIntervals.delete(element); + delete element._tooltipRetryCount; this.createTooltip(element, content, options, rect); + } else { + const retryCount = element._tooltipRetryCount || 0; + if (retryCount < 5) { + element._tooltipRetryCount = retryCount + 1; + requestAnimationFrame(checkSize); + } else { + delete element._tooltipRetryCount; + } } }; - this.sizeCheckIntervals.set(element, setInterval(checkSize, 50)); - checkSize(); + requestAnimationFrame(checkSize); return null; } @@ -62,6 +74,8 @@ class TooltipManager { } } + const tooltipId = `tooltip-${++this.tooltipIdCounter}`; + const instance = tippy(element, { content, allowHTML: true, @@ -75,6 +89,28 @@ class TooltipManager { theme: '', moveTransition: 'none', offset: [0, 10], + onShow(instance) { + if (!document.body.contains(element)) { + return false; + } + + const rect = element.getBoundingClientRect(); + if (!rect.width || !rect.height) { + return false; + } + + return true; + }, + onMount(instance) { + if (instance.popper.firstElementChild) { + instance.popper.firstElementChild.classList.add(bgClass); + instance.popper.setAttribute('data-for-tooltip-id', tooltipId); + } + const arrow = instance.popper.querySelector('.tippy-arrow'); + if (arrow) { + arrow.style.setProperty('color', arrowColor, 'important'); + } + }, popperOptions: { strategy: 'fixed', modifiers: [ @@ -93,45 +129,11 @@ class TooltipManager { } } ] - }, - onCreate(instance) { - instance._originalPlacement = instance.props.placement; - }, - onShow(instance) { - if (!document.body.contains(element)) { - return false; - } - - const rect = element.getBoundingClientRect(); - if (!rect.width || !rect.height) { - return false; - } - - instance.setProps({ - placement: instance._originalPlacement - }); - - if (instance.popper.firstElementChild) { - instance.popper.firstElementChild.classList.add(bgClass); - } - - return true; - }, - onMount(instance) { - if (instance.popper.firstElementChild) { - instance.popper.firstElementChild.classList.add(bgClass); - } - const arrow = instance.popper.querySelector('.tippy-arrow'); - if (arrow) { - arrow.style.setProperty('color', arrowColor, 'important'); - } } }); - const id = element.getAttribute('data-tooltip-trigger-id') || - `tooltip-${Math.random().toString(36).substring(7)}`; - element.setAttribute('data-tooltip-trigger-id', id); - this.activeTooltips.set(id, instance); + element.setAttribute('data-tooltip-trigger-id', tooltipId); + this.activeTooltips.set(element, instance); return instance; } @@ -139,40 +141,33 @@ class TooltipManager { destroy(element) { if (!element) return; - if (this.sizeCheckIntervals.has(element)) { - clearInterval(this.sizeCheckIntervals.get(element)); - this.sizeCheckIntervals.delete(element); - } - + delete element._tooltipRetryCount; + const id = element.getAttribute('data-tooltip-trigger-id'); if (!id) return; - const instance = this.activeTooltips.get(id); + const instance = this.activeTooltips.get(element); if (instance?.[0]) { try { instance[0].destroy(); } catch (e) { console.warn('Error destroying tooltip:', e); + + const tippyRoot = document.querySelector(`[data-for-tooltip-id="${id}"]`); + if (tippyRoot && tippyRoot.parentNode) { + tippyRoot.parentNode.removeChild(tippyRoot); + } } } - this.activeTooltips.delete(id); + + this.activeTooltips.delete(element); element.removeAttribute('data-tooltip-trigger-id'); } cleanup() { - this.sizeCheckIntervals.forEach((interval) => clearInterval(interval)); - this.sizeCheckIntervals.clear(); - - this.activeTooltips.forEach((instance, id) => { - if (instance?.[0]) { - try { - instance[0].destroy(); - } catch (e) { - console.warn('Error cleaning up tooltip:', e); - } - } + document.querySelectorAll('[data-tooltip-trigger-id]').forEach(element => { + this.destroy(element); }); - this.activeTooltips.clear(); document.querySelectorAll('[data-tippy-root]').forEach(element => { if (element.parentNode) { @@ -181,6 +176,63 @@ class TooltipManager { }); } + getActiveTooltipInstances() { + const result = []; + + document.querySelectorAll('[data-tooltip-trigger-id]').forEach(element => { + const instance = this.activeTooltips.get(element); + if (instance) { + result.push([element, instance]); + } + }); + + return result; + } + + initializeMutationObserver() { + if (this.mutationObserver) return; + + this.mutationObserver = new MutationObserver(mutations => { + let needsCleanup = false; + + mutations.forEach(mutation => { + if (mutation.removedNodes.length) { + Array.from(mutation.removedNodes).forEach(node => { + if (node.nodeType === 1) { + if (node.hasAttribute && node.hasAttribute('data-tooltip-trigger-id')) { + this.destroy(node); + needsCleanup = true; + } + + if (node.querySelectorAll) { + node.querySelectorAll('[data-tooltip-trigger-id]').forEach(el => { + this.destroy(el); + needsCleanup = true; + }); + } + } + }); + } + }); + + if (needsCleanup) { + document.querySelectorAll('[data-tippy-root]').forEach(element => { + const id = element.getAttribute('data-for-tooltip-id'); + if (id && !document.querySelector(`[data-tooltip-trigger-id="${id}"]`)) { + if (element.parentNode) { + element.parentNode.removeChild(element); + } + } + }); + } + }); + + this.mutationObserver.observe(document.body, { + childList: true, + subtree: true + }); + } + setupStyles() { if (document.getElementById('tooltip-styles')) return; @@ -274,13 +326,22 @@ class TooltipManager { } setupCleanupEvents() { - window.addEventListener('beforeunload', () => this.cleanup()); - window.addEventListener('unload', () => this.cleanup()); - document.addEventListener('visibilitychange', () => { + this.boundCleanup = this.cleanup.bind(this); + this.handleVisibilityChange = () => { if (document.hidden) { this.cleanup(); } - }); + }; + + window.addEventListener('beforeunload', this.boundCleanup); + window.addEventListener('unload', this.boundCleanup); + document.addEventListener('visibilitychange', this.handleVisibilityChange); + } + + removeCleanupEvents() { + window.removeEventListener('beforeunload', this.boundCleanup); + window.removeEventListener('unload', this.boundCleanup); + document.removeEventListener('visibilitychange', this.handleVisibilityChange); } initializeTooltips(selector = '[data-tooltip-target]') { @@ -295,6 +356,26 @@ class TooltipManager { } }); } + + dispose() { + this.cleanup(); + + if (this.mutationObserver) { + this.mutationObserver.disconnect(); + this.mutationObserver = null; + } + + this.removeCleanupEvents(); + + const styleElement = document.getElementById('tooltip-styles'); + if (styleElement && styleElement.parentNode) { + styleElement.parentNode.removeChild(styleElement); + } + + if (window.TooltipManager === this) { + window.TooltipManager = null; + } + } } if (typeof module !== 'undefined' && module.exports) { diff --git a/basicswap/templates/header.html b/basicswap/templates/header.html index df0a461..a8ed7ae 100644 --- a/basicswap/templates/header.html +++ b/basicswap/templates/header.html @@ -120,6 +120,23 @@ updateShutdownButtons(); }); + + + diff --git a/basicswap/templates/offers.html b/basicswap/templates/offers.html index b4afc97..80b00c9 100644 --- a/basicswap/templates/offers.html +++ b/basicswap/templates/offers.html @@ -8,22 +8,6 @@ }; - -