-
Notifications
You must be signed in to change notification settings - Fork 369
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
🔬 Make useRawLogs more efficient and responsive #1151
Changes from 2 commits
5b4f0fd
620bdba
5e10010
0611e13
9450e66
2617464
33d7551
1bade54
5526c85
68a2aec
cc92969
3fdef33
5287f89
adb11a6
7c429dd
3e05b97
99c4639
7447756
8501c3e
53c335b
548d973
2363153
17d24e4
3371c32
af09026
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -1,10 +1,75 @@ | ||||||||||||||||||
import { useEffect, useMemo, useState } from 'react' | ||||||||||||||||||
import { useEffect, useMemo, useRef, useState } from 'react' | ||||||||||||||||||
import { useEthers } from './useEthers' | ||||||||||||||||||
import { useReadonlyNetworks } from '../providers/network/readonlyNetworks' | ||||||||||||||||||
import { useBlockNumbers, useBlockNumber } from '../hooks' | ||||||||||||||||||
import { useBlockNumbers, useBlockNumber, useConfig } from '../hooks' | ||||||||||||||||||
import { QueryParams } from '../constants/type/QueryParams' | ||||||||||||||||||
import type { Filter, FilterByBlockHash, Log } from '@ethersproject/abstract-provider' | ||||||||||||||||||
import { Falsy } from '../model/types' | ||||||||||||||||||
import { ChainId } from '../constants' | ||||||||||||||||||
|
||||||||||||||||||
function deepEqual(obj1: any, obj2: any) { | ||||||||||||||||||
if (obj1 === obj2) | ||||||||||||||||||
// it's just the same object. No need to compare. | ||||||||||||||||||
return true | ||||||||||||||||||
|
||||||||||||||||||
if (obj1 == null) return obj1 == null | ||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
I believe this is a typo otherwise There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't know if this is intended behaviour but it seems it would return There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch! |
||||||||||||||||||
|
||||||||||||||||||
if (obj2 == null) return false | ||||||||||||||||||
|
||||||||||||||||||
if (isPrimitive(obj1) && isPrimitive(obj2)) | ||||||||||||||||||
// compare primitives | ||||||||||||||||||
return obj1 === obj2 | ||||||||||||||||||
|
||||||||||||||||||
if (Object.keys(obj1).length !== Object.keys(obj2).length) return false | ||||||||||||||||||
|
||||||||||||||||||
// compare objects with same number of keys | ||||||||||||||||||
for (const key in obj1) { | ||||||||||||||||||
if (!(key in obj2)) return false //other object doesn't have this prop | ||||||||||||||||||
if (!deepEqual(obj1[key], obj2[key])) return false | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
return true | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
function isPrimitive(obj: any) { | ||||||||||||||||||
return obj !== Object(obj) | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
function useResolvedFilter( | ||||||||||||||||||
filter: Filter | FilterByBlockHash | Promise<Filter | FilterByBlockHash> | Falsy | ||||||||||||||||||
): Filter | FilterByBlockHash | Falsy { | ||||||||||||||||||
const [resolvedFilter, setResolvedFilter] = useState<Filter | FilterByBlockHash | Falsy>( | ||||||||||||||||||
filter instanceof Promise ? undefined : filter | ||||||||||||||||||
) | ||||||||||||||||||
|
||||||||||||||||||
useEffect(() => { | ||||||||||||||||||
let active = true // Flag to prevent setting state after unmount | ||||||||||||||||||
|
||||||||||||||||||
const resolveFilter = async () => { | ||||||||||||||||||
let _filter: Filter | FilterByBlockHash | Falsy = undefined | ||||||||||||||||||
|
||||||||||||||||||
if (filter instanceof Promise) { | ||||||||||||||||||
_filter = await filter | ||||||||||||||||||
} else { | ||||||||||||||||||
_filter = filter | ||||||||||||||||||
} | ||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
If you await non promise value it will just return it as value otherwise it will resolve promise https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await#return_value There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Clean |
||||||||||||||||||
|
||||||||||||||||||
if (!deepEqual(_filter, resolvedFilter)) { | ||||||||||||||||||
if (active) { | ||||||||||||||||||
setResolvedFilter(_filter) | ||||||||||||||||||
} | ||||||||||||||||||
} | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
void resolveFilter() | ||||||||||||||||||
|
||||||||||||||||||
return () => { | ||||||||||||||||||
active = false // Cleanup to prevent state update after component unmounts | ||||||||||||||||||
} | ||||||||||||||||||
}) | ||||||||||||||||||
|
||||||||||||||||||
return resolvedFilter | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
/** | ||||||||||||||||||
* Returns all blockchain logs given a block filter. | ||||||||||||||||||
|
@@ -25,21 +90,104 @@ export function useRawLogs( | |||||||||||||||||
const blockNumbers = useBlockNumbers() | ||||||||||||||||||
|
||||||||||||||||||
const [logs, setLogs] = useState<Log[] | undefined>() | ||||||||||||||||||
const [lastContractAddress, setLastContractAddress] = useState<string | undefined>() | ||||||||||||||||||
const [lastTopics, setLastTopics] = useState<string | undefined>() | ||||||||||||||||||
const [lastChainId, setLastChainId] = useState<ChainId | undefined>() | ||||||||||||||||||
const [lastBlockNumber, setLastBlockNumber] = useState<number | undefined>() | ||||||||||||||||||
const resolvedFilter = useResolvedFilter(filter) | ||||||||||||||||||
|
||||||||||||||||||
const isLoadingRef = useRef(false) | ||||||||||||||||||
|
||||||||||||||||||
const { chainId } = queryParams | ||||||||||||||||||
const { chainId, isStatic } = queryParams | ||||||||||||||||||
const config = useConfig() | ||||||||||||||||||
const refresh = queryParams?.refresh ?? config.refresh | ||||||||||||||||||
|
||||||||||||||||||
const [provider, blockNumber] = useMemo( | ||||||||||||||||||
() => (chainId ? [providers[chainId], blockNumbers[chainId]] : [library, _blockNumber]), | ||||||||||||||||||
[providers, library, blockNumbers, _blockNumber, chainId] | ||||||||||||||||||
) | ||||||||||||||||||
|
||||||||||||||||||
async function updateLogs() { | ||||||||||||||||||
setLogs(!filter ? undefined : await provider?.getLogs(filter)) | ||||||||||||||||||
} | ||||||||||||||||||
const deps: any[] = [provider] | ||||||||||||||||||
|
||||||||||||||||||
const filterTopicsAsJson = resolvedFilter && JSON.stringify(resolvedFilter.topics) | ||||||||||||||||||
|
||||||||||||||||||
// Push the filter elements to the dependencies. We do this individually b/c hook dependency checks are shallow | ||||||||||||||||||
deps.push(resolvedFilter && resolvedFilter.address) | ||||||||||||||||||
deps.push(filterTopicsAsJson) | ||||||||||||||||||
deps.push(resolvedFilter && (resolvedFilter as FilterByBlockHash).blockHash) | ||||||||||||||||||
deps.push(resolvedFilter && (resolvedFilter as Filter).fromBlock) | ||||||||||||||||||
deps.push(resolvedFilter && (resolvedFilter as Filter).toBlock) | ||||||||||||||||||
|
||||||||||||||||||
// Push the block number if we are not static | ||||||||||||||||||
deps.push(!isStatic && refresh !== 'never' ? blockNumber : 0) | ||||||||||||||||||
|
||||||||||||||||||
useEffect(() => { | ||||||||||||||||||
let active = true // Flag to indicate if the effect is still in effect | ||||||||||||||||||
|
||||||||||||||||||
async function updateLogs() { | ||||||||||||||||||
if (isLoadingRef.current || !active) { | ||||||||||||||||||
// We are already loading, don't start another request | ||||||||||||||||||
// or the component has been unmounted | ||||||||||||||||||
return | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
isLoadingRef.current = true | ||||||||||||||||||
try { | ||||||||||||||||||
let filterChanged = true | ||||||||||||||||||
if ( | ||||||||||||||||||
chainId === lastChainId && | ||||||||||||||||||
resolvedFilter && | ||||||||||||||||||
lastContractAddress === resolvedFilter.address && | ||||||||||||||||||
lastTopics === filterTopicsAsJson | ||||||||||||||||||
) { | ||||||||||||||||||
// The filter did not change | ||||||||||||||||||
filterChanged = false | ||||||||||||||||||
} else { | ||||||||||||||||||
// Filter changed. Reset logs | ||||||||||||||||||
setLogs(undefined) | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
if (!filterChanged) { | ||||||||||||||||||
if (isStatic || refresh === 'never') { | ||||||||||||||||||
// Only update logs if contract address or topics changed | ||||||||||||||||||
return | ||||||||||||||||||
} else if (typeof refresh === 'number') { | ||||||||||||||||||
// Only update logs if the block number has increased by the refresh interval | ||||||||||||||||||
if (blockNumber && lastBlockNumber && blockNumber - lastBlockNumber < refresh) { | ||||||||||||||||||
return | ||||||||||||||||||
} | ||||||||||||||||||
} | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
// Shallow copy the criteria to later store it | ||||||||||||||||||
// This is necessary because the resolved filter can change after the async call, leading to a mismatch and | ||||||||||||||||||
// thus logs being stale | ||||||||||||||||||
const usedContractAddress = !resolvedFilter ? undefined : resolvedFilter.address | ||||||||||||||||||
const usedTopics = !resolvedFilter ? undefined : JSON.stringify(resolvedFilter.topics) | ||||||||||||||||||
const usedChainId = chainId | ||||||||||||||||||
const usedBlockNumber = blockNumber | ||||||||||||||||||
|
||||||||||||||||||
const rawLogs = !resolvedFilter ? undefined : await provider?.getLogs(resolvedFilter) | ||||||||||||||||||
|
||||||||||||||||||
// Active state could have changed while we were waiting for the logs. Don't update state if it has | ||||||||||||||||||
if (active) { | ||||||||||||||||||
setLogs(rawLogs) | ||||||||||||||||||
setLastContractAddress(usedContractAddress) | ||||||||||||||||||
setLastTopics(usedTopics) | ||||||||||||||||||
setLastChainId(usedChainId) | ||||||||||||||||||
setLastBlockNumber(usedBlockNumber) | ||||||||||||||||||
} | ||||||||||||||||||
} finally { | ||||||||||||||||||
isLoadingRef.current = false | ||||||||||||||||||
} | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
void updateLogs() | ||||||||||||||||||
}, [provider, blockNumber]) | ||||||||||||||||||
|
||||||||||||||||||
return () => { | ||||||||||||||||||
active = false // Prevent state updates after the component has unmounted | ||||||||||||||||||
} | ||||||||||||||||||
}, deps) | ||||||||||||||||||
|
||||||||||||||||||
return logs | ||||||||||||||||||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe it's self explanatory enough no need to add comment