feat: usePoll (#3530)

* feat: usePoll

* chore: comments

* fix: cleanup

* fix: review updates

* fix: lint nits
This commit is contained in:
Zach Pomerantz 2022-03-16 14:54:17 -07:00 committed by GitHub
parent 7c88a5a008
commit 828bf540ba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 91 additions and 58 deletions

@ -1,6 +1,7 @@
import { BigintIsh, CurrencyAmount, Token, TradeType } from '@uniswap/sdk-core'
import { AlphaRouter, AlphaRouterConfig, AlphaRouterParams, ChainId } from '@uniswap/smart-order-router'
import JSBI from 'jsbi'
import useBlockNumber from 'lib/hooks/useBlockNumber'
import { GetQuoteResult } from 'state/routing/types'
import { transformSwapRouteToGetQuoteResult } from 'utils/transformSwapRouteToGetQuoteResult'
@ -98,3 +99,10 @@ export async function getClientSideQuote(
routerConfig
)
}
export function useFreshQuote(quoteResult: GetQuoteResult | undefined, maxBlockAge = 10): GetQuoteResult | undefined {
const block = useBlockNumber()
if (!block || !quoteResult) return undefined
if (block - (Number(quoteResult.blockNumber) || 0) > maxBlockAge) return undefined
return quoteResult
}

@ -3,13 +3,14 @@ import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core'
import { ChainId } from '@uniswap/smart-order-router'
import useDebounce from 'hooks/useDebounce'
import { useStablecoinAmountFromFiatValue } from 'hooks/useUSDCPrice'
import { useEffect, useMemo, useState } from 'react'
import { useCallback, useMemo } from 'react'
import { GetQuoteResult, InterfaceTrade, TradeState } from 'state/routing/types'
import { computeRoutes, transformRoutesToTrade } from 'state/routing/utils'
import useWrapCallback, { WrapType } from '../swap/useWrapCallback'
import useActiveWeb3React from '../useActiveWeb3React'
import { getClientSideQuote } from './clientSideSmartOrderRouter'
import usePoll from '../usePoll'
import { getClientSideQuote, useFreshQuote } from './clientSideSmartOrderRouter'
import { useRoutingAPIArguments } from './useRoutingAPIArguments'
/**
@ -23,14 +24,11 @@ const DistributionPercents: { [key: number]: number } = {
[ChainId.ARBITRUM_ONE]: 25,
[ChainId.ARBITRUM_RINKEBY]: 25,
}
const DEFAULT_DISTRIBUTION_PERCENT = 10
function getConfig(chainId: ChainId | undefined) {
return {
// Limit to only V2 and V3.
protocols: [Protocol.V2, Protocol.V3],
distributionPercent: (chainId && DistributionPercents[chainId]) ?? DEFAULT_DISTRIBUTION_PERCENT,
}
}
@ -68,45 +66,22 @@ export default function useClientSideSmartOrderRouterTrade<TTradeType extends Tr
useClientSideRouter: true,
})
const params = useMemo(() => chainId && library && { chainId, provider: library }, [chainId, library])
const [loading, setLoading] = useState(false)
const [{ data: quoteResult, error }, setResult] = useState<{
data?: GetQuoteResult
error?: unknown
}>({ error: undefined })
const config = useMemo(() => getConfig(chainId), [chainId])
const { type: wrapType } = useWrapCallback()
// When arguments update, make a new call to SOR for updated quote
useEffect(() => {
if (wrapType !== WrapType.NOT_APPLICABLE) {
return
const getQuoteResult = useCallback(async (): Promise<{ data?: GetQuoteResult; error?: unknown }> => {
if (wrapType !== WrapType.NOT_APPLICABLE) return { error: undefined }
if (!queryArgs || !params) return { error: undefined }
try {
return await getClientSideQuote(queryArgs, params, config)
} catch {
return { error: true }
}
setLoading(true)
if (isDebouncing) return
let stale = false
fetchQuote()
return () => {
stale = true
setLoading(false)
}
async function fetchQuote() {
if (queryArgs && params) {
let result
try {
result = await getClientSideQuote(queryArgs, params, config)
} catch {
result = { error: true }
}
if (!stale) {
setResult(result)
setLoading(false)
}
}
}
}, [queryArgs, params, config, isDebouncing, wrapType])
}, [config, params, queryArgs, wrapType])
const { data, error } = usePoll(getQuoteResult, JSON.stringify(queryArgs)) ?? {
error: undefined,
}
const quoteResult = useFreshQuote(data)
const route = useMemo(
() => computeRoutes(currencyIn, currencyOut, tradeType, quoteResult),
@ -130,10 +105,12 @@ export default function useClientSideSmartOrderRouterTrade<TTradeType extends Tr
}
// Returns the last trade state while syncing/loading to avoid jank from clearing the last trade while loading.
if (isDebouncing) {
return { state: TradeState.SYNCING, trade }
} else if (loading) {
return { state: TradeState.LOADING, trade }
if (!quoteResult && !error) {
if (isDebouncing) {
return { state: TradeState.SYNCING, trade }
} else {
return { state: TradeState.LOADING, trade }
}
}
let otherAmount = undefined
@ -156,5 +133,5 @@ export default function useClientSideSmartOrderRouterTrade<TTradeType extends Tr
return { state: TradeState.VALID, trade }
}
return { state: TradeState.INVALID, trade: undefined }
}, [currencyIn, currencyOut, isDebouncing, loading, quoteResult, error, route, queryArgs, trade, tradeType])
}, [currencyIn, currencyOut, quoteResult, error, route, queryArgs, trade, isDebouncing, tradeType])
}

59
src/lib/hooks/usePoll.ts Normal file

@ -0,0 +1,59 @@
import ms from 'ms.macro'
import { useEffect, useMemo, useState } from 'react'
const DEFAULT_POLLING_INTERVAL = ms`15s`
const DEFAULT_KEEP_UNUSED_DATA_FOR = ms`10s`
export default function usePoll<T>(
fetch: () => Promise<T>,
key = '',
pollingInterval = DEFAULT_POLLING_INTERVAL,
keepUnusedDataFor = DEFAULT_KEEP_UNUSED_DATA_FOR
): T | undefined {
const cache = useMemo(() => new Map<string, { ttl: number; result?: T }>(), [])
const [data, setData] = useState<{ key: string; result?: T }>({ key })
useEffect(() => {
let timeout: number
const entry = cache.get(key)
if (entry && entry.ttl + keepUnusedDataFor > Date.now()) {
// If there is a fresh entry, return it and queue the next poll.
setData({ key, result: entry.result })
timeout = setTimeout(poll, Math.max(0, entry.ttl - Date.now()))
} else {
// Otherwise, set a new entry (to avoid duplicate polling) and trigger a poll immediately.
cache.set(key, { ttl: Date.now() + pollingInterval })
setData({ key })
poll()
}
return () => {
clearTimeout(timeout)
}
async function poll(ttl = Date.now() + pollingInterval) {
timeout = setTimeout(poll, pollingInterval)
const result = await fetch()
// Always set the result in the cache, but only set it as data if the key is still being queried.
cache.set(key, { ttl, result })
setData((data) => {
return data.key === key ? { key, result } : data
})
}
}, [cache, fetch, keepUnusedDataFor, key, pollingInterval])
useEffect(() => {
// Cleanup stale entries when a new key is used.
void key
const now = Date.now()
cache.forEach(({ ttl }, key) => {
if (ttl + keepUnusedDataFor <= now) {
cache.delete(key)
}
})
}, [cache, keepUnusedDataFor, key])
return data.result
}

@ -2,8 +2,8 @@ import { skipToken } from '@reduxjs/toolkit/query/react'
import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core'
import { IMetric, MetricLoggerUnit, setGlobalMetric } from '@uniswap/smart-order-router'
import { useStablecoinAmountFromFiatValue } from 'hooks/useUSDCPrice'
import { useFreshQuote } from 'lib/hooks/routing/clientSideSmartOrderRouter'
import { useRoutingAPIArguments } from 'lib/hooks/routing/useRoutingAPIArguments'
import useBlockNumber from 'lib/hooks/useBlockNumber'
import ms from 'ms.macro'
import { useMemo } from 'react'
import ReactGA from 'react-ga'
@ -13,17 +13,6 @@ import { useClientSideRouter } from 'state/user/hooks'
import { GetQuoteResult, InterfaceTrade, TradeState } from './types'
import { computeRoutes, transformRoutesToTrade } from './utils'
function useFreshData<T>(data: T, dataBlockNumber: number, maxBlockAge = 10): T | undefined {
const localBlockNumber = useBlockNumber()
if (!localBlockNumber) return undefined
if (localBlockNumber - dataBlockNumber > maxBlockAge) {
return undefined
}
return data
}
/**
* Returns the best trade by invoking the routing api or the smart order router on the client
* @param tradeType whether the swap is an exact in/out
@ -61,7 +50,7 @@ export function useRoutingAPITrade<TTradeType extends TradeType>(
refetchOnFocus: true,
})
const quoteResult: GetQuoteResult | undefined = useFreshData(data, Number(data?.blockNumber) || 0)
const quoteResult: GetQuoteResult | undefined = useFreshQuote(data)
const route = useMemo(
() => computeRoutes(currencyIn, currencyOut, tradeType, quoteResult),