Merge pull request from gerlofvanek/ws

JS: Fix websocket delay / loading tables faster.
This commit is contained in:
tecnovert 2025-02-17 09:57:17 +00:00 committed by GitHub
commit 8d317e4b67
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -75,6 +75,7 @@ let filterTimeout = null;
// CONFIGURATION CONSTANTS
// TIME CONSTANTS
const CACHE_DURATION = 10 * 60 * 1000;
const wsPort = config.port || window.ws_port || '11700';
// APP CONSTANTS
const itemsPerPage = 50;
@ -206,7 +207,7 @@ const WebSocketManager = {
this.connect();
}
this.startHealthCheck();
}, 1000);
}, 0);
},
startHealthCheck() {
@ -1158,26 +1159,50 @@ async function fetchOffers() {
const refreshText = document.getElementById('refreshText');
try {
refreshButton.disabled = true;
refreshIcon.classList.add('animate-spin');
refreshText.textContent = 'Refreshing...';
refreshButton.classList.add('opacity-75', 'cursor-wait');
if (refreshButton) {
refreshButton.disabled = true;
refreshIcon.classList.add('animate-spin');
refreshText.textContent = 'Refreshing...';
refreshButton.classList.add('opacity-75', 'cursor-wait');
}
const endpoint = isSentOffers ? '/json/sentoffers' : '/json/offers';
const response = await fetch(endpoint);
const data = await response.json();
const [offersResponse, pricesData] = await Promise.all([
fetch(isSentOffers ? '/json/sentoffers' : '/json/offers'),
fetchLatestPrices()
]);
jsonData = formatInitialData(data);
if (!offersResponse.ok) {
throw new Error(`HTTP error! status: ${offersResponse.status}`);
}
const data = await offersResponse.json();
const processedData = Array.isArray(data) ? data : Object.values(data);
jsonData = formatInitialData(processedData);
originalJsonData = [...jsonData];
latestPrices = pricesData || getEmptyPriceData();
await updateOffersTable();
updatePaginationInfo();
} catch (error) {
console.error('[Debug] Error fetching offers:', error);
const cachedOffers = CacheManager.get('offers_cached');
if (cachedOffers?.value) {
jsonData = cachedOffers.value;
originalJsonData = [...jsonData];
await updateOffersTable();
}
ui.displayErrorMessage('Failed to fetch offers. Please try again later.');
} finally {
stopRefreshAnimation();
if (refreshButton) {
refreshButton.disabled = false;
refreshIcon.classList.remove('animate-spin');
refreshText.textContent = 'Refresh';
refreshButton.classList.remove('opacity-75', 'cursor-wait');
}
}
}
@ -1467,78 +1492,50 @@ async function updateOffersTable() {
window.TooltipManager.cleanup();
}
const PRICES_CACHE_KEY = 'prices_coingecko';
const cachedPrices = CacheManager.get(PRICES_CACHE_KEY);
latestPrices = cachedPrices?.value || getEmptyPriceData();
const validOffers = getValidOffers();
const startIndex = (currentPage - 1) * itemsPerPage;
const endIndex = Math.min(startIndex + itemsPerPage, validOffers.length);
const itemsToDisplay = validOffers.slice(startIndex, endIndex);
fetchLatestPrices().then(freshPrices => {
if (freshPrices) {
latestPrices = freshPrices;
updateProfitLossDisplays();
}
}).catch(error => {
console.warn('Price fetch failed:', error);
});
const BATCH_SIZE = 5;
const identities = [];
for (let i = 0; i < itemsToDisplay.length; i += BATCH_SIZE) {
const batch = itemsToDisplay.slice(i, i + BATCH_SIZE);
const batchPromises = batch.map(offer =>
offer.addr_from ? IdentityManager.getIdentityData(offer.addr_from) : Promise.resolve(null)
);
const batchResults = await Promise.all(batchPromises);
identities.push(...batchResults);
if (i + BATCH_SIZE < itemsToDisplay.length) {
await new Promise(resolve => setTimeout(resolve, 100));
}
}
if (validOffers.length === 0) {
handleNoOffersScenario();
return;
}
const totalPages = Math.max(1, Math.ceil(validOffers.length / itemsPerPage));
currentPage = Math.min(currentPage, totalPages);
const existingRows = offersBody.querySelectorAll('tr');
existingRows.forEach(row => {
cleanupRow(row);
});
const startIndex = (currentPage - 1) * itemsPerPage;
const endIndex = Math.min(startIndex + itemsPerPage, validOffers.length);
const itemsToDisplay = validOffers.slice(startIndex, endIndex);
const fragment = document.createDocumentFragment();
itemsToDisplay.forEach((offer, index) => {
const identity = identities[index];
const row = createTableRow(offer, identity);
if (row) {
fragment.appendChild(row);
}
});
offersBody.textContent = '';
offersBody.appendChild(fragment);
const BATCH_SIZE = 10;
for (let i = 0; i < itemsToDisplay.length; i += BATCH_SIZE) {
const batch = itemsToDisplay.slice(i, i + BATCH_SIZE);
const tooltipTriggers = offersBody.querySelectorAll('[data-tooltip-target]');
tooltipTriggers.forEach(trigger => {
const targetId = trigger.getAttribute('data-tooltip-target');
const tooltipContent = document.getElementById(targetId);
if (tooltipContent) {
window.TooltipManager.create(trigger, tooltipContent.innerHTML, {
placement: trigger.getAttribute('data-tooltip-placement') || 'top'
});
const batchPromises = batch.map(offer =>
offer.addr_from ? IdentityManager.getIdentityData(offer.addr_from) : Promise.resolve(null)
);
const batchIdentities = await Promise.all(batchPromises);
batch.forEach((offer, index) => {
const row = createTableRow(offer, batchIdentities[index]);
if (row) fragment.appendChild(row);
});
if (i + BATCH_SIZE < itemsToDisplay.length) {
await new Promise(resolve => setTimeout(resolve, 16));
}
});
}
if (offersBody) {
const existingRows = offersBody.querySelectorAll('tr');
existingRows.forEach(row => cleanupRow(row));
offersBody.textContent = '';
offersBody.appendChild(fragment);
}
initializeTooltips();
requestAnimationFrame(() => {
updateRowTimes();
updatePaginationControls(totalPages);
updatePaginationControls(Math.ceil(validOffers.length / itemsPerPage));
if (tableRateModule?.initializeTable) {
tableRateModule.initializeTable();
}
@ -1550,16 +1547,6 @@ async function updateOffersTable() {
} catch (error) {
console.error('[Debug] Error in updateOffersTable:', error);
handleTableError();
try {
const cachedOffers = CacheManager.get('offers_cached');
if (cachedOffers?.value) {
jsonData = cachedOffers.value;
updateOffersTable();
}
} catch (recoveryError) {
console.error('Recovery attempt failed:', recoveryError);
}
}
}
@ -1837,12 +1824,11 @@ function createOrderbookColumn(offer, coinFrom) {
}
function createRateColumn(offer, coinFrom, coinTo) {
const rate = parseFloat(offer.rate);
const inverseRate = 1 / rate;
const fromSymbol = getCoinSymbol(coinFrom);
const toSymbol = getCoinSymbol(coinTo);
const rate = parseFloat(offer.rate) || 0;
const inverseRate = rate ? (1 / rate) : 0;
const getPriceKey = (coin) => {
if (!coin) return null;
const lowerCoin = coin.toLowerCase();
if (lowerCoin === 'firo' || lowerCoin === 'zcoin') {
return 'zcoin';
@ -1857,13 +1843,15 @@ function createRateColumn(offer, coinFrom, coinTo) {
};
const toSymbolKey = getPriceKey(coinTo);
let toPriceUSD = latestPrices[toSymbolKey]?.usd;
let toPriceUSD = latestPrices && toSymbolKey ? latestPrices[toSymbolKey]?.usd : null;
if (!toPriceUSD || isNaN(toPriceUSD)) {
toPriceUSD = tableRateModule.getFallbackValue(toSymbolKey);
}
const rateInUSD = toPriceUSD && !isNaN(toPriceUSD) && !isNaN(rate) ? rate * toPriceUSD : null;
const fromSymbol = getCoinSymbol(coinFrom);
const toSymbol = getCoinSymbol(coinTo);
return `
<td class="py-3 semibold monospace text-xs text-right items-center rate-table-info">
@ -1884,6 +1872,7 @@ function createRateColumn(offer, coinFrom, coinTo) {
`;
}
function createPercentageColumn(offer) {
return `
<td class="py-3 px-2 bold text-sm text-center monospace items-center rate-table-info">
@ -2710,95 +2699,83 @@ const timerManager = {
}
};
// INITIALIZATION AND EVENT BINDING
document.addEventListener('DOMContentLoaded', () => {
const saved = localStorage.getItem('offersTableSettings');
if (saved) {
const settings = JSON.parse(saved);
async function initializeTableAndData() {
loadSavedSettings();
updateClearFiltersButton();
initializeTableEvents();
initializeTooltips();
updateCoinFilterImages();
['coin_to', 'coin_from', 'status', 'sent_from'].forEach(id => {
const element = document.getElementById(id);
if (element && settings[id]) element.value = settings[id];
});
try {
await fetchOffers();
applyFilters();
} catch (error) {
console.error('Error loading initial data:', error);
ui.displayErrorMessage('Error loading data. Retrying in background...');
}
}
function loadSavedSettings() {
const saved = localStorage.getItem('offersTableSettings');
if (saved) {
const settings = JSON.parse(saved);
['coin_to', 'coin_from', 'status', 'sent_from'].forEach(id => {
const element = document.getElementById(id);
if (element && settings[id]) element.value = settings[id];
});
if (settings.sortColumn !== undefined) {
currentSortColumn = settings.sortColumn;
currentSortDirection = settings.sortDirection;
if (settings.sortColumn !== undefined) {
currentSortColumn = settings.sortColumn;
currentSortDirection = settings.sortDirection;
updateSortIndicators();
}
}
}
function updateSortIndicators() {
document.querySelectorAll('.sort-icon').forEach(icon => {
icon.classList.remove('text-blue-500');
icon.textContent = '↓';
});
document.querySelectorAll('.sort-icon').forEach(icon => {
icon.classList.remove('text-blue-500');
icon.textContent = '↓';
});
const sortIcon = document.getElementById(`sort-icon-${currentSortColumn}`);
if (sortIcon) {
sortIcon.textContent = currentSortDirection === 'asc' ? '↑' : '↓';
sortIcon.classList.add('text-blue-500');
}
}
const sortIcon = document.getElementById(`sort-icon-${currentSortColumn}`);
if (sortIcon) {
sortIcon.textContent = currentSortDirection === 'asc' ? '↑' : '↓';
sortIcon.classList.add('text-blue-500');
}
}
}
document.addEventListener('DOMContentLoaded', async () => {
const tableLoadPromise = initializeTableAndData();
updateClearFiltersButton();
initializeTableEvents();
initializeTooltips();
updateCoinFilterImages();
WebSocketManager.initialize();
setTimeout(() => {
WebSocketManager.initialize();
}, 1000);
await tableLoadPromise;
if (initializeTableRateModule()) {
continueInitialization();
} else {
let retryCount = 0;
const maxRetries = 5;
const retryInterval = setInterval(() => {
retryCount++;
if (initializeTableRateModule()) {
clearInterval(retryInterval);
continueInitialization();
} else if (retryCount >= maxRetries) {
clearInterval(retryInterval);
continueInitialization();
}
}, 1000);
}
timerManager.addInterval(() => {
if (WebSocketManager.isConnected()) {
console.log('🟢 WebSocket connection established');
}
}, 30000);
timerManager.addInterval(() => {
if (WebSocketManager.isConnected()) {
console.log('🟢 WebSocket connection one established');
}
}, 30000);
timerManager.addInterval(() => {
CacheManager.cleanup();
}, 300000);
timerManager.addInterval(() => {
CacheManager.cleanup();
}, 300000);
timerManager.addInterval(updateRowTimes, 900000);
timerManager.addInterval(updateRowTimes, 900000);
EventManager.add(document, 'visibilitychange', () => {
if (!document.hidden) {
if (!WebSocketManager.isConnected()) {
WebSocketManager.connect();
}
}
});
EventManager.add(document, 'visibilitychange', () => {
if (!document.hidden) {
if (!WebSocketManager.isConnected()) {
WebSocketManager.connect();
}
}
});
EventManager.add(window, 'beforeunload', () => {
cleanup();
});
updateCoinFilterImages();
fetchOffers().then(() => {
applyFilters();
if (!isSentOffers) {
return;
}
}).catch(error => {
console.error('Error fetching initial offers:', error);
});
EventManager.add(window, 'beforeunload', () => {
cleanup();
});
});
async function cleanup() {