@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 +25 -0
- package/README.cn.md +17 -79
- package/README.md +17 -79
- package/dist/main.cjs +196 -84
- package/dist/main.cjs.map +1 -1
- package/dist/main.mjs +196 -84
- package/dist/main.mjs.map +1 -1
- package/dist/types.d.ts +87 -1
- package/package.json +6 -6
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
|
[](https://jsr.io/@happy-ts/fetch-t)
|
|
9
9
|
[](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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
[](https://jsr.io/@happy-ts/fetch-t)
|
|
9
9
|
[](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
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
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
|
-
} =
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}
|
|
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
|
-
|
|
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
|
|
145
|
+
return happyRusty.Ok(await response2.arrayBuffer());
|
|
114
146
|
}
|
|
115
147
|
case "blob": {
|
|
116
|
-
return happyRusty.Ok(await
|
|
148
|
+
return happyRusty.Ok(await response2.blob());
|
|
117
149
|
}
|
|
118
150
|
case "json": {
|
|
119
151
|
try {
|
|
120
|
-
return happyRusty.Ok(await
|
|
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
|
|
161
|
+
return happyRusty.Ok(await response2.text());
|
|
127
162
|
}
|
|
128
163
|
default: {
|
|
129
|
-
return happyRusty.Ok(
|
|
164
|
+
return happyRusty.Ok(response2);
|
|
130
165
|
}
|
|
131
166
|
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
152
|
-
|
|
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
|
|
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;
|