@reduxjs/toolkit
Version:
The official, opinionated, batteries-included toolset for efficient Redux development
1,006 lines (817 loc) • 28.2 kB
text/typescript
import type { BaseQueryFn, FetchBaseQueryError } from '@reduxjs/toolkit/query'
import { createApi, retry } from '@reduxjs/toolkit/query'
import { setupApiStore } from '../../tests/utils/helpers'
beforeEach(() => {
vi.useFakeTimers()
})
const loopTimers = async (max: number = 12) => {
let count = 0
while (count < max) {
await vi.advanceTimersByTimeAsync(1)
vi.advanceTimersByTime(120_000)
count++
}
}
describe('configuration', () => {
test('retrying without any config options', async () => {
const baseBaseQuery = vi.fn<
Parameters<BaseQueryFn>,
ReturnType<BaseQueryFn>
>()
baseBaseQuery.mockResolvedValue({ error: 'rejected' })
const baseQuery = retry(baseBaseQuery)
const api = createApi({
baseQuery,
endpoints: (build) => ({
q1: build.query({
query: () => {},
}),
}),
})
const storeRef = setupApiStore(api, undefined, {
withoutTestLifecycles: true,
})
storeRef.store.dispatch(api.endpoints.q1.initiate({}))
await loopTimers(7)
expect(baseBaseQuery).toHaveBeenCalledTimes(6)
})
test('retrying with baseQuery config that overrides default behavior (maxRetries: 5)', async () => {
const baseBaseQuery = vi.fn<
Parameters<BaseQueryFn>,
ReturnType<BaseQueryFn>
>()
baseBaseQuery.mockResolvedValue({ error: 'rejected' })
const baseQuery = retry(baseBaseQuery, { maxRetries: 3 })
const api = createApi({
baseQuery,
endpoints: (build) => ({
q1: build.query({
query: () => {},
}),
}),
})
const storeRef = setupApiStore(api, undefined, {
withoutTestLifecycles: true,
})
storeRef.store.dispatch(api.endpoints.q1.initiate({}))
await loopTimers(5)
expect(baseBaseQuery).toHaveBeenCalledTimes(4)
})
test('retrying with endpoint config that overrides baseQuery config', async () => {
const baseBaseQuery = vi.fn<
Parameters<BaseQueryFn>,
ReturnType<BaseQueryFn>
>()
baseBaseQuery.mockResolvedValue({ error: 'rejected' })
const baseQuery = retry(baseBaseQuery, { maxRetries: 3 })
const api = createApi({
baseQuery,
endpoints: (build) => ({
q1: build.query({
query: () => {},
}),
q2: build.query({
query: () => {},
extraOptions: { maxRetries: 8 },
}),
}),
})
const storeRef = setupApiStore(api, undefined, {
withoutTestLifecycles: true,
})
storeRef.store.dispatch(api.endpoints.q1.initiate({}))
await loopTimers(5)
expect(baseBaseQuery).toHaveBeenCalledTimes(4)
baseBaseQuery.mockClear()
storeRef.store.dispatch(api.endpoints.q2.initiate({}))
await loopTimers(10)
expect(baseBaseQuery).toHaveBeenCalledTimes(9)
})
test('stops retrying a query after a success', async () => {
const baseBaseQuery = vi.fn<
Parameters<BaseQueryFn>,
ReturnType<BaseQueryFn>
>()
baseBaseQuery
.mockResolvedValueOnce({ error: 'rejected' })
.mockResolvedValueOnce({ error: 'rejected' })
.mockResolvedValue({ data: { success: true } })
const baseQuery = retry(baseBaseQuery, { maxRetries: 10 })
const api = createApi({
baseQuery,
endpoints: (build) => ({
q1: build.mutation({
query: () => {},
}),
}),
})
const storeRef = setupApiStore(api, undefined, {
withoutTestLifecycles: true,
})
storeRef.store.dispatch(api.endpoints.q1.initiate({}))
await loopTimers(6)
expect(baseBaseQuery).toHaveBeenCalledTimes(3)
})
test('retrying also works with mutations', async () => {
const baseBaseQuery = vi.fn<
Parameters<BaseQueryFn>,
ReturnType<BaseQueryFn>
>()
baseBaseQuery.mockResolvedValue({ error: 'rejected' })
const baseQuery = retry(baseBaseQuery, { maxRetries: 3 })
const api = createApi({
baseQuery,
endpoints: (build) => ({
m1: build.mutation({
query: () => ({ method: 'PUT' }),
}),
}),
})
const storeRef = setupApiStore(api, undefined, {
withoutTestLifecycles: true,
})
storeRef.store.dispatch(api.endpoints.m1.initiate({}))
await loopTimers(5)
expect(baseBaseQuery).toHaveBeenCalledTimes(4)
})
test('retrying stops after a success from a mutation', async () => {
const baseBaseQuery = vi.fn<
Parameters<BaseQueryFn>,
ReturnType<BaseQueryFn>
>()
baseBaseQuery
.mockRejectedValueOnce(new Error('rejected'))
.mockRejectedValueOnce(new Error('rejected'))
.mockResolvedValue({ data: { success: true } })
const baseQuery = retry(baseBaseQuery, { maxRetries: 3 })
const api = createApi({
baseQuery,
endpoints: (build) => ({
m1: build.mutation({
query: () => ({ method: 'PUT' }),
}),
}),
})
const storeRef = setupApiStore(api, undefined, {
withoutTestLifecycles: true,
})
storeRef.store.dispatch(api.endpoints.m1.initiate({}))
await loopTimers(5)
expect(baseBaseQuery).toHaveBeenCalledTimes(3)
})
test('non-error-cases should **not** retry', async () => {
const baseBaseQuery = vi.fn<
Parameters<BaseQueryFn>,
ReturnType<BaseQueryFn>
>()
baseBaseQuery.mockResolvedValue({ data: { success: true } })
const baseQuery = retry(baseBaseQuery, { maxRetries: 3 })
const api = createApi({
baseQuery,
endpoints: (build) => ({
q1: build.query({
query: () => {},
}),
}),
})
const storeRef = setupApiStore(api, undefined, {
withoutTestLifecycles: true,
})
storeRef.store.dispatch(api.endpoints.q1.initiate({}))
await loopTimers(2)
expect(baseBaseQuery).toHaveBeenCalledOnce()
})
test('calling retry.fail(error) will skip retrying and expose the error directly', async () => {
const error = { message: 'banana' }
const baseBaseQuery = vi.fn<
Parameters<BaseQueryFn>,
ReturnType<BaseQueryFn>
>()
baseBaseQuery.mockImplementation((input) => {
retry.fail(error)
return { data: `this won't happen` }
})
const baseQuery = retry(baseBaseQuery)
const api = createApi({
baseQuery,
endpoints: (build) => ({
q1: build.query({
query: () => {},
}),
}),
})
const storeRef = setupApiStore(api, undefined, {
withoutTestLifecycles: true,
})
const result = await storeRef.store.dispatch(api.endpoints.q1.initiate({}))
await loopTimers(2)
expect(baseBaseQuery).toHaveBeenCalledOnce()
expect(result.error).toEqual(error)
expect(result).toEqual({
endpointName: 'q1',
error,
isError: true,
isLoading: false,
isSuccess: false,
isUninitialized: false,
originalArgs: expect.any(Object),
requestId: expect.any(String),
startedTimeStamp: expect.any(Number),
status: 'rejected',
})
})
test('wrapping retry(retry(..., { maxRetries: 3 }), { maxRetries: 3 }) should retry 16 times', async () => {
/**
* Note:
* This will retry 16 total times because we try the initial + 3 retries (sum: 4), then retry that process 3 times (starting at 0 for a total of 4)... 4x4=16 (allegedly)
*/
const baseBaseQuery = vi.fn<
Parameters<BaseQueryFn>,
ReturnType<BaseQueryFn>
>()
baseBaseQuery.mockResolvedValue({ error: 'rejected' })
const baseQuery = retry(retry(baseBaseQuery, { maxRetries: 3 }), {
maxRetries: 3,
})
const api = createApi({
baseQuery,
endpoints: (build) => ({
q1: build.query({
query: () => {},
}),
}),
})
const storeRef = setupApiStore(api, undefined, {
withoutTestLifecycles: true,
})
storeRef.store.dispatch(api.endpoints.q1.initiate({}))
await loopTimers(18)
expect(baseBaseQuery).toHaveBeenCalledTimes(16)
})
test('accepts a custom backoff fn', async () => {
const baseBaseQuery = vi.fn<
Parameters<BaseQueryFn>,
ReturnType<BaseQueryFn>
>()
baseBaseQuery.mockResolvedValue({ error: 'rejected' })
const baseQuery = retry(baseBaseQuery, {
maxRetries: 8,
backoff: async (attempt, maxRetries) => {
const attempts = Math.min(attempt, maxRetries)
const timeout = attempts * 300 // Scale up by 300ms per request, ex: 300ms, 600ms, 900ms, 1200ms...
await new Promise((resolve) =>
setTimeout((res: any) => resolve(res), timeout),
)
},
})
const api = createApi({
baseQuery,
endpoints: (build) => ({
q1: build.query({
query: () => {},
}),
}),
})
const storeRef = setupApiStore(api, undefined, {
withoutTestLifecycles: true,
})
storeRef.store.dispatch(api.endpoints.q1.initiate({}))
await loopTimers()
expect(baseBaseQuery).toHaveBeenCalledTimes(9)
})
test('accepts a custom retryCondition fn', async () => {
const baseBaseQuery = vi.fn<
Parameters<BaseQueryFn>,
ReturnType<BaseQueryFn>
>()
baseBaseQuery.mockResolvedValue({ error: 'rejected' })
const overrideMaxRetries = 3
const baseQuery = retry(baseBaseQuery, {
retryCondition: (_, __, { attempt }) => attempt <= overrideMaxRetries,
})
const api = createApi({
baseQuery,
endpoints: (build) => ({
q1: build.query({
query: () => {},
}),
}),
})
const storeRef = setupApiStore(api, undefined, {
withoutTestLifecycles: true,
})
storeRef.store.dispatch(api.endpoints.q1.initiate({}))
await loopTimers()
expect(baseBaseQuery).toHaveBeenCalledTimes(overrideMaxRetries + 1)
})
test('retryCondition with endpoint config that overrides baseQuery config', async () => {
const baseBaseQuery = vi.fn<
Parameters<BaseQueryFn>,
ReturnType<BaseQueryFn>
>()
baseBaseQuery.mockResolvedValue({ error: 'rejected' })
const baseQuery = retry(baseBaseQuery, {
maxRetries: 10,
})
const api = createApi({
baseQuery,
endpoints: (build) => ({
q1: build.query({
query: () => {},
extraOptions: {
retryCondition: (_, __, { attempt }) => attempt <= 5,
},
}),
}),
})
const storeRef = setupApiStore(api, undefined, {
withoutTestLifecycles: true,
})
storeRef.store.dispatch(api.endpoints.q1.initiate({}))
await loopTimers()
expect(baseBaseQuery).toHaveBeenCalledTimes(6)
})
test('retryCondition also works with mutations', async () => {
const baseBaseQuery = vi.fn<
Parameters<BaseQueryFn>,
ReturnType<BaseQueryFn>
>()
baseBaseQuery
.mockRejectedValueOnce(new Error('rejected'))
.mockRejectedValueOnce(new Error('hello retryCondition'))
.mockRejectedValueOnce(new Error('rejected'))
.mockResolvedValue({ error: 'hello retryCondition' })
const baseQuery = retry(baseBaseQuery, {})
const api = createApi({
baseQuery,
endpoints: (build) => ({
m1: build.mutation({
query: () => ({ method: 'PUT' }),
extraOptions: {
retryCondition: (e) =>
(e as FetchBaseQueryError).data === 'hello retryCondition',
},
}),
}),
})
const storeRef = setupApiStore(api, undefined, {
withoutTestLifecycles: true,
})
storeRef.store.dispatch(api.endpoints.m1.initiate({}))
await loopTimers()
expect(baseBaseQuery).toHaveBeenCalledTimes(4)
})
test('Specifying maxRetries as 0 in RetryOptions prevents retries', async () => {
const baseBaseQuery = vi.fn<
Parameters<BaseQueryFn>,
ReturnType<BaseQueryFn>
>()
baseBaseQuery.mockResolvedValue({ error: 'rejected' })
const baseQuery = retry(baseBaseQuery, { maxRetries: 0 })
const api = createApi({
baseQuery,
endpoints: (build) => ({
q1: build.query({
query: () => {},
}),
}),
})
const storeRef = setupApiStore(api, undefined, {
withoutTestLifecycles: true,
})
storeRef.store.dispatch(api.endpoints.q1.initiate({}))
await loopTimers(2)
expect(baseBaseQuery).toHaveBeenCalledOnce()
})
test('retryCondition receives abort signal and stops retrying when cache entry is removed', async () => {
let capturedSignal: AbortSignal | undefined
let retryAttempts = 0
const baseBaseQuery = vi.fn<
Parameters<BaseQueryFn>,
ReturnType<BaseQueryFn>
>()
// Always return an error to trigger retries
baseBaseQuery.mockResolvedValue({ error: 'network error' })
let retryConditionCalled = false
const baseQuery = retry(baseBaseQuery, {
retryCondition: (error, args, { attempt, baseQueryApi }) => {
retryConditionCalled = true
retryAttempts = attempt
capturedSignal = baseQueryApi.signal
// Stop retrying if the signal is aborted
if (baseQueryApi.signal.aborted) {
return false
}
// Otherwise, retry up to 10 times
return attempt <= 10
},
backoff: async () => {
// Short backoff for faster test
await new Promise((resolve) => setTimeout(resolve, 10))
},
})
const api = createApi({
baseQuery,
endpoints: (build) => ({
getTest: build.query<string, number>({
query: (id) => ({ url: `test/${id}` }),
keepUnusedDataFor: 0.01, // Very short timeout (10ms)
}),
}),
})
const storeRef = setupApiStore(api, undefined, {
withoutTestLifecycles: true,
})
// Start the query
const queryPromise = storeRef.store.dispatch(
api.endpoints.getTest.initiate(1),
)
// Wait for the first retry to happen so we capture the signal
await loopTimers(2)
// Verify the retry condition was called and we have a signal
expect(retryConditionCalled).toBe(true)
expect(capturedSignal).toBeDefined()
expect(capturedSignal!.aborted).toBe(false)
// Unsubscribe to trigger cache removal
queryPromise.unsubscribe()
// Wait for the cache entry to be removed (keepUnusedDataFor: 0.01s = 10ms)
await vi.advanceTimersByTimeAsync(50)
// Allow some time for more retries to potentially happen
await loopTimers(3)
// The signal should now be aborted
expect(capturedSignal!.aborted).toBe(true)
// We should have stopped retrying early due to the abort signal
// If abort signal wasn't working, we'd see many more retry attempts
expect(retryAttempts).toBeLessThan(10)
// The base query should have been called at least once (initial attempt)
// but not the full 10+ times it would without abort signal
expect(baseBaseQuery).toHaveBeenCalled()
expect(baseBaseQuery.mock.calls.length).toBeLessThan(10)
})
// Tests for issue #4079: Thrown errors should respect maxRetries
test('thrown errors (not HandledError) should respect maxRetries', async () => {
const baseBaseQuery = vi.fn<
Parameters<BaseQueryFn>,
ReturnType<BaseQueryFn>
>()
// Simulate network error that keeps throwing
baseBaseQuery.mockRejectedValue(new Error('Network timeout'))
const baseQuery = retry(baseBaseQuery, { maxRetries: 3 })
const api = createApi({
baseQuery,
endpoints: (build) => ({
q1: build.query({
query: () => {},
}),
}),
})
const storeRef = setupApiStore(api, undefined, {
withoutTestLifecycles: true,
})
storeRef.store.dispatch(api.endpoints.q1.initiate({}))
await loopTimers(5)
// Should try initial + 3 retries = 4 total, then stop
// Currently this will fail because it retries infinitely
expect(baseBaseQuery).toHaveBeenCalledTimes(4)
})
test('graphql-style thrown errors should respect maxRetries', async () => {
class ClientError extends Error {
constructor(message: string) {
super(message)
this.name = 'ClientError'
}
}
const baseBaseQuery = vi.fn<
Parameters<BaseQueryFn>,
ReturnType<BaseQueryFn>
>()
// Simulate graphql-request throwing ClientError
baseBaseQuery.mockImplementation(() => {
throw new ClientError('GraphQL network error')
})
const baseQuery = retry(baseBaseQuery, { maxRetries: 2 })
const api = createApi({
baseQuery,
endpoints: (build) => ({
q1: build.query({
query: () => {},
}),
}),
})
const storeRef = setupApiStore(api, undefined, {
withoutTestLifecycles: true,
})
storeRef.store.dispatch(api.endpoints.q1.initiate({}))
await loopTimers(4)
// Should try initial + 2 retries = 3 total, then stop
// Currently this will fail because it retries infinitely
expect(baseBaseQuery).toHaveBeenCalledTimes(3)
})
test('handles mix of returned errors and thrown errors', async () => {
const baseBaseQuery = vi.fn<
Parameters<BaseQueryFn>,
ReturnType<BaseQueryFn>
>()
baseBaseQuery
.mockResolvedValueOnce({ error: 'returned error' }) // HandledError
.mockRejectedValueOnce(new Error('thrown error')) // Not HandledError
.mockResolvedValueOnce({ error: 'returned error' }) // HandledError
.mockResolvedValue({ data: { success: true } })
const baseQuery = retry(baseBaseQuery, { maxRetries: 5 })
const api = createApi({
baseQuery,
endpoints: (build) => ({
q1: build.query({
query: () => {},
}),
}),
})
const storeRef = setupApiStore(api, undefined, {
withoutTestLifecycles: true,
})
storeRef.store.dispatch(api.endpoints.q1.initiate({}))
await loopTimers(6)
// Should eventually succeed after 4 attempts
expect(baseBaseQuery).toHaveBeenCalledTimes(4)
})
test('thrown errors with mutations should respect maxRetries', async () => {
const baseBaseQuery = vi.fn<
Parameters<BaseQueryFn>,
ReturnType<BaseQueryFn>
>()
// Simulate persistent network error
baseBaseQuery.mockRejectedValue(new Error('Connection refused'))
const baseQuery = retry(baseBaseQuery, { maxRetries: 2 })
const api = createApi({
baseQuery,
endpoints: (build) => ({
m1: build.mutation({
query: () => ({ method: 'POST' }),
}),
}),
})
const storeRef = setupApiStore(api, undefined, {
withoutTestLifecycles: true,
})
storeRef.store.dispatch(api.endpoints.m1.initiate({}))
await loopTimers(4)
// Should try initial + 2 retries = 3 total, then stop
// Currently this will fail because it retries infinitely
expect(baseBaseQuery).toHaveBeenCalledTimes(3)
})
// These tests validate the abort signal handling implementation
describe('abort signal handling', () => {
test('retry loop exits immediately when signal is aborted before retry', async () => {
const baseBaseQuery = vi.fn<
Parameters<BaseQueryFn>,
ReturnType<BaseQueryFn>
>()
baseBaseQuery.mockResolvedValue({ error: 'network error' })
const baseQuery = retry(baseBaseQuery, { maxRetries: 10 })
const api = createApi({
baseQuery,
endpoints: (build) => ({
q1: build.query({ query: () => {} }),
}),
})
const storeRef = setupApiStore(api, undefined, {
withoutTestLifecycles: true,
})
const promise = storeRef.store.dispatch(api.endpoints.q1.initiate({}))
// Let first attempt fail
await loopTimers(1)
expect(baseBaseQuery).toHaveBeenCalledTimes(1)
// Abort the query
promise.abort()
// Advance timers to allow retry attempts
await loopTimers(5)
// Should not have retried after abort
expect(baseBaseQuery).toHaveBeenCalledTimes(1)
})
test('abort during active request prevents retry', async () => {
let requestInProgress = false
const baseBaseQuery = vi.fn<
Parameters<BaseQueryFn>,
ReturnType<BaseQueryFn>
>()
baseBaseQuery.mockImplementation(async () => {
requestInProgress = true
await new Promise((resolve) => setTimeout(resolve, 100))
requestInProgress = false
return { error: 'network error' }
})
const baseQuery = retry(baseBaseQuery, { maxRetries: 5 })
const api = createApi({
baseQuery,
endpoints: (build) => ({
q1: build.query({ query: () => {} }),
}),
})
const storeRef = setupApiStore(api, undefined, {
withoutTestLifecycles: true,
})
const promise = storeRef.store.dispatch(api.endpoints.q1.initiate({}))
// Wait for request to start
await vi.advanceTimersByTimeAsync(50)
expect(requestInProgress).toBe(true)
// Abort while request is in progress
promise.abort()
// Let request complete
await loopTimers(2)
// Should not retry after abort
expect(baseBaseQuery).toHaveBeenCalledTimes(1)
})
test('custom backoff without signal parameter still works', async () => {
const baseBaseQuery = vi.fn<
Parameters<BaseQueryFn>,
ReturnType<BaseQueryFn>
>()
baseBaseQuery.mockResolvedValue({ error: 'network error' })
// Custom backoff that doesn't accept signal (backward compatibility)
const customBackoff = async (attempt: number, maxRetries: number) => {
await new Promise((resolve) => setTimeout(resolve, 100))
}
const baseQuery = retry(baseBaseQuery, {
maxRetries: 3,
backoff: customBackoff,
})
const api = createApi({
baseQuery,
endpoints: (build) => ({
q1: build.query({ query: () => {} }),
}),
})
const storeRef = setupApiStore(api, undefined, {
withoutTestLifecycles: true,
})
storeRef.store.dispatch(api.endpoints.q1.initiate({}))
await loopTimers(5)
// Should complete all retries (not cancellable without signal)
expect(baseBaseQuery).toHaveBeenCalledTimes(4)
})
test('abort signal is checked before each retry attempt', async () => {
const attemptNumbers: number[] = []
const baseBaseQuery = vi.fn<
Parameters<BaseQueryFn>,
ReturnType<BaseQueryFn>
>()
baseBaseQuery.mockImplementation(async () => {
attemptNumbers.push(attemptNumbers.length + 1)
return { error: 'network error' }
})
const baseQuery = retry(baseBaseQuery, { maxRetries: 10 })
const api = createApi({
baseQuery,
endpoints: (build) => ({
q1: build.query({ query: () => {} }),
}),
})
const storeRef = setupApiStore(api, undefined, {
withoutTestLifecycles: true,
})
const promise = storeRef.store.dispatch(api.endpoints.q1.initiate({}))
// Let 3 attempts happen
await loopTimers(3)
expect(attemptNumbers).toEqual([1, 2, 3])
// Abort
promise.abort()
// Try to let more attempts happen
await loopTimers(5)
// Should not have any more attempts
expect(attemptNumbers).toEqual([1, 2, 3])
})
test('mutations respect abort signal during retry', async () => {
const baseBaseQuery = vi.fn<
Parameters<BaseQueryFn>,
ReturnType<BaseQueryFn>
>()
baseBaseQuery.mockResolvedValue({ error: 'network error' })
const baseQuery = retry(baseBaseQuery, { maxRetries: 5 })
const api = createApi({
baseQuery,
endpoints: (build) => ({
m1: build.mutation({ query: () => ({ method: 'POST' }) }),
}),
})
const storeRef = setupApiStore(api, undefined, {
withoutTestLifecycles: true,
})
const promise = storeRef.store.dispatch(api.endpoints.m1.initiate({}))
// Let first attempt fail
await loopTimers(1)
expect(baseBaseQuery).toHaveBeenCalledTimes(1)
// Abort
promise.abort()
// Try to let retries happen
await loopTimers(5)
// Should not have retried
expect(baseBaseQuery).toHaveBeenCalledTimes(1)
})
test('abort after successful retry does not affect result', async () => {
const baseBaseQuery = vi.fn<
Parameters<BaseQueryFn>,
ReturnType<BaseQueryFn>
>()
baseBaseQuery
.mockResolvedValueOnce({ error: 'network error' })
.mockResolvedValue({ data: { success: true } })
const baseQuery = retry(baseBaseQuery, { maxRetries: 5 })
const api = createApi({
baseQuery,
endpoints: (build) => ({
q1: build.query({ query: () => {} }),
}),
})
const storeRef = setupApiStore(api, undefined, {
withoutTestLifecycles: true,
})
const promise = storeRef.store.dispatch(api.endpoints.q1.initiate({}))
// Let it succeed on retry
await loopTimers(3)
expect(baseBaseQuery).toHaveBeenCalledTimes(2)
const result = await promise
// Abort after success
promise.abort()
// Result should still be successful
expect(result.isSuccess).toBe(true)
expect(result.data).toEqual({ success: true })
})
test('multiple aborts are handled gracefully', async () => {
const baseBaseQuery = vi.fn<
Parameters<BaseQueryFn>,
ReturnType<BaseQueryFn>
>()
baseBaseQuery.mockResolvedValue({ error: 'network error' })
const baseQuery = retry(baseBaseQuery, { maxRetries: 10 })
const api = createApi({
baseQuery,
endpoints: (build) => ({
q1: build.query({ query: () => {} }),
}),
})
const storeRef = setupApiStore(api, undefined, {
withoutTestLifecycles: true,
})
const promise = storeRef.store.dispatch(api.endpoints.q1.initiate({}))
await loopTimers(1)
// Call abort multiple times
promise.abort()
promise.abort()
promise.abort()
await loopTimers(3)
// Should handle gracefully
expect(baseBaseQuery).toHaveBeenCalledTimes(1)
})
test('abort signal already aborted before retry starts', async () => {
const baseBaseQuery = vi.fn<
Parameters<BaseQueryFn>,
ReturnType<BaseQueryFn>
>()
baseBaseQuery.mockResolvedValue({ error: 'network error' })
const baseQuery = retry(baseBaseQuery, { maxRetries: 5 })
const api = createApi({
baseQuery,
endpoints: (build) => ({
q1: build.query({ query: () => {} }),
}),
})
const storeRef = setupApiStore(api, undefined, {
withoutTestLifecycles: true,
})
const promise = storeRef.store.dispatch(api.endpoints.q1.initiate({}))
// Abort immediately
promise.abort()
await loopTimers(5)
// May have started the first attempt before abort was processed
// but should not retry
expect(baseBaseQuery.mock.calls.length).toBeLessThanOrEqual(1)
})
test('resetApiState aborts retrying queries', async () => {
const baseBaseQuery = vi.fn<
Parameters<BaseQueryFn>,
ReturnType<BaseQueryFn>
>()
baseBaseQuery.mockResolvedValue({ error: 'network error' })
const baseQuery = retry(baseBaseQuery, { maxRetries: 10 })
const api = createApi({
baseQuery,
endpoints: (build) => ({
q1: build.query({ query: () => {} }),
}),
})
const storeRef = setupApiStore(api, undefined, {
withoutTestLifecycles: true,
})
storeRef.store.dispatch(api.endpoints.q1.initiate({}))
// Let first attempt fail and start retrying
await loopTimers(2)
expect(baseBaseQuery).toHaveBeenCalledTimes(2)
// Reset API state (should abort the retry loop)
storeRef.store.dispatch(api.util.resetApiState())
// Try to let more retries happen
await loopTimers(5)
// Should not have retried after resetApiState
expect(baseBaseQuery).toHaveBeenCalledTimes(2)
})
})
})