fch 2.0.0 → 3.0.1
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/fetch.js +137 -90
- package/package.json +7 -5
- package/readme.md +350 -73
- package/fetch.test.js +0 -370
package/fetch.js
CHANGED
|
@@ -1,20 +1,26 @@
|
|
|
1
|
-
import swear from "swear";
|
|
2
|
-
|
|
3
|
-
// To avoid making parallel requests to the same url if one is ongoing
|
|
4
|
-
const ongoing = new Map();
|
|
5
|
-
|
|
6
1
|
// Plain-ish object
|
|
7
2
|
const hasPlainBody = (options) => {
|
|
8
3
|
if (options.headers["content-type"]) return;
|
|
9
|
-
if (typeof options.body
|
|
4
|
+
if (!["object", "array"].includes(typeof options.body)) return;
|
|
10
5
|
if (options.body instanceof FormData) return;
|
|
11
6
|
return true;
|
|
12
7
|
};
|
|
13
8
|
|
|
14
|
-
const createUrl = (
|
|
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
|
+
|
|
15
21
|
if (!base) return path;
|
|
16
|
-
const
|
|
17
|
-
return
|
|
22
|
+
const fullUrl = new URL(path, base);
|
|
23
|
+
return fullUrl.href;
|
|
18
24
|
};
|
|
19
25
|
|
|
20
26
|
const createHeaders = (user, base) => {
|
|
@@ -31,6 +37,15 @@ const createHeaders = (user, base) => {
|
|
|
31
37
|
return headers;
|
|
32
38
|
};
|
|
33
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
|
+
|
|
34
49
|
const createFetch = (request, { after, dedupe, error, output }) => {
|
|
35
50
|
return fetch(request.url, request).then(async (res) => {
|
|
36
51
|
// No longer ongoing at this point
|
|
@@ -70,90 +85,122 @@ const createFetch = (request, { after, dedupe, error, output }) => {
|
|
|
70
85
|
});
|
|
71
86
|
};
|
|
72
87
|
|
|
73
|
-
const
|
|
74
|
-
//
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
if (hasPlainBody(request)) {
|
|
117
|
-
request.body = JSON.stringify(request.body);
|
|
118
|
-
request.headers["content-type"] = "application/json; charset=utf-8";
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// Hijack the requeset and modify it
|
|
122
|
-
if (before) {
|
|
123
|
-
request = before(request);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// It should be cached
|
|
127
|
-
if (dedupe) {
|
|
128
|
-
// It's already cached! Just return it
|
|
129
|
-
if (dedupe.get()) return dedupe.get();
|
|
88
|
+
const create = (defaults = {}) => {
|
|
89
|
+
// DEDUPLICATION is created on a per-instance basis
|
|
90
|
+
// To avoid making parallel requests to the same url if one is ongoing
|
|
91
|
+
const ongoing = new Map();
|
|
92
|
+
|
|
93
|
+
const fch = async (url, options = {}) => {
|
|
94
|
+
// Second parameter always has to be an object, even when it defaults
|
|
95
|
+
if (typeof options !== "object") options = {};
|
|
96
|
+
|
|
97
|
+
// Accept either fch(options) or fch(url, options)
|
|
98
|
+
options = typeof url === "string" ? { url, ...options } : url || {};
|
|
99
|
+
|
|
100
|
+
// Exctract the options
|
|
101
|
+
let {
|
|
102
|
+
dedupe = fch.dedupe,
|
|
103
|
+
output = fch.output,
|
|
104
|
+
baseURL = fch.baseURL, // DO NOT USE; it's here only for user friendliness
|
|
105
|
+
baseUrl = baseURL || fch.baseUrl,
|
|
106
|
+
|
|
107
|
+
// Extract it since it should not be part of fetch()
|
|
108
|
+
query = {},
|
|
109
|
+
|
|
110
|
+
// Interceptors can also be passed as parameters
|
|
111
|
+
before = fch.before,
|
|
112
|
+
after = fch.after,
|
|
113
|
+
error = fch.error,
|
|
114
|
+
|
|
115
|
+
...request
|
|
116
|
+
} = options; // Local option OR global value (including defaults)
|
|
117
|
+
|
|
118
|
+
// Merge it, first the global and then the local
|
|
119
|
+
query = { ...fch.query, ...query };
|
|
120
|
+
// Absolute URL if possible; Default method; merge the default headers
|
|
121
|
+
request.url = createUrl(request.url || "/", query, baseUrl);
|
|
122
|
+
request.method = (request.method ?? fch.method).toLowerCase();
|
|
123
|
+
request.headers = createHeaders(request.headers, fch.headers);
|
|
124
|
+
|
|
125
|
+
if (request.method !== "get") {
|
|
126
|
+
dedupe = false;
|
|
127
|
+
}
|
|
128
|
+
if (dedupe) {
|
|
129
|
+
dedupe = createDedupe(ongoing, request.url);
|
|
130
|
+
}
|
|
130
131
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
return createFetch(request, { output, error, after });
|
|
136
|
-
}
|
|
137
|
-
});
|
|
132
|
+
if (!["body", "response"].includes(output)) {
|
|
133
|
+
const msg = `options.output needs to be either "body" (default) or "response", not "${output}"`;
|
|
134
|
+
throw new Error(msg);
|
|
135
|
+
}
|
|
138
136
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
fch.del = (url, options = {}) => fch(url, { ...options, method: "delete" });
|
|
137
|
+
// JSON-encode plain objects
|
|
138
|
+
if (hasPlainBody(request)) {
|
|
139
|
+
request.body = JSON.stringify(request.body);
|
|
140
|
+
request.headers["content-type"] = "application/json; charset=utf-8";
|
|
141
|
+
}
|
|
145
142
|
|
|
146
|
-
//
|
|
147
|
-
|
|
148
|
-
|
|
143
|
+
// Hijack the requeset and modify it
|
|
144
|
+
if (before) {
|
|
145
|
+
request = before(request);
|
|
146
|
+
}
|
|
149
147
|
|
|
150
|
-
//
|
|
151
|
-
|
|
152
|
-
|
|
148
|
+
// It should be cached and it's not being manually manipulated
|
|
149
|
+
if (dedupe && !request.signal) {
|
|
150
|
+
// It's already cached! Just return it
|
|
151
|
+
if (dedupe.get()) return dedupe.get();
|
|
153
152
|
|
|
154
|
-
//
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
153
|
+
// Otherwise, save it in the cache and return the promise
|
|
154
|
+
return dedupe.save(
|
|
155
|
+
createFetch(request, { dedupe, output, error, after })
|
|
156
|
+
);
|
|
157
|
+
} else {
|
|
158
|
+
// PUT, POST, etc should never dedupe and just return the plain request
|
|
159
|
+
return createFetch(request, { output, error, after });
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
// Default values
|
|
164
|
+
fch.method = defaults.method ?? "get";
|
|
165
|
+
fch.query = defaults.query ?? {};
|
|
166
|
+
fch.headers = defaults.headers ?? {};
|
|
167
|
+
|
|
168
|
+
// Default options
|
|
169
|
+
fch.dedupe = defaults.dedupe ?? true;
|
|
170
|
+
fch.output = defaults.output ?? "body";
|
|
171
|
+
fch.credentials = defaults.credentials ?? "include";
|
|
172
|
+
|
|
173
|
+
// Interceptors
|
|
174
|
+
fch.before = defaults.before ?? ((request) => request);
|
|
175
|
+
fch.after = defaults.after ?? ((response) => response);
|
|
176
|
+
fch.error = defaults.error ?? ((error) => Promise.reject(error));
|
|
177
|
+
|
|
178
|
+
const get = (url, opts = {}) => fch(url, { ...opts });
|
|
179
|
+
const head = (url, opts = {}) => fch(url, { ...opts, method: "head" });
|
|
180
|
+
const post = (url, opts = {}) => fch(url, { ...opts, method: "post" });
|
|
181
|
+
const patch = (url, opts = {}) => fch(url, { ...opts, method: "patch" });
|
|
182
|
+
const put = (url, opts = {}) => fch(url, { ...opts, method: "put" });
|
|
183
|
+
const del = (url, opts = {}) => fch(url, { ...opts, method: "delete" });
|
|
184
|
+
|
|
185
|
+
fch.get = get;
|
|
186
|
+
fch.head = head;
|
|
187
|
+
fch.post = post;
|
|
188
|
+
fch.patch = patch;
|
|
189
|
+
fch.put = put;
|
|
190
|
+
fch.del = del;
|
|
191
|
+
|
|
192
|
+
return fch;
|
|
193
|
+
};
|
|
158
194
|
|
|
159
|
-
export default
|
|
195
|
+
// Need to export it globally with `global`, since if we use export default then
|
|
196
|
+
// we cannot load it in the browser as a normal <script>, and if we load it in
|
|
197
|
+
// the browser as a <script module> then we cannot run a normal script after it
|
|
198
|
+
// since the modules are deferred by default. Basically this is a big mess and
|
|
199
|
+
// I wish I could just do if `(typeof window !== 'undefined') window.fch = fch`,
|
|
200
|
+
// but unfortunately that's not possible now and I need this as a traditionally
|
|
201
|
+
// global definition, and then another file to import it as ESM. UGHHH
|
|
202
|
+
let glob = {};
|
|
203
|
+
if (typeof global !== "undefined") glob = global;
|
|
204
|
+
if (typeof window !== "undefined") glob = window;
|
|
205
|
+
glob.fch = create();
|
|
206
|
+
glob.fch.create = create;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fch",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.1",
|
|
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",
|
|
@@ -21,14 +21,16 @@
|
|
|
21
21
|
"async",
|
|
22
22
|
"ajax"
|
|
23
23
|
],
|
|
24
|
-
"main": "
|
|
24
|
+
"main": "./index.js",
|
|
25
|
+
"browser": "./fetch.js",
|
|
26
|
+
"files": [
|
|
27
|
+
"fetch.js"
|
|
28
|
+
],
|
|
25
29
|
"type": "module",
|
|
26
30
|
"engines": {
|
|
27
31
|
"node": ">=18.0.0"
|
|
28
32
|
},
|
|
29
|
-
"dependencies": {
|
|
30
|
-
"swear": "^1.1.0"
|
|
31
|
-
},
|
|
33
|
+
"dependencies": {},
|
|
32
34
|
"devDependencies": {
|
|
33
35
|
"jest": "^28.0.1",
|
|
34
36
|
"jest-fetch-mock": "^3.0.3"
|
package/readme.md
CHANGED
|
@@ -1,31 +1,48 @@
|
|
|
1
1
|
# Fch [](https://www.npmjs.com/package/fch) [](https://github.com/franciscop/fetch/blob/master/fetch.js)
|
|
2
2
|
|
|
3
|
-
A library to make API calls easier. Similar to Axios, but tiny size and simpler API:
|
|
3
|
+
A tiny library to make API calls easier. Similar to Axios, but tiny size and simpler API:
|
|
4
4
|
|
|
5
5
|
```js
|
|
6
6
|
import api from "fch";
|
|
7
|
-
|
|
8
|
-
api.baseUrl = "https://pokeapi.co/";
|
|
9
|
-
|
|
10
|
-
const mew = await api("/pokemon/150");
|
|
7
|
+
const mew = await api.get("https://pokeapi.co/pokemon/150");
|
|
11
8
|
console.log(mew);
|
|
12
9
|
```
|
|
13
10
|
|
|
14
|
-
`
|
|
15
|
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
19
|
-
-
|
|
20
|
-
- Better error handling. `>= 400 and <= 100` will _reject_ the promise with an error instance.
|
|
21
|
-
- Advanced [Promise interface](https://www.npmjs.com/swear) for better scripting.
|
|
22
|
-
- Import with the shorthand for tighter syntax. `import { get, post } from 'fch';`.
|
|
23
|
-
- Easily define shared options straight on the root `fetch.baseUrl = "https://...";`.
|
|
11
|
+
- Automatically `JSON.stringify()` and `Content-Type: 'application/json'` for objects.
|
|
12
|
+
- Automatically parse server response taking into account the headers.
|
|
13
|
+
- Works the same way in Node.js and the browser.
|
|
14
|
+
- Await/Async Promises. `>= 400 and <= 100` will _reject_ the promise and throw an error.
|
|
15
|
+
- No dependencies; include it with a simple `<script>`
|
|
16
|
+
- Easily define shared options straight on the root `fch.baseUrl = "https://...";`.
|
|
24
17
|
- Interceptors: `before` (the request), `after` (the response) and `error` (it fails).
|
|
25
18
|
- Deduplicates parallel GET requests.
|
|
26
19
|
- Configurable to return either just the body, or the full response.
|
|
27
|
-
|
|
28
|
-
|
|
20
|
+
|
|
21
|
+
```js
|
|
22
|
+
import api from 'fch';
|
|
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
|
+
```
|
|
33
|
+
|
|
34
|
+
| Options | Default | Description |
|
|
35
|
+
|-----------|--------------------|---------------------------------------------|
|
|
36
|
+
| `url` | `null` | The path or full url for the request |
|
|
37
|
+
| `baseUrl` | `null` | The shared base of the API |
|
|
38
|
+
| `method` | `"get"` | Default method to use for the call |
|
|
39
|
+
| `query` | `{}` | Add query parameters to the URL |
|
|
40
|
+
| `headers` | `{}` | Shared headers across all requests |
|
|
41
|
+
| `dedupe` | `true` | Reuse GET requests made concurrently |
|
|
42
|
+
| `output` | `"body"` | The return value of the API call |
|
|
43
|
+
| `before` | `req => req` | Process the request before sending it |
|
|
44
|
+
| `after` | `res => res` | Process the response before returning it |
|
|
45
|
+
| `error` | `err => throw err` | Process errors before returning them |
|
|
29
46
|
|
|
30
47
|
## Getting Started
|
|
31
48
|
|
|
@@ -37,85 +54,227 @@ npm install fch
|
|
|
37
54
|
|
|
38
55
|
Then import it to be able to use it in your code:
|
|
39
56
|
|
|
57
|
+
```js
|
|
58
|
+
import fch from 'fch';
|
|
59
|
+
const body = await fch.get('/');
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
On the browser you can add it with a script and it will be available as `fch`:
|
|
63
|
+
|
|
64
|
+
```html
|
|
65
|
+
<!-- Import it as usual -->
|
|
66
|
+
<script src="https://cdn.jsdelivr.net/npm/fch"></script>
|
|
67
|
+
<script>
|
|
68
|
+
fch('/hello');
|
|
69
|
+
</script>
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Options
|
|
73
|
+
|
|
40
74
|
```js
|
|
41
75
|
import api from 'fch';
|
|
42
76
|
|
|
43
|
-
|
|
77
|
+
// General options with their defaults; all of these are also parameters:
|
|
78
|
+
api.baseUrl = null; // Set an API base URL reused all across requests
|
|
79
|
+
api.method = 'get'; // Default method to use for api()
|
|
80
|
+
api.query = {}; // Is merged with the query parameters passed manually
|
|
81
|
+
api.headers = {}; // Is merged with the headers on a per-request basis
|
|
82
|
+
|
|
83
|
+
// Control simple variables
|
|
84
|
+
api.dedupe = true; // Avoid parallel GET requests to the same path
|
|
85
|
+
api.output = 'body'; // Return the body; use 'response' for the full response
|
|
86
|
+
|
|
87
|
+
// Interceptors
|
|
88
|
+
api.before = req => req;
|
|
89
|
+
api.after = res => res;
|
|
90
|
+
api.error = err => Promise.reject(err);
|
|
91
|
+
|
|
92
|
+
// Similar API to fetch()
|
|
93
|
+
api(url, { method, body, headers, ... });
|
|
94
|
+
|
|
95
|
+
// Our highly recommended style:
|
|
96
|
+
api.get(url, { headers, ... });
|
|
97
|
+
api.post(url, { body, headers, ... });
|
|
98
|
+
api.put(url, { body, headers, ... });
|
|
99
|
+
// ...
|
|
44
100
|
```
|
|
45
101
|
|
|
102
|
+
### Method
|
|
46
103
|
|
|
47
|
-
|
|
104
|
+
The HTTP method to make the request. When using the shorthand, it defaults to `GET`. We recommend using the method syntax:
|
|
48
105
|
|
|
49
|
-
|
|
106
|
+
```js
|
|
107
|
+
import api from 'fch';
|
|
108
|
+
|
|
109
|
+
api.get('/cats');
|
|
110
|
+
api.post('/cats', { body: { name: 'snowball' } });
|
|
111
|
+
api.put(`/cats/3`, { body: { name: 'snowball' }});
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
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:
|
|
50
115
|
|
|
51
116
|
```js
|
|
52
|
-
|
|
117
|
+
// Recommended way of dealing with methods:
|
|
118
|
+
api.get(...);
|
|
119
|
+
|
|
120
|
+
// INVALID; won't work
|
|
121
|
+
api.GET(...);
|
|
53
122
|
|
|
54
|
-
//
|
|
55
|
-
|
|
56
|
-
|
|
123
|
+
// Both of these are valid:
|
|
124
|
+
api({ method; 'GET' })
|
|
125
|
+
api({ method; 'get'})
|
|
57
126
|
```
|
|
58
127
|
|
|
59
|
-
|
|
128
|
+
Example: adding a new cat and fixing a typo:
|
|
60
129
|
|
|
61
|
-
|
|
130
|
+
```js
|
|
131
|
+
import api from 'fch';
|
|
132
|
+
|
|
133
|
+
const cats = await api.get('/cats');
|
|
134
|
+
console.log(cats);
|
|
135
|
+
const { id } = await api.post('/cats', { body: { name: 'snowbll' } });
|
|
136
|
+
await api.put(`/cats/${id}`, { body: { name: 'snowball' }})
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Url
|
|
140
|
+
|
|
141
|
+
Specify where to send the request to. It's normally the first argument, though technically you can use both styles:
|
|
62
142
|
|
|
63
143
|
```js
|
|
64
|
-
import api from
|
|
144
|
+
import api from 'fch';
|
|
65
145
|
|
|
66
|
-
|
|
146
|
+
// Recommended way of specifying the Url
|
|
147
|
+
await api.post('/hello', { body: '...', headers: {} })
|
|
67
148
|
|
|
68
|
-
|
|
69
|
-
|
|
149
|
+
// These are also valid if you prefer their style; we won't judge
|
|
150
|
+
await api('/hello', { method: 'post', body: '...', headers: {} });
|
|
151
|
+
await api({ url: '/hello', method: 'post', headers: {}, body: '...' });
|
|
152
|
+
await api.post({ url: '/hello', headers: {}, body: '...' });
|
|
70
153
|
```
|
|
71
154
|
|
|
72
|
-
|
|
155
|
+
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`:
|
|
73
156
|
|
|
74
157
|
```js
|
|
75
|
-
|
|
76
|
-
api.
|
|
77
|
-
|
|
158
|
+
import api from 'fch';
|
|
159
|
+
api.baseUrl = 'https//api.filemon.io/';
|
|
160
|
+
api.get('/hello');
|
|
161
|
+
// Called https//api.filemon.io/hello
|
|
162
|
+
```
|
|
78
163
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
164
|
+
> Note: with Node.js you need to either set an absolute baseUrl or make the URL absolute
|
|
165
|
+
|
|
166
|
+
### Body
|
|
167
|
+
|
|
168
|
+
The `body` can be a string, a plain object|array or a FormData instance. If it's an array or object, it'll be stringified and the header `application/json` will be added. Otherwise it'll be sent as plain text:
|
|
169
|
+
|
|
170
|
+
```js
|
|
171
|
+
import api from 'api';
|
|
172
|
+
|
|
173
|
+
// Sending plain text
|
|
174
|
+
await api.post('/houses', { body: 'plain text' });
|
|
175
|
+
|
|
176
|
+
// Will JSON.stringify it internally, and add the JSON headers
|
|
177
|
+
await api.post('/houses', { body: { id: 1, name: 'Cute Cottage' } });
|
|
178
|
+
|
|
179
|
+
// Send it as FormData
|
|
180
|
+
form.onsubmit = e => {
|
|
181
|
+
await api.post('/houses', { body: new FormData(e.target) });
|
|
182
|
+
};
|
|
82
183
|
```
|
|
83
184
|
|
|
84
|
-
|
|
185
|
+
The methods `GET` and `HEAD` do not accept a body and it'll be ignored.
|
|
85
186
|
|
|
86
|
-
|
|
187
|
+
The **response body** will be returned by default as the output of the call:
|
|
87
188
|
|
|
88
189
|
```js
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
190
|
+
const body = await api.get('/cats');
|
|
191
|
+
console.log(body);
|
|
192
|
+
// [{ id: 1, }, ...]
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
When the server specifies the header `Content-Type` as `application/json`, then we'll attempt to parse the response body and return that as the variable. Otherwise, the plain text will be returned.
|
|
196
|
+
|
|
197
|
+
When the function returns the response (if you set `output: "response"` as an option), then the body can be accessed as `response.body`:
|
|
198
|
+
|
|
199
|
+
```js
|
|
200
|
+
const response = await api.get('/cats', { output: 'response' });
|
|
201
|
+
console.log(response.body);
|
|
202
|
+
// [{ id: 1, }, ...]
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
### Query
|
|
207
|
+
|
|
208
|
+
You can easily pass GET query parameters by using the option `query`:
|
|
209
|
+
|
|
210
|
+
```js
|
|
211
|
+
api.get('/cats', { query: { limit: 3 } });
|
|
212
|
+
// /cats?limit=3
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
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:
|
|
216
|
+
|
|
217
|
+
```js
|
|
218
|
+
import api from 'fch';
|
|
219
|
+
api.query.myparam = 'abc';
|
|
220
|
+
|
|
221
|
+
api.get('/cats', { query: { limit: 3 } });
|
|
222
|
+
// /cats?limit=3&myparam=abc
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
### Headers
|
|
227
|
+
|
|
228
|
+
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:
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
```js
|
|
232
|
+
import api from 'fch';
|
|
233
|
+
|
|
234
|
+
// Globally, so they are reused across all requests
|
|
235
|
+
api.headers.a = 'b';
|
|
236
|
+
|
|
237
|
+
// With an interceptor, in case you need dynamic headers per-request
|
|
238
|
+
api.before = req => {
|
|
239
|
+
req.headers.c = 'd';
|
|
93
240
|
return req;
|
|
94
241
|
};
|
|
95
242
|
|
|
96
|
-
//
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
};
|
|
243
|
+
// Set them for this single request:
|
|
244
|
+
api.get('/hello', { headers: { e: 'f' } });
|
|
245
|
+
// Total headers on the request:
|
|
246
|
+
// { a: 'b', c: 'd', e: 'f' }
|
|
247
|
+
```
|
|
102
248
|
|
|
103
|
-
|
|
104
|
-
fch.error = async err => {
|
|
105
|
-
// Need to re-throw if we want to throw on error
|
|
106
|
-
...
|
|
107
|
-
throw err;
|
|
249
|
+
When to use each?
|
|
108
250
|
|
|
109
|
-
|
|
110
|
-
|
|
251
|
+
- If you need headers shared across all requests, like an API key, then the global one is the best place.
|
|
252
|
+
- 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.
|
|
253
|
+
- When it changes on each request, it's not consistent or it's an one-off, use the option argument.
|
|
111
254
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
255
|
+
|
|
256
|
+
### Output
|
|
257
|
+
|
|
258
|
+
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:
|
|
259
|
+
|
|
260
|
+
```js
|
|
261
|
+
import api from 'fch';
|
|
262
|
+
|
|
263
|
+
// "body" (default) or "response"
|
|
264
|
+
api.output = 'body';
|
|
265
|
+
|
|
266
|
+
// Return only the body, this is the default
|
|
267
|
+
const body = await api.get('/data');
|
|
268
|
+
|
|
269
|
+
// Return the whole response (with .body):
|
|
270
|
+
const response = await api.get('/data', { output: 'response' });
|
|
271
|
+
|
|
272
|
+
// Throws error
|
|
273
|
+
const invalid = await api.get('/data', { output: 'invalid' });
|
|
115
274
|
```
|
|
116
275
|
|
|
117
276
|
|
|
118
|
-
|
|
277
|
+
### Dedupe
|
|
119
278
|
|
|
120
279
|
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:
|
|
121
280
|
|
|
@@ -139,7 +298,7 @@ fch('/a', { dedupe: true }); // [DEFAULT] Dedupes GET requests
|
|
|
139
298
|
fch('/a', { dedupe: false }) // All fetch() calls trigger a network call
|
|
140
299
|
```
|
|
141
300
|
|
|
142
|
-
> We do not support deduping other methods
|
|
301
|
+
> We do not support deduping other methods besides `GET` right now
|
|
143
302
|
|
|
144
303
|
Note that opting out of deduping a request will _also_ make that request not be reusable, see this test for details:
|
|
145
304
|
|
|
@@ -157,28 +316,77 @@ it("can opt out locally", async () => {
|
|
|
157
316
|
});
|
|
158
317
|
```
|
|
159
318
|
|
|
319
|
+
### Interceptors
|
|
160
320
|
|
|
161
|
-
|
|
321
|
+
You can also easily add the interceptors `before`, `after` and `error`:
|
|
162
322
|
|
|
163
|
-
|
|
323
|
+
```js
|
|
324
|
+
// Perform an action or request transformation before the request is sent
|
|
325
|
+
fch.before = async req => {
|
|
326
|
+
// Normalized request ready to be sent
|
|
327
|
+
...
|
|
328
|
+
return req;
|
|
329
|
+
};
|
|
164
330
|
|
|
165
|
-
|
|
331
|
+
// Perform an action or data transformation after the request is finished
|
|
332
|
+
fch.after = async res => {
|
|
333
|
+
// Full response as just after the request is made
|
|
334
|
+
...
|
|
335
|
+
return res;
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
// Perform an action or data transformation when an error is thrown
|
|
339
|
+
fch.error = async err => {
|
|
340
|
+
// Need to re-throw if we want to throw on error
|
|
341
|
+
...
|
|
342
|
+
throw err;
|
|
343
|
+
|
|
344
|
+
// OR, resolve it as if it didn't fail
|
|
345
|
+
return err.response;
|
|
346
|
+
|
|
347
|
+
// OR, resolve it with a custom value
|
|
348
|
+
return { message: 'Request failed with a code ' + err.response.status };
|
|
349
|
+
};
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
### Define shared options
|
|
354
|
+
|
|
355
|
+
You can also define values straight away:
|
|
166
356
|
|
|
167
357
|
```js
|
|
168
|
-
import
|
|
169
|
-
fch.error = error => error.response;
|
|
358
|
+
import api from "fch";
|
|
170
359
|
|
|
171
|
-
|
|
172
|
-
|
|
360
|
+
api.baseUrl = "https://pokeapi.co/";
|
|
361
|
+
|
|
362
|
+
const mew = await api.get("/pokemon/150");
|
|
363
|
+
console.log(mew);
|
|
173
364
|
```
|
|
174
365
|
|
|
175
|
-
|
|
366
|
+
If you prefer Axios' style of outputting the whole response, you can do:
|
|
367
|
+
|
|
368
|
+
```js
|
|
369
|
+
// Default, already only returns the data on a successful call
|
|
370
|
+
api.output = "data";
|
|
371
|
+
const name = await api.get("/users/1").name;
|
|
372
|
+
|
|
373
|
+
// Axios-like
|
|
374
|
+
api.output = "response";
|
|
375
|
+
const name = await api.get("/users/1").data.name;
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
## How to
|
|
380
|
+
|
|
381
|
+
### Stop errors from throwing
|
|
382
|
+
|
|
383
|
+
While you can handle this on a per-request basis, if you want to overwrite the global behavior you can write a interceptor:
|
|
176
384
|
|
|
177
385
|
```js
|
|
178
386
|
import fch from 'fch';
|
|
179
387
|
fch.error = error => error.response;
|
|
180
388
|
|
|
181
|
-
const res = fch('/notfound');
|
|
389
|
+
const res = await fch('/notfound');
|
|
182
390
|
expect(res.status).toBe(404);
|
|
183
391
|
```
|
|
184
392
|
|
|
@@ -214,8 +422,8 @@ There's a configuration parameter for that:
|
|
|
214
422
|
import fch from 'fch';
|
|
215
423
|
fch.baseUrl = 'https://api.filemon.io/';
|
|
216
424
|
|
|
217
|
-
// Calls "https://api.filemon.io/blabla
|
|
218
|
-
const body = await fch.get('/blabla
|
|
425
|
+
// Calls "https://api.filemon.io/blabla"
|
|
426
|
+
const body = await fch.get('/blabla');
|
|
219
427
|
```
|
|
220
428
|
|
|
221
429
|
|
|
@@ -230,7 +438,7 @@ fch.headers.Authorization = 'bearer abc';
|
|
|
230
438
|
const me = await fch('/users/me');
|
|
231
439
|
```
|
|
232
440
|
|
|
233
|
-
|
|
441
|
+
Or globally on a per-request basis, for example if you take the value from localStorage:
|
|
234
442
|
|
|
235
443
|
```js
|
|
236
444
|
import fch from 'fch';
|
|
@@ -254,3 +462,72 @@ import fch from 'fch';
|
|
|
254
462
|
|
|
255
463
|
const me = await fch('/users/me', { headers: { Authorization: 'bearer abc' } });
|
|
256
464
|
```
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
### Create an instance
|
|
468
|
+
|
|
469
|
+
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:
|
|
470
|
+
|
|
471
|
+
```js
|
|
472
|
+
import fch from 'fch';
|
|
473
|
+
|
|
474
|
+
const api = fch.create({
|
|
475
|
+
baseUrl: 'https://api.filemon.io/',
|
|
476
|
+
...
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
api.get('/hello'); // Gets https://api.filemon.io/hello
|
|
480
|
+
fch.get('/hello'); // Gets http://localhost:3000/hello (or wherever you are)
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
Note: for server-side (Node.js) usage, you always want to set `baseUrl`.
|
|
484
|
+
|
|
485
|
+
### How to cancel an ongoing request?
|
|
486
|
+
|
|
487
|
+
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:
|
|
488
|
+
|
|
489
|
+
```js
|
|
490
|
+
import api from 'fch';
|
|
491
|
+
|
|
492
|
+
const controller = new AbortController();
|
|
493
|
+
const signal = controller.signal;
|
|
494
|
+
|
|
495
|
+
abortButton.addEventListener('click', () => {
|
|
496
|
+
controller.abort();
|
|
497
|
+
console.log('Download aborted');
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
api.get(url, { signal });
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
### What are the differences in Node.js vs Browser?
|
|
504
|
+
|
|
505
|
+
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.
|
|
506
|
+
|
|
507
|
+
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.
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
### What are the differences with Axios?
|
|
511
|
+
|
|
512
|
+
The main difference is that things are simplified with fch:
|
|
513
|
+
|
|
514
|
+
```js
|
|
515
|
+
// Modify headers
|
|
516
|
+
axios.defaults.headers.Authorization = '...';
|
|
517
|
+
fch.headers.Authorization = '...';
|
|
518
|
+
|
|
519
|
+
// Set a base URL
|
|
520
|
+
axios.defaults.baseURL = '...';
|
|
521
|
+
fch.baseUrl = '...';
|
|
522
|
+
|
|
523
|
+
// Add an interceptor
|
|
524
|
+
axios.interceptors.request.use(fn);
|
|
525
|
+
fch.before = fn;
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
API size is also strikingly different, with **7.8kb** for Axios and **1.9kb** for fch.
|
|
529
|
+
|
|
530
|
+
As disadvantages, I can think of two major ones for `fch`:
|
|
531
|
+
|
|
532
|
+
- Requires Node.js 18+, which is the version that includes `fetch()` by default.
|
|
533
|
+
- Does not support many of the more advanced options, like `onUploadProgress` nor `onDownloadProgress`.
|
package/fetch.test.js
DELETED
|
@@ -1,370 +0,0 @@
|
|
|
1
|
-
import fch from "./fetch.js";
|
|
2
|
-
import mock from "jest-fetch-mock";
|
|
3
|
-
|
|
4
|
-
mock.enableMocks();
|
|
5
|
-
|
|
6
|
-
const jsonHeaders = { headers: { "Content-Type": "application/json" } };
|
|
7
|
-
|
|
8
|
-
describe("fetch()", () => {
|
|
9
|
-
beforeEach(() => {
|
|
10
|
-
fetch.resetMocks();
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
it("can create a basic request", async () => {
|
|
14
|
-
fetch.once("hello");
|
|
15
|
-
const data = await fch("/");
|
|
16
|
-
|
|
17
|
-
expect(data).toEqual("hello");
|
|
18
|
-
expect(fetch.mock.calls.length).toEqual(1);
|
|
19
|
-
expect(fetch.mock.calls[0][0]).toEqual("/");
|
|
20
|
-
expect(fetch.mock.calls[0][1].method).toEqual("get");
|
|
21
|
-
expect(fetch.mock.calls[0][1].headers).toEqual({});
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it("accepts Axios syntax as well", async () => {
|
|
25
|
-
fetch.once("hello");
|
|
26
|
-
const data = await fch({ url: "/" });
|
|
27
|
-
|
|
28
|
-
expect(data).toEqual("hello");
|
|
29
|
-
expect(fetch.mock.calls.length).toEqual(1);
|
|
30
|
-
expect(fetch.mock.calls[0][0]).toEqual("/");
|
|
31
|
-
expect(fetch.mock.calls[0][1].method).toEqual("get");
|
|
32
|
-
expect(fetch.mock.calls[0][1].headers).toEqual({});
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it("can receive a full response", async () => {
|
|
36
|
-
fetch.once("hello", { status: 200, headers: { hello: "world" } });
|
|
37
|
-
const res = await fch("/", { output: "response" });
|
|
38
|
-
|
|
39
|
-
expect(res.body).toEqual("hello");
|
|
40
|
-
expect(res.status).toEqual(200);
|
|
41
|
-
expect(res.statusText).toEqual("OK");
|
|
42
|
-
expect(res.headers.hello).toEqual("world");
|
|
43
|
-
expect(fetch.mock.calls.length).toEqual(1);
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it("works with JSON", async () => {
|
|
47
|
-
fetch.once(JSON.stringify({ secret: "12345" }), jsonHeaders);
|
|
48
|
-
const data = await fch("https://google.com/");
|
|
49
|
-
|
|
50
|
-
expect(data).toEqual({ secret: "12345" });
|
|
51
|
-
expect(fetch.mock.calls.length).toEqual(1);
|
|
52
|
-
expect(fetch.mock.calls[0][0]).toEqual("https://google.com/");
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it("can use the swear interface", async () => {
|
|
56
|
-
fetch.once(JSON.stringify({ secret: "12345" }), jsonHeaders);
|
|
57
|
-
const hello = await fch("/").secret;
|
|
58
|
-
expect(hello).toEqual("12345");
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it("can use the baseUrl", async () => {
|
|
62
|
-
fetch.once("hi");
|
|
63
|
-
fch.baseUrl = "https://google.com/";
|
|
64
|
-
const data = await fch.get("/hello");
|
|
65
|
-
expect(data).toBe("hi");
|
|
66
|
-
expect(fetch.mock.calls[0][0]).toBe("https://google.com/hello");
|
|
67
|
-
expect(fetch.mock.calls[0][1].method).toEqual("get");
|
|
68
|
-
fch.baseUrl = null;
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it("can use the `fetch.get()` shorthand", async () => {
|
|
72
|
-
fetch.once("my-data");
|
|
73
|
-
const data = await fch.get("/");
|
|
74
|
-
expect(data).toBe("my-data");
|
|
75
|
-
expect(fetch.mock.calls[0][1].method).toEqual("get");
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it("can use the `fetch.patch()` shorthand", async () => {
|
|
79
|
-
fetch.once("my-data");
|
|
80
|
-
expect(await fch.patch("/")).toBe("my-data");
|
|
81
|
-
expect(fetch.mock.calls[0][1].method).toEqual("patch");
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it("can use the `fetch.put()` shorthand", async () => {
|
|
85
|
-
fetch.once("my-data");
|
|
86
|
-
expect(await fch.put("/")).toBe("my-data");
|
|
87
|
-
expect(fetch.mock.calls[0][1].method).toEqual("put");
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it("can use the `fetch.post()` shorthand", async () => {
|
|
91
|
-
fetch.once("my-data");
|
|
92
|
-
expect(await fch.post("/")).toBe("my-data");
|
|
93
|
-
expect(fetch.mock.calls[0][1].method).toEqual("post");
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it("can use the `fetch.del()` shorthand", async () => {
|
|
97
|
-
fetch.once("my-data");
|
|
98
|
-
expect(await fch.del("/")).toBe("my-data");
|
|
99
|
-
expect(fetch.mock.calls[0][1].method).toEqual("delete");
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it("ignores invalid options", async () => {
|
|
103
|
-
fetch.once(JSON.stringify({ secret: "12345" }), jsonHeaders);
|
|
104
|
-
const res = await fch("https://google.com/", 10);
|
|
105
|
-
|
|
106
|
-
expect(res).toEqual({ secret: "12345" });
|
|
107
|
-
expect(fetch.mock.calls.length).toEqual(1);
|
|
108
|
-
expect(fetch.mock.calls[0][0]).toEqual("https://google.com/");
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
it("will not overwrite if it is FormData", async () => {
|
|
112
|
-
fetch.once(JSON.stringify({ secret: "12345" }), jsonHeaders);
|
|
113
|
-
const res = await fch("/", { method: "post", body: new FormData() });
|
|
114
|
-
|
|
115
|
-
expect(res).toEqual({ secret: "12345" });
|
|
116
|
-
expect(fetch.mock.calls.length).toEqual(1);
|
|
117
|
-
const [url, opts] = fetch.mock.calls[0];
|
|
118
|
-
expect(opts).toMatchObject({ body: expect.any(FormData) });
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it("will not overwrite if content-type is set", async () => {
|
|
122
|
-
fetch.once(JSON.stringify({ secret: "12345" }), jsonHeaders);
|
|
123
|
-
const res = await fch("/", {
|
|
124
|
-
method: "POST",
|
|
125
|
-
body: { a: "b" },
|
|
126
|
-
headers: { "Content-Type": "xxx" },
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
expect(res).toEqual({ secret: "12345" });
|
|
130
|
-
expect(fetch.mock.calls.length).toEqual(1);
|
|
131
|
-
const [url, opts] = fetch.mock.calls[0];
|
|
132
|
-
expect(url).toEqual("/");
|
|
133
|
-
expect(opts).toMatchObject({
|
|
134
|
-
method: "post",
|
|
135
|
-
body: { a: "b" },
|
|
136
|
-
headers: { "content-type": "xxx" },
|
|
137
|
-
});
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
it("will send JSON", async () => {
|
|
141
|
-
fetch.once(JSON.stringify({ secret: "12345" }), jsonHeaders);
|
|
142
|
-
const res = await fch("/", { method: "POST", body: { a: "b" } });
|
|
143
|
-
|
|
144
|
-
expect(res).toEqual({ secret: "12345" });
|
|
145
|
-
expect(fetch.mock.calls.length).toEqual(1);
|
|
146
|
-
const [url, opts] = fetch.mock.calls[0];
|
|
147
|
-
expect(url).toEqual("/");
|
|
148
|
-
expect(opts).toMatchObject({
|
|
149
|
-
method: "post",
|
|
150
|
-
body: '{"a":"b"}',
|
|
151
|
-
headers: { "content-type": "application/json; charset=utf-8" },
|
|
152
|
-
});
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
it("can run in parallel", async () => {
|
|
156
|
-
fetch.once("a").once("b");
|
|
157
|
-
const res = await Promise.all([fch("/a"), fch("/b")]);
|
|
158
|
-
|
|
159
|
-
expect(res).toEqual(["a", "b"]);
|
|
160
|
-
expect(fetch.mock.calls.length).toEqual(2);
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
it("can set `accepts` insensitively", async () => {
|
|
164
|
-
fetch.once(JSON.stringify({ secret: "12345" }), jsonHeaders);
|
|
165
|
-
const res = await fch("/", { headers: { Accepts: "text/xml" } });
|
|
166
|
-
|
|
167
|
-
expect(fetch.mock.calls[0][1].headers).toEqual({ accepts: "text/xml" });
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
it("can accept network rejections", async () => {
|
|
171
|
-
fetch.mockResponseOnce(JSON.stringify("unauthorized"), {
|
|
172
|
-
status: 401,
|
|
173
|
-
ok: false,
|
|
174
|
-
});
|
|
175
|
-
await expect(fch("/")).rejects.toMatchObject({
|
|
176
|
-
message: "Unauthorized",
|
|
177
|
-
});
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
it("throws with the wrong 'output' option", async () => {
|
|
181
|
-
fetch.once("hello");
|
|
182
|
-
await expect(fch("/", { output: "abc" })).rejects.toMatchObject({
|
|
183
|
-
message: `options.output needs to be either "body" (default) or "response", not "abc"`,
|
|
184
|
-
});
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
it("can accept rejections", async () => {
|
|
188
|
-
fetch.mockRejectOnce(new Error("fake error message"));
|
|
189
|
-
await expect(fch("/error")).rejects.toMatchObject({
|
|
190
|
-
message: "fake error message",
|
|
191
|
-
});
|
|
192
|
-
});
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
describe("interceptors", () => {
|
|
196
|
-
beforeEach(() => {
|
|
197
|
-
fetch.resetMocks();
|
|
198
|
-
});
|
|
199
|
-
afterEach(() => {
|
|
200
|
-
fetch.resetMocks();
|
|
201
|
-
delete fch.before;
|
|
202
|
-
delete fch.after;
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
it("can create a before interceptor", async () => {
|
|
206
|
-
fetch.once("hello");
|
|
207
|
-
const data = await fch("/", {
|
|
208
|
-
before: (req) => {
|
|
209
|
-
req.url = "/hello";
|
|
210
|
-
req.method = "put";
|
|
211
|
-
return req;
|
|
212
|
-
},
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
expect(data).toEqual("hello");
|
|
216
|
-
expect(fetch.mock.calls.length).toEqual(1);
|
|
217
|
-
expect(fetch.mock.calls[0][0]).toEqual("/hello");
|
|
218
|
-
expect(fetch.mock.calls[0][1].method).toEqual("put");
|
|
219
|
-
expect(fetch.mock.calls[0][1].headers).toEqual({});
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
it("can create a global before interceptor", async () => {
|
|
223
|
-
fetch.once("hello");
|
|
224
|
-
fch.before = (req) => {
|
|
225
|
-
req.url = "/hello";
|
|
226
|
-
req.method = "put";
|
|
227
|
-
return req;
|
|
228
|
-
};
|
|
229
|
-
const data = await fch("/");
|
|
230
|
-
|
|
231
|
-
expect(data).toEqual("hello");
|
|
232
|
-
expect(fetch.mock.calls.length).toEqual(1);
|
|
233
|
-
expect(fetch.mock.calls[0][0]).toEqual("/hello");
|
|
234
|
-
expect(fetch.mock.calls[0][1].method).toEqual("put");
|
|
235
|
-
expect(fetch.mock.calls[0][1].headers).toEqual({});
|
|
236
|
-
|
|
237
|
-
delete fch.before;
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
it("can create an after interceptor", async () => {
|
|
241
|
-
fetch.once("hello", { status: 201, headers: { hello: "world" } });
|
|
242
|
-
const res = await fch("/", {
|
|
243
|
-
output: "response",
|
|
244
|
-
after: (res) => {
|
|
245
|
-
res.body = "bye";
|
|
246
|
-
res.status = 200;
|
|
247
|
-
res.headers.hello = "world";
|
|
248
|
-
return res;
|
|
249
|
-
},
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
expect(res.body).toEqual("bye");
|
|
253
|
-
expect(res.status).toEqual(200);
|
|
254
|
-
expect(res.statusText).toEqual("Created");
|
|
255
|
-
expect(res.headers.hello).toEqual("world");
|
|
256
|
-
expect(fetch.mock.calls.length).toEqual(1);
|
|
257
|
-
expect(fetch.mock.calls[0][0]).toEqual("/");
|
|
258
|
-
expect(fetch.mock.calls[0][1].method).toEqual("get");
|
|
259
|
-
expect(fetch.mock.calls[0][1].headers).toEqual({});
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
it("can create a global after interceptor", async () => {
|
|
263
|
-
fetch.once("hello", { status: 201, headers: { hello: "world" } });
|
|
264
|
-
fch.after = (res) => {
|
|
265
|
-
res.body = "bye";
|
|
266
|
-
res.status = 200;
|
|
267
|
-
res.headers.hello = "world";
|
|
268
|
-
return res;
|
|
269
|
-
};
|
|
270
|
-
const res = await fch("/", { output: "response" });
|
|
271
|
-
|
|
272
|
-
expect(res.body).toEqual("bye");
|
|
273
|
-
expect(res.status).toEqual(200);
|
|
274
|
-
expect(res.statusText).toEqual("Created");
|
|
275
|
-
expect(res.headers.hello).toEqual("world");
|
|
276
|
-
expect(fetch.mock.calls.length).toEqual(1);
|
|
277
|
-
expect(fetch.mock.calls[0][0]).toEqual("/");
|
|
278
|
-
expect(fetch.mock.calls[0][1].method).toEqual("get");
|
|
279
|
-
expect(fetch.mock.calls[0][1].headers).toEqual({});
|
|
280
|
-
});
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
describe("dedupe network calls", () => {
|
|
284
|
-
beforeEach(() => {
|
|
285
|
-
fetch.resetMocks();
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
it("dedupes ongoing/parallel calls", async () => {
|
|
289
|
-
fetch.once("a").once("b");
|
|
290
|
-
const res = await Promise.all([fch("/a"), fch("/a")]);
|
|
291
|
-
|
|
292
|
-
expect(res).toEqual(["a", "a"]);
|
|
293
|
-
expect(fetch.mock.calls.length).toEqual(1);
|
|
294
|
-
});
|
|
295
|
-
|
|
296
|
-
it("dedupes named calls", async () => {
|
|
297
|
-
fetch.once("a").once("b");
|
|
298
|
-
const res = await Promise.all([fch.get("/a"), fch.get("/a")]);
|
|
299
|
-
|
|
300
|
-
expect(res).toEqual(["a", "a"]);
|
|
301
|
-
expect(fetch.mock.calls.length).toEqual(1);
|
|
302
|
-
});
|
|
303
|
-
|
|
304
|
-
it("can opt out locally", async () => {
|
|
305
|
-
fetch.once("a").once("b");
|
|
306
|
-
const res = await Promise.all([fch("/a"), fch("/a", { dedupe: false })]);
|
|
307
|
-
|
|
308
|
-
expect(res).toEqual(["a", "b"]);
|
|
309
|
-
expect(fetch.mock.calls.length).toEqual(2);
|
|
310
|
-
});
|
|
311
|
-
|
|
312
|
-
it("cannot reuse an opted out call", async () => {
|
|
313
|
-
fetch.once("a").once("b");
|
|
314
|
-
const res = await Promise.all([fch("/a", { dedupe: false }), fch("/a")]);
|
|
315
|
-
|
|
316
|
-
expect(res).toEqual(["a", "b"]);
|
|
317
|
-
expect(fetch.mock.calls.length).toEqual(2);
|
|
318
|
-
});
|
|
319
|
-
|
|
320
|
-
it("will reuse the last opt-in call", async () => {
|
|
321
|
-
fetch.once("a").once("b").once("c");
|
|
322
|
-
const res = await Promise.all([
|
|
323
|
-
fch("/a"),
|
|
324
|
-
fch("/a", { dedupe: false }),
|
|
325
|
-
fch("/a"),
|
|
326
|
-
]);
|
|
327
|
-
|
|
328
|
-
expect(res).toEqual(["a", "b", "a"]);
|
|
329
|
-
expect(fetch.mock.calls.length).toEqual(2);
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
it("can opt out globally", async () => {
|
|
333
|
-
fetch.once("a").once("b").once("c");
|
|
334
|
-
fch.dedupe = false;
|
|
335
|
-
const res = await Promise.all([fch("/a"), fch("/a"), fch("/a")]);
|
|
336
|
-
|
|
337
|
-
expect(res).toEqual(["a", "b", "c"]);
|
|
338
|
-
expect(fetch.mock.calls.length).toEqual(3);
|
|
339
|
-
fch.dedupe = true;
|
|
340
|
-
});
|
|
341
|
-
|
|
342
|
-
it("does NOT dedupe/cache serial requests", async () => {
|
|
343
|
-
fetch.once("a").once("b");
|
|
344
|
-
const resa = await fch("/a");
|
|
345
|
-
const resb = await fch("/a");
|
|
346
|
-
|
|
347
|
-
expect([resa, resb]).toEqual(["a", "b"]);
|
|
348
|
-
expect(fetch.mock.calls.length).toEqual(2);
|
|
349
|
-
});
|
|
350
|
-
|
|
351
|
-
it("does NOT dedupe/cache other request types", async () => {
|
|
352
|
-
fetch.once("a").once("b");
|
|
353
|
-
const res = await Promise.all([fch.get("/a"), fch.post("/a")]);
|
|
354
|
-
|
|
355
|
-
expect(res).toEqual(["a", "b"]);
|
|
356
|
-
expect(fetch.mock.calls.length).toEqual(2);
|
|
357
|
-
});
|
|
358
|
-
|
|
359
|
-
it("NOT deduping are invisible for other request", async () => {
|
|
360
|
-
fetch.once("a").once("b").once("c");
|
|
361
|
-
const res = await Promise.all([
|
|
362
|
-
fch.get("/a"),
|
|
363
|
-
fch.post("/a"),
|
|
364
|
-
fch.get("/a"),
|
|
365
|
-
]);
|
|
366
|
-
|
|
367
|
-
expect(res).toEqual(["a", "b", "a"]);
|
|
368
|
-
expect(fetch.mock.calls.length).toEqual(2);
|
|
369
|
-
});
|
|
370
|
-
});
|