@superutils/fetch 1.2.1 → 1.2.3
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/README.md +232 -68
- package/dist/index.d.ts +351 -292
- package/dist/index.js +222 -153
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -4,19 +4,28 @@ A lightweight `fetch` wrapper for browsers and Node.js, designed to simplify dat
|
|
|
4
4
|
|
|
5
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
6
|
|
|
7
|
+
<div v-if="false">
|
|
8
|
+
|
|
9
|
+
For full API reference check out the [docs page](https://alien45.github.io/superutils/packages/@superutils/fetch/).
|
|
10
|
+
|
|
11
|
+
</div>
|
|
12
|
+
|
|
7
13
|
## Table of Contents
|
|
8
14
|
|
|
9
15
|
- Features
|
|
10
16
|
- Installation
|
|
11
17
|
- Usage
|
|
12
|
-
- [`fetch()`](#fetch):
|
|
18
|
+
- [`fetch()`](#fetch): drop-in replacement for built-in `fetch()`
|
|
19
|
+
- [`PromisE Features`](#promise-features): status, early finalization etc
|
|
13
20
|
- [`Method Specific Functions`](#methods)
|
|
21
|
+
- [`fetch.get()`](#fetch-get)
|
|
14
22
|
- [`fetch.get.deferred()`](#fetch-deferred): cancellable and debounced or throttled `fetch()`
|
|
15
23
|
- [`fetch.post()`](#post): make post requests
|
|
16
24
|
- [`fetch.post.deferred()`](#post-deferred): cancellable and debounced or throttled `post()`
|
|
17
25
|
- [`Retry`](#retry) Retry on request failure
|
|
18
26
|
- [`Timeout`](#timeout) Abort request on timeout
|
|
19
27
|
- [`Interceptors/Transformers`](#interceptors)
|
|
28
|
+
- [`Reusable Clients`](#reusable-clients)
|
|
20
29
|
|
|
21
30
|
## Features
|
|
22
31
|
|
|
@@ -40,7 +49,7 @@ npm install @superutils/fetch
|
|
|
40
49
|
|
|
41
50
|
### `fetch(url, options)`
|
|
42
51
|
|
|
43
|
-
Use as a drop-in replacement to
|
|
52
|
+
Use as a drop-in replacement to built-in `fetch()`.
|
|
44
53
|
|
|
45
54
|
```javascript
|
|
46
55
|
import fetch from '@superutils/fetch'
|
|
@@ -50,6 +59,48 @@ fetch('https://dummyjson.com/products/1')
|
|
|
50
59
|
.then(console.log)
|
|
51
60
|
```
|
|
52
61
|
|
|
62
|
+
<div id="promise-features"></div>
|
|
63
|
+
|
|
64
|
+
### PromisE Instance: status, early cancellation
|
|
65
|
+
|
|
66
|
+
All fetch calls return a `PromisE` (`@superutils/promise`) instance which means they come with additional features available in `PromisE`:
|
|
67
|
+
|
|
68
|
+
1. Status tracking: all instances come with `.pending`, `.resolved` and `.rejected` attributes that indicate the current state of the promise.
|
|
69
|
+
|
|
70
|
+
```javascript
|
|
71
|
+
import fetch from '@superutils/fetch'
|
|
72
|
+
|
|
73
|
+
const request = fetch('https://dummyjson.com/products/1')
|
|
74
|
+
|
|
75
|
+
console.log(request.pending) // true
|
|
76
|
+
|
|
77
|
+
request.then(() => {
|
|
78
|
+
console.log(request.resolved) // true
|
|
79
|
+
console.log(request.pending) // false
|
|
80
|
+
console.log(request.rejected) // false
|
|
81
|
+
})
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
2. Early finalization: all `PromisE` instances expose `.resolve()` and `.reject()` methods that allow early finalization and `.onEarlyFinalize` array that allows adding callbacks to be executed when the promise is finalized externally using these methods. Fetch promises utilize this to abort the request when appropriate.
|
|
85
|
+
|
|
86
|
+
```javascript
|
|
87
|
+
import fetch from '@superutils/fetch'
|
|
88
|
+
|
|
89
|
+
// Request that will take 5 seconds to resolve
|
|
90
|
+
const request = fetch('https://dummyjson.com/products?delay=5000')
|
|
91
|
+
|
|
92
|
+
request.then(result => console.log(result), console.warn)
|
|
93
|
+
|
|
94
|
+
// Add a callback to do stuff whenever request is aborted externally.
|
|
95
|
+
// This will not be invoked if fetch fails or resolves (promise finalized naturally) using the Promise executor.
|
|
96
|
+
request.onEarlyFinalize.push((resolved, valueOrReason) =>
|
|
97
|
+
console.log('Aborted externally:', { resolved, valueOrReason }),
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
// resolve/reject before the promise is finalized
|
|
101
|
+
request.reject(new Error('No longer needed'))
|
|
102
|
+
```
|
|
103
|
+
|
|
53
104
|
<div id="methods"></div>
|
|
54
105
|
|
|
55
106
|
### Method Specific Functions
|
|
@@ -76,6 +127,14 @@ While `fetch()` provides access to all HTTP request methods by specifying it in
|
|
|
76
127
|
|
|
77
128
|
All method specific functions by default return result parsed as JSON. No need for `response.json()` or `result.data.data` drilling.
|
|
78
129
|
|
|
130
|
+
<div id="fetch-get"></div>
|
|
131
|
+
|
|
132
|
+
### `fetch.get(url, options)`
|
|
133
|
+
|
|
134
|
+
Performs a GET request and returns the result parsed as JSON by default.
|
|
135
|
+
|
|
136
|
+
Equivalent to `fetch(url, { method: 'get', as: 'json' })`.
|
|
137
|
+
|
|
79
138
|
```javascript
|
|
80
139
|
import fetch from '@superutils/fetch'
|
|
81
140
|
|
|
@@ -134,10 +193,11 @@ setTimeout(() => {
|
|
|
134
193
|
3. `ResolveIgnored.NEVER`: The promise for the aborted "iphone" request is neither resolved nor rejected.
|
|
135
194
|
It will remain pending indefinitely.
|
|
136
195
|
- **`resolveError` (enum)**: Controls how failed requests are handled.
|
|
137
|
-
1. `ResolveError.NEVER`:
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
196
|
+
1. `ResolveError.NEVER`: The promise for a failed request will neither resolve nor reject, causing it to remain pending indefinitely.
|
|
197
|
+
> **Warning:** Use with caution, as this may lead to memory leaks if not handled properly.
|
|
198
|
+
2. `ResolveError.WITH_ERROR`: The promise resolves with the `FetchError` object instead of being rejected.
|
|
199
|
+
3. `ResolveError.WITH_UNDEFINED`: The promise resolves with an `undefined` value upon failure.
|
|
200
|
+
4. `ResolveError.REJECT`: (Default) The promise is rejected with a `FetchError`, adhering to standard promise behavior.
|
|
141
201
|
|
|
142
202
|
#### Using defaults to reduce redundancy
|
|
143
203
|
|
|
@@ -255,6 +315,7 @@ const requestNewToken = fetch.post.deferred(
|
|
|
255
315
|
)
|
|
256
316
|
|
|
257
317
|
// First authenticate user to get the initial refresh token and then request new referesh tokens
|
|
318
|
+
// First authenticate user to get the initial refresh token and then request new refresh tokens
|
|
258
319
|
fetch
|
|
259
320
|
.post<{ refreshToken: string }>(
|
|
260
321
|
'https://dummyjson.com/auth/login',
|
|
@@ -301,74 +362,86 @@ The following interceptor callbacks allow intercepting and/or transforming at di
|
|
|
301
362
|
- Value returned (transformed) by an interceptor will be carried over to the subsequent interceptor of the same type.
|
|
302
363
|
- There are 2 category of interceptors:
|
|
303
364
|
- Local: interceptors provided when making a request.
|
|
304
|
-
- Global: intereptors that are executed application-wide on every request. Global interceptors can be added/accessed at `fetch.defaults.interceptors`. Global interceptors are always executed before local interceptors.
|
|
305
365
|
|
|
306
|
-
**Example:
|
|
366
|
+
**Example: Interceptor usage**
|
|
307
367
|
|
|
308
368
|
```javascript
|
|
309
|
-
import fetch from '@superutils/fetch'
|
|
369
|
+
import fetch, { FetchError } from '@superutils/fetch'
|
|
370
|
+
|
|
371
|
+
const interceptors = {
|
|
372
|
+
error: [
|
|
373
|
+
(err, url, options) => {
|
|
374
|
+
console.log('Request failed', err, url, options)
|
|
375
|
+
// return nothing/undefined to keep the error unchanged
|
|
376
|
+
// or return modified/new error
|
|
377
|
+
err.message = 'My custom error message!'
|
|
378
|
+
// or create a new FetchError by cloning it (make sure all the required properties are set correctly)
|
|
379
|
+
return err.clone('My custom error message!')
|
|
380
|
+
},
|
|
381
|
+
],
|
|
382
|
+
request: [
|
|
383
|
+
(url, options) => {
|
|
384
|
+
// add extra headers or modify request options here
|
|
385
|
+
options.headers.append('x-custom-header', 'some value')
|
|
386
|
+
|
|
387
|
+
// transform the URL by returning a modified URL
|
|
388
|
+
return url + '?param=value'
|
|
389
|
+
},
|
|
390
|
+
],
|
|
391
|
+
response: [
|
|
392
|
+
(response, url, options) => {
|
|
393
|
+
if (response.ok) return
|
|
394
|
+
console.log('request was successful', { url, options })
|
|
395
|
+
|
|
396
|
+
// You can transform the response by returning different `Response` object or even make a completely new HTTP reuqest.
|
|
397
|
+
// You can transform the response by returning different `Response` object or even make a completely new HTTP request.
|
|
398
|
+
// The subsequent response interceptors will receive the returned response
|
|
399
|
+
return fetch('https://dummyjson.com/products/1') // promise will be resolved automatically
|
|
400
|
+
},
|
|
401
|
+
],
|
|
402
|
+
result: [
|
|
403
|
+
(result, url, options) => {
|
|
404
|
+
const productId = Number(
|
|
405
|
+
new URL(url).pathname.split('/products/')[1],
|
|
406
|
+
)
|
|
407
|
+
if (options.method === 'get' && !Number.isNaN(productId)) {
|
|
408
|
+
result.title ??= 'Unknown title'
|
|
409
|
+
}
|
|
410
|
+
return result
|
|
411
|
+
},
|
|
412
|
+
],
|
|
413
|
+
}
|
|
414
|
+
fetch
|
|
415
|
+
.get('https://dummyjson.com/products/1', { interceptors })
|
|
416
|
+
.then(product => console.log({ product }))
|
|
417
|
+
```
|
|
310
418
|
|
|
311
|
-
|
|
312
|
-
interceptors.request.push((url, options) => {
|
|
313
|
-
// a headers to all requests make by the application
|
|
314
|
-
options.headers.append('x-auth', 'token')
|
|
315
|
-
})
|
|
419
|
+
- Global: interceptors that are executed application-wide on every request. Global interceptors can be added/accessed at `fetch.defaults.interceptors`. Global interceptors are always executed before local interceptors.
|
|
316
420
|
|
|
317
|
-
|
|
318
|
-
// log whenever a request fails
|
|
319
|
-
console.log('Error interceptor', err)
|
|
320
|
-
})
|
|
321
|
-
```
|
|
421
|
+
**Example: Add global request and error interceptors**
|
|
322
422
|
|
|
323
|
-
```javascript
|
|
324
|
-
import fetch
|
|
325
|
-
|
|
326
|
-
const interceptors =
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
],
|
|
346
|
-
response: [
|
|
347
|
-
(response, url, options) => {
|
|
348
|
-
if (response.ok) return
|
|
349
|
-
console.log('request was successful', { url, options })
|
|
350
|
-
|
|
351
|
-
// You can transform the response by returning different `Response` object or even make a completely new HTTP reuqest.
|
|
352
|
-
// The subsequent response interceptors will receive the returned response
|
|
353
|
-
return fetch('https://dummyjson.com/products/1') // promise will be resolved automatically
|
|
354
|
-
},
|
|
355
|
-
],
|
|
356
|
-
result: [
|
|
357
|
-
(result, url, options) => {
|
|
358
|
-
const productId = Number(
|
|
359
|
-
new URL(url).pathname.split('/products/')[1],
|
|
360
|
-
)
|
|
361
|
-
if (options.method === 'get' && !Number.isNaN(productId)) {
|
|
362
|
-
result.title ??= 'Unknown title'
|
|
363
|
-
}
|
|
364
|
-
return result
|
|
365
|
-
},
|
|
366
|
-
],
|
|
367
|
-
}
|
|
368
|
-
fetch
|
|
369
|
-
.get('https://dummyjson.com/products/1', { interceptors })
|
|
370
|
-
.then(product => console.log({ product }))
|
|
371
|
-
```
|
|
423
|
+
```javascript
|
|
424
|
+
import fetch from '@superutils/fetch'
|
|
425
|
+
|
|
426
|
+
const { interceptors } = fetch.defaults
|
|
427
|
+
|
|
428
|
+
interceptors.request.push((url, options) => {
|
|
429
|
+
// a headers to all requests make by the application
|
|
430
|
+
// add headers to all requests made by the application
|
|
431
|
+
options.headers.append('x-auth', 'token')
|
|
432
|
+
})
|
|
433
|
+
|
|
434
|
+
interceptors.error.push((err, url, options) => {
|
|
435
|
+
// log whenever a request fails
|
|
436
|
+
console.log('Error interceptor', err)
|
|
437
|
+
})
|
|
438
|
+
|
|
439
|
+
// Each time a requst is made using @superutils/fetch, the above interceptors will be executed when appropriate
|
|
440
|
+
fetch('https://dummyjson.com/products/1').then(
|
|
441
|
+
console.log,
|
|
442
|
+
console.warn,
|
|
443
|
+
)
|
|
444
|
+
```
|
|
372
445
|
|
|
373
446
|
<div id="retry"></div>
|
|
374
447
|
|
|
@@ -437,3 +510,94 @@ fetch.get('https://dummyjson.com/products/1', {
|
|
|
437
510
|
as: FetchAs.text,
|
|
438
511
|
})
|
|
439
512
|
```
|
|
513
|
+
|
|
514
|
+
<div id="reusable-clients"></div>
|
|
515
|
+
|
|
516
|
+
### `createClient(fixedOptions, commonOptions, commonDeferOptions)`: Reusable Clients
|
|
517
|
+
|
|
518
|
+
The `createClient` utility streamlines the creation of dedicated API clients by generating pre-configured fetch functions. These functions can be equipped with default options like headers, timeouts, or a specific HTTP method, which minimizes code repetition across your application. If a method is not specified during creation, the client will default to `GET`.
|
|
519
|
+
|
|
520
|
+
The returned client also includes a `.deferred()` method, providing the same debounce, throttle, and sequential execution capabilities found in functions like `fetch.get.deferred()`.
|
|
521
|
+
|
|
522
|
+
```javascript
|
|
523
|
+
import { createClient } from '@superutils/fetch'
|
|
524
|
+
|
|
525
|
+
// Create a "GET" client with default headers and a 5-second timeout
|
|
526
|
+
const apiClient = createClient(
|
|
527
|
+
{
|
|
528
|
+
// fixed options cannot be overridden
|
|
529
|
+
method: 'get',
|
|
530
|
+
},
|
|
531
|
+
{
|
|
532
|
+
// default options can be overridden
|
|
533
|
+
headers: {
|
|
534
|
+
Authorization: 'Bearer my-secret-token',
|
|
535
|
+
'Content-Type': 'application/json',
|
|
536
|
+
},
|
|
537
|
+
timeout: 5000,
|
|
538
|
+
},
|
|
539
|
+
{
|
|
540
|
+
// default defer options (can be overridden)
|
|
541
|
+
delayMs: 300,
|
|
542
|
+
retry: 2, // If request fails, retry up to two more times
|
|
543
|
+
},
|
|
544
|
+
)
|
|
545
|
+
|
|
546
|
+
// Use it just like the standard fetch
|
|
547
|
+
apiClient('https://dummyjson.com/products/1', {
|
|
548
|
+
// The 'method' property cannot be overridden as it is used in the fixed options when creating the client.
|
|
549
|
+
// In TypeScript, the compiler will not allow this property.
|
|
550
|
+
// In Javascript, it will simply be ignored.
|
|
551
|
+
// method: 'post',
|
|
552
|
+
timeout: 3000, // The 'timeout' property can be overridden
|
|
553
|
+
}).then(console.log, console.warn)
|
|
554
|
+
|
|
555
|
+
// create a deferred client using "apiClient"
|
|
556
|
+
const deferredClient = apiClient.deferred(
|
|
557
|
+
{ retry: 0 }, // disable retrying by overriding the `retry` defer option
|
|
558
|
+
'https://dummyjson.com/products/1',
|
|
559
|
+
{ timeout: 3000 },
|
|
560
|
+
)
|
|
561
|
+
deferredClient({ timeout: 10000 }) // timeout is overridden by individual request
|
|
562
|
+
.then(console.log, console.warn)
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
### `createPostClient(mandatoryOptions, commonOptions, commonDeferOptions)`: Reusable Post-like Clients
|
|
566
|
+
|
|
567
|
+
While `createClient()` is versatile enough for any HTTP method, `createPostClient()` is specifically designed for methods that require a request body, such as `DELETE`, `PATCH`, `POST`, and `PUT`. If a method is not provided, it defaults to `POST`. The generated client accepts an additional second parameter (`data`) for the request payload.
|
|
568
|
+
|
|
569
|
+
Similar to `createClient`, the returned function comes equipped with a `.deferred()` method, enabling debounced, throttled, or sequential execution.
|
|
570
|
+
|
|
571
|
+
```javascript
|
|
572
|
+
import { createPostClient, FetchAs } from '@superutils/fetch'
|
|
573
|
+
|
|
574
|
+
// Create a POST client with 10-second as the default timeout
|
|
575
|
+
const postClient = createPostClient(
|
|
576
|
+
{
|
|
577
|
+
method: 'post',
|
|
578
|
+
headers: { 'content-type': 'application/json' },
|
|
579
|
+
},
|
|
580
|
+
{ timeout: 10000 },
|
|
581
|
+
)
|
|
582
|
+
|
|
583
|
+
// Invoking `postClient()` automatically applies the pre-configured options
|
|
584
|
+
postClient(
|
|
585
|
+
'https://dummyjson.com/products/add',
|
|
586
|
+
{ title: 'New Product' }, // data/body
|
|
587
|
+
{}, // other options
|
|
588
|
+
).then(console.log)
|
|
589
|
+
|
|
590
|
+
// create a deferred client using "postClient"
|
|
591
|
+
const updateProduct = postClient.deferred(
|
|
592
|
+
{
|
|
593
|
+
delayMs: 300, // debounce duration
|
|
594
|
+
},
|
|
595
|
+
'https://dummyjson.com/products/1',
|
|
596
|
+
{
|
|
597
|
+
method: 'patch',
|
|
598
|
+
timeout: 3000,
|
|
599
|
+
},
|
|
600
|
+
)
|
|
601
|
+
updateProduct({ title: 'New title 1' }) // ignored by debounce
|
|
602
|
+
updateProduct({ title: 'New title 2' }) // executed
|
|
603
|
+
```
|