@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/dist/main.cjs CHANGED
@@ -1,323 +1,517 @@
1
- 'use strict';
2
-
3
- Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
4
-
5
- const happyRusty = require('happy-rusty');
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
- class FetchError extends Error {
11
- /**
12
- * The error name, always `'FetchError'`.
13
- */
14
- name = "FetchError";
15
- /**
16
- * The HTTP status code of the response (e.g., 404, 500).
17
- */
18
- status;
19
- /**
20
- * Creates a new FetchError instance.
21
- *
22
- * @param message - The status text from the HTTP response (e.g., "Not Found").
23
- * @param status - The HTTP status code (e.g., 404).
24
- */
25
- constructor(message, status) {
26
- super(message);
27
- this.status = status;
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
- const parsedUrl = validateUrl(url);
33
- const fetchInit = init ?? {};
34
- const {
35
- retries,
36
- delay: retryDelay,
37
- when: retryWhen,
38
- onRetry
39
- } = validateOptions(fetchInit);
40
- const {
41
- // default not abortable
42
- abortable = false,
43
- responseType,
44
- timeout,
45
- onProgress,
46
- onChunk,
47
- ...rest
48
- } = fetchInit;
49
- const userSignal = rest.signal;
50
- let userController;
51
- if (abortable) {
52
- userController = new AbortController();
53
- }
54
- const shouldRetry = (error, attempt) => {
55
- if (error.name === ABORT_ERROR) {
56
- return false;
57
- }
58
- if (!retryWhen) {
59
- return !(error instanceof FetchError);
60
- }
61
- if (Array.isArray(retryWhen)) {
62
- return error instanceof FetchError && retryWhen.includes(error.status);
63
- }
64
- return retryWhen(error, attempt);
65
- };
66
- const getRetryDelay = (attempt) => {
67
- return typeof retryDelay === "function" ? retryDelay(attempt) : retryDelay;
68
- };
69
- const configureSignal = () => {
70
- const signals = [];
71
- if (userSignal) {
72
- signals.push(userSignal);
73
- }
74
- if (userController) {
75
- signals.push(userController.signal);
76
- }
77
- if (typeof timeout === "number") {
78
- signals.push(AbortSignal.timeout(timeout));
79
- }
80
- if (signals.length > 0) {
81
- rest.signal = signals.length === 1 ? signals[0] : AbortSignal.any(signals);
82
- }
83
- };
84
- const doFetch = async () => {
85
- configureSignal();
86
- try {
87
- const response = await fetch(parsedUrl, rest);
88
- if (!response.ok) {
89
- response.body?.cancel().catch(() => {
90
- });
91
- return happyRusty.Err(new FetchError(response.statusText, response.status));
92
- }
93
- return await processResponse(response);
94
- } catch (err) {
95
- return happyRusty.Err(
96
- err instanceof Error ? err : wrapAbortReason(err)
97
- );
98
- }
99
- };
100
- const setupProgressCallbacks = async (response) => {
101
- let totalByteLength;
102
- let completedByteLength = 0;
103
- if (onProgress) {
104
- const contentLength = response.headers.get("content-length");
105
- if (contentLength == null) {
106
- try {
107
- onProgress(happyRusty.Err(new Error("No content-length in response headers")));
108
- } catch {
109
- }
110
- } else {
111
- totalByteLength = Number.parseInt(contentLength, 10);
112
- }
113
- }
114
- const body = response.clone().body;
115
- try {
116
- for await (const chunk of body) {
117
- if (onChunk) {
118
- try {
119
- onChunk(chunk);
120
- } catch {
121
- }
122
- }
123
- if (onProgress && totalByteLength != null) {
124
- completedByteLength += chunk.byteLength;
125
- try {
126
- onProgress(happyRusty.Ok({
127
- totalByteLength,
128
- completedByteLength
129
- }));
130
- } catch {
131
- }
132
- }
133
- }
134
- } catch {
135
- }
136
- };
137
- const processResponse = async (response) => {
138
- if (response.body && (onProgress || onChunk)) {
139
- setupProgressCallbacks(response);
140
- }
141
- switch (responseType) {
142
- case "json": {
143
- if (response.body == null) {
144
- return happyRusty.Ok(null);
145
- }
146
- try {
147
- return happyRusty.Ok(await response.json());
148
- } catch {
149
- return happyRusty.Err(new Error("Response is invalid json while responseType is json"));
150
- }
151
- }
152
- case "text": {
153
- return happyRusty.Ok(await response.text());
154
- }
155
- case "bytes": {
156
- if (typeof response.bytes === "function") {
157
- return happyRusty.Ok(await response.bytes());
158
- }
159
- return happyRusty.Ok(new Uint8Array(await response.arrayBuffer()));
160
- }
161
- case "arraybuffer": {
162
- return happyRusty.Ok(await response.arrayBuffer());
163
- }
164
- case "blob": {
165
- return happyRusty.Ok(await response.blob());
166
- }
167
- case "stream": {
168
- return happyRusty.Ok(response.body);
169
- }
170
- default: {
171
- return happyRusty.Ok(response);
172
- }
173
- }
174
- };
175
- const fetchWithRetry = async () => {
176
- let lastError;
177
- let attempt = 0;
178
- do {
179
- if (attempt > 0) {
180
- if (userController?.signal.aborted) {
181
- return happyRusty.Err(userController.signal.reason);
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
- return new Promise((resolve) => setTimeout(resolve, ms));
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
- const error = new Error(typeof reason === "string" ? reason : String(reason));
232
- error.name = ABORT_ERROR;
233
- error.cause = reason;
234
- return error;
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
- const {
238
- responseType,
239
- timeout,
240
- retry: retryOptions = 0,
241
- onProgress,
242
- onChunk
243
- } = init;
244
- if (responseType != null) {
245
- const validTypes = ["text", "arraybuffer", "blob", "json", "bytes", "stream"];
246
- if (!validTypes.includes(responseType)) {
247
- throw new TypeError(`responseType must be one of ${validTypes.join(", ")} but received ${responseType}`);
248
- }
249
- }
250
- if (timeout != null) {
251
- if (typeof timeout !== "number") {
252
- throw new TypeError(`timeout must be a number but received ${typeof timeout}`);
253
- }
254
- if (timeout <= 0) {
255
- throw new Error(`timeout must be a number greater than 0 but received ${timeout}`);
256
- }
257
- }
258
- if (onProgress != null) {
259
- if (typeof onProgress !== "function") {
260
- throw new TypeError(`onProgress callback must be a function but received ${typeof onProgress}`);
261
- }
262
- }
263
- if (onChunk != null) {
264
- if (typeof onChunk !== "function") {
265
- throw new TypeError(`onChunk callback must be a function but received ${typeof onChunk}`);
266
- }
267
- }
268
- let retries = 0;
269
- let delay2 = 0;
270
- let when;
271
- let onRetry;
272
- if (typeof retryOptions === "number") {
273
- retries = retryOptions;
274
- } else if (retryOptions && typeof retryOptions === "object") {
275
- retries = retryOptions.retries ?? 0;
276
- delay2 = retryOptions.delay ?? 0;
277
- when = retryOptions.when;
278
- onRetry = retryOptions.onRetry;
279
- }
280
- if (!Number.isInteger(retries)) {
281
- throw new TypeError(`Retry count must be an integer but received ${retries}`);
282
- }
283
- if (retries < 0) {
284
- throw new Error(`Retry count must be non-negative but received ${retries}`);
285
- }
286
- if (typeof delay2 === "number") {
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
- if (url instanceof URL) {
309
- return url;
310
- }
311
- try {
312
- const base = typeof location !== "undefined" ? location.href : void 0;
313
- return new URL(url, base);
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
- //# sourceMappingURL=main.cjs.map
516
+
517
+ //# sourceMappingURL=main.cjs.map