source: node_modules/@reduxjs/toolkit/src/query/tests/retry.test.ts

Last change on this file was a762898, checked in by istevanoska <ilinastevanoska@…>, 5 months ago

Added visualizations

  • Property mode set to 100644
File size: 26.0 KB
Line 
1import type { BaseQueryFn, FetchBaseQueryError } from '@reduxjs/toolkit/query'
2import { createApi, retry } from '@reduxjs/toolkit/query'
3import { setupApiStore } from '../../tests/utils/helpers'
4
5beforeEach(() => {
6 vi.useFakeTimers()
7})
8
9const loopTimers = async (max: number = 12) => {
10 let count = 0
11 while (count < max) {
12 await vi.advanceTimersByTimeAsync(1)
13 vi.advanceTimersByTime(120_000)
14 count++
15 }
16}
17
18describe('configuration', () => {
19 test('retrying without any config options', async () => {
20 const baseBaseQuery = vi.fn<BaseQueryFn>()
21 baseBaseQuery.mockResolvedValue({ error: 'rejected' })
22
23 const baseQuery = retry(baseBaseQuery)
24 const api = createApi({
25 baseQuery,
26 endpoints: (build) => ({
27 q1: build.query({
28 query: () => {},
29 }),
30 }),
31 })
32
33 const storeRef = setupApiStore(api, undefined, {
34 withoutTestLifecycles: true,
35 })
36 storeRef.store.dispatch(api.endpoints.q1.initiate({}))
37
38 await loopTimers(7)
39
40 expect(baseBaseQuery).toHaveBeenCalledTimes(6)
41 })
42
43 test('retrying with baseQuery config that overrides default behavior (maxRetries: 5)', async () => {
44 const baseBaseQuery = vi.fn<BaseQueryFn>()
45 baseBaseQuery.mockResolvedValue({ error: 'rejected' })
46
47 const baseQuery = retry(baseBaseQuery, { maxRetries: 3 })
48 const api = createApi({
49 baseQuery,
50 endpoints: (build) => ({
51 q1: build.query({
52 query: () => {},
53 }),
54 }),
55 })
56
57 const storeRef = setupApiStore(api, undefined, {
58 withoutTestLifecycles: true,
59 })
60 storeRef.store.dispatch(api.endpoints.q1.initiate({}))
61
62 await loopTimers(5)
63
64 expect(baseBaseQuery).toHaveBeenCalledTimes(4)
65 })
66
67 test('retrying with endpoint config that overrides baseQuery config', async () => {
68 const baseBaseQuery = vi.fn<BaseQueryFn>()
69 baseBaseQuery.mockResolvedValue({ error: 'rejected' })
70
71 const baseQuery = retry(baseBaseQuery, { maxRetries: 3 })
72 const api = createApi({
73 baseQuery,
74 endpoints: (build) => ({
75 q1: build.query({
76 query: () => {},
77 }),
78 q2: build.query({
79 query: () => {},
80 extraOptions: { maxRetries: 8 },
81 }),
82 }),
83 })
84
85 const storeRef = setupApiStore(api, undefined, {
86 withoutTestLifecycles: true,
87 })
88
89 storeRef.store.dispatch(api.endpoints.q1.initiate({}))
90 await loopTimers(5)
91
92 expect(baseBaseQuery).toHaveBeenCalledTimes(4)
93
94 baseBaseQuery.mockClear()
95
96 storeRef.store.dispatch(api.endpoints.q2.initiate({}))
97
98 await loopTimers(10)
99
100 expect(baseBaseQuery).toHaveBeenCalledTimes(9)
101 })
102
103 test('stops retrying a query after a success', async () => {
104 const baseBaseQuery = vi.fn<BaseQueryFn>()
105 baseBaseQuery
106 .mockResolvedValueOnce({ error: 'rejected' })
107 .mockResolvedValueOnce({ error: 'rejected' })
108 .mockResolvedValue({ data: { success: true } })
109
110 const baseQuery = retry(baseBaseQuery, { maxRetries: 10 })
111 const api = createApi({
112 baseQuery,
113 endpoints: (build) => ({
114 q1: build.mutation({
115 query: () => {},
116 }),
117 }),
118 })
119
120 const storeRef = setupApiStore(api, undefined, {
121 withoutTestLifecycles: true,
122 })
123 storeRef.store.dispatch(api.endpoints.q1.initiate({}))
124
125 await loopTimers(6)
126
127 expect(baseBaseQuery).toHaveBeenCalledTimes(3)
128 })
129
130 test('retrying also works with mutations', async () => {
131 const baseBaseQuery = vi.fn<BaseQueryFn>()
132 baseBaseQuery.mockResolvedValue({ error: 'rejected' })
133
134 const baseQuery = retry(baseBaseQuery, { maxRetries: 3 })
135 const api = createApi({
136 baseQuery,
137 endpoints: (build) => ({
138 m1: build.mutation({
139 query: () => ({ method: 'PUT' }),
140 }),
141 }),
142 })
143
144 const storeRef = setupApiStore(api, undefined, {
145 withoutTestLifecycles: true,
146 })
147
148 storeRef.store.dispatch(api.endpoints.m1.initiate({}))
149
150 await loopTimers(5)
151
152 expect(baseBaseQuery).toHaveBeenCalledTimes(4)
153 })
154
155 test('retrying stops after a success from a mutation', async () => {
156 const baseBaseQuery = vi.fn<BaseQueryFn>()
157 baseBaseQuery
158 .mockRejectedValueOnce(new Error('rejected'))
159 .mockRejectedValueOnce(new Error('rejected'))
160 .mockResolvedValue({ data: { success: true } })
161
162 const baseQuery = retry(baseBaseQuery, { maxRetries: 3 })
163 const api = createApi({
164 baseQuery,
165 endpoints: (build) => ({
166 m1: build.mutation({
167 query: () => ({ method: 'PUT' }),
168 }),
169 }),
170 })
171
172 const storeRef = setupApiStore(api, undefined, {
173 withoutTestLifecycles: true,
174 })
175
176 storeRef.store.dispatch(api.endpoints.m1.initiate({}))
177
178 await loopTimers(5)
179
180 expect(baseBaseQuery).toHaveBeenCalledTimes(3)
181 })
182 test('non-error-cases should **not** retry', async () => {
183 const baseBaseQuery = vi.fn<BaseQueryFn>()
184 baseBaseQuery.mockResolvedValue({ data: { success: true } })
185
186 const baseQuery = retry(baseBaseQuery, { maxRetries: 3 })
187 const api = createApi({
188 baseQuery,
189 endpoints: (build) => ({
190 q1: build.query({
191 query: () => {},
192 }),
193 }),
194 })
195
196 const storeRef = setupApiStore(api, undefined, {
197 withoutTestLifecycles: true,
198 })
199
200 storeRef.store.dispatch(api.endpoints.q1.initiate({}))
201
202 await loopTimers(2)
203
204 expect(baseBaseQuery).toHaveBeenCalledOnce()
205 })
206 test('calling retry.fail(error) will skip retrying and expose the error directly', async () => {
207 const error = { message: 'banana' }
208
209 const baseBaseQuery = vi.fn<BaseQueryFn>()
210 baseBaseQuery.mockImplementation((input) => {
211 retry.fail(error)
212 return { data: `this won't happen` }
213 })
214
215 const baseQuery = retry(baseBaseQuery)
216 const api = createApi({
217 baseQuery,
218 endpoints: (build) => ({
219 q1: build.query({
220 query: () => {},
221 }),
222 }),
223 })
224
225 const storeRef = setupApiStore(api, undefined, {
226 withoutTestLifecycles: true,
227 })
228
229 const result = await storeRef.store.dispatch(api.endpoints.q1.initiate({}))
230
231 await loopTimers(2)
232
233 expect(baseBaseQuery).toHaveBeenCalledOnce()
234 expect(result.error).toEqual(error)
235 expect(result).toEqual({
236 endpointName: 'q1',
237 error,
238 isError: true,
239 isLoading: false,
240 isSuccess: false,
241 isUninitialized: false,
242 originalArgs: expect.any(Object),
243 requestId: expect.any(String),
244 startedTimeStamp: expect.any(Number),
245 status: 'rejected',
246 })
247 })
248
249 test('wrapping retry(retry(..., { maxRetries: 3 }), { maxRetries: 3 }) should retry 16 times', async () => {
250 /**
251 * Note:
252 * 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)
253 */
254 const baseBaseQuery = vi.fn<BaseQueryFn>()
255 baseBaseQuery.mockResolvedValue({ error: 'rejected' })
256
257 const baseQuery = retry(retry(baseBaseQuery, { maxRetries: 3 }), {
258 maxRetries: 3,
259 })
260 const api = createApi({
261 baseQuery,
262 endpoints: (build) => ({
263 q1: build.query({
264 query: () => {},
265 }),
266 }),
267 })
268
269 const storeRef = setupApiStore(api, undefined, {
270 withoutTestLifecycles: true,
271 })
272
273 storeRef.store.dispatch(api.endpoints.q1.initiate({}))
274
275 await loopTimers(18)
276
277 expect(baseBaseQuery).toHaveBeenCalledTimes(16)
278 })
279
280 test('accepts a custom backoff fn', async () => {
281 const baseBaseQuery = vi.fn<BaseQueryFn>()
282 baseBaseQuery.mockResolvedValue({ error: 'rejected' })
283
284 const baseQuery = retry(baseBaseQuery, {
285 maxRetries: 8,
286 backoff: async (attempt, maxRetries) => {
287 const attempts = Math.min(attempt, maxRetries)
288 const timeout = attempts * 300 // Scale up by 300ms per request, ex: 300ms, 600ms, 900ms, 1200ms...
289 await new Promise((resolve) =>
290 setTimeout((res: any) => resolve(res), timeout),
291 )
292 },
293 })
294 const api = createApi({
295 baseQuery,
296 endpoints: (build) => ({
297 q1: build.query({
298 query: () => {},
299 }),
300 }),
301 })
302
303 const storeRef = setupApiStore(api, undefined, {
304 withoutTestLifecycles: true,
305 })
306 storeRef.store.dispatch(api.endpoints.q1.initiate({}))
307
308 await loopTimers()
309
310 expect(baseBaseQuery).toHaveBeenCalledTimes(9)
311 })
312
313 test('accepts a custom retryCondition fn', async () => {
314 const baseBaseQuery = vi.fn<BaseQueryFn>()
315 baseBaseQuery.mockResolvedValue({ error: 'rejected' })
316
317 const overrideMaxRetries = 3
318
319 const baseQuery = retry(baseBaseQuery, {
320 retryCondition: (_, __, { attempt }) => attempt <= overrideMaxRetries,
321 })
322 const api = createApi({
323 baseQuery,
324 endpoints: (build) => ({
325 q1: build.query({
326 query: () => {},
327 }),
328 }),
329 })
330
331 const storeRef = setupApiStore(api, undefined, {
332 withoutTestLifecycles: true,
333 })
334 storeRef.store.dispatch(api.endpoints.q1.initiate({}))
335
336 await loopTimers()
337
338 expect(baseBaseQuery).toHaveBeenCalledTimes(overrideMaxRetries + 1)
339 })
340
341 test('retryCondition with endpoint config that overrides baseQuery config', async () => {
342 const baseBaseQuery = vi.fn<BaseQueryFn>()
343 baseBaseQuery.mockResolvedValue({ error: 'rejected' })
344
345 const baseQuery = retry(baseBaseQuery, {
346 maxRetries: 10,
347 })
348 const api = createApi({
349 baseQuery,
350 endpoints: (build) => ({
351 q1: build.query({
352 query: () => {},
353 extraOptions: {
354 retryCondition: (_, __, { attempt }) => attempt <= 5,
355 },
356 }),
357 }),
358 })
359
360 const storeRef = setupApiStore(api, undefined, {
361 withoutTestLifecycles: true,
362 })
363 storeRef.store.dispatch(api.endpoints.q1.initiate({}))
364
365 await loopTimers()
366
367 expect(baseBaseQuery).toHaveBeenCalledTimes(6)
368 })
369
370 test('retryCondition also works with mutations', async () => {
371 const baseBaseQuery = vi.fn<BaseQueryFn>()
372
373 baseBaseQuery
374 .mockRejectedValueOnce(new Error('rejected'))
375 .mockRejectedValueOnce(new Error('hello retryCondition'))
376 .mockRejectedValueOnce(new Error('rejected'))
377 .mockResolvedValue({ error: 'hello retryCondition' })
378
379 const baseQuery = retry(baseBaseQuery, {})
380 const api = createApi({
381 baseQuery,
382 endpoints: (build) => ({
383 m1: build.mutation({
384 query: () => ({ method: 'PUT' }),
385 extraOptions: {
386 retryCondition: (e) =>
387 (e as FetchBaseQueryError).data === 'hello retryCondition',
388 },
389 }),
390 }),
391 })
392
393 const storeRef = setupApiStore(api, undefined, {
394 withoutTestLifecycles: true,
395 })
396 storeRef.store.dispatch(api.endpoints.m1.initiate({}))
397
398 await loopTimers()
399
400 expect(baseBaseQuery).toHaveBeenCalledTimes(4)
401 })
402
403 test('Specifying maxRetries as 0 in RetryOptions prevents retries', async () => {
404 const baseBaseQuery = vi.fn<BaseQueryFn>()
405 baseBaseQuery.mockResolvedValue({ error: 'rejected' })
406
407 const baseQuery = retry(baseBaseQuery, { maxRetries: 0 })
408 const api = createApi({
409 baseQuery,
410 endpoints: (build) => ({
411 q1: build.query({
412 query: () => {},
413 }),
414 }),
415 })
416
417 const storeRef = setupApiStore(api, undefined, {
418 withoutTestLifecycles: true,
419 })
420
421 storeRef.store.dispatch(api.endpoints.q1.initiate({}))
422 await loopTimers(2)
423
424 expect(baseBaseQuery).toHaveBeenCalledOnce()
425 })
426
427 test('retryCondition receives abort signal and stops retrying when cache entry is removed', async () => {
428 let capturedSignal: AbortSignal | undefined
429 let retryAttempts = 0
430
431 const baseBaseQuery = vi.fn<BaseQueryFn>()
432
433 // Always return an error to trigger retries
434 baseBaseQuery.mockResolvedValue({ error: 'network error' })
435
436 let retryConditionCalled = false
437
438 const baseQuery = retry(baseBaseQuery, {
439 retryCondition: (error, args, { attempt, baseQueryApi }) => {
440 retryConditionCalled = true
441 retryAttempts = attempt
442 capturedSignal = baseQueryApi.signal
443
444 // Stop retrying if the signal is aborted
445 if (baseQueryApi.signal.aborted) {
446 return false
447 }
448
449 // Otherwise, retry up to 10 times
450 return attempt <= 10
451 },
452 backoff: async () => {
453 // Short backoff for faster test
454 await new Promise((resolve) => setTimeout(resolve, 10))
455 },
456 })
457
458 const api = createApi({
459 baseQuery,
460 endpoints: (build) => ({
461 getTest: build.query<string, number>({
462 query: (id) => ({ url: `test/${id}` }),
463 keepUnusedDataFor: 0.01, // Very short timeout (10ms)
464 }),
465 }),
466 })
467
468 const storeRef = setupApiStore(api, undefined, {
469 withoutTestLifecycles: true,
470 })
471
472 // Start the query
473 const queryPromise = storeRef.store.dispatch(
474 api.endpoints.getTest.initiate(1),
475 )
476
477 // Wait for the first retry to happen so we capture the signal
478 await loopTimers(2)
479
480 // Verify the retry condition was called and we have a signal
481 expect(retryConditionCalled).toBe(true)
482 expect(capturedSignal).toBeDefined()
483 expect(capturedSignal!.aborted).toBe(false)
484
485 // Unsubscribe to trigger cache removal
486 queryPromise.unsubscribe()
487
488 // Wait for the cache entry to be removed (keepUnusedDataFor: 0.01s = 10ms)
489 await vi.advanceTimersByTimeAsync(50)
490
491 // Allow some time for more retries to potentially happen
492 await loopTimers(3)
493
494 // The signal should now be aborted
495 expect(capturedSignal!.aborted).toBe(true)
496
497 // We should have stopped retrying early due to the abort signal
498 // If abort signal wasn't working, we'd see many more retry attempts
499 expect(retryAttempts).toBeLessThan(10)
500
501 // The base query should have been called at least once (initial attempt)
502 // but not the full 10+ times it would without abort signal
503 expect(baseBaseQuery).toHaveBeenCalled()
504 expect(baseBaseQuery.mock.calls.length).toBeLessThan(10)
505 })
506
507 // Tests for issue #4079: Thrown errors should respect maxRetries
508 test('thrown errors (not HandledError) should respect maxRetries', async () => {
509 const baseBaseQuery = vi.fn<BaseQueryFn>()
510 // Simulate network error that keeps throwing
511 baseBaseQuery.mockRejectedValue(new Error('Network timeout'))
512
513 const baseQuery = retry(baseBaseQuery, { maxRetries: 3 })
514 const api = createApi({
515 baseQuery,
516 endpoints: (build) => ({
517 q1: build.query({
518 query: () => {},
519 }),
520 }),
521 })
522
523 const storeRef = setupApiStore(api, undefined, {
524 withoutTestLifecycles: true,
525 })
526
527 storeRef.store.dispatch(api.endpoints.q1.initiate({}))
528
529 await loopTimers(5)
530
531 // Should try initial + 3 retries = 4 total, then stop
532 // Currently this will fail because it retries infinitely
533 expect(baseBaseQuery).toHaveBeenCalledTimes(4)
534 })
535
536 test('graphql-style thrown errors should respect maxRetries', async () => {
537 class ClientError extends Error {
538 constructor(message: string) {
539 super(message)
540 this.name = 'ClientError'
541 }
542 }
543
544 const baseBaseQuery = vi.fn<BaseQueryFn>()
545 // Simulate graphql-request throwing ClientError
546 baseBaseQuery.mockImplementation(() => {
547 throw new ClientError('GraphQL network error')
548 })
549
550 const baseQuery = retry(baseBaseQuery, { maxRetries: 2 })
551 const api = createApi({
552 baseQuery,
553 endpoints: (build) => ({
554 q1: build.query({
555 query: () => {},
556 }),
557 }),
558 })
559
560 const storeRef = setupApiStore(api, undefined, {
561 withoutTestLifecycles: true,
562 })
563
564 storeRef.store.dispatch(api.endpoints.q1.initiate({}))
565
566 await loopTimers(4)
567
568 // Should try initial + 2 retries = 3 total, then stop
569 // Currently this will fail because it retries infinitely
570 expect(baseBaseQuery).toHaveBeenCalledTimes(3)
571 })
572
573 test('handles mix of returned errors and thrown errors', async () => {
574 const baseBaseQuery = vi.fn<BaseQueryFn>()
575 baseBaseQuery
576 .mockResolvedValueOnce({ error: 'returned error' }) // HandledError
577 .mockRejectedValueOnce(new Error('thrown error')) // Not HandledError
578 .mockResolvedValueOnce({ error: 'returned error' }) // HandledError
579 .mockResolvedValue({ data: { success: true } })
580
581 const baseQuery = retry(baseBaseQuery, { maxRetries: 5 })
582 const api = createApi({
583 baseQuery,
584 endpoints: (build) => ({
585 q1: build.query({
586 query: () => {},
587 }),
588 }),
589 })
590
591 const storeRef = setupApiStore(api, undefined, {
592 withoutTestLifecycles: true,
593 })
594
595 storeRef.store.dispatch(api.endpoints.q1.initiate({}))
596
597 await loopTimers(6)
598
599 // Should eventually succeed after 4 attempts
600 expect(baseBaseQuery).toHaveBeenCalledTimes(4)
601 })
602
603 test('thrown errors with mutations should respect maxRetries', async () => {
604 const baseBaseQuery = vi.fn<BaseQueryFn>()
605 // Simulate persistent network error
606 baseBaseQuery.mockRejectedValue(new Error('Connection refused'))
607
608 const baseQuery = retry(baseBaseQuery, { maxRetries: 2 })
609 const api = createApi({
610 baseQuery,
611 endpoints: (build) => ({
612 m1: build.mutation({
613 query: () => ({ method: 'POST' }),
614 }),
615 }),
616 })
617
618 const storeRef = setupApiStore(api, undefined, {
619 withoutTestLifecycles: true,
620 })
621
622 storeRef.store.dispatch(api.endpoints.m1.initiate({}))
623
624 await loopTimers(4)
625
626 // Should try initial + 2 retries = 3 total, then stop
627 // Currently this will fail because it retries infinitely
628 expect(baseBaseQuery).toHaveBeenCalledTimes(3)
629 })
630
631 // These tests validate the abort signal handling implementation
632 describe('abort signal handling', () => {
633 test('retry loop exits immediately when signal is aborted before retry', async () => {
634 const baseBaseQuery = vi.fn<BaseQueryFn>()
635 baseBaseQuery.mockResolvedValue({ error: 'network error' })
636
637 const baseQuery = retry(baseBaseQuery, { maxRetries: 10 })
638 const api = createApi({
639 baseQuery,
640 endpoints: (build) => ({
641 q1: build.query({ query: () => {} }),
642 }),
643 })
644
645 const storeRef = setupApiStore(api, undefined, {
646 withoutTestLifecycles: true,
647 })
648 const promise = storeRef.store.dispatch(api.endpoints.q1.initiate({}))
649
650 // Let first attempt fail
651 await loopTimers(1)
652 expect(baseBaseQuery).toHaveBeenCalledTimes(1)
653
654 // Abort the query
655 promise.abort()
656
657 // Advance timers to allow retry attempts
658 await loopTimers(5)
659
660 // Should not have retried after abort
661 expect(baseBaseQuery).toHaveBeenCalledTimes(1)
662 })
663
664 test('abort during active request prevents retry', async () => {
665 let requestInProgress = false
666 const baseBaseQuery = vi.fn<BaseQueryFn>()
667
668 baseBaseQuery.mockImplementation(async () => {
669 requestInProgress = true
670 await new Promise((resolve) => setTimeout(resolve, 100))
671 requestInProgress = false
672 return { error: 'network error' }
673 })
674
675 const baseQuery = retry(baseBaseQuery, { maxRetries: 5 })
676 const api = createApi({
677 baseQuery,
678 endpoints: (build) => ({
679 q1: build.query({ query: () => {} }),
680 }),
681 })
682
683 const storeRef = setupApiStore(api, undefined, {
684 withoutTestLifecycles: true,
685 })
686 const promise = storeRef.store.dispatch(api.endpoints.q1.initiate({}))
687
688 // Wait for request to start
689 await vi.advanceTimersByTimeAsync(50)
690 expect(requestInProgress).toBe(true)
691
692 // Abort while request is in progress
693 promise.abort()
694
695 // Let request complete
696 await loopTimers(2)
697
698 // Should not retry after abort
699 expect(baseBaseQuery).toHaveBeenCalledTimes(1)
700 })
701
702 test('custom backoff without signal parameter still works', async () => {
703 const baseBaseQuery = vi.fn<BaseQueryFn>()
704 baseBaseQuery.mockResolvedValue({ error: 'network error' })
705
706 // Custom backoff that doesn't accept signal (backward compatibility)
707 const customBackoff = async (attempt: number, maxRetries: number) => {
708 await new Promise((resolve) => setTimeout(resolve, 100))
709 }
710
711 const baseQuery = retry(baseBaseQuery, {
712 maxRetries: 3,
713 backoff: customBackoff,
714 })
715
716 const api = createApi({
717 baseQuery,
718 endpoints: (build) => ({
719 q1: build.query({ query: () => {} }),
720 }),
721 })
722
723 const storeRef = setupApiStore(api, undefined, {
724 withoutTestLifecycles: true,
725 })
726 storeRef.store.dispatch(api.endpoints.q1.initiate({}))
727
728 await loopTimers(5)
729
730 // Should complete all retries (not cancellable without signal)
731 expect(baseBaseQuery).toHaveBeenCalledTimes(4)
732 })
733
734 test('abort signal is checked before each retry attempt', async () => {
735 const attemptNumbers: number[] = []
736 const baseBaseQuery = vi.fn<BaseQueryFn>()
737 baseBaseQuery.mockImplementation(async () => {
738 attemptNumbers.push(attemptNumbers.length + 1)
739 return { error: 'network error' }
740 })
741
742 const baseQuery = retry(baseBaseQuery, { maxRetries: 10 })
743 const api = createApi({
744 baseQuery,
745 endpoints: (build) => ({
746 q1: build.query({ query: () => {} }),
747 }),
748 })
749
750 const storeRef = setupApiStore(api, undefined, {
751 withoutTestLifecycles: true,
752 })
753 const promise = storeRef.store.dispatch(api.endpoints.q1.initiate({}))
754
755 // Let 3 attempts happen
756 await loopTimers(3)
757 expect(attemptNumbers).toEqual([1, 2, 3])
758
759 // Abort
760 promise.abort()
761
762 // Try to let more attempts happen
763 await loopTimers(5)
764
765 // Should not have any more attempts
766 expect(attemptNumbers).toEqual([1, 2, 3])
767 })
768
769 test('mutations respect abort signal during retry', async () => {
770 const baseBaseQuery = vi.fn<BaseQueryFn>()
771 baseBaseQuery.mockResolvedValue({ error: 'network error' })
772
773 const baseQuery = retry(baseBaseQuery, { maxRetries: 5 })
774 const api = createApi({
775 baseQuery,
776 endpoints: (build) => ({
777 m1: build.mutation({ query: () => ({ method: 'POST' }) }),
778 }),
779 })
780
781 const storeRef = setupApiStore(api, undefined, {
782 withoutTestLifecycles: true,
783 })
784 const promise = storeRef.store.dispatch(api.endpoints.m1.initiate({}))
785
786 // Let first attempt fail
787 await loopTimers(1)
788 expect(baseBaseQuery).toHaveBeenCalledTimes(1)
789
790 // Abort
791 promise.abort()
792
793 // Try to let retries happen
794 await loopTimers(5)
795
796 // Should not have retried
797 expect(baseBaseQuery).toHaveBeenCalledTimes(1)
798 })
799
800 test('abort after successful retry does not affect result', async () => {
801 const baseBaseQuery = vi.fn<BaseQueryFn>()
802 baseBaseQuery
803 .mockResolvedValueOnce({ error: 'network error' })
804 .mockResolvedValue({ data: { success: true } })
805
806 const baseQuery = retry(baseBaseQuery, { maxRetries: 5 })
807 const api = createApi({
808 baseQuery,
809 endpoints: (build) => ({
810 q1: build.query({ query: () => {} }),
811 }),
812 })
813
814 const storeRef = setupApiStore(api, undefined, {
815 withoutTestLifecycles: true,
816 })
817 const promise = storeRef.store.dispatch(api.endpoints.q1.initiate({}))
818
819 // Let it succeed on retry
820 await loopTimers(3)
821 expect(baseBaseQuery).toHaveBeenCalledTimes(2)
822
823 const result = await promise
824
825 // Abort after success
826 promise.abort()
827
828 // Result should still be successful
829 expect(result.isSuccess).toBe(true)
830 expect(result.data).toEqual({ success: true })
831 })
832
833 test('multiple aborts are handled gracefully', async () => {
834 const baseBaseQuery = vi.fn<BaseQueryFn>()
835 baseBaseQuery.mockResolvedValue({ error: 'network error' })
836
837 const baseQuery = retry(baseBaseQuery, { maxRetries: 10 })
838 const api = createApi({
839 baseQuery,
840 endpoints: (build) => ({
841 q1: build.query({ query: () => {} }),
842 }),
843 })
844
845 const storeRef = setupApiStore(api, undefined, {
846 withoutTestLifecycles: true,
847 })
848 const promise = storeRef.store.dispatch(api.endpoints.q1.initiate({}))
849
850 await loopTimers(1)
851
852 // Call abort multiple times
853 promise.abort()
854 promise.abort()
855 promise.abort()
856
857 await loopTimers(3)
858
859 // Should handle gracefully
860 expect(baseBaseQuery).toHaveBeenCalledTimes(1)
861 })
862
863 test('abort signal already aborted before retry starts', async () => {
864 const baseBaseQuery = vi.fn<BaseQueryFn>()
865 baseBaseQuery.mockResolvedValue({ error: 'network error' })
866
867 const baseQuery = retry(baseBaseQuery, { maxRetries: 5 })
868 const api = createApi({
869 baseQuery,
870 endpoints: (build) => ({
871 q1: build.query({ query: () => {} }),
872 }),
873 })
874
875 const storeRef = setupApiStore(api, undefined, {
876 withoutTestLifecycles: true,
877 })
878 const promise = storeRef.store.dispatch(api.endpoints.q1.initiate({}))
879
880 // Abort immediately
881 promise.abort()
882
883 await loopTimers(5)
884
885 // May have started the first attempt before abort was processed
886 // but should not retry
887 expect(baseBaseQuery.mock.calls.length).toBeLessThanOrEqual(1)
888 })
889
890 test('resetApiState aborts retrying queries', async () => {
891 const baseBaseQuery = vi.fn<BaseQueryFn>()
892 baseBaseQuery.mockResolvedValue({ error: 'network error' })
893
894 const baseQuery = retry(baseBaseQuery, { maxRetries: 10 })
895 const api = createApi({
896 baseQuery,
897 endpoints: (build) => ({
898 q1: build.query({ query: () => {} }),
899 }),
900 })
901
902 const storeRef = setupApiStore(api, undefined, {
903 withoutTestLifecycles: true,
904 })
905 storeRef.store.dispatch(api.endpoints.q1.initiate({}))
906
907 // Let first attempt fail and start retrying
908 await loopTimers(2)
909 expect(baseBaseQuery).toHaveBeenCalledTimes(2)
910
911 // Reset API state (should abort the retry loop)
912 storeRef.store.dispatch(api.util.resetApiState())
913
914 // Try to let more retries happen
915 await loopTimers(5)
916
917 // Should not have retried after resetApiState
918 expect(baseBaseQuery).toHaveBeenCalledTimes(2)
919 })
920 })
921})
Note: See TracBrowser for help on using the repository browser.