Merge pull request from gerlofvanek/cleanup-1

JS: Cleanup + Fixes
This commit is contained in:
tecnovert 2025-01-17 10:46:07 +00:00 committed by GitHub
commit fe02441619
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 597 additions and 612 deletions
basicswap/static/js

View file

@ -449,33 +449,49 @@ const CacheManager = {
try { try {
this.cleanup(); this.cleanup();
if (!value) {
console.warn('Attempted to cache null/undefined value for key:', key);
return false;
}
const item = { const item = {
value: value, value: value,
timestamp: Date.now(), timestamp: Date.now(),
expiresAt: Date.now() + (customTtl || CACHE_DURATION) expiresAt: Date.now() + (customTtl || CACHE_DURATION)
}; };
const itemSize = new Blob([JSON.stringify(item)]).size; try {
if (itemSize > this.maxSize) { JSON.stringify(item);
//console.error(`Cache item exceeds maximum size (${(itemSize/1024/1024).toFixed(2)}MB)`); } catch (e) {
console.error('Failed to serialize cache item:', e);
return false; return false;
} }
localStorage.setItem(key, JSON.stringify(item)); const itemSize = new Blob([JSON.stringify(item)]).size;
return true; if (itemSize > this.maxSize) {
console.warn(`Cache item exceeds maximum size (${(itemSize/1024/1024).toFixed(2)}MB)`);
return false;
}
try {
localStorage.setItem(key, JSON.stringify(item));
return true;
} catch (storageError) {
if (storageError.name === 'QuotaExceededError') {
this.cleanup(true);
try {
localStorage.setItem(key, JSON.stringify(item));
return true;
} catch (retryError) {
console.error('Storage quota exceeded even after cleanup:', retryError);
return false;
}
}
throw storageError;
}
} catch (error) { } catch (error) {
if (error.name === 'QuotaExceededError') { console.error('Cache set error:', error);
this.cleanup(true); // Aggressive cleanup
try {
localStorage.setItem(key, JSON.stringify(item));
return true;
} catch (error) {
console.error('Storage quota exceeded even after cleanup:', error.message);
return false;
}
}
//console.error('Cache set error:', error);
return false; return false;
} }
}, },
@ -483,11 +499,26 @@ const CacheManager = {
get: function(key) { get: function(key) {
try { try {
const itemStr = localStorage.getItem(key); const itemStr = localStorage.getItem(key);
if (!itemStr) return null; if (!itemStr) {
return null;
}
let item;
try {
item = JSON.parse(itemStr);
} catch (parseError) {
console.error('Failed to parse cached item:', parseError);
localStorage.removeItem(key);
return null;
}
if (!item || typeof item.expiresAt !== 'number' || !item.hasOwnProperty('value')) {
console.warn('Invalid cache item structure for key:', key);
localStorage.removeItem(key);
return null;
}
const item = JSON.parse(itemStr);
const now = Date.now(); const now = Date.now();
if (now < item.expiresAt) { if (now < item.expiresAt) {
return { return {
value: item.value, value: item.value,
@ -496,11 +527,17 @@ const CacheManager = {
} }
localStorage.removeItem(key); localStorage.removeItem(key);
return null;
} catch (error) { } catch (error) {
console.error("An error occured:", error.message); console.error("Cache retrieval error:", error);
localStorage.removeItem(key); try {
localStorage.removeItem(key);
} catch (removeError) {
console.error("Failed to remove invalid cache entry:", removeError);
}
return null;
} }
return null;
}, },
cleanup: function(aggressive = false) { cleanup: function(aggressive = false) {
@ -533,7 +570,7 @@ const CacheManager = {
totalSize += size; totalSize += size;
itemCount++; itemCount++;
} catch (error) { } catch (error) {
console.error("An error occured:", error.message); console.error("Error processing cache item:", error);
localStorage.removeItem(key); localStorage.removeItem(key);
} }
} }
@ -543,11 +580,21 @@ const CacheManager = {
while ((totalSize > this.maxSize || itemCount > this.maxItems) && items.length > 0) { while ((totalSize > this.maxSize || itemCount > this.maxItems) && items.length > 0) {
const item = items.pop(); const item = items.pop();
localStorage.removeItem(item.key); try {
totalSize -= item.size; localStorage.removeItem(item.key);
itemCount--; totalSize -= item.size;
itemCount--;
} catch (error) {
console.error("Error removing cache item:", error);
}
} }
} }
return {
totalSize,
itemCount,
cleaned: items.length
};
}, },
clear: function() { clear: function() {
@ -559,7 +606,13 @@ const CacheManager = {
} }
} }
keys.forEach(key => localStorage.removeItem(key)); keys.forEach(key => {
try {
localStorage.removeItem(key);
} catch (error) {
console.error("Error clearing cache item:", error);
}
});
}, },
getStats: function() { getStats: function() {
@ -584,7 +637,7 @@ const CacheManager = {
expiredCount++; expiredCount++;
} }
} catch (error) { } catch (error) {
console.error("An error occured:", error.message); console.error("Error getting cache stats:", error);
} }
} }
@ -597,6 +650,8 @@ const CacheManager = {
} }
}; };
window.CacheManager = CacheManager;
// Identity cache management // Identity cache management
const IdentityManager = { const IdentityManager = {
cache: new Map(), cache: new Map(),
@ -939,15 +994,44 @@ function filterAndSortData() {
comparison = a.offer_id.localeCompare(b.offer_id); comparison = a.offer_id.localeCompare(b.offer_id);
break; break;
} }
return currentSortDirection === 'desc' ? -comparison : comparison; return currentSortDirection === 'desc' ? -comparison : comparison;
}); });
} }
//console.log(`[Debug] Filtered data length: ${filteredData.length}`); //console.log(`[Debug] Filtered data length: ${filteredData.length}`);
return filteredData; return filteredData;
} }
function getPriceWithFallback(coin, latestPrices) {
const getPriceKey = (coin) => {
const lowerCoin = coin.toLowerCase();
if (lowerCoin === 'firo' || lowerCoin === 'zcoin') {
return 'zcoin';
}
if (lowerCoin === 'bitcoin cash') {
return 'bitcoin-cash';
}
if (lowerCoin === 'particl anon' || lowerCoin === 'particl blind') {
return 'particl';
}
return coinNameToSymbol[coin] || lowerCoin;
};
const priceKey = getPriceKey(coin);
const livePrice = latestPrices[priceKey]?.usd;
if (livePrice !== undefined && livePrice !== null) {
return livePrice;
}
if (window.tableRateModule) {
const fallback = window.tableRateModule.getFallbackValue(priceKey);
if (fallback !== null) {
return fallback;
}
}
return null;
}
async function calculateProfitLoss(fromCoin, toCoin, fromAmount, toAmount, isOwnOffer) { async function calculateProfitLoss(fromCoin, toCoin, fromAmount, toAmount, isOwnOffer) {
return new Promise((resolve) => { return new Promise((resolve) => {
if (!latestPrices) { if (!latestPrices) {
@ -972,26 +1056,33 @@ async function calculateProfitLoss(fromCoin, toCoin, fromAmount, toAmount, isOwn
const fromSymbol = getPriceKey(fromCoin); const fromSymbol = getPriceKey(fromCoin);
const toSymbol = getPriceKey(toCoin); const toSymbol = getPriceKey(toCoin);
let fromPriceUSD = latestPrices[fromSymbol]?.usd;
let toPriceUSD = latestPrices[toSymbol]?.usd;
const fromPriceUSD = latestPrices[fromSymbol]?.usd; if (!fromPriceUSD || !toPriceUSD) {
const toPriceUSD = latestPrices[toSymbol]?.usd; fromPriceUSD = tableRateModule.getFallbackValue(fromSymbol);
toPriceUSD = tableRateModule.getFallbackValue(toSymbol);
if (fromPriceUSD === null || toPriceUSD === null || }
fromPriceUSD === undefined || toPriceUSD === undefined) { if (!fromPriceUSD || !toPriceUSD || isNaN(fromPriceUSD) || isNaN(toPriceUSD)) {
resolve(null); resolve(null);
return; return;
} }
const fromValueUSD = fromAmount * fromPriceUSD; const fromValueUSD = fromAmount * fromPriceUSD;
const toValueUSD = toAmount * toPriceUSD; const toValueUSD = toAmount * toPriceUSD;
if (isNaN(fromValueUSD) || isNaN(toValueUSD) || fromValueUSD === 0 || toValueUSD === 0) {
resolve(null);
return;
}
let percentDiff; let percentDiff;
if (isOwnOffer) { if (isOwnOffer) {
percentDiff = ((toValueUSD / fromValueUSD) - 1) * 100; percentDiff = ((toValueUSD / fromValueUSD) - 1) * 100;
} else { } else {
percentDiff = ((fromValueUSD / toValueUSD) - 1) * 100; percentDiff = ((fromValueUSD / toValueUSD) - 1) * 100;
} }
if (isNaN(percentDiff)) {
resolve(null);
return;
}
resolve(percentDiff); resolve(percentDiff);
}); });
} }
@ -1015,94 +1106,75 @@ function getEmptyPriceData() {
async function fetchLatestPrices() { async function fetchLatestPrices() {
const PRICES_CACHE_KEY = 'prices_coingecko'; const PRICES_CACHE_KEY = 'prices_coingecko';
const RETRY_DELAY = 5000;
const MAX_RETRIES = 3;
const cachedData = CacheManager.get(PRICES_CACHE_KEY); if (!window.isManualRefresh) {
if (cachedData && cachedData.remainingTime > 30000) { const cachedData = CacheManager.get(PRICES_CACHE_KEY);
console.log('Using cached price data'); if (cachedData && cachedData.remainingTime > 60000) {
latestPrices = cachedData.value; console.log('Using cached price data');
return cachedData.value; latestPrices = cachedData.value;
Object.entries(cachedData.value).forEach(([coin, prices]) => {
if (prices.usd) {
tableRateModule.setFallbackValue(coin, prices.usd);
}
});
return cachedData.value;
}
} }
const url = `${offersConfig.apiEndpoints.coinGecko}/simple/price?ids=bitcoin,bitcoin-cash,dash,dogecoin,decred,litecoin,particl,pivx,monero,zano,wownero,zcoin&vs_currencies=USD,BTC&api_key=${offersConfig.apiKeys.coinGecko}`;
const baseUrl = `${offersConfig.apiEndpoints.coinGecko}/simple/price?ids=bitcoin,bitcoin-cash,dash,dogecoin,decred,litecoin,particl,pivx,monero,zcoin,zano,wownero&vs_currencies=USD,BTC`; try {
console.log('Initiating fresh price data fetch...');
const response = await fetch('/json/readurl', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
url: url,
headers: {}
})
});
let retryCount = 0; if (!response.ok) {
let data = null; throw new Error(`HTTP Error: ${response.status} ${response.statusText}`);
while (!data && retryCount < MAX_RETRIES) {
if (retryCount > 0) {
const delay = RETRY_DELAY * Math.pow(2, retryCount - 1);
console.log(`Waiting ${delay}ms before retry ${retryCount + 1}...`);
await new Promise(resolve => setTimeout(resolve, delay));
} }
try { const data = await response.json();
console.log('Attempting price fetch with API key...');
const urlWithKey = `${baseUrl}&api_key=${offersConfig.apiKeys.coinGecko}`;
const response = await fetch('/json/readurl', { if (data.Error) {
method: 'POST', console.error('API Error:', data.Error);
headers: { throw new Error(data.Error);
'Content-Type': 'application/json' }
},
body: JSON.stringify({ if (data && Object.keys(data).length > 0) {
url: urlWithKey, console.log('Processing fresh price data...');
headers: {} latestPrices = data;
}) CacheManager.set(PRICES_CACHE_KEY, data, CACHE_DURATION);
Object.entries(data).forEach(([coin, prices]) => {
if (prices.usd) {
tableRateModule.setFallbackValue(coin, prices.usd);
}
}); });
const responseData = await response.json(); return data;
} else {
if (responseData.error) { console.warn('No price data received');
if (responseData.error.includes('429')) { return null;
console.log('Rate limited, retrying...');
} else {
console.warn('Invalid price data received:', responseData);
}
retryCount++;
continue;
}
const hasValidPrices = Object.values(responseData).some(coin =>
coin && typeof coin === 'object' &&
typeof coin.usd === 'number' &&
!isNaN(coin.usd)
);
if (!hasValidPrices) {
console.warn('No valid price data found in response');
retryCount++;
continue;
}
data = responseData;
break;
} catch (error) {
console.warn('Error fetching prices:', error);
retryCount++;
} }
} catch (error) {
console.error('Price Fetch Error:', error);
const fallbackPrices = {};
Object.keys(getEmptyPriceData()).forEach(coin => {
const fallbackValue = tableRateModule.getFallbackValue(coin);
if (fallbackValue !== null) {
fallbackPrices[coin] = { usd: fallbackValue, btc: null };
}
});
return Object.keys(fallbackPrices).length > 0 ? fallbackPrices : null;
} finally {
window.isManualRefresh = false;
} }
if (!data) {
console.warn('All price fetch attempts failed, using empty price data');
const naData = getEmptyPriceData();
latestPrices = naData;
return naData;
}
console.log('Successfully fetched fresh price data');
latestPrices = data;
CacheManager.set(PRICES_CACHE_KEY, data, CACHE_DURATION);
Object.entries(data).forEach(([coin, prices]) => {
if (prices && typeof prices.usd === 'number' && !isNaN(prices.usd)) {
tableRateModule.setFallbackValue(coin, prices.usd);
}
});
return data;
} }
async function fetchOffers() { async function fetchOffers() {
@ -1275,20 +1347,18 @@ function updatePaginationControls(totalPages) {
function updateProfitLoss(row, fromCoin, toCoin, fromAmount, toAmount, isOwnOffer) { function updateProfitLoss(row, fromCoin, toCoin, fromAmount, toAmount, isOwnOffer) {
const profitLossElement = row.querySelector('.profit-loss'); const profitLossElement = row.querySelector('.profit-loss');
if (!profitLossElement) { if (!profitLossElement) {
//console.warn('Profit loss element not found in row');
return; return;
} }
if (!fromCoin || !toCoin) { if (!fromCoin || !toCoin) {
//console.error(`Invalid coin names: fromCoin=${fromCoin}, toCoin=${toCoin}`); profitLossElement.textContent = 'N/A';
profitLossElement.textContent = 'Error'; profitLossElement.className = 'profit-loss text-lg font-bold text-gray-300';
profitLossElement.className = 'profit-loss text-lg font-bold text-red-500';
return; return;
} }
calculateProfitLoss(fromCoin, toCoin, fromAmount, toAmount, isOwnOffer) calculateProfitLoss(fromCoin, toCoin, fromAmount, toAmount, isOwnOffer)
.then(percentDiff => { .then(percentDiff => {
if (percentDiff === null) { if (percentDiff === null || isNaN(percentDiff)) {
profitLossElement.textContent = 'N/A'; profitLossElement.textContent = 'N/A';
profitLossElement.className = 'profit-loss text-lg font-bold text-gray-300'; profitLossElement.className = 'profit-loss text-lg font-bold text-gray-300';
return; return;
@ -1302,6 +1372,7 @@ function updateProfitLoss(row, fromCoin, toCoin, fromAmount, toAmount, isOwnOffe
profitLossElement.textContent = `${percentDiffDisplay}%`; profitLossElement.textContent = `${percentDiffDisplay}%`;
profitLossElement.className = `profit-loss text-lg font-bold ${colorClass}`; profitLossElement.className = `profit-loss text-lg font-bold ${colorClass}`;
// Update tooltip if it exists
const tooltipId = `percentage-tooltip-${row.getAttribute('data-offer-id')}`; const tooltipId = `percentage-tooltip-${row.getAttribute('data-offer-id')}`;
const tooltipElement = document.getElementById(tooltipId); const tooltipElement = document.getElementById(tooltipId);
if (tooltipElement) { if (tooltipElement) {
@ -1316,8 +1387,8 @@ function updateProfitLoss(row, fromCoin, toCoin, fromAmount, toAmount, isOwnOffe
}) })
.catch(error => { .catch(error => {
console.error('Error in updateProfitLoss:', error); console.error('Error in updateProfitLoss:', error);
profitLossElement.textContent = 'Error'; profitLossElement.textContent = 'N/A';
profitLossElement.className = 'profit-loss text-lg font-bold text-red-500'; profitLossElement.className = 'profit-loss text-lg font-bold text-gray-300';
}); });
} }
@ -1802,18 +1873,27 @@ function createRateColumn(offer, coinFrom, coinTo) {
if (lowerCoin === 'bitcoin cash') { if (lowerCoin === 'bitcoin cash') {
return 'bitcoin-cash'; return 'bitcoin-cash';
} }
if (lowerCoin === 'particl anon' || lowerCoin === 'particl blind') {
return 'particl';
}
return coinNameToSymbol[coin] || lowerCoin; return coinNameToSymbol[coin] || lowerCoin;
}; };
const toPriceUSD = latestPrices[getPriceKey(coinTo)]?.usd || 0; const toSymbolKey = getPriceKey(coinTo);
const rateInUSD = rate * toPriceUSD; let toPriceUSD = latestPrices[toSymbolKey]?.usd;
if (!toPriceUSD || isNaN(toPriceUSD)) {
toPriceUSD = tableRateModule.getFallbackValue(toSymbolKey);
}
const rateInUSD = toPriceUSD && !isNaN(toPriceUSD) && !isNaN(rate) ? rate * toPriceUSD : null;
return ` return `
<td class="py-3 semibold monospace text-xs text-right items-center rate-table-info"> <td class="py-3 semibold monospace text-xs text-right items-center rate-table-info">
<div class="relative"> <div class="relative">
<div class="flex flex-col items-end pr-3" data-tooltip-target="tooltip-rate-${offer.offer_id}"> <div class="flex flex-col items-end pr-3" data-tooltip-target="tooltip-rate-${offer.offer_id}">
<span class="text-sm bold text-gray-700 dark:text-white"> <span class="text-sm bold text-gray-700 dark:text-white">
$${rateInUSD.toFixed(2)} USD ${rateInUSD !== null ? `$${rateInUSD.toFixed(2)} USD` : 'N/A'}
</span> </span>
<span class="bold text-gray-700 dark:text-white"> <span class="bold text-gray-700 dark:text-white">
${rate.toFixed(8)} ${toSymbol}/${fromSymbol} ${rate.toFixed(8)} ${toSymbol}/${fromSymbol}
@ -2443,49 +2523,76 @@ function initializeTableEvents() {
}); });
} }
const refreshButton = document.getElementById('refreshOffers'); const refreshButton = document.getElementById('refreshOffers');
if (refreshButton) { if (refreshButton) {
EventManager.add(refreshButton, 'click', async () => { EventManager.add(refreshButton, 'click', async () => {
console.log('Manual refresh initiated'); console.log('Manual refresh initiated');
const refreshIcon = document.getElementById('refreshIcon'); const refreshIcon = document.getElementById('refreshIcon');
const refreshText = document.getElementById('refreshText'); const refreshText = document.getElementById('refreshText');
refreshButton.disabled = true; refreshButton.disabled = true;
refreshIcon.classList.add('animate-spin'); refreshIcon.classList.add('animate-spin');
refreshText.textContent = 'Refreshing...'; refreshText.textContent = 'Refreshing...';
refreshButton.classList.add('opacity-75', 'cursor-wait'); refreshButton.classList.add('opacity-75', 'cursor-wait');
try { try {
const endpoint = isSentOffers ? '/json/sentoffers' : '/json/offers'; const PRICES_CACHE_KEY = 'prices_coingecko';
const response = await fetch(endpoint); localStorage.removeItem(PRICES_CACHE_KEY);
if (!response.ok) { CacheManager.clear();
throw new Error(`HTTP error! status: ${response.status}`); window.isManualRefresh = true;
} const endpoint = isSentOffers ? '/json/sentoffers' : '/json/offers';
const newData = await response.json(); const response = await fetch(endpoint);
if (!response.ok) {
const processedNewData = Array.isArray(newData) ? newData : Object.values(newData); throw new Error(`HTTP error! status: ${response.status}`);
console.log('Fetched offers:', processedNewData.length);
jsonData = formatInitialData(processedNewData);
originalJsonData = [...jsonData];
await updateOffersTable();
updateJsonView();
updatePaginationInfo();
console.log('Manual refresh completed successfully');
} catch (error) {
console.error('Error during manual refresh:', error);
ui.displayErrorMessage('Failed to refresh offers. Please try again later.');
} finally {
refreshButton.disabled = false;
refreshIcon.classList.remove('animate-spin');
refreshText.textContent = 'Refresh';
refreshButton.classList.remove('opacity-75', 'cursor-wait');
} }
}); const newData = await response.json();
} const processedNewData = Array.isArray(newData) ? newData : Object.values(newData);
console.log('Fetched offers:', processedNewData.length);
jsonData = formatInitialData(processedNewData);
originalJsonData = [...jsonData];
const url = `${offersConfig.apiEndpoints.coinGecko}/simple/price?ids=bitcoin,bitcoin-cash,dash,dogecoin,decred,litecoin,particl,pivx,monero,zano,wownero,zcoin&vs_currencies=USD,BTC&api_key=${offersConfig.apiKeys.coinGecko}`;
console.log('Fetching fresh prices...');
const priceResponse = await fetch('/json/readurl', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url: url, headers: {} })
});
if (priceResponse.ok) {
const priceData = await priceResponse.json();
if (priceData && Object.keys(priceData).length > 0) {
console.log('Updating with fresh price data');
latestPrices = priceData;
CacheManager.set(PRICES_CACHE_KEY, priceData, CACHE_DURATION);
Object.entries(priceData).forEach(([coin, prices]) => {
if (prices.usd) {
tableRateModule.setFallbackValue(coin, prices.usd);
}
});
}
}
await updateOffersTable();
updateJsonView();
updatePaginationInfo();
lastRefreshTime = Date.now();
updateLastRefreshTime();
console.log('Manual refresh completed successfully');
} catch (error) {
console.error('Error during manual refresh:', error);
ui.displayErrorMessage('Failed to refresh offers. Please try again later.');
} finally {
window.isManualRefresh = false;
refreshButton.disabled = false;
refreshIcon.classList.remove('animate-spin');
refreshText.textContent = 'Refresh';
refreshButton.classList.remove('opacity-75', 'cursor-wait');
}
});
}
document.querySelectorAll('th[data-sortable="true"]').forEach(header => { document.querySelectorAll('th[data-sortable="true"]').forEach(header => {
EventManager.add(header, 'click', () => { EventManager.add(header, 'click', () => {
@ -2718,11 +2825,6 @@ async function cleanup() {
cleanupTable(); cleanupTable();
debug.addStep('Table cleanup completed', `Cleaned up ${rowCount} rows`); debug.addStep('Table cleanup completed', `Cleaned up ${rowCount} rows`);
debug.addStep('Starting cache cleanup');
const cacheStats = CacheManager.getStats();
CacheManager.clear();
debug.addStep('Cache cleanup completed', `Cleared ${cacheStats.itemCount} cached items`);
debug.addStep('Resetting global state'); debug.addStep('Resetting global state');
const globals = { const globals = {
currentPage: currentPage, currentPage: currentPage,

File diff suppressed because it is too large Load diff