@happy-ts/fetch-t 1.4.0 → 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,42 @@ 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
+
32
+ ## [1.4.1] - 2025-12-25
33
+
34
+ ### Fixed
35
+
36
+ - Fix unhandled promise rejections when `onChunk` or `onProgress` callbacks throw errors
37
+ - Fix unhandled promise rejections during stream read errors
38
+
39
+ ### Changed
40
+
41
+ - Upgrade `happy-rusty` dependency to ^1.6.2
42
+ - Upgrade `typescript-eslint` to ^8.50.1
43
+
8
44
  ## [1.4.0] - 2025-12-19
9
45
 
10
46
  ### Changed
@@ -107,7 +143,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
107
143
  - Timeout support
108
144
  - Rust-like Result type error handling via `happy-rusty` library
109
145
 
110
- [Unreleased]: https://github.com/JiangJie/fetch-t/compare/v1.4.0...HEAD
146
+ [1.5.0]: https://github.com/JiangJie/fetch-t/compare/v1.4.1...v1.5.0
147
+ [1.4.1]: https://github.com/JiangJie/fetch-t/compare/v1.4.0...v1.4.1
111
148
  [1.4.0]: https://github.com/JiangJie/fetch-t/compare/v1.3.3...v1.4.0
112
149
  [1.3.3]: https://github.com/JiangJie/fetch-t/compare/v1.3.2...v1.3.3
113
150
  [1.3.2]: https://github.com/JiangJie/fetch-t/compare/v1.3.1...v1.3.2
package/README.cn.md CHANGED
@@ -1,16 +1,20 @@
1
1
  # fetchT
2
2
 
3
+ [![License](https://img.shields.io/npm/l/@happy-ts/fetch-t.svg)](LICENSE)
4
+ [![Build Status](https://github.com/JiangJie/fetch-t/actions/workflows/test.yml/badge.svg)](https://github.com/JiangJie/fetch-t/actions/workflows/test.yml)
5
+ [![codecov](https://codecov.io/gh/JiangJie/fetch-t/graph/badge.svg)](https://codecov.io/gh/JiangJie/fetch-t)
3
6
  [![NPM version](https://img.shields.io/npm/v/@happy-ts/fetch-t.svg)](https://npmjs.org/package/@happy-ts/fetch-t)
4
7
  [![NPM downloads](https://badgen.net/npm/dm/@happy-ts/fetch-t)](https://npmjs.org/package/@happy-ts/fetch-t)
5
8
  [![JSR Version](https://jsr.io/badges/@happy-ts/fetch-t)](https://jsr.io/@happy-ts/fetch-t)
6
9
  [![JSR Score](https://jsr.io/badges/@happy-ts/fetch-t/score)](https://jsr.io/@happy-ts/fetch-t/score)
7
- [![Build Status](https://github.com/JiangJie/fetch-t/actions/workflows/test.yml/badge.svg)](https://github.com/JiangJie/fetch-t/actions/workflows/test.yml)
8
- [![codecov](https://codecov.io/gh/JiangJie/fetch-t/graph/badge.svg)](https://codecov.io/gh/JiangJie/fetch-t)
9
- [![License](https://img.shields.io/npm/l/@happy-ts/fetch-t.svg)](https://github.com/JiangJie/fetch-t/blob/main/LICENSE)
10
10
 
11
- [English](README.md)
11
+ 类型安全的 Fetch API 封装,支持可中止请求、超时、进度追踪、自动重试和 Rust 风格的 Result 错误处理。
12
+
13
+ ---
12
14
 
13
- 类型安全的 Fetch API 封装,支持可中止请求、超时、进度追踪和 Rust 风格的 Result 错误处理。
15
+ [English](README.md) | [API 文档](https://jiangjie.github.io/fetch-t/)
16
+
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,98 +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/) 目录。
167
-
168
- ## 文档
169
-
170
- 完整 API 文档请访问 [https://jiangjie.github.io/fetch-t/](https://jiangjie.github.io/fetch-t/)
100
+ - [基础用法](examples/basic.ts) - 基本请求示例
101
+ - [进度追踪](examples/with-progress.ts) - 下载进度和数据流处理
102
+ - [可中止请求](examples/abortable.ts) - 取消和超时请求
103
+ - [自动重试](examples/with-retry.ts) - 自动重试策略
104
+ - [错误处理](examples/error-handling.ts) - 错误处理模式
171
105
 
172
106
  ## 许可证
173
107
 
package/README.md CHANGED
@@ -1,16 +1,20 @@
1
1
  # fetchT
2
2
 
3
+ [![License](https://img.shields.io/npm/l/@happy-ts/fetch-t.svg)](LICENSE)
4
+ [![Build Status](https://github.com/JiangJie/fetch-t/actions/workflows/test.yml/badge.svg)](https://github.com/JiangJie/fetch-t/actions/workflows/test.yml)
5
+ [![codecov](https://codecov.io/gh/JiangJie/fetch-t/graph/badge.svg)](https://codecov.io/gh/JiangJie/fetch-t)
3
6
  [![NPM version](https://img.shields.io/npm/v/@happy-ts/fetch-t.svg)](https://npmjs.org/package/@happy-ts/fetch-t)
4
7
  [![NPM downloads](https://badgen.net/npm/dm/@happy-ts/fetch-t)](https://npmjs.org/package/@happy-ts/fetch-t)
5
8
  [![JSR Version](https://jsr.io/badges/@happy-ts/fetch-t)](https://jsr.io/@happy-ts/fetch-t)
6
9
  [![JSR Score](https://jsr.io/badges/@happy-ts/fetch-t/score)](https://jsr.io/@happy-ts/fetch-t/score)
7
- [![Build Status](https://github.com/JiangJie/fetch-t/actions/workflows/test.yml/badge.svg)](https://github.com/JiangJie/fetch-t/actions/workflows/test.yml)
8
- [![codecov](https://codecov.io/gh/JiangJie/fetch-t/graph/badge.svg)](https://codecov.io/gh/JiangJie/fetch-t)
9
- [![License](https://img.shields.io/npm/l/@happy-ts/fetch-t.svg)](https://github.com/JiangJie/fetch-t/blob/main/LICENSE)
10
10
 
11
- [中文文档](README.cn.md)
11
+ Type-safe Fetch API wrapper with abortable requests, timeout support, progress tracking, automatic retry, and Rust-like Result error handling.
12
+
13
+ ---
12
14
 
13
- Type-safe Fetch API wrapper with abortable requests, timeout support, progress tracking, and Rust-like Result error handling.
15
+ [中文](README.cn.md) | [API Documentation](https://jiangjie.github.io/fetch-t/)
16
+
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,98 +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.
167
-
168
- ## Documentation
169
-
170
- Full API documentation is available at [https://jiangjie.github.io/fetch-t/](https://jiangjie.github.io/fetch-t/)
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
171
105
 
172
106
  ## License
173
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,107 +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) {
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 {
72
106
  onProgress(happyRusty.Err(new Error("No content-length in response headers.")));
73
- } else {
74
- totalByteLength = parseInt(contentLength, 10);
107
+ } catch {
75
108
  }
109
+ } else {
110
+ totalByteLength = parseInt(contentLength, 10);
76
111
  }
77
- reader.read().then(function notify({ done, value }) {
78
- if (done) {
79
- return;
80
- }
81
- if (shouldNotifyChunk) {
112
+ }
113
+ reader.read().then(function notify({ done, value }) {
114
+ if (done) {
115
+ return;
116
+ }
117
+ if (onChunk) {
118
+ try {
82
119
  onChunk(value);
120
+ } catch {
83
121
  }
84
- if (shouldNotifyProgress && totalByteLength != null) {
85
- completedByteLength += value.byteLength;
122
+ }
123
+ if (onProgress && totalByteLength != null) {
124
+ completedByteLength += value.byteLength;
125
+ try {
86
126
  onProgress(happyRusty.Ok({
87
127
  totalByteLength,
88
128
  completedByteLength
89
129
  }));
130
+ } catch {
90
131
  }
91
- reader.read().then(notify);
92
- });
93
- res = new Response(stream2, {
94
- headers: res.headers,
95
- status: res.status,
96
- statusText: res.statusText
132
+ }
133
+ reader.read().then(notify).catch(() => {
97
134
  });
98
- }
135
+ }).catch(() => {
136
+ });
137
+ response2 = new Response(stream2, {
138
+ headers: res.headers,
139
+ status: res.status,
140
+ statusText: res.statusText
141
+ });
99
142
  }
100
143
  switch (responseType) {
101
144
  case "arraybuffer": {
102
- return happyRusty.Ok(await res.arrayBuffer());
145
+ return happyRusty.Ok(await response2.arrayBuffer());
103
146
  }
104
147
  case "blob": {
105
- return happyRusty.Ok(await res.blob());
148
+ return happyRusty.Ok(await response2.blob());
106
149
  }
107
150
  case "json": {
108
151
  try {
109
- return happyRusty.Ok(await res.json());
152
+ return happyRusty.Ok(await response2.json());
110
153
  } catch {
111
154
  return happyRusty.Err(new Error("Response is invalid json while responseType is json"));
112
155
  }
113
156
  }
157
+ case "stream": {
158
+ return happyRusty.Ok(response2.body);
159
+ }
114
160
  case "text": {
115
- return happyRusty.Ok(await res.text());
161
+ return happyRusty.Ok(await response2.text());
116
162
  }
117
163
  default: {
118
- return happyRusty.Ok(res);
164
+ return happyRusty.Ok(response2);
119
165
  }
120
166
  }
121
- }).catch((err) => {
122
- cancelTimer?.();
123
- return happyRusty.Err(err);
124
- });
125
- if (shouldWaitTimeout) {
126
- const timer = setTimeout(() => {
127
- const error = new Error();
128
- error.name = TIMEOUT_ERROR;
129
- controller.abort(error);
130
- }, timeout);
131
- cancelTimer = () => {
132
- clearTimeout(timer);
133
- cancelTimer = null;
134
- };
135
- }
136
- 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) {
137
199
  return {
138
200
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
139
201
  abort(reason) {
140
- cancelTimer?.();
141
- 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
+ }
142
209
  },
143
210
  get aborted() {
144
- return controller.signal.aborted;
211
+ return userController.signal.aborted;
145
212
  },
146
213
  get response() {
147
214
  return response;
@@ -150,6 +217,62 @@ function fetchT(url, init) {
150
217
  }
151
218
  return response;
152
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
+ }
153
276
 
154
277
  exports.ABORT_ERROR = ABORT_ERROR;
155
278
  exports.FetchError = FetchError;