@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 +21 -0
- package/README.md +133 -0
- package/dist/index.d.ts +520 -0
- package/dist/index.js +347 -0
- package/package.json +40 -0
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)`
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|