fch 3.0.6 → 4.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/package.json +23 -11
- package/readme.md +219 -126
- package/fetch.js +0 -202
- package/fetch.min.js +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fch",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.1.0",
|
|
4
4
|
"description": "Fetch interface with better promises, deduplication, defaults, etc.",
|
|
5
5
|
"homepage": "https://github.com/franciscop/fetch",
|
|
6
6
|
"repository": "https://github.com/franciscop/fetch.git",
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
"author": "Francisco Presencia <public@francisco.io> (https://francisco.io/)",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"scripts": {
|
|
12
|
-
"build": "
|
|
12
|
+
"build": "rollup -c",
|
|
13
|
+
"size": "echo $(gzip -c index.min.js | wc -c) bytes",
|
|
13
14
|
"start": "npm run watch # Start ~= Start dev",
|
|
14
15
|
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage --detectOpenHandles",
|
|
15
16
|
"watch": "node --experimental-vm-modules node_modules/jest/bin/jest.js --watch --coverage --detectOpenHandles"
|
|
@@ -22,24 +23,35 @@
|
|
|
22
23
|
"async",
|
|
23
24
|
"ajax"
|
|
24
25
|
],
|
|
25
|
-
"main": "./
|
|
26
|
-
"
|
|
27
|
-
"browser": "./fetch.min.js",
|
|
28
|
-
"files": [
|
|
29
|
-
"fetch.js",
|
|
30
|
-
"fetch.min.js"
|
|
31
|
-
],
|
|
26
|
+
"main": "./index.min.js",
|
|
27
|
+
"files": [],
|
|
32
28
|
"type": "module",
|
|
33
29
|
"engines": {
|
|
34
30
|
"node": ">=18.0.0"
|
|
35
31
|
},
|
|
36
32
|
"devDependencies": {
|
|
37
|
-
"
|
|
33
|
+
"@babel/core": "^7.15.0",
|
|
34
|
+
"@babel/preset-env": "^7.15.0",
|
|
35
|
+
"@babel/preset-react": "^7.14.5",
|
|
36
|
+
"babel-loader": "^8.2.2",
|
|
37
|
+
"babel-polyfill": "^6.26.0",
|
|
38
|
+
"jest": "^28.1.0",
|
|
39
|
+
"jest-environment-jsdom": "^28.1.0",
|
|
38
40
|
"jest-fetch-mock": "^3.0.3",
|
|
39
|
-
"
|
|
41
|
+
"rollup": "^1.32.1",
|
|
42
|
+
"rollup-plugin-babel": "^4.4.0",
|
|
43
|
+
"rollup-plugin-node-resolve": "^5.2.0",
|
|
44
|
+
"rollup-plugin-terser": "^5.2.0",
|
|
45
|
+
"swear": "^1.1.2"
|
|
40
46
|
},
|
|
41
47
|
"jest": {
|
|
42
48
|
"testEnvironment": "jest-environment-node",
|
|
43
49
|
"transform": {}
|
|
50
|
+
},
|
|
51
|
+
"babel": {
|
|
52
|
+
"presets": [
|
|
53
|
+
"@babel/preset-env",
|
|
54
|
+
"@babel/preset-react"
|
|
55
|
+
]
|
|
44
56
|
}
|
|
45
57
|
}
|
package/readme.md
CHANGED
|
@@ -3,47 +3,54 @@
|
|
|
3
3
|
A tiny library to make API calls easier. Similar to Axios, but tiny size and simpler API:
|
|
4
4
|
|
|
5
5
|
```js
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const mew = await
|
|
6
|
+
// Plain usage
|
|
7
|
+
import fch from "fch";
|
|
8
|
+
const mew = await fch("https://pokeapi.co/pokemon/150");
|
|
9
9
|
console.log(mew);
|
|
10
|
+
|
|
11
|
+
// As an API abstraction
|
|
12
|
+
const api = fch.create({ baseUrl: "https://pokeapi.co/" });
|
|
13
|
+
const mew = await api.get("/pokemon/150");
|
|
14
|
+
await api.patch("/pokemon/150", { body: { type: "psychic" } });
|
|
10
15
|
```
|
|
11
16
|
|
|
12
17
|
- Create instances with shared options across requests.
|
|
13
18
|
- Automatically encode object and array bodies as JSON.
|
|
14
19
|
- Automatically decode JSON responses based on the headers.
|
|
15
20
|
- Await/Async Promises; `>= 400 and <= 100` will _reject_ with an error.
|
|
21
|
+
- Credentials: "include" by default
|
|
16
22
|
- Interceptors: `before` the request, `after` the response and catch with `error`.
|
|
17
23
|
- Deduplicates parallel GET requests.
|
|
18
24
|
- Works the same way in Node.js and the browser.
|
|
19
25
|
- No dependencies; include it with a simple `<script>` on the browser.
|
|
20
26
|
|
|
21
27
|
```js
|
|
22
|
-
import api from
|
|
23
|
-
|
|
24
|
-
api.get(url, { headers, ...options })
|
|
25
|
-
api.head(url, { headers, ...options })
|
|
26
|
-
api.post(url, { body, headers, ...options })
|
|
27
|
-
api.patch(url, { body, headers, ...options })
|
|
28
|
-
api.put(url, { body, headers, ...options })
|
|
29
|
-
api.del(url, { body, headers, ...options })
|
|
30
|
-
|
|
31
|
-
api.create({ url, body, headers, ...options})
|
|
32
|
-
```
|
|
28
|
+
import api from "fch";
|
|
33
29
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
|
46
|
-
|
|
|
30
|
+
api.get(url, { headers, ...options });
|
|
31
|
+
api.head(url, { headers, ...options });
|
|
32
|
+
api.post(url, { body, headers, ...options });
|
|
33
|
+
api.patch(url, { body, headers, ...options });
|
|
34
|
+
api.put(url, { body, headers, ...options });
|
|
35
|
+
api.del(url, { body, headers, ...options });
|
|
36
|
+
api.delete(url, { body, headers, ...options });
|
|
37
|
+
|
|
38
|
+
api.create({ url, body, headers, ...options });
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
| Options | Default | Description |
|
|
42
|
+
| ------------------------- | ------------------ | ---------------------------------------- |
|
|
43
|
+
| [`method`](#method) | `"get"` | Default method to use for the call |
|
|
44
|
+
| [`url`](#url) | `"/"` | The path or url for the request |
|
|
45
|
+
| [`baseUrl`](#url) | `null` | The shared base of the API |
|
|
46
|
+
| [`body`](#body) | `null` | The body to send with the request |
|
|
47
|
+
| [`query`](#query) | `{}` | Add query parameters to the URL |
|
|
48
|
+
| [`headers`](#headers) | `{}` | Shared headers across all requests |
|
|
49
|
+
| [`output`](#output) | `"body"` | The return value of the API call |
|
|
50
|
+
| [`dedupe`](#dedupe) | `true` | Reuse concurrently GET requests |
|
|
51
|
+
| [`before`](#interceptors) | `req => req` | Process the request before sending it |
|
|
52
|
+
| [`after`](#interceptors) | `res => res` | Process the response before returning it |
|
|
53
|
+
| [`error`](#interceptors) | `err => throw err` | Process errors before returning them |
|
|
47
54
|
|
|
48
55
|
## Getting Started
|
|
49
56
|
|
|
@@ -56,17 +63,17 @@ npm install fch
|
|
|
56
63
|
Then import it to be able to use it in your code:
|
|
57
64
|
|
|
58
65
|
```js
|
|
59
|
-
import fch from
|
|
60
|
-
const body = await fch.get(
|
|
66
|
+
import fch from "fch";
|
|
67
|
+
const body = await fch.get("/");
|
|
61
68
|
```
|
|
62
69
|
|
|
63
70
|
On the browser you can add it with a script and it will be available as `fch`:
|
|
64
71
|
|
|
65
72
|
```html
|
|
66
73
|
<!-- Import it as usual -->
|
|
67
|
-
<script src="https://cdn.jsdelivr.net/npm/fch"></script>
|
|
68
|
-
<script>
|
|
69
|
-
fch(
|
|
74
|
+
<script src="https://cdn.jsdelivr.net/npm/fch" type="module"></script>
|
|
75
|
+
<script type="module">
|
|
76
|
+
fch("/hello");
|
|
70
77
|
</script>
|
|
71
78
|
```
|
|
72
79
|
|
|
@@ -75,23 +82,23 @@ On the browser you can add it with a script and it will be available as `fch`:
|
|
|
75
82
|
These are all available options and their defaults:
|
|
76
83
|
|
|
77
84
|
```js
|
|
78
|
-
import api from
|
|
85
|
+
import api from "fch";
|
|
79
86
|
|
|
80
87
|
// General options with their defaults; all of these are also parameters:
|
|
81
|
-
api.method =
|
|
82
|
-
api.url =
|
|
83
|
-
api.baseUrl = null;
|
|
84
|
-
api.query = {};
|
|
85
|
-
api.headers = {};
|
|
88
|
+
api.method = "get"; // Default method to use for api()
|
|
89
|
+
api.url = "/"; // Relative or absolute url where the request is sent
|
|
90
|
+
api.baseUrl = null; // Set an API base URL reused all across requests
|
|
91
|
+
api.query = {}; // Merged with the query parameters passed manually
|
|
92
|
+
api.headers = {}; // Merged with the headers on a per-request basis
|
|
86
93
|
|
|
87
94
|
// Control simple variables
|
|
88
|
-
api.output =
|
|
89
|
-
api.dedupe = true;
|
|
95
|
+
api.output = "body"; // Return the parsed body; use 'response' or 'stream' otherwise
|
|
96
|
+
api.dedupe = true; // Avoid sending concurrent GET requests to the same path
|
|
90
97
|
|
|
91
98
|
// Interceptors
|
|
92
|
-
api.before = req => req;
|
|
93
|
-
api.after = res => res;
|
|
94
|
-
api.error = err => Promise.reject(err);
|
|
99
|
+
api.before = (req) => req;
|
|
100
|
+
api.after = (res) => res;
|
|
101
|
+
api.error = (err) => Promise.reject(err);
|
|
95
102
|
```
|
|
96
103
|
|
|
97
104
|
They can all be defined globally as shown above, passed manually as the options argument, or be used when [creating a new instance](#create-an-instance).
|
|
@@ -101,11 +108,11 @@ They can all be defined globally as shown above, passed manually as the options
|
|
|
101
108
|
The HTTP method to make the request. When using the shorthand, it defaults to `GET`. We recommend using the method syntax:
|
|
102
109
|
|
|
103
110
|
```js
|
|
104
|
-
import api from
|
|
111
|
+
import api from "fch";
|
|
105
112
|
|
|
106
|
-
api.get(
|
|
107
|
-
api.post(
|
|
108
|
-
api.put(`/cats/3`, { body: { name:
|
|
113
|
+
api.get("/cats");
|
|
114
|
+
api.post("/cats", { body: { name: "snowball" } });
|
|
115
|
+
api.put(`/cats/3`, { body: { name: "snowball" } });
|
|
109
116
|
```
|
|
110
117
|
|
|
111
118
|
You can use it with the plain function as an option parameter. The methods are all lowercase but the option as a parameter is case insensitive; it can be either uppercase or lowercase:
|
|
@@ -125,12 +132,12 @@ api({ method; 'get'})
|
|
|
125
132
|
Example: adding a new cat and fixing a typo:
|
|
126
133
|
|
|
127
134
|
```js
|
|
128
|
-
import api from
|
|
135
|
+
import api from "fch";
|
|
129
136
|
|
|
130
|
-
const cats = await api.get(
|
|
137
|
+
const cats = await api.get("/cats");
|
|
131
138
|
console.log(cats);
|
|
132
|
-
const { id } = await api.post(
|
|
133
|
-
await api.put(`/cats/${id}`, { body: { name:
|
|
139
|
+
const { id } = await api.post("/cats", { body: { name: "snowbll" } });
|
|
140
|
+
await api.put(`/cats/${id}`, { body: { name: "snowball" } });
|
|
134
141
|
```
|
|
135
142
|
|
|
136
143
|
### Url
|
|
@@ -138,23 +145,23 @@ await api.put(`/cats/${id}`, { body: { name: 'snowball' }})
|
|
|
138
145
|
Specify where to send the request to. It's normally the first argument, though technically you can use both styles:
|
|
139
146
|
|
|
140
147
|
```js
|
|
141
|
-
import api from
|
|
148
|
+
import api from "fch";
|
|
142
149
|
|
|
143
150
|
// Recommended way of specifying the Url
|
|
144
|
-
await api.post(
|
|
151
|
+
await api.post("/hello", { body: "...", headers: {} });
|
|
145
152
|
|
|
146
153
|
// These are also valid if you prefer their style; we won't judge
|
|
147
|
-
await api(
|
|
148
|
-
await api({ url:
|
|
149
|
-
await api.post({ url:
|
|
154
|
+
await api("/hello", { method: "post", body: "...", headers: {} });
|
|
155
|
+
await api({ url: "/hello", method: "post", headers: {}, body: "..." });
|
|
156
|
+
await api.post({ url: "/hello", headers: {}, body: "..." });
|
|
150
157
|
```
|
|
151
158
|
|
|
152
159
|
It can be either absolute or relative, in which case it'll use the local one in the page. It's recommending to set `baseUrl`:
|
|
153
160
|
|
|
154
161
|
```js
|
|
155
|
-
import api from
|
|
156
|
-
api.baseUrl =
|
|
157
|
-
api.get(
|
|
162
|
+
import api from "fch";
|
|
163
|
+
api.baseUrl = "https//api.filemon.io/";
|
|
164
|
+
api.get("/hello");
|
|
158
165
|
// Called https//api.filemon.io/hello
|
|
159
166
|
```
|
|
160
167
|
|
|
@@ -183,8 +190,10 @@ The methods `GET` and `HEAD` do not accept a body and it'll be ignored.
|
|
|
183
190
|
|
|
184
191
|
The **response body** will be returned by default as the output of the call:
|
|
185
192
|
|
|
193
|
+
> See more info in [**Output**](#output)
|
|
194
|
+
|
|
186
195
|
```js
|
|
187
|
-
const body = await api.get(
|
|
196
|
+
const body = await api.get("/cats");
|
|
188
197
|
console.log(body);
|
|
189
198
|
// [{ id: 1, }, ...]
|
|
190
199
|
```
|
|
@@ -194,51 +203,48 @@ When the server specifies the header `Content-Type` as `application/json`, then
|
|
|
194
203
|
When the function returns the response (if you set `output: "response"` as an option), then the body can be accessed as `response.body`:
|
|
195
204
|
|
|
196
205
|
```js
|
|
197
|
-
const response = await api.get(
|
|
206
|
+
const response = await api.get("/cats", { output: "response" });
|
|
198
207
|
console.log(response.body);
|
|
199
208
|
// [{ id: 1, }, ...]
|
|
200
209
|
```
|
|
201
210
|
|
|
202
|
-
|
|
203
211
|
### Query
|
|
204
212
|
|
|
205
213
|
You can easily pass GET query parameters by using the option `query`:
|
|
206
214
|
|
|
207
215
|
```js
|
|
208
|
-
api.get(
|
|
216
|
+
api.get("/cats", { query: { limit: 3 } });
|
|
209
217
|
// /cats?limit=3
|
|
210
218
|
```
|
|
211
219
|
|
|
212
220
|
While rare, some times you might want to persist a query parameter across requests and always include it; in that case, you can define it globally and it'll be added to every request:
|
|
213
221
|
|
|
214
222
|
```js
|
|
215
|
-
import api from
|
|
216
|
-
api.query.myparam =
|
|
223
|
+
import api from "fch";
|
|
224
|
+
api.query.myparam = "abc";
|
|
217
225
|
|
|
218
|
-
api.get(
|
|
226
|
+
api.get("/cats", { query: { limit: 3 } });
|
|
219
227
|
// /cats?limit=3&myparam=abc
|
|
220
228
|
```
|
|
221
229
|
|
|
222
|
-
|
|
223
230
|
### Headers
|
|
224
231
|
|
|
225
232
|
You can define headers globally, in which case they'll be added to every request, or locally, so that they are only added to the current request. You can also add them in the `before` callback:
|
|
226
233
|
|
|
227
|
-
|
|
228
234
|
```js
|
|
229
|
-
import api from
|
|
235
|
+
import api from "fch";
|
|
230
236
|
|
|
231
237
|
// Globally, so they are reused across all requests
|
|
232
|
-
api.headers.a =
|
|
238
|
+
api.headers.a = "b";
|
|
233
239
|
|
|
234
240
|
// With an interceptor, in case you need dynamic headers per-request
|
|
235
|
-
api.before = req => {
|
|
236
|
-
req.headers.c =
|
|
241
|
+
api.before = (req) => {
|
|
242
|
+
req.headers.c = "d";
|
|
237
243
|
return req;
|
|
238
244
|
};
|
|
239
245
|
|
|
240
246
|
// Set them for this single request:
|
|
241
|
-
api.get(
|
|
247
|
+
api.get("/hello", { headers: { e: "f" } });
|
|
242
248
|
// Total headers on the request:
|
|
243
249
|
// { a: 'b', c: 'd', e: 'f' }
|
|
244
250
|
```
|
|
@@ -249,31 +255,75 @@ When to use each?
|
|
|
249
255
|
- When you need to extract them dynamically from somewhere it's better to use the .before() interceptor. An example would be the user Authorization token.
|
|
250
256
|
- When it changes on each request, it's not consistent or it's an one-off, use the option argument.
|
|
251
257
|
|
|
252
|
-
|
|
253
258
|
### Output
|
|
254
259
|
|
|
255
|
-
|
|
260
|
+
The default output manipulation is to expect either plan `TEXT` as `plain/text` or `JSON` as `application/json` from the `Content-Type`. If your API works with these (the vast majority of APIs do) then you should be fine out of the box!
|
|
261
|
+
|
|
262
|
+
```js
|
|
263
|
+
const cats = await api.get("/cats");
|
|
264
|
+
console.log(cats); // [{ id: 1, name: 'Whiskers', ... }, ...]
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
For more expressive control, you can use the **`output` option** (either as a default when [creating an instance](#create-an-instance) or with each call), or using a method:
|
|
256
268
|
|
|
257
269
|
```js
|
|
258
|
-
|
|
270
|
+
const api = fch.create({ output: "json" }); // JSON by default
|
|
271
|
+
const streamImg = await api.get("/cats/123/image", { output: "stream" }); // Stream the image
|
|
272
|
+
const streamImg2 = await api.get("/cats/123/image").stream(); // Shortcut for the one above
|
|
273
|
+
```
|
|
259
274
|
|
|
260
|
-
|
|
261
|
-
api.output = 'body';
|
|
275
|
+
This controls whether the call returns just the body (default), or the whole response. It can be controlled globally or on a per-request basis:
|
|
262
276
|
|
|
263
|
-
|
|
264
|
-
|
|
277
|
+
```js
|
|
278
|
+
// Return only the body (default)
|
|
279
|
+
const body = await api.get("/data");
|
|
265
280
|
|
|
266
281
|
// Return the whole response (with .body):
|
|
267
|
-
const response = await api.get(
|
|
282
|
+
const response = await api.get("/data", { output: "response" });
|
|
283
|
+
|
|
284
|
+
// Return a plain body stream
|
|
285
|
+
const stream = await api.get("/data", { output: "stream" });
|
|
286
|
+
stream.pipeTo(outStream);
|
|
268
287
|
|
|
269
|
-
//
|
|
270
|
-
const
|
|
288
|
+
// Return a blob, since `response.blob()` is available:
|
|
289
|
+
const blob = await api.get("/data", { output: "blob" });
|
|
271
290
|
```
|
|
272
291
|
|
|
292
|
+
There are few options that can be specified:
|
|
293
|
+
|
|
294
|
+
- `output: "body"` (default): returns the body, parsed as JSON or plain TEXT depending on the headers.
|
|
295
|
+
- `output: "response"`: return the full response with the properties `body`, `headers`, `status`. The body will be parsed as JSON or plain TEXT depending on the headers. If you want the raw `response`, use `raw` or `clone` instead (see below in "raw" or "clone").
|
|
296
|
+
- `output: "stream"`: return a [web ReadableStream](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream) of the body as the result of the promise.
|
|
297
|
+
- `output: "arrayBuffer"`\*: returns an arrayBuffer of the response body.
|
|
298
|
+
- `output: "blob"`\*: returns an arrayBuffer of the response body.
|
|
299
|
+
- `output: "clone"`\*: returns the raw Response, with the raw body. See also `raw` below.
|
|
300
|
+
- `output: "formData"`\* (might be unavailable): returns an instance of FormData with all the parsed data.
|
|
301
|
+
- `output: "json"`\*: attempts to parse the response as JSON.
|
|
302
|
+
- `output: "text"`\*: puts the response body as plain text.
|
|
303
|
+
- `output: "raw"`\*: an alias for `clone`, returning the raw response (after passing through `after`).
|
|
304
|
+
|
|
305
|
+
\* Standard [MDN methods](https://developer.mozilla.org/en-US/docs/Web/API/Response#methods)
|
|
306
|
+
|
|
307
|
+
The `output` values can all be used as a method as well. So all of these are equivalent:
|
|
308
|
+
|
|
309
|
+
```js
|
|
310
|
+
const text = await api.get("/cats", { output: "text" });
|
|
311
|
+
const text = await api.get("/cats").text();
|
|
312
|
+
|
|
313
|
+
const raw = await api.get("/cats", { output: "raw" });
|
|
314
|
+
const raw = await api.get("/cats").raw();
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
For example, return the raw body as a `ReadableStream` with the option `stream`:
|
|
318
|
+
|
|
319
|
+
```js
|
|
320
|
+
const stream = await api.get('/cats', { output: 'stream' });
|
|
321
|
+
stream.pipeTo(...);
|
|
322
|
+
```
|
|
273
323
|
|
|
274
324
|
### Dedupe
|
|
275
325
|
|
|
276
|
-
When you perform a GET request to a given URL, but another GET request
|
|
326
|
+
When you perform a GET request to a given URL, but another GET request _to the same_ URL is ongoing, it'll **not** create a new request and instead reuse the response when the first one is finished:
|
|
277
327
|
|
|
278
328
|
```js
|
|
279
329
|
fetch.mockOnce("a").mockOnce("b");
|
|
@@ -287,12 +337,12 @@ You can disable this by setting either the global `fch.dedupe` option to `false`
|
|
|
287
337
|
|
|
288
338
|
```js
|
|
289
339
|
// Globally set it for all calls
|
|
290
|
-
fch.dedupe = true;
|
|
340
|
+
fch.dedupe = true; // [DEFAULT] Dedupes GET requests
|
|
291
341
|
fch.dedupe = false; // All fetch() calls trigger a network call
|
|
292
342
|
|
|
293
343
|
// Set it on a per-call basis
|
|
294
|
-
fch(
|
|
295
|
-
fch(
|
|
344
|
+
fch("/a", { dedupe: true }); // [DEFAULT] Dedupes GET requests
|
|
345
|
+
fch("/a", { dedupe: false }); // All fetch() calls trigger a network call
|
|
296
346
|
```
|
|
297
347
|
|
|
298
348
|
> We do not support deduping other methods besides `GET` right now
|
|
@@ -305,7 +355,7 @@ it("can opt out locally", async () => {
|
|
|
305
355
|
const res = await Promise.all([
|
|
306
356
|
fch("/a"),
|
|
307
357
|
fch("/a", { dedupe: false }),
|
|
308
|
-
fch("/a"),
|
|
358
|
+
fch("/a"), // Reuses the 1st response, not the 2nd one
|
|
309
359
|
]);
|
|
310
360
|
|
|
311
361
|
expect(res).toEqual(["a", "b", "a"]);
|
|
@@ -317,6 +367,12 @@ it("can opt out locally", async () => {
|
|
|
317
367
|
|
|
318
368
|
You can also easily add the interceptors `before`, `after` and `error`:
|
|
319
369
|
|
|
370
|
+
- `before`: Called when the request is fully formed, but before actually launching it.
|
|
371
|
+
- `after`: Called just after the response is created and if there was no error, but before parsing anything else.
|
|
372
|
+
- `error`: When the response is not okay, if possible it'll include the `response` object.
|
|
373
|
+
|
|
374
|
+
> Note: interceptors are never deduped/cached and always execute once per call, even if the main async fetch() has been deduped.
|
|
375
|
+
|
|
320
376
|
```js
|
|
321
377
|
// Perform an action or request transformation before the request is sent
|
|
322
378
|
fch.before = async req => {
|
|
@@ -346,7 +402,6 @@ fch.error = async err => {
|
|
|
346
402
|
};
|
|
347
403
|
```
|
|
348
404
|
|
|
349
|
-
|
|
350
405
|
## How to
|
|
351
406
|
|
|
352
407
|
### Stop errors from throwing
|
|
@@ -354,10 +409,11 @@ fch.error = async err => {
|
|
|
354
409
|
While you can handle this on a per-request basis, if you want to overwrite the global behavior you can write a interceptor:
|
|
355
410
|
|
|
356
411
|
```js
|
|
357
|
-
import fch from
|
|
358
|
-
fch.
|
|
412
|
+
import fch from "fch";
|
|
413
|
+
fch.output = "response";
|
|
414
|
+
fch.error = (error) => error.response;
|
|
359
415
|
|
|
360
|
-
const res = await fch(
|
|
416
|
+
const res = await fch("/notfound");
|
|
361
417
|
expect(res.status).toBe(404);
|
|
362
418
|
```
|
|
363
419
|
|
|
@@ -366,75 +422,83 @@ expect(res.status).toBe(404);
|
|
|
366
422
|
By default a successful request will just return the data. However this one is configurable on a global level:
|
|
367
423
|
|
|
368
424
|
```js
|
|
369
|
-
import fch from
|
|
370
|
-
fch.output =
|
|
425
|
+
import fch from "fch";
|
|
426
|
+
fch.output = "response";
|
|
371
427
|
|
|
372
|
-
const res = await fch(
|
|
428
|
+
const res = await fch("/hello");
|
|
429
|
+
console.log(res.status);
|
|
373
430
|
```
|
|
374
431
|
|
|
375
432
|
Or on a per-request level:
|
|
376
433
|
|
|
377
434
|
```js
|
|
378
|
-
import fch from
|
|
435
|
+
import fch from "fch";
|
|
379
436
|
|
|
380
437
|
// Valid values are 'body' (default) or 'response'
|
|
381
|
-
const res = await fch(
|
|
438
|
+
const res = await fch("/hello", { output: "response" });
|
|
382
439
|
|
|
383
440
|
// Does not affect others
|
|
384
|
-
const body = await fch(
|
|
441
|
+
const body = await fch("/hello");
|
|
385
442
|
```
|
|
386
443
|
|
|
444
|
+
It does perform some basic parsing of the `body`, if you don't want any of that you can retrieve the very raw response:
|
|
445
|
+
|
|
446
|
+
```js
|
|
447
|
+
import fch from "fch";
|
|
448
|
+
|
|
449
|
+
// Valid values are 'body' (default) or 'response'
|
|
450
|
+
const res = await fch("/hello", { output: "raw" });
|
|
451
|
+
console.log(res.body); // ReadableStream
|
|
452
|
+
```
|
|
387
453
|
|
|
388
454
|
### Set a base URL
|
|
389
455
|
|
|
390
456
|
There's a configuration parameter for that:
|
|
391
457
|
|
|
392
458
|
```js
|
|
393
|
-
import fch from
|
|
394
|
-
fch.baseUrl =
|
|
459
|
+
import fch from "fch";
|
|
460
|
+
fch.baseUrl = "https://api.filemon.io/";
|
|
395
461
|
|
|
396
462
|
// Calls "https://api.filemon.io/blabla"
|
|
397
|
-
const body = await fch.get(
|
|
463
|
+
const body = await fch.get("/blabla");
|
|
398
464
|
```
|
|
399
465
|
|
|
400
|
-
|
|
401
466
|
### Set authorization headers
|
|
402
467
|
|
|
403
468
|
You can set that globally as a header:
|
|
404
469
|
|
|
405
470
|
```js
|
|
406
|
-
import fch from
|
|
407
|
-
fch.headers.Authorization =
|
|
471
|
+
import fch from "fch";
|
|
472
|
+
fch.headers.Authorization = "bearer abc";
|
|
408
473
|
|
|
409
|
-
const me = await fch(
|
|
474
|
+
const me = await fch("/users/me");
|
|
410
475
|
```
|
|
411
476
|
|
|
412
477
|
Or globally on a per-request basis, for example if you take the value from localStorage:
|
|
413
478
|
|
|
414
479
|
```js
|
|
415
|
-
import fch from
|
|
480
|
+
import fch from "fch";
|
|
416
481
|
|
|
417
482
|
// All the requests will add the Authorization header when the token is
|
|
418
483
|
// in localStorage
|
|
419
|
-
fch.before = req => {
|
|
420
|
-
if (localStorage.get(
|
|
421
|
-
req.headers.Authorization =
|
|
484
|
+
fch.before = (req) => {
|
|
485
|
+
if (localStorage.get("token")) {
|
|
486
|
+
req.headers.Authorization = "bearer " + localStorage.get("token");
|
|
422
487
|
}
|
|
423
488
|
return req;
|
|
424
489
|
};
|
|
425
490
|
|
|
426
|
-
const me = await fch(
|
|
491
|
+
const me = await fch("/users/me");
|
|
427
492
|
```
|
|
428
493
|
|
|
429
494
|
Or on a per-request basis, though we wouldn't recommend this:
|
|
430
495
|
|
|
431
496
|
```js
|
|
432
|
-
import fch from
|
|
497
|
+
import fch from "fch";
|
|
433
498
|
|
|
434
|
-
const me = await fch(
|
|
499
|
+
const me = await fch("/users/me", { headers: { Authorization: "bearer abc" } });
|
|
435
500
|
```
|
|
436
501
|
|
|
437
|
-
|
|
438
502
|
### Create an instance
|
|
439
503
|
|
|
440
504
|
You can create an instance with its own defaults and global options easily. It's common when writing an API that you want to encapsulate away:
|
|
@@ -453,28 +517,47 @@ fch.get('/hello'); // Gets http://localhost:3000/hello (or wherever you are)
|
|
|
453
517
|
|
|
454
518
|
Note: for server-side (Node.js) usage, you always want to set `baseUrl`.
|
|
455
519
|
|
|
520
|
+
### Streaming a response body
|
|
456
521
|
|
|
522
|
+
To stream the body, you need to use the `output: "stream"` option so that it returns a [WebStream ReadableStream](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream):
|
|
523
|
+
|
|
524
|
+
```js
|
|
525
|
+
import fch from "fch";
|
|
526
|
+
|
|
527
|
+
// Valid values are 'body' (default) or 'response'
|
|
528
|
+
const stream = await fch("/data", { output: "stream" });
|
|
529
|
+
stream.pipeTo(outStream);
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
You might want to convert it to a Node.js ReadStream:
|
|
533
|
+
|
|
534
|
+
```js
|
|
535
|
+
import fch from "fch";
|
|
536
|
+
import { Readable } from "node:stream";
|
|
537
|
+
|
|
538
|
+
const stream = await fch("/data", { output: "stream" });
|
|
539
|
+
const readableNodeStream = Readable.fromWeb(stream);
|
|
540
|
+
// ...
|
|
541
|
+
```
|
|
457
542
|
|
|
458
543
|
### Cancel ongoing requests
|
|
459
544
|
|
|
460
545
|
You can cancel ongoing requests [similarly to native fetch()](https://developer.mozilla.org/en-US/docs/Web/API/AbortController/abort#examples), by passing it a signal:
|
|
461
546
|
|
|
462
547
|
```js
|
|
463
|
-
import api from
|
|
548
|
+
import api from "fch";
|
|
464
549
|
|
|
465
550
|
const controller = new AbortController();
|
|
466
551
|
const signal = controller.signal;
|
|
467
552
|
|
|
468
|
-
abortButton.addEventListener(
|
|
553
|
+
abortButton.addEventListener("click", () => {
|
|
469
554
|
controller.abort();
|
|
470
|
-
console.log(
|
|
555
|
+
console.log("Download aborted");
|
|
471
556
|
});
|
|
472
557
|
|
|
473
558
|
api.get(url, { signal });
|
|
474
559
|
```
|
|
475
560
|
|
|
476
|
-
|
|
477
|
-
|
|
478
561
|
### Define shared options
|
|
479
562
|
|
|
480
563
|
You can also define values straight away:
|
|
@@ -490,28 +573,24 @@ console.log(mew);
|
|
|
490
573
|
|
|
491
574
|
You can also [create an instance](#create-an-instance) that will have the same options for all requests made with that instance.
|
|
492
575
|
|
|
493
|
-
|
|
494
|
-
|
|
495
576
|
### Node.js vs Browser
|
|
496
577
|
|
|
497
578
|
First, we use the native Node.js' fetch() and the browser's native fetch(), so any difference between those also applies to this library. For example, if you were to call `"/"` in the browser it'd refer to the current URL, while in Node.js it'd fail since you need to specify the full URL. Some other places where you might find differences: CORS, cache, etc.
|
|
498
579
|
|
|
499
580
|
In the library itself there's nothing different between the browser and Node.js, but it might be interesting to note that (if/when implemented) things like cache, etc. in Node.js are normally long-lived and shared, while in a browser request it'd bound to the request itself.
|
|
500
581
|
|
|
501
|
-
|
|
502
|
-
|
|
503
582
|
### Differences with Axios
|
|
504
583
|
|
|
505
584
|
The main difference is that things are simplified with fch:
|
|
506
585
|
|
|
507
586
|
```js
|
|
508
587
|
// Modify headers
|
|
509
|
-
axios.defaults.headers.Authorization =
|
|
510
|
-
fch.headers.Authorization =
|
|
588
|
+
axios.defaults.headers.Authorization = "...";
|
|
589
|
+
fch.headers.Authorization = "...";
|
|
511
590
|
|
|
512
591
|
// Set a base URL
|
|
513
|
-
axios.defaults.baseURL =
|
|
514
|
-
fch.baseUrl =
|
|
592
|
+
axios.defaults.baseURL = "...";
|
|
593
|
+
fch.baseUrl = "...";
|
|
515
594
|
|
|
516
595
|
// Add an interceptor
|
|
517
596
|
axios.interceptors.request.use(fn);
|
|
@@ -524,3 +603,17 @@ As disadvantages, I can think of two major ones for `fch`:
|
|
|
524
603
|
|
|
525
604
|
- Requires Node.js 18+, which is the version that includes `fetch()` by default.
|
|
526
605
|
- Does not support many of the more advanced options, like `onUploadProgress` nor `onDownloadProgress`.
|
|
606
|
+
|
|
607
|
+
## Releases
|
|
608
|
+
|
|
609
|
+
### V4
|
|
610
|
+
|
|
611
|
+
Breaking changes:
|
|
612
|
+
|
|
613
|
+
- Only ESM exports. Meaning, if you use it in a browser you'll need the `<script type="module">`.
|
|
614
|
+
- The method `fch.del()` (and derivates with fch.create()) have been renamed to `fch.delete()`.
|
|
615
|
+
|
|
616
|
+
Changes:
|
|
617
|
+
|
|
618
|
+
- Added `output` options: `raw`, `stream`, `arrayBuffer`, `blob`, `clone`, `formData`, `json`, `text`
|
|
619
|
+
- Gone from 1.2kb down to 1.0kb
|
package/fetch.js
DELETED
|
@@ -1,202 +0,0 @@
|
|
|
1
|
-
// Plain-ish object
|
|
2
|
-
const hasPlainBody = (options) => {
|
|
3
|
-
if (options.headers["content-type"]) return;
|
|
4
|
-
if (!["object", "array"].includes(typeof options.body)) return;
|
|
5
|
-
if (options.body instanceof FormData) return;
|
|
6
|
-
return true;
|
|
7
|
-
};
|
|
8
|
-
|
|
9
|
-
const createUrl = (url, query, base) => {
|
|
10
|
-
let [path, urlQuery = {}] = url.split("?");
|
|
11
|
-
|
|
12
|
-
// Merge global params with passed params with url params
|
|
13
|
-
const entries = new URLSearchParams({
|
|
14
|
-
...Object.fromEntries(new URLSearchParams(query)),
|
|
15
|
-
...Object.fromEntries(new URLSearchParams(urlQuery)),
|
|
16
|
-
}).toString();
|
|
17
|
-
if (entries) {
|
|
18
|
-
path = path + "?" + entries;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
if (!base) return path;
|
|
22
|
-
const fullUrl = new URL(path, base);
|
|
23
|
-
return fullUrl.href;
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
const createHeaders = (user, base) => {
|
|
27
|
-
// User-set headers overwrite the base headers
|
|
28
|
-
const headers = { ...base, ...user };
|
|
29
|
-
|
|
30
|
-
// Make the headers lowercase
|
|
31
|
-
for (let key in headers) {
|
|
32
|
-
const value = headers[key];
|
|
33
|
-
delete headers[key];
|
|
34
|
-
headers[key.toLowerCase()] = value;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
return headers;
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
const createDedupe = (ongoing, url) => ({
|
|
41
|
-
save: (prom) => {
|
|
42
|
-
ongoing.set(url, prom);
|
|
43
|
-
return prom;
|
|
44
|
-
},
|
|
45
|
-
get: () => ongoing.get(url),
|
|
46
|
-
clear: () => ongoing.delete(url),
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
const createFetch = (request, { after, dedupe, error, output }) => {
|
|
50
|
-
return fetch(request.url, request).then(async (res) => {
|
|
51
|
-
// No longer ongoing at this point
|
|
52
|
-
if (dedupe) dedupe.clear();
|
|
53
|
-
|
|
54
|
-
// Need to manually create it to set some things like the proper response
|
|
55
|
-
let response = {
|
|
56
|
-
status: res.status,
|
|
57
|
-
statusText: res.statusText,
|
|
58
|
-
headers: {},
|
|
59
|
-
};
|
|
60
|
-
for (let key of res.headers.keys()) {
|
|
61
|
-
response.headers[key.toLowerCase()] = res.headers.get(key);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Oops, throw it
|
|
65
|
-
if (!res.ok) {
|
|
66
|
-
const err = new Error(res.statusText);
|
|
67
|
-
err.response = response;
|
|
68
|
-
return error(err);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// Automatically parse the response
|
|
72
|
-
const type = res.headers.get("content-type");
|
|
73
|
-
const isJson = type && type.includes("application/json");
|
|
74
|
-
response.body = await (isJson ? res.json() : res.text());
|
|
75
|
-
|
|
76
|
-
// Hijack the response and modify it
|
|
77
|
-
if (after) {
|
|
78
|
-
response = after(response);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
if (output === "body") {
|
|
82
|
-
return response.body;
|
|
83
|
-
} else {
|
|
84
|
-
return response;
|
|
85
|
-
}
|
|
86
|
-
});
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
const create = (defaults = {}) => {
|
|
90
|
-
// DEDUPLICATION is created on a per-instance basis
|
|
91
|
-
// To avoid making parallel requests to the same url if one is ongoing
|
|
92
|
-
const ongoing = new Map();
|
|
93
|
-
|
|
94
|
-
const fch = async (url, options = {}) => {
|
|
95
|
-
// Second parameter always has to be an object, even when it defaults
|
|
96
|
-
if (typeof options !== "object") options = {};
|
|
97
|
-
|
|
98
|
-
// Accept either fch(options) or fch(url, options)
|
|
99
|
-
options = typeof url === "string" ? { url, ...options } : url || {};
|
|
100
|
-
|
|
101
|
-
// Exctract the options
|
|
102
|
-
let {
|
|
103
|
-
dedupe = fch.dedupe,
|
|
104
|
-
output = fch.output,
|
|
105
|
-
baseURL = fch.baseURL, // DO NOT USE; it's here only for user friendliness
|
|
106
|
-
baseUrl = baseURL || fch.baseUrl,
|
|
107
|
-
|
|
108
|
-
// Extract it since it should not be part of fetch()
|
|
109
|
-
query = {},
|
|
110
|
-
|
|
111
|
-
// Interceptors can also be passed as parameters
|
|
112
|
-
before = fch.before,
|
|
113
|
-
after = fch.after,
|
|
114
|
-
error = fch.error,
|
|
115
|
-
|
|
116
|
-
...request
|
|
117
|
-
} = options; // Local option OR global value (including defaults)
|
|
118
|
-
|
|
119
|
-
// Merge it, first the global and then the local
|
|
120
|
-
query = { ...fch.query, ...query };
|
|
121
|
-
// Absolute URL if possible; Default method; merge the default headers
|
|
122
|
-
request.url = createUrl(request.url ?? fch.url, query, baseUrl);
|
|
123
|
-
request.method = (request.method ?? fch.method).toLowerCase();
|
|
124
|
-
request.headers = createHeaders(request.headers, fch.headers);
|
|
125
|
-
|
|
126
|
-
if (request.method !== "get") {
|
|
127
|
-
dedupe = false;
|
|
128
|
-
}
|
|
129
|
-
if (dedupe) {
|
|
130
|
-
dedupe = createDedupe(ongoing, request.url);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
if (!["body", "response"].includes(output)) {
|
|
134
|
-
const msg = `options.output needs to be either "body" (default) or "response", not "${output}"`;
|
|
135
|
-
throw new Error(msg);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// JSON-encode plain objects
|
|
139
|
-
if (hasPlainBody(request)) {
|
|
140
|
-
request.body = JSON.stringify(request.body);
|
|
141
|
-
request.headers["content-type"] = "application/json; charset=utf-8";
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// Hijack the requeset and modify it
|
|
145
|
-
if (before) {
|
|
146
|
-
request = before(request);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// It should be cached and it's not being manually manipulated
|
|
150
|
-
if (dedupe && !request.signal) {
|
|
151
|
-
// It's already cached! Just return it
|
|
152
|
-
if (dedupe.get()) return dedupe.get();
|
|
153
|
-
|
|
154
|
-
// Otherwise, save it in the cache and return the promise
|
|
155
|
-
return dedupe.save(
|
|
156
|
-
createFetch(request, { dedupe, output, error, after })
|
|
157
|
-
);
|
|
158
|
-
} else {
|
|
159
|
-
// PUT, POST, etc should never dedupe and just return the plain request
|
|
160
|
-
return createFetch(request, { output, error, after });
|
|
161
|
-
}
|
|
162
|
-
};
|
|
163
|
-
|
|
164
|
-
// Default values
|
|
165
|
-
fch.url = defaults.url ?? "/";
|
|
166
|
-
fch.method = defaults.method ?? "get";
|
|
167
|
-
fch.query = defaults.query ?? {};
|
|
168
|
-
fch.headers = defaults.headers ?? {};
|
|
169
|
-
fch.baseUrl = defaults.baseUrl ?? defaults.baseURL ?? null;
|
|
170
|
-
|
|
171
|
-
// Default options
|
|
172
|
-
fch.dedupe = defaults.dedupe ?? true;
|
|
173
|
-
fch.output = defaults.output ?? "body";
|
|
174
|
-
fch.credentials = defaults.credentials ?? "include";
|
|
175
|
-
|
|
176
|
-
// Interceptors
|
|
177
|
-
fch.before = defaults.before ?? ((request) => request);
|
|
178
|
-
fch.after = defaults.after ?? ((response) => response);
|
|
179
|
-
fch.error = defaults.error ?? ((error) => Promise.reject(error));
|
|
180
|
-
|
|
181
|
-
const get = (url, opts = {}) => fch(url, { ...opts });
|
|
182
|
-
const head = (url, opts = {}) => fch(url, { ...opts, method: "head" });
|
|
183
|
-
const post = (url, opts = {}) => fch(url, { ...opts, method: "post" });
|
|
184
|
-
const patch = (url, opts = {}) => fch(url, { ...opts, method: "patch" });
|
|
185
|
-
const put = (url, opts = {}) => fch(url, { ...opts, method: "put" });
|
|
186
|
-
const del = (url, opts = {}) => fch(url, { ...opts, method: "delete" });
|
|
187
|
-
|
|
188
|
-
fch.get = get;
|
|
189
|
-
fch.head = head;
|
|
190
|
-
fch.post = post;
|
|
191
|
-
fch.patch = patch;
|
|
192
|
-
fch.put = put;
|
|
193
|
-
fch.del = del;
|
|
194
|
-
|
|
195
|
-
fch.create = create;
|
|
196
|
-
|
|
197
|
-
return fch;
|
|
198
|
-
};
|
|
199
|
-
|
|
200
|
-
const fch = create();
|
|
201
|
-
|
|
202
|
-
export default fch;
|
package/fetch.min.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
const hasPlainBody=e=>{if(!e.headers["content-type"]&&["object","array"].includes(typeof e.body)&&!(e.body instanceof FormData))return!0},createUrl=(e,t,r)=>{let[o,a={}]=e.split("?");const s=new URLSearchParams({...Object.fromEntries(new URLSearchParams(t)),...Object.fromEntries(new URLSearchParams(a))}).toString();if(s&&(o=o+"?"+s),!r)return o;return new URL(o,r).href},createHeaders=(e,t)=>{const r={...t,...e};for(let e in r){const t=r[e];delete r[e],r[e.toLowerCase()]=t}return r},createDedupe=(e,t)=>({save:r=>(e.set(t,r),r),get:()=>e.get(t),clear:()=>e.delete(t)}),createFetch=(e,{after:t,dedupe:r,error:o,output:a})=>fetch(e.url,e).then((async e=>{r&&r.clear();let s={status:e.status,statusText:e.statusText,headers:{}};for(let t of e.headers.keys())s.headers[t.toLowerCase()]=e.headers.get(t);if(!e.ok){const t=new Error(e.statusText);return t.response=s,o(t)}const d=e.headers.get("content-type"),n=d&&d.includes("application/json");return s.body=await(n?e.json():e.text()),t&&(s=t(s)),"body"===a?s.body:s})),create=(e={})=>{const t=new Map,r=async(e,o={})=>{"object"!=typeof o&&(o={}),o="string"==typeof e?{url:e,...o}:e||{};let{dedupe:a=r.dedupe,output:s=r.output,baseURL:d=r.baseURL,baseUrl:n=d||r.baseUrl,query:u={},before:c=r.before,after:h=r.after,error:l=r.error,...p}=o;if(u={...r.query,...u},p.url=createUrl(p.url??r.url,u,n),p.method=(p.method??r.method).toLowerCase(),p.headers=createHeaders(p.headers,r.headers),"get"!==p.method&&(a=!1),a&&(a=createDedupe(t,p.url)),!["body","response"].includes(s)){throw new Error(`options.output needs to be either "body" (default) or "response", not "${s}"`)}return hasPlainBody(p)&&(p.body=JSON.stringify(p.body),p.headers["content-type"]="application/json; charset=utf-8"),c&&(p=c(p)),a&&!p.signal?a.get()?a.get():a.save(createFetch(p,{dedupe:a,output:s,error:l,after:h})):createFetch(p,{output:s,error:l,after:h})};r.url=e.url??"/",r.method=e.method??"get",r.query=e.query??{},r.headers=e.headers??{},r.baseUrl=e.baseUrl??e.baseURL??null,r.dedupe=e.dedupe??!0,r.output=e.output??"body",r.credentials=e.credentials??"include",r.before=e.before??(e=>e),r.after=e.after??(e=>e),r.error=e.error??(e=>Promise.reject(e));return r.get=(e,t={})=>r(e,{...t}),r.head=(e,t={})=>r(e,{...t,method:"head"}),r.post=(e,t={})=>r(e,{...t,method:"post"}),r.patch=(e,t={})=>r(e,{...t,method:"patch"}),r.put=(e,t={})=>r(e,{...t,method:"put"}),r.del=(e,t={})=>r(e,{...t,method:"delete"}),r.create=create,r},fch=create();window.fch=fch;
|