@happy-ts/fetch-t 1.4.1 → 1.5.1

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,43 @@ 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.1] - 2026-01-04
9
+
10
+ ### Fixed
11
+
12
+ - Fix `ReadableStream<Uint8Array<ArrayBuffer>>` type parameter for abortable stream overload
13
+ - Fix `ReadableStream` generic type parameter
14
+
15
+ ### Changed
16
+
17
+ - Refactor `multiplexStream` as internal function of `fetchT` for better encapsulation
18
+ - Use `Number.parseInt` instead of global `parseInt` for stricter linting compliance
19
+ - Upgrade `happy-rusty` dependency to ^1.9.0
20
+
21
+ ## [1.5.0] - 2026-01-04
22
+
23
+ ### Added
24
+
25
+ - Add automatic retry support with configurable strategies (`retry` option)
26
+ - `retries`: Number of retry attempts
27
+ - `delay`: Static delay or exponential backoff function
28
+ - `when`: Retry on specific HTTP status codes or custom condition
29
+ - `onRetry`: Callback before each retry attempt
30
+ - Add `'stream'` responseType to return raw `ReadableStream<Uint8Array>`
31
+ - Add runtime validation for `fetchT` options (responseType, timeout, callbacks, retry)
32
+ - Add `examples/with-retry.ts` with comprehensive retry examples
33
+
34
+ ### Changed
35
+
36
+ - Optimize timeout handling using native `AbortSignal.timeout()` and `AbortSignal.any()` APIs
37
+ - Upgrade `happy-rusty` dependency to ^1.8.0
38
+ - Upgrade `typescript-eslint` to ^8.51.0
39
+ - Upgrade `msw` to ^2.12.7
40
+
41
+ ### Fixed
42
+
43
+ - Fix abort reason always wrapped as Error with proper `ABORT_ERROR` name
44
+
8
45
  ## [1.4.1] - 2025-12-25
9
46
 
10
47
  ### Fixed
@@ -119,6 +156,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
119
156
  - Timeout support
120
157
  - Rust-like Result type error handling via `happy-rusty` library
121
158
 
159
+ [1.5.1]: https://github.com/JiangJie/fetch-t/compare/v1.5.0...v1.5.1
160
+ [1.5.0]: https://github.com/JiangJie/fetch-t/compare/v1.4.1...v1.5.0
122
161
  [1.4.1]: https://github.com/JiangJie/fetch-t/compare/v1.4.0...v1.4.1
123
162
  [1.4.0]: https://github.com/JiangJie/fetch-t/compare/v1.3.3...v1.4.0
124
163
  [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,170 @@ 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;
60
59
  }
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);
78
- }
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);
75
+ }
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 multiplexStream = (res) => {
96
+ const [notifyStream, responseStream] = res.body.tee();
97
+ const reader = notifyStream.getReader();
98
+ let totalByteLength = null;
99
+ let completedByteLength = 0;
100
+ if (onProgress) {
101
+ const contentLength = res.headers.get("content-length");
102
+ if (contentLength == null) {
103
+ try {
104
+ onProgress(happyRusty.Err(new Error("No content-length in response headers.")));
105
+ } catch {
79
106
  }
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
- }
89
- }
90
- if (shouldNotifyProgress && totalByteLength != null) {
91
- completedByteLength += value.byteLength;
92
- try {
93
- onProgress(happyRusty.Ok({
94
- totalByteLength,
95
- completedByteLength
96
- }));
97
- } catch {
98
- }
99
- }
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
108
- });
107
+ } else {
108
+ totalByteLength = Number.parseInt(contentLength, 10);
109
109
  }
110
110
  }
111
+ reader.read().then(function notify({ done, value }) {
112
+ if (done) {
113
+ return;
114
+ }
115
+ if (onChunk) {
116
+ try {
117
+ onChunk(value);
118
+ } catch {
119
+ }
120
+ }
121
+ if (onProgress && totalByteLength != null) {
122
+ completedByteLength += value.byteLength;
123
+ try {
124
+ onProgress(happyRusty.Ok({
125
+ totalByteLength,
126
+ completedByteLength
127
+ }));
128
+ } catch {
129
+ }
130
+ }
131
+ reader.read().then(notify).catch(() => {
132
+ });
133
+ }).catch(() => {
134
+ });
135
+ return new Response(responseStream, {
136
+ headers: res.headers,
137
+ status: res.status,
138
+ statusText: res.statusText
139
+ });
140
+ };
141
+ const processResponse = async (res) => {
142
+ let response2 = res;
143
+ if (res.body && (onProgress || onChunk)) {
144
+ response2 = multiplexStream(res);
145
+ }
111
146
  switch (responseType) {
112
147
  case "arraybuffer": {
113
- return happyRusty.Ok(await res.arrayBuffer());
148
+ return happyRusty.Ok(await response2.arrayBuffer());
114
149
  }
115
150
  case "blob": {
116
- return happyRusty.Ok(await res.blob());
151
+ return happyRusty.Ok(await response2.blob());
117
152
  }
118
153
  case "json": {
119
154
  try {
120
- return happyRusty.Ok(await res.json());
155
+ return happyRusty.Ok(await response2.json());
121
156
  } catch {
122
157
  return happyRusty.Err(new Error("Response is invalid json while responseType is json"));
123
158
  }
124
159
  }
160
+ case "stream": {
161
+ return happyRusty.Ok(response2.body);
162
+ }
125
163
  case "text": {
126
- return happyRusty.Ok(await res.text());
164
+ return happyRusty.Ok(await response2.text());
127
165
  }
128
166
  default: {
129
- return happyRusty.Ok(res);
167
+ return happyRusty.Ok(response2);
130
168
  }
131
169
  }
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) {
170
+ };
171
+ const fetchWithRetry = async () => {
172
+ let lastError;
173
+ let attempt = 0;
174
+ do {
175
+ if (attempt > 0) {
176
+ if (userController?.signal.aborted) {
177
+ return happyRusty.Err(userController.signal.reason);
178
+ }
179
+ const delayMs = getRetryDelay(attempt);
180
+ if (delayMs > 0) {
181
+ await delay(delayMs);
182
+ if (userController?.signal.aborted) {
183
+ return happyRusty.Err(userController.signal.reason);
184
+ }
185
+ }
186
+ try {
187
+ onRetry?.(lastError, attempt);
188
+ } catch {
189
+ }
190
+ }
191
+ const result = await doFetch();
192
+ if (result.isOk()) {
193
+ return result;
194
+ }
195
+ lastError = result.unwrapErr();
196
+ attempt++;
197
+ } while (attempt <= retries && shouldRetry(lastError, attempt));
198
+ return happyRusty.Err(lastError);
199
+ };
200
+ const response = fetchWithRetry();
201
+ if (abortable && userController) {
148
202
  return {
149
203
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
150
204
  abort(reason) {
151
- cancelTimer?.();
152
- controller.abort(reason);
205
+ if (reason instanceof Error) {
206
+ userController.abort(reason);
207
+ } else if (reason != null) {
208
+ userController.abort(wrapAbortReason(reason));
209
+ } else {
210
+ userController.abort();
211
+ }
153
212
  },
154
213
  get aborted() {
155
- return controller.signal.aborted;
214
+ return userController.signal.aborted;
156
215
  },
157
216
  get response() {
158
217
  return response;
@@ -161,6 +220,62 @@ function fetchT(url, init) {
161
220
  }
162
221
  return response;
163
222
  }
223
+ function delay(ms) {
224
+ return new Promise((resolve) => setTimeout(resolve, ms));
225
+ }
226
+ function wrapAbortReason(reason) {
227
+ const error = new Error(typeof reason === "string" ? reason : String(reason));
228
+ error.name = ABORT_ERROR;
229
+ error.cause = reason;
230
+ return error;
231
+ }
232
+ function validateOptions(init) {
233
+ const {
234
+ responseType,
235
+ timeout,
236
+ retry: retryOptions = 0,
237
+ onProgress,
238
+ onChunk
239
+ } = init;
240
+ if (responseType != null) {
241
+ const validTypes = ["text", "arraybuffer", "blob", "json", "stream"];
242
+ invariant(validTypes.includes(responseType), () => `responseType must be one of ${validTypes.join(", ")} but received ${responseType}`);
243
+ }
244
+ if (timeout != null) {
245
+ invariant(typeof timeout === "number" && timeout > 0, () => `timeout must be a number greater than 0 but received ${timeout}`);
246
+ }
247
+ if (onProgress != null) {
248
+ invariant(typeof onProgress === "function", () => `onProgress callback must be a function but received ${typeof onProgress}`);
249
+ }
250
+ if (onChunk != null) {
251
+ invariant(typeof onChunk === "function", () => `onChunk callback must be a function but received ${typeof onChunk}`);
252
+ }
253
+ let retries = 0;
254
+ let delay2 = 0;
255
+ let when;
256
+ let onRetry;
257
+ if (typeof retryOptions === "number") {
258
+ retries = retryOptions;
259
+ } else if (retryOptions && typeof retryOptions === "object") {
260
+ retries = retryOptions.retries ?? 0;
261
+ delay2 = retryOptions.delay ?? 0;
262
+ when = retryOptions.when;
263
+ onRetry = retryOptions.onRetry;
264
+ }
265
+ invariant(Number.isInteger(retries) && retries >= 0, () => `Retry count must be a non-negative integer but received ${retries}`);
266
+ if (typeof delay2 === "number") {
267
+ invariant(delay2 >= 0, () => `Retry delay must be a non-negative number but received ${delay2}`);
268
+ } else {
269
+ invariant(typeof delay2 === "function", () => `Retry delay must be a number or a function but received ${typeof delay2}`);
270
+ }
271
+ if (when != null) {
272
+ invariant(Array.isArray(when) || typeof when === "function", () => `Retry when condition must be an array of status codes or a function but received ${typeof when}`);
273
+ }
274
+ if (onRetry != null) {
275
+ invariant(typeof onRetry === "function", () => `Retry onRetry callback must be a function but received ${typeof onRetry}`);
276
+ }
277
+ return { retries, delay: delay2, when, onRetry };
278
+ }
164
279
 
165
280
  exports.ABORT_ERROR = ABORT_ERROR;
166
281
  exports.FetchError = FetchError;