diff --git a/src/pages/Earn/useFarmRegistry.ts b/src/pages/Earn/useFarmRegistry.ts index 5917c1ee69d..0ba3d195f1c 100644 --- a/src/pages/Earn/useFarmRegistry.ts +++ b/src/pages/Earn/useFarmRegistry.ts @@ -6,7 +6,7 @@ import { ChainId, Percent, TokenAmount } from '@ubeswap/sdk' import { ethers } from 'ethers' import { FarmDataEvent, FarmInfoEvent, LPInfoEvent } from 'generated/FarmRegistry' import { useFarmRegistryContract } from 'hooks/useContract' -import React, { useEffect } from 'react' +import React, { useEffect, useMemo } from 'react' import fetchEvents from 'utils/fetchEvents' import { farmRegistryAddresses } from '../../constants' @@ -72,8 +72,12 @@ export const useFarmRegistry = () => { const farmRegistryContract = useFarmRegistryContract(farmRegistryAddress) const client = useApolloClient() const [farmSummaries, setFarmSummaries] = React.useState([]) - const olderFarmInfoEvents = cachedFarmInfoEvents.map((e) => e.returnValues) - const olderLpInfoEvents = cachedLpInfoEvents.map((e) => e.returnValues) + const olderFarmInfoEvents = useMemo(() => { + return cachedFarmInfoEvents.map((e) => e.returnValues) + }, []) + const olderLpInfoEvents = useMemo(() => { + return cachedLpInfoEvents.map((e) => e.returnValues) + }, []) const call = React.useCallback(async () => { if (!farmRegistryAddress || !farmRegistryContract || !client) return diff --git a/src/utils/concurrencyLimiter.ts b/src/utils/concurrencyLimiter.ts new file mode 100644 index 00000000000..c23c651ee84 --- /dev/null +++ b/src/utils/concurrencyLimiter.ts @@ -0,0 +1,74 @@ +interface Options { + concurrency?: number + maxQueueSize?: number +} + +export default function makeConcurrencyLimited( + function_: (...arguments_: Arguments) => PromiseLike, + options?: Options | number +): (...arguments_: Arguments) => Promise { + let concurrency = 2 + let maxQueueSize = 20 + if (typeof options == 'number') { + concurrency = options + maxQueueSize = options * 10 + } else { + concurrency = options?.concurrency || 3 + maxQueueSize = options?.maxQueueSize || 100 + } + + if (!((Number.isInteger(concurrency) || concurrency === Number.POSITIVE_INFINITY) && concurrency > 0)) { + throw new TypeError('Expected `concurrency` to be a number from 1 and up') + } + + const queue: (() => Promise)[] = [] + let activeCount = 0 + + const next = () => { + activeCount-- + + if (queue.length > 0) { + queue.shift()?.() + } + } + + const run = async (resolve: (value: ReturnType | PromiseLike) => void, arguments_: Arguments) => { + activeCount++ + + const result = (async () => function_(...arguments_))() + + resolve(result) + + try { + await result + } catch { + // ignore + } + + next() + } + + const enqueue = (resolve: (value: ReturnType | PromiseLike) => void, arguments_: Arguments) => { + queue.push(run.bind(undefined, resolve, arguments_)) + ;(async () => { + // This function needs to wait until the next microtask before comparing + // `activeCount` to `concurrency`, because `activeCount` is updated asynchronously + // when the run function is dequeued and called. The comparison in the if-statement + // needs to happen asynchronously as well to get an up-to-date value for `activeCount`. + await Promise.resolve() + + if (activeCount < concurrency && queue.length > 0) { + queue.shift()?.() + } + })() + } + + return (...arguments_: Arguments) => { + return new Promise((resolve, reject) => { + if (queue.length > maxQueueSize) { + reject(new Error('Too many task')) + } + enqueue(resolve, arguments_) + }) + } +} diff --git a/src/utils/fetchEvents.ts b/src/utils/fetchEvents.ts index 17842a8b851..8e89e9a0c38 100644 --- a/src/utils/fetchEvents.ts +++ b/src/utils/fetchEvents.ts @@ -2,16 +2,18 @@ import { BlockTag } from '@ethersproject/abstract-provider' import { JsonRpcProvider } from '@ethersproject/providers' import { ChainId } from '@ubeswap/sdk' import { BaseContract, Event, EventFilter } from 'ethers' +import makeConcurrencyLimited from 'utils/concurrencyLimiter' +import pMemoize from 'utils/promiseMemoize' import { EVENT_FETCH_RPC_URLS } from '../constants' -export default async function fetchEvents( +async function eventFetcher( contract: BaseContract, filter: EventFilter, fromBlockOrBlockhash?: BlockTag | undefined, toBlock?: BlockTag | undefined ): Promise { - const chainId = (await contract.provider.getNetwork()).chainId + const chainId = (contract.provider as any)._network.chainId const promises = [contract.queryFilter(filter, fromBlockOrBlockhash, toBlock)] @@ -20,7 +22,24 @@ export default async function fetchEvents( const alternativeProvider = new JsonRpcProvider(alternativeRpc) promises.push(contract.connect(alternativeProvider).queryFilter(filter, fromBlockOrBlockhash, toBlock)) } - const result = await Promise.any(promises) return result as unknown as T[] } + +function generateCacheKey( + arguments_: [ + contract: BaseContract, + filter: EventFilter, + fromBlockOrBlockhash?: BlockTag | undefined, + toBlock?: BlockTag | undefined + ] +): string { + return arguments_[0].address + '-' + JSON.stringify(arguments_[1]) + '-' + arguments_[2] + '-' + arguments_[3] +} + +const rateLimited = makeConcurrencyLimited(eventFetcher, 3) +const memoized = pMemoize(rateLimited, { + cacheKey: generateCacheKey, +}) + +export default memoized diff --git a/src/utils/promiseMemoize.ts b/src/utils/promiseMemoize.ts new file mode 100644 index 00000000000..17d2cb54cc1 --- /dev/null +++ b/src/utils/promiseMemoize.ts @@ -0,0 +1,157 @@ +/** + * This code is taken from https://github.com/sindresorhus/p-memoize + * Made some cleanup + */ + +type AsyncFunction = (...arguments_: any[]) => Promise +type AsyncReturnType = Awaited> + +type AnyAsyncFunction = (...arguments_: readonly any[]) => Promise + +const cacheStore = new WeakMap | false>() + +export type CacheStorage = { + has: (key: KeyType) => Promise | boolean + get: (key: KeyType) => Promise | ValueType | undefined + set: (key: KeyType, value: ValueType) => Promise | unknown + delete: (key: KeyType) => unknown + clear?: () => unknown +} + +export type Options = { + /** + Determines the cache key for storing the result based on the function arguments. By default, __only the first argument is considered__ and it only works with [primitives](https://developer.mozilla.org/en-US/docs/Glossary/Primitive). + + A `cacheKey` function can return any type supported by `Map` (or whatever structure you use in the `cache` option). + + You can have it cache **all** the arguments by value with `JSON.stringify`, if they are compatible: + + ``` + import pMemoize from 'p-memoize'; + + pMemoize(function_, {cacheKey: JSON.stringify}); + ``` + + Or you can use a more full-featured serializer like [serialize-javascript](https://github.com/yahoo/serialize-javascript) to add support for `RegExp`, `Date` and so on. + + ``` + import pMemoize from 'p-memoize'; + import serializeJavascript from 'serialize-javascript'; + + pMemoize(function_, {cacheKey: serializeJavascript}); + ``` + + @default arguments_ => arguments_[0] + @example arguments_ => JSON.stringify(arguments_) + */ + readonly cacheKey?: (arguments_: Parameters) => CacheKeyType + + /** + Use a different cache storage. Must implement the following methods: `.has(key)`, `.get(key)`, `.set(key, value)`, `.delete(key)`, and optionally `.clear()`. You could for example use a `WeakMap` instead or [`quick-lru`](https://github.com/sindresorhus/quick-lru) for a LRU cache. To disable caching so that only concurrent executions resolve with the same value, pass `false`. + + @default new Map() + @example new WeakMap() + */ + readonly cache?: CacheStorage> | false +} + +/** +[Memoize](https://en.wikipedia.org/wiki/Memoization) functions - An optimization used to speed up consecutive function calls by caching the result of calls with identical input. + +@param fn - Function to be memoized. + +@example +``` +import {setTimeout as delay} from 'node:timer/promises'; +import pMemoize from 'p-memoize'; +import got from 'got'; + +const memoizedGot = pMemoize(got); + +await memoizedGot('https://sindresorhus.com'); + +// This call is cached +await memoizedGot('https://sindresorhus.com'); + +await delay(2000); + +// This call is not cached as the cache has expired +await memoizedGot('https://sindresorhus.com'); +``` +*/ +export default function pMemoize( + fn: FunctionToMemoize, + { + cacheKey = ([firstArgument]) => firstArgument as CacheKeyType, + cache = new Map>(), + }: Options = {} +): FunctionToMemoize { + // Promise objects can't be serialized so we keep track of them internally and only provide their resolved values to `cache` + // `Promise>` is used instead of `ReturnType` because promise properties are not kept + const promiseCache = new Map>>() + + const memoized = function ( + this: any, + ...arguments_: Parameters + ): Promise> { + // eslint-disable-line @typescript-eslint/promise-function-async + const key = cacheKey(arguments_) + + if (promiseCache.has(key)) { + return promiseCache.get(key) as Promise>> + } + + const promise = (async () => { + try { + if (cache && (await cache.has(key))) { + return await cache.get(key) + } + + const promise = fn.apply(this, arguments_) as Promise> + + const result = await promise + + try { + return result + } finally { + if (cache) { + await cache.set(key, result) + } + } + } finally { + promiseCache.delete(key) + } + })() as Promise> + + promiseCache.set(key, promise) + + return promise + } as FunctionToMemoize + + cacheStore.set(memoized, cache) + + return memoized +} + +/** +Clear all cached data of a memoized function. + +@param fn - Memoized function. +*/ +/*export function pMemoizeClear(fn: AnyAsyncFunction): void { + if (!cacheStore.has(fn)) { + throw new TypeError("Can't clear a function that was not memoized!") + } + + const cache = cacheStore.get(fn) + + if (!cache) { + throw new TypeError("Can't clear a function that doesn't use a cache!") + } + + if (typeof cache.clear !== 'function') { + throw new TypeError("The cache Map can't be cleared!") + } + + cache.clear() +}*/