@superutils/fetch 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Toufiqur Rahaman Chowdhury
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,133 @@
1
+ # @superutils/fetch
2
+
3
+ A lightweight `fetch` wrapper for browsers and Node.js, designed to simplify data fetching and reduce boilerplate.
4
+
5
+ This package enhances the native `fetch` API by providing a streamlined interface and integrating practical & useful features from `@superutils/promise`. It offers built-in support for automatic retries, request timeouts, interceptors, and effortless request cancellation, making complex asynchronous flows simple and manageable.
6
+
7
+ ## Table of Contents
8
+
9
+ - Features
10
+ - Installation
11
+ - Usage
12
+ - `fetch(url, options)`
13
+ - `fetchDeferred(deferOptions, url, fetchOptions)`
14
+
15
+ ## Features
16
+
17
+ - **Simplified API**: Automatically parses JSON responses, eliminating the need for `.then(res => res.json())`.
18
+ - **Built-in Retries**: Automatic request retries with configurable exponential or fixed backoff strategies.
19
+ - **Request Timeouts**: Easily specify a timeout for any request to prevent it from hanging indefinitely.
20
+ - **Cancellable & Debounced Requests**: The `fetchDeferred` utility provides debouncing and throttling capabilities, automatically cancelling stale or intermediate requests. This is ideal for features like live search inputs.
21
+ - **Interceptors**: Hook into the request/response lifecycle to globally modify requests, handle responses, or manage errors.
22
+ - **Strongly Typed**: Written in TypeScript for excellent autocompletion and type safety.
23
+ - **Isomorphic**: Works seamlessly in both Node.js and browser environments.
24
+
25
+ ## Installation
26
+
27
+ ```bash
28
+ npm install @superutils/fetch
29
+ ```
30
+
31
+ ## Usage
32
+
33
+ <div id="fetch"></div>
34
+
35
+ ### `fetch(url, options)`
36
+
37
+ Make a simple GET request. No need for `response.json()` or `result.data.theActualData` drilling.
38
+
39
+ ```typescript
40
+ import { fetch } from '@superutils/fetch'
41
+
42
+ const theActualData = await fetch('https://dummyjson.com/products/1')
43
+ console.log(theActualData)
44
+ ```
45
+
46
+ <div id="fetchDeferred"></div>
47
+
48
+ ### `fetchDeferred(deferOptions, url, fetchOptions)`
49
+
50
+ A practical utility that combines `PromisE.deferred()` from the `@superutils/promise` package with `fetch()`. It's perfect for implementing cancellable, debounced, or throttled search inputs.
51
+
52
+ ```typescript
53
+ import { fetchDeferred, ResolveIgnored } from '@superutils/fetch'
54
+
55
+ // Create a debounced search function with a 300ms delay.
56
+ const searchProducts = fetchDeferred({
57
+ delayMs: 300, // Debounce delay
58
+ resolveIgnored: ResolveIgnored.WITH_UNDEFINED, // Ignored (aborted) promises will resolve with `undefined`
59
+ })
60
+
61
+ // User types 'iphone'
62
+ searchProducts('https://dummyjson.com/products/search?q=iphone').then(
63
+ result => {
64
+ console.log('Result for "iphone":', result)
65
+ },
66
+ )
67
+
68
+ // Before 300ms has passed, the user continues typing 'iphone 9'
69
+ setTimeout(() => {
70
+ searchProducts('https://dummyjson.com/products/search?q=iphone 9').then(
71
+ result => {
72
+ console.log('Result for "iphone 9":', result)
73
+ },
74
+ )
75
+ }, 200)
76
+ // Outcome:
77
+ // The first request for "iphone" is aborted.
78
+ // The first promise resolves with `undefined`.
79
+ // The second request for "iphone 9" is executed after the 300ms debounce delay.
80
+ ```
81
+
82
+ **Behavior with different `deferOptions` in the example above:**
83
+
84
+ - **`throttle: true`**: Switches from debounce to throttle mode. The first request for "iphone" would
85
+ execute immediately. The second request for "iphone 9", made within the 300ms throttle window, would be ignored.
86
+ - **`delayMs: 0`**: Disables debouncing and throttling, enabling sequential/queue mode. Both requests ("iphone"
87
+ and "iphone 9") would execute, but one after the other, never simultaneously.
88
+ - **`resolveIgnored`**: Controls how the promise for an aborted request (like the first "iphone" call) resolves.
89
+ 1. `ResolveIgnored.WITH_UNDEFINED` (used in the example): The promise for the aborted "iphone"
90
+ request resolves with `undefined`.
91
+ 2. `ResolveIgnored.WITH_LAST`: The promise for the aborted "iphone" request waits and resolves with the result
92
+ of the final "iphone 9" request. Both promises resolve to the same value.
93
+ 3. `ResolveIgnored.NEVER`: The promise for the aborted "iphone" request is neither resolved nor rejected.
94
+ It will remain pending indefinitely.
95
+ 4. `ResolveIgnored.WITH_ERROR`: The promise for the aborted "iphone" request is rejected with a `FetchError`.
96
+
97
+ #### Using defaults to reduce redundancy
98
+
99
+ ```typescript
100
+ import { fetchDeferred, ResolveIgnored } from '@superutils/fetch'
101
+
102
+ // Create a throttled function to fetch a random quote.
103
+ // The URL and a 3-second timeout are set as defaults, creating a reusable client.
104
+ const getRandomQuote = fetchDeferred(
105
+ {
106
+ delayMs: 300, // Throttle window
107
+ throttle: true,
108
+ // Ignored calls will resolve with the result of the last successful call.
109
+ resolveIgnored: ResolveIgnored.WITH_LAST,
110
+ },
111
+ 'https://dummyjson.com/quotes/random', // Default URL
112
+ { timeout: 3000 }, // Default fetch options
113
+ )
114
+
115
+ // Call the function multiple times in quick succession.
116
+ getRandomQuote().then(quote => console.log('Call 1 resolved:', quote.id))
117
+ getRandomQuote().then(quote => console.log('Call 2 resolved:', quote.id))
118
+ getRandomQuote().then(quote => console.log('Call 3 resolved:', quote.id))
119
+
120
+ // Outcome:
121
+ // Due to throttling, only one network request is made.
122
+ // Because `resolveIgnored` is `WITH_LAST`, all three promises resolve with the same quote.
123
+ // The promises for the two ignored calls resolve as soon as the first successful call resolves.
124
+ // Console output will show the same quote ID for all three calls.
125
+ ```
126
+
127
+ <div id="post"></div>
128
+
129
+ ### `post(url, options)`
130
+
131
+ <div id="postDeferred"></div>
132
+
133
+ ### `postDeferred(deferOptions, url, postOptions)`
@@ -0,0 +1,520 @@
1
+ import * as _superutils_promise from '@superutils/promise';
2
+ import { RetryOptions, Config as Config$1, IPromisE, DeferredOptions } from '@superutils/promise';
3
+ export { DeferredOptions, ResolveError, ResolveIgnored } from '@superutils/promise';
4
+ import { ValueOrPromise } from '@superutils/core';
5
+
6
+ type FetchArgs = [url: string | URL, options?: FetchOptions];
7
+ type FetchArgsInterceptor = [
8
+ url: string | URL,
9
+ options: FetchOptionsInterceptor
10
+ ];
11
+ declare enum FetchAs {
12
+ arrayBuffer = "arrayBuffer",
13
+ blob = "blob",
14
+ bytes = "bytes",
15
+ formData = "formData",
16
+ json = "json",
17
+ response = "response",
18
+ text = "text"
19
+ }
20
+ type FetchConf = {
21
+ /**
22
+ * Specify how the parse the result. To get raw response use {@link FetchAs.response}.
23
+ * Default: 'json'
24
+ */
25
+ as?: FetchAs;
26
+ abortCtrl?: AbortController;
27
+ errMsgs?: FetchErrMsgs;
28
+ interceptors?: FetchInterceptors;
29
+ /** Request timeout in milliseconds. */
30
+ timeout?: number;
31
+ };
32
+ /** Default args */
33
+ type FetchDeferredArgs = [
34
+ url?: string | URL,
35
+ options?: Omit<FetchOptions, 'abortCtrl'>
36
+ ];
37
+ type FetchErrMsgs = {
38
+ invalidUrl?: string;
39
+ parseFailed?: string;
40
+ reqTimedout?: string;
41
+ requestFailed?: string;
42
+ };
43
+ /** Custom error message for fetch requests with more detailed info about the request URL, fetch options and response */
44
+ declare class FetchError extends Error {
45
+ options?: FetchOptions;
46
+ response?: Response;
47
+ url: string | URL;
48
+ constructor(message: string, options: {
49
+ cause?: unknown;
50
+ options: FetchOptions;
51
+ response?: Response;
52
+ url: string | URL;
53
+ });
54
+ }
55
+ /**
56
+ * Fetch error interceptor to be invoked whenever an exception occurs.
57
+ * This interceptor can also be used as the error transformer by returning {@link FetchError}.
58
+ *
59
+ * @param {FetchError} fetchError custom error that also contain URL, options & response
60
+ *
61
+ * @returns returning undefined or not returning anything will not override the error
62
+ *
63
+ * @example intercept fetch errors to log errors
64
+ * ```typescript
65
+ * import PromisE from '@superutils/promise'
66
+ *
67
+ * // not returning anything or returning undefined will avoid transforming the error.
68
+ * const logError = fetchErr => console.log(fetchErr)
69
+ * const result = await PromisE.fetch('https://my.domain.com/api/that/fails', {
70
+ * interceptors: {
71
+ * error: [logError]
72
+ * }
73
+ * })
74
+ * ```
75
+ *
76
+ * @example intercept & transform fetch errors
77
+ * ```typescript
78
+ * import PromisE from '@superutils/promise'
79
+ *
80
+ * // Interceptors can be async functions or just return a promise that resolves to the error.
81
+ * // If the execution of the interceptor fails or promise rejects, it will be ignored.
82
+ * // To transform the error it must directly return an error or a Promise that `resolves` with an error.
83
+ * const transformError = async fetchErr => {
84
+ * fetchErr.message = 'Custom errormessage'
85
+ * return Promise.resolve(fetchErr)
86
+ * }
87
+ * const result = await PromisE.fetch('https://my.domain.com/api/that/fails', {
88
+ * interceptors: {
89
+ * error: [transformError]
90
+ * }
91
+ * })
92
+ * ```
93
+ */
94
+ type FetchInterceptorError = Interceptor<FetchError, []>;
95
+ /**
96
+ *
97
+ * Fetch request interceptor to be invoked before making a fetch request.
98
+ * This interceptor can also be used as a transformer:
99
+ * 1. by returning an API URL (string/URL)
100
+ * 2. by modifying the properties of the options object to be used before making the fetch request
101
+ *
102
+ * @example intercept and transform fetch request
103
+ * ```typescript
104
+ * import PromisE from '@superutils/promise'
105
+ *
106
+ * // update API version number
107
+ * const apiV1ToV2 = url => `${url}`.replace('api/v1', 'api/v2')
108
+ * const includeAuthToken = (url, options) => {
109
+ * options.headers.set('x-auth-token', 'my-auth-token')
110
+ * }
111
+ * const data = await PromisE.fetch('https://my.domain.com/api', {
112
+ * interceptors: {
113
+ * result: [apiV1ToV2, includeAuthToken]
114
+ * }
115
+ * })
116
+ * ```
117
+ */
118
+ type FetchInterceptorRequest = Interceptor<FetchArgs[0], FetchArgsInterceptor>;
119
+ /**
120
+ * Fetch response interceptor to be invoked before making a fetch request.
121
+ *
122
+ * This interceptor can also be used as a transformer by return a different/modified {@link Response}.
123
+ *
124
+ * @example intercept and transform response:
125
+ * ```typescript
126
+ * import PromisE from '@superutils/promise'
127
+ *
128
+ * // After successful login, retrieve user balance.
129
+ * // This is probably better suited as a result transformer but play along as this is
130
+ * // just a hypothetical scenario ;)
131
+ * const includeBalance = async response => {
132
+ * const balance = await PromisE.fetch('https://my.domain.com/api/user/12325345/balance')
133
+ * const user = await response.json()
134
+ * user.balance = balance
135
+ * return new Response(JSON.stringify(user))
136
+ * }
137
+ * const user = await PromisE.fetch('https://my.domain.com/api/login', {
138
+ * interceptors: {
139
+ * response: [includeBalance]
140
+ * }
141
+ * })
142
+ * ```
143
+ */
144
+ type FetchInterceptorResponse = Interceptor<Response, FetchArgsInterceptor>;
145
+ /**
146
+ *
147
+ * Fetch result interceptor to be invoked before returning parsed fetch result.
148
+ *
149
+ * Result interceptors are executed ONLY when a result is successfully parsed (as ArrayBuffer, Blob, JSON, Text...).
150
+ * Result interceptors WILL NOT be executed if:
151
+ * - return type is set to `Response` by using {@link FetchAs.response} in the {@link FetchOptions.as}
152
+ * - exceptions is thrown even before attempting to parse
153
+ * - parse fails
154
+ *
155
+ * This interceptor can also be used as a transformer by returns a different/modified result.
156
+ *
157
+ *
158
+ * @example intercept and transform fetch result
159
+ * ```typescript
160
+ * import PromisE from '@superutils/promise'
161
+ *
162
+ * // first transform result by extracting result.data
163
+ * const extractData = result => result?.data ?? result
164
+ * // then check convert hexadecimal number to BigInt
165
+ * const hexToBigInt = data => {
166
+ * if (data.hasOwnProperty('balance') && `${data.balance}`.startsWith('0x')) {
167
+ * data.balance = BigInt(data.balance)
168
+ * }
169
+ * return data
170
+ * }
171
+ * // then log balance (no transformation)
172
+ * const logBalance = data => {
173
+ * data?.hasOwnProperty('balance') && console.log(data.balance)
174
+ * }
175
+ * const data = await PromisE.fetch('https://my.domain.com/api', {
176
+ * interceptors: {
177
+ * result: [
178
+ * extractData,
179
+ * hexToBigInt,
180
+ * logBalance
181
+ * ]
182
+ * }
183
+ * })
184
+ * ```
185
+ */
186
+ type FetchInterceptorResult = Interceptor<unknown, FetchArgsInterceptor>;
187
+ /**
188
+ * All valid interceptors for fetch requests are:
189
+ * ---
190
+ * 1. error,
191
+ * 2. request
192
+ * 3. response
193
+ * 4. result.
194
+ *
195
+ * An interceptor can be any of the following:
196
+ * ---
197
+ * 1. synchronous function
198
+ * 2. synchronous function that returns a promise (or sometimes returns a promise)
199
+ * 3. asynchronous functions
200
+ *
201
+ * An interceptor can return:
202
+ * ---
203
+ * 1. undefined (void/no return): plain interceptor that does other stuff but does not transform
204
+ * 2. value: act as a transformer. Returned value depends on the type of interceptor.
205
+ * 3. promise resolves with (1) value or (2) undefined
206
+ *
207
+ * PS:
208
+ * ---
209
+ * 1. Any exception thrown by interceptors will gracefully ignored.
210
+ * 2. Interceptors will be executed in the sequence they're given.
211
+ * 3. Execution priority: global interceprors will always be executed before local interceptors.
212
+ *
213
+ *
214
+ *
215
+ * More info & examples:
216
+ * ---
217
+ * See the following for more details and examples:
218
+ *
219
+ * - `error`: {@link FetchInterceptorError}
220
+ * - `request`: {@link FetchInterceptorRequest}
221
+ * - `response`: {@link FetchInterceptorResponse}
222
+ * - `result`: {@link FetchInterceptorResult}
223
+ */
224
+ type FetchInterceptors = {
225
+ error?: FetchInterceptorError[];
226
+ request?: FetchInterceptorRequest[];
227
+ response?: FetchInterceptorResponse[];
228
+ result?: FetchInterceptorResult[];
229
+ };
230
+ /**
231
+ * Fetch request options
232
+ */
233
+ type FetchOptions = RequestInit & FetchConf & FetchRetryOptions;
234
+ /**
235
+ * Fetch options available to interceptors
236
+ */
237
+ type FetchOptionsInterceptor = Omit<FetchOptions, 'as' | 'errMsgs' | 'interceptors' | 'headers' | keyof FetchRetryOptions> & {
238
+ as: FetchAs;
239
+ errMsgs: Required<FetchErrMsgs>;
240
+ headers: Headers;
241
+ interceptors: Required<FetchInterceptors>;
242
+ } & Required<FetchRetryOptions>;
243
+ /**
244
+ * Result types for specific parsers ("as": FetchAs)
245
+ */
246
+ interface FetchResult<T> {
247
+ arrayBuffer: ArrayBuffer;
248
+ blob: Blob;
249
+ bytes: Uint8Array<ArrayBuffer>;
250
+ formData: FormData;
251
+ json: T;
252
+ text: string;
253
+ response: Response;
254
+ }
255
+ /**
256
+ * Fetch retry options
257
+ */
258
+ type FetchRetryOptions = Partial<RetryOptions> & {
259
+ /**
260
+ * Maximum number of retries.
261
+ *
262
+ * The total number of attempts will be `retry + 1`.
263
+ *
264
+ * Default: `0`
265
+ */
266
+ retry?: number;
267
+ };
268
+ /**
269
+ * Generic fetch interceptor type
270
+ */
271
+ type Interceptor<T, TArgs extends unknown[], TArgsCb extends unknown[] = [value: T, ...TArgs]> = (...args: TArgsCb) => ValueOrPromise<void> | ValueOrPromise<T>;
272
+ type PostBody = Record<string, unknown> | BodyInit | null;
273
+ type PostArgs = [
274
+ url: string | URL,
275
+ data?: PostBody,
276
+ options?: Omit<FetchOptions, 'method'> & {
277
+ /** Default: `'post'` */
278
+ method?: 'post' | 'put' | 'patch' | 'delete';
279
+ }
280
+ ];
281
+
282
+ type Config = Config$1 & {
283
+ fetchOptions: FetchOptionsInterceptor;
284
+ };
285
+ declare const config: Config;
286
+
287
+ /**
288
+ * A `fetch()` replacement that simplifies data fetching with automatic JSON parsing, request timeouts, retries,
289
+ * and powerful interceptors. It also includes deferred and throttled request capabilities for complex asynchronous
290
+ * control flows.
291
+ *
292
+ * Will reject promise if response status code is not 2xx (200 <= status < 300).
293
+ *
294
+ * @param url
295
+ * @param options (optional) all built-in `fetch()` options such as "method", "headers" and the additionals below.
296
+ * @param options.abortCtrl (optional) if not provided `AbortController` will be instantiated when `timeout` used.
297
+ * @param options.headers (optional) request headers. Default: `{ 'content-type' : 'application/json'}`
298
+ * @param options.interceptors (optional) request interceptor callbacks. See {@link FetchInterceptors} for details.
299
+ * @param options.method (optional) Default: `"get"`
300
+ * @param options.timeout (optional) duration in milliseconds to abort the request if it takes longer.
301
+ * @param options.parse (optional) specify how to parse the result.
302
+ * Default: {@link FetchAs.json}
303
+ * For raw `Response` use {@link FetchAs.response}
304
+ *
305
+ * @example Make a simple HTTP requests
306
+ * ```typescript
307
+ * import { fetch } from '@superutils/fetch'
308
+ *
309
+ * // no need for `response.json()` or `result.data.theActualData` drilling
310
+ * fetch('https://dummyjson.com/products/1').then(theActualData => console.log(theActualData))
311
+ * ```
312
+ */
313
+ declare function fetch$1<TJSON, TOptions extends FetchOptions = FetchOptions, TReturn = TOptions['as'] extends FetchAs ? FetchResult<TJSON>[TOptions['as']] : TJSON>(url: string | URL, options?: TOptions): IPromisE<TReturn>;
314
+
315
+ /**
316
+ * Creates a deferred/throttled version of {@link fetch}, powered by {@link PromisE.deferred}.
317
+ * This is ideal for scenarios requiring advanced control over HTTP requests, such as debouncing search inputs,
318
+ * throttling API calls, or ensuring sequential request execution.
319
+ *
320
+ * It leverages the robust capabilities of the underlying {@link fetch} function, which includes features like request timeouts and manual abortion.
321
+ * `fetchDeferred` uses this to automatically abort pending requests when a new one is initiated, preventing race conditions and redundant network traffic.
322
+ *
323
+ * @param deferOptions Configuration for the deferred execution behavior (e.g., `delayMs`, `throttle`).
324
+ * See {@link DeferredOptions} for details.
325
+ * @param defaultFetchArgs (optional) Default `url` and `fetchOptions` to be used for every call made by the
326
+ * returned function. This is useful for creating a reusable client for a specific endpoint.
327
+ *
328
+ *
329
+ * @example Debounce/Throttle requests for an auto-complete search input
330
+ * ```typescript
331
+ * import { fetchDeferred, ResolveIgnored } from '@superutils/fetch'
332
+ *
333
+ * // Create a debounced search function with a 300ms delay.
334
+ * const searchProducts = fetchDeferred({
335
+ * delayMs: 300, // Debounce delay
336
+ * resolveIgnored: ResolveIgnored.WITH_UNDEFINED, // Ignored (aborted) promises will resolve with `undefined`
337
+ * })
338
+ *
339
+ * // User types 'iphone'
340
+ * searchProducts('https://dummyjson.com/products/search?q=iphone').then(result => {
341
+ * console.log('Result for "iphone":', result);
342
+ * });
343
+ *
344
+ * // Before 300ms has passed, the user continues typing 'iphone 9'
345
+ * setTimeout(() => {
346
+ * searchProducts('https://dummyjson.com/products/search?q=iphone 9').then(result => {
347
+ * console.log('Result for "iphone 9":', result);
348
+ * });
349
+ * }, 200);
350
+ *
351
+ * // Outcome:
352
+ * // The first request for "iphone" is aborted.
353
+ * // The first promise resolves with `undefined`.
354
+ * // The second request for "iphone 9" is executed after the 300ms debounce delay.
355
+ * ```
356
+ *
357
+ * **Behavior with different `deferOptions` in the example above:**
358
+ * - **`throttle: true`**: Switches from debounce to throttle mode. The first request for "iphone" would
359
+ * execute immediately. The second request for "iphone 9", made within the 300ms throttle window, would be ignored.
360
+ * - **`delayMs: 0`**: Disables debouncing and throttling, enabling sequential/queue mode. Both requests ("iphone"
361
+ * and "iphone 9") would execute, but one after the other, never simultaneously.
362
+ * - **`resolveIgnored`**: Controls how the promise for an aborted request (like the first "iphone" call) resolves.
363
+ * 1. `ResolveIgnored.WITH_UNDEFINED` (used in the example): The promise for the aborted "iphone"
364
+ * request resolves with `undefined`.
365
+ * 2. `ResolveIgnored.WITH_LAST`: The promise for the aborted "iphone" request waits and resolves with the result
366
+ * of the final "iphone 9" request. Both promises resolve to the same value.
367
+ * 3. `ResolveIgnored.NEVER`: The promise for the aborted "iphone" request is neither resolved nor rejected.
368
+ * It will remain pending indefinitely.
369
+ * 4. `ResolveIgnored.WITH_ERROR`: The promise for the aborted "iphone" request is rejected with a `FetchError`.
370
+ *
371
+ * @example Creating a reusable, pre-configured client
372
+ * ```typescript
373
+ * import { fetchDeferred, ResolveIgnored } from '@superutils/fetch'
374
+ *
375
+ * // Create a throttled function to fetch a random quote.
376
+ * // The URL and a 3-second timeout are set as defaults, creating a reusable client.
377
+ * const getRandomQuote = fetchDeferred(
378
+ * {
379
+ * delayMs: 300, // Throttle window
380
+ * throttle: true,
381
+ * // Ignored calls will resolve with the result of the last successful call.
382
+ * resolveIgnored: ResolveIgnored.WITH_LAST,
383
+ * },
384
+ * 'https://dummyjson.com/quotes/random', // Default URL
385
+ * { timeout: 3000 }, // Default fetch options
386
+ * )
387
+ *
388
+ * // Call the function multiple times in quick succession.
389
+ * getRandomQuote().then(quote => console.log('Call 1 resolved:', quote.id));
390
+ * getRandomQuote().then(quote => console.log('Call 2 resolved:', quote.id));
391
+ * getRandomQuote().then(quote => console.log('Call 3 resolved:', quote.id));
392
+ *
393
+ * // Outcome:
394
+ * // Due to throttling, only one network request is made.
395
+ * // Because `resolveIgnored` is `WITH_LAST`, all three promises resolve with the same quote.
396
+ * // The promises for the two ignored calls resolve as soon as the first successful call resolves.
397
+ * // Console output will show the same quote ID for all three calls.
398
+ * ```
399
+ */
400
+ declare function fetchDeferred<ThisArg, DefaultUrl extends string | URL>(deferOptions?: DeferredOptions<ThisArg>, defaultUrl?: DefaultUrl, defaultOptions?: FetchDeferredArgs[1]): <TResult = unknown>(...args: DefaultUrl extends undefined ? FetchArgs : [url?: string | URL | undefined, options?: FetchOptions | undefined]) => _superutils_promise.IPromisE<TResult>;
401
+
402
+ /**
403
+ * Creates a deferred/throttled function for making `POST`, `PUT`, or `PATCH` requests, powered by
404
+ * {@link PromisE.deferred}.
405
+ * This is ideal for scenarios like auto-saving form data, preventing duplicate submissions on button clicks,
406
+ * or throttling API updates.
407
+ *
408
+ * Like `fetchDeferred`, it automatically aborts pending requests when a new one is initiated, ensuring only
409
+ * the most recent or relevant action is executed.
410
+ *
411
+ * @example Debouncing an authentication token refresh
412
+ * ```typescript
413
+ * import { postDeferred } from '@superutils/fetch'
414
+ * import PromisE from '@superutils/promise'
415
+ *
416
+ * // Mock a simple token store
417
+ * let currentRefreshToken = 'initial-refresh-token'
418
+ *
419
+ * // Create a debounced function to refresh the auth token.
420
+ * // It waits 300ms after the last call before executing.
421
+ * const refreshAuthToken = postDeferred(
422
+ * {
423
+ * delayMs: 300, // debounce delay
424
+ * onResult: (result: { token: string }) => {
425
+ * console.log(`Auth token successfully refreshed at ${new Date().toISOString()}`)
426
+ * currentRefreshToken = result.token
427
+ * },
428
+ * },
429
+ * 'https://dummyjson.com/auth/refresh', // Default URL
430
+ * )
431
+ *
432
+ * // This function would be called from various parts of an app,
433
+ * // for example, in response to multiple failed API calls.
434
+ * function requestNewToken() {
435
+ * const body = {
436
+ * refreshToken: currentRefreshToken,
437
+ * expiresInMins: 30,
438
+ * }
439
+ * refreshAuthToken(body)
440
+ * }
441
+ *
442
+ * requestNewToken() // Called at 0ms
443
+ * PromisE.delay(50, requestNewToken) // Called at 50ms
444
+ * PromisE.delay(100, requestNewToken) // Called at 100ms
445
+ *
446
+ * // Outcome:
447
+ * // The first two calls are aborted by the debounce mechanism.
448
+ * // Only the final call executes, 300ms after it was made (at the 400ms mark).
449
+ * // The token is refreshed only once, preventing redundant network requests.
450
+ * ```
451
+ *
452
+ * @example Auto-saving form data with throttling
453
+ * ```typescript
454
+ * import { postDeferred } from '@superutils/fetch'
455
+ * import PromisE from '@superutils/promise'
456
+ *
457
+ * // Create a throttled function to auto-save product updates.
458
+ * const saveProductThrottled = postDeferred(
459
+ * {
460
+ * delayMs: 1000, // Throttle window of 1 second
461
+ * throttle: true,
462
+ * trailing: true, // Ensures the very last update is always saved
463
+ * onResult: (product) => console.log(`[Saved] Product: ${product.title}`),
464
+ * },
465
+ * 'https://dummyjson.com/products/1', // Default URL
466
+ * undefined, // No default data
467
+ * { method: 'put' }, // Default method
468
+ * )
469
+ *
470
+ * // Simulate a user typing quickly, triggering multiple saves.
471
+ * console.log('User starts typing...');
472
+ * saveProductThrottled({ title: 'iPhone' }); // Executed immediately (leading edge)
473
+ * await PromisE.delay(200);
474
+ * saveProductThrottled({ title: 'iPhone 15' }); // Ignored (within 1000ms throttle window)
475
+ * await PromisE.delay(300);
476
+ * saveProductThrottled({ title: 'iPhone 15 Pro' }); // Ignored
477
+ * await PromisE.delay(400);
478
+ * saveProductThrottled({ title: 'iPhone 15 Pro Max' }); // Queued to execute on the trailing edge
479
+ *
480
+ * // Outcome:
481
+ * // The first call ('iPhone') is executed immediately.
482
+ * // The next two calls are ignored by the throttle.
483
+ * // The final call ('iPhone 15 Pro Max') is executed after the 1000ms throttle window closes,
484
+ * // thanks to `trailing: true`.
485
+ * // This results in only two network requests instead of four.
486
+ * ```
487
+ */
488
+ declare function postDeferred<ThisArg, DefaultUrl extends string | URL>(deferOptions?: DeferredOptions<ThisArg>, defaultUrl?: DefaultUrl, defaultData?: PostArgs[1], defaultOptions?: PostArgs[2]): <TResult = unknown>(...args: DefaultUrl extends undefined ? PostArgs : [url?: string | URL | undefined, data?: PostBody | undefined, options?: (Omit<FetchOptions, "method"> & {
489
+ method?: "post" | "put" | "patch" | "delete";
490
+ }) | undefined]) => _superutils_promise.IPromisE<TResult>;
491
+
492
+ /**
493
+ * Merge one or more {@link FetchOptions} with global fetch options ({@link config.fetchOptions}).
494
+ *
495
+ * Notes:
496
+ * - {@link config.fetchOptions} will be added as the base and not necessary to be included
497
+ * - item properties will be prioritized in the order of sequence they were passed in
498
+ * - the following properties will be merged
499
+ * * `errMsgs`
500
+ * * `headers`
501
+ * * `interceptors`
502
+ * - all other properties will simply override previous values
503
+ *
504
+ * @returns combined
505
+ */
506
+ declare const mergeFetchOptions: (...allOptions: FetchOptions[]) => FetchOptionsInterceptor;
507
+
508
+ type Func = <T, Options extends Omit<FetchOptions, 'method'>>(url: string | URL, options?: Options) => ReturnType<typeof fetch$1<T, Options>>;
509
+ type MethodFunc = Func & ({
510
+ deferred: typeof fetchDeferred;
511
+ } | {
512
+ deferred: typeof postDeferred;
513
+ });
514
+ type FetchDeferred = typeof fetchDeferred | typeof postDeferred;
515
+ interface DefaultFetch extends Record<string, MethodFunc> {
516
+ <T, O extends FetchOptions>(...params: Parameters<typeof fetch$1<T, O>>): ReturnType<typeof fetch$1<T, O>>;
517
+ }
518
+ declare const fetch: DefaultFetch;
519
+
520
+ export { type Config, type DefaultFetch, type FetchArgs, type FetchArgsInterceptor, FetchAs, type FetchConf, type FetchDeferred, type FetchDeferredArgs, type FetchErrMsgs, FetchError, type FetchInterceptorError, type FetchInterceptorRequest, type FetchInterceptorResponse, type FetchInterceptorResult, type FetchInterceptors, type FetchOptions, type FetchOptionsInterceptor, type FetchResult, type FetchRetryOptions, type Func, type Interceptor, type MethodFunc, type PostArgs, type PostBody, config, fetch as default, fetch, fetchDeferred, mergeFetchOptions, postDeferred };
package/dist/index.js ADDED
@@ -0,0 +1,347 @@
1
+ // src/config.ts
2
+ import {
3
+ config as promiseConfig
4
+ } from "@superutils/promise";
5
+
6
+ // src/types.ts
7
+ var FetchAs = /* @__PURE__ */ ((FetchAs3) => {
8
+ FetchAs3["arrayBuffer"] = "arrayBuffer";
9
+ FetchAs3["blob"] = "blob";
10
+ FetchAs3["bytes"] = "bytes";
11
+ FetchAs3["formData"] = "formData";
12
+ FetchAs3["json"] = "json";
13
+ FetchAs3["response"] = "response";
14
+ FetchAs3["text"] = "text";
15
+ return FetchAs3;
16
+ })(FetchAs || {});
17
+ var FetchError = class extends Error {
18
+ constructor(message, options) {
19
+ super(message, { cause: options.cause });
20
+ this.name = "FetchError";
21
+ this.options = options.options;
22
+ this.response = options.response;
23
+ this.url = options.url;
24
+ }
25
+ };
26
+
27
+ // src/config.ts
28
+ var fetchOptions = {
29
+ as: "json" /* json */,
30
+ errMsgs: {
31
+ invalidUrl: "Invalid URL",
32
+ parseFailed: "Failed to parse response as",
33
+ reqTimedout: "Request timed out",
34
+ requestFailed: "Request failed with status code:"
35
+ },
36
+ // all error messages must be defined here
37
+ headers: new Headers([["content-type", "application/json"]]),
38
+ /** Global interceptors for fetch requests */
39
+ interceptors: {
40
+ /**
41
+ * Global error interceptors to be invoked whenever an exception occurs
42
+ * Returning an
43
+ */
44
+ error: [],
45
+ /** Interceptors to be invoked before making fetch requests */
46
+ request: [],
47
+ response: [],
48
+ result: []
49
+ },
50
+ ...promiseConfig.retryOptions,
51
+ retryIf: null,
52
+ timeout: 0
53
+ };
54
+ var config = promiseConfig;
55
+ config.fetchOptions = fetchOptions;
56
+ var config_default = config;
57
+
58
+ // src/fetch.ts
59
+ import {
60
+ isFn as isFn2,
61
+ isPositiveNumber,
62
+ isPromise,
63
+ isUrlValid
64
+ } from "@superutils/core";
65
+ import PromisE2 from "@superutils/promise";
66
+
67
+ // src/mergeFetchOptions.ts
68
+ import { isEmpty, objKeys } from "@superutils/core";
69
+ var mergeFetchOptions = (...allOptions) => allOptions.reduce((o1, o2) => {
70
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n;
71
+ const { errMsgs = {}, headers, interceptors: ints1 = {} } = o1;
72
+ const { errMsgs: msgs2 = {}, interceptors: ints2 = {} } = o2;
73
+ o2.headers && new Headers(o2.headers).forEach(
74
+ (value, key) => headers.set(key, value)
75
+ );
76
+ for (const key of objKeys(msgs2)) {
77
+ if (!isEmpty(msgs2[key])) continue;
78
+ errMsgs[key] = msgs2[key];
79
+ }
80
+ return {
81
+ ...o1,
82
+ ...o2,
83
+ errMsgs,
84
+ headers,
85
+ interceptors: {
86
+ error: (_c = (_b = ints1 == null ? void 0 : ints1.error) == null ? void 0 : _b.concat((_a = ints2 == null ? void 0 : ints2.error) != null ? _a : [])) != null ? _c : [],
87
+ request: (_f = (_e = ints1 == null ? void 0 : ints1.request) == null ? void 0 : _e.concat((_d = ints2 == null ? void 0 : ints2.request) != null ? _d : [])) != null ? _f : [],
88
+ response: (_i = (_h = ints1 == null ? void 0 : ints1.response) == null ? void 0 : _h.concat((_g = ints2 == null ? void 0 : ints2.response) != null ? _g : [])) != null ? _i : [],
89
+ result: (_l = (_k = ints1 == null ? void 0 : ints1.result) == null ? void 0 : _k.concat((_j = ints2 == null ? void 0 : ints2.result) != null ? _j : [])) != null ? _l : []
90
+ },
91
+ timeout: (_n = (_m = o2.timeout) != null ? _m : o1.timeout) != null ? _n : 0
92
+ };
93
+ }, config_default.fetchOptions);
94
+ var mergeFetchOptions_default = mergeFetchOptions;
95
+
96
+ // src/executeInterceptors.ts
97
+ import { fallbackIfFails, isFn } from "@superutils/core";
98
+ var executeInterceptors = async (value, interceptors, ...args) => {
99
+ var _a;
100
+ for (const interceptor of interceptors.filter(isFn)) {
101
+ value = (_a = await fallbackIfFails(
102
+ interceptor,
103
+ [value, args],
104
+ void 0
105
+ )) != null ? _a : value;
106
+ }
107
+ return value;
108
+ };
109
+ var executeInterceptors_default = executeInterceptors;
110
+
111
+ // src/getResponse.ts
112
+ import PromisE from "@superutils/promise";
113
+ var getResponse = async (...[url, options]) => {
114
+ const doFetch = () => globalThis.fetch(url, options).catch(
115
+ (err) => err.message === "Failed to fetch" ? (
116
+ // catch network errors to allow retries
117
+ new Response(null, {
118
+ status: 0,
119
+ statusText: "Network Error"
120
+ })
121
+ ) : globalThis.Promise.reject(err)
122
+ );
123
+ const response = await PromisE.retry(doFetch, {
124
+ ...options,
125
+ retryIf: (res, count) => {
126
+ var _a;
127
+ return !(res == null ? void 0 : res.ok) && ((_a = options == null ? void 0 : options.retryIf) == null ? void 0 : _a.call(options, res, count)) !== false;
128
+ }
129
+ }).catch((err) => {
130
+ if (!(options == null ? void 0 : options.retry)) return Promise.reject(err);
131
+ const msg = `Request failed after attempt #${(options.retry || 0) + 1}`;
132
+ return Promise.reject(new Error(msg, { cause: err }));
133
+ });
134
+ return response;
135
+ };
136
+ var getResponse_default = getResponse;
137
+
138
+ // src/fetch.ts
139
+ function fetch(url, options = {}) {
140
+ var _a;
141
+ let abortCtrl;
142
+ let timeoutId;
143
+ (_a = options.method) != null ? _a : options.method = "get";
144
+ const promise = new PromisE2(async (resolve, reject) => {
145
+ var _a2, _b;
146
+ const _options = mergeFetchOptions_default(options);
147
+ const errorInterceptors = [..._options.interceptors.error];
148
+ const requestInterceptors = [..._options.interceptors.request];
149
+ const responseInterceptors = [..._options.interceptors.response];
150
+ const resultInterceptors = [..._options.interceptors.result];
151
+ url = await executeInterceptors_default(url, requestInterceptors, url, _options);
152
+ const { as: parseAs, errMsgs, timeout } = _options;
153
+ if (isPositiveNumber(timeout)) {
154
+ (_a2 = _options.abortCtrl) != null ? _a2 : _options.abortCtrl = new AbortController();
155
+ timeoutId = setTimeout(() => {
156
+ var _a3;
157
+ return (_a3 = _options.abortCtrl) == null ? void 0 : _a3.abort();
158
+ }, timeout);
159
+ }
160
+ abortCtrl = _options.abortCtrl;
161
+ if (_options.abortCtrl) _options.signal = _options.abortCtrl.signal;
162
+ let errResponse;
163
+ try {
164
+ if (!isUrlValid(url, false)) throw errMsgs.invalidUrl;
165
+ let response = await getResponse_default(url, _options);
166
+ response = await executeInterceptors_default(
167
+ response,
168
+ responseInterceptors,
169
+ url,
170
+ _options
171
+ );
172
+ errResponse = response;
173
+ const { status = 0 } = response;
174
+ const isSuccess = status >= 200 && status < 300;
175
+ if (!isSuccess) {
176
+ const jsonError = await response.json();
177
+ const message = (jsonError == null ? void 0 : jsonError.message) || `${errMsgs.requestFailed} ${status}.`;
178
+ throw new Error(`${message}`.replace("Error: ", ""), {
179
+ cause: jsonError
180
+ });
181
+ }
182
+ let result = response;
183
+ const parseFunc = response[parseAs];
184
+ if (isFn2(parseFunc)) {
185
+ const handleErr = (err) => {
186
+ var _a3, _b2;
187
+ err = new Error(
188
+ [
189
+ errMsgs.parseFailed,
190
+ parseAs + ".",
191
+ (_b2 = `${(_a3 = err == null ? void 0 : err.message) != null ? _a3 : err}`) == null ? void 0 : _b2.replace("Error: ", "")
192
+ ].join(" "),
193
+ { cause: err }
194
+ );
195
+ return globalThis.Promise.reject(err);
196
+ };
197
+ result = parseFunc();
198
+ if (isPromise(result)) result = result.catch(handleErr);
199
+ result = await executeInterceptors_default(
200
+ result,
201
+ resultInterceptors,
202
+ url,
203
+ _options
204
+ );
205
+ }
206
+ resolve(await result);
207
+ } catch (err) {
208
+ const errX = err;
209
+ let error = new FetchError(
210
+ (errX == null ? void 0 : errX.name) === "AbortError" ? errMsgs.reqTimedout : err instanceof Error ? err.message : String(err),
211
+ {
212
+ cause: (_b = errX == null ? void 0 : errX.cause) != null ? _b : err,
213
+ response: errResponse,
214
+ options: _options,
215
+ url
216
+ }
217
+ );
218
+ error = await executeInterceptors_default(
219
+ error,
220
+ errorInterceptors,
221
+ url,
222
+ _options
223
+ );
224
+ reject(error);
225
+ }
226
+ timeoutId && clearTimeout(timeoutId);
227
+ });
228
+ promise.onEarlyFinalize.push(() => abortCtrl == null ? void 0 : abortCtrl.abort());
229
+ return promise;
230
+ }
231
+ var fetch_default = fetch;
232
+
233
+ // src/fetchDeferred.ts
234
+ import { forceCast } from "@superutils/core";
235
+ import PromisE3 from "@superutils/promise";
236
+ import {
237
+ ResolveError,
238
+ ResolveIgnored
239
+ } from "@superutils/promise";
240
+ function fetchDeferred(deferOptions = {}, defaultUrl, defaultOptions) {
241
+ let _abortCtrl;
242
+ const fetchCallback = (...args) => {
243
+ var _a, _b;
244
+ const [url, options = {}] = args;
245
+ (_a = options.abortCtrl) != null ? _a : options.abortCtrl = new AbortController();
246
+ (_b = options.timeout) != null ? _b : options.timeout = defaultOptions == null ? void 0 : defaultOptions.timeout;
247
+ options.errMsgs = { ...defaultOptions == null ? void 0 : defaultOptions.errMsgs, ...options.errMsgs };
248
+ const { abortCtrl } = options;
249
+ _abortCtrl == null ? void 0 : _abortCtrl.abort();
250
+ _abortCtrl = abortCtrl;
251
+ const promise = fetch_default(
252
+ ...forceCast([
253
+ url != null ? url : defaultUrl,
254
+ mergeFetchOptions_default(defaultOptions != null ? defaultOptions : {}, options)
255
+ ])
256
+ );
257
+ promise.onEarlyFinalize.push(() => _abortCtrl == null ? void 0 : _abortCtrl.abort());
258
+ return promise;
259
+ };
260
+ return PromisE3.deferredCallback(fetchCallback, deferOptions);
261
+ }
262
+ var fetchDeferred_default = fetchDeferred;
263
+
264
+ // src/postDeferred.ts
265
+ import { forceCast as forceCast2 } from "@superutils/core";
266
+ import PromisE4 from "@superutils/promise";
267
+
268
+ // src/post.ts
269
+ import { isStr } from "@superutils/core";
270
+ function post(...[url = "", data, options = {}]) {
271
+ return fetch_default(
272
+ url,
273
+ mergeFetchOptions_default(
274
+ {
275
+ method: "post",
276
+ body: isStr(data) ? data : JSON.stringify(data)
277
+ },
278
+ options
279
+ )
280
+ );
281
+ }
282
+
283
+ // src/postDeferred.ts
284
+ import {
285
+ ResolveError as ResolveError2,
286
+ ResolveIgnored as ResolveIgnored2
287
+ } from "@superutils/promise";
288
+ function postDeferred(deferOptions = {}, defaultUrl, defaultData, defaultOptions) {
289
+ let _abortCtrl;
290
+ const doPost = (...[url, data, options = {}]) => {
291
+ var _a;
292
+ (_a = options.abortCtrl) != null ? _a : options.abortCtrl = new AbortController();
293
+ _abortCtrl == null ? void 0 : _abortCtrl.abort();
294
+ _abortCtrl = options.abortCtrl;
295
+ const mergedOptions = mergeFetchOptions_default(options, defaultOptions != null ? defaultOptions : {});
296
+ const promise = post(
297
+ ...forceCast2([
298
+ url != null ? url : defaultUrl,
299
+ data != null ? data : defaultData,
300
+ mergedOptions
301
+ ])
302
+ );
303
+ promise.onEarlyFinalize.push(() => _abortCtrl == null ? void 0 : _abortCtrl.abort());
304
+ return promise;
305
+ };
306
+ return PromisE4.deferredCallback(doPost, deferOptions);
307
+ }
308
+ var postDeferred_default = postDeferred;
309
+
310
+ // src/index.ts
311
+ var fetchGet = (method = "get") => {
312
+ const methodFunc = ((url, options = {}) => {
313
+ ;
314
+ options.method = method;
315
+ return fetch_default(url, options);
316
+ });
317
+ methodFunc.deferred = (...args) => fetchDeferred_default(...args);
318
+ return methodFunc;
319
+ };
320
+ var fetchPost = (method = "post") => {
321
+ const methodFunc = ((url, options = {}) => {
322
+ ;
323
+ options.method = method;
324
+ return post(url, options);
325
+ });
326
+ methodFunc.deferred = (...args) => postDeferred_default(...args);
327
+ return methodFunc;
328
+ };
329
+ var fetch2 = fetch_default;
330
+ fetch2.get = fetchGet("get");
331
+ fetch2.head = fetchGet("head");
332
+ fetch2.delete = fetchGet("options");
333
+ fetch2.delete = fetchPost("delete");
334
+ fetch2.patch = fetchPost("patch");
335
+ fetch2.post = fetchPost("post");
336
+ fetch2.put = fetchPost("put");
337
+ var index_default = fetch2;
338
+ export {
339
+ FetchAs,
340
+ FetchError,
341
+ config,
342
+ index_default as default,
343
+ fetch2 as fetch,
344
+ fetchDeferred,
345
+ mergeFetchOptions,
346
+ postDeferred
347
+ };
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "author": "Toufiqur Rahaman Chowdhury",
3
+ "description": "Fetch utilities",
4
+ "dependencies": {
5
+ "@superutils/core": "^1.0.1",
6
+ "@superutils/promise": "^1.0.2"
7
+ },
8
+ "files": [
9
+ "dist",
10
+ "README.md",
11
+ "LICENSE"
12
+ ],
13
+ "keywords": [
14
+ "promise",
15
+ "async",
16
+ "util",
17
+ "typescript"
18
+ ],
19
+ "license": "MIT",
20
+ "main": "dist/index.js",
21
+ "name": "@superutils/fetch",
22
+ "peerDependencies": {
23
+ "@superutils/core": "^1.0.1",
24
+ "@superutils/promise": "^1.0.2"
25
+ },
26
+ "publishConfig": {
27
+ "access": "public"
28
+ },
29
+ "scripts": {
30
+ "_build": "tsc -p tsconfig.json",
31
+ "_watch": "tsc -p tsconfig.json --watch",
32
+ "build": "tsup src/index.ts --format esm --dts --clean --config ../../tsup.config.js",
33
+ "dev": "npm run build -- --watch",
34
+ "test": "cd ../../ && npm run test promise"
35
+ },
36
+ "sideEffects": false,
37
+ "type": "module",
38
+ "types": "dist/index.d.ts",
39
+ "version": "0.1.0"
40
+ }