fch 2.0.1 → 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 +137 -102
- package/package.json +7 -6
- package/readme.md +95 -52
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,102 +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
|
-
|
|
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
|
+
}
|
|
109
131
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
+
}
|
|
114
136
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
+
}
|
|
120
142
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
143
|
+
// Hijack the requeset and modify it
|
|
144
|
+
if (before) {
|
|
145
|
+
request = before(request);
|
|
146
|
+
}
|
|
125
147
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
148
|
+
// It should be cached
|
|
149
|
+
if (dedupe) {
|
|
150
|
+
// It's already cached! Just return it
|
|
151
|
+
if (dedupe.get()) return dedupe.get();
|
|
130
152
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
});
|
|
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
|
+
};
|
|
138
194
|
|
|
139
|
-
//
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
//
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
fch
|
|
150
|
-
fch.
|
|
151
|
-
|
|
152
|
-
const request = (url, opts = {}) => fch(url, { ...opts });
|
|
153
|
-
const get = (url, opts = {}) => fch(url, { ...opts });
|
|
154
|
-
const head = (url, opts = {}) => fch(url, { ...opts, method: "head" });
|
|
155
|
-
const post = (url, opts = {}) => fch(url, { ...opts, method: "post" });
|
|
156
|
-
const patch = (url, opts = {}) => fch(url, { ...opts, method: "patch" });
|
|
157
|
-
const put = (url, opts = {}) => fch(url, { ...opts, method: "put" });
|
|
158
|
-
const del = (url, opts = {}) => fch(url, { ...opts, method: "delete" });
|
|
159
|
-
|
|
160
|
-
fch.request = request;
|
|
161
|
-
fch.get = get;
|
|
162
|
-
fch.head = head;
|
|
163
|
-
fch.post = post;
|
|
164
|
-
fch.patch = patch;
|
|
165
|
-
fch.put = put;
|
|
166
|
-
fch.del = del;
|
|
167
|
-
|
|
168
|
-
fch.swear = swear;
|
|
169
|
-
|
|
170
|
-
export default fch;
|
|
171
|
-
export { request, get, head, post, patch, put, del, swear };
|
|
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.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",
|
|
@@ -21,15 +21,16 @@
|
|
|
21
21
|
"async",
|
|
22
22
|
"ajax"
|
|
23
23
|
],
|
|
24
|
-
"main": "
|
|
25
|
-
"
|
|
24
|
+
"main": "./index.js",
|
|
25
|
+
"browser": "./fetch.js",
|
|
26
|
+
"files": [
|
|
27
|
+
"fetch.js"
|
|
28
|
+
],
|
|
26
29
|
"type": "module",
|
|
27
30
|
"engines": {
|
|
28
31
|
"node": ">=18.0.0"
|
|
29
32
|
},
|
|
30
|
-
"dependencies": {
|
|
31
|
-
"swear": "^1.1.0"
|
|
32
|
-
},
|
|
33
|
+
"dependencies": {},
|
|
33
34
|
"devDependencies": {
|
|
34
35
|
"jest": "^28.0.1",
|
|
35
36
|
"jest-fetch-mock": "^3.0.3"
|
package/readme.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
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";
|
|
@@ -8,38 +8,40 @@ const mew = await api("https://pokeapi.co/pokemon/150");
|
|
|
8
8
|
console.log(mew);
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
`
|
|
12
|
-
|
|
13
|
-
- Automatically `JSON.stringify()` and `Content-Type: 'application/json'` for plain objects.
|
|
14
|
-
- Automatically parse server response as json if it includes the headers, or text otherwise.
|
|
11
|
+
- Automatically `JSON.stringify()` and `Content-Type: 'application/json'` for objects.
|
|
12
|
+
- Automatically parse server response taking into account the headers.
|
|
15
13
|
- Works the same way in Node.js and the browser.
|
|
16
|
-
- Await/Async
|
|
17
|
-
-
|
|
18
|
-
-
|
|
19
|
-
- Easily define shared options straight on the root `fetch.baseUrl = "https://...";`.
|
|
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://...";`.
|
|
20
17
|
- Interceptors: `before` (the request), `after` (the response) and `error` (it fails).
|
|
21
18
|
- Deduplicates parallel GET requests.
|
|
22
19
|
- Configurable to return either just the body, or the full response.
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
+
```
|
|
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 |
|
|
43
45
|
|
|
44
46
|
## Getting Started
|
|
45
47
|
|
|
@@ -52,15 +54,24 @@ npm install fch
|
|
|
52
54
|
Then import it to be able to use it in your code:
|
|
53
55
|
|
|
54
56
|
```js
|
|
55
|
-
import
|
|
57
|
+
import fch from 'fch';
|
|
58
|
+
const body = await fch.get('/');
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
On the browser you can add it with a script and it will be available as `fch`:
|
|
56
62
|
|
|
57
|
-
|
|
63
|
+
```html
|
|
64
|
+
<!-- Import it as usual -->
|
|
65
|
+
<script src="https://cdn.jsdelivr.net/npm/fch"></script>
|
|
66
|
+
<script>
|
|
67
|
+
fch('/hello');
|
|
68
|
+
</script>
|
|
58
69
|
```
|
|
59
70
|
|
|
60
71
|
## Options
|
|
61
72
|
|
|
62
73
|
```js
|
|
63
|
-
import api
|
|
74
|
+
import api from 'fch';
|
|
64
75
|
|
|
65
76
|
// General options with their defaults; most of these are also parameters:
|
|
66
77
|
api.baseUrl = null; // Set an API endpoint
|
|
@@ -84,12 +95,6 @@ api.get(url, { headers, ... });
|
|
|
84
95
|
api.post(url, { body, headers, ... });
|
|
85
96
|
api.put(url, { body, headers, ... });
|
|
86
97
|
// ...
|
|
87
|
-
|
|
88
|
-
// Just import/use the method you need
|
|
89
|
-
get(url, { headers, ... });
|
|
90
|
-
post(url, { body, headers, ... });
|
|
91
|
-
put(url, { body, headers, ... });
|
|
92
|
-
// ...
|
|
93
98
|
```
|
|
94
99
|
|
|
95
100
|
### URL
|
|
@@ -120,7 +125,7 @@ api.get('/hello');
|
|
|
120
125
|
|
|
121
126
|
### Body
|
|
122
127
|
|
|
123
|
-
The `body` can be a string, a
|
|
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:
|
|
124
129
|
|
|
125
130
|
```js
|
|
126
131
|
import api from 'api';
|
|
@@ -250,19 +255,6 @@ fch.error = async err => {
|
|
|
250
255
|
```
|
|
251
256
|
|
|
252
257
|
|
|
253
|
-
|
|
254
|
-
### Advanced promise interface
|
|
255
|
-
|
|
256
|
-
This library uses [the `swear` promise interface](https://www.npmjs.com/swear), which allows you to query operations seamlessly on top of your promise:
|
|
257
|
-
|
|
258
|
-
```js
|
|
259
|
-
import api from "fch";
|
|
260
|
-
|
|
261
|
-
// You can keep performing actions like it was sync
|
|
262
|
-
const firstFriendName = await api.get("/friends")[0].name;
|
|
263
|
-
console.log(firstFriendName);
|
|
264
|
-
```
|
|
265
|
-
|
|
266
258
|
### Define shared options
|
|
267
259
|
|
|
268
260
|
You can also define values straight away:
|
|
@@ -375,3 +367,54 @@ import fch from 'fch';
|
|
|
375
367
|
|
|
376
368
|
const me = await fch('/users/me', { headers: { Authorization: 'bearer abc' } });
|
|
377
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,
|