@happy-ts/fetch-t 1.4.1 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,30 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.5.0] - 2026-01-04
9
+
10
+ ### Added
11
+
12
+ - Add automatic retry support with configurable strategies (`retry` option)
13
+ - `retries`: Number of retry attempts
14
+ - `delay`: Static delay or exponential backoff function
15
+ - `when`: Retry on specific HTTP status codes or custom condition
16
+ - `onRetry`: Callback before each retry attempt
17
+ - Add `'stream'` responseType to return raw `ReadableStream<Uint8Array>`
18
+ - Add runtime validation for `fetchT` options (responseType, timeout, callbacks, retry)
19
+ - Add `examples/with-retry.ts` with comprehensive retry examples
20
+
21
+ ### Changed
22
+
23
+ - Optimize timeout handling using native `AbortSignal.timeout()` and `AbortSignal.any()` APIs
24
+ - Upgrade `happy-rusty` dependency to ^1.8.0
25
+ - Upgrade `typescript-eslint` to ^8.51.0
26
+ - Upgrade `msw` to ^2.12.7
27
+
28
+ ### Fixed
29
+
30
+ - Fix abort reason always wrapped as Error with proper `ABORT_ERROR` name
31
+
8
32
  ## [1.4.1] - 2025-12-25
9
33
 
10
34
  ### Fixed
@@ -119,6 +143,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
119
143
  - Timeout support
120
144
  - Rust-like Result type error handling via `happy-rusty` library
121
145
 
146
+ [1.5.0]: https://github.com/JiangJie/fetch-t/compare/v1.4.1...v1.5.0
122
147
  [1.4.1]: https://github.com/JiangJie/fetch-t/compare/v1.4.0...v1.4.1
123
148
  [1.4.0]: https://github.com/JiangJie/fetch-t/compare/v1.3.3...v1.4.0
124
149
  [1.3.3]: https://github.com/JiangJie/fetch-t/compare/v1.3.2...v1.3.3
package/README.cn.md CHANGED
@@ -8,9 +8,13 @@
8
8
  [![JSR Version](https://jsr.io/badges/@happy-ts/fetch-t)](https://jsr.io/@happy-ts/fetch-t)
9
9
  [![JSR Score](https://jsr.io/badges/@happy-ts/fetch-t/score)](https://jsr.io/@happy-ts/fetch-t/score)
10
10
 
11
+ 类型安全的 Fetch API 封装,支持可中止请求、超时、进度追踪、自动重试和 Rust 风格的 Result 错误处理。
12
+
13
+ ---
14
+
11
15
  [English](README.md) | [API 文档](https://jiangjie.github.io/fetch-t/)
12
16
 
13
- 类型安全的 Fetch API 封装,支持可中止请求、超时、进度追踪和 Rust 风格的 Result 错误处理。
17
+ ---
14
18
 
15
19
  ## 特性
16
20
 
@@ -19,6 +23,7 @@
19
23
  - **超时支持** - 指定毫秒数后自动中止请求
20
24
  - **进度追踪** - 通过 `onProgress` 回调监控下载进度
21
25
  - **数据流处理** - 通过 `onChunk` 回调访问原始数据块
26
+ - **自动重试** - 通过 `retry` 选项配置失败重试策略
22
27
  - **Result 错误处理** - Rust 风格的 `Result` 类型实现显式错误处理
23
28
  - **跨平台** - 支持 Deno、Node.js、Bun 和浏览器
24
29
 
@@ -76,94 +81,27 @@ setTimeout(() => {
76
81
  const result = await task.response;
77
82
  ```
78
83
 
79
- ### 超时控制
84
+ ### 自动重试
80
85
 
81
86
  ```ts
82
87
  const result = await fetchT('https://api.example.com/data', {
83
- responseType: 'json',
84
- timeout: 3000, // 3 秒后自动中止
85
- });
86
- ```
87
-
88
- ### 进度追踪
89
-
90
- ```ts
91
- const result = await fetchT('https://api.example.com/large-file', {
92
- responseType: 'blob',
93
- onProgress(progressResult) {
94
- progressResult.inspect(progress => {
95
- const percent = (progress.completedByteLength / progress.totalByteLength * 100).toFixed(1);
96
- console.log(`下载进度: ${percent}%`);
97
- });
88
+ retry: {
89
+ retries: 3,
90
+ delay: (attempt) => Math.min(1000 * Math.pow(2, attempt - 1), 10000),
91
+ when: [500, 502, 503, 504],
92
+ onRetry: (error, attempt) => console.log(`重试 ${attempt}: ${error.message}`),
98
93
  },
99
- });
100
- ```
101
-
102
- ### 错误处理
103
-
104
- ```ts
105
- import { fetchT, ABORT_ERROR, TIMEOUT_ERROR } from '@happy-ts/fetch-t';
106
-
107
- const result = await fetchT('https://api.example.com/data', {
108
94
  responseType: 'json',
109
- timeout: 3000,
110
95
  });
111
-
112
- if (result.isErr()) {
113
- const err = result.unwrapErr();
114
- if (err.name === TIMEOUT_ERROR) {
115
- console.log('请求超时');
116
- } else if (err.name === ABORT_ERROR) {
117
- console.log('请求已中止');
118
- } else {
119
- console.log('请求失败:', err.message);
120
- }
121
- } else {
122
- console.log('数据:', result.unwrap());
123
- }
124
96
  ```
125
97
 
126
- ## API
127
-
128
- ### `fetchT(url, options?)`
129
-
130
- | 参数 | 类型 | 描述 |
131
- |------|------|------|
132
- | `url` | `string \| URL` | 请求 URL |
133
- | `options` | `FetchInit` | 扩展的 fetch 选项 |
134
-
135
- ### `FetchInit` 选项
136
-
137
- 继承标准 `RequestInit`,额外支持:
138
-
139
- | 选项 | 类型 | 描述 |
140
- |------|------|------|
141
- | `abortable` | `boolean` | 如为 `true`,返回 `FetchTask` 而非 `FetchResponse` |
142
- | `responseType` | `'text' \| 'arraybuffer' \| 'blob' \| 'json'` | 指定返回数据类型 |
143
- | `timeout` | `number` | 指定毫秒数后自动中止 |
144
- | `onProgress` | `(result: IOResult<FetchProgress>) => void` | 下载进度回调 |
145
- | `onChunk` | `(chunk: Uint8Array) => void` | 原始数据块回调 |
146
-
147
- ### `FetchTask<T>`
148
-
149
- 当 `abortable: true` 时返回:
150
-
151
- | 属性/方法 | 类型 | 描述 |
152
- |-----------|------|------|
153
- | `response` | `FetchResponse<T>` | 响应 Promise |
154
- | `abort(reason?)` | `void` | 中止请求 |
155
- | `aborted` | `boolean` | 请求是否已中止 |
156
-
157
- ### 常量
158
-
159
- | 常量 | 描述 |
160
- |------|------|
161
- | `ABORT_ERROR` | 中止请求的错误名称 |
162
- | `TIMEOUT_ERROR` | 超时请求的错误名称 |
163
-
164
98
  ## 示例
165
99
 
166
- 更多示例请参见 [examples](examples/) 目录。
100
+ - [基础用法](examples/basic.ts) - 基本请求示例
101
+ - [进度追踪](examples/with-progress.ts) - 下载进度和数据流处理
102
+ - [可中止请求](examples/abortable.ts) - 取消和超时请求
103
+ - [自动重试](examples/with-retry.ts) - 自动重试策略
104
+ - [错误处理](examples/error-handling.ts) - 错误处理模式
167
105
 
168
106
  ## 许可证
169
107
 
package/README.md CHANGED
@@ -8,9 +8,13 @@
8
8
  [![JSR Version](https://jsr.io/badges/@happy-ts/fetch-t)](https://jsr.io/@happy-ts/fetch-t)
9
9
  [![JSR Score](https://jsr.io/badges/@happy-ts/fetch-t/score)](https://jsr.io/@happy-ts/fetch-t/score)
10
10
 
11
+ Type-safe Fetch API wrapper with abortable requests, timeout support, progress tracking, automatic retry, and Rust-like Result error handling.
12
+
13
+ ---
14
+
11
15
  [中文](README.cn.md) | [API Documentation](https://jiangjie.github.io/fetch-t/)
12
16
 
13
- Type-safe Fetch API wrapper with abortable requests, timeout support, progress tracking, and Rust-like Result error handling.
17
+ ---
14
18
 
15
19
  ## Features
16
20
 
@@ -19,6 +23,7 @@ Type-safe Fetch API wrapper with abortable requests, timeout support, progress t
19
23
  - **Timeout Support** - Auto-abort requests after specified milliseconds
20
24
  - **Progress Tracking** - Monitor download progress with `onProgress` callback
21
25
  - **Chunk Streaming** - Access raw data chunks via `onChunk` callback
26
+ - **Automatic Retry** - Configurable retry strategies with `retry` option
22
27
  - **Result Error Handling** - Rust-like `Result` type for explicit error handling
23
28
  - **Cross-platform** - Works with Deno, Node.js, Bun, and browsers
24
29
 
@@ -76,94 +81,27 @@ setTimeout(() => {
76
81
  const result = await task.response;
77
82
  ```
78
83
 
79
- ### With Timeout
84
+ ### Automatic Retry
80
85
 
81
86
  ```ts
82
87
  const result = await fetchT('https://api.example.com/data', {
83
- responseType: 'json',
84
- timeout: 3000, // Auto-abort after 3 seconds
85
- });
86
- ```
87
-
88
- ### Progress Tracking
89
-
90
- ```ts
91
- const result = await fetchT('https://api.example.com/large-file', {
92
- responseType: 'blob',
93
- onProgress(progressResult) {
94
- progressResult.inspect(progress => {
95
- const percent = (progress.completedByteLength / progress.totalByteLength * 100).toFixed(1);
96
- console.log(`Download: ${percent}%`);
97
- });
88
+ retry: {
89
+ retries: 3,
90
+ delay: (attempt) => Math.min(1000 * Math.pow(2, attempt - 1), 10000),
91
+ when: [500, 502, 503, 504],
92
+ onRetry: (error, attempt) => console.log(`Retry ${attempt}: ${error.message}`),
98
93
  },
99
- });
100
- ```
101
-
102
- ### Error Handling
103
-
104
- ```ts
105
- import { fetchT, ABORT_ERROR, TIMEOUT_ERROR } from '@happy-ts/fetch-t';
106
-
107
- const result = await fetchT('https://api.example.com/data', {
108
94
  responseType: 'json',
109
- timeout: 3000,
110
95
  });
111
-
112
- if (result.isErr()) {
113
- const err = result.unwrapErr();
114
- if (err.name === TIMEOUT_ERROR) {
115
- console.log('Request timed out');
116
- } else if (err.name === ABORT_ERROR) {
117
- console.log('Request was aborted');
118
- } else {
119
- console.log('Request failed:', err.message);
120
- }
121
- } else {
122
- console.log('Data:', result.unwrap());
123
- }
124
96
  ```
125
97
 
126
- ## API
127
-
128
- ### `fetchT(url, options?)`
129
-
130
- | Parameter | Type | Description |
131
- |-----------|------|-------------|
132
- | `url` | `string \| URL` | Request URL |
133
- | `options` | `FetchInit` | Extended fetch options |
134
-
135
- ### `FetchInit` Options
136
-
137
- Extends standard `RequestInit` with:
138
-
139
- | Option | Type | Description |
140
- |--------|------|-------------|
141
- | `abortable` | `boolean` | If `true`, returns `FetchTask` instead of `FetchResponse` |
142
- | `responseType` | `'text' \| 'arraybuffer' \| 'blob' \| 'json'` | Specifies return data type |
143
- | `timeout` | `number` | Auto-abort after milliseconds |
144
- | `onProgress` | `(result: IOResult<FetchProgress>) => void` | Download progress callback |
145
- | `onChunk` | `(chunk: Uint8Array) => void` | Raw data chunk callback |
146
-
147
- ### `FetchTask<T>`
148
-
149
- Returned when `abortable: true`:
150
-
151
- | Property/Method | Type | Description |
152
- |-----------------|------|-------------|
153
- | `response` | `FetchResponse<T>` | The response promise |
154
- | `abort(reason?)` | `void` | Abort the request |
155
- | `aborted` | `boolean` | Whether request was aborted |
156
-
157
- ### Constants
158
-
159
- | Constant | Description |
160
- |----------|-------------|
161
- | `ABORT_ERROR` | Error name for aborted requests |
162
- | `TIMEOUT_ERROR` | Error name for timed out requests |
163
-
164
98
  ## Examples
165
99
 
166
- For more examples, see the [examples](examples/) directory.
100
+ - [Basic](examples/basic.ts) - Basic fetch requests
101
+ - [Progress Tracking](examples/with-progress.ts) - Download progress and chunk streaming
102
+ - [Abortable](examples/abortable.ts) - Cancel and timeout requests
103
+ - [Retry](examples/with-retry.ts) - Automatic retry strategies
104
+ - [Error Handling](examples/error-handling.ts) - Error handling patterns
167
105
 
168
106
  ## License
169
107
 
package/dist/main.cjs CHANGED
@@ -31,8 +31,15 @@ class FetchError extends Error {
31
31
 
32
32
  function fetchT(url, init) {
33
33
  if (typeof url !== "string") {
34
- invariant(url instanceof URL, () => `Url must be a string or URL object but received ${url}.`);
34
+ invariant(url instanceof URL, () => `Url must be a string or URL object but received ${url}`);
35
35
  }
36
+ const fetchInit = init ?? {};
37
+ const {
38
+ retries,
39
+ delay: retryDelay,
40
+ when: retryWhen,
41
+ onRetry
42
+ } = validateOptions(fetchInit);
36
43
  const {
37
44
  // default not abortable
38
45
  abortable = false,
@@ -41,118 +48,167 @@ function fetchT(url, init) {
41
48
  onProgress,
42
49
  onChunk,
43
50
  ...rest
44
- } = init ?? {};
45
- const shouldWaitTimeout = timeout != null;
46
- let cancelTimer;
47
- if (shouldWaitTimeout) {
48
- invariant(typeof timeout === "number" && timeout > 0, () => `Timeout must be a number greater than 0 but received ${timeout}.`);
49
- }
50
- let controller;
51
- if (abortable || shouldWaitTimeout) {
52
- controller = new AbortController();
53
- rest.signal = controller.signal;
51
+ } = fetchInit;
52
+ let userController;
53
+ if (abortable) {
54
+ userController = new AbortController();
54
55
  }
55
- const response = fetch(url, rest).then(async (res) => {
56
- cancelTimer?.();
57
- if (!res.ok) {
58
- await res.body?.cancel();
59
- return happyRusty.Err(new FetchError(res.statusText, res.status));
56
+ const shouldRetry = (error, attempt) => {
57
+ if (error.name === ABORT_ERROR) {
58
+ return false;
59
+ }
60
+ if (!retryWhen) {
61
+ return !(error instanceof FetchError);
62
+ }
63
+ if (Array.isArray(retryWhen)) {
64
+ return error instanceof FetchError && retryWhen.includes(error.status);
65
+ }
66
+ return retryWhen(error, attempt);
67
+ };
68
+ const getRetryDelay = (attempt) => {
69
+ return typeof retryDelay === "function" ? retryDelay(attempt) : retryDelay;
70
+ };
71
+ const doFetch = async () => {
72
+ const signals = [];
73
+ if (userController) {
74
+ signals.push(userController.signal);
60
75
  }
61
- if (res.body) {
62
- const shouldNotifyProgress = typeof onProgress === "function";
63
- const shouldNotifyChunk = typeof onChunk === "function";
64
- if (shouldNotifyProgress || shouldNotifyChunk) {
65
- const [stream1, stream2] = res.body.tee();
66
- const reader = stream1.getReader();
67
- let totalByteLength = null;
68
- let completedByteLength = 0;
69
- if (shouldNotifyProgress) {
70
- const contentLength = res.headers.get("content-length");
71
- if (contentLength == null) {
72
- try {
73
- onProgress(happyRusty.Err(new Error("No content-length in response headers.")));
74
- } catch {
75
- }
76
- } else {
77
- totalByteLength = parseInt(contentLength, 10);
76
+ if (typeof timeout === "number") {
77
+ signals.push(AbortSignal.timeout(timeout));
78
+ }
79
+ if (signals.length > 0) {
80
+ rest.signal = signals.length === 1 ? signals[0] : AbortSignal.any(signals);
81
+ }
82
+ try {
83
+ const res = await fetch(url, rest);
84
+ if (!res.ok) {
85
+ await res.body?.cancel();
86
+ return happyRusty.Err(new FetchError(res.statusText, res.status));
87
+ }
88
+ return await processResponse(res);
89
+ } catch (err) {
90
+ return happyRusty.Err(
91
+ err instanceof Error ? err : wrapAbortReason(err)
92
+ );
93
+ }
94
+ };
95
+ const processResponse = async (res) => {
96
+ let response2 = res;
97
+ if (res.body && (onProgress || onChunk)) {
98
+ const [stream1, stream2] = res.body.tee();
99
+ const reader = stream1.getReader();
100
+ let totalByteLength = null;
101
+ let completedByteLength = 0;
102
+ if (onProgress) {
103
+ const contentLength = res.headers.get("content-length");
104
+ if (contentLength == null) {
105
+ try {
106
+ onProgress(happyRusty.Err(new Error("No content-length in response headers.")));
107
+ } catch {
78
108
  }
109
+ } else {
110
+ totalByteLength = parseInt(contentLength, 10);
79
111
  }
80
- reader.read().then(function notify({ done, value }) {
81
- if (done) {
82
- return;
83
- }
84
- if (shouldNotifyChunk) {
85
- try {
86
- onChunk(value);
87
- } catch {
88
- }
112
+ }
113
+ reader.read().then(function notify({ done, value }) {
114
+ if (done) {
115
+ return;
116
+ }
117
+ if (onChunk) {
118
+ try {
119
+ onChunk(value);
120
+ } catch {
89
121
  }
90
- if (shouldNotifyProgress && totalByteLength != null) {
91
- completedByteLength += value.byteLength;
92
- try {
93
- onProgress(happyRusty.Ok({
94
- totalByteLength,
95
- completedByteLength
96
- }));
97
- } catch {
98
- }
122
+ }
123
+ if (onProgress && totalByteLength != null) {
124
+ completedByteLength += value.byteLength;
125
+ try {
126
+ onProgress(happyRusty.Ok({
127
+ totalByteLength,
128
+ completedByteLength
129
+ }));
130
+ } catch {
99
131
  }
100
- reader.read().then(notify).catch(() => {
101
- });
102
- }).catch(() => {
103
- });
104
- res = new Response(stream2, {
105
- headers: res.headers,
106
- status: res.status,
107
- statusText: res.statusText
132
+ }
133
+ reader.read().then(notify).catch(() => {
108
134
  });
109
- }
135
+ }).catch(() => {
136
+ });
137
+ response2 = new Response(stream2, {
138
+ headers: res.headers,
139
+ status: res.status,
140
+ statusText: res.statusText
141
+ });
110
142
  }
111
143
  switch (responseType) {
112
144
  case "arraybuffer": {
113
- return happyRusty.Ok(await res.arrayBuffer());
145
+ return happyRusty.Ok(await response2.arrayBuffer());
114
146
  }
115
147
  case "blob": {
116
- return happyRusty.Ok(await res.blob());
148
+ return happyRusty.Ok(await response2.blob());
117
149
  }
118
150
  case "json": {
119
151
  try {
120
- return happyRusty.Ok(await res.json());
152
+ return happyRusty.Ok(await response2.json());
121
153
  } catch {
122
154
  return happyRusty.Err(new Error("Response is invalid json while responseType is json"));
123
155
  }
124
156
  }
157
+ case "stream": {
158
+ return happyRusty.Ok(response2.body);
159
+ }
125
160
  case "text": {
126
- return happyRusty.Ok(await res.text());
161
+ return happyRusty.Ok(await response2.text());
127
162
  }
128
163
  default: {
129
- return happyRusty.Ok(res);
164
+ return happyRusty.Ok(response2);
130
165
  }
131
166
  }
132
- }).catch((err) => {
133
- cancelTimer?.();
134
- return happyRusty.Err(err);
135
- });
136
- if (shouldWaitTimeout) {
137
- const timer = setTimeout(() => {
138
- const error = new Error();
139
- error.name = TIMEOUT_ERROR;
140
- controller.abort(error);
141
- }, timeout);
142
- cancelTimer = () => {
143
- clearTimeout(timer);
144
- cancelTimer = null;
145
- };
146
- }
147
- if (abortable) {
167
+ };
168
+ const fetchWithRetry = async () => {
169
+ let lastError;
170
+ let attempt = 0;
171
+ do {
172
+ if (attempt > 0) {
173
+ if (userController?.signal.aborted) {
174
+ return happyRusty.Err(userController.signal.reason);
175
+ }
176
+ const delayMs = getRetryDelay(attempt);
177
+ if (delayMs > 0) {
178
+ await delay(delayMs);
179
+ if (userController?.signal.aborted) {
180
+ return happyRusty.Err(userController.signal.reason);
181
+ }
182
+ }
183
+ try {
184
+ onRetry?.(lastError, attempt);
185
+ } catch {
186
+ }
187
+ }
188
+ const result = await doFetch();
189
+ if (result.isOk()) {
190
+ return result;
191
+ }
192
+ lastError = result.unwrapErr();
193
+ attempt++;
194
+ } while (attempt <= retries && shouldRetry(lastError, attempt));
195
+ return happyRusty.Err(lastError);
196
+ };
197
+ const response = fetchWithRetry();
198
+ if (abortable && userController) {
148
199
  return {
149
200
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
150
201
  abort(reason) {
151
- cancelTimer?.();
152
- controller.abort(reason);
202
+ if (reason instanceof Error) {
203
+ userController.abort(reason);
204
+ } else if (reason != null) {
205
+ userController.abort(wrapAbortReason(reason));
206
+ } else {
207
+ userController.abort();
208
+ }
153
209
  },
154
210
  get aborted() {
155
- return controller.signal.aborted;
211
+ return userController.signal.aborted;
156
212
  },
157
213
  get response() {
158
214
  return response;
@@ -161,6 +217,62 @@ function fetchT(url, init) {
161
217
  }
162
218
  return response;
163
219
  }
220
+ function delay(ms) {
221
+ return new Promise((resolve) => setTimeout(resolve, ms));
222
+ }
223
+ function wrapAbortReason(reason) {
224
+ const error = new Error(typeof reason === "string" ? reason : String(reason));
225
+ error.name = ABORT_ERROR;
226
+ error.cause = reason;
227
+ return error;
228
+ }
229
+ function validateOptions(init) {
230
+ const {
231
+ responseType,
232
+ timeout,
233
+ retry: retryOptions = 0,
234
+ onProgress,
235
+ onChunk
236
+ } = init;
237
+ if (responseType != null) {
238
+ const validTypes = ["text", "arraybuffer", "blob", "json", "stream"];
239
+ invariant(validTypes.includes(responseType), () => `responseType must be one of ${validTypes.join(", ")} but received ${responseType}`);
240
+ }
241
+ if (timeout != null) {
242
+ invariant(typeof timeout === "number" && timeout > 0, () => `timeout must be a number greater than 0 but received ${timeout}`);
243
+ }
244
+ if (onProgress != null) {
245
+ invariant(typeof onProgress === "function", () => `onProgress callback must be a function but received ${typeof onProgress}`);
246
+ }
247
+ if (onChunk != null) {
248
+ invariant(typeof onChunk === "function", () => `onChunk callback must be a function but received ${typeof onChunk}`);
249
+ }
250
+ let retries = 0;
251
+ let delay2 = 0;
252
+ let when;
253
+ let onRetry;
254
+ if (typeof retryOptions === "number") {
255
+ retries = retryOptions;
256
+ } else if (retryOptions && typeof retryOptions === "object") {
257
+ retries = retryOptions.retries ?? 0;
258
+ delay2 = retryOptions.delay ?? 0;
259
+ when = retryOptions.when;
260
+ onRetry = retryOptions.onRetry;
261
+ }
262
+ invariant(Number.isInteger(retries) && retries >= 0, () => `Retry count must be a non-negative integer but received ${retries}`);
263
+ if (typeof delay2 === "number") {
264
+ invariant(delay2 >= 0, () => `Retry delay must be a non-negative number but received ${delay2}`);
265
+ } else {
266
+ invariant(typeof delay2 === "function", () => `Retry delay must be a number or a function but received ${typeof delay2}`);
267
+ }
268
+ if (when != null) {
269
+ invariant(Array.isArray(when) || typeof when === "function", () => `Retry when condition must be an array of status codes or a function but received ${typeof when}`);
270
+ }
271
+ if (onRetry != null) {
272
+ invariant(typeof onRetry === "function", () => `Retry onRetry callback must be a function but received ${typeof onRetry}`);
273
+ }
274
+ return { retries, delay: delay2, when, onRetry };
275
+ }
164
276
 
165
277
  exports.ABORT_ERROR = ABORT_ERROR;
166
278
  exports.FetchError = FetchError;