fch 1.0.0 → 3.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/fetch.js +181 -55
- package/package.json +33 -27
- package/readme.md +384 -45
- package/.babelrc +0 -3
- package/fetch.min.js +0 -1
- package/fetch.test.js +0 -146
package/fetch.js
CHANGED
|
@@ -1,34 +1,31 @@
|
|
|
1
|
-
import swear from './node_modules/swear/swear.js';
|
|
2
|
-
|
|
3
|
-
if (typeof require !== 'undefined') {
|
|
4
|
-
require('isomorphic-fetch');
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
// To avoid making parallel requests to the same url if one is ongoing
|
|
8
|
-
const ongoing = new Map();
|
|
9
|
-
|
|
10
1
|
// Plain-ish object
|
|
11
|
-
const hasPlainBody = options => {
|
|
12
|
-
if (options.headers[
|
|
13
|
-
if (typeof options.body
|
|
2
|
+
const hasPlainBody = (options) => {
|
|
3
|
+
if (options.headers["content-type"]) return;
|
|
4
|
+
if (!["object", "array"].includes(typeof options.body)) return;
|
|
14
5
|
if (options.body instanceof FormData) return;
|
|
15
6
|
return true;
|
|
16
7
|
};
|
|
17
8
|
|
|
18
|
-
const
|
|
9
|
+
const createUrl = (url, query, base) => {
|
|
10
|
+
let [path, urlQuery = {}] = url.split("?");
|
|
19
11
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
...(
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
if (options.method.toLowerCase() === 'get') {
|
|
28
|
-
if (ongoing.get(url)) return ongoing.get(url);
|
|
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;
|
|
29
19
|
}
|
|
30
20
|
|
|
31
|
-
|
|
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 };
|
|
32
29
|
|
|
33
30
|
// Make the headers lowercase
|
|
34
31
|
for (let key in headers) {
|
|
@@ -37,44 +34,173 @@ const fch = (url, options = {}) => {
|
|
|
37
34
|
headers[key.toLowerCase()] = value;
|
|
38
35
|
}
|
|
39
36
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
options.body = JSON.stringify(options.body);
|
|
43
|
-
headers['content-type'] = 'application/json; charset=utf-8';
|
|
44
|
-
}
|
|
37
|
+
return headers;
|
|
38
|
+
};
|
|
45
39
|
|
|
46
|
-
|
|
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) => {
|
|
47
51
|
// No longer ongoing at this point
|
|
48
|
-
|
|
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
|
+
}
|
|
49
63
|
|
|
50
|
-
//
|
|
64
|
+
// Oops, throw it
|
|
51
65
|
if (!res.ok) {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
error
|
|
55
|
-
|
|
66
|
+
const err = new Error(res.statusText);
|
|
67
|
+
err.response = response;
|
|
68
|
+
return error(err);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Automatically parse the response
|
|
72
|
+
const isJson = res.headers.get("content-type").includes("application/json");
|
|
73
|
+
response.body = await (isJson ? res.json() : res.text());
|
|
74
|
+
|
|
75
|
+
// Hijack the response and modify it
|
|
76
|
+
if (after) {
|
|
77
|
+
response = after(response);
|
|
56
78
|
}
|
|
57
79
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
return
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
65
|
-
return mem.get(key);
|
|
66
|
-
};
|
|
67
|
-
}});
|
|
68
|
-
})));
|
|
69
|
-
|
|
70
|
-
return ongoing.get(url);
|
|
80
|
+
if (output === "body") {
|
|
81
|
+
return response.body;
|
|
82
|
+
} else {
|
|
83
|
+
return response;
|
|
84
|
+
}
|
|
85
|
+
});
|
|
71
86
|
};
|
|
72
87
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
fch
|
|
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
|
+
}
|
|
131
|
+
|
|
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
|
+
}
|
|
136
|
+
|
|
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
|
+
}
|
|
142
|
+
|
|
143
|
+
// Hijack the requeset and modify it
|
|
144
|
+
if (before) {
|
|
145
|
+
request = before(request);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// It should be cached
|
|
149
|
+
if (dedupe) {
|
|
150
|
+
// It's already cached! Just return it
|
|
151
|
+
if (dedupe.get()) return dedupe.get();
|
|
152
|
+
|
|
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
|
+
};
|
|
79
194
|
|
|
80
|
-
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,36 +1,42 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fch",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "Fetch interface with
|
|
5
|
-
"
|
|
6
|
-
"
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
"test": "npm run build && jest --coverage --detectOpenHandles",
|
|
10
|
-
"watch": "nodemon --exec \"npm run build && npm test && npm run gzip\" --watch src --watch test --watch webpack.config.js --watch package.json",
|
|
11
|
-
"gzip": "gzip -c fetch.min.js | wc -c && echo 'bytes' # Only for Unix"
|
|
12
|
-
},
|
|
13
|
-
"keywords": [],
|
|
3
|
+
"version": "3.0.0",
|
|
4
|
+
"description": "Fetch interface with better promises, deduplication, defaults, etc.",
|
|
5
|
+
"homepage": "https://github.com/franciscop/fetch",
|
|
6
|
+
"repository": "https://github.com/franciscop/fetch.git",
|
|
7
|
+
"bugs": "https://github.com/franciscop/fetch/issues",
|
|
8
|
+
"funding": "https://www.paypal.me/franciscopresencia/19",
|
|
14
9
|
"author": "Francisco Presencia <public@francisco.io> (https://francisco.io/)",
|
|
15
10
|
"license": "MIT",
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
|
|
20
|
-
"bugs": {
|
|
21
|
-
"url": "https://github.com/franciscop/fetch/issues"
|
|
11
|
+
"scripts": {
|
|
12
|
+
"start": "npm run watch # Start ~= Start dev",
|
|
13
|
+
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage --detectOpenHandles",
|
|
14
|
+
"watch": "node --experimental-vm-modules node_modules/jest/bin/jest.js --watch --coverage --detectOpenHandles"
|
|
22
15
|
},
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"
|
|
16
|
+
"keywords": [
|
|
17
|
+
"fetch",
|
|
18
|
+
"axios",
|
|
19
|
+
"http",
|
|
20
|
+
"https",
|
|
21
|
+
"async",
|
|
22
|
+
"ajax"
|
|
23
|
+
],
|
|
24
|
+
"main": "./index.js",
|
|
25
|
+
"browser": "./fetch.js",
|
|
26
|
+
"files": [
|
|
27
|
+
"fetch.js"
|
|
28
|
+
],
|
|
29
|
+
"type": "module",
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=18.0.0"
|
|
26
32
|
},
|
|
33
|
+
"dependencies": {},
|
|
27
34
|
"devDependencies": {
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"uglify-es": "^3.3.9"
|
|
35
|
+
"jest": "^28.0.1",
|
|
36
|
+
"jest-fetch-mock": "^3.0.3"
|
|
37
|
+
},
|
|
38
|
+
"jest": {
|
|
39
|
+
"testEnvironment": "jest-environment-node",
|
|
40
|
+
"transform": {}
|
|
35
41
|
}
|
|
36
42
|
}
|
package/readme.md
CHANGED
|
@@ -1,81 +1,420 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Fch [](https://www.npmjs.com/package/fch) [](https://github.com/franciscop/fetch/blob/master/fetch.js)
|
|
2
2
|
|
|
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
|
-
import
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const url = 'https://api.jsonbin.io/b/5bc69ae7716f9364f8c58651';
|
|
10
|
-
|
|
11
|
-
(async () => {
|
|
12
|
-
// Using the Swear interface
|
|
13
|
-
const name = await fch(url).json().name;
|
|
14
|
-
console.log(name); // "Francisco"
|
|
15
|
-
|
|
16
|
-
// Using plain-old promises
|
|
17
|
-
const data = await fch(url).then(res => res.json());
|
|
18
|
-
console.log(data.name); // "Francisco"
|
|
19
|
-
})();
|
|
6
|
+
import api from "fch";
|
|
7
|
+
const mew = await api("https://pokeapi.co/pokemon/150");
|
|
8
|
+
console.log(mew);
|
|
20
9
|
```
|
|
21
10
|
|
|
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://...";`.
|
|
17
|
+
- Interceptors: `before` (the request), `after` (the response) and `error` (it fails).
|
|
18
|
+
- Deduplicates parallel GET requests.
|
|
19
|
+
- Configurable to return either just the body, or the full response.
|
|
22
20
|
|
|
21
|
+
```js
|
|
22
|
+
// Calls and methods available:
|
|
23
|
+
api(url, { method, body, headers, ...options })
|
|
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
|
+
fch.create({ url, body, headers, ...options})
|
|
31
|
+
```
|
|
23
32
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
+
|Options/variables |Default |Description |
|
|
34
|
+
|------------------|---------------|-------------------------------------------|
|
|
35
|
+
|`url` |`null` |The path or full url for the request |
|
|
36
|
+
|`api.baseUrl` |`null` |The shared base of the API |
|
|
37
|
+
|`api.method` |`"get"` |Default method to use for the call |
|
|
38
|
+
|`api.query` |`{}` |Add query parameters to the URL |
|
|
39
|
+
|`api.headers` |`{}` |Shared headers across all requests |
|
|
40
|
+
|`api.dedupe` |`true` |Reuse GET requests made concurrently |
|
|
41
|
+
|`api.output` |`"body"` |The return value of the API call |
|
|
42
|
+
|`api.before` |`req => req` |Process the request before sending it |
|
|
43
|
+
|`api.after` |`res => res` |Process the response before receiving it |
|
|
44
|
+
|`api.error` |`err => reject(err)` |Process errors before returning them |
|
|
33
45
|
|
|
34
|
-
## Getting
|
|
46
|
+
## Getting Started
|
|
35
47
|
|
|
36
48
|
Install it in your project:
|
|
37
49
|
|
|
38
|
-
```
|
|
50
|
+
```bash
|
|
39
51
|
npm install fch
|
|
40
52
|
```
|
|
41
53
|
|
|
42
54
|
Then import it to be able to use it in your code:
|
|
43
55
|
|
|
44
56
|
```js
|
|
45
|
-
|
|
46
|
-
|
|
57
|
+
import fch from 'fch';
|
|
58
|
+
const body = await fch.get('/');
|
|
47
59
|
```
|
|
48
60
|
|
|
49
|
-
|
|
61
|
+
On the browser you can add it with a script and it will be available as `fch`:
|
|
50
62
|
|
|
51
63
|
```html
|
|
64
|
+
<!-- Import it as usual -->
|
|
52
65
|
<script src="https://cdn.jsdelivr.net/npm/fch"></script>
|
|
53
66
|
<script>
|
|
54
|
-
|
|
67
|
+
fch('/hello');
|
|
55
68
|
</script>
|
|
56
69
|
```
|
|
57
70
|
|
|
71
|
+
## Options
|
|
72
|
+
|
|
73
|
+
```js
|
|
74
|
+
import api from 'fch';
|
|
75
|
+
|
|
76
|
+
// General options with their defaults; most of these are also parameters:
|
|
77
|
+
api.baseUrl = null; // Set an API endpoint
|
|
78
|
+
api.method = 'get'; // Default method to use for api()
|
|
79
|
+
api.headers = {}; // Is merged with the headers on a per-request basis
|
|
80
|
+
|
|
81
|
+
// Control simple variables
|
|
82
|
+
api.dedupe = true; // Avoid parallel GET requests to the same path
|
|
83
|
+
api.output = 'body'; // Return the body; use 'response' for the full response
|
|
84
|
+
|
|
85
|
+
// Interceptors
|
|
86
|
+
api.before = req => req;
|
|
87
|
+
api.after = res => res;
|
|
88
|
+
api.error = err => Promise.reject(err);
|
|
89
|
+
|
|
90
|
+
// Similar API to fetch()
|
|
91
|
+
api(url, { method, body, headers, ... });
|
|
92
|
+
|
|
93
|
+
// Our highly recommended style:
|
|
94
|
+
api.get(url, { headers, ... });
|
|
95
|
+
api.post(url, { body, headers, ... });
|
|
96
|
+
api.put(url, { body, headers, ... });
|
|
97
|
+
// ...
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### URL
|
|
101
|
+
|
|
102
|
+
This is normally the first argument, though technically you can use both styles:
|
|
103
|
+
|
|
104
|
+
```js
|
|
105
|
+
// All of these methods are valid
|
|
106
|
+
import api from 'fch';
|
|
107
|
+
|
|
108
|
+
// We strongly recommend using this style for your normal code:
|
|
109
|
+
await api.post('/hello', { body: '...', headers: {} })
|
|
110
|
+
|
|
111
|
+
// Try to avoid these, but they are also valid:
|
|
112
|
+
await api('/hello', { method: 'post', body: '...', headers: {} });
|
|
113
|
+
await api({ url: '/hello', method: 'post', headers: {}, body: '...' });
|
|
114
|
+
await api.post({ url: '/hello', headers: {}, body: '...' });
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
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`:
|
|
118
|
+
|
|
119
|
+
```js
|
|
120
|
+
import api from 'fch';
|
|
121
|
+
api.baseUrl = 'https//api.filemon.io/';
|
|
122
|
+
api.get('/hello');
|
|
123
|
+
// Called https//api.filemon.io/hello
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Body
|
|
127
|
+
|
|
128
|
+
The `body` can be a string, a plain object|array or a FormData instance. If it's an object, it'll be stringified and the header `application/json` will be added. Otherwise it'll be sent as plain text:
|
|
129
|
+
|
|
130
|
+
```js
|
|
131
|
+
import api from 'api';
|
|
132
|
+
|
|
133
|
+
// Sending plain text
|
|
134
|
+
await api.post('/houses', { body: 'plain text' });
|
|
135
|
+
|
|
136
|
+
// Will JSON.stringify it internally, and add the JSON headers
|
|
137
|
+
await api.post('/houses', { body: { id: 1, name: 'Cute Cottage' } });
|
|
138
|
+
|
|
139
|
+
// Send it as FormData
|
|
140
|
+
form.onsubmit = e => {
|
|
141
|
+
await api.post('/houses', { body: new FormData(e.target) });
|
|
142
|
+
};
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
### Headers
|
|
147
|
+
|
|
148
|
+
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:
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
```js
|
|
152
|
+
import api from 'fch';
|
|
153
|
+
api.headers.abc = 'def';
|
|
154
|
+
|
|
155
|
+
api.get('/helle', { headers: { ghi: 'jkl' } });
|
|
156
|
+
// Total headers on the request:
|
|
157
|
+
// { abc: 'def', ghi: 'jkl' }
|
|
158
|
+
```
|
|
159
|
+
|
|
58
160
|
|
|
161
|
+
### Output
|
|
59
162
|
|
|
60
|
-
|
|
163
|
+
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:
|
|
61
164
|
|
|
62
|
-
|
|
165
|
+
```js
|
|
166
|
+
import api from 'fch';
|
|
167
|
+
|
|
168
|
+
// "body" (default) or "response"
|
|
169
|
+
api.output = 'body';
|
|
170
|
+
|
|
171
|
+
// Return only the body, this is the default
|
|
172
|
+
const body = await api.get('/data');
|
|
173
|
+
|
|
174
|
+
// Return the whole response (with .body):
|
|
175
|
+
const response = await api.get('/data', { output: 'response' });
|
|
176
|
+
|
|
177
|
+
// Throws error
|
|
178
|
+
const invalid = await api.get('/data', { output: 'invalid' });
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
### Dedupe
|
|
183
|
+
|
|
184
|
+
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:
|
|
63
185
|
|
|
64
186
|
```js
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
187
|
+
fetch.mockOnce("a").mockOnce("b");
|
|
188
|
+
const res = await Promise.all([fch("/a"), fch("/a")]);
|
|
189
|
+
|
|
190
|
+
// Reuses the first response if two are launched in parallel
|
|
191
|
+
expect(res).toEqual(["a", "a"]);
|
|
69
192
|
```
|
|
70
193
|
|
|
194
|
+
You can disable this by setting either the global `fch.dedupe` option to `false` or by passing an option per request:
|
|
195
|
+
|
|
71
196
|
```js
|
|
72
|
-
//
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
197
|
+
// Globally set it for all calls
|
|
198
|
+
fch.dedupe = true; // [DEFAULT] Dedupes GET requests
|
|
199
|
+
fch.dedupe = false; // All fetch() calls trigger a network call
|
|
200
|
+
|
|
201
|
+
// Set it on a per-call basis
|
|
202
|
+
fch('/a', { dedupe: true }); // [DEFAULT] Dedupes GET requests
|
|
203
|
+
fch('/a', { dedupe: false }) // All fetch() calls trigger a network call
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
> We do not support deduping other methods right now besides `GET` right now
|
|
207
|
+
|
|
208
|
+
Note that opting out of deduping a request will _also_ make that request not be reusable, see this test for details:
|
|
209
|
+
|
|
210
|
+
```js
|
|
211
|
+
it("can opt out locally", async () => {
|
|
212
|
+
fetch.once("a").once("b").once("c");
|
|
213
|
+
const res = await Promise.all([
|
|
214
|
+
fch("/a"),
|
|
215
|
+
fch("/a", { dedupe: false }),
|
|
216
|
+
fch("/a"), // Reuses the 1st response, not the 2nd one
|
|
217
|
+
]);
|
|
218
|
+
|
|
219
|
+
expect(res).toEqual(["a", "b", "a"]);
|
|
220
|
+
expect(fetch.mock.calls.length).toEqual(2);
|
|
77
221
|
});
|
|
78
|
-
if (!res.ok) throw new Error(res.statusText);
|
|
79
|
-
const data = await res.json();
|
|
80
|
-
console.log(data);
|
|
81
222
|
```
|
|
223
|
+
|
|
224
|
+
### Interceptors
|
|
225
|
+
|
|
226
|
+
You can also easily add the interceptors `before`, `after` and `error`:
|
|
227
|
+
|
|
228
|
+
```js
|
|
229
|
+
// Perform an action or request transformation before the request is sent
|
|
230
|
+
fch.before = async req => {
|
|
231
|
+
// Normalized request ready to be sent
|
|
232
|
+
...
|
|
233
|
+
return req;
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
// Perform an action or data transformation after the request is finished
|
|
237
|
+
fch.after = async res => {
|
|
238
|
+
// Full response as just after the request is made
|
|
239
|
+
...
|
|
240
|
+
return res;
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
// Perform an action or data transformation when an error is thrown
|
|
244
|
+
fch.error = async err => {
|
|
245
|
+
// Need to re-throw if we want to throw on error
|
|
246
|
+
...
|
|
247
|
+
throw err;
|
|
248
|
+
|
|
249
|
+
// OR, resolve it as if it didn't fail
|
|
250
|
+
return err.response;
|
|
251
|
+
|
|
252
|
+
// OR, resolve it with a custom value
|
|
253
|
+
return { message: 'Request failed with a code ' + err.response.status };
|
|
254
|
+
};
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
### Define shared options
|
|
259
|
+
|
|
260
|
+
You can also define values straight away:
|
|
261
|
+
|
|
262
|
+
```js
|
|
263
|
+
import api from "fch";
|
|
264
|
+
|
|
265
|
+
api.baseUrl = "https://pokeapi.co/";
|
|
266
|
+
|
|
267
|
+
const mew = await api.get("/pokemon/150");
|
|
268
|
+
console.log(mew);
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
If you prefer Axios' style of outputting the whole response, you can do:
|
|
272
|
+
|
|
273
|
+
```js
|
|
274
|
+
// Default, already only returns the data on a successful call
|
|
275
|
+
api.output = "data";
|
|
276
|
+
const name = await api.get("/users/1").name;
|
|
277
|
+
|
|
278
|
+
// Axios-like
|
|
279
|
+
api.output = "response";
|
|
280
|
+
const name = await api.get("/users/1").data.name;
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
## How to
|
|
285
|
+
|
|
286
|
+
### Stop errors from throwing
|
|
287
|
+
|
|
288
|
+
While you can handle this on a per-request basis, if you want to overwrite the global behavior you can write a interceptor:
|
|
289
|
+
|
|
290
|
+
```js
|
|
291
|
+
import fch from 'fch';
|
|
292
|
+
fch.error = error => error.response;
|
|
293
|
+
|
|
294
|
+
const res = await fch('/notfound');
|
|
295
|
+
expect(res.status).toBe(404);
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
### Return the full response
|
|
299
|
+
|
|
300
|
+
By default a successful request will just return the data. However this one is configurable on a global level:
|
|
301
|
+
|
|
302
|
+
```js
|
|
303
|
+
import fch from 'fch';
|
|
304
|
+
fch.output = 'response'; // Valid values are 'body' (default) or 'response'
|
|
305
|
+
|
|
306
|
+
const res = await fch('/hello');
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
Or on a per-request level:
|
|
310
|
+
|
|
311
|
+
```js
|
|
312
|
+
import fch from 'fch';
|
|
313
|
+
|
|
314
|
+
// Valid values are 'body' (default) or 'response'
|
|
315
|
+
const res = await fch('/hello', { output: 'response' });
|
|
316
|
+
|
|
317
|
+
// Does not affect others
|
|
318
|
+
const body = await fch('/hello');
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
### Set a base URL
|
|
323
|
+
|
|
324
|
+
There's a configuration parameter for that:
|
|
325
|
+
|
|
326
|
+
```js
|
|
327
|
+
import fch from 'fch';
|
|
328
|
+
fch.baseUrl = 'https://api.filemon.io/';
|
|
329
|
+
|
|
330
|
+
// Calls "https://api.filemon.io/blabla"
|
|
331
|
+
const body = await fch.get('/blabla');
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
### Set the authorization headers
|
|
336
|
+
|
|
337
|
+
You can set that globally as a header:
|
|
338
|
+
|
|
339
|
+
```js
|
|
340
|
+
import fch from 'fch';
|
|
341
|
+
fch.headers.Authorization = 'bearer abc';
|
|
342
|
+
|
|
343
|
+
const me = await fch('/users/me');
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
Or globally on a per-request basis, for example if you take the value from localStorage:
|
|
347
|
+
|
|
348
|
+
```js
|
|
349
|
+
import fch from 'fch';
|
|
350
|
+
|
|
351
|
+
// All the requests will add the Authorization header when the token is
|
|
352
|
+
// in localStorage
|
|
353
|
+
fch.before = req => {
|
|
354
|
+
if (localStorage.get('token')) {
|
|
355
|
+
req.headers.Authorization = 'bearer ' + localStorage.get('token');
|
|
356
|
+
}
|
|
357
|
+
return req;
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
const me = await fch('/users/me');
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
Or on a per-request basis, though we wouldn't recommend this:
|
|
364
|
+
|
|
365
|
+
```js
|
|
366
|
+
import fch from 'fch';
|
|
367
|
+
|
|
368
|
+
const me = await fch('/users/me', { headers: { Authorization: 'bearer abc' } });
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
### Create an instance
|
|
373
|
+
|
|
374
|
+
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:
|
|
375
|
+
|
|
376
|
+
```js
|
|
377
|
+
import fch from 'fch';
|
|
378
|
+
|
|
379
|
+
const api = fch.create({
|
|
380
|
+
baseUrl: 'https://api.filemon.io/',
|
|
381
|
+
...
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
api.get('/hello'); // Gets https://api.filemon.io/hello
|
|
385
|
+
fch.get('/hello'); // Gets http://localhost:3000/hello (or wherever you are)
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
Note: for server-side (Node.js) usage, you always want to set `baseUrl`.
|
|
389
|
+
|
|
390
|
+
### What are the differences in Node.js vs Browser?
|
|
391
|
+
|
|
392
|
+
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.
|
|
393
|
+
|
|
394
|
+
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.
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
### What are the differences with Axios?
|
|
398
|
+
|
|
399
|
+
The main difference is that things are simplified with fch:
|
|
400
|
+
|
|
401
|
+
```js
|
|
402
|
+
// Modify headers
|
|
403
|
+
axios.defaults.headers.Authorization = '...';
|
|
404
|
+
fch.headers.Authorization = '...';
|
|
405
|
+
|
|
406
|
+
// Set a base URL
|
|
407
|
+
axios.defaults.baseURL = '...';
|
|
408
|
+
fch.baseUrl = '...';
|
|
409
|
+
|
|
410
|
+
// Add an interceptor
|
|
411
|
+
axios.interceptors.request.use(fn);
|
|
412
|
+
fch.before = fn;
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
API size is also strikingly different, with **7.8kb** for Axios and **1.9kb** for fch.
|
|
416
|
+
|
|
417
|
+
As disadvantages, I can think of two major ones for `fch`:
|
|
418
|
+
|
|
419
|
+
- Requires Node.js 18+, which is the version that includes `fetch()` by default.
|
|
420
|
+
- Does not support some more advanced options,
|
package/.babelrc
DELETED
package/fetch.min.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
(function(global,factory){typeof exports==="object"&&typeof module!=="undefined"?module.exports=factory():typeof define==="function"&&define.amd?define(factory):(global=global||self,global.fch=factory())})(this,function(){"use strict";const resolve=async value=>{value=await value;if(Array.isArray(value)){return await Promise.all(value.map(resolve))}return value};const reject=message=>Promise.reject(new Error(message));const regexpCallback=cb=>cb instanceof RegExp?cb.test.bind(cb):cb;const callback=(cb,self)=>(...args)=>regexpCallback(cb).call(self,...args);const extend=(cb,self)=>async(value,i,all)=>({value:value,extra:await callback(cb,self)(value,i,all)});const extraUp=({extra:extra})=>extra;const valueUp=({value:value})=>value;const extendArray={every:async(obj,cb,self)=>{for(let i=0;i<obj.length;i++){const found=await callback(cb,self)(obj[i],i,obj);if(!found)return false}return true},filter:async(obj,cb,self)=>{const data=await resolve(obj.map(extend(cb,self)));return data.filter(extraUp).map(valueUp)},find:async(obj,cb,self)=>{for(let i=0;i<obj.length;i++){const found=await callback(cb,self)(obj[i],i,obj);if(found)return obj[i]}},findIndex:async(obj,cb,self)=>{for(let i=0;i<obj.length;i++){const found=await callback(cb,self)(obj[i],i,obj);if(found)return i}return-1},forEach:async(obj,cb,self)=>{await resolve(obj.map(extend(cb,self)));return obj},reduce:async(obj,cb,init)=>{const hasInit=typeof init!=="undefined";if(!hasInit)init=obj[0];for(let i=hasInit?0:1;i<obj.length;i++){init=await callback(cb)(init,obj[i],i,obj)}return init},reduceRight:async(obj,cb,init)=>{const hasInit=typeof init!=="undefined";if(!hasInit)init=obj[obj.length-1];for(let i=obj.length-(hasInit?1:2);i>=0;i--){init=await callback(cb)(init,obj[i],i,obj)}return init},some:async(obj,cb,self)=>{for(let i=0;i<obj.length;i++){const found=await callback(cb,self)(obj[i],i,obj);if(found)return true}return false}};const getter=(obj,extend)=>(target,key)=>{if(key==="then")return(...args)=>{return resolve(obj).then(...args)};if(key==="catch")return(...args)=>{return root(resolve(obj).catch(...args))};return func(resolve(obj).then(obj=>{if(typeof key==="symbol")return obj[key];if(key in extend){return func((...args)=>extend[key](obj,...args),extend)}if(typeof obj==="number"&&key in extend.number){return func((...args)=>extend.number[key](obj,...args),extend)}if(typeof obj==="string"&&key in extend.string){return func((...args)=>extend.string[key](obj,...args),extend)}if(Array.isArray(obj)&&key in extend.array){return func((...args)=>extend.array[key](obj,...args),extend)}if(obj[key]&&obj[key].bind){return func(obj[key].bind(obj),extend)}return func(obj[key],extend)}),extend)};const applier=(obj,extend)=>(target,self,args)=>{return func(resolve(obj).then(obj=>{if(typeof obj!=="function"){return reject(`You tried to call "${JSON.stringify(obj)}" (${typeof obj}) as a function, but it is not.`)}return obj(...args)}),extend)};const func=(obj,extend)=>new Proxy(()=>{},{get:getter(obj,extend),apply:applier(obj,extend)});const root=(obj,{number:number,string:string,array:array,...others}={})=>new Proxy({},{get:getter(obj,{number:{...number},string:{...string},array:{...extendArray,...array},...others})});if(typeof require!=="undefined"){require("isomorphic-fetch")}const ongoing=new Map;const hasPlainBody=options=>{if(options.headers["content-type"])return;if(typeof options.body!=="object")return;if(options.body instanceof FormData)return;return true};const fch=(url,options={})=>{options={method:"get",headers:{},...typeof options==="object"?options:{}};if(options.method.toLowerCase()==="get"){if(ongoing.get(url))return ongoing.get(url)}const headers=options.headers;for(let key in headers){const value=headers[key];delete headers[key];headers[key.toLowerCase()]=value}if(hasPlainBody(options)){options.body=JSON.stringify(options.body);headers["content-type"]="application/json; charset=utf-8"}ongoing.set(url,root(fetch(url,{...options,headers:headers}).then(res=>{ongoing.delete(url);if(!res.ok){const error=new Error(res.statusText);error.response=res;return Promise.reject(error)}const mem=new Map;return new Proxy(res,{get:(target,key)=>{if(["then","catch","finally"].includes(key))return res[key];return()=>{if(!mem.get(key)){mem.set(key,target[key]())}return mem.get(key)}}})})));return ongoing.get(url)};fch.head=((url,options={})=>fch(url,{...options,method:"head"}));fch.get=((url,options={})=>fch(url,{...options,method:"get"}));fch.post=((url,options={})=>fch(url,{...options,method:"post"}));fch.patch=((url,options={})=>fch(url,{...options,method:"patch"}));fch.put=((url,options={})=>fch(url,{...options,method:"put"}));fch.del=((url,options={})=>fch(url,{...options,method:"delete"}));return fch});
|
package/fetch.test.js
DELETED
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
import fch, { get } from './fetch.min';
|
|
2
|
-
global.fetch = require('jest-fetch-mock');
|
|
3
|
-
|
|
4
|
-
describe('fetch()', () => {
|
|
5
|
-
beforeEach(() => {
|
|
6
|
-
fetch.resetMocks();
|
|
7
|
-
});
|
|
8
|
-
|
|
9
|
-
it('works', async () => {
|
|
10
|
-
fetch.once(JSON.stringify({ secret: '12345' }));
|
|
11
|
-
const res = await fch('https://google.com/').json();
|
|
12
|
-
|
|
13
|
-
expect(res).toEqual({ secret: '12345' });
|
|
14
|
-
expect(fetch.mock.calls.length).toEqual(1);
|
|
15
|
-
expect(fetch.mock.calls[0][0]).toEqual('https://google.com/');
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
it('can use the `fetch.get()` shorthand', async () => {
|
|
19
|
-
fetch.once('get');
|
|
20
|
-
expect(await fch.get('/').text()).toBe('get');
|
|
21
|
-
expect(fetch.mock.calls[0][1].method).toEqual('get');
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it('can use the `fetch.patch()` shorthand', async () => {
|
|
25
|
-
fetch.once('patch');
|
|
26
|
-
expect(await fch.patch('/').text()).toBe('patch');
|
|
27
|
-
expect(fetch.mock.calls[0][1].method).toEqual('patch');
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it('can use the `fetch.put()` shorthand', async () => {
|
|
31
|
-
fetch.once('put');
|
|
32
|
-
expect(await fch.put('/').text()).toBe('put');
|
|
33
|
-
expect(fetch.mock.calls[0][1].method).toEqual('put');
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it('can use the `fetch.head()` shorthand', async () => {
|
|
37
|
-
fetch.once('head');
|
|
38
|
-
expect(await fch.head('/').text()).toBe('head');
|
|
39
|
-
expect(fetch.mock.calls[0][1].method).toEqual('head');
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it('can use the `fetch.post()` shorthand', async () => {
|
|
43
|
-
fetch.once('post');
|
|
44
|
-
expect(await fch.post('/').text()).toBe('post');
|
|
45
|
-
expect(fetch.mock.calls[0][1].method).toEqual('post');
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it('can use the `fetch.del()` shorthand', async () => {
|
|
49
|
-
fetch.once('del');
|
|
50
|
-
expect(await fch.del('/').text()).toBe('del');
|
|
51
|
-
expect(fetch.mock.calls[0][1].method).toEqual('delete');
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it('ignores invalid options', async () => {
|
|
55
|
-
fetch.once(JSON.stringify({ secret: '12345' }));
|
|
56
|
-
const res = await fch('https://google.com/', 10).json();
|
|
57
|
-
|
|
58
|
-
expect(res).toEqual({ secret: '12345' });
|
|
59
|
-
expect(fetch.mock.calls.length).toEqual(1);
|
|
60
|
-
expect(fetch.mock.calls[0][0]).toEqual('https://google.com/');
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it('will not overwrite if it is FormData', async () => {
|
|
64
|
-
fetch.once(JSON.stringify({ secret: '12345' }));
|
|
65
|
-
const res = await fch('/', { method: 'POST', body: new FormData() }).json();
|
|
66
|
-
|
|
67
|
-
expect(res).toEqual({ secret: '12345' });
|
|
68
|
-
expect(fetch.mock.calls.length).toEqual(1);
|
|
69
|
-
const [url, opts] = fetch.mock.calls[0];
|
|
70
|
-
expect(opts).toMatchObject({ body: expect.any(FormData) });
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it('will not overwrite if content-type is set', async () => {
|
|
74
|
-
fetch.once(JSON.stringify({ secret: '12345' }));
|
|
75
|
-
const res = await fch('/', {
|
|
76
|
-
method: 'POST',
|
|
77
|
-
body: { a: 'b'},
|
|
78
|
-
headers: { 'Content-Type': 'xxx' }
|
|
79
|
-
}).json();
|
|
80
|
-
|
|
81
|
-
expect(res).toEqual({ secret: '12345' });
|
|
82
|
-
expect(fetch.mock.calls.length).toEqual(1);
|
|
83
|
-
const [url, opts] = fetch.mock.calls[0];
|
|
84
|
-
expect(url).toEqual('/');
|
|
85
|
-
expect(opts).toMatchObject({
|
|
86
|
-
method: 'POST',
|
|
87
|
-
body: { a: 'b' },
|
|
88
|
-
headers: { 'content-type': 'xxx' }
|
|
89
|
-
});
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it('will send JSON', async () => {
|
|
93
|
-
fetch.once(JSON.stringify({ secret: '12345' }));
|
|
94
|
-
const res = await fch('/', { method: 'POST', body: { a: 'b'} }).json();
|
|
95
|
-
|
|
96
|
-
expect(res).toEqual({ secret: '12345' });
|
|
97
|
-
expect(fetch.mock.calls.length).toEqual(1);
|
|
98
|
-
const [url, opts] = fetch.mock.calls[0];
|
|
99
|
-
expect(url).toEqual('/');
|
|
100
|
-
expect(opts).toMatchObject({
|
|
101
|
-
method: 'POST',
|
|
102
|
-
body: '{"a":"b"}',
|
|
103
|
-
headers: { 'content-type': 'application/json; charset=utf-8' }
|
|
104
|
-
});
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
it('can run in parallel', async () => {
|
|
108
|
-
fetch.once(JSON.stringify('a')).once(JSON.stringify('b'));
|
|
109
|
-
const res = await Promise.all(['/a', '/b'].map(url => fch(url).json()));
|
|
110
|
-
|
|
111
|
-
expect(res).toEqual(['a', 'b']);
|
|
112
|
-
expect(fetch.mock.calls.length).toEqual(2);
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
// There is a bug with node-fetch so this is difficult to test right now
|
|
116
|
-
// https://github.com/bitinn/node-fetch/issues/386
|
|
117
|
-
it.skip('will not trigger race conditions on get for the same url', async () => {
|
|
118
|
-
fetch.once(JSON.stringify('a')).once(JSON.stringify('b'));
|
|
119
|
-
const res = await Promise.all(['/', '/'].map(url => fch(url).json()));
|
|
120
|
-
|
|
121
|
-
expect(res).toEqual(['a', 'a']);
|
|
122
|
-
expect(fetch.mock.calls.length).toEqual(1);
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
it('can set `accepts` insensitively', async () => {
|
|
126
|
-
fetch.once(JSON.stringify({ secret: '12345' }));
|
|
127
|
-
const res = await fch('/', { headers: { 'Accepts': 'text/xml' } }).json();
|
|
128
|
-
|
|
129
|
-
const [url, opts] = fetch.mock.calls[0];
|
|
130
|
-
expect(opts.headers).toEqual({ 'accepts': 'text/xml' });
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
it('can accept network rejections', async () => {
|
|
134
|
-
fetch.mockResponseOnce(JSON.stringify("unauthorized"), { status: 401, ok: false });
|
|
135
|
-
await expect(fch('/')).rejects.toMatchObject({
|
|
136
|
-
message: 'Unauthorized'
|
|
137
|
-
});
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
it('can accept rejections', async () => {
|
|
141
|
-
fetch.mockRejectOnce(new Error('fake error message'));
|
|
142
|
-
await expect(fch('/')).rejects.toMatchObject({
|
|
143
|
-
message: 'fake error message'
|
|
144
|
-
});
|
|
145
|
-
});
|
|
146
|
-
});
|