From 20d9e43b1fdfac5876c577d9e716f3f2a139b83b Mon Sep 17 00:00:00 2001 From: 0xFirekeeper <0xFirekeeper@gmail.com> Date: Wed, 17 Dec 2025 06:00:34 +0700 Subject: [PATCH] Improve portfolio fetch concurrency and error handling Refactored fetchAllPortfolios to use up to 50 concurrent workers instead of batching, improving throughput. Updated fetchWithRetry to return null on failure instead of throwing, and adjusted downstream logic to handle null responses gracefully. Errors for individual addresses are now logged and do not interrupt processing of others. --- .../src/@/hooks/useWalletPortfolio.ts | 58 +++++++++---------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/apps/dashboard/src/@/hooks/useWalletPortfolio.ts b/apps/dashboard/src/@/hooks/useWalletPortfolio.ts index adba6d2a992..c0cf569c9fc 100644 --- a/apps/dashboard/src/@/hooks/useWalletPortfolio.ts +++ b/apps/dashboard/src/@/hooks/useWalletPortfolio.ts @@ -12,14 +12,12 @@ export type WalletPortfolioData = { tokens: WalletPortfolioToken[]; }; -// Retry helper with exponential backoff +// Retry helper with exponential backoff - returns null on failure instead of throwing async function fetchWithRetry( url: string, options: RequestInit, maxRetries = 3, -): Promise { - let lastError: Error | null = null; - +): Promise { for (let attempt = 0; attempt < maxRetries; attempt++) { try { const response = await fetch(url, options); @@ -32,15 +30,15 @@ async function fetchWithRetry( } return response; - } catch (e) { - lastError = e instanceof Error ? e : new Error(String(e)); + } catch (_e) { // Network error - retry with backoff const delay = Math.min(1000 * 2 ** attempt, 10000); await new Promise((resolve) => setTimeout(resolve, delay)); } } - throw lastError || new Error("Max retries exceeded"); + // All retries failed - return null instead of throwing + return null; } // Fetch tokens for a single address on a single chain @@ -74,7 +72,8 @@ async function fetchTokensForChain( }, }); - if (!response.ok) { + // If all retries failed or response not ok, return empty (balance = 0) + if (!response || !response.ok) { return []; } @@ -158,15 +157,18 @@ async function fetchAllPortfolios( onProgress?: (completed: number, total: number) => void, ): Promise> { const results = new Map(); - const batchSize = 10; // Process 10 addresses at a time for better throughput + const concurrency = 50; // Process up to 50 addresses concurrently let completed = 0; + let index = 0; - for (let i = 0; i < addresses.length; i += batchSize) { - const batch = addresses.slice(i, i + batchSize); + // Worker function that processes one address at a time + async function worker() { + while (index < addresses.length) { + const currentIndex = index++; + const address = addresses[currentIndex]; + if (!address) continue; - // Process batch concurrently with individual error handling - const batchResults = await Promise.allSettled( - batch.map(async (address) => { + try { const data = await fetchWalletPortfolio( address, chainIds, @@ -175,26 +177,24 @@ async function fetchAllPortfolios( clientId, ecosystemSlug, ); - return { address, data }; - }), - ); - - // Only add successful results - for (const result of batchResults) { - if (result.status === "fulfilled") { - results.set(result.value.address, result.value.data); + results.set(address, data); + } catch (e) { + // Silent fail - continue with others + console.warn(`Failed to fetch portfolio for ${address}:`, e); } - } - - completed = Math.min(i + batchSize, addresses.length); - onProgress?.(completed, addresses.length); - // Small delay between batches to avoid overwhelming the API - if (i + batchSize < addresses.length) { - await new Promise((resolve) => setTimeout(resolve, 100)); + completed++; + onProgress?.(completed, addresses.length); } } + // Start concurrent workers + const workers = Array.from( + { length: Math.min(concurrency, addresses.length) }, + () => worker(), + ); + await Promise.all(workers); + return results; }