fetchbasequery 0.2.1 → 1.0.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 +1 -1
- package/README.md +262 -36
- package/jsconfig.json +12 -0
- package/package.json +26 -6
- package/src/decorators/index.d.ts +10 -0
- package/src/decorators/index.js +1 -0
- package/src/decorators/json.js +107 -0
- package/src/fetchBaseQuery.js +101 -14
- package/src/index.d.ts +19 -0
- package/src/index.js +4 -0
- package/src/middlewares/index.d.ts +14 -0
- package/src/middlewares/index.js +3 -4
- package/src/middlewares/muteErrors.js +17 -0
- package/src/middlewares/retryOn429.js +37 -0
- package/src/middlewares/retryOnError.js +32 -0
- package/src/utils/sleep.js +17 -0
- package/tests/index.html +7 -0
- package/tests/index.js +546 -0
- package/src/baseQuery.js +0 -80
- package/src/helpers/abortable.js +0 -13
- package/src/helpers/encodeJson.js +0 -8
- package/src/helpers/mergeHeaders.js +0 -9
- package/src/helpers/pipe.js +0 -1
- package/src/helpers/purge.js +0 -16
- package/src/helpers/responseFromError.js +0 -5
- package/src/middlewares/accessToken.js +0 -62
- package/src/middlewares/commonReply.js +0 -23
- package/src/middlewares/encodeBody.js +0 -85
- package/src/middlewares/jsonReply.js +0 -35
- package/types/index.d.ts +0 -64
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
# fetchBaseQuery
|
|
2
2
|
|
|
3
3
|
Library for easily make HTTP queries without mass of boilerplate code.
|
|
4
|
+
It works fine in any environment that supports `Fetch API`.
|
|
5
|
+
|
|
6
|
+
## Features
|
|
7
|
+
|
|
8
|
+
- Lightweight modular typescript ready code
|
|
9
|
+
- Supports middleware programming pattern
|
|
10
|
+
- Using only modern API, has no any legacy code
|
|
4
11
|
|
|
5
12
|
## Installation
|
|
6
13
|
|
|
@@ -18,70 +25,289 @@ pnpm add fetchbasequery
|
|
|
18
25
|
|
|
19
26
|
## Usage
|
|
20
27
|
|
|
21
|
-
|
|
28
|
+
### Basic usage
|
|
29
|
+
|
|
30
|
+
Use a factory function, which enhance a bare `fetch()`:
|
|
22
31
|
|
|
23
32
|
```js
|
|
24
|
-
import fetchBaseQuery
|
|
33
|
+
import fetchBaseQuery from 'fetchbasequery'
|
|
25
34
|
|
|
26
35
|
const query = fetchBaseQuery({
|
|
27
36
|
// main options ...
|
|
28
|
-
baseUrl: 'https://api.example.org/v1'
|
|
29
|
-
baseQuery,
|
|
37
|
+
baseUrl: 'https://api.example.org/v1',
|
|
30
38
|
timeout: 10000, // in milliseconds
|
|
31
|
-
|
|
39
|
+
fetch: globalThis.fetch, // it is possible to use a custom `fetch()` here
|
|
32
40
|
// add some defaults for `fetch()` ...
|
|
33
41
|
mode: 'cors',
|
|
34
42
|
credentials: 'include',
|
|
35
43
|
cache: 'reload',
|
|
36
|
-
//
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
+
// ... see `RequestInit` for more options ...
|
|
45
|
+
})
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Then use the returned function instead of bare `fetch()`:
|
|
49
|
+
|
|
50
|
+
```js
|
|
51
|
+
const resource = `/users/${id}` // full or short URL
|
|
52
|
+
const options = { // define some options, see `RequestInit`
|
|
53
|
+
headers: { 'Authorization': '...' }
|
|
54
|
+
}
|
|
55
|
+
const response = await query(resource, options) // all options defined in `fetchBaseQuery()` will be passed into request
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Function returns an abortable promise described using type `AbortablePromise<T>`.
|
|
59
|
+
So it is possible to abort the request at any time.
|
|
60
|
+
|
|
61
|
+
```js
|
|
62
|
+
const promise = query(...)
|
|
63
|
+
// ...
|
|
64
|
+
promise.abort('Component was unmounted...') // `reason` message is optional
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Options of factory function described by type `FetchBaseQueryOptions`:
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
export type FetchBaseQueryOptions = RequestInit & {
|
|
71
|
+
baseUrl?: string, // origin or prefix, used for URLs without it
|
|
72
|
+
timeout?: number, // default timeout in milliseconds
|
|
73
|
+
fetch?: BaseFetchFunction, // by default this is reference to `globalThis.fetch()`
|
|
74
|
+
middlewares?: Middleware[] // list of middleware functions
|
|
75
|
+
};
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Using middlewares
|
|
79
|
+
|
|
80
|
+
```js
|
|
81
|
+
import fetchBaseQuery from 'fetchbasequery'
|
|
82
|
+
import { retryOnError } from 'fetchbasequery/middlewares'
|
|
83
|
+
|
|
84
|
+
const query = fetchBaseQuery({
|
|
85
|
+
// ... options ...
|
|
86
|
+
middlewares: [ // add some middlewares
|
|
87
|
+
retryOnError({
|
|
88
|
+
maxNumAttempts: 5
|
|
44
89
|
}),
|
|
45
|
-
|
|
46
|
-
commonReply()
|
|
90
|
+
// ...
|
|
47
91
|
]
|
|
48
92
|
})
|
|
49
93
|
```
|
|
50
94
|
|
|
51
|
-
|
|
95
|
+
### Using decorators
|
|
52
96
|
|
|
53
97
|
```js
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
98
|
+
import fetchBaseQuery from 'fetchbasequery'
|
|
99
|
+
import { json } from 'fetchbasequery/decorators'
|
|
100
|
+
|
|
101
|
+
const query = json(fetchBaseQuery({
|
|
102
|
+
// ... options ...
|
|
103
|
+
}))
|
|
104
|
+
|
|
105
|
+
// Now `query()` accepts plain object as body and stringify it automatically.
|
|
106
|
+
// Also response has a `data` property, which contains parsed json returned by backend.
|
|
107
|
+
const { data } = await query('/articles', {
|
|
108
|
+
method: 'post',
|
|
109
|
+
body: {
|
|
110
|
+
title: '...',
|
|
111
|
+
poster: '...',
|
|
112
|
+
// ...
|
|
113
|
+
}
|
|
114
|
+
})
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Writing a custom middleware
|
|
118
|
+
|
|
119
|
+
Middleware described by type `Middleware`:
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
export type Middleware = (request: Request, next: (request: Request) => Promise<Response>) => Promise<Response>;
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Every middleware follows the simple pattern. Get `request`, do something with request,
|
|
126
|
+
then call `next(request)` to get `response`, do something with response and return it.
|
|
127
|
+
|
|
128
|
+
Let's create simple middleware for example:
|
|
129
|
+
|
|
130
|
+
```js
|
|
131
|
+
// Add to the request a CSRF-token as a custom header `X-CSRF-Token`
|
|
132
|
+
async function useCsrfToken(request, next) {
|
|
133
|
+
const token = await getCsrfToken() // an abstract function just for example
|
|
134
|
+
// modify the request
|
|
135
|
+
const headers = new Headers(request.headers)
|
|
136
|
+
headers.set('X-CSRF-Token', token)
|
|
137
|
+
const modifiedRequest = new Request(request, { headers })
|
|
138
|
+
// make a query using the modified request
|
|
139
|
+
const response = await next(modifiedRequest)
|
|
140
|
+
// it is possible to modify response too, but in this example we does not need it
|
|
141
|
+
return response // just return the response as is
|
|
62
142
|
}
|
|
63
143
|
```
|
|
64
144
|
|
|
65
|
-
|
|
145
|
+
Usage:
|
|
66
146
|
|
|
67
147
|
```js
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
148
|
+
const query = fetchBaseQuery({
|
|
149
|
+
baseUrl: 'https://...', // an abstract
|
|
150
|
+
// ...
|
|
151
|
+
middlewares: [
|
|
152
|
+
// ...
|
|
153
|
+
useCsrfToken,
|
|
154
|
+
// ...
|
|
155
|
+
]
|
|
156
|
+
})
|
|
157
|
+
// ...
|
|
158
|
+
const formData = getFormData() // an abstract
|
|
159
|
+
// post the form to the protected route
|
|
160
|
+
const response = await query('/articles', {
|
|
161
|
+
method: 'post',
|
|
162
|
+
body: formData
|
|
72
163
|
})
|
|
73
164
|
```
|
|
74
165
|
|
|
75
|
-
|
|
166
|
+
### Writing a custom decorators
|
|
76
167
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
168
|
+
Decorators can extends logic and redefine types of request and response.
|
|
169
|
+
|
|
170
|
+
There are no restrictions for a decorator. It just a get original function and
|
|
171
|
+
return it's enhanced version. That's all.
|
|
172
|
+
|
|
173
|
+
Let's create a simple example:
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
import type { FetchFunction, AbortablePromise } from 'fetchbasequery';
|
|
177
|
+
import fetchBaseQuery from 'fetchbasequery';
|
|
178
|
+
|
|
179
|
+
export type XmlResponse = Response & { document?: XMLDocument };
|
|
180
|
+
|
|
181
|
+
// Parses automatically XML documents provided by a raw response
|
|
182
|
+
export function withXmlParser(query: FetchFunction<Response>) {
|
|
183
|
+
const supportedTypes = [
|
|
184
|
+
'application/xml',
|
|
185
|
+
'text/xml',
|
|
186
|
+
]
|
|
187
|
+
return function (
|
|
188
|
+
resource: string | Request,
|
|
189
|
+
{ signal: initSignal, ...options }: RequestInit = {}
|
|
190
|
+
): AbortablePromise<XmlResponse> {
|
|
191
|
+
const abortController = new AbortController()
|
|
192
|
+
const signal = AbortSignal.any([
|
|
193
|
+
abortController.signal,
|
|
194
|
+
...(initSignal instanceof AbortSignal ? [ initSignal ] : [])
|
|
195
|
+
])
|
|
196
|
+
const promise = (async () => {
|
|
197
|
+
const response = (await query(resource, { signal, ...options })) as XmlResponse
|
|
198
|
+
const contentType = String(response.headers.get('content-type')).split(';')[0]
|
|
199
|
+
if (supportedTypes.includes(contentType)) { // if the response contains XML document
|
|
200
|
+
// then parse content into special property `.document`
|
|
201
|
+
const parser = new DOMParser()
|
|
202
|
+
response.document = parser.parseFromString(await response.text(), 'application/xml')
|
|
203
|
+
}
|
|
204
|
+
return response
|
|
205
|
+
})() as AbortablePromise<XmlResponse>;
|
|
206
|
+
promise.abort = (reason) => { abortController.abort(reason) }
|
|
207
|
+
return promise
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Usage:
|
|
213
|
+
|
|
214
|
+
```ts
|
|
215
|
+
const query = withXmlParser(fetchBaseQuery({
|
|
216
|
+
baseUrl: 'https://...', // an abstract
|
|
217
|
+
// ...
|
|
218
|
+
}))
|
|
219
|
+
const response = await query('/sitemap.xml')
|
|
220
|
+
console.log('Status:', response.status)
|
|
221
|
+
console.log('Content-Type:', response.headers.get('content-type'))
|
|
222
|
+
console.log('Document:', response.document)
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
## List of provided middlewares
|
|
226
|
+
|
|
227
|
+
### `muteErrors()`
|
|
228
|
+
|
|
229
|
+
Catch any error thrown by `next()` mute it and returns `Response.error()`.
|
|
230
|
+
Typically usage: use conditional statements instead of using `try-catch` statements.
|
|
231
|
+
|
|
232
|
+
### `retryOnError(options?: RetryOnErrorOptions)`
|
|
233
|
+
|
|
234
|
+
Retry to make a query when an error was thrown by `next()`.
|
|
235
|
+
Typically usage: repeat the request in case of a temporary loss of connection.
|
|
236
|
+
|
|
237
|
+
Options:
|
|
238
|
+
|
|
239
|
+
```ts
|
|
240
|
+
export type RetryOnErrorOptions = {
|
|
241
|
+
maxNumAttempts?: number = 5, // the maximum number of attempts
|
|
242
|
+
backoffTime?: (attempt: number) => number, // delay time in milliseconds
|
|
243
|
+
};
|
|
80
244
|
```
|
|
81
245
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
246
|
+
### `retryOn429(options?: RetryOn429Options)`
|
|
247
|
+
|
|
248
|
+
Retry to make a query when server returns status `429 Too Many Requests`.
|
|
249
|
+
Typically usage: repeat the request in case when servers busy or when rate limit was reached.
|
|
250
|
+
|
|
251
|
+
Options:
|
|
252
|
+
|
|
253
|
+
```ts
|
|
254
|
+
export type RetryOn429Options = {
|
|
255
|
+
maxNumAttempts?: number = 5, // the maximum number of attempts
|
|
256
|
+
maxBackoffTime?: number = Infinity, // the maximum permissible delay time
|
|
257
|
+
backoffTime?: (attempt: number, response: Response) => number, // delay time in milliseconds
|
|
258
|
+
};
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
## List of provided decorators
|
|
262
|
+
|
|
263
|
+
### `json<T>(query: FetchFunction<Response>): JsonFetchFunction<JsonResponse<T>>`
|
|
264
|
+
|
|
265
|
+
Allows to pass a plain objects as `body` and automatically serialize plain objects into JSON.
|
|
266
|
+
It also automatically parse JSON documents provided by a raw response into special property `.data`.
|
|
267
|
+
|
|
268
|
+
Usage:
|
|
269
|
+
```ts
|
|
270
|
+
import fetchBaseQuery from 'fetchbasequery'
|
|
271
|
+
import { json } from 'fetchbasequery/decorators'
|
|
272
|
+
|
|
273
|
+
const query = json(fetchBaseQuery({
|
|
274
|
+
// ...
|
|
275
|
+
}))
|
|
276
|
+
// ...
|
|
277
|
+
const response = await query('https://...', {
|
|
278
|
+
method: 'put',
|
|
279
|
+
body: { // decorator allows to pass a plain objects as body
|
|
280
|
+
title: '...',
|
|
281
|
+
// ...
|
|
282
|
+
}
|
|
283
|
+
})
|
|
284
|
+
console.log('Status:', response.status)
|
|
285
|
+
console.log('Data:', response.data) // decorator also parses body returned as json
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
## Testing
|
|
289
|
+
|
|
290
|
+
Running tests:
|
|
291
|
+
|
|
292
|
+
```sh
|
|
293
|
+
npm run test
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
## TODO
|
|
297
|
+
|
|
298
|
+
- Chaining methods of `AbortablePromise<T>` (`.then()`, `.catch()` and `.finally()`)
|
|
299
|
+
must returns instance of `AbortablePromise<T>` not pure `Promise<T>`;
|
|
300
|
+
- Add static methods to `AbortablePromise<T>` (`.all()`, `.any()`, `.race()`, etc);
|
|
301
|
+
- Describe and export utility function `sleep()` - abortable sleep function;
|
|
302
|
+
- Add, describe and export utility function `mergeHeaders()` to helps to write a middleware;
|
|
303
|
+
|
|
304
|
+
## References
|
|
305
|
+
|
|
306
|
+
- [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API)
|
|
307
|
+
- [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request)
|
|
308
|
+
- [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response)
|
|
309
|
+
- [RequestInit](https://developer.mozilla.org/en-US/docs/Web/API/RequestInit)
|
|
310
|
+
- [429 Too Many Requests](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/429)
|
|
85
311
|
|
|
86
312
|
## License
|
|
87
313
|
|
package/jsconfig.json
ADDED
package/package.json
CHANGED
|
@@ -1,19 +1,39 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fetchbasequery",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "Library for easily make HTTP queries without mass of boilerplate code.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/fetchBaseQuery.js",
|
|
7
|
-
"types": "
|
|
7
|
+
"types": "src/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./src/index.d.ts",
|
|
11
|
+
"import": "./src/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./decorators": {
|
|
14
|
+
"types": "./src/decorators/index.d.ts",
|
|
15
|
+
"import": "./src/decorators/index.js"
|
|
16
|
+
},
|
|
17
|
+
"./middlewares": {
|
|
18
|
+
"types": "./src/middlewares/index.d.ts",
|
|
19
|
+
"import": "./src/middlewares/index.js"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
8
22
|
"scripts": {
|
|
23
|
+
"test": "node tests/index.js"
|
|
9
24
|
},
|
|
10
25
|
"keywords": [
|
|
11
|
-
"fetch",
|
|
26
|
+
"fetch",
|
|
27
|
+
"fetchBaseQuery",
|
|
28
|
+
"middleware",
|
|
29
|
+
"retry",
|
|
30
|
+
"retryOnError",
|
|
31
|
+
"retryOn429",
|
|
32
|
+
"muteErrors",
|
|
33
|
+
"json"
|
|
12
34
|
],
|
|
13
|
-
"author": "h41ku <h41ku.c0de@ya.ru>",
|
|
14
35
|
"license": "MIT",
|
|
15
|
-
"dependencies": {
|
|
16
|
-
},
|
|
17
36
|
"devDependencies": {
|
|
37
|
+
"@types/node": "^26.6.5"
|
|
18
38
|
}
|
|
19
39
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { AbortablePromise, FetchFunction } from "../index.d.ts";
|
|
2
|
+
|
|
3
|
+
type Override<T, U> = Omit<T, keyof U> & U;
|
|
4
|
+
|
|
5
|
+
export type PlainObject = Record<string, any>;
|
|
6
|
+
export type JsonRequestInit = Override<RequestInit, { body?: BodyInit | PlainObject }>;
|
|
7
|
+
export type JsonResponse<T> = Response & { data?: T };
|
|
8
|
+
export type JsonFetchFunction<T> = (resource: string | Request, options?: JsonRequestInit) => AbortablePromise<T>;
|
|
9
|
+
|
|
10
|
+
export declare function json<T>(query: FetchFunction<Response>): JsonFetchFunction<JsonResponse<T>>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { json } from './json.js'
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {import('../index.d.ts').FetchFunction<T>} FetchFunction
|
|
3
|
+
* @template T
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @typedef {import('./index.d.ts').JsonFetchFunction<T>} JsonFetchFunction
|
|
8
|
+
* @template T
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {import('./index.d.ts').PlainObject} PlainObject
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @typedef {import('./index.d.ts').JsonRequestInit} JsonRequestInit
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @typedef {import('../index.d.ts').AbortablePromise<T>} AbortablePromise
|
|
21
|
+
* @template T
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @typedef {import('./index.d.ts').JsonResponse<T>} JsonResponse
|
|
26
|
+
* @template T
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
*
|
|
31
|
+
* @param {BodyInit | PlainObject} value
|
|
32
|
+
* @returns {value is PlainObject}
|
|
33
|
+
*/
|
|
34
|
+
function isPlainObject(value) {
|
|
35
|
+
return (
|
|
36
|
+
typeof value === 'object'
|
|
37
|
+
//&& value !== null
|
|
38
|
+
&& !ArrayBuffer.isView(value)
|
|
39
|
+
&& !(value instanceof FormData)
|
|
40
|
+
&& !(value instanceof Blob)
|
|
41
|
+
&& !(value instanceof ReadableStream)
|
|
42
|
+
&& !(value instanceof URLSearchParams)
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
*
|
|
48
|
+
* @param {PlainObject} body
|
|
49
|
+
* @returns {string}
|
|
50
|
+
*/
|
|
51
|
+
function modifyBody(body) {
|
|
52
|
+
return JSON.stringify(body, (key, value) => {
|
|
53
|
+
if (value instanceof Error) {
|
|
54
|
+
return value.stack.split(/\r?\n/g)
|
|
55
|
+
} else if (typeof value === 'bigint') {
|
|
56
|
+
return value.toString()
|
|
57
|
+
}
|
|
58
|
+
return value
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
*
|
|
64
|
+
* @param {HeadersInit} headers
|
|
65
|
+
* @returns {Headers}
|
|
66
|
+
*/
|
|
67
|
+
function modifyHeaders(headers) {
|
|
68
|
+
const result = new Headers(headers)
|
|
69
|
+
if (!result.has('content-type'))
|
|
70
|
+
result.set('content-type', 'application/json; charset=utf-8')
|
|
71
|
+
return result
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
*
|
|
76
|
+
* @param {FetchFunction<Response>} query
|
|
77
|
+
* @returns {JsonFetchFunction<JsonResponse<T>>}
|
|
78
|
+
* @template T
|
|
79
|
+
*/
|
|
80
|
+
export function json(query) {
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* @param {string | Request} resource
|
|
84
|
+
* @param {JsonRequestInit} [options = {}]
|
|
85
|
+
*/
|
|
86
|
+
return (resource, { body: initBody, headers: initHeaders, signal: initSignal, ...options } = {}) => {
|
|
87
|
+
const abortController = new AbortController()
|
|
88
|
+
const signal = AbortSignal.any([
|
|
89
|
+
abortController.signal,
|
|
90
|
+
...(initSignal instanceof AbortSignal ? [ initSignal ] : [])
|
|
91
|
+
])
|
|
92
|
+
/**
|
|
93
|
+
* @type {AbortablePromise<JsonResponse<T>>}
|
|
94
|
+
*/
|
|
95
|
+
const promise = (async () => {
|
|
96
|
+
const isJson = isPlainObject(initBody)
|
|
97
|
+
const body = isJson ? modifyBody(initBody) : initBody
|
|
98
|
+
const headers = isJson ? modifyHeaders(initHeaders) : initHeaders
|
|
99
|
+
const response = /** @type {JsonResponse<T>} */ (await query(resource, { body, headers, signal, ...options }))
|
|
100
|
+
if (/^application\/json(\;| |$)/.test(String(response.headers.get('content-type'))))
|
|
101
|
+
response.data = await (response.clone().json())
|
|
102
|
+
return response
|
|
103
|
+
})();
|
|
104
|
+
promise.abort = (reason) => { abortController.abort(reason) }
|
|
105
|
+
return promise
|
|
106
|
+
}
|
|
107
|
+
}
|
package/src/fetchBaseQuery.js
CHANGED
|
@@ -1,15 +1,102 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {import('./index.d.ts').FetchBaseQueryOptions} FetchBaseQueryOptions
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @typedef {import('./index.d.ts').AbortablePromise<T>} AbortablePromise<T>
|
|
7
|
+
* @template T
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {import('./index.d.ts').FetchFunction<T>} FetchFunction<T>
|
|
12
|
+
* @template T
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
*
|
|
17
|
+
* @param {import('./index.d.ts').FetchBaseQueryOptions} options
|
|
18
|
+
* @returns {FetchFunction<T>}
|
|
19
|
+
* @template T
|
|
20
|
+
*/
|
|
21
|
+
export function fetchBaseQuery({
|
|
22
|
+
baseUrl = '',
|
|
23
|
+
timeout = Number.MAX_SAFE_INTEGER,
|
|
24
|
+
fetch = globalThis.fetch,
|
|
25
|
+
middlewares = [],
|
|
26
|
+
...baseOptions
|
|
27
|
+
} = {}) {
|
|
13
28
|
|
|
14
|
-
|
|
15
|
-
|
|
29
|
+
const abortController = new AbortController()
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
*
|
|
33
|
+
* @param {number} index
|
|
34
|
+
* @param {Request} request
|
|
35
|
+
* @returns {Promise<Response>}
|
|
36
|
+
*/
|
|
37
|
+
const runChain = (index, request) => {
|
|
38
|
+
if (index === middlewares.length) {
|
|
39
|
+
return fetch(request)
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* @param {Request} request
|
|
43
|
+
* @returns {Promise<Response>}
|
|
44
|
+
*/
|
|
45
|
+
const next = (request) => runChain(index + 1, request)
|
|
46
|
+
return middlewares[index](request, next)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
*
|
|
51
|
+
* @param {string} endpoint
|
|
52
|
+
* @returns {string}
|
|
53
|
+
*/
|
|
54
|
+
const useBaseUrl = (endpoint) => {
|
|
55
|
+
let url, origin, prefix = '/'
|
|
56
|
+
if (baseUrl) {
|
|
57
|
+
const url = new URL(baseUrl)
|
|
58
|
+
origin = url.origin
|
|
59
|
+
prefix = url.pathname
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
url = new URL(endpoint)
|
|
63
|
+
} catch (error) {
|
|
64
|
+
url = new URL(
|
|
65
|
+
/\/$/.test(prefix) && /^\//.test(endpoint)
|
|
66
|
+
? prefix + endpoint.substring(1)
|
|
67
|
+
: prefix + endpoint,
|
|
68
|
+
origin
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
return url.toString()
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
*
|
|
76
|
+
* @param {string | Request} resource
|
|
77
|
+
* @param {RequestInit} [options = {}]
|
|
78
|
+
* @returns {AbortablePromise<T>}
|
|
79
|
+
*/
|
|
80
|
+
return (resource, options = {}) => {
|
|
81
|
+
const signalByTimeout = timeout !== Number.MAX_SAFE_INTEGER ? AbortSignal.timeout(timeout) : undefined
|
|
82
|
+
const signalFromRequest = resource instanceof Request ? resource.signal : undefined
|
|
83
|
+
const { signal: signalFromOptions, ...otherOptions } = options
|
|
84
|
+
const target = resource instanceof Request ? resource : useBaseUrl(resource)
|
|
85
|
+
const request = new Request(target, {
|
|
86
|
+
...baseOptions,
|
|
87
|
+
signal: AbortSignal.any([
|
|
88
|
+
abortController.signal,
|
|
89
|
+
signalByTimeout,
|
|
90
|
+
signalFromRequest,
|
|
91
|
+
signalFromOptions
|
|
92
|
+
].filter(signal => signal instanceof AbortSignal)),
|
|
93
|
+
...otherOptions
|
|
94
|
+
})
|
|
95
|
+
const promise = /** @type {AbortablePromise<T>} */ (runChain(0, request))
|
|
96
|
+
/**
|
|
97
|
+
* @param {string} reason
|
|
98
|
+
*/
|
|
99
|
+
promise.abort = (reason) => { abortController.abort(reason) }
|
|
100
|
+
return promise
|
|
101
|
+
}
|
|
102
|
+
}
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export type AbortablePromise<T> = Promise<T> & {
|
|
2
|
+
abort: (reason?: string) => void
|
|
3
|
+
};
|
|
4
|
+
|
|
5
|
+
export type Middleware = (request: Request, next: (request: Request) => Promise<Response>) => Promise<Response>;
|
|
6
|
+
|
|
7
|
+
export type BaseFetchFunction = (resource: string | Request, options?: RequestInit) => Promise<Response>;
|
|
8
|
+
export type FetchBaseQueryOptions = RequestInit & {
|
|
9
|
+
baseUrl?: string, // origin or prefix, used for URLs without it
|
|
10
|
+
timeout?: number, // default timeout in milliseconds
|
|
11
|
+
fetch?: BaseFetchFunction, // by default this is reference to `globalThis.fetch()`
|
|
12
|
+
middlewares?: Middleware[] // list of middleware functions
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type FetchFunction<T> = (resource: string | Request, options?: RequestInit) => AbortablePromise<T>;
|
|
16
|
+
|
|
17
|
+
export declare function fetchBaseQuery<T>(options?: FetchBaseQueryOptions): FetchFunction<T>;
|
|
18
|
+
|
|
19
|
+
export default fetchBaseQuery
|
package/src/index.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type RetryOnErrorOptions = {
|
|
2
|
+
maxNumAttempts?: number = 5, // the maximum number of attempts
|
|
3
|
+
backoffTime?: (attempt: number) => number, // delay time in milliseconds
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
export type RetryOn429Options = {
|
|
7
|
+
maxNumAttempts?: number = 5, // the maximum number of attempts
|
|
8
|
+
maxBackoffTime?: number = Infinity, // the maximum permissible delay time
|
|
9
|
+
backoffTime?: (attempt: number, response: Response) => number, // delay time in milliseconds
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export declare function muteErrors(): Middleware;
|
|
13
|
+
export declare function retryOn429(options?: RetryOn429Options): Middleware;
|
|
14
|
+
export declare function retryOnError(options?: RetryOnErrorOptions): Middleware;
|