mirror of
https://github.com/basicswap/basicswap.git
synced 2025-05-05 20:32:15 +00:00
Merge pull request #262 from gerlofvanek/ws
JS: Fix websocket delay / loading tables faster.
This commit is contained in:
commit
8d317e4b67
1 changed files with 136 additions and 159 deletions
|
@ -75,6 +75,7 @@ let filterTimeout = null;
|
||||||
// CONFIGURATION CONSTANTS
|
// CONFIGURATION CONSTANTS
|
||||||
// TIME CONSTANTS
|
// TIME CONSTANTS
|
||||||
const CACHE_DURATION = 10 * 60 * 1000;
|
const CACHE_DURATION = 10 * 60 * 1000;
|
||||||
|
const wsPort = config.port || window.ws_port || '11700';
|
||||||
|
|
||||||
// APP CONSTANTS
|
// APP CONSTANTS
|
||||||
const itemsPerPage = 50;
|
const itemsPerPage = 50;
|
||||||
|
@ -206,7 +207,7 @@ const WebSocketManager = {
|
||||||
this.connect();
|
this.connect();
|
||||||
}
|
}
|
||||||
this.startHealthCheck();
|
this.startHealthCheck();
|
||||||
}, 1000);
|
}, 0);
|
||||||
},
|
},
|
||||||
|
|
||||||
startHealthCheck() {
|
startHealthCheck() {
|
||||||
|
@ -1158,26 +1159,50 @@ async function fetchOffers() {
|
||||||
const refreshText = document.getElementById('refreshText');
|
const refreshText = document.getElementById('refreshText');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
refreshButton.disabled = true;
|
if (refreshButton) {
|
||||||
refreshIcon.classList.add('animate-spin');
|
refreshButton.disabled = true;
|
||||||
refreshText.textContent = 'Refreshing...';
|
refreshIcon.classList.add('animate-spin');
|
||||||
refreshButton.classList.add('opacity-75', 'cursor-wait');
|
refreshText.textContent = 'Refreshing...';
|
||||||
|
refreshButton.classList.add('opacity-75', 'cursor-wait');
|
||||||
|
}
|
||||||
|
|
||||||
const endpoint = isSentOffers ? '/json/sentoffers' : '/json/offers';
|
const [offersResponse, pricesData] = await Promise.all([
|
||||||
const response = await fetch(endpoint);
|
fetch(isSentOffers ? '/json/sentoffers' : '/json/offers'),
|
||||||
const data = await response.json();
|
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];
|
originalJsonData = [...jsonData];
|
||||||
|
|
||||||
|
latestPrices = pricesData || getEmptyPriceData();
|
||||||
|
|
||||||
await updateOffersTable();
|
await updateOffersTable();
|
||||||
updatePaginationInfo();
|
updatePaginationInfo();
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[Debug] Error fetching offers:', 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.');
|
ui.displayErrorMessage('Failed to fetch offers. Please try again later.');
|
||||||
} finally {
|
} 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();
|
window.TooltipManager.cleanup();
|
||||||
}
|
}
|
||||||
|
|
||||||
const PRICES_CACHE_KEY = 'prices_coingecko';
|
|
||||||
const cachedPrices = CacheManager.get(PRICES_CACHE_KEY);
|
|
||||||
latestPrices = cachedPrices?.value || getEmptyPriceData();
|
|
||||||
|
|
||||||
const validOffers = getValidOffers();
|
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) {
|
if (validOffers.length === 0) {
|
||||||
handleNoOffersScenario();
|
handleNoOffersScenario();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const totalPages = Math.max(1, Math.ceil(validOffers.length / itemsPerPage));
|
const startIndex = (currentPage - 1) * itemsPerPage;
|
||||||
currentPage = Math.min(currentPage, totalPages);
|
const endIndex = Math.min(startIndex + itemsPerPage, validOffers.length);
|
||||||
|
const itemsToDisplay = validOffers.slice(startIndex, endIndex);
|
||||||
const existingRows = offersBody.querySelectorAll('tr');
|
|
||||||
existingRows.forEach(row => {
|
|
||||||
cleanupRow(row);
|
|
||||||
});
|
|
||||||
|
|
||||||
const fragment = document.createDocumentFragment();
|
const fragment = document.createDocumentFragment();
|
||||||
itemsToDisplay.forEach((offer, index) => {
|
|
||||||
const identity = identities[index];
|
|
||||||
const row = createTableRow(offer, identity);
|
|
||||||
if (row) {
|
|
||||||
fragment.appendChild(row);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
offersBody.textContent = '';
|
const BATCH_SIZE = 10;
|
||||||
offersBody.appendChild(fragment);
|
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]');
|
const batchPromises = batch.map(offer =>
|
||||||
tooltipTriggers.forEach(trigger => {
|
offer.addr_from ? IdentityManager.getIdentityData(offer.addr_from) : Promise.resolve(null)
|
||||||
const targetId = trigger.getAttribute('data-tooltip-target');
|
);
|
||||||
const tooltipContent = document.getElementById(targetId);
|
|
||||||
|
const batchIdentities = await Promise.all(batchPromises);
|
||||||
if (tooltipContent) {
|
|
||||||
window.TooltipManager.create(trigger, tooltipContent.innerHTML, {
|
batch.forEach((offer, index) => {
|
||||||
placement: trigger.getAttribute('data-tooltip-placement') || 'top'
|
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(() => {
|
requestAnimationFrame(() => {
|
||||||
updateRowTimes();
|
updateRowTimes();
|
||||||
updatePaginationControls(totalPages);
|
updatePaginationControls(Math.ceil(validOffers.length / itemsPerPage));
|
||||||
if (tableRateModule?.initializeTable) {
|
if (tableRateModule?.initializeTable) {
|
||||||
tableRateModule.initializeTable();
|
tableRateModule.initializeTable();
|
||||||
}
|
}
|
||||||
|
@ -1550,16 +1547,6 @@ async function updateOffersTable() {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[Debug] Error in updateOffersTable:', error);
|
console.error('[Debug] Error in updateOffersTable:', error);
|
||||||
handleTableError();
|
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) {
|
function createRateColumn(offer, coinFrom, coinTo) {
|
||||||
const rate = parseFloat(offer.rate);
|
const rate = parseFloat(offer.rate) || 0;
|
||||||
const inverseRate = 1 / rate;
|
const inverseRate = rate ? (1 / rate) : 0;
|
||||||
const fromSymbol = getCoinSymbol(coinFrom);
|
|
||||||
const toSymbol = getCoinSymbol(coinTo);
|
|
||||||
|
|
||||||
const getPriceKey = (coin) => {
|
const getPriceKey = (coin) => {
|
||||||
|
if (!coin) return null;
|
||||||
const lowerCoin = coin.toLowerCase();
|
const lowerCoin = coin.toLowerCase();
|
||||||
if (lowerCoin === 'firo' || lowerCoin === 'zcoin') {
|
if (lowerCoin === 'firo' || lowerCoin === 'zcoin') {
|
||||||
return 'zcoin';
|
return 'zcoin';
|
||||||
|
@ -1857,13 +1843,15 @@ function createRateColumn(offer, coinFrom, coinTo) {
|
||||||
};
|
};
|
||||||
|
|
||||||
const toSymbolKey = getPriceKey(coinTo);
|
const toSymbolKey = getPriceKey(coinTo);
|
||||||
let toPriceUSD = latestPrices[toSymbolKey]?.usd;
|
let toPriceUSD = latestPrices && toSymbolKey ? latestPrices[toSymbolKey]?.usd : null;
|
||||||
|
|
||||||
if (!toPriceUSD || isNaN(toPriceUSD)) {
|
if (!toPriceUSD || isNaN(toPriceUSD)) {
|
||||||
toPriceUSD = tableRateModule.getFallbackValue(toSymbolKey);
|
toPriceUSD = tableRateModule.getFallbackValue(toSymbolKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
const rateInUSD = toPriceUSD && !isNaN(toPriceUSD) && !isNaN(rate) ? rate * toPriceUSD : null;
|
const rateInUSD = toPriceUSD && !isNaN(toPriceUSD) && !isNaN(rate) ? rate * toPriceUSD : null;
|
||||||
|
const fromSymbol = getCoinSymbol(coinFrom);
|
||||||
|
const toSymbol = getCoinSymbol(coinTo);
|
||||||
|
|
||||||
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">
|
||||||
|
@ -1884,6 +1872,7 @@ function createRateColumn(offer, coinFrom, coinTo) {
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function createPercentageColumn(offer) {
|
function createPercentageColumn(offer) {
|
||||||
return `
|
return `
|
||||||
<td class="py-3 px-2 bold text-sm text-center monospace items-center rate-table-info">
|
<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
|
async function initializeTableAndData() {
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
loadSavedSettings();
|
||||||
const saved = localStorage.getItem('offersTableSettings');
|
updateClearFiltersButton();
|
||||||
if (saved) {
|
initializeTableEvents();
|
||||||
const settings = JSON.parse(saved);
|
initializeTooltips();
|
||||||
|
updateCoinFilterImages();
|
||||||
|
|
||||||
['coin_to', 'coin_from', 'status', 'sent_from'].forEach(id => {
|
try {
|
||||||
const element = document.getElementById(id);
|
await fetchOffers();
|
||||||
if (element && settings[id]) element.value = settings[id];
|
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) {
|
if (settings.sortColumn !== undefined) {
|
||||||
currentSortColumn = settings.sortColumn;
|
currentSortColumn = settings.sortColumn;
|
||||||
currentSortDirection = settings.sortDirection;
|
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 => {
|
const sortIcon = document.getElementById(`sort-icon-${currentSortColumn}`);
|
||||||
icon.classList.remove('text-blue-500');
|
if (sortIcon) {
|
||||||
icon.textContent = '↓';
|
sortIcon.textContent = currentSortDirection === 'asc' ? '↑' : '↓';
|
||||||
});
|
sortIcon.classList.add('text-blue-500');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const sortIcon = document.getElementById(`sort-icon-${currentSortColumn}`);
|
document.addEventListener('DOMContentLoaded', async () => {
|
||||||
if (sortIcon) {
|
const tableLoadPromise = initializeTableAndData();
|
||||||
sortIcon.textContent = currentSortDirection === 'asc' ? '↑' : '↓';
|
|
||||||
sortIcon.classList.add('text-blue-500');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateClearFiltersButton();
|
WebSocketManager.initialize();
|
||||||
initializeTableEvents();
|
|
||||||
initializeTooltips();
|
|
||||||
updateCoinFilterImages();
|
|
||||||
|
|
||||||
setTimeout(() => {
|
await tableLoadPromise;
|
||||||
WebSocketManager.initialize();
|
|
||||||
}, 1000);
|
|
||||||
|
|
||||||
if (initializeTableRateModule()) {
|
timerManager.addInterval(() => {
|
||||||
continueInitialization();
|
if (WebSocketManager.isConnected()) {
|
||||||
} else {
|
console.log('🟢 WebSocket connection established');
|
||||||
let retryCount = 0;
|
}
|
||||||
const maxRetries = 5;
|
}, 30000);
|
||||||
const retryInterval = setInterval(() => {
|
|
||||||
retryCount++;
|
|
||||||
if (initializeTableRateModule()) {
|
|
||||||
clearInterval(retryInterval);
|
|
||||||
continueInitialization();
|
|
||||||
} else if (retryCount >= maxRetries) {
|
|
||||||
clearInterval(retryInterval);
|
|
||||||
continueInitialization();
|
|
||||||
}
|
|
||||||
}, 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
timerManager.addInterval(() => {
|
timerManager.addInterval(() => {
|
||||||
if (WebSocketManager.isConnected()) {
|
CacheManager.cleanup();
|
||||||
console.log('🟢 WebSocket connection one established');
|
}, 300000);
|
||||||
}
|
|
||||||
}, 30000);
|
|
||||||
|
|
||||||
timerManager.addInterval(() => {
|
timerManager.addInterval(updateRowTimes, 900000);
|
||||||
CacheManager.cleanup();
|
|
||||||
}, 300000);
|
|
||||||
|
|
||||||
timerManager.addInterval(updateRowTimes, 900000);
|
EventManager.add(document, 'visibilitychange', () => {
|
||||||
|
if (!document.hidden) {
|
||||||
|
if (!WebSocketManager.isConnected()) {
|
||||||
|
WebSocketManager.connect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
EventManager.add(document, 'visibilitychange', () => {
|
EventManager.add(window, 'beforeunload', () => {
|
||||||
if (!document.hidden) {
|
cleanup();
|
||||||
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);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
async function cleanup() {
|
async function cleanup() {
|
||||||
|
|
Loading…
Reference in a new issue