diff --git a/basicswap/static/js/modules/api-manager.js b/basicswap/static/js/modules/api-manager.js index e38440c..bac3f22 100644 --- a/basicswap/static/js/modules/api-manager.js +++ b/basicswap/static/js/modules/api-manager.js @@ -260,30 +260,80 @@ const ApiManager = (function() { fetchVolumeData: async function() { return this.rateLimiter.queueRequest('coingecko', async () => { try { - const coins = (window.config && window.config.coins) ? + let coinList = (window.config && window.config.coins) ? window.config.coins .filter(coin => coin.usesCoinGecko) - .map(coin => getCoinBackendId ? getCoinBackendId(coin.name) : coin.name) + .map(coin => { + return window.config.getCoinBackendId ? + window.config.getCoinBackendId(coin.name) : + (typeof getCoinBackendId === 'function' ? + getCoinBackendId(coin.name) : coin.name.toLowerCase()); + }) .join(',') : - 'bitcoin,monero,particl,bitcoin-cash,pivx,firo,dash,litecoin,dogecoin,decred,namecoin'; + 'bitcoin,monero,particl,bitcoin-cash,pivx,firo,dash,litecoin,dogecoin,decred,namecoin,wownero'; - const url = `https://api.coingecko.com/api/v3/simple/price?ids=${coins}&vs_currencies=usd&include_24hr_vol=true&include_24hr_change=true`; + if (!coinList.includes('zcoin') && coinList.includes('firo')) { + coinList = coinList + ',zcoin'; + } + const url = `https://api.coingecko.com/api/v3/simple/price?ids=${coinList}&vs_currencies=usd&include_24hr_vol=true&include_24hr_change=true`; + const response = await this.makePostRequest(url, { 'User-Agent': 'Mozilla/5.0', 'Accept': 'application/json' }); + if (!response || typeof response !== 'object') { + throw new Error('Invalid response from CoinGecko API'); + } + const volumeData = {}; + Object.entries(response).forEach(([coinId, data]) => { - if (data && data.usd_24h_vol) { + if (data && data.usd_24h_vol !== undefined) { volumeData[coinId] = { - total_volume: data.usd_24h_vol, + total_volume: data.usd_24h_vol || 0, price_change_percentage_24h: data.usd_24h_change || 0 }; } }); + const coinMappings = { + 'firo': ['firo', 'zcoin'], + 'zcoin': ['zcoin', 'firo'], + 'bitcoin-cash': ['bitcoin-cash', 'bitcoincash', 'bch'], + 'bitcoincash': ['bitcoincash', 'bitcoin-cash', 'bch'], + 'particl': ['particl', 'part'] + }; + + if (response['zcoin'] && (!volumeData['firo'] || volumeData['firo'].total_volume === 0)) { + volumeData['firo'] = { + total_volume: response['zcoin'].usd_24h_vol || 0, + price_change_percentage_24h: response['zcoin'].usd_24h_change || 0 + }; + } + + if (response['bitcoin-cash'] && (!volumeData['bitcoincash'] || volumeData['bitcoincash'].total_volume === 0)) { + volumeData['bitcoincash'] = { + total_volume: response['bitcoin-cash'].usd_24h_vol || 0, + price_change_percentage_24h: response['bitcoin-cash'].usd_24h_change || 0 + }; + } + + for (const [mainCoin, alternativeIds] of Object.entries(coinMappings)) { + if (!volumeData[mainCoin] || volumeData[mainCoin].total_volume === 0) { + for (const altId of alternativeIds) { + if (response[altId] && response[altId].usd_24h_vol) { + volumeData[mainCoin] = { + total_volume: response[altId].usd_24h_vol, + price_change_percentage_24h: response[altId].usd_24h_change || 0 + }; + break; + } + } + } + } + return volumeData; } catch (error) { console.error("Error fetching volume data:", error); @@ -364,7 +414,6 @@ const ApiManager = (function() { }, dispose: function() { - // Clear any pending requests or resources rateLimiter.requestQueue = {}; rateLimiter.lastRequestTime = {}; state.isInitialized = false; diff --git a/basicswap/static/js/modules/memory-manager.js b/basicswap/static/js/modules/memory-manager.js index fd5489f..8a96323 100644 --- a/basicswap/static/js/modules/memory-manager.js +++ b/basicswap/static/js/modules/memory-manager.js @@ -1,219 +1,663 @@ const MemoryManager = (function() { + const config = { + tooltipCleanupInterval: 60000, + maxTooltipsThreshold: 100, + diagnosticsInterval: 300000, + tooltipLifespan: 240000, + debug: false, + autoCleanup: true, + elementVerificationInterval: 50000, + tooltipSelectors: [ + '[data-tippy-root]', + '[data-tooltip-trigger-id]', + '.tooltip', + '.tippy-box', + '.tippy-content' + ] + }; - const state = { - isMonitoringEnabled: false, - monitorInterval: null, - cleanupInterval: null - }; + let mutationObserver = null; - const config = { - monitorInterval: 30000, - cleanupInterval: 60000, - debug: false - }; + const safeGet = (obj, path, defaultValue = null) => { + if (!obj) return defaultValue; + const pathParts = path.split('.'); + let result = obj; + for (const part of pathParts) { + if (result === null || result === undefined) return defaultValue; + result = result[part]; + } + return result !== undefined ? result : defaultValue; + }; - function log(message, ...args) { - if (config.debug) { - console.log(`[MemoryManager] ${message}`, ...args); + const state = { + intervals: new Map(), + trackedTooltips: new Map(), + trackedElements: new WeakMap(), + startTime: Date.now(), + lastCleanupTime: Date.now(), + metrics: { + tooltipsCreated: 0, + tooltipsDestroyed: 0, + orphanedTooltipsRemoved: 0, + elementsProcessed: 0, + cleanupRuns: 0, + manualCleanupRuns: 0, + lastMemoryUsage: null + } + }; + + const log = (message, ...args) => { + if (!config.debug) return; + const now = new Date().toISOString(); + console.log(`[MemoryManager ${now}]`, message, ...args); + }; + + const logError = (message, error) => { + console.error(`[MemoryManager] ${message}`, error); + }; + + const trackTooltip = (element, tooltipInstance) => { + try { + if (!element || !tooltipInstance) return; + + const timestamp = Date.now(); + const tooltipId = element.getAttribute('data-tooltip-trigger-id') || `tooltip_${timestamp}_${Math.random().toString(36).substring(2, 9)}`; + + state.trackedTooltips.set(tooltipId, { + timestamp, + element, + instance: tooltipInstance, + processed: false + }); + + state.metrics.tooltipsCreated++; + + setTimeout(() => { + if (state.trackedTooltips.has(tooltipId)) { + destroyTooltip(tooltipId); } + }, config.tooltipLifespan); + + return tooltipId; + } catch (error) { + logError('Error tracking tooltip:', error); + return null; + } + }; + + const destroyTooltip = (tooltipId) => { + try { + const tooltipInfo = state.trackedTooltips.get(tooltipId); + if (!tooltipInfo) return false; + + const { element, instance } = tooltipInfo; + + if (instance && typeof instance.destroy === 'function') { + instance.destroy(); + } + + if (element && element.removeAttribute) { + element.removeAttribute('data-tooltip-trigger-id'); + element.removeAttribute('aria-describedby'); + } + + const tippyRoot = document.querySelector(`[data-for-tooltip-id="${tooltipId}"]`); + if (tippyRoot && tippyRoot.parentNode) { + tippyRoot.parentNode.removeChild(tippyRoot); + } + + state.trackedTooltips.delete(tooltipId); + state.metrics.tooltipsDestroyed++; + + return true; + } catch (error) { + logError(`Error destroying tooltip ${tooltipId}:`, error); + return false; + } + }; + + const removeOrphanedTooltips = () => { + try { + const tippyRoots = document.querySelectorAll('[data-tippy-root]'); + let removed = 0; + + tippyRoots.forEach(root => { + const tooltipId = root.getAttribute('data-for-tooltip-id'); + + const trigger = tooltipId ? + document.querySelector(`[data-tooltip-trigger-id="${tooltipId}"]`) : + null; + + if (!trigger || !document.body.contains(trigger)) { + if (root.parentNode) { + root.parentNode.removeChild(root); + removed++; + } + } + }); + + document.querySelectorAll('[data-tooltip-trigger-id]').forEach(trigger => { + const tooltipId = trigger.getAttribute('data-tooltip-trigger-id'); + const root = document.querySelector(`[data-for-tooltip-id="${tooltipId}"]`); + + if (!root) { + trigger.removeAttribute('data-tooltip-trigger-id'); + trigger.removeAttribute('aria-describedby'); + removed++; + } + }); + + state.metrics.orphanedTooltipsRemoved += removed; + return removed; + } catch (error) { + logError('Error removing orphaned tooltips:', error); + return 0; + } + }; + + const checkMemoryUsage = () => { + if (window.performance && window.performance.memory) { + const memoryUsage = { + usedJSHeapSize: window.performance.memory.usedJSHeapSize, + totalJSHeapSize: window.performance.memory.totalJSHeapSize, + jsHeapSizeLimit: window.performance.memory.jsHeapSizeLimit, + percentUsed: (window.performance.memory.usedJSHeapSize / window.performance.memory.jsHeapSizeLimit * 100).toFixed(2) + }; + + state.metrics.lastMemoryUsage = memoryUsage; + return memoryUsage; } - const publicAPI = { - enableMonitoring: function(interval = config.monitorInterval) { - if (state.monitorInterval) { - clearInterval(state.monitorInterval); - } + return null; + }; - state.isMonitoringEnabled = true; - config.monitorInterval = interval; + const checkForDisconnectedElements = () => { + try { + const disconnectedElements = new Set(); - this.logMemoryUsage(); - - state.monitorInterval = setInterval(() => { - this.logMemoryUsage(); - }, interval); - - console.log(`Memory monitoring enabled - reporting every ${interval/1000} seconds`); - return true; - }, - - disableMonitoring: function() { - if (state.monitorInterval) { - clearInterval(state.monitorInterval); - state.monitorInterval = null; - } - - state.isMonitoringEnabled = false; - console.log('Memory monitoring disabled'); - return true; - }, - - logMemoryUsage: function() { - const timestamp = new Date().toLocaleTimeString(); - console.log(`=== Memory Monitor [${timestamp}] ===`); - - if (window.performance && window.performance.memory) { - console.log('Memory usage:', { - usedJSHeapSize: (window.performance.memory.usedJSHeapSize / 1024 / 1024).toFixed(2) + ' MB', - totalJSHeapSize: (window.performance.memory.totalJSHeapSize / 1024 / 1024).toFixed(2) + ' MB' - }); - } - - if (navigator.deviceMemory) { - console.log('Device memory:', navigator.deviceMemory, 'GB'); - } - - const nodeCount = document.querySelectorAll('*').length; - console.log('DOM node count:', nodeCount); - - if (window.CleanupManager) { - const counts = CleanupManager.getResourceCounts(); - console.log('Managed resources:', counts); - } - - if (window.TooltipManager) { - const tooltipInstances = document.querySelectorAll('[data-tippy-root]').length; - const tooltipTriggers = document.querySelectorAll('[data-tooltip-trigger-id]').length; - console.log('Tooltip instances:', tooltipInstances, '- Tooltip triggers:', tooltipTriggers); - } - - if (window.CacheManager && window.CacheManager.getStats) { - const cacheStats = CacheManager.getStats(); - console.log('Cache stats:', cacheStats); - } - - if (window.IdentityManager && window.IdentityManager.getStats) { - const identityStats = window.IdentityManager.getStats(); - console.log('Identity cache stats:', identityStats); - } - - console.log('=============================='); - }, - - enableAutoCleanup: function(interval = config.cleanupInterval) { - if (state.cleanupInterval) { - clearInterval(state.cleanupInterval); - } - - config.cleanupInterval = interval; - - this.forceCleanup(); - - state.cleanupInterval = setInterval(() => { - this.forceCleanup(); - }, interval); - - log('Auto-cleanup enabled every', interval/1000, 'seconds'); - return true; - }, - - disableAutoCleanup: function() { - if (state.cleanupInterval) { - clearInterval(state.cleanupInterval); - state.cleanupInterval = null; - } - - console.log('Memory auto-cleanup disabled'); - return true; - }, - - forceCleanup: function() { - if (config.debug) { - console.log('Running memory cleanup...', new Date().toLocaleTimeString()); - } - - if (window.CacheManager && CacheManager.cleanup) { - CacheManager.cleanup(true); - } - - if (window.TooltipManager && TooltipManager.cleanup) { - window.TooltipManager.cleanup(); - } - - document.querySelectorAll('[data-tooltip-trigger-id]').forEach(element => { - if (window.TooltipManager && TooltipManager.destroy) { - window.TooltipManager.destroy(element); - } - }); - - if (window.chartModule && chartModule.cleanup) { - chartModule.cleanup(); - } - - if (window.gc) { - window.gc(); - } else { - const arr = new Array(1000); - for (let i = 0; i < 1000; i++) { - arr[i] = new Array(10000).join('x'); - } - } - - if (config.debug) { - console.log('Memory cleanup completed'); - } - - return true; - }, - - setDebugMode: function(enabled) { - config.debug = Boolean(enabled); - return `Debug mode ${config.debug ? 'enabled' : 'disabled'}`; - }, - - getStatus: function() { - return { - monitoring: { - enabled: Boolean(state.monitorInterval), - interval: config.monitorInterval - }, - autoCleanup: { - enabled: Boolean(state.cleanupInterval), - interval: config.cleanupInterval - }, - debug: config.debug - }; - }, - - initialize: function(options = {}) { - if (options.debug !== undefined) { - this.setDebugMode(options.debug); - } - - if (options.enableMonitoring) { - this.enableMonitoring(options.monitorInterval || config.monitorInterval); - } - - if (options.enableAutoCleanup) { - this.enableAutoCleanup(options.cleanupInterval || config.cleanupInterval); - } - - if (window.CleanupManager) { - window.CleanupManager.registerResource('memoryManager', this, (mgr) => mgr.dispose()); - } - - log('MemoryManager initialized'); - return this; - }, - - dispose: function() { - this.disableMonitoring(); - this.disableAutoCleanup(); - log('MemoryManager disposed'); + state.trackedTooltips.forEach((info, id) => { + const { element } = info; + if (element && !document.body.contains(element)) { + disconnectedElements.add(id); } - }; + }); - return publicAPI; + disconnectedElements.forEach(id => { + destroyTooltip(id); + }); + + return disconnectedElements.size; + } catch (error) { + logError('Error checking for disconnected elements:', error); + return 0; + } + }; + + const setupMutationObserver = () => { + if (mutationObserver) { + mutationObserver.disconnect(); + } + + 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')) { + const tooltipId = node.getAttribute('data-tooltip-trigger-id'); + destroyTooltip(tooltipId); + needsCleanup = true; + } + + if (node.querySelectorAll) { + const tooltipTriggers = node.querySelectorAll('[data-tooltip-trigger-id]'); + if (tooltipTriggers.length > 0) { + tooltipTriggers.forEach(el => { + const tooltipId = el.getAttribute('data-tooltip-trigger-id'); + destroyTooltip(tooltipId); + }); + needsCleanup = true; + } + } + } + }); + } + }); + + if (needsCleanup) { + removeOrphanedTooltips(); + } + }); + + mutationObserver.observe(document.body, { + childList: true, + subtree: true + }); + + return mutationObserver; + }; + + const performCleanup = (force = false) => { + try { + log('Starting tooltip cleanup' + (force ? ' (forced)' : '')); + + state.lastCleanupTime = Date.now(); + state.metrics.cleanupRuns++; + + if (force) { + state.metrics.manualCleanupRuns++; + } + + document.querySelectorAll('[data-tippy-root]').forEach(root => { + const instance = safeGet(root, '_tippy'); + if (instance && instance._animationFrame) { + cancelAnimationFrame(instance._animationFrame); + instance._animationFrame = null; + } + }); + + const orphanedRemoved = removeOrphanedTooltips(); + + const disconnectedRemoved = checkForDisconnectedElements(); + + const tooltipCount = document.querySelectorAll('[data-tippy-root]').length; + const triggerCount = document.querySelectorAll('[data-tooltip-trigger-id]').length; + + if (force || tooltipCount > config.maxTooltipsThreshold) { + if (tooltipCount > config.maxTooltipsThreshold) { + document.querySelectorAll('[data-tooltip-trigger-id]').forEach(trigger => { + const tooltipId = trigger.getAttribute('data-tooltip-trigger-id'); + destroyTooltip(tooltipId); + }); + + document.querySelectorAll('[data-tippy-root]').forEach(root => { + if (root.parentNode) { + root.parentNode.removeChild(root); + } + }); + } + + document.querySelectorAll('[data-tooltip-trigger-id], [aria-describedby]').forEach(el => { + if (window.CleanupManager && window.CleanupManager.removeListenersByElement) { + window.CleanupManager.removeListenersByElement(el); + } else { + if (el.parentNode) { + const clone = el.cloneNode(true); + el.parentNode.replaceChild(clone, el); + } + } + }); + } + + if (window.gc) { + window.gc(); + } else if (force) { + const arr = new Array(1000); + for (let i = 0; i < 1000; i++) { + arr[i] = new Array(10000).join('x'); + } + } + + checkMemoryUsage(); + + const result = { + orphanedRemoved, + disconnectedRemoved, + tooltipCount: document.querySelectorAll('[data-tippy-root]').length, + triggerCount: document.querySelectorAll('[data-tooltip-trigger-id]').length, + memoryUsage: state.metrics.lastMemoryUsage + }; + + log('Cleanup completed', result); + return result; + } catch (error) { + logError('Error during cleanup:', error); + return { error: error.message }; + } + }; + + const runDiagnostics = () => { + try { + log('Running memory diagnostics'); + + const memoryUsage = checkMemoryUsage(); + const tooltipCount = document.querySelectorAll('[data-tippy-root]').length; + const triggerCount = document.querySelectorAll('[data-tooltip-trigger-id]').length; + + const diagnostics = { + time: new Date().toISOString(), + uptime: Date.now() - state.startTime, + memoryUsage, + elementsCount: { + tippyRoots: tooltipCount, + tooltipTriggers: triggerCount, + orphanedTriggers: triggerCount - tooltipCount > 0 ? triggerCount - tooltipCount : 0, + orphanedTooltips: tooltipCount - triggerCount > 0 ? tooltipCount - triggerCount : 0 + }, + metrics: { ...state.metrics }, + issues: [] + }; + + if (tooltipCount > config.maxTooltipsThreshold) { + diagnostics.issues.push({ + severity: 'high', + message: `Excessive tooltip count: ${tooltipCount} (threshold: ${config.maxTooltipsThreshold})`, + recommendation: 'Run cleanup and check for tooltip creation loops' + }); + } + + if (Math.abs(tooltipCount - triggerCount) > 10) { + diagnostics.issues.push({ + severity: 'medium', + message: `Mismatch between tooltips (${tooltipCount}) and triggers (${triggerCount})`, + recommendation: 'Remove orphaned tooltips and tooltip triggers' + }); + } + + if (memoryUsage && memoryUsage.percentUsed > 80) { + diagnostics.issues.push({ + severity: 'high', + message: `High memory usage: ${memoryUsage.percentUsed}%`, + recommendation: 'Force garbage collection and check for memory leaks' + }); + } + + if (config.autoCleanup && diagnostics.issues.some(issue => issue.severity === 'high')) { + log('Critical issues detected, triggering automatic cleanup'); + performCleanup(true); + } + + return diagnostics; + } catch (error) { + logError('Error running diagnostics:', error); + return { error: error.message }; + } + }; + + const patchTooltipManager = () => { + try { + if (!window.TooltipManager) { + log('TooltipManager not found'); + return false; + } + + log('Patching TooltipManager'); + + const originalCreate = window.TooltipManager.create; + const originalDestroy = window.TooltipManager.destroy; + const originalCleanup = window.TooltipManager.cleanup; + + window.TooltipManager.create = function(element, content, options = {}) { + if (!element) return null; + + try { + const result = originalCreate.call(this, element, content, options); + const tooltipId = element.getAttribute('data-tooltip-trigger-id'); + + if (tooltipId) { + const tippyInstance = safeGet(element, '_tippy') || null; + trackTooltip(element, tippyInstance); + } + + return result; + } catch (error) { + logError('Error in patched create:', error); + return originalCreate.call(this, element, content, options); + } + }; + + window.TooltipManager.destroy = function(element) { + if (!element) return; + + try { + const tooltipId = element.getAttribute('data-tooltip-trigger-id'); + + originalDestroy.call(this, element); + + if (tooltipId) { + state.trackedTooltips.delete(tooltipId); + state.metrics.tooltipsDestroyed++; + } + } catch (error) { + logError('Error in patched destroy:', error); + originalDestroy.call(this, element); + } + }; + + window.TooltipManager.cleanup = function() { + try { + originalCleanup.call(this); + removeOrphanedTooltips(); + } catch (error) { + logError('Error in patched cleanup:', error); + originalCleanup.call(this); + } + }; + + return true; + } catch (error) { + logError('Error patching TooltipManager:', error); + return false; + } + }; + + const patchTippy = () => { + try { + if (typeof tippy !== 'function') { + log('tippy.js not found globally'); + return false; + } + + log('Patching global tippy'); + + const originalTippy = window.tippy; + + window.tippy = function(...args) { + const result = originalTippy.apply(this, args); + + if (Array.isArray(result)) { + result.forEach(instance => { + const reference = instance.reference; + + if (reference) { + const originalShow = instance.show; + const originalHide = instance.hide; + const originalDestroy = instance.destroy; + + instance.show = function(...showArgs) { + return originalShow.apply(this, showArgs); + }; + + instance.hide = function(...hideArgs) { + return originalHide.apply(this, hideArgs); + }; + + instance.destroy = function(...destroyArgs) { + return originalDestroy.apply(this, destroyArgs); + }; + } + }); + } + + return result; + }; + + Object.assign(window.tippy, originalTippy); + + return true; + } catch (error) { + logError('Error patching tippy:', error); + return false; + } + }; + + const startMonitoring = () => { + try { + stopMonitoring(); + + state.intervals.set('cleanup', setInterval(() => { + performCleanup(); + }, config.tooltipCleanupInterval)); + + state.intervals.set('diagnostics', setInterval(() => { + runDiagnostics(); + }, config.diagnosticsInterval)); + + state.intervals.set('elementVerification', setInterval(() => { + checkForDisconnectedElements(); + }, config.elementVerificationInterval)); + + setupMutationObserver(); + + log('Monitoring started'); + return true; + } catch (error) { + logError('Error starting monitoring:', error); + return false; + } + }; + + const stopMonitoring = () => { + try { + state.intervals.forEach((interval, key) => { + clearInterval(interval); + }); + + state.intervals.clear(); + + if (mutationObserver) { + mutationObserver.disconnect(); + mutationObserver = null; + } + + log('Monitoring stopped'); + return true; + } catch (error) { + logError('Error stopping monitoring:', error); + return false; + } + }; + + const autoFix = () => { + try { + log('Running auto-fix'); + + performCleanup(true); + + document.querySelectorAll('[data-tooltip-trigger-id]').forEach(element => { + const tooltipId = element.getAttribute('data-tooltip-trigger-id'); + const duplicates = document.querySelectorAll(`[data-tooltip-trigger-id="${tooltipId}"]`); + + if (duplicates.length > 1) { + for (let i = 1; i < duplicates.length; i++) { + duplicates[i].removeAttribute('data-tooltip-trigger-id'); + duplicates[i].removeAttribute('aria-describedby'); + } + } + }); + + const tippyRoots = document.querySelectorAll('[data-tippy-root]'); + tippyRoots.forEach(root => { + if (!document.body.contains(root) && root.parentNode) { + root.parentNode.removeChild(root); + } + }); + + if (window.TooltipManager && window.TooltipManager.getInstance) { + const manager = window.TooltipManager.getInstance(); + if (manager && manager.chartRefs && manager.chartRefs.clear) { + manager.chartRefs.clear(); + } + + if (manager && manager.tooltipElementsMap && manager.tooltipElementsMap.clear) { + manager.tooltipElementsMap.clear(); + } + } + + patchTooltipManager(); + patchTippy(); + + return true; + } catch (error) { + logError('Error during auto-fix:', error); + return false; + } + }; + + const initialize = (options = {}) => { + try { + Object.assign(config, options); + + if (document.head) { + const metaCache = document.createElement('meta'); + metaCache.setAttribute('http-equiv', 'Cache-Control'); + metaCache.setAttribute('content', 'no-store, max-age=0'); + document.head.appendChild(metaCache); + } + + patchTooltipManager(); + patchTippy(); + + startMonitoring(); + + if (window.CleanupManager && window.CleanupManager.registerResource) { + window.CleanupManager.registerResource('memorymanager', MemoryManager, (optimizer) => { + optimizer.dispose(); + }); + } + + log('Memory Optimizer initialized', config); + + setTimeout(() => { + runDiagnostics(); + }, 5000); + + return MemoryManager; + } catch (error) { + logError('Error initializing Memory Optimizer:', error); + return null; + } + }; + + const dispose = () => { + try { + log('Disposing Memory Optimizer'); + + performCleanup(true); + + stopMonitoring(); + + state.trackedTooltips.clear(); + + return true; + } catch (error) { + logError('Error disposing Memory Optimizer:', error); + return false; + } + }; + + return { + initialize, + dispose, + performCleanup, + runDiagnostics, + autoFix, + getConfig: () => ({ ...config }), + getMetrics: () => ({ ...state.metrics }), + setDebugMode: (enabled) => { + config.debug = Boolean(enabled); + return config.debug; + } + }; })(); +if (typeof document !== 'undefined') { + document.addEventListener('DOMContentLoaded', function() { + MemoryManager.initialize(); + }); +} + window.MemoryManager = MemoryManager; - -document.addEventListener('DOMContentLoaded', function() { - if (!window.memoryManagerInitialized) { - MemoryManager.initialize(); - window.memoryManagerInitialized = true; - } -}); - -//console.log('MemoryManager initialized with methods:', Object.keys(MemoryManager)); -console.log('MemoryManager initialized'); +console.log('Memory Manager initialized'); diff --git a/basicswap/static/js/modules/tooltips-manager.js b/basicswap/static/js/modules/tooltips-manager.js index 6af1b6e..69219f0 100644 --- a/basicswap/static/js/modules/tooltips-manager.js +++ b/basicswap/static/js/modules/tooltips-manager.js @@ -1,46 +1,52 @@ const TooltipManager = (function() { let instance = null; + const tooltipInstanceMap = new WeakMap(); + class TooltipManagerImpl { constructor() { - if (instance) { return instance; } - - this.activeTooltips = new WeakMap(); - this.tooltipIdCounter = 0; this.pendingAnimationFrames = new Set(); - this.tooltipElementsMap = new Map(); - this.maxTooltips = 300; - this.cleanupThreshold = 1.3; + this.pendingTimeouts = new Set(); + this.tooltipIdCounter = 0; + this.maxTooltips = 200; + this.cleanupThreshold = 1.2; this.disconnectedCheckInterval = null; - + this.cleanupInterval = null; + this.mutationObserver = null; + this.debug = false; + this.tooltipData = new WeakMap(); this.setupStyles(); - this.setupCleanupEvents(); - this.initializeMutationObserver(); + this.setupMutationObserver(); + this.startPeriodicCleanup(); this.startDisconnectedElementsCheck(); - instance = this; } + log(message, ...args) { + if (this.debug) { + console.log(`[TooltipManager] ${message}`, ...args); + } + } + create(element, content, options = {}) { - if (!element) return null; + if (!element || !document.body.contains(element)) return null; + + if (!document.contains(element)) { + this.log('Tried to create tooltip for detached element'); + return null; + } this.destroy(element); - if (this.tooltipElementsMap.size > this.maxTooltips * this.cleanupThreshold) { - const oldestEntries = Array.from(this.tooltipElementsMap.entries()) - .sort((a, b) => a[1].timestamp - b[1].timestamp) - .slice(0, 20); - - oldestEntries.forEach(([el]) => { - this.destroy(el); - }); + const currentTooltipCount = document.querySelectorAll('[data-tooltip-trigger-id]').length; + if (currentTooltipCount > this.maxTooltips * this.cleanupThreshold) { + this.cleanupOrphanedTooltips(); + this.performPeriodicCleanup(true); } - const originalContent = content; - const rafId = requestAnimationFrame(() => { this.pendingAnimationFrames.delete(rafId); @@ -48,81 +54,50 @@ const TooltipManager = (function() { const rect = element.getBoundingClientRect(); if (rect.width > 0 && rect.height > 0) { - this.createTooltip(element, originalContent, options, rect); + this.createTooltipInstance(element, content, options); } else { let retryCount = 0; + const maxRetries = 3; + const retryCreate = () => { const newRect = element.getBoundingClientRect(); - if ((newRect.width > 0 && newRect.height > 0) || retryCount >= 3) { + if ((newRect.width > 0 && newRect.height > 0) || retryCount >= maxRetries) { if (newRect.width > 0 && newRect.height > 0) { - this.createTooltip(element, originalContent, options, newRect); + this.createTooltipInstance(element, content, options); } } else { retryCount++; - const newRafId = requestAnimationFrame(retryCreate); - this.pendingAnimationFrames.add(newRafId); + const timeoutId = setTimeout(() => { + this.pendingTimeouts.delete(timeoutId); + const newRafId = requestAnimationFrame(retryCreate); + this.pendingAnimationFrames.add(newRafId); + }, 100); + this.pendingTimeouts.add(timeoutId); } }; - const initialRetryId = requestAnimationFrame(retryCreate); - this.pendingAnimationFrames.add(initialRetryId); + + const initialTimeoutId = setTimeout(() => { + this.pendingTimeouts.delete(initialTimeoutId); + const retryRafId = requestAnimationFrame(retryCreate); + this.pendingAnimationFrames.add(retryRafId); + }, 100); + this.pendingTimeouts.add(initialTimeoutId); } }); this.pendingAnimationFrames.add(rafId); return null; } - - createTooltip(element, content, options, rect) { - const targetId = element.getAttribute('data-tooltip-target'); - let bgClass = 'bg-gray-400'; - let arrowColor = 'rgb(156 163 175)'; - - if (targetId?.includes('tooltip-offer-') && window.jsonData) { - try { - const offerId = targetId.split('tooltip-offer-')[1]; - let actualOfferId = offerId; - - if (offerId.includes('_')) { - [actualOfferId] = offerId.split('_'); - } - - let offer = null; - if (Array.isArray(window.jsonData)) { - for (let i = 0; i < window.jsonData.length; i++) { - const o = window.jsonData[i]; - if (o && (o.unique_id === offerId || o.offer_id === actualOfferId)) { - offer = o; - break; - } - } - } - - if (offer) { - if (offer.is_revoked) { - bgClass = 'bg-red-500'; - arrowColor = 'rgb(239 68 68)'; - } else if (offer.is_own_offer) { - bgClass = 'bg-gray-300'; - arrowColor = 'rgb(209 213 219)'; - } else { - bgClass = 'bg-green-700'; - arrowColor = 'rgb(21 128 61)'; - } - } - } catch (e) { - console.warn('Error finding offer for tooltip:', e); - } + + createTooltipInstance(element, content, options = {}) { + if (!element || !document.body.contains(element) || !window.tippy) { + return null; } - const tooltipId = `tooltip-${++this.tooltipIdCounter}`; - try { - if (typeof tippy !== 'function') { - console.error('Tippy.js is not loaded. Cannot create tooltip.'); - return null; - } + const tooltipId = `tooltip-${++this.tooltipIdCounter}`; - const instance = tippy(element, { + const tooltipOptions = { content: content, allowHTML: true, placement: options.placement || 'top', @@ -143,14 +118,25 @@ const TooltipManager = (function() { }, onMount(instance) { if (instance.popper && instance.popper.firstElementChild) { + const bgClass = options.bgClass || 'bg-gray-400'; instance.popper.firstElementChild.classList.add(bgClass); instance.popper.setAttribute('data-for-tooltip-id', tooltipId); } const arrow = instance.popper.querySelector('.tippy-arrow'); if (arrow) { + const arrowColor = options.arrowColor || 'rgb(156 163 175)'; arrow.style.setProperty('color', arrowColor, 'important'); } }, + onHidden(instance) { + if (!document.body.contains(element)) { + setTimeout(() => { + if (instance && instance.destroy) { + instance.destroy(); + } + }, 100); + } + }, popperOptions: { strategy: 'fixed', modifiers: [ @@ -170,19 +156,27 @@ const TooltipManager = (function() { } ] } - }); + }; - element.setAttribute('data-tooltip-trigger-id', tooltipId); - this.activeTooltips.set(element, instance); + const tippyInstance = window.tippy(element, tooltipOptions); - this.tooltipElementsMap.set(element, { - timestamp: Date.now(), - id: tooltipId - }); + if (tippyInstance && Array.isArray(tippyInstance) && tippyInstance[0]) { + this.tooltipData.set(element, { + id: tooltipId, + instance: tippyInstance[0], + timestamp: Date.now() + }); - return instance; - } catch (e) { - console.error('Error creating tooltip:', e); + element.setAttribute('data-tooltip-trigger-id', tooltipId); + + tooltipInstanceMap.set(element, tippyInstance[0]); + + return tippyInstance[0]; + } + + return null; + } catch (error) { + console.error('Error creating tooltip:', error); return null; } } @@ -190,35 +184,49 @@ const TooltipManager = (function() { destroy(element) { if (!element) return; - const id = element.getAttribute('data-tooltip-trigger-id'); - if (!id) return; + try { + const tooltipId = element.getAttribute('data-tooltip-trigger-id'); + if (!tooltipId) return; - const instance = this.activeTooltips.get(element); - if (instance?.[0]) { - try { - instance[0].destroy(); - } catch (e) { - console.warn('Error destroying tooltip:', e); + const tooltipData = this.tooltipData.get(element); + const instance = tooltipData?.instance || tooltipInstanceMap.get(element); - const tippyRoot = document.querySelector(`[data-for-tooltip-id="${id}"]`); - if (tippyRoot && tippyRoot.parentNode) { - tippyRoot.parentNode.removeChild(tippyRoot); + if (instance) { + try { + instance.destroy(); + } catch (e) { + console.warn('Error destroying tooltip instance:', e); } } + + element.removeAttribute('data-tooltip-trigger-id'); + element.removeAttribute('aria-describedby'); + + const tippyRoot = document.querySelector(`[data-for-tooltip-id="${tooltipId}"]`); + if (tippyRoot && tippyRoot.parentNode) { + tippyRoot.parentNode.removeChild(tippyRoot); + } + + this.tooltipData.delete(element); + tooltipInstanceMap.delete(element); + } catch (error) { + console.error('Error destroying tooltip:', error); } - - this.activeTooltips.delete(element); - this.tooltipElementsMap.delete(element); - - element.removeAttribute('data-tooltip-trigger-id'); } cleanup() { + this.log('Running tooltip cleanup'); + this.pendingAnimationFrames.forEach(id => { cancelAnimationFrame(id); }); this.pendingAnimationFrames.clear(); + this.pendingTimeouts.forEach(id => { + clearTimeout(id); + }); + this.pendingTimeouts.clear(); + const elements = document.querySelectorAll('[data-tooltip-trigger-id]'); const batchSize = 20; @@ -236,26 +244,144 @@ const TooltipManager = (function() { }); this.pendingAnimationFrames.add(rafId); } else { - this.cleanupOrphanedTippyElements(); + this.cleanupOrphanedTooltips(); } }; if (elements.length > 0) { processElementsBatch(0); } else { - this.cleanupOrphanedTippyElements(); + this.cleanupOrphanedTooltips(); } - - this.tooltipElementsMap.clear(); } - cleanupOrphanedTippyElements() { + cleanupOrphanedTooltips() { const tippyElements = document.querySelectorAll('[data-tippy-root]'); + let removed = 0; + tippyElements.forEach(element => { - if (element.parentNode) { - element.parentNode.removeChild(element); + const tooltipId = element.getAttribute('data-for-tooltip-id'); + const trigger = tooltipId ? + document.querySelector(`[data-tooltip-trigger-id="${tooltipId}"]`) : + null; + + if (!trigger || !document.body.contains(trigger)) { + if (element.parentNode) { + element.parentNode.removeChild(element); + removed++; + } } }); + + if (removed > 0) { + this.log(`Removed ${removed} orphaned tooltip elements`); + } + + return removed; + } + + setupMutationObserver() { + if (this.mutationObserver) { + this.mutationObserver.disconnect(); + } + + this.mutationObserver = new MutationObserver(mutations => { + let needsCleanup = false; + + mutations.forEach(mutation => { + if (mutation.removedNodes.length) { + Array.from(mutation.removedNodes).forEach(node => { + if (node.nodeType === Node.ELEMENT_NODE) { + if (node.hasAttribute && node.hasAttribute('data-tooltip-trigger-id')) { + this.destroy(node); + needsCleanup = true; + } + + if (node.querySelectorAll) { + const tooltipTriggers = node.querySelectorAll('[data-tooltip-trigger-id]'); + if (tooltipTriggers.length > 0) { + tooltipTriggers.forEach(trigger => { + this.destroy(trigger); + }); + needsCleanup = true; + } + } + } + }); + } + }); + + if (needsCleanup) { + this.cleanupOrphanedTooltips(); + } + }); + + this.mutationObserver.observe(document.body, { + childList: true, + subtree: true + }); + + return this.mutationObserver; + } + + startDisconnectedElementsCheck() { + if (this.disconnectedCheckInterval) { + clearInterval(this.disconnectedCheckInterval); + } + + this.disconnectedCheckInterval = setInterval(() => { + this.checkForDisconnectedElements(); + }, 60000); + } + + checkForDisconnectedElements() { + const elements = document.querySelectorAll('[data-tooltip-trigger-id]'); + let removedCount = 0; + + elements.forEach(element => { + if (!document.body.contains(element)) { + this.destroy(element); + removedCount++; + } + }); + + if (removedCount > 0) { + this.log(`Removed ${removedCount} tooltips for disconnected elements`); + this.cleanupOrphanedTooltips(); + } + } + + startPeriodicCleanup() { + if (this.cleanupInterval) { + clearInterval(this.cleanupInterval); + } + + this.cleanupInterval = setInterval(() => { + this.performPeriodicCleanup(); + }, 120000); + } + + performPeriodicCleanup(force = false) { + this.cleanupOrphanedTooltips(); + + this.checkForDisconnectedElements(); + + const tooltipCount = document.querySelectorAll('[data-tippy-root]').length; + + if (force || tooltipCount > this.maxTooltips) { + this.log(`Performing aggressive cleanup (${tooltipCount} tooltips)`); + + this.cleanup(); + + if (window.gc) { + window.gc(); + } else { + const arr = new Array(1000); + for (let i = 0; i < 1000; i++) { + arr[i] = new Array(10000).join('x'); + } + } + } } setupStyles() { @@ -350,136 +476,11 @@ const TooltipManager = (function() { `); } - setupCleanupEvents() { - this.boundCleanup = this.cleanup.bind(this); - this.handleVisibilityChange = () => { - if (document.hidden) { - this.cleanup(); - - if (window.MemoryManager) { - window.MemoryManager.forceCleanup(); - } - } - }; - - window.addEventListener('beforeunload', this.boundCleanup); - window.addEventListener('unload', this.boundCleanup); - document.addEventListener('visibilitychange', this.handleVisibilityChange); - - if (window.CleanupManager) { - window.CleanupManager.registerResource('tooltipManager', this, (tm) => tm.dispose()); - } - - this.cleanupInterval = setInterval(() => { - this.performPeriodicCleanup(); - }, 120000); - } - - startDisconnectedElementsCheck() { - - if (this.disconnectedCheckInterval) { - clearInterval(this.disconnectedCheckInterval); - } - - this.disconnectedCheckInterval = setInterval(() => { - this.checkForDisconnectedElements(); - }, 60000); - } - - checkForDisconnectedElements() { - if (this.tooltipElementsMap.size === 0) return; - - const elementsToCheck = Array.from(this.tooltipElementsMap.keys()); - let removedCount = 0; - - elementsToCheck.forEach(element => { - - if (!document.body.contains(element)) { - this.destroy(element); - removedCount++; - } - }); - - if (removedCount > 0) { - this.cleanupOrphanedTippyElements(); - } - } - - performPeriodicCleanup() { - this.cleanupOrphanedTippyElements(); - this.checkForDisconnectedElements(); - - if (this.tooltipElementsMap.size > this.maxTooltips * this.cleanupThreshold) { - const sortedTooltips = Array.from(this.tooltipElementsMap.entries()) - .sort((a, b) => a[1].timestamp - b[1].timestamp); - - const tooltipsToRemove = sortedTooltips.slice(0, sortedTooltips.length - this.maxTooltips); - tooltipsToRemove.forEach(([element]) => { - this.destroy(element); - }); - } - } - - removeCleanupEvents() { - window.removeEventListener('beforeunload', this.boundCleanup); - window.removeEventListener('unload', this.boundCleanup); - document.removeEventListener('visibilitychange', this.handleVisibilityChange); - - if (this.cleanupInterval) { - clearInterval(this.cleanupInterval); - this.cleanupInterval = null; - } - - if (this.disconnectedCheckInterval) { - clearInterval(this.disconnectedCheckInterval); - this.disconnectedCheckInterval = null; - } - } - - 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) { - const tooltipTriggers = node.querySelectorAll('[data-tooltip-trigger-id]'); - if (tooltipTriggers.length > 0) { - tooltipTriggers.forEach(el => { - this.destroy(el); - }); - needsCleanup = true; - } - } - } - }); - } - }); - - if (needsCleanup) { - this.cleanupOrphanedTippyElements(); - } - }); - - this.mutationObserver.observe(document.body, { - childList: true, - subtree: true - }); - } - initializeTooltips(selector = '[data-tooltip-target]') { document.querySelectorAll(selector).forEach(element => { const targetId = element.getAttribute('data-tooltip-target'); + if (!targetId) return; + const tooltipContent = document.getElementById(targetId); if (tooltipContent) { @@ -491,6 +492,8 @@ const TooltipManager = (function() { } dispose() { + this.log('Disposing TooltipManager'); + this.cleanup(); this.pendingAnimationFrames.forEach(id => { @@ -498,31 +501,50 @@ const TooltipManager = (function() { }); this.pendingAnimationFrames.clear(); + this.pendingTimeouts.forEach(id => { + clearTimeout(id); + }); + this.pendingTimeouts.clear(); + + if (this.disconnectedCheckInterval) { + clearInterval(this.disconnectedCheckInterval); + this.disconnectedCheckInterval = null; + } + + if (this.cleanupInterval) { + clearInterval(this.cleanupInterval); + this.cleanupInterval = null; + } + 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); } - this.activeTooltips = new WeakMap(); - this.tooltipElementsMap.clear(); - instance = null; + return true; + } + + setDebugMode(enabled) { + this.debug = Boolean(enabled); + return this.debug; } initialize(options = {}) { - if (options.maxTooltips) { this.maxTooltips = options.maxTooltips; } - console.log('TooltipManager initialized'); + if (options.debug !== undefined) { + this.setDebugMode(options.debug); + } + + this.log('TooltipManager initialized'); return this; } } @@ -538,7 +560,7 @@ const TooltipManager = (function() { getInstance: function() { if (!instance) { - const manager = new TooltipManagerImpl(); + this.initialize(); } return instance; }, @@ -562,6 +584,11 @@ const TooltipManager = (function() { const manager = this.getInstance(); return manager.initializeTooltips(...args); }, + + setDebugMode: function(enabled) { + const manager = this.getInstance(); + return manager.setDebugMode(enabled); + }, dispose: function(...args) { const manager = this.getInstance(); @@ -570,19 +597,37 @@ const TooltipManager = (function() { }; })(); -window.TooltipManager = TooltipManager; +function installTooltipManager() { + const originalTooltipManager = window.TooltipManager; -document.addEventListener('DOMContentLoaded', function() { - if (!window.tooltipManagerInitialized) { - TooltipManager.initialize(); - TooltipManager.initializeTooltips(); - window.tooltipManagerInitialized = true; + window.TooltipManager = TooltipManager; + + window.TooltipManager.initialize({ + maxTooltips: 200, + debug: false + }); + + document.addEventListener('DOMContentLoaded', function() { + if (!window.tooltipManagerInitialized) { + window.TooltipManager.initializeTooltips(); + window.tooltipManagerInitialized = true; + } + }); + + return originalTooltipManager; +} + +if (typeof document !== 'undefined') { + if (document.readyState === 'complete' || document.readyState === 'interactive') { + installTooltipManager(); + } else { + document.addEventListener('DOMContentLoaded', installTooltipManager); } -}); +} if (typeof module !== 'undefined' && module.exports) { module.exports = TooltipManager; } -//console.log('TooltipManager initialized with methods:', Object.keys(TooltipManager)); +window.TooltipManager = TooltipManager; console.log('TooltipManager initialized'); diff --git a/basicswap/static/js/offers.js b/basicswap/static/js/offers.js index 0993312..8cf7f71 100644 --- a/basicswap/static/js/offers.js +++ b/basicswap/static/js/offers.js @@ -2343,10 +2343,31 @@ function cleanup() { } } - if (window.TooltipManager) { - if (typeof window.TooltipManager.cleanup === 'function') { - window.TooltipManager.cleanup(); - } + const offersBody = document.getElementById('offers-body'); + if (offersBody) { + const existingRows = Array.from(offersBody.querySelectorAll('tr')); + existingRows.forEach(row => { + const tooltipTriggers = row.querySelectorAll('[data-tooltip-trigger-id]'); + tooltipTriggers.forEach(trigger => { + if (window.TooltipManager) { + window.TooltipManager.destroy(trigger); + } + }); + + if (window.CleanupManager) { + window.CleanupManager.removeListenersByElement(row); + } + + while (row.attributes && row.attributes.length > 0) { + row.removeAttribute(row.attributes[0].name); + } + + while (row.firstChild) { + row.removeChild(row.firstChild); + } + }); + + offersBody.innerHTML = ''; } const filterForm = document.getElementById('filterForm'); @@ -2358,21 +2379,21 @@ function cleanup() { }); } - const paginationButtons = document.querySelectorAll('#prevPage, #nextPage'); - paginationButtons.forEach(button => { - CleanupManager.removeListenersByElement(button); - }); - - document.querySelectorAll('th[data-sortable="true"]').forEach(header => { - CleanupManager.removeListenersByElement(header); - }); - - cleanupTable(); - jsonData = null; originalJsonData = null; latestPrices = null; + if (window.TooltipManager) { + window.TooltipManager.cleanup(); + } + + if (window.MemoryManager) { + if (window.MemoryManager.cleanupTooltips) { + window.MemoryManager.cleanupTooltips(true); + } + window.MemoryManager.forceCleanup(); + } + console.log('Offers.js cleanup completed'); } catch (error) { console.error('Error during cleanup:', error); diff --git a/basicswap/static/js/pricechart.js b/basicswap/static/js/pricechart.js index 3b2b15a..8b56ba4 100644 --- a/basicswap/static/js/pricechart.js +++ b/basicswap/static/js/pricechart.js @@ -120,18 +120,33 @@ const api = { fetchCoinGeckoDataXHR: async () => { try { const priceData = await window.PriceManager.getPrices(); - const transformedData = {}; + const btcPriceUSD = priceData.bitcoin?.usd || 0; + if (btcPriceUSD > 0) { + window.btcPriceUSD = btcPriceUSD; + } + window.config.coins.forEach(coin => { const symbol = coin.symbol.toLowerCase(); const coinData = priceData[symbol] || priceData[coin.name.toLowerCase()]; if (coinData && coinData.usd) { + let priceBtc; + if (symbol === 'btc') { + priceBtc = 1; + } else if (window.btcPriceUSD && window.btcPriceUSD > 0) { + priceBtc = coinData.usd / window.btcPriceUSD; + } else { + priceBtc = coinData.btc || 0; + } + transformedData[symbol] = { current_price: coinData.usd, - price_btc: coinData.btc || (priceData.bitcoin ? coinData.usd / priceData.bitcoin.usd : 0), - displayName: coin.displayName || coin.symbol + price_btc: priceBtc, + displayName: coin.displayName || coin.symbol, + total_volume: coinData.total_volume, + price_change_percentage_24h: coinData.price_change_percentage_24h }; } }); @@ -274,63 +289,72 @@ const rateLimiter = { const ui = { displayCoinData: (coin, data) => { - let priceUSD, priceBTC, priceChange1d, volume24h; - const updateUI = (isError = false) => { - const priceUsdElement = document.querySelector(`#${coin.toLowerCase()}-price-usd`); - const volumeDiv = document.querySelector(`#${coin.toLowerCase()}-volume-div`); - const volumeElement = document.querySelector(`#${coin.toLowerCase()}-volume-24h`); - const btcPriceDiv = document.querySelector(`#${coin.toLowerCase()}-btc-price-div`); - const priceBtcElement = document.querySelector(`#${coin.toLowerCase()}-price-btc`); - - if (priceUsdElement) { - priceUsdElement.textContent = isError ? 'N/A' : `$ ${ui.formatPrice(coin, priceUSD)}`; - } - - if (volumeDiv && volumeElement) { - if (isError || volume24h === null || volume24h === undefined) { - volumeElement.textContent = 'N/A'; - } else { - volumeElement.textContent = `${utils.formatNumber(volume24h, 0)} USD`; - } - volumeDiv.style.display = volumeToggle.isVisible ? 'flex' : 'none'; - } - - if (btcPriceDiv && priceBtcElement) { - if (coin === 'BTC') { - btcPriceDiv.style.display = 'none'; - } else { - priceBtcElement.textContent = isError ? 'N/A' : `${priceBTC.toFixed(8)}`; - btcPriceDiv.style.display = 'flex'; - } - } - - ui.updatePriceChangeContainer(coin, isError ? null : priceChange1d); - }; - - try { - if (data.error) { - throw new Error(data.error); - } - - if (!data || !data.current_price) { - throw new Error(`Invalid data structure for ${coin}`); - } - - priceUSD = data.current_price; - priceBTC = coin === 'BTC' ? 1 : data.price_btc || (data.current_price / app.btcPriceUSD); - priceChange1d = data.price_change_percentage_24h || 0; - volume24h = data.total_volume || 0; - - if (isNaN(priceUSD) || isNaN(priceBTC)) { - throw new Error(`Invalid numeric values in data for ${coin}`); - } - - updateUI(false); - } catch (error) { - logger.error(`Failed to display data for ${coin}:`, error.message); - updateUI(true); + let priceUSD, priceBTC, priceChange1d, volume24h; + const updateUI = (isError = false) => { + const priceUsdElement = document.querySelector(`#${coin.toLowerCase()}-price-usd`); + const volumeDiv = document.querySelector(`#${coin.toLowerCase()}-volume-div`); + const volumeElement = document.querySelector(`#${coin.toLowerCase()}-volume-24h`); + const btcPriceDiv = document.querySelector(`#${coin.toLowerCase()}-btc-price-div`); + const priceBtcElement = document.querySelector(`#${coin.toLowerCase()}-price-btc`); + if (priceUsdElement) { + priceUsdElement.textContent = isError ? 'N/A' : `$ ${ui.formatPrice(coin, priceUSD)}`; } - }, + if (volumeDiv && volumeElement) { + if (isError || volume24h === null || volume24h === undefined) { + volumeElement.textContent = 'N/A'; + } else { + volumeElement.textContent = `${utils.formatNumber(volume24h, 0)} USD`; + } + volumeDiv.style.display = volumeToggle.isVisible ? 'flex' : 'none'; + } + if (btcPriceDiv && priceBtcElement) { + if (coin === 'BTC') { + btcPriceDiv.style.display = 'none'; + } else { + priceBtcElement.textContent = isError ? 'N/A' : `${priceBTC.toFixed(8)}`; + btcPriceDiv.style.display = 'flex'; + } + } + ui.updatePriceChangeContainer(coin, isError ? null : priceChange1d); + }; + try { + if (data.error) { + throw new Error(data.error); + } + if (!data || !data.current_price) { + throw new Error(`Invalid data structure for ${coin}`); + } + priceUSD = data.current_price; + + if (coin === 'BTC') { + priceBTC = 1; + } else { + + if (data.price_btc !== undefined && data.price_btc !== null) { + priceBTC = data.price_btc; + } + else if (window.btcPriceUSD && window.btcPriceUSD > 0) { + priceBTC = priceUSD / window.btcPriceUSD; + } + else if (app && app.btcPriceUSD && app.btcPriceUSD > 0) { + priceBTC = priceUSD / app.btcPriceUSD; + } + else { + priceBTC = 0; + } + } + + priceChange1d = data.price_change_percentage_24h || 0; + volume24h = data.total_volume || 0; + if (isNaN(priceUSD) || isNaN(priceBTC)) { + throw new Error(`Invalid numeric values in data for ${coin}`); + } + updateUI(false); + } catch (error) { + logger.error(`Failed to display data for ${coin}:`, error.message); + updateUI(true); + } +}, showLoader: () => { const loader = document.getElementById('loader'); @@ -1746,7 +1770,14 @@ document.addEventListener('DOMContentLoaded', () => { app.init(); if (window.MemoryManager) { + if (typeof MemoryManager.enableAutoCleanup === 'function') { MemoryManager.enableAutoCleanup(); + } else { + MemoryManager.initialize({ + autoCleanup: true, + debug: false + }); + } } CleanupManager.setInterval(() => {