@happy-ts/fetch-t 1.9.1 → 1.10.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 +15 -0
- package/dist/main.cjs +500 -306
- package/dist/main.cjs.map +1 -1
- package/dist/main.mjs +499 -302
- package/dist/main.mjs.map +1 -1
- package/dist/types.d.ts +3 -3
- package/package.json +14 -14
package/dist/main.cjs
CHANGED
|
@@ -1,323 +1,517 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
let happy_rusty = require("happy-rusty");
|
|
3
|
+
//#region src/fetch/constants.ts
|
|
4
|
+
/**
|
|
5
|
+
* Error name for aborted fetch requests.
|
|
6
|
+
*
|
|
7
|
+
* This matches the standard `AbortError` name used by the Fetch API when a request
|
|
8
|
+
* is cancelled via `AbortController.abort()`.
|
|
9
|
+
*
|
|
10
|
+
* @since 1.0.0
|
|
11
|
+
* @example
|
|
12
|
+
* ```typescript
|
|
13
|
+
* import { fetchT, ABORT_ERROR } from '@happy-ts/fetch-t';
|
|
14
|
+
*
|
|
15
|
+
* const task = fetchT('https://api.example.com/data', { abortable: true });
|
|
16
|
+
* task.abort();
|
|
17
|
+
*
|
|
18
|
+
* const result = await task.result;
|
|
19
|
+
* result.inspectErr((err) => {
|
|
20
|
+
* if (err.name === ABORT_ERROR) {
|
|
21
|
+
* console.log('Request was aborted');
|
|
22
|
+
* }
|
|
23
|
+
* });
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
7
26
|
const ABORT_ERROR = "AbortError";
|
|
27
|
+
/**
|
|
28
|
+
* Error name for timed out fetch requests.
|
|
29
|
+
*
|
|
30
|
+
* This is set on the `Error.name` property when a request exceeds the specified
|
|
31
|
+
* `timeout` duration and is automatically aborted.
|
|
32
|
+
*
|
|
33
|
+
* @since 1.0.0
|
|
34
|
+
* @example
|
|
35
|
+
* ```typescript
|
|
36
|
+
* import { fetchT, TIMEOUT_ERROR } from '@happy-ts/fetch-t';
|
|
37
|
+
*
|
|
38
|
+
* const result = await fetchT('https://api.example.com/slow-endpoint', {
|
|
39
|
+
* timeout: 5000, // 5 seconds
|
|
40
|
+
* });
|
|
41
|
+
*
|
|
42
|
+
* result.inspectErr((err) => {
|
|
43
|
+
* if (err.name === TIMEOUT_ERROR) {
|
|
44
|
+
* console.log('Request timed out after 5 seconds');
|
|
45
|
+
* }
|
|
46
|
+
* });
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
8
49
|
const TIMEOUT_ERROR = "TimeoutError";
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
50
|
+
//#endregion
|
|
51
|
+
//#region src/fetch/defines.ts
|
|
52
|
+
/**
|
|
53
|
+
* Custom error class for HTTP error responses (non-2xx status codes).
|
|
54
|
+
*
|
|
55
|
+
* Thrown when `Response.ok` is `false`. Contains the HTTP status code
|
|
56
|
+
* for programmatic error handling.
|
|
57
|
+
*
|
|
58
|
+
* @since 1.0.0
|
|
59
|
+
* @example
|
|
60
|
+
* ```typescript
|
|
61
|
+
* import { fetchT, FetchError } from '@happy-ts/fetch-t';
|
|
62
|
+
*
|
|
63
|
+
* const result = await fetchT('https://api.example.com/not-found', {
|
|
64
|
+
* responseType: 'json',
|
|
65
|
+
* });
|
|
66
|
+
*
|
|
67
|
+
* result.inspectErr((err) => {
|
|
68
|
+
* if (err instanceof FetchError) {
|
|
69
|
+
* console.log('HTTP Status:', err.status); // e.g., 404
|
|
70
|
+
* console.log('Status Text:', err.message); // e.g., "Not Found"
|
|
71
|
+
*
|
|
72
|
+
* // Handle specific status codes
|
|
73
|
+
* switch (err.status) {
|
|
74
|
+
* case 401:
|
|
75
|
+
* console.log('Unauthorized - please login');
|
|
76
|
+
* break;
|
|
77
|
+
* case 404:
|
|
78
|
+
* console.log('Resource not found');
|
|
79
|
+
* break;
|
|
80
|
+
* case 500:
|
|
81
|
+
* console.log('Server error');
|
|
82
|
+
* break;
|
|
83
|
+
* }
|
|
84
|
+
* }
|
|
85
|
+
* });
|
|
86
|
+
* ```
|
|
87
|
+
*/
|
|
88
|
+
var FetchError = class extends Error {
|
|
89
|
+
/**
|
|
90
|
+
* The error name, always `'FetchError'`.
|
|
91
|
+
*/
|
|
92
|
+
name = "FetchError";
|
|
93
|
+
/**
|
|
94
|
+
* The HTTP status code of the response (e.g., 404, 500).
|
|
95
|
+
*/
|
|
96
|
+
status;
|
|
97
|
+
/**
|
|
98
|
+
* Creates a new FetchError instance.
|
|
99
|
+
*
|
|
100
|
+
* @param message - The status text from the HTTP response (e.g., "Not Found").
|
|
101
|
+
* @param status - The HTTP status code (e.g., 404).
|
|
102
|
+
*/
|
|
103
|
+
constructor(message, status) {
|
|
104
|
+
super(message);
|
|
105
|
+
this.status = status;
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
//#endregion
|
|
109
|
+
//#region src/fetch/polyfills.ts
|
|
110
|
+
/**
|
|
111
|
+
* @internal
|
|
112
|
+
* Polyfills for AbortSignal and related APIs.
|
|
113
|
+
*/
|
|
114
|
+
/**
|
|
115
|
+
* Creates an AbortSignal that aborts after the specified timeout.
|
|
116
|
+
* Uses native `AbortSignal.timeout` when available, otherwise falls back to a manual implementation.
|
|
117
|
+
*/
|
|
118
|
+
function signalTimeout(ms) {
|
|
119
|
+
if (typeof AbortSignal.timeout === "function") return AbortSignal.timeout(ms);
|
|
120
|
+
const controller = new AbortController();
|
|
121
|
+
const timeoutError = createTimeoutError();
|
|
122
|
+
const timerId = setTimeout(() => {
|
|
123
|
+
controller.abort(timeoutError);
|
|
124
|
+
}, ms);
|
|
125
|
+
if (typeof timerId?.unref === "function") timerId.unref();
|
|
126
|
+
return controller.signal;
|
|
29
127
|
}
|
|
30
|
-
|
|
128
|
+
/**
|
|
129
|
+
* Creates an AbortSignal that aborts when any of the provided signals abort.
|
|
130
|
+
* Uses native `AbortSignal.any` when available, otherwise falls back to a manual implementation.
|
|
131
|
+
*
|
|
132
|
+
* The polyfill properly cleans up event listeners on all signals when one fires,
|
|
133
|
+
* preventing memory leaks from long-lived signals.
|
|
134
|
+
*/
|
|
135
|
+
function signalAny(signals) {
|
|
136
|
+
if (typeof AbortSignal.any === "function") return AbortSignal.any(signals);
|
|
137
|
+
const controller = new AbortController();
|
|
138
|
+
for (const signal of signals) if (signal.aborted) {
|
|
139
|
+
controller.abort(signal.reason);
|
|
140
|
+
return controller.signal;
|
|
141
|
+
}
|
|
142
|
+
const onAbort = (event) => {
|
|
143
|
+
controller.abort(event.target.reason);
|
|
144
|
+
};
|
|
145
|
+
const cleanup = () => {
|
|
146
|
+
for (const signal of signals) signal.removeEventListener("abort", onAbort);
|
|
147
|
+
};
|
|
148
|
+
for (const signal of signals) signal.addEventListener("abort", onAbort);
|
|
149
|
+
controller.signal.addEventListener("abort", cleanup);
|
|
150
|
+
return controller.signal;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Creates a DOMException with TimeoutError name.
|
|
154
|
+
* Falls back to a plain Error in environments where DOMException constructor is unavailable.
|
|
155
|
+
*/
|
|
156
|
+
function createTimeoutError() {
|
|
157
|
+
const message = "The operation timed out.";
|
|
158
|
+
try {
|
|
159
|
+
return new DOMException(message, TIMEOUT_ERROR);
|
|
160
|
+
} catch {
|
|
161
|
+
const error = new Error(message);
|
|
162
|
+
error.name = TIMEOUT_ERROR;
|
|
163
|
+
return error;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
//#endregion
|
|
167
|
+
//#region src/fetch/fetch.ts
|
|
168
|
+
/**
|
|
169
|
+
* Enhanced fetch function that wraps the native Fetch API with additional capabilities.
|
|
170
|
+
*
|
|
171
|
+
* Features:
|
|
172
|
+
* - **Abortable requests**: Set `abortable: true` to get a `FetchTask` with `abort()` method.
|
|
173
|
+
* - **Type-safe responses**: Use `responseType` to automatically parse responses as text, JSON, ArrayBuffer, Blob, bytes, or stream.
|
|
174
|
+
* - **Timeout support**: Set `timeout` in milliseconds to auto-abort long-running requests.
|
|
175
|
+
* - **Progress tracking**: Use `onProgress` callback to track download progress (requires Content-Length header).
|
|
176
|
+
* - **Chunk streaming**: Use `onChunk` callback to receive raw data chunks as they arrive.
|
|
177
|
+
* - **Retry support**: Use `retry` to automatically retry failed requests with configurable delay and conditions.
|
|
178
|
+
* - **Result type error handling**: Returns `Result<T, Error>` instead of throwing exceptions for runtime errors.
|
|
179
|
+
*
|
|
180
|
+
* **Note**: Invalid parameters throw synchronously (fail-fast) rather than returning rejected Promises.
|
|
181
|
+
* This differs from native `fetch` behavior and helps catch programming errors during development.
|
|
182
|
+
*
|
|
183
|
+
* @typeParam T - The expected type of the response data.
|
|
184
|
+
* @param url - The resource to fetch. Can be a URL object or a string representing a URL.
|
|
185
|
+
* @param init - Additional options for the fetch operation, extending standard `RequestInit` with custom properties.
|
|
186
|
+
* @returns A `FetchTask<T>` if `abortable: true`, otherwise a `FetchResult<T>` (which is `AsyncIOResult<T>`).
|
|
187
|
+
* @throws {TypeError} If `url` is invalid or a relative URL in non-browser environment.
|
|
188
|
+
* @throws {TypeError} If `responseType` is not a valid response type.
|
|
189
|
+
* @throws {TypeError} If `timeout` is not a number.
|
|
190
|
+
* @throws {Error} If `timeout` is not greater than 0.
|
|
191
|
+
* @throws {TypeError} If `onProgress` or `onChunk` is provided but not a function.
|
|
192
|
+
* @throws {TypeError} If `retry.retries` is not an integer.
|
|
193
|
+
* @throws {Error} If `retry.retries` is negative.
|
|
194
|
+
* @throws {TypeError} If `retry.delay` is not a number or function.
|
|
195
|
+
* @throws {Error} If `retry.delay` is a negative number.
|
|
196
|
+
* @throws {TypeError} If `retry.when` is not an array or function.
|
|
197
|
+
* @throws {TypeError} If `retry.onRetry` is provided but not a function.
|
|
198
|
+
* @since 1.0.0
|
|
199
|
+
* @example
|
|
200
|
+
* // Basic GET request - returns Response object wrapped in Result
|
|
201
|
+
* const result = await fetchT('https://api.example.com/data');
|
|
202
|
+
* result
|
|
203
|
+
* .inspect((res) => console.log('Status:', res.status))
|
|
204
|
+
* .inspectErr((err) => console.error('Error:', err));
|
|
205
|
+
*
|
|
206
|
+
* @example
|
|
207
|
+
* // GET JSON with type safety
|
|
208
|
+
* interface User {
|
|
209
|
+
* id: number;
|
|
210
|
+
* name: string;
|
|
211
|
+
* }
|
|
212
|
+
* const result = await fetchT<User>('https://api.example.com/user/1', {
|
|
213
|
+
* responseType: 'json',
|
|
214
|
+
* });
|
|
215
|
+
* result.inspect((user) => console.log(user.name));
|
|
216
|
+
*
|
|
217
|
+
* @example
|
|
218
|
+
* // POST request with JSON body
|
|
219
|
+
* const result = await fetchT<User>('https://api.example.com/users', {
|
|
220
|
+
* method: 'POST',
|
|
221
|
+
* headers: { 'Content-Type': 'application/json' },
|
|
222
|
+
* body: JSON.stringify({ name: 'John' }),
|
|
223
|
+
* responseType: 'json',
|
|
224
|
+
* });
|
|
225
|
+
*
|
|
226
|
+
* @example
|
|
227
|
+
* // Abortable request with timeout
|
|
228
|
+
* const task = fetchT('https://api.example.com/data', {
|
|
229
|
+
* abortable: true,
|
|
230
|
+
* timeout: 5000, // 5 seconds
|
|
231
|
+
* });
|
|
232
|
+
*
|
|
233
|
+
* // Cancel the request if needed
|
|
234
|
+
* task.abort('User cancelled');
|
|
235
|
+
*
|
|
236
|
+
* // Check if aborted
|
|
237
|
+
* console.log('Aborted:', task.aborted);
|
|
238
|
+
*
|
|
239
|
+
* // Wait for result
|
|
240
|
+
* const result = await task.result;
|
|
241
|
+
*
|
|
242
|
+
* @example
|
|
243
|
+
* // Track download progress
|
|
244
|
+
* const result = await fetchT('https://example.com/large-file.zip', {
|
|
245
|
+
* responseType: 'blob',
|
|
246
|
+
* onProgress: (progressResult) => {
|
|
247
|
+
* progressResult
|
|
248
|
+
* .inspect(({ completedByteLength, totalByteLength }) => {
|
|
249
|
+
* const percent = ((completedByteLength / totalByteLength) * 100).toFixed(1);
|
|
250
|
+
* console.log(`Progress: ${percent}%`);
|
|
251
|
+
* })
|
|
252
|
+
* .inspectErr((err) => console.warn('Progress unavailable:', err.message));
|
|
253
|
+
* },
|
|
254
|
+
* });
|
|
255
|
+
*
|
|
256
|
+
* @example
|
|
257
|
+
* // Stream data chunks
|
|
258
|
+
* const chunks: Uint8Array[] = [];
|
|
259
|
+
* const result = await fetchT('https://example.com/stream', {
|
|
260
|
+
* onChunk: (chunk) => chunks.push(chunk),
|
|
261
|
+
* });
|
|
262
|
+
*
|
|
263
|
+
* @example
|
|
264
|
+
* // Retry with exponential backoff
|
|
265
|
+
* const result = await fetchT('https://api.example.com/data', {
|
|
266
|
+
* retry: {
|
|
267
|
+
* retries: 3,
|
|
268
|
+
* delay: (attempt) => Math.min(1000 * Math.pow(2, attempt - 1), 10000),
|
|
269
|
+
* when: [500, 502, 503, 504],
|
|
270
|
+
* onRetry: (error, attempt) => console.log(`Retry ${attempt}: ${error.message}`),
|
|
271
|
+
* },
|
|
272
|
+
* responseType: 'json',
|
|
273
|
+
* });
|
|
274
|
+
*/
|
|
31
275
|
function fetchT(url, init) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
const delayMs = getRetryDelay(attempt);
|
|
184
|
-
if (delayMs > 0) {
|
|
185
|
-
await delay(delayMs);
|
|
186
|
-
if (userController?.signal.aborted) {
|
|
187
|
-
return happyRusty.Err(userController.signal.reason);
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
try {
|
|
191
|
-
onRetry?.(lastError, attempt);
|
|
192
|
-
} catch {
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
const result2 = await doFetch();
|
|
196
|
-
if (result2.isOk()) {
|
|
197
|
-
return result2;
|
|
198
|
-
}
|
|
199
|
-
lastError = result2.unwrapErr();
|
|
200
|
-
attempt++;
|
|
201
|
-
} while (attempt <= retries && shouldRetry(lastError, attempt));
|
|
202
|
-
return happyRusty.Err(lastError);
|
|
203
|
-
};
|
|
204
|
-
const result = fetchWithRetry();
|
|
205
|
-
if (abortable && userController) {
|
|
206
|
-
return {
|
|
207
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
208
|
-
abort(reason) {
|
|
209
|
-
if (reason instanceof Error) {
|
|
210
|
-
userController.abort(reason);
|
|
211
|
-
} else if (reason != null) {
|
|
212
|
-
userController.abort(wrapAbortReason(reason));
|
|
213
|
-
} else {
|
|
214
|
-
userController.abort();
|
|
215
|
-
}
|
|
216
|
-
},
|
|
217
|
-
get aborted() {
|
|
218
|
-
return userController.signal.aborted;
|
|
219
|
-
},
|
|
220
|
-
get result() {
|
|
221
|
-
return result;
|
|
222
|
-
}
|
|
223
|
-
};
|
|
224
|
-
}
|
|
225
|
-
return result;
|
|
276
|
+
const parsedUrl = validateUrl(url);
|
|
277
|
+
const fetchInit = init ?? {};
|
|
278
|
+
const { retries, delay: retryDelay, when: retryWhen, onRetry } = validateOptions(fetchInit);
|
|
279
|
+
const { abortable = false, responseType, timeout, onProgress, onChunk, ...rest } = fetchInit;
|
|
280
|
+
const userSignal = rest.signal;
|
|
281
|
+
let userController;
|
|
282
|
+
if (abortable) userController = new AbortController();
|
|
283
|
+
/**
|
|
284
|
+
* Determines if the error should trigger a retry.
|
|
285
|
+
* By default, only network errors (not FetchError) trigger retries.
|
|
286
|
+
*/
|
|
287
|
+
const shouldRetry = (error, attempt) => {
|
|
288
|
+
if (error.name === "AbortError") return false;
|
|
289
|
+
if (!retryWhen) return !(error instanceof FetchError);
|
|
290
|
+
if (Array.isArray(retryWhen)) return error instanceof FetchError && retryWhen.includes(error.status);
|
|
291
|
+
return retryWhen(error, attempt);
|
|
292
|
+
};
|
|
293
|
+
/**
|
|
294
|
+
* Calculates the delay before the next retry attempt.
|
|
295
|
+
*/
|
|
296
|
+
const getRetryDelay = (attempt) => {
|
|
297
|
+
return typeof retryDelay === "function" ? retryDelay(attempt) : retryDelay;
|
|
298
|
+
};
|
|
299
|
+
/**
|
|
300
|
+
* Configures the abort signal for a fetch attempt.
|
|
301
|
+
*
|
|
302
|
+
* Combines multiple signals:
|
|
303
|
+
* - User's external signal (from init.signal)
|
|
304
|
+
* - Internal abort controller signal (for abortable requests)
|
|
305
|
+
* - Timeout signal (creates a new one each call for per-attempt timeout)
|
|
306
|
+
*
|
|
307
|
+
* Must be called before each fetch attempt to ensure fresh timeout signal on retries.
|
|
308
|
+
*/
|
|
309
|
+
const configureSignal = () => {
|
|
310
|
+
const signals = [];
|
|
311
|
+
if (userSignal) signals.push(userSignal);
|
|
312
|
+
if (userController) signals.push(userController.signal);
|
|
313
|
+
if (typeof timeout === "number") signals.push(signalTimeout(timeout));
|
|
314
|
+
if (signals.length > 0) rest.signal = signals.length === 1 ? signals[0] : signalAny(signals);
|
|
315
|
+
};
|
|
316
|
+
/**
|
|
317
|
+
* Performs a single fetch attempt with optional timeout.
|
|
318
|
+
*/
|
|
319
|
+
const doFetch = async () => {
|
|
320
|
+
configureSignal();
|
|
321
|
+
try {
|
|
322
|
+
const response = await fetch(parsedUrl, rest);
|
|
323
|
+
if (!response.ok) {
|
|
324
|
+
response.body?.cancel().catch(() => {});
|
|
325
|
+
return (0, happy_rusty.Err)(new FetchError(response.statusText, response.status));
|
|
326
|
+
}
|
|
327
|
+
return await processResponse(response);
|
|
328
|
+
} catch (err) {
|
|
329
|
+
return (0, happy_rusty.Err)(err instanceof Error ? err : wrapAbortReason(err));
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
/**
|
|
333
|
+
* Sets up progress tracking and chunk callbacks using a cloned response.
|
|
334
|
+
* The original response is returned unchanged for further processing.
|
|
335
|
+
*/
|
|
336
|
+
const setupProgressCallbacks = async (response) => {
|
|
337
|
+
let totalByteLength;
|
|
338
|
+
let completedByteLength = 0;
|
|
339
|
+
if (onProgress) {
|
|
340
|
+
const contentLength = response.headers.get("content-length");
|
|
341
|
+
if (contentLength == null) try {
|
|
342
|
+
onProgress((0, happy_rusty.Err)(/* @__PURE__ */ new Error("No content-length in response headers")));
|
|
343
|
+
} catch {}
|
|
344
|
+
else totalByteLength = Number.parseInt(contentLength, 10);
|
|
345
|
+
}
|
|
346
|
+
const body = response.clone().body;
|
|
347
|
+
try {
|
|
348
|
+
for await (const chunk of body) {
|
|
349
|
+
if (onChunk) try {
|
|
350
|
+
onChunk(chunk);
|
|
351
|
+
} catch {}
|
|
352
|
+
if (onProgress && totalByteLength != null) {
|
|
353
|
+
completedByteLength += chunk.byteLength;
|
|
354
|
+
try {
|
|
355
|
+
onProgress((0, happy_rusty.Ok)({
|
|
356
|
+
totalByteLength,
|
|
357
|
+
completedByteLength
|
|
358
|
+
}));
|
|
359
|
+
} catch {}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
} catch {}
|
|
363
|
+
};
|
|
364
|
+
/**
|
|
365
|
+
* Processes the response based on responseType and callbacks.
|
|
366
|
+
*/
|
|
367
|
+
const processResponse = async (response) => {
|
|
368
|
+
if (response.body && (onProgress || onChunk)) setupProgressCallbacks(response);
|
|
369
|
+
switch (responseType) {
|
|
370
|
+
case "json":
|
|
371
|
+
if (response.body == null) return (0, happy_rusty.Ok)(null);
|
|
372
|
+
try {
|
|
373
|
+
return (0, happy_rusty.Ok)(await response.json());
|
|
374
|
+
} catch {
|
|
375
|
+
return (0, happy_rusty.Err)(/* @__PURE__ */ new Error("Response is invalid json while responseType is json"));
|
|
376
|
+
}
|
|
377
|
+
case "text": return (0, happy_rusty.Ok)(await response.text());
|
|
378
|
+
case "bytes":
|
|
379
|
+
if (typeof response.bytes === "function") return (0, happy_rusty.Ok)(await response.bytes());
|
|
380
|
+
return (0, happy_rusty.Ok)(new Uint8Array(await response.arrayBuffer()));
|
|
381
|
+
case "arraybuffer": return (0, happy_rusty.Ok)(await response.arrayBuffer());
|
|
382
|
+
case "blob": return (0, happy_rusty.Ok)(await response.blob());
|
|
383
|
+
case "stream": return (0, happy_rusty.Ok)(response.body);
|
|
384
|
+
default: return (0, happy_rusty.Ok)(response);
|
|
385
|
+
}
|
|
386
|
+
};
|
|
387
|
+
/**
|
|
388
|
+
* Performs fetch with retry logic.
|
|
389
|
+
*/
|
|
390
|
+
const fetchWithRetry = async () => {
|
|
391
|
+
let lastError;
|
|
392
|
+
let attempt = 0;
|
|
393
|
+
do {
|
|
394
|
+
if (attempt > 0) {
|
|
395
|
+
if (userController?.signal.aborted) return (0, happy_rusty.Err)(userController.signal.reason);
|
|
396
|
+
const delayMs = getRetryDelay(attempt);
|
|
397
|
+
if (delayMs > 0) {
|
|
398
|
+
await delay(delayMs);
|
|
399
|
+
if (userController?.signal.aborted) return (0, happy_rusty.Err)(userController.signal.reason);
|
|
400
|
+
}
|
|
401
|
+
try {
|
|
402
|
+
onRetry?.(lastError, attempt);
|
|
403
|
+
} catch {}
|
|
404
|
+
}
|
|
405
|
+
const result = await doFetch();
|
|
406
|
+
if (result.isOk()) return result;
|
|
407
|
+
lastError = result.unwrapErr();
|
|
408
|
+
attempt++;
|
|
409
|
+
} while (attempt <= retries && shouldRetry(lastError, attempt));
|
|
410
|
+
return (0, happy_rusty.Err)(lastError);
|
|
411
|
+
};
|
|
412
|
+
const result = fetchWithRetry();
|
|
413
|
+
if (abortable && userController) return {
|
|
414
|
+
abort(reason) {
|
|
415
|
+
if (reason instanceof Error) userController.abort(reason);
|
|
416
|
+
else if (reason != null) userController.abort(wrapAbortReason(reason));
|
|
417
|
+
else userController.abort();
|
|
418
|
+
},
|
|
419
|
+
get aborted() {
|
|
420
|
+
return userController.signal.aborted;
|
|
421
|
+
},
|
|
422
|
+
get result() {
|
|
423
|
+
return result;
|
|
424
|
+
}
|
|
425
|
+
};
|
|
426
|
+
return result;
|
|
226
427
|
}
|
|
428
|
+
/**
|
|
429
|
+
* Delays execution for the specified number of milliseconds.
|
|
430
|
+
*/
|
|
227
431
|
function delay(ms) {
|
|
228
|
-
|
|
432
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
229
433
|
}
|
|
434
|
+
/**
|
|
435
|
+
* Wraps a non-Error abort reason into an Error with ABORT_ERROR name.
|
|
436
|
+
*/
|
|
230
437
|
function wrapAbortReason(reason) {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
438
|
+
const error = new Error(typeof reason === "string" ? reason : String(reason));
|
|
439
|
+
error.name = ABORT_ERROR;
|
|
440
|
+
error.cause = reason;
|
|
441
|
+
return error;
|
|
235
442
|
}
|
|
443
|
+
/**
|
|
444
|
+
* Validates fetch options and parses retry configuration.
|
|
445
|
+
*/
|
|
236
446
|
function validateOptions(init) {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
if (delay2 < 0) {
|
|
288
|
-
throw new Error(`Retry delay must be a non-negative number but received ${delay2}`);
|
|
289
|
-
}
|
|
290
|
-
} else {
|
|
291
|
-
if (typeof delay2 !== "function") {
|
|
292
|
-
throw new TypeError(`Retry delay must be a number or a function but received ${typeof delay2}`);
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
if (when != null) {
|
|
296
|
-
if (!Array.isArray(when) && typeof when !== "function") {
|
|
297
|
-
throw new TypeError(`Retry when condition must be an array of status codes or a function but received ${typeof when}`);
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
if (onRetry != null) {
|
|
301
|
-
if (typeof onRetry !== "function") {
|
|
302
|
-
throw new TypeError(`Retry onRetry callback must be a function but received ${typeof onRetry}`);
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
return { retries, delay: delay2, when, onRetry };
|
|
447
|
+
const { responseType, timeout, retry: retryOptions = 0, onProgress, onChunk } = init;
|
|
448
|
+
if (responseType != null) {
|
|
449
|
+
const validTypes = [
|
|
450
|
+
"text",
|
|
451
|
+
"arraybuffer",
|
|
452
|
+
"blob",
|
|
453
|
+
"json",
|
|
454
|
+
"bytes",
|
|
455
|
+
"stream"
|
|
456
|
+
];
|
|
457
|
+
if (!validTypes.includes(responseType)) throw new TypeError(`responseType must be one of ${validTypes.join(", ")} but received ${responseType}`);
|
|
458
|
+
}
|
|
459
|
+
if (timeout != null) {
|
|
460
|
+
if (typeof timeout !== "number") throw new TypeError(`timeout must be a number but received ${typeof timeout}`);
|
|
461
|
+
if (timeout <= 0) throw new Error(`timeout must be a number greater than 0 but received ${timeout}`);
|
|
462
|
+
}
|
|
463
|
+
if (onProgress != null) {
|
|
464
|
+
if (typeof onProgress !== "function") throw new TypeError(`onProgress callback must be a function but received ${typeof onProgress}`);
|
|
465
|
+
}
|
|
466
|
+
if (onChunk != null) {
|
|
467
|
+
if (typeof onChunk !== "function") throw new TypeError(`onChunk callback must be a function but received ${typeof onChunk}`);
|
|
468
|
+
}
|
|
469
|
+
let retries = 0;
|
|
470
|
+
let delay = 0;
|
|
471
|
+
let when;
|
|
472
|
+
let onRetry;
|
|
473
|
+
if (typeof retryOptions === "number") retries = retryOptions;
|
|
474
|
+
else if (retryOptions && typeof retryOptions === "object") {
|
|
475
|
+
retries = retryOptions.retries ?? 0;
|
|
476
|
+
delay = retryOptions.delay ?? 0;
|
|
477
|
+
when = retryOptions.when;
|
|
478
|
+
onRetry = retryOptions.onRetry;
|
|
479
|
+
}
|
|
480
|
+
if (!Number.isInteger(retries)) throw new TypeError(`Retry count must be an integer but received ${retries}`);
|
|
481
|
+
if (retries < 0) throw new Error(`Retry count must be non-negative but received ${retries}`);
|
|
482
|
+
if (typeof delay === "number") {
|
|
483
|
+
if (delay < 0) throw new Error(`Retry delay must be a non-negative number but received ${delay}`);
|
|
484
|
+
} else if (typeof delay !== "function") throw new TypeError(`Retry delay must be a number or a function but received ${typeof delay}`);
|
|
485
|
+
if (when != null) {
|
|
486
|
+
if (!Array.isArray(when) && typeof when !== "function") throw new TypeError(`Retry when condition must be an array of status codes or a function but received ${typeof when}`);
|
|
487
|
+
}
|
|
488
|
+
if (onRetry != null) {
|
|
489
|
+
if (typeof onRetry !== "function") throw new TypeError(`Retry onRetry callback must be a function but received ${typeof onRetry}`);
|
|
490
|
+
}
|
|
491
|
+
return {
|
|
492
|
+
retries,
|
|
493
|
+
delay,
|
|
494
|
+
when,
|
|
495
|
+
onRetry
|
|
496
|
+
};
|
|
306
497
|
}
|
|
498
|
+
/**
|
|
499
|
+
* Validates and parses a URL string or URL object.
|
|
500
|
+
* In browser environments, relative URLs are resolved against `location.href`.
|
|
501
|
+
* In non-browser environments (Node/Deno/Bun), only absolute URLs are valid.
|
|
502
|
+
*/
|
|
307
503
|
function validateUrl(url) {
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
} catch {
|
|
315
|
-
throw new TypeError(`Invalid URL: ${url}`);
|
|
316
|
-
}
|
|
504
|
+
if (url instanceof URL) return url;
|
|
505
|
+
try {
|
|
506
|
+
return new URL(url, typeof location !== "undefined" ? location.href : void 0);
|
|
507
|
+
} catch {
|
|
508
|
+
throw new TypeError(`Invalid URL: ${url}`);
|
|
509
|
+
}
|
|
317
510
|
}
|
|
318
|
-
|
|
511
|
+
//#endregion
|
|
319
512
|
exports.ABORT_ERROR = ABORT_ERROR;
|
|
320
513
|
exports.FetchError = FetchError;
|
|
321
514
|
exports.TIMEOUT_ERROR = TIMEOUT_ERROR;
|
|
322
515
|
exports.fetchT = fetchT;
|
|
323
|
-
|
|
516
|
+
|
|
517
|
+
//# sourceMappingURL=main.cjs.map
|