Skip to content
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

Merged
merged 25 commits into from
May 27, 2024
Merged
Changes from 2 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
5b4f0fd
Make useRawLogs more efficient and responsive
TylerEther Apr 21, 2024
620bdba
Improve useRawLogs
TylerEther Apr 30, 2024
5e10010
Fix and simplify nullish checking in deepEqual
TylerEther May 9, 2024
0611e13
Remove comment in deepEqual
TylerEther May 9, 2024
9450e66
Cleanup useResolvedFilter
TylerEther May 9, 2024
2617464
Move isPrimitive and deepEqual to common helper
TylerEther May 9, 2024
33d7551
Fix bug in deepEqual
TylerEther May 9, 2024
1bade54
Add comment to deepEqual
TylerEther May 9, 2024
5526c85
Make slight deepEqual performance improvement
TylerEther May 9, 2024
68a2aec
Add tests for deepEqual and isPrimitive
TylerEther May 9, 2024
cc92969
Export common in helpers
TylerEther May 9, 2024
3fdef33
Extract useResolvedFilter to useResolvedPromise
TylerEther May 9, 2024
5287f89
Add tests for useResolvedPromise
TylerEther May 9, 2024
adb11a6
Remove unused import
TylerEther May 9, 2024
7c429dd
Fix code smell
TylerEther May 9, 2024
3e05b97
Disable unused var linting
TylerEther May 9, 2024
99c4639
Improve performance of useResolvedPromise
TylerEther May 9, 2024
7447756
Fix staleness issue
TylerEther May 9, 2024
8501c3e
Fix bugs in useResolvedPromise
TylerEther May 9, 2024
53c335b
Disable buggy optimization
TylerEther May 9, 2024
548d973
Merge branch 'TrueFiEng:master' into use-raw-logs-improvements
TylerEther May 13, 2024
2363153
Merge branch 'master' into use-raw-logs-improvements
JustynaBroniszewska May 15, 2024
17d24e4
Merge branch 'master' into use-raw-logs-improvements
nezouse May 17, 2024
3371c32
Create hot-pots-tap.md
nezouse May 17, 2024
af09026
Merge branch 'master' into use-raw-logs-improvements
nezouse May 17, 2024
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
162 changes: 155 additions & 7 deletions packages/core/src/hooks/useRawLogs.ts
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (obj1 === obj2)
// it's just the same object. No need to compare.
return true
if (obj1 === obj2) return true

I believe it's self explanatory enough no need to add comment


if (obj1 == null) return obj1 == null
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (obj1 == null) return obj1 == null
if (obj1 == null) return obj2 == null

I believe this is a typo

otherwise deepEqual(null, 2) would return true

Copy link
Contributor

Choose a reason for hiding this comment

The 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
deepEqual(null, undefined) as true

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let _filter: Filter | FilterByBlockHash | Falsy = undefined
if (filter instanceof Promise) {
_filter = await filter
} else {
_filter = filter
}
let _filter: Filter | FilterByBlockHash | Falsy = await filter

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

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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.
Expand All @@ -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
}