Skip to content

Commit

Permalink
fix(core): retry for infinite queries (issue #8046) (#8049)
Browse files Browse the repository at this point in the history
* Add reproduction test for infinite loop retries (issue #8046)

* ci: apply automated fixes

* fix: retry for infinite queries

The retryer lives above the fetchFn, and it re-runs the fetchFn whenever a retry happens. Usually, the fetchFn is a thin wrapper around the actual queryFn passed by the user. However, for infinite queries, it fetches all pages in a loop.
The retryer breaks out of this loop if an error occurs on e.g. the second page, and then retries by running the fetchFn - which will re-set the loop

This fix hoists the currentPage counter out of the fetchFn - into the closure created by onFetch. The outer closure is created from running `query.fetch` once, so it won't be re-set between retries.
The fix also re-writes the fetch loop to always take the `currentPage` into account, where it was previously treating the first page differently

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Dominik Dorfmeister <office@dorfmeister.cc>
  • Loading branch information
3 people authored Sep 12, 2024
1 parent d29c37a commit a106d63
Show file tree
Hide file tree
Showing 2 changed files with 89 additions and 19 deletions.
75 changes: 74 additions & 1 deletion packages/query-core/src/__tests__/infiniteQueryBehavior.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
import { waitFor } from '@testing-library/react'
import { CancelledError, InfiniteQueryObserver } from '..'
import { createQueryClient, queryKey, sleep } from './utils'
import type { InfiniteQueryObserverResult, QueryCache, QueryClient } from '..'
import type {
InfiniteData,
InfiniteQueryObserverResult,
QueryCache,
QueryClient,
} from '..'

describe('InfiniteQueryBehavior', () => {
let queryClient: QueryClient
Expand Down Expand Up @@ -323,4 +328,72 @@ describe('InfiniteQueryBehavior', () => {

unsubscribe()
})

test('InfiniteQueryBehavior should not enter an infinite loop when a page errors while retry is on #8046', async () => {
let errorCount = 0
const key = queryKey()

interface TestResponse {
data: Array<{ id: string }>
nextToken?: number
}

const fakeData = [
{ data: [{ id: 'item-1' }], nextToken: 1 },
{ data: [{ id: 'item-2' }], nextToken: 2 },
{ data: [{ id: 'item-3' }], nextToken: 3 },
{ data: [{ id: 'item-4' }] },
]

const fetchData = async ({ nextToken = 0 }: { nextToken?: number }) =>
new Promise<TestResponse>((resolve, reject) => {
setTimeout(() => {
if (nextToken == 2 && errorCount < 3) {
errorCount += 1
reject({ statusCode: 429 })
return
}
resolve(fakeData[nextToken] as TestResponse)
}, 10)
})

const observer = new InfiniteQueryObserver<
TestResponse,
Error,
InfiniteData<TestResponse>,
TestResponse,
typeof key,
number
>(queryClient, {
retry: 5,
staleTime: 0,
retryDelay: 10,

queryKey: key,
initialPageParam: 1,
getNextPageParam: (lastPage) => lastPage.nextToken,
queryFn: ({ pageParam }) => fetchData({ nextToken: pageParam }),
})

// Fetch Page 1
const page1Data = await observer.fetchNextPage()
expect(page1Data.data?.pageParams).toEqual([1])

// Fetch Page 2, as per the queryFn, this will reject 2 times then resolves
const page2Data = await observer.fetchNextPage()
expect(page2Data.data?.pageParams).toEqual([1, 2])

// Fetch Page 3
const page3Data = await observer.fetchNextPage()
expect(page3Data.data?.pageParams).toEqual([1, 2, 3])

// Now the real deal; re-fetching this query **should not** stamp into an
// infinite loop where the retryer every time restarts from page 1
// once it reaches the page where it errors.
// For this to work, we'd need to reset the error count so we actually retry
errorCount = 0
const reFetchedData = await observer.refetch()

expect(reFetchedData.data?.pageParams).toEqual([1, 2, 3])
})
})
33 changes: 15 additions & 18 deletions packages/query-core/src/infiniteQueryBehavior.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,15 @@ export function infiniteQueryBehavior<TQueryFnData, TError, TData, TPageParam>(
): QueryBehavior<TQueryFnData, TError, InfiniteData<TData, TPageParam>> {
return {
onFetch: (context, query) => {
const options = context.options as InfiniteQueryPageParamsOptions<TData>
const direction = context.fetchOptions?.meta?.fetchMore?.direction
const oldPages = context.state.data?.pages || []
const oldPageParams = context.state.data?.pageParams || []
let result: InfiniteData<unknown> = { pages: [], pageParams: [] }
let currentPage = 0

const fetchFn = async () => {
const options = context.options as InfiniteQueryPageParamsOptions<TData>
const direction = context.fetchOptions?.meta?.fetchMore?.direction
const oldPages = context.state.data?.pages || []
const oldPageParams = context.state.data?.pageParams || []
const empty = { pages: [], pageParams: [] }
let cancelled = false

const addSignalProperty = (object: unknown) => {
Object.defineProperty(object, 'signal', {
enumerable: true,
Expand Down Expand Up @@ -78,8 +79,6 @@ export function infiniteQueryBehavior<TQueryFnData, TError, TData, TPageParam>(
}
}

let result: InfiniteData<unknown>

// fetch next / previous page?
if (direction && oldPages.length) {
const previous = direction === 'backward'
Expand All @@ -92,22 +91,20 @@ export function infiniteQueryBehavior<TQueryFnData, TError, TData, TPageParam>(

result = await fetchPage(oldData, param, previous)
} else {
// Fetch first page
result = await fetchPage(
empty,
oldPageParams[0] ?? options.initialPageParam,
)

const remainingPages = pages ?? oldPages.length

// Fetch remaining pages
for (let i = 1; i < remainingPages; i++) {
const param = getNextPageParam(options, result)
// Fetch all pages
do {
const param =
currentPage === 0
? (oldPageParams[0] ?? options.initialPageParam)
: getNextPageParam(options, result)
if (param == null) {
break
}
result = await fetchPage(result, param)
}
currentPage++
} while (currentPage < remainingPages)
}

return result
Expand Down

0 comments on commit a106d63

Please sign in to comment.