diff --git a/src/transactions/feed/TransactionFeed.test.tsx b/src/transactions/feed/TransactionFeed.test.tsx index 6be8d21043a..edaeafa0c34 100644 --- a/src/transactions/feed/TransactionFeed.test.tsx +++ b/src/transactions/feed/TransactionFeed.test.tsx @@ -330,7 +330,7 @@ describe('TransactionFeed', () => { }) it('renders correct status for a complete transaction', async () => { - mockFetch.mockResponse(JSON.stringify(MOCK_RESPONSE)) + mockFetch.mockResponse(JSON.stringify(MOCK_RESPONSE_NO_NEXT_PAGE)) const { getByTestId, getByText } = renderScreen({}) diff --git a/src/transactions/feed/queryHelper.ts b/src/transactions/feed/queryHelper.ts index 68a524b9dec..83407c5cc40 100644 --- a/src/transactions/feed/queryHelper.ts +++ b/src/transactions/feed/queryHelper.ts @@ -44,6 +44,8 @@ export interface QueryResponse { } } +type ActiveRequests = { [key in NetworkId]: boolean } + const TAG = 'transactions/feed/queryHelper' // Query poll interval @@ -79,6 +81,22 @@ export function useFetchTransactions(): QueryHookResult { // on the home feed, since they get cached in Redux -- this is just a network optimization. const allowedNetworkIds = getAllowedNetworkIds() + // Track which networks are currently fetching transactions via polling to avoid duplicate requests + const [activePollingRequests, setActivePollingRequests] = useState( + allowedNetworkIds.reduce((acc, networkId) => { + acc[networkId] = false + return acc + }, {} as ActiveRequests) + ) + + // Track which networks are currently fetching transactions via pagination to avoid duplicate requests + const [activePaginationRequests, setActivePaginationRequests] = useState( + allowedNetworkIds.reduce((acc, networkId) => { + acc[networkId] = false + return acc + }, {} as ActiveRequests) + ) + // Track cumulative transactions and most recent page info for all chains in one // piece of state so that they don't become out of sync. const [fetchedResult, setFetchedResult] = useState<{ @@ -107,61 +125,6 @@ export function useFetchTransactions(): QueryHookResult { const [counter, setCounter] = useState(0) useInterval(() => setCounter((n) => n + 1), POLL_INTERVAL) - const handleResult = (results: { [key in NetworkId]?: QueryResponse }, isPollResult: boolean) => { - Logger.info(TAG, `Fetched ${isPollResult ? 'new' : 'next page of'} transactions`) - - for (const [networkId, result] of Object.entries(results) as Array< - [NetworkId, QueryResponse] - >) { - const returnedTransactions = result.data?.tokenTransactionsV3?.transactions ?? [] - - const returnedPageInfo = result.data?.tokenTransactionsV3?.pageInfo ?? null - - // the initial feed fetch is from polling, exclude polled updates from that scenario - const isPolledUpdate = isPollResult && fetchedResult.pageInfo[networkId] !== null - - if (returnedTransactions.length || returnedPageInfo?.hasNextPage) { - setFetchedResult((prev) => ({ - transactions: deduplicateTransactions(prev.transactions, returnedTransactions), - // avoid updating pageInfo and hasReturnedTransactions for polled - // updates, as these variables are used for fetching the next pages - pageInfo: isPolledUpdate - ? prev.pageInfo - : { ...prev.pageInfo, [networkId]: returnedPageInfo }, - hasTransactionsOnCurrentPage: isPolledUpdate - ? prev.hasTransactionsOnCurrentPage - : { - ...prev.hasTransactionsOnCurrentPage, - [networkId]: returnedTransactions.length > 0, - }, - })) - - if (isPollResult && returnedTransactions.length) { - // We store the first page in redux to show them to the users when they open the app. - // Filter out now empty transactions to avoid redux issues - const nonEmptyTransactions = returnedTransactions.filter( - (returnedTransaction) => !isEmpty(returnedTransaction) - ) - const knownTransactionHashes = transactionHashesByNetwork[networkId] - let hasNewTransaction = false - - // Compare the new tx hashes with the ones we already have in redux - for (const tx of nonEmptyTransactions) { - if (!knownTransactionHashes || !knownTransactionHashes.has(tx.transactionHash)) { - hasNewTransaction = true - break // We only need one new tx justify a refresh - } - } - // If there are new transactions update transactions in redux and fetch balances - if (hasNewTransaction) { - dispatch(updateTransactions(networkId, nonEmptyTransactions)) - vibrateSuccess() - } - } - } - } - } - const handleError = (error: Error) => { Logger.error(TAG, 'Error while fetching transactions', error) } @@ -169,14 +132,58 @@ export function useFetchTransactions(): QueryHookResult { // Query for new transaction every POLL_INTERVAL const { loading, error } = useAsync( async () => { - const result = await queryTransactionsFeed({ + await queryTransactionsFeed({ address, localCurrencyCode, params: allowedNetworkIds.map((networkId) => { return { networkId } }), + onNetworkResponse: (networkId, result) => { + const returnedTransactions = result?.data.tokenTransactionsV3?.transactions ?? [] + const returnedPageInfo = result?.data.tokenTransactionsV3?.pageInfo ?? null + + // During the initial feed fetch we need to perform some first time setup + const isInitialFetch = fetchedResult.pageInfo[networkId] === null + if (returnedTransactions.length || returnedPageInfo?.hasNextPage) { + setFetchedResult((prev) => ({ + transactions: deduplicateTransactions(prev.transactions, returnedTransactions), + pageInfo: isInitialFetch + ? { ...prev.pageInfo, [networkId]: returnedPageInfo } + : prev.pageInfo, + hasTransactionsOnCurrentPage: isInitialFetch + ? { + ...prev.hasTransactionsOnCurrentPage, + [networkId]: returnedTransactions.length > 0, + } + : prev.hasTransactionsOnCurrentPage, + })) + } + if (returnedTransactions.length) { + // We store the first page in redux to show them to the users when they open the app. + // Filter out now empty transactions to avoid redux issues + const nonEmptyTransactions = returnedTransactions.filter( + (returnedTransaction) => !isEmpty(returnedTransaction) + ) + const knownTransactionHashes = transactionHashesByNetwork[networkId] + let hasNewTransaction = false + + // Compare the new tx hashes with the ones we already have in redux + for (const tx of nonEmptyTransactions) { + if (!knownTransactionHashes || !knownTransactionHashes.has(tx.transactionHash)) { + hasNewTransaction = true + break // We only need one new tx justify a refresh + } + } + // If there are new transactions update transactions in redux and fetch balances + if (hasNewTransaction) { + dispatch(updateTransactions(networkId, nonEmptyTransactions)) + vibrateSuccess() + } + } + }, + setActiveRequests: setActivePollingRequests, + activeRequests: activePollingRequests, }) - handleResult(result, true) }, [counter], { @@ -187,7 +194,7 @@ export function useFetchTransactions(): QueryHookResult { // Query for more transactions if requested useAsync( async () => { - if (!fetchingMoreTransactions || !anyNetworkHasMorePages(fetchedResult.pageInfo)) { + if (!anyNetworkHasMorePages(fetchedResult.pageInfo)) { setFetchingMoreTransactions(false) return } @@ -201,13 +208,28 @@ export function useFetchTransactions(): QueryHookResult { return { networkId, afterCursor: pageInfo?.endCursor } }) .filter((networkParams) => fetchedResult.pageInfo[networkParams.networkId]?.hasNextPage) - const result = await queryTransactionsFeed({ + await queryTransactionsFeed({ address, localCurrencyCode, params, + onNetworkResponse: (networkId, result) => { + const returnedTransactions = result?.data.tokenTransactionsV3?.transactions ?? [] + const returnedPageInfo = result?.data.tokenTransactionsV3?.pageInfo ?? null + if (returnedTransactions.length || returnedPageInfo?.hasNextPage) { + setFetchedResult((prev) => ({ + transactions: deduplicateTransactions(prev.transactions, returnedTransactions), + pageInfo: { ...prev.pageInfo, [networkId]: returnedPageInfo }, + hasTransactionsOnCurrentPage: { + ...prev.hasTransactionsOnCurrentPage, + [networkId]: returnedTransactions.length > 0, + }, + })) + } + }, + setActiveRequests: setActivePaginationRequests, + activeRequests: activePaginationRequests, }) setFetchingMoreTransactions(false) - handleResult(result, false) }, [fetchingMoreTransactions], { @@ -231,10 +253,10 @@ export function useFetchTransactions(): QueryHookResult { // // This has the effect of setting the fetchingMoreTransactions flag to true iff // - We are not already loading - // - There exists at least one chain that has futher pages + // - There exists at least one chain that has further pages // - EITHER we do not yet have enough TXs, OR NO chains whatsoever produced results // in the most recent round of fetching (which corresponds to case 2. above - // occuring for all chains simaltaneously) + // occurring for all chains simultaneously) const { transactions, pageInfo, hasTransactionsOnCurrentPage } = fetchedResult if ( !loading && @@ -282,6 +304,9 @@ async function queryTransactionsFeed({ address, localCurrencyCode, params, + onNetworkResponse, + setActiveRequests, + activeRequests, }: { address: string | null localCurrencyCode: string @@ -289,24 +314,35 @@ async function queryTransactionsFeed({ networkId: NetworkId afterCursor?: string }> -}): Promise<{ [key in NetworkId]?: QueryResponse }> { - const results = await Promise.all( - params.map(({ networkId, afterCursor }) => - queryChainTransactionsFeed({ + onNetworkResponse: (networkId: NetworkId, data: QueryResponse | null) => void + setActiveRequests: (updateFunc: (prevState: ActiveRequests) => ActiveRequests) => void + activeRequests: ActiveRequests +}): Promise { + // Launch all network requests without waiting for each to finish before starting the next + const requests = params.map(async ({ networkId, afterCursor }) => { + // Prevent duplicate requests for the same network + if (activeRequests[networkId]) { + Logger.info(TAG, `Skipping fetch for ${networkId} as it is already active`) + return + } else { + Logger.info(TAG, `Fetching transactions for ${networkId} with cursor: ${afterCursor}`) + setActiveRequests((prev) => ({ ...prev, [networkId]: true })) + } + try { + const result = await queryChainTransactionsFeed({ address, localCurrencyCode, networkId, afterCursor, }) - ) - ) - - return results.reduce((acc, result, index) => { - return { - ...acc, - [params[index].networkId]: result, + Logger.info(TAG, `Fetched transactions for ${networkId}`, result) + onNetworkResponse(networkId, result) // Update state as soon as data is available + } finally { + setActiveRequests((prev) => ({ ...prev, [networkId]: false })) } - }, {}) + }) + + await Promise.all(requests) // Wait for all requests to finish for use in useAsync hooks } async function queryChainTransactionsFeed({