perf(multicall): improve fetching code to allow for fetching immutable data like token symbols/names/decimals
This commit is contained in:
parent
87d24c404b
commit
5e486fca7f
@ -30,8 +30,25 @@ export function parseCallKey(callKey: string): Call {
|
||||
}
|
||||
}
|
||||
|
||||
export const addMulticallListeners = createAction<{ chainId: number; calls: Call[] }>('addMulticallListeners')
|
||||
export const removeMulticallListeners = createAction<{ chainId: number; calls: Call[] }>('removeMulticallListeners')
|
||||
interface ListenerOptions {
|
||||
// how often this data should be fetched, by default 1
|
||||
blocksPerFetch?: number
|
||||
}
|
||||
|
||||
export const addMulticallListeners = createAction<{ chainId: number; calls: Call[]; options?: ListenerOptions }>(
|
||||
'addMulticallListeners'
|
||||
)
|
||||
export const removeMulticallListeners = createAction<{ chainId: number; calls: Call[]; options?: ListenerOptions }>(
|
||||
'removeMulticallListeners'
|
||||
)
|
||||
export const fetchingMulticallResults = createAction<{ chainId: number; calls: Call[]; fetchingBlockNumber: number }>(
|
||||
'fetchingMulticallResults'
|
||||
)
|
||||
export const errorFetchingMulticallResults = createAction<{
|
||||
chainId: number
|
||||
calls: Call[]
|
||||
fetchingBlockNumber: number
|
||||
}>('errorFetchingMulticallResults')
|
||||
export const updateMulticallResults = createAction<{
|
||||
chainId: number
|
||||
blockNumber: number
|
||||
|
@ -53,11 +53,9 @@ function useCallsData(calls: (Call | undefined)[]): CallResult[] {
|
||||
[calls]
|
||||
)
|
||||
|
||||
const debouncedSerializedCallKeys = useDebounce(serializedCallKeys, 20)
|
||||
|
||||
// update listeners when there is an actual change that persists for at least 100ms
|
||||
useEffect(() => {
|
||||
const callKeys: string[] = JSON.parse(debouncedSerializedCallKeys)
|
||||
const callKeys: string[] = JSON.parse(serializedCallKeys)
|
||||
if (!chainId || callKeys.length === 0) return
|
||||
const calls = callKeys.map(key => parseCallKey(key))
|
||||
dispatch(
|
||||
@ -75,7 +73,7 @@ function useCallsData(calls: (Call | undefined)[]): CallResult[] {
|
||||
})
|
||||
)
|
||||
}
|
||||
}, [chainId, dispatch, debouncedSerializedCallKeys])
|
||||
}, [chainId, dispatch, serializedCallKeys])
|
||||
|
||||
return useMemo(
|
||||
() =>
|
||||
|
@ -1,10 +1,22 @@
|
||||
import { createReducer } from '@reduxjs/toolkit'
|
||||
import { addMulticallListeners, removeMulticallListeners, toCallKey, updateMulticallResults } from './actions'
|
||||
import {
|
||||
addMulticallListeners,
|
||||
errorFetchingMulticallResults,
|
||||
fetchingMulticallResults,
|
||||
removeMulticallListeners,
|
||||
toCallKey,
|
||||
updateMulticallResults
|
||||
} from './actions'
|
||||
|
||||
interface MulticallState {
|
||||
callListeners: {
|
||||
callListeners?: {
|
||||
// on a per-chain basis
|
||||
[chainId: number]: {
|
||||
[callKey: string]: number
|
||||
// stores for each call key the listeners' preferences
|
||||
[callKey: string]: {
|
||||
// stores how many listeners there are per each blocks per fetch preference
|
||||
[blocksPerFetch: number]: number
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,36 +25,71 @@ interface MulticallState {
|
||||
[callKey: string]: {
|
||||
data: string | null
|
||||
blockNumber: number
|
||||
fetchingBlockNumber?: number
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const initialState: MulticallState = {
|
||||
callListeners: {},
|
||||
callResults: {}
|
||||
}
|
||||
|
||||
export default createReducer(initialState, builder =>
|
||||
builder
|
||||
.addCase(addMulticallListeners, (state, { payload: { calls, chainId } }) => {
|
||||
state.callListeners[chainId] = state.callListeners[chainId] ?? {}
|
||||
.addCase(addMulticallListeners, (state, { payload: { calls, chainId, options: { blocksPerFetch = 1 } = {} } }) => {
|
||||
const listeners: MulticallState['callListeners'] = state.callListeners
|
||||
? state.callListeners
|
||||
: (state.callListeners = {})
|
||||
listeners[chainId] = listeners[chainId] ?? {}
|
||||
calls.forEach(call => {
|
||||
const callKey = toCallKey(call)
|
||||
state.callListeners[chainId][callKey] = (state.callListeners[chainId][callKey] ?? 0) + 1
|
||||
listeners[chainId][callKey] = listeners[chainId][callKey] ?? {}
|
||||
listeners[chainId][callKey][blocksPerFetch] = (listeners[chainId][callKey][blocksPerFetch] ?? 0) + 1
|
||||
})
|
||||
})
|
||||
.addCase(removeMulticallListeners, (state, { payload: { chainId, calls } }) => {
|
||||
if (!state.callListeners[chainId]) return
|
||||
.addCase(
|
||||
removeMulticallListeners,
|
||||
(state, { payload: { chainId, calls, options: { blocksPerFetch = 1 } = {} } }) => {
|
||||
const listeners: MulticallState['callListeners'] = state.callListeners
|
||||
? state.callListeners
|
||||
: (state.callListeners = {})
|
||||
|
||||
if (!listeners[chainId]) return
|
||||
calls.forEach(call => {
|
||||
const callKey = toCallKey(call)
|
||||
if (!listeners[chainId][callKey]) return
|
||||
if (!listeners[chainId][callKey][blocksPerFetch]) return
|
||||
|
||||
if (listeners[chainId][callKey][blocksPerFetch] === 1) {
|
||||
delete listeners[chainId][callKey][blocksPerFetch]
|
||||
} else {
|
||||
listeners[chainId][callKey][blocksPerFetch]--
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
.addCase(fetchingMulticallResults, (state, { payload: { chainId, fetchingBlockNumber, calls } }) => {
|
||||
state.callResults[chainId] = state.callResults[chainId] ?? {}
|
||||
calls.forEach(call => {
|
||||
const callKey = toCallKey(call)
|
||||
if (state.callListeners[chainId][callKey] === 1) {
|
||||
delete state.callListeners[chainId][callKey]
|
||||
} else {
|
||||
state.callListeners[chainId][callKey]--
|
||||
const current = state.callResults[chainId][callKey]
|
||||
if (current && current.blockNumber > fetchingBlockNumber) return
|
||||
state.callResults[chainId][callKey] = {
|
||||
...state.callResults[chainId][callKey],
|
||||
fetchingBlockNumber
|
||||
}
|
||||
})
|
||||
})
|
||||
.addCase(errorFetchingMulticallResults, (state, { payload: { fetchingBlockNumber, chainId, calls } }) => {
|
||||
state.callResults[chainId] = state.callResults[chainId] ?? {}
|
||||
calls.forEach(call => {
|
||||
const callKey = toCallKey(call)
|
||||
const current = state.callResults[chainId][callKey]
|
||||
if (current && current.fetchingBlockNumber !== fetchingBlockNumber) return
|
||||
delete current.fetchingBlockNumber
|
||||
})
|
||||
})
|
||||
.addCase(updateMulticallResults, (state, { payload: { chainId, results, blockNumber } }) => {
|
||||
state.callResults[chainId] = state.callResults[chainId] ?? {}
|
||||
Object.keys(results).forEach(callKey => {
|
||||
|
@ -7,48 +7,96 @@ import useDebounce from '../../hooks/useDebounce'
|
||||
import chunkArray from '../../utils/chunkArray'
|
||||
import { useBlockNumber } from '../application/hooks'
|
||||
import { AppDispatch, AppState } from '../index'
|
||||
import { parseCallKey, updateMulticallResults } from './actions'
|
||||
import {
|
||||
errorFetchingMulticallResults,
|
||||
fetchingMulticallResults,
|
||||
parseCallKey,
|
||||
updateMulticallResults
|
||||
} from './actions'
|
||||
|
||||
// chunk calls so we do not exceed the gas limit
|
||||
const CALL_CHUNK_SIZE = 250
|
||||
|
||||
/**
|
||||
* From the current all listeners state, return each call key mapped to the
|
||||
* minimum number of blocks per fetch. This is how often each key must be fetched.
|
||||
* @param allListeners the all listeners state
|
||||
* @param chainId the current chain id
|
||||
*/
|
||||
function activeListeningKeys(
|
||||
allListeners: AppState['multicall']['callListeners'],
|
||||
chainId?: number
|
||||
): { [callKey: string]: number } {
|
||||
if (!allListeners || !chainId) return {}
|
||||
const listeners = allListeners[chainId]
|
||||
if (!listeners) return {}
|
||||
|
||||
return Object.keys(listeners).reduce<{ [callKey: string]: number }>((memo, callKey) => {
|
||||
const keyListeners = listeners[callKey]
|
||||
|
||||
memo[callKey] = Object.keys(keyListeners)
|
||||
.filter(key => keyListeners[parseInt(key)] > 0)
|
||||
.reduce((previousMin, current) => {
|
||||
return Math.min(previousMin, parseInt(current))
|
||||
}, Infinity)
|
||||
return memo
|
||||
}, {})
|
||||
}
|
||||
|
||||
export default function Updater() {
|
||||
const dispatch = useDispatch<AppDispatch>()
|
||||
const state = useSelector<AppState, AppState['multicall']>(state => state.multicall)
|
||||
// wait for listeners to settle before triggering updates
|
||||
const debouncedListeners = useDebounce(state.callListeners, 100)
|
||||
const latestBlockNumber = useBlockNumber()
|
||||
const { chainId } = useActiveWeb3React()
|
||||
const multicallContract = useMulticallContract()
|
||||
|
||||
const listeningKeys = useMemo(() => {
|
||||
if (!chainId || !state.callListeners[chainId]) return []
|
||||
return Object.keys(state.callListeners[chainId]).filter(callKey => state.callListeners[chainId][callKey] > 0)
|
||||
}, [state.callListeners, chainId])
|
||||
|
||||
const debouncedResults = useDebounce(state.callResults, 20)
|
||||
const debouncedListeningKeys = useDebounce(listeningKeys, 20)
|
||||
const listeningKeys: { [callKey: string]: number } = useMemo(() => {
|
||||
return activeListeningKeys(debouncedListeners, chainId)
|
||||
}, [debouncedListeners, chainId])
|
||||
|
||||
const unserializedOutdatedCallKeys = useMemo(() => {
|
||||
if (!chainId || !debouncedResults[chainId]) return debouncedListeningKeys
|
||||
if (!latestBlockNumber) return []
|
||||
// wait for these before fetching any data
|
||||
if (!chainId || !latestBlockNumber) return []
|
||||
// no results at all, load everything
|
||||
if (!state.callResults[chainId]) return Object.keys(listeningKeys)
|
||||
|
||||
return debouncedListeningKeys.filter(key => {
|
||||
const data = debouncedResults[chainId][key]
|
||||
return !data || data.blockNumber < latestBlockNumber
|
||||
return Object.keys(listeningKeys).filter(callKey => {
|
||||
const blocksPerFetch = listeningKeys[callKey]
|
||||
|
||||
const data = state.callResults[chainId][callKey]
|
||||
// no data, must fetch
|
||||
if (!data) return true
|
||||
|
||||
// already fetching it
|
||||
if (data.fetchingBlockNumber && data.fetchingBlockNumber >= latestBlockNumber + blocksPerFetch) return false
|
||||
|
||||
// data block number is older than blocksPerFetch blocks
|
||||
return data.blockNumber <= latestBlockNumber - blocksPerFetch
|
||||
})
|
||||
}, [chainId, debouncedResults, debouncedListeningKeys, latestBlockNumber])
|
||||
}, [chainId, state.callResults, listeningKeys, latestBlockNumber])
|
||||
|
||||
const serializedOutdatedCallKeys = useMemo(() => JSON.stringify(unserializedOutdatedCallKeys.sort()), [
|
||||
unserializedOutdatedCallKeys
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
if (!latestBlockNumber || !chainId || !multicallContract) return
|
||||
|
||||
const outdatedCallKeys: string[] = JSON.parse(serializedOutdatedCallKeys)
|
||||
if (!multicallContract || !chainId || outdatedCallKeys.length === 0) return
|
||||
if (outdatedCallKeys.length === 0) return
|
||||
const calls = outdatedCallKeys.map(key => parseCallKey(key))
|
||||
|
||||
const chunkedCalls = chunkArray(calls, CALL_CHUNK_SIZE)
|
||||
|
||||
console.debug('Firing off chunked calls', chunkedCalls)
|
||||
dispatch(
|
||||
fetchingMulticallResults({
|
||||
calls,
|
||||
chainId,
|
||||
fetchingBlockNumber: latestBlockNumber
|
||||
})
|
||||
)
|
||||
|
||||
chunkedCalls.forEach((chunk, index) =>
|
||||
multicallContract
|
||||
@ -73,9 +121,16 @@ export default function Updater() {
|
||||
})
|
||||
.catch((error: any) => {
|
||||
console.error('Failed to fetch multicall chunk', chunk, chainId, error)
|
||||
dispatch(
|
||||
errorFetchingMulticallResults({
|
||||
calls: chunk,
|
||||
chainId,
|
||||
fetchingBlockNumber: latestBlockNumber
|
||||
})
|
||||
)
|
||||
})
|
||||
)
|
||||
}, [chainId, multicallContract, dispatch, serializedOutdatedCallKeys])
|
||||
}, [chainId, multicallContract, dispatch, serializedOutdatedCallKeys, latestBlockNumber])
|
||||
|
||||
return null
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user