From 2c809cde65262c05204803bef81f724ee3913a13 Mon Sep 17 00:00:00 2001 From: memoyil <2213635+memoyil@users.noreply.github.com> Date: Mon, 4 Nov 2024 08:59:44 +0100 Subject: [PATCH 1/3] feat: Add debounce and abort signal to position manager --- .../GaugesVoting/hooks/useGaugesFilter.ts | 78 ++++++++----- .../hooks/usePositionManagerName.ts | 9 +- apps/web/src/views/GaugesVoting/utils.ts | 4 +- .../hooks/usePositionManager.ts | 9 +- .../src/utils/fetchPositionManager.ts | 103 ++++++++++-------- 5 files changed, 113 insertions(+), 90 deletions(-) diff --git a/apps/web/src/views/GaugesVoting/hooks/useGaugesFilter.ts b/apps/web/src/views/GaugesVoting/hooks/useGaugesFilter.ts index 71bd894a9dabb..2177113960ff7 100644 --- a/apps/web/src/views/GaugesVoting/hooks/useGaugesFilter.ts +++ b/apps/web/src/views/GaugesVoting/hooks/useGaugesFilter.ts @@ -10,6 +10,7 @@ import { useQueryStates, } from 'nuqs' import { useCallback, useEffect, useState } from 'react' +import { useDebounce } from '@pancakeswap/hooks' import { Filter, FilterValue, Gauges, OptionsType, SortOptions } from '../components/GaugesFilter' import { getPositionManagerName } from '../utils' @@ -135,7 +136,7 @@ const useFilteredGauges = ({ filter, fullGauges, searchText, sort, setSort }) => }, [fullGauges, setSort, sort]) useEffect(() => { - const fetchFilteredGauges = async () => { + const fetchFilteredGauges = async (signal) => { if (!fullGauges || !fullGauges.length) { setFilteredGauges([]) return @@ -160,34 +161,56 @@ const useFilteredGauges = ({ filter, fullGauges, searchText, sort, setSort }) => // Asynchronous search based on searchText if (searchText?.length > 0) { - const updatedResults = await Promise.all( - results.map(async (gauge) => { - const positionManagerName = await getPositionManagerName(gauge) - const isMatch = [ - // search by pairName or tokenName - gauge.pairName.toLowerCase(), - // search by gauges type, e.g. "v2", "v3", "position manager" - GAUGE_TYPE_NAMES[gauge.type].toLowerCase(), - // search by chain name - chainNames[gauge.chainId], - // search by chain id - String(gauge.chainId), - // search by boost multiplier, e.g. "1.5x" - `${Number(gauge.boostMultiplier) / 100}x`, - // search by alm strategy name - positionManagerName.toLowerCase(), - ].some((text) => text?.includes(searchText.toLowerCase())) - return isMatch ? gauge : null - }), - ) - results = updatedResults.filter(Boolean) // Remove nulls - } + try { + const updatedResults = await Promise.all( + results.map(async (gauge) => { + try { + const positionManagerName = await getPositionManagerName(gauge, signal) + const isMatch = [ + // search by pairName or tokenName + gauge.pairName.toLowerCase(), + // search by gauges type, e.g. "v2", "v3", "position manager" + GAUGE_TYPE_NAMES[gauge.type].toLowerCase(), + // search by chain name + chainNames[gauge.chainId], + // search by chain id + String(gauge.chainId), + // search by boost multiplier, e.g. "1.5x" + `${Number(gauge.boostMultiplier) / 100}x`, + // search by alm strategy name + positionManagerName.toLowerCase(), + ].some((text) => text?.includes(searchText.toLowerCase())) + return isMatch ? gauge : null + } catch (error) { + if (error instanceof Error) { + if (error.name !== 'AbortError') { + console.error('Error fetching position manager name:', error) + } else { + throw error + } + } + return null + } + }), + ) + results = updatedResults.filter(Boolean) // Remove nulls - const sorter = getSorter(sort) - setFilteredGauges(results.sort(sorter)) + const sorter = getSorter(sort) + setFilteredGauges(results.sort(sorter)) + } catch (error) { + // eslint-disable-next-line no-empty + } + } } - fetchFilteredGauges() + const controller = new AbortController() + const { signal } = controller + + fetchFilteredGauges(signal) + + return () => { + controller.abort() + } }, [filter, fullGauges, searchText, sort]) return filteredGauges @@ -196,12 +219,13 @@ const useFilteredGauges = ({ filter, fullGauges, searchText, sort, setSort }) => export const useGaugesQueryFilter = (fullGauges: Gauge[] | undefined) => { const { filter, setFilter, searchText, setSearchText } = useGaugesFilterQueryState() const [sort, setSort] = useState() + const debouncedQuery = useDebounce(searchText, 200) const filterGauges = useFilteredGauges({ filter, fullGauges, searchText, sort, setSort }) return { filterGauges, - searchText, + searchText: debouncedQuery, setSearchText, filter, diff --git a/apps/web/src/views/GaugesVoting/hooks/usePositionManagerName.ts b/apps/web/src/views/GaugesVoting/hooks/usePositionManagerName.ts index eaaa49e886ea4..d5d4fbcd914e8 100644 --- a/apps/web/src/views/GaugesVoting/hooks/usePositionManagerName.ts +++ b/apps/web/src/views/GaugesVoting/hooks/usePositionManagerName.ts @@ -5,14 +5,7 @@ import { getPositionManagerName } from 'views/GaugesVoting/utils' export const usePositionManagerName = (data: Gauge) => { const { data: managerName } = useQuery({ queryKey: ['position-manager-name'], - queryFn: async () => { - try { - const result = await getPositionManagerName(data) - return result - } catch { - return '' - } - }, + queryFn: async ({ signal }) => getPositionManagerName(data, signal), enabled: Boolean(data), refetchOnWindowFocus: false, refetchOnReconnect: false, diff --git a/apps/web/src/views/GaugesVoting/utils.ts b/apps/web/src/views/GaugesVoting/utils.ts index 5e8071236d3c8..583de84dfe905 100644 --- a/apps/web/src/views/GaugesVoting/utils.ts +++ b/apps/web/src/views/GaugesVoting/utils.ts @@ -6,10 +6,10 @@ export const getGaugeHash = (gaugeAddress: Address = zeroAddress, chainId: numbe return keccak256(encodePacked(['address', 'uint256'], [gaugeAddress, BigInt(chainId || 0)])) } -export const getPositionManagerName = async (gauge: Gauge): Promise => { +export const getPositionManagerName = async (gauge: Gauge, signal?: AbortSignal): Promise => { if (gauge.type !== GaugeType.ALM) return '' - const vaults: PCSDuoTokenVaultConfig[] = await fetchPositionManager(gauge.chainId) + const vaults: PCSDuoTokenVaultConfig[] = await fetchPositionManager(gauge.chainId, signal) const matchedVault = vaults.find((v) => v.vaultAddress === gauge.address) if (!matchedVault) return gauge.managerName ?? '' diff --git a/apps/web/src/views/PositionManagers/hooks/usePositionManager.ts b/apps/web/src/views/PositionManagers/hooks/usePositionManager.ts index b3f39fe34f9af..8d6ca4e34e68b 100644 --- a/apps/web/src/views/PositionManagers/hooks/usePositionManager.ts +++ b/apps/web/src/views/PositionManagers/hooks/usePositionManager.ts @@ -5,14 +5,7 @@ import { useQuery } from '@tanstack/react-query' export const usePositionManager = (chainId: ChainId): VaultConfig[] => { const { data } = useQuery({ queryKey: ['vault-config-by-chain', chainId], - queryFn: async () => { - try { - const result = await fetchPositionManager(chainId) - return result - } catch { - return [] - } - }, + queryFn: async ({ signal }) => fetchPositionManager(chainId, signal), enabled: Boolean(chainId), refetchOnWindowFocus: false, refetchOnReconnect: false, diff --git a/packages/position-managers/src/utils/fetchPositionManager.ts b/packages/position-managers/src/utils/fetchPositionManager.ts index 3460f2886222d..d81192f010697 100644 --- a/packages/position-managers/src/utils/fetchPositionManager.ts +++ b/packages/position-managers/src/utils/fetchPositionManager.ts @@ -5,7 +5,24 @@ import { VaultConfig } from '../types' const positionManagerCache: Record = {} -export const fetchPositionManager = async (chainId: ChainId): Promise => { +function anySignal(signals: AbortSignal[]): AbortSignal { + const controller = new AbortController() + + for (const signal of signals) { + if (signal.aborted) { + controller.abort() + return signal + } + + signal.addEventListener('abort', () => controller.abort(signal.reason), { + signal: controller.signal, + }) + } + + return controller.signal +} + +export const fetchPositionManager = async (chainId: ChainId, signal?: AbortSignal): Promise => { const cacheKey = `${chainId}-all}` // Return cached data if it exists @@ -13,49 +30,45 @@ export const fetchPositionManager = async (chainId: ChainId): Promise `${encodeURIComponent(key)}=${encodeURIComponent(value)}`) - .join('&') + const params = { chainId } + const queryString = Object.entries(params) + .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`) + .join('&') - const response = await fetch(`${POSITION_MANAGER_API_V2}?${queryString}`, { - signal: AbortSignal.timeout(3000), - }) - const result = await response.json() - const newData: VaultConfig[] = result.map((p: any) => ({ - ...p, - currencyA: new ERC20Token( - p.currencyA.chainId, - p.currencyA.address, - p.currencyA.decimals, - p.currencyA.symbol, - p.currencyA.name, - p.currencyA.projectLink, - ), - currencyB: new ERC20Token( - p.currencyB.chainId, - p.currencyB.address, - p.currencyB.decimals, - p.currencyB.symbol, - p.currencyB.name, - p.currencyB.projectLink, - ), - earningToken: new ERC20Token( - p.earningToken.chainId, - p.earningToken.address, - p.earningToken.decimals, - p.earningToken.symbol, - p.earningToken.name, - p.earningToken.projectLink, - ), - })) - - // Cache the result before returning it - positionManagerCache[cacheKey] = newData - - return newData - } catch (error) { - return [] - } + const response = await fetch(`${POSITION_MANAGER_API_V2}?${queryString}`, { + signal: anySignal([AbortSignal.timeout(3000), ...(signal ? [signal] : [])]), + }) + const result = await response.json() + const newData: VaultConfig[] = result.map((p: any) => ({ + ...p, + currencyA: new ERC20Token( + p.currencyA.chainId, + p.currencyA.address, + p.currencyA.decimals, + p.currencyA.symbol, + p.currencyA.name, + p.currencyA.projectLink, + ), + currencyB: new ERC20Token( + p.currencyB.chainId, + p.currencyB.address, + p.currencyB.decimals, + p.currencyB.symbol, + p.currencyB.name, + p.currencyB.projectLink, + ), + earningToken: new ERC20Token( + p.earningToken.chainId, + p.earningToken.address, + p.earningToken.decimals, + p.earningToken.symbol, + p.earningToken.name, + p.earningToken.projectLink, + ), + })) + + // Cache the result before returning it + positionManagerCache[cacheKey] = newData + + return newData } From 6f45e7a9158f1a30b6e8c4a151d3808681ab45db Mon Sep 17 00:00:00 2001 From: memoyil <2213635+memoyil@users.noreply.github.com> Date: Mon, 4 Nov 2024 09:16:06 +0100 Subject: [PATCH 2/3] fix: Initial load --- apps/web/src/views/GaugesVoting/hooks/useGaugesFilter.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/apps/web/src/views/GaugesVoting/hooks/useGaugesFilter.ts b/apps/web/src/views/GaugesVoting/hooks/useGaugesFilter.ts index 2177113960ff7..be457ebcbc359 100644 --- a/apps/web/src/views/GaugesVoting/hooks/useGaugesFilter.ts +++ b/apps/web/src/views/GaugesVoting/hooks/useGaugesFilter.ts @@ -194,13 +194,12 @@ const useFilteredGauges = ({ filter, fullGauges, searchText, sort, setSort }) => }), ) results = updatedResults.filter(Boolean) // Remove nulls - - const sorter = getSorter(sort) - setFilteredGauges(results.sort(sorter)) } catch (error) { - // eslint-disable-next-line no-empty + return } } + const sorter = getSorter(sort) + setFilteredGauges(results.sort(sorter)) } const controller = new AbortController() From fb3f96fe805096fda4e0611e0fabbba43e01db12 Mon Sep 17 00:00:00 2001 From: memoyil <2213635+memoyil@users.noreply.github.com> Date: Mon, 4 Nov 2024 09:55:54 +0100 Subject: [PATCH 3/3] perf: Fetch vaults by chainid instead of per gauge --- .../GaugesVoting/hooks/useGaugesFilter.ts | 25 +++++++++++++++++-- .../hooks/usePositionManagerName.ts | 2 +- apps/web/src/views/GaugesVoting/utils.ts | 13 +++++++--- 3 files changed, 34 insertions(+), 6 deletions(-) diff --git a/apps/web/src/views/GaugesVoting/hooks/useGaugesFilter.ts b/apps/web/src/views/GaugesVoting/hooks/useGaugesFilter.ts index be457ebcbc359..8dbebd4b73461 100644 --- a/apps/web/src/views/GaugesVoting/hooks/useGaugesFilter.ts +++ b/apps/web/src/views/GaugesVoting/hooks/useGaugesFilter.ts @@ -1,4 +1,4 @@ -import { chainNames } from '@pancakeswap/chains' +import { ChainId, chainNames } from '@pancakeswap/chains' import { GAUGES_SUPPORTED_CHAIN_IDS, GAUGE_TYPE_NAMES, Gauge, GaugeType } from '@pancakeswap/gauges' import { FeeAmount } from '@pancakeswap/v3-sdk' import { @@ -11,6 +11,8 @@ import { } from 'nuqs' import { useCallback, useEffect, useState } from 'react' import { useDebounce } from '@pancakeswap/hooks' +import { fetchPositionManager, PCSDuoTokenVaultConfig } from '@pancakeswap/position-managers' +import fromPairs from 'lodash/fromPairs' import { Filter, FilterValue, Gauges, OptionsType, SortOptions } from '../components/GaugesFilter' import { getPositionManagerName } from '../utils' @@ -162,10 +164,29 @@ const useFilteredGauges = ({ filter, fullGauges, searchText, sort, setSort }) => // Asynchronous search based on searchText if (searchText?.length > 0) { try { + const positionManagerPairs: Partial> = fromPairs( + await Promise.all( + results + .reduce((acc, gauge) => { + if (!acc.includes(gauge.chainId)) { + acc.push(gauge.chainId) + } + return acc + }, []) + .map(async (chainId) => { + const positionManagerName = await fetchPositionManager(chainId, signal) + return [chainId, positionManagerName] + }), + ), + ) const updatedResults = await Promise.all( results.map(async (gauge) => { try { - const positionManagerName = await getPositionManagerName(gauge, signal) + const positionManagerName = await getPositionManagerName( + gauge, + positionManagerPairs?.[gauge.chainId] ?? undefined, + signal, + ) const isMatch = [ // search by pairName or tokenName gauge.pairName.toLowerCase(), diff --git a/apps/web/src/views/GaugesVoting/hooks/usePositionManagerName.ts b/apps/web/src/views/GaugesVoting/hooks/usePositionManagerName.ts index d5d4fbcd914e8..7687aac9080a4 100644 --- a/apps/web/src/views/GaugesVoting/hooks/usePositionManagerName.ts +++ b/apps/web/src/views/GaugesVoting/hooks/usePositionManagerName.ts @@ -5,7 +5,7 @@ import { getPositionManagerName } from 'views/GaugesVoting/utils' export const usePositionManagerName = (data: Gauge) => { const { data: managerName } = useQuery({ queryKey: ['position-manager-name'], - queryFn: async ({ signal }) => getPositionManagerName(data, signal), + queryFn: async ({ signal }) => getPositionManagerName(data, undefined, signal), enabled: Boolean(data), refetchOnWindowFocus: false, refetchOnReconnect: false, diff --git a/apps/web/src/views/GaugesVoting/utils.ts b/apps/web/src/views/GaugesVoting/utils.ts index 583de84dfe905..586e48fc088f6 100644 --- a/apps/web/src/views/GaugesVoting/utils.ts +++ b/apps/web/src/views/GaugesVoting/utils.ts @@ -6,11 +6,18 @@ export const getGaugeHash = (gaugeAddress: Address = zeroAddress, chainId: numbe return keccak256(encodePacked(['address', 'uint256'], [gaugeAddress, BigInt(chainId || 0)])) } -export const getPositionManagerName = async (gauge: Gauge, signal?: AbortSignal): Promise => { +export const getPositionManagerName = async ( + gauge: Gauge, + vaults?: PCSDuoTokenVaultConfig[], + signal?: AbortSignal, +): Promise => { if (gauge.type !== GaugeType.ALM) return '' - const vaults: PCSDuoTokenVaultConfig[] = await fetchPositionManager(gauge.chainId, signal) - const matchedVault = vaults.find((v) => v.vaultAddress === gauge.address) + let _vaults = vaults + if (!vaults) { + _vaults = await fetchPositionManager(gauge.chainId, signal) + } + const matchedVault = _vaults?.find((v) => v.vaultAddress === gauge.address) if (!matchedVault) return gauge.managerName ?? ''