Commit 963b9105 authored by Zach Pomerantz's avatar Zach Pomerantz Committed by GitHub

fix: trade loading state (#3572)

* fix: invert stale callback

* fix: polling and validation logic

* fix: rm unused conditional
parent 9e2dc9a4
...@@ -84,11 +84,11 @@ export default function useClientSideSmartOrderRouterTrade<TTradeType extends Tr ...@@ -84,11 +84,11 @@ export default function useClientSideSmartOrderRouterTrade<TTradeType extends Tr
const getIsValidBlock = useGetIsValidBlock() const getIsValidBlock = useGetIsValidBlock()
const { data: quoteResult, error } = usePoll(getQuoteResult, JSON.stringify(queryArgs), { const { data: quoteResult, error } = usePoll(getQuoteResult, JSON.stringify(queryArgs), {
debounce: isDebouncing, debounce: isDebouncing,
staleCallback: useCallback(({ data }) => getIsValidBlock(Number(data?.blockNumber) || 0), [getIsValidBlock]), staleCallback: useCallback(({ data }) => !getIsValidBlock(Number(data?.blockNumber) || 0), [getIsValidBlock]),
}) ?? { }) ?? {
error: undefined, error: undefined,
} }
const isLoading = !quoteResult const isValid = getIsValidBlock(Number(quoteResult?.blockNumber) || 0)
const route = useMemo( const route = useMemo(
() => computeRoutes(currencyIn, currencyOut, tradeType, quoteResult), () => computeRoutes(currencyIn, currencyOut, tradeType, quoteResult),
...@@ -118,14 +118,14 @@ export default function useClientSideSmartOrderRouterTrade<TTradeType extends Tr ...@@ -118,14 +118,14 @@ 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. // Returns the last trade state while syncing/loading to avoid jank from clearing the last trade while loading.
if (!quoteResult && !error) { if (!trade && !error) {
if (isStale) { if (isStale) {
return { state: TradeState.LOADING, trade: undefined } return { state: TradeState.LOADING, trade: undefined }
} else if (isDebouncing) { } else if (isDebouncing) {
return { state: TradeState.SYNCING, trade: lastTrade } return { state: TradeState.SYNCING, trade: lastTrade }
} else if (isLoading) {
return { state: TradeState.LOADING, trade: lastTrade }
} }
} else if (!isValid && !error) {
return { state: TradeState.LOADING, trade: lastTrade }
} }
let otherAmount = undefined let otherAmount = undefined
...@@ -151,14 +151,14 @@ export default function useClientSideSmartOrderRouterTrade<TTradeType extends Tr ...@@ -151,14 +151,14 @@ export default function useClientSideSmartOrderRouterTrade<TTradeType extends Tr
}, [ }, [
currencyIn, currencyIn,
currencyOut, currencyOut,
quoteResult, trade,
error, error,
isValid,
quoteResult,
route, route,
queryArgs, queryArgs,
trade,
isStale, isStale,
isDebouncing, isDebouncing,
isLoading,
lastTrade, lastTrade,
tradeType, tradeType,
]) ])
......
...@@ -8,13 +8,18 @@ interface PollingOptions<T> { ...@@ -8,13 +8,18 @@ interface PollingOptions<T> {
// If true, any cached result will be returned, but no new fetch will be initiated. // If true, any cached result will be returned, but no new fetch will be initiated.
debounce?: boolean debounce?: boolean
// If stale, a result will not be returned, and a new fetch will be immediately initiated. // If stale, any cached result will be returned, and a new fetch will be initiated.
staleCallback?: (value: T) => boolean staleCallback?: (value: T) => boolean
pollingInterval?: number pollingInterval?: number
keepUnusedDataFor?: number keepUnusedDataFor?: number
} }
interface CacheEntry<T> {
ttl: number | null // null denotes a pending fetch
result?: T
}
export default function usePoll<T>( export default function usePoll<T>(
fetch: () => Promise<T>, fetch: () => Promise<T>,
key = '', key = '',
...@@ -25,7 +30,7 @@ export default function usePoll<T>( ...@@ -25,7 +30,7 @@ export default function usePoll<T>(
keepUnusedDataFor = DEFAULT_KEEP_UNUSED_DATA_FOR, keepUnusedDataFor = DEFAULT_KEEP_UNUSED_DATA_FOR,
}: PollingOptions<T> }: PollingOptions<T>
): T | undefined { ): T | undefined {
const cache = useMemo(() => new Map<string, { ttl: number; result?: T }>(), []) const cache = useMemo(() => new Map<string, CacheEntry<T>>(), [])
const [, setData] = useState<{ key: string; result?: T }>({ key }) const [, setData] = useState<{ key: string; result?: T }>({ key })
useEffect(() => { useEffect(() => {
...@@ -35,29 +40,33 @@ export default function usePoll<T>( ...@@ -35,29 +40,33 @@ export default function usePoll<T>(
const entry = cache.get(key) const entry = cache.get(key)
const isStale = staleCallback && entry?.result !== undefined ? staleCallback(entry.result) : false const isStale = staleCallback && entry?.result !== undefined ? staleCallback(entry.result) : false
if (entry && entry.ttl + keepUnusedDataFor > Date.now() && !isStale) { if (entry) {
// If there is a fresh entry, return it and queue the next poll. // If there is not a pending fetch (and there should be), queue one.
setData({ key, result: entry.result }) if (entry.ttl) {
timeout = setTimeout(poll, Math.max(0, entry.ttl - Date.now())) if (isStale) {
poll() // stale results should be refetched immediately
} else if (entry.ttl && entry.ttl + keepUnusedDataFor > Date.now()) {
timeout = setTimeout(poll, Math.max(0, entry.ttl - Date.now()))
}
}
} else { } else {
// Otherwise, set a new entry (to avoid duplicate polling) and trigger a poll immediately. // If there is no cached entry, trigger a poll immediately.
cache.set(key, { ttl: Date.now() + pollingInterval })
setData({ key })
poll() poll()
} }
setData({ key, result: entry?.result })
return () => { return () => {
clearTimeout(timeout) clearTimeout(timeout)
} }
async function poll(ttl = Date.now() + pollingInterval) { async function poll(ttl = Date.now() + pollingInterval) {
timeout = setTimeout(poll, pollingInterval) timeout = setTimeout(poll, pollingInterval) // queue the next poll
const result = await fetch() cache.set(key, { ttl: null, ...cache.get(key) }) // mark the entry as a pending fetch
// Always set the result in the cache, but only set it as data if the key is still being queried. // Always set the result in the cache, but only set it as data if the key is still being queried.
const result = await fetch()
cache.set(key, { ttl, result }) cache.set(key, { ttl, result })
setData((data) => { setData((data) => (data.key === key ? { key, result } : data))
return data.key === key ? { key, result } : data
})
} }
}, [cache, debounce, fetch, keepUnusedDataFor, key, pollingInterval, staleCallback]) }, [cache, debounce, fetch, keepUnusedDataFor, key, pollingInterval, staleCallback])
...@@ -67,7 +76,7 @@ export default function usePoll<T>( ...@@ -67,7 +76,7 @@ export default function usePoll<T>(
const now = Date.now() const now = Date.now()
cache.forEach(({ ttl }, key) => { cache.forEach(({ ttl }, key) => {
if (ttl + keepUnusedDataFor <= now) { if (ttl && ttl + keepUnusedDataFor <= now) {
cache.delete(key) cache.delete(key)
} }
}) })
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment