Fix: Price Tiles volume/btc display + Better memory clean / tooltip manager.

This commit is contained in:
gerlofvanek
2025-05-04 19:51:22 +02:00
parent aa898a9601
commit d57a148ff4
5 changed files with 1129 additions and 539 deletions

View File

@@ -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;

View File

@@ -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');

View File

@@ -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,23 +54,34 @@ 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);
}
});
@@ -72,57 +89,15 @@ const TooltipManager = (function() {
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;
},
@@ -563,6 +585,11 @@ const TooltipManager = (function() {
return manager.initializeTooltips(...args);
},
setDebugMode: function(enabled) {
const manager = this.getInstance();
return manager.setDebugMode(enabled);
},
dispose: function(...args) {
const manager = this.getInstance();
return manager.dispose(...args);
@@ -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');

View File

@@ -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);

View File

@@ -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(() => {