Skip to content
This repository has been archived by the owner on Jan 15, 2021. It is now read-only.

Add timeout for fetching tokens #1644

Merged
merged 2 commits into from
Nov 30, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { Network } from 'types'
import { DisabledTokensMaps, TokenOverride, AddressToOverrideMap } from 'types/config'

export const BATCH_TIME_IN_MS = BATCH_TIME * 1000
export const DEFAULT_TIMEOUT = 5000

export const ZERO_BIG_NUMBER = new BigNumber(0)
export const ONE_BIG_NUMBER = new BigNumber(1)
Expand Down
48 changes: 30 additions & 18 deletions src/hooks/useTokenBalances.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { erc20Api, depositApi } from 'api'
import useSafeState from './useSafeState'
import { useWalletConnection } from './useWalletConnection'

import { formatSmart, logDebug, isTokenEnabled } from 'utils'
import { formatSmart, logDebug, isTokenEnabled, timeout, notEmpty } from 'utils'
import { TokenBalanceDetails, TokenDetails } from 'types'
import { WalletInfo } from 'api/wallet/WalletApi'
import { PendingFlux } from 'api/deposit/DepositApi'
Expand All @@ -34,6 +34,7 @@ async function fetchBalancesForToken(
networkId: number,
): Promise<TokenBalanceDetails> {
const tokenAddress = token.address

const [
exchangeBalance,
pendingDeposit,
Expand Down Expand Up @@ -83,29 +84,40 @@ async function _getBalances(walletInfo: WalletInfo, tokens: TokenDetails[]): Pro
const contractAddress = depositApi.getContractAddress(networkId)
assert(contractAddress, 'No valid contract address found. Stopping.')

const balancePromises: Promise<TokenBalanceDetails | null>[] = tokens.map((token) =>
fetchBalancesForToken(token, userAddress, contractAddress, networkId)
.then((balance) => {
const cacheKey = constructCacheKey({ token, userAddress, contractAddress, networkId })
const balancePromises: Promise<TokenBalanceDetails | null>[] = tokens.map((token) => {
const cacheKey = constructCacheKey({ token, userAddress, contractAddress, networkId })

// timoutPromise == Promise<never>, correctly determined to always throw
const timeoutPromise = timeout({
timeoutErrorMsg: 'Timeout fetching balances for ' + token.address,
})

const fetchBalancesPromise = fetchBalancesForToken(token, userAddress, contractAddress, networkId).then(
(balance) => {
balanceCache[cacheKey] = balance
return balance
})
.catch((e) => {
console.error('[useTokenBalances] Error for', token, userAddress, contractAddress, e)
},
)

const cacheKey = constructCacheKey({ token, userAddress, contractAddress, networkId })
// balancePromise == Promise<TokenBalanceDetails | never> == Promise<TokenBalanceDetails> or throws
const balancePromise = Promise.race([fetchBalancesPromise, timeoutPromise])

const cachedValue = balanceCache[cacheKey]
if (cachedValue) {
logDebug('Using cached value for', token, userAddress, contractAddress)
return cachedValue
}
return balancePromise.catch((e) => {
console.error('[useTokenBalances] Error for', token, userAddress, contractAddress, e)
const cachedValue = balanceCache[cacheKey]
if (cachedValue) {
logDebug('Using cached value for', token, userAddress, contractAddress)
return cachedValue
}

return null
})
})

return null
}),
)
const balances = await Promise.all(balancePromises)
return balances.filter(Boolean) as TokenBalanceDetails[]

// TODO: Would be better to show the errored tokens in error state
return balances.filter(notEmpty)
}

export const useTokenBalances = (passOnParams: Partial<UseTokenListParams> = {}): UseBalanceResult => {
Expand Down
23 changes: 21 additions & 2 deletions src/utils/miscellaneous.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { TokenDetails, Unpromise } from 'types'
import { AssertionError } from 'assert'
import { AuctionElement, Trade, Order } from 'api/exchange/ExchangeApi'
import { batchIdToDate } from './time'
import { ORDER_FILLED_FACTOR, MINIMUM_ALLOWANCE_DECIMALS } from 'const'
import { ORDER_FILLED_FACTOR, MINIMUM_ALLOWANCE_DECIMALS, DEFAULT_TIMEOUT } from 'const'
import { TEN, ZERO } from '@gnosis.pm/dex-js'

export function assertNonNull<T>(val: T, message: string): asserts val is NonNullable<T> {
Expand Down Expand Up @@ -72,7 +72,8 @@ export function getToken<T extends TokenDetails, K extends keyof T>(
return token
}

export const delay = <T>(ms = 100, result?: T): Promise<T> => new Promise((resolve) => setTimeout(resolve, ms, result))
export const delay = <T = void>(ms = 100, result?: T): Promise<T> =>
new Promise((resolve) => setTimeout(resolve, ms, result))

/**
* Uses images from https://github.com/trustwallet/tokens
Expand Down Expand Up @@ -210,3 +211,21 @@ export function notEmpty<TValue>(value: TValue | null | undefined): value is TVa
}

export const isNonZeroNumber = (value?: string | number): boolean => !!value && !!+value

export interface TimeoutParams<T> {
time?: number
result?: T
timeoutErrorMsg?: string
}

export function timeout(params: TimeoutParams<undefined>): Promise<never> // never means function throws
export function timeout<T>(params: TimeoutParams<T extends undefined ? never : T>): Promise<T>
export async function timeout<T>(params: TimeoutParams<T>): Promise<T | never> {
const { time = DEFAULT_TIMEOUT, result, timeoutErrorMsg: timeoutMsg = 'Timeout' } = params

await delay(time)
// provided defined result -- return it
if (result !== undefined) return result
// no defined result -- throw message
throw new Error(timeoutMsg)
}