mirror of
https://github.com/basicswap/basicswap.git
synced 2025-05-04 20:02:30 +00:00
Pricechart optimization.
This commit is contained in:
parent
a5c3c692a0
commit
2db145d0d3
1 changed files with 452 additions and 117 deletions
|
@ -1,3 +1,128 @@
|
||||||
|
// CLEANUP
|
||||||
|
const cleanupManager = {
|
||||||
|
eventListeners: [],
|
||||||
|
timeouts: [],
|
||||||
|
intervals: [],
|
||||||
|
animationFrames: [],
|
||||||
|
|
||||||
|
addListener: function(element, type, handler, options) {
|
||||||
|
if (!element) return null;
|
||||||
|
element.addEventListener(type, handler, options);
|
||||||
|
this.eventListeners.push({ element, type, handler, options });
|
||||||
|
return handler;
|
||||||
|
},
|
||||||
|
|
||||||
|
setTimeout: function(callback, delay) {
|
||||||
|
const id = setTimeout(callback, delay);
|
||||||
|
this.timeouts.push(id);
|
||||||
|
return id;
|
||||||
|
},
|
||||||
|
|
||||||
|
setInterval: function(callback, delay) {
|
||||||
|
const id = setInterval(callback, delay);
|
||||||
|
this.intervals.push(id);
|
||||||
|
return id;
|
||||||
|
},
|
||||||
|
|
||||||
|
requestAnimationFrame: function(callback) {
|
||||||
|
const id = requestAnimationFrame(callback);
|
||||||
|
this.animationFrames.push(id);
|
||||||
|
return id;
|
||||||
|
},
|
||||||
|
|
||||||
|
clearAll: function() {
|
||||||
|
this.eventListeners.forEach(({ element, type, handler, options }) => {
|
||||||
|
if (element) {
|
||||||
|
try {
|
||||||
|
element.removeEventListener(type, handler, options);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Error removing event listener:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.eventListeners = [];
|
||||||
|
|
||||||
|
this.timeouts.forEach(id => clearTimeout(id));
|
||||||
|
this.timeouts = [];
|
||||||
|
|
||||||
|
this.intervals.forEach(id => clearInterval(id));
|
||||||
|
this.intervals = [];
|
||||||
|
|
||||||
|
this.animationFrames.forEach(id => cancelAnimationFrame(id));
|
||||||
|
this.animationFrames = [];
|
||||||
|
|
||||||
|
console.log('All resources cleaned up');
|
||||||
|
},
|
||||||
|
|
||||||
|
clearTimeouts: function() {
|
||||||
|
this.timeouts.forEach(id => clearTimeout(id));
|
||||||
|
this.timeouts = [];
|
||||||
|
},
|
||||||
|
|
||||||
|
clearIntervals: function() {
|
||||||
|
this.intervals.forEach(id => clearInterval(id));
|
||||||
|
this.intervals = [];
|
||||||
|
},
|
||||||
|
|
||||||
|
removeListenersByElement: function(element) {
|
||||||
|
if (!element) return;
|
||||||
|
|
||||||
|
const listenersToRemove = this.eventListeners.filter(
|
||||||
|
listener => listener.element === element
|
||||||
|
);
|
||||||
|
|
||||||
|
listenersToRemove.forEach(({ element, type, handler, options }) => {
|
||||||
|
try {
|
||||||
|
element.removeEventListener(type, handler, options);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Error removing event listener:', e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.eventListeners = this.eventListeners.filter(
|
||||||
|
listener => listener.element !== element
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// MEMORY
|
||||||
|
const memoryMonitor = {
|
||||||
|
isEnabled: true,
|
||||||
|
lastLogTime: 0,
|
||||||
|
logInterval: 5 * 60 * 1000,
|
||||||
|
monitorInterval: null,
|
||||||
|
|
||||||
|
startMonitoring: function() {
|
||||||
|
console.log('Starting memory monitoring');
|
||||||
|
if (!this.isEnabled) return;
|
||||||
|
|
||||||
|
if (this.monitorInterval) {
|
||||||
|
clearInterval(this.monitorInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.monitorInterval = setInterval(() => {
|
||||||
|
this.logMemoryUsage();
|
||||||
|
}, this.logInterval);
|
||||||
|
|
||||||
|
this.logMemoryUsage();
|
||||||
|
},
|
||||||
|
|
||||||
|
logMemoryUsage: function() {
|
||||||
|
console.log('Logging memory usage');
|
||||||
|
if (window.performance && window.performance.memory) {
|
||||||
|
const memory = window.performance.memory;
|
||||||
|
console.log(`Memory Usage: ${Math.round(memory.usedJSHeapSize / (1024 * 1024))}MB / ${Math.round(memory.jsHeapSizeLimit / (1024 * 1024))}MB`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
stopMonitoring: function() {
|
||||||
|
if (this.monitorInterval) {
|
||||||
|
clearInterval(this.monitorInterval);
|
||||||
|
this.monitorInterval = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// CONFIG
|
// CONFIG
|
||||||
const config = {
|
const config = {
|
||||||
apiKeys: getAPIKeys(),
|
apiKeys: getAPIKeys(),
|
||||||
|
@ -393,49 +518,138 @@ const rateLimiter = {
|
||||||
|
|
||||||
// CACHE
|
// CACHE
|
||||||
const cache = {
|
const cache = {
|
||||||
set: (key, value, customTtl = null) => {
|
maxSizeBytes: 10 * 1024 * 1024,
|
||||||
|
maxItems: 200,
|
||||||
|
cacheTTL: 5 * 60 * 1000,
|
||||||
|
|
||||||
|
set: function(key, value, customTtl = null) {
|
||||||
|
this.cleanup();
|
||||||
|
|
||||||
const item = {
|
const item = {
|
||||||
value: value,
|
value: value,
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
expiresAt: Date.now() + (customTtl || app.cacheTTL)
|
expiresAt: Date.now() + (customTtl || this.cacheTTL)
|
||||||
};
|
};
|
||||||
localStorage.setItem(key, JSON.stringify(item));
|
|
||||||
//console.log(`Cache set for ${key}, expires in ${(customTtl || app.cacheTTL) / 1000} seconds`);
|
try {
|
||||||
|
const serialized = JSON.stringify(item);
|
||||||
|
localStorage.setItem(key, serialized);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Cache set error:', e);
|
||||||
|
this.clear();
|
||||||
|
try {
|
||||||
|
const serialized = JSON.stringify(item);
|
||||||
|
localStorage.setItem(key, serialized);
|
||||||
|
} catch (e2) {
|
||||||
|
console.error('Failed to store in cache even after cleanup:', e2);
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
get: (key) => {
|
|
||||||
|
get: function(key) {
|
||||||
const itemStr = localStorage.getItem(key);
|
const itemStr = localStorage.getItem(key);
|
||||||
if (!itemStr) {
|
if (!itemStr) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const item = JSON.parse(itemStr);
|
const item = JSON.parse(itemStr);
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|
||||||
if (now < item.expiresAt) {
|
if (now < item.expiresAt) {
|
||||||
//console.log(`Cache hit for ${key}, ${(item.expiresAt - now) / 1000} seconds remaining`);
|
|
||||||
return {
|
return {
|
||||||
value: item.value,
|
value: item.value,
|
||||||
remainingTime: item.expiresAt - now
|
remainingTime: item.expiresAt - now
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
//console.log(`Cache expired for ${key}`);
|
|
||||||
localStorage.removeItem(key);
|
localStorage.removeItem(key);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
//console.error('Error parsing cache item:', error.message);
|
console.error('Error parsing cache item:', error.message);
|
||||||
localStorage.removeItem(key);
|
localStorage.removeItem(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
isValid: (key) => {
|
|
||||||
return cache.get(key) !== null;
|
isValid: function(key) {
|
||||||
|
return this.get(key) !== null;
|
||||||
},
|
},
|
||||||
clear: () => {
|
|
||||||
Object.keys(localStorage).forEach(key => {
|
clear: function() {
|
||||||
|
const keysToRemove = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < localStorage.length; i++) {
|
||||||
|
const key = localStorage.key(i);
|
||||||
if (key.startsWith('coinData_') || key.startsWith('chartData_') || key === 'coinGeckoOneLiner') {
|
if (key.startsWith('coinData_') || key.startsWith('chartData_') || key === 'coinGeckoOneLiner') {
|
||||||
localStorage.removeItem(key);
|
keysToRemove.push(key);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
keysToRemove.forEach(key => {
|
||||||
|
localStorage.removeItem(key);
|
||||||
});
|
});
|
||||||
//console.log('Cache cleared');
|
|
||||||
|
console.log(`Cache cleared: removed ${keysToRemove.length} items`);
|
||||||
|
},
|
||||||
|
|
||||||
|
cleanup: function() {
|
||||||
|
let totalSize = 0;
|
||||||
|
const items = [];
|
||||||
|
const keysToRemove = [];
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
for (let i = 0; i < localStorage.length; i++) {
|
||||||
|
const key = localStorage.key(i);
|
||||||
|
if (key.startsWith('coinData_') || key.startsWith('chartData_') || key === 'coinGeckoOneLiner') {
|
||||||
|
try {
|
||||||
|
const value = localStorage.getItem(key);
|
||||||
|
const size = new Blob([value]).size;
|
||||||
|
|
||||||
|
const item = JSON.parse(value);
|
||||||
|
|
||||||
|
if (item.expiresAt && item.expiresAt < now) {
|
||||||
|
keysToRemove.push(key);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
totalSize += size;
|
||||||
|
items.push({
|
||||||
|
key,
|
||||||
|
size,
|
||||||
|
timestamp: item.timestamp || 0,
|
||||||
|
expiresAt: item.expiresAt || 0
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
keysToRemove.push(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
keysToRemove.forEach(key => {
|
||||||
|
localStorage.removeItem(key);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (totalSize > this.maxSizeBytes || items.length > this.maxItems) {
|
||||||
|
items.sort((a, b) => a.timestamp - b.timestamp);
|
||||||
|
|
||||||
|
const itemsToRemove = Math.max(
|
||||||
|
Math.ceil(items.length * 0.2),
|
||||||
|
items.length - this.maxItems
|
||||||
|
);
|
||||||
|
|
||||||
|
items.slice(0, itemsToRemove).forEach(item => {
|
||||||
|
localStorage.removeItem(item.key);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Cache cleanup: removed ${itemsToRemove} items, freed ${Math.round((totalSize - this.maxSizeBytes) / 1024)}KB`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
totalSize,
|
||||||
|
itemCount: items.length,
|
||||||
|
removedCount: keysToRemove.length
|
||||||
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -631,6 +845,8 @@ const chartModule = {
|
||||||
chart: null,
|
chart: null,
|
||||||
currentCoin: 'BTC',
|
currentCoin: 'BTC',
|
||||||
loadStartTime: 0,
|
loadStartTime: 0,
|
||||||
|
chartRefs: new WeakMap(),
|
||||||
|
|
||||||
verticalLinePlugin: {
|
verticalLinePlugin: {
|
||||||
id: 'verticalLine',
|
id: 'verticalLine',
|
||||||
beforeDraw: (chart, args, options) => {
|
beforeDraw: (chart, args, options) => {
|
||||||
|
@ -652,15 +868,44 @@ const chartModule = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
initChart: () => {
|
getChartByElement: function(element) {
|
||||||
const ctx = document.getElementById('coin-chart')?.getContext('2d');
|
return this.chartRefs.get(element);
|
||||||
|
},
|
||||||
|
|
||||||
|
setChartReference: function(element, chart) {
|
||||||
|
this.chartRefs.set(element, chart);
|
||||||
|
},
|
||||||
|
|
||||||
|
destroyChart: function() {
|
||||||
|
if (chartModule.chart) {
|
||||||
|
try {
|
||||||
|
chartModule.chart.destroy();
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error destroying chart:', e);
|
||||||
|
}
|
||||||
|
chartModule.chart = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
initChart: function() {
|
||||||
|
this.destroyChart();
|
||||||
|
|
||||||
|
const canvas = document.getElementById('coin-chart');
|
||||||
|
if (!canvas) {
|
||||||
|
logger.error('Chart canvas element not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
if (!ctx) {
|
if (!ctx) {
|
||||||
logger.error('Failed to get chart context. Make sure the canvas element exists.');
|
logger.error('Failed to get chart context. Make sure the canvas element exists.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const gradient = ctx.createLinearGradient(0, 0, 0, 400);
|
const gradient = ctx.createLinearGradient(0, 0, 0, 400);
|
||||||
gradient.addColorStop(0, 'rgba(77, 132, 240, 0.2)');
|
gradient.addColorStop(0, 'rgba(77, 132, 240, 0.2)');
|
||||||
gradient.addColorStop(1, 'rgba(77, 132, 240, 0)');
|
gradient.addColorStop(1, 'rgba(77, 132, 240, 0)');
|
||||||
|
|
||||||
chartModule.chart = new Chart(ctx, {
|
chartModule.chart = new Chart(ctx, {
|
||||||
type: 'line',
|
type: 'line',
|
||||||
data: {
|
data: {
|
||||||
|
@ -811,11 +1056,15 @@ const chartModule = {
|
||||||
},
|
},
|
||||||
plugins: [chartModule.verticalLinePlugin]
|
plugins: [chartModule.verticalLinePlugin]
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.setChartReference(canvas, chartModule.chart);
|
||||||
},
|
},
|
||||||
prepareChartData: (coinSymbol, data) => {
|
|
||||||
|
prepareChartData: function(coinSymbol, data) {
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let preparedData;
|
let preparedData;
|
||||||
|
|
||||||
|
@ -825,9 +1074,11 @@ const chartModule = {
|
||||||
const endUnix = endTime.getTime();
|
const endUnix = endTime.getTime();
|
||||||
const startUnix = endUnix - (24 * 3600000);
|
const startUnix = endUnix - (24 * 3600000);
|
||||||
const hourlyPoints = [];
|
const hourlyPoints = [];
|
||||||
|
|
||||||
for (let hourUnix = startUnix; hourUnix <= endUnix; hourUnix += 3600000) {
|
for (let hourUnix = startUnix; hourUnix <= endUnix; hourUnix += 3600000) {
|
||||||
const targetHour = new Date(hourUnix);
|
const targetHour = new Date(hourUnix);
|
||||||
targetHour.setUTCMinutes(0, 0, 0);
|
targetHour.setUTCMinutes(0, 0, 0);
|
||||||
|
|
||||||
const closestPoint = data.reduce((prev, curr) => {
|
const closestPoint = data.reduce((prev, curr) => {
|
||||||
const prevTime = new Date(prev[0]);
|
const prevTime = new Date(prev[0]);
|
||||||
const currTime = new Date(curr[0]);
|
const currTime = new Date(curr[0]);
|
||||||
|
@ -868,6 +1119,7 @@ const chartModule = {
|
||||||
y: price
|
y: price
|
||||||
}));
|
}));
|
||||||
} else {
|
} else {
|
||||||
|
console.warn('Unknown data format for chartData:', data);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
return preparedData.map(point => ({
|
return preparedData.map(point => ({
|
||||||
|
@ -880,7 +1132,7 @@ const chartModule = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
ensureHourlyData: (data) => {
|
ensureHourlyData: function(data) {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
now.setUTCMinutes(0, 0, 0);
|
now.setUTCMinutes(0, 0, 0);
|
||||||
const twentyFourHoursAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
const twentyFourHoursAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
||||||
|
@ -888,136 +1140,176 @@ const chartModule = {
|
||||||
|
|
||||||
for (let i = 0; i < 24; i++) {
|
for (let i = 0; i < 24; i++) {
|
||||||
const targetTime = new Date(twentyFourHoursAgo.getTime() + i * 60 * 60 * 1000);
|
const targetTime = new Date(twentyFourHoursAgo.getTime() + i * 60 * 60 * 1000);
|
||||||
const closestDataPoint = data.reduce((prev, curr) =>
|
|
||||||
Math.abs(new Date(curr.x).getTime() - targetTime.getTime()) <
|
if (data.length > 0) {
|
||||||
Math.abs(new Date(prev.x).getTime() - targetTime.getTime()) ? curr : prev
|
const closestDataPoint = data.reduce((prev, curr) =>
|
||||||
);
|
Math.abs(new Date(curr.x).getTime() - targetTime.getTime()) <
|
||||||
hourlyData.push({
|
Math.abs(new Date(prev.x).getTime() - targetTime.getTime()) ? curr : prev
|
||||||
x: targetTime.getTime(),
|
);
|
||||||
y: closestDataPoint.y
|
hourlyData.push({
|
||||||
});
|
x: targetTime.getTime(),
|
||||||
|
y: closestDataPoint.y
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return hourlyData;
|
return hourlyData;
|
||||||
},
|
},
|
||||||
|
|
||||||
updateChart: async (coinSymbol, forceRefresh = false) => {
|
updateChart: async function(coinSymbol, forceRefresh = false) {
|
||||||
|
try {
|
||||||
|
if (!chartModule.chart) {
|
||||||
|
chartModule.initChart();
|
||||||
|
}
|
||||||
|
const currentChartData = chartModule.chart?.data?.datasets[0]?.data || [];
|
||||||
|
if (currentChartData.length === 0) {
|
||||||
|
chartModule.showChartLoader();
|
||||||
|
}
|
||||||
|
chartModule.loadStartTime = Date.now();
|
||||||
|
const cacheKey = `chartData_${coinSymbol}_${config.currentResolution}`;
|
||||||
|
let cachedData = !forceRefresh ? cache.get(cacheKey) : null;
|
||||||
|
let data;
|
||||||
|
if (cachedData && Object.keys(cachedData.value).length > 0) {
|
||||||
|
data = cachedData.value;
|
||||||
|
} else {
|
||||||
try {
|
try {
|
||||||
const currentChartData = chartModule.chart?.data.datasets[0].data || [];
|
const allData = await api.fetchHistoricalDataXHR([coinSymbol]);
|
||||||
if (currentChartData.length === 0) {
|
data = allData[coinSymbol];
|
||||||
chartModule.showChartLoader();
|
|
||||||
}
|
if (!data || Object.keys(data).length === 0) {
|
||||||
chartModule.loadStartTime = Date.now();
|
throw new Error(`No data returned for ${coinSymbol}`);
|
||||||
const cacheKey = `chartData_${coinSymbol}_${config.currentResolution}`;
|
}
|
||||||
let cachedData = !forceRefresh ? cache.get(cacheKey) : null;
|
|
||||||
let data;
|
|
||||||
if (cachedData && Object.keys(cachedData.value).length > 0) {
|
|
||||||
data = cachedData.value;
|
|
||||||
//console.log(`Using cached data for ${coinSymbol}`);
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
const allData = await api.fetchHistoricalDataXHR([coinSymbol]);
|
|
||||||
data = allData[coinSymbol];
|
|
||||||
if (!data || Object.keys(data).length === 0) {
|
|
||||||
throw new Error(`No data returned for ${coinSymbol}`);
|
|
||||||
}
|
|
||||||
cache.set(cacheKey, data, config.cacheTTL);
|
|
||||||
} catch (error) {
|
|
||||||
if (error.message.includes('429') && currentChartData.length > 0) {
|
|
||||||
//console.warn(`Rate limit hit for ${coinSymbol}, maintaining current chart`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const expiredCache = localStorage.getItem(cacheKey);
|
|
||||||
if (expiredCache) {
|
|
||||||
try {
|
|
||||||
const parsedCache = JSON.parse(expiredCache);
|
|
||||||
data = parsedCache.value;
|
|
||||||
//console.log(`Using expired cache data for ${coinSymbol}`);
|
|
||||||
} catch (cacheError) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const chartData = chartModule.prepareChartData(coinSymbol, data);
|
|
||||||
if (chartData.length > 0 && chartModule.chart) {
|
|
||||||
chartModule.chart.data.datasets[0].data = chartData;
|
|
||||||
chartModule.chart.data.datasets[0].label = `${coinSymbol} Price (USD)`;
|
|
||||||
if (coinSymbol === 'WOW') {
|
|
||||||
chartModule.chart.options.scales.x.time.unit = 'hour';
|
|
||||||
} else {
|
|
||||||
const resolution = config.resolutions[config.currentResolution];
|
|
||||||
chartModule.chart.options.scales.x.time.unit =
|
|
||||||
resolution.interval === 'hourly' ? 'hour' :
|
|
||||||
config.currentResolution === 'year' ? 'month' : 'day';
|
|
||||||
}
|
|
||||||
chartModule.chart.update('active');
|
|
||||||
chartModule.currentCoin = coinSymbol;
|
|
||||||
const loadTime = Date.now() - chartModule.loadStartTime;
|
|
||||||
ui.updateLoadTimeAndCache(loadTime, cachedData);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Error updating chart for ${coinSymbol}:`, error);
|
|
||||||
if (!(chartModule.chart?.data.datasets[0].data.length > 0)) {
|
|
||||||
chartModule.chart.data.datasets[0].data = [];
|
|
||||||
chartModule.chart.update('active');
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
chartModule.hideChartLoader();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
showChartLoader: () => {
|
cache.set(cacheKey, data, config.cacheTTL);
|
||||||
|
} catch (error) {
|
||||||
|
if (error.message.includes('429') && currentChartData.length > 0) {
|
||||||
|
console.warn(`Rate limit hit for ${coinSymbol}, maintaining current chart`);
|
||||||
|
chartModule.hideChartLoader();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const expiredCache = localStorage.getItem(cacheKey);
|
||||||
|
if (expiredCache) {
|
||||||
|
try {
|
||||||
|
const parsedCache = JSON.parse(expiredCache);
|
||||||
|
data = parsedCache.value;
|
||||||
|
} catch (cacheError) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (chartModule.currentCoin !== coinSymbol) {
|
||||||
|
chartModule.destroyChart();
|
||||||
|
chartModule.initChart();
|
||||||
|
}
|
||||||
|
|
||||||
|
const chartData = chartModule.prepareChartData(coinSymbol, data);
|
||||||
|
if (chartData.length > 0 && chartModule.chart) {
|
||||||
|
chartModule.chart.data.datasets[0].data = chartData;
|
||||||
|
chartModule.chart.data.datasets[0].label = `${coinSymbol} Price (USD)`;
|
||||||
|
if (coinSymbol === 'WOW') {
|
||||||
|
chartModule.chart.options.scales.x.time.unit = 'hour';
|
||||||
|
} else {
|
||||||
|
const resolution = config.resolutions[config.currentResolution];
|
||||||
|
chartModule.chart.options.scales.x.time.unit =
|
||||||
|
resolution.interval === 'hourly' ? 'hour' :
|
||||||
|
config.currentResolution === 'year' ? 'month' : 'day';
|
||||||
|
}
|
||||||
|
chartModule.chart.update('active');
|
||||||
|
chartModule.currentCoin = coinSymbol;
|
||||||
|
const loadTime = Date.now() - chartModule.loadStartTime;
|
||||||
|
ui.updateLoadTimeAndCache(loadTime, cachedData);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error updating chart for ${coinSymbol}:`, error);
|
||||||
|
|
||||||
|
// Keep existing chart data if possible
|
||||||
|
if (!(chartModule.chart?.data?.datasets[0]?.data?.length > 0)) {
|
||||||
|
if (!chartModule.chart) {
|
||||||
|
chartModule.initChart();
|
||||||
|
}
|
||||||
|
if (chartModule.chart) {
|
||||||
|
chartModule.chart.data.datasets[0].data = [];
|
||||||
|
chartModule.chart.update('active');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
chartModule.hideChartLoader();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
showChartLoader: function() {
|
||||||
const loader = document.getElementById('chart-loader');
|
const loader = document.getElementById('chart-loader');
|
||||||
const chart = document.getElementById('coin-chart');
|
const chart = document.getElementById('coin-chart');
|
||||||
if (!loader || !chart) {
|
if (!loader || !chart) {
|
||||||
//console.warn('Chart loader or chart container elements not found');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
loader.classList.remove('hidden');
|
loader.classList.remove('hidden');
|
||||||
chart.classList.add('hidden');
|
chart.classList.add('hidden');
|
||||||
},
|
},
|
||||||
|
|
||||||
hideChartLoader: () => {
|
hideChartLoader: function() {
|
||||||
const loader = document.getElementById('chart-loader');
|
const loader = document.getElementById('chart-loader');
|
||||||
const chart = document.getElementById('coin-chart');
|
const chart = document.getElementById('coin-chart');
|
||||||
if (!loader || !chart) {
|
if (!loader || !chart) {
|
||||||
//console.warn('Chart loader or chart container elements not found');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
loader.classList.add('hidden');
|
loader.classList.add('hidden');
|
||||||
chart.classList.remove('hidden');
|
chart.classList.remove('hidden');
|
||||||
},
|
},
|
||||||
|
cleanup: function() {
|
||||||
|
this.destroyChart();
|
||||||
|
this.currentCoin = null;
|
||||||
|
this.loadStartTime = 0;
|
||||||
|
console.log('Chart module cleaned up');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Chart.register(chartModule.verticalLinePlugin);
|
Chart.register(chartModule.verticalLinePlugin);
|
||||||
|
|
||||||
const volumeToggle = {
|
const volumeToggle = {
|
||||||
isVisible: localStorage.getItem('volumeToggleState') === 'true',
|
isVisible: localStorage.getItem('volumeToggleState') === 'true',
|
||||||
init: () => {
|
init: function() {
|
||||||
const toggleButton = document.getElementById('toggle-volume');
|
const toggleButton = document.getElementById('toggle-volume');
|
||||||
if (toggleButton) {
|
if (toggleButton) {
|
||||||
|
if (typeof cleanupManager !== 'undefined') {
|
||||||
|
cleanupManager.addListener(toggleButton, 'click', volumeToggle.toggle);
|
||||||
|
} else {
|
||||||
toggleButton.addEventListener('click', volumeToggle.toggle);
|
toggleButton.addEventListener('click', volumeToggle.toggle);
|
||||||
volumeToggle.updateVolumeDisplay();
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
toggle: () => {
|
|
||||||
volumeToggle.isVisible = !volumeToggle.isVisible;
|
|
||||||
localStorage.setItem('volumeToggleState', volumeToggle.isVisible.toString());
|
|
||||||
volumeToggle.updateVolumeDisplay();
|
volumeToggle.updateVolumeDisplay();
|
||||||
},
|
|
||||||
updateVolumeDisplay: () => {
|
|
||||||
const volumeDivs = document.querySelectorAll('[id$="-volume-div"]');
|
|
||||||
volumeDivs.forEach(div => {
|
|
||||||
div.style.display = volumeToggle.isVisible ? 'flex' : 'none';
|
|
||||||
});
|
|
||||||
const toggleButton = document.getElementById('toggle-volume');
|
|
||||||
if (toggleButton) {
|
|
||||||
updateButtonStyles(toggleButton, volumeToggle.isVisible, 'green');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
},
|
||||||
|
|
||||||
|
toggle: function() {
|
||||||
|
volumeToggle.isVisible = !volumeToggle.isVisible;
|
||||||
|
localStorage.setItem('volumeToggleState', volumeToggle.isVisible.toString());
|
||||||
|
volumeToggle.updateVolumeDisplay();
|
||||||
|
},
|
||||||
|
|
||||||
|
updateVolumeDisplay: function() {
|
||||||
|
const volumeDivs = document.querySelectorAll('[id$="-volume-div"]');
|
||||||
|
volumeDivs.forEach(div => {
|
||||||
|
if (div) {
|
||||||
|
div.style.display = volumeToggle.isVisible ? 'flex' : 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const toggleButton = document.getElementById('toggle-volume');
|
||||||
|
if (toggleButton) {
|
||||||
|
updateButtonStyles(toggleButton, volumeToggle.isVisible, 'green');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
cleanup: function() {
|
||||||
|
const toggleButton = document.getElementById('toggle-volume');
|
||||||
|
if (toggleButton) {
|
||||||
|
toggleButton.removeEventListener('click', volumeToggle.toggle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
function updateButtonStyles(button, isActive, color) {
|
function updateButtonStyles(button, isActive, color) {
|
||||||
button.classList.toggle('text-' + color + '-500', isActive);
|
button.classList.toggle('text-' + color + '-500', isActive);
|
||||||
|
@ -1689,5 +1981,48 @@ resolutionButtons.forEach(button => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// LOAD
|
||||||
|
const appCleanup = {
|
||||||
|
init: function() {
|
||||||
|
memoryMonitor.startMonitoring();
|
||||||
|
window.addEventListener('beforeunload', this.globalCleanup);
|
||||||
|
},
|
||||||
|
|
||||||
|
globalCleanup: function() {
|
||||||
|
try {
|
||||||
|
if (app.autoRefreshInterval) {
|
||||||
|
clearTimeout(app.autoRefreshInterval);
|
||||||
|
}
|
||||||
|
if (chartModule) {
|
||||||
|
chartModule.cleanup();
|
||||||
|
}
|
||||||
|
if (volumeToggle) {
|
||||||
|
volumeToggle.cleanup();
|
||||||
|
}
|
||||||
|
cleanupManager.clearAll();
|
||||||
|
memoryMonitor.stopMonitoring();
|
||||||
|
cache.clear();
|
||||||
|
|
||||||
|
console.log('Global application cleanup completed');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error during global cleanup:', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
manualCleanup: function() {
|
||||||
|
this.globalCleanup();
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
app.init = () => {
|
||||||
|
//console.log('Init');
|
||||||
|
window.addEventListener('load', app.onLoad);
|
||||||
|
appCleanup.init();
|
||||||
|
app.loadLastRefreshedTime();
|
||||||
|
app.updateAutoRefreshButton();
|
||||||
|
memoryMonitor.startMonitoring();
|
||||||
|
//console.log('App initialized');
|
||||||
|
};
|
||||||
|
|
||||||
// LOAD
|
// LOAD
|
||||||
app.init();
|
app.init();
|
||||||
|
|
Loading…
Reference in a new issue