@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 +38 -1
- package/README.cn.md +21 -87
- package/README.md +21 -87
- package/dist/main.cjs +190 -67
- package/dist/main.cjs.map +1 -1
- package/dist/main.mjs +190 -67
- package/dist/main.mjs.map +1 -1
- package/dist/types.d.ts +87 -1
- package/package.json +6 -7
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
|
-
[
|
|
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)
|
|
4
|
+
[](https://github.com/JiangJie/fetch-t/actions/workflows/test.yml)
|
|
5
|
+
[](https://codecov.io/gh/JiangJie/fetch-t)
|
|
3
6
|
[](https://npmjs.org/package/@happy-ts/fetch-t)
|
|
4
7
|
[](https://npmjs.org/package/@happy-ts/fetch-t)
|
|
5
8
|
[](https://jsr.io/@happy-ts/fetch-t)
|
|
6
9
|
[](https://jsr.io/@happy-ts/fetch-t/score)
|
|
7
|
-
[](https://github.com/JiangJie/fetch-t/actions/workflows/test.yml)
|
|
8
|
-
[](https://codecov.io/gh/JiangJie/fetch-t)
|
|
9
|
-
[](https://github.com/JiangJie/fetch-t/blob/main/LICENSE)
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
类型安全的 Fetch API 封装,支持可中止请求、超时、进度追踪、自动重试和 Rust 风格的 Result 错误处理。
|
|
12
|
+
|
|
13
|
+
---
|
|
12
14
|
|
|
13
|
-
|
|
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
|
-
|
|
84
|
-
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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)
|
|
4
|
+
[](https://github.com/JiangJie/fetch-t/actions/workflows/test.yml)
|
|
5
|
+
[](https://codecov.io/gh/JiangJie/fetch-t)
|
|
3
6
|
[](https://npmjs.org/package/@happy-ts/fetch-t)
|
|
4
7
|
[](https://npmjs.org/package/@happy-ts/fetch-t)
|
|
5
8
|
[](https://jsr.io/@happy-ts/fetch-t)
|
|
6
9
|
[](https://jsr.io/@happy-ts/fetch-t/score)
|
|
7
|
-
[](https://github.com/JiangJie/fetch-t/actions/workflows/test.yml)
|
|
8
|
-
[](https://codecov.io/gh/JiangJie/fetch-t)
|
|
9
|
-
[](https://github.com/JiangJie/fetch-t/blob/main/LICENSE)
|
|
10
10
|
|
|
11
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
84
|
+
### Automatic Retry
|
|
80
85
|
|
|
81
86
|
```ts
|
|
82
87
|
const result = await fetchT('https://api.example.com/data', {
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
} =
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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 (
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
}
|
|
74
|
-
totalByteLength = parseInt(contentLength, 10);
|
|
107
|
+
} catch {
|
|
75
108
|
}
|
|
109
|
+
} else {
|
|
110
|
+
totalByteLength = parseInt(contentLength, 10);
|
|
76
111
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
85
|
-
|
|
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
|
-
|
|
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
|
|
145
|
+
return happyRusty.Ok(await response2.arrayBuffer());
|
|
103
146
|
}
|
|
104
147
|
case "blob": {
|
|
105
|
-
return happyRusty.Ok(await
|
|
148
|
+
return happyRusty.Ok(await response2.blob());
|
|
106
149
|
}
|
|
107
150
|
case "json": {
|
|
108
151
|
try {
|
|
109
|
-
return happyRusty.Ok(await
|
|
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
|
|
161
|
+
return happyRusty.Ok(await response2.text());
|
|
116
162
|
}
|
|
117
163
|
default: {
|
|
118
|
-
return happyRusty.Ok(
|
|
164
|
+
return happyRusty.Ok(response2);
|
|
119
165
|
}
|
|
120
166
|
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
141
|
-
|
|
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
|
|
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;
|