source: node_modules/@reduxjs/toolkit/src/query/retry.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: 6.4 KB
Line 
1import type {
2 BaseQueryApi,
3 BaseQueryArg,
4 BaseQueryEnhancer,
5 BaseQueryError,
6 BaseQueryExtraOptions,
7 BaseQueryFn,
8 BaseQueryMeta,
9} from './baseQueryTypes'
10import type { FetchBaseQueryError } from './fetchBaseQuery'
11import { HandledError } from './HandledError'
12
13/**
14 * Exponential backoff based on the attempt number.
15 *
16 * @remarks
17 * 1. 600ms * random(0.4, 1.4)
18 * 2. 1200ms * random(0.4, 1.4)
19 * 3. 2400ms * random(0.4, 1.4)
20 * 4. 4800ms * random(0.4, 1.4)
21 * 5. 9600ms * random(0.4, 1.4)
22 *
23 * @param attempt - Current attempt
24 * @param maxRetries - Maximum number of retries
25 */
26async function defaultBackoff(
27 attempt: number = 0,
28 maxRetries: number = 5,
29 signal?: AbortSignal,
30) {
31 const attempts = Math.min(attempt, maxRetries)
32
33 const timeout = ~~((Math.random() + 0.4) * (300 << attempts)) // Force a positive int in the case we make this an option
34
35 await new Promise<void>((resolve, reject) => {
36 const timeoutId = setTimeout(() => resolve(), timeout)
37
38 // If signal is provided and gets aborted, clear timeout and reject
39 if (signal) {
40 const abortHandler = () => {
41 clearTimeout(timeoutId)
42 reject(new Error('Aborted'))
43 }
44
45 // Check if already aborted
46 if (signal.aborted) {
47 clearTimeout(timeoutId)
48 reject(new Error('Aborted'))
49 } else {
50 signal.addEventListener('abort', abortHandler, { once: true })
51 }
52 }
53 })
54}
55
56type RetryConditionFunction = (
57 error: BaseQueryError<BaseQueryFn>,
58 args: BaseQueryArg<BaseQueryFn>,
59 extraArgs: {
60 attempt: number
61 baseQueryApi: BaseQueryApi
62 extraOptions: BaseQueryExtraOptions<BaseQueryFn> & RetryOptions
63 },
64) => boolean
65
66export type RetryOptions = {
67 /**
68 * Function used to determine delay between retries
69 */
70 backoff?: (
71 attempt: number,
72 maxRetries: number,
73 signal?: AbortSignal,
74 ) => Promise<void>
75} & (
76 | {
77 /**
78 * How many times the query will be retried (default: 5)
79 */
80 maxRetries?: number
81 retryCondition?: undefined
82 }
83 | {
84 /**
85 * Callback to determine if a retry should be attempted.
86 * Return `true` for another retry and `false` to quit trying prematurely.
87 */
88 retryCondition?: RetryConditionFunction
89 maxRetries?: undefined
90 }
91)
92
93function fail<BaseQuery extends BaseQueryFn = BaseQueryFn>(
94 error: BaseQueryError<BaseQuery>,
95 meta?: BaseQueryMeta<BaseQuery>,
96): never {
97 throw Object.assign(new HandledError({ error, meta }), {
98 throwImmediately: true,
99 })
100}
101
102/**
103 * Checks if the abort signal is aborted and fails immediately if so.
104 * Used to exit retry loops cleanly when a request is aborted.
105 */
106function failIfAborted(signal: AbortSignal): void {
107 if (signal.aborted) {
108 fail({ status: 'CUSTOM_ERROR', error: 'Aborted' })
109 }
110}
111
112const EMPTY_OPTIONS = {}
113
114const retryWithBackoff: BaseQueryEnhancer<
115 unknown,
116 RetryOptions,
117 RetryOptions | void
118> = (baseQuery, defaultOptions) => async (args, api, extraOptions) => {
119 // We need to figure out `maxRetries` before we define `defaultRetryCondition.
120 // This is probably goofy, but ought to work.
121 // Put our defaults in one array, filter out undefineds, grab the last value.
122 const possibleMaxRetries: number[] = [
123 5,
124 ((defaultOptions as any) || EMPTY_OPTIONS).maxRetries,
125 ((extraOptions as any) || EMPTY_OPTIONS).maxRetries,
126 ].filter((x) => x !== undefined)
127 const [maxRetries] = possibleMaxRetries.slice(-1)
128
129 const defaultRetryCondition: RetryConditionFunction = (_, __, { attempt }) =>
130 attempt <= maxRetries
131
132 const options: {
133 maxRetries: number
134 backoff: typeof defaultBackoff
135 retryCondition: typeof defaultRetryCondition
136 } = {
137 maxRetries,
138 backoff: defaultBackoff,
139 retryCondition: defaultRetryCondition,
140 ...defaultOptions,
141 ...extraOptions,
142 }
143 let retry = 0
144
145 while (true) {
146 // Check if aborted before each attempt
147 failIfAborted(api.signal)
148
149 try {
150 const result = await baseQuery(args, api, extraOptions)
151 // baseQueries _should_ return an error property, so we should check for that and throw it to continue retrying
152 if (result.error) {
153 throw new HandledError(result)
154 }
155 return result
156 } catch (e: any) {
157 retry++
158
159 if (e.throwImmediately) {
160 if (e instanceof HandledError) {
161 return e.value
162 }
163
164 // We don't know what this is, so we have to rethrow it
165 throw e
166 }
167
168 if (e instanceof HandledError) {
169 if (
170 !options.retryCondition(e.value.error as FetchBaseQueryError, args, {
171 attempt: retry,
172 baseQueryApi: api,
173 extraOptions,
174 })
175 ) {
176 return e.value // Max retries for expected error
177 }
178 } else {
179 // For unexpected errors, respect maxRetries
180 if (retry > options.maxRetries) {
181 // Return the error as a proper error response instead of throwing
182 return { error: e }
183 }
184 }
185
186 // Check if aborted before backoff
187 failIfAborted(api.signal)
188
189 try {
190 await options.backoff(retry, options.maxRetries, api.signal)
191 } catch (backoffError) {
192 // If backoff was aborted, exit the retry loop
193 failIfAborted(api.signal)
194 // Otherwise, rethrow the backoff error
195 throw backoffError
196 }
197 }
198 }
199}
200
201/**
202 * A utility that can wrap `baseQuery` in the API definition to provide retries with a basic exponential backoff.
203 *
204 * @example
205 *
206 * ```ts
207 * // codeblock-meta title="Retry every request 5 times by default"
208 * import { createApi, fetchBaseQuery, retry } from '@reduxjs/toolkit/query/react'
209 * interface Post {
210 * id: number
211 * name: string
212 * }
213 * type PostsResponse = Post[]
214 *
215 * // maxRetries: 5 is the default, and can be omitted. Shown for documentation purposes.
216 * const staggeredBaseQuery = retry(fetchBaseQuery({ baseUrl: '/' }), { maxRetries: 5 });
217 * export const api = createApi({
218 * baseQuery: staggeredBaseQuery,
219 * endpoints: (build) => ({
220 * getPosts: build.query<PostsResponse, void>({
221 * query: () => ({ url: 'posts' }),
222 * }),
223 * getPost: build.query<PostsResponse, string>({
224 * query: (id) => ({ url: `post/${id}` }),
225 * extraOptions: { maxRetries: 8 }, // You can override the retry behavior on each endpoint
226 * }),
227 * }),
228 * });
229 *
230 * export const { useGetPostsQuery, useGetPostQuery } = api;
231 * ```
232 */
233export const retry = /* @__PURE__ */ Object.assign(retryWithBackoff, { fail })
Note: See TracBrowser for help on using the repository browser.