fch 1.0.0 → 2.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 +131 -52
- package/fetch.test.js +308 -84
- package/package.json +29 -25
- package/readme.md +221 -46
- package/.babelrc +0 -3
- package/fetch.min.js +0 -1
package/fetch.js
CHANGED
|
@@ -1,34 +1,25 @@
|
|
|
1
|
-
import swear from
|
|
2
|
-
|
|
3
|
-
if (typeof require !== 'undefined') {
|
|
4
|
-
require('isomorphic-fetch');
|
|
5
|
-
}
|
|
1
|
+
import swear from "swear";
|
|
6
2
|
|
|
7
3
|
// To avoid making parallel requests to the same url if one is ongoing
|
|
8
4
|
const ongoing = new Map();
|
|
9
5
|
|
|
10
6
|
// Plain-ish object
|
|
11
|
-
const hasPlainBody = options => {
|
|
12
|
-
if (options.headers[
|
|
13
|
-
if (typeof options.body !==
|
|
7
|
+
const hasPlainBody = (options) => {
|
|
8
|
+
if (options.headers["content-type"]) return;
|
|
9
|
+
if (typeof options.body !== "object") return;
|
|
14
10
|
if (options.body instanceof FormData) return;
|
|
15
11
|
return true;
|
|
16
12
|
};
|
|
17
13
|
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
...(typeof options === 'object' ? options : {})
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
// GET requests should not have race conditions
|
|
27
|
-
if (options.method.toLowerCase() === 'get') {
|
|
28
|
-
if (ongoing.get(url)) return ongoing.get(url);
|
|
29
|
-
}
|
|
14
|
+
const createUrl = (path, base) => {
|
|
15
|
+
if (!base) return path;
|
|
16
|
+
const url = new URL(path, base);
|
|
17
|
+
return url.href;
|
|
18
|
+
};
|
|
30
19
|
|
|
31
|
-
|
|
20
|
+
const createHeaders = (user, base) => {
|
|
21
|
+
// User-set headers overwrite the base headers
|
|
22
|
+
const headers = { ...base, ...user };
|
|
32
23
|
|
|
33
24
|
// Make the headers lowercase
|
|
34
25
|
for (let key in headers) {
|
|
@@ -37,44 +28,132 @@ const fch = (url, options = {}) => {
|
|
|
37
28
|
headers[key.toLowerCase()] = value;
|
|
38
29
|
}
|
|
39
30
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
options.body = JSON.stringify(options.body);
|
|
43
|
-
headers['content-type'] = 'application/json; charset=utf-8';
|
|
44
|
-
}
|
|
31
|
+
return headers;
|
|
32
|
+
};
|
|
45
33
|
|
|
46
|
-
|
|
34
|
+
const createFetch = (request, { after, dedupe, error, output }) => {
|
|
35
|
+
return fetch(request.url, request).then(async (res) => {
|
|
47
36
|
// No longer ongoing at this point
|
|
48
|
-
|
|
37
|
+
if (dedupe) dedupe.clear();
|
|
38
|
+
|
|
39
|
+
// Need to manually create it to set some things like the proper response
|
|
40
|
+
let response = {
|
|
41
|
+
status: res.status,
|
|
42
|
+
statusText: res.statusText,
|
|
43
|
+
headers: {},
|
|
44
|
+
};
|
|
45
|
+
for (let key of res.headers.keys()) {
|
|
46
|
+
response.headers[key.toLowerCase()] = res.headers.get(key);
|
|
47
|
+
}
|
|
49
48
|
|
|
50
|
-
//
|
|
49
|
+
// Oops, throw it
|
|
51
50
|
if (!res.ok) {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
error
|
|
55
|
-
return Promise.reject(error);
|
|
51
|
+
const err = new Error(res.statusText);
|
|
52
|
+
err.response = response;
|
|
53
|
+
return error(err);
|
|
56
54
|
}
|
|
57
55
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
56
|
+
// Automatically parse the response
|
|
57
|
+
const isJson = res.headers.get("content-type").includes("application/json");
|
|
58
|
+
response.body = await (isJson ? res.json() : res.text());
|
|
59
|
+
|
|
60
|
+
// Hijack the response and modify it
|
|
61
|
+
if (after) {
|
|
62
|
+
response = after(response);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (output === "body") {
|
|
66
|
+
return response.body;
|
|
67
|
+
} else {
|
|
68
|
+
return response;
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
71
|
};
|
|
72
72
|
|
|
73
|
-
fch
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
73
|
+
const fch = swear((url, options = {}) => {
|
|
74
|
+
// Second parameter always has to be an object, even when it defaults
|
|
75
|
+
if (typeof options !== "object") options = {};
|
|
76
|
+
|
|
77
|
+
// Accept either fch(options) or fch(url, options)
|
|
78
|
+
options = typeof url === "string" ? { url, ...options } : url;
|
|
79
|
+
|
|
80
|
+
// Absolute URL if possible; Default method; merge the default headers
|
|
81
|
+
options.url = createUrl(options.url, fch.baseUrl);
|
|
82
|
+
options.method = (options.method ?? fch.method).toLowerCase();
|
|
83
|
+
options.headers = createHeaders(options.headers, fch.headers);
|
|
84
|
+
|
|
85
|
+
let {
|
|
86
|
+
dedupe = fch.dedupe,
|
|
87
|
+
output = fch.output,
|
|
88
|
+
|
|
89
|
+
before = fch.before,
|
|
90
|
+
after = fch.after,
|
|
91
|
+
error = fch.error,
|
|
92
|
+
|
|
93
|
+
...request
|
|
94
|
+
} = options; // Local option OR global value (including defaults)
|
|
95
|
+
|
|
96
|
+
if (request.method !== "get") {
|
|
97
|
+
dedupe = false;
|
|
98
|
+
}
|
|
99
|
+
if (dedupe) {
|
|
100
|
+
dedupe = {
|
|
101
|
+
save: (prom) => {
|
|
102
|
+
ongoing.set(request.url, prom);
|
|
103
|
+
return prom;
|
|
104
|
+
},
|
|
105
|
+
get: () => ongoing.get(request.url),
|
|
106
|
+
clear: () => ongoing.delete(request.url),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (!["body", "response"].includes(output)) {
|
|
111
|
+
const msg = `options.output needs to be either "body" (default) or "response", not "${output}"`;
|
|
112
|
+
throw new Error(msg);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// JSON-encode plain objects
|
|
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();
|
|
130
|
+
|
|
131
|
+
// Otherwise, save it in the cache and return the promise
|
|
132
|
+
return dedupe.save(createFetch(request, { dedupe, output, error, after }));
|
|
133
|
+
} else {
|
|
134
|
+
// PUT, POST, etc should never dedupe and just return the plain request
|
|
135
|
+
return createFetch(request, { output, error, after });
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
fch.get = (url, options = {}) => fch(url, { ...options, method: "get" });
|
|
140
|
+
fch.head = (url, options = {}) => fch(url, { ...options, method: "head" });
|
|
141
|
+
fch.post = (url, options = {}) => fch(url, { ...options, method: "post" });
|
|
142
|
+
fch.patch = (url, options = {}) => fch(url, { ...options, method: "patch" });
|
|
143
|
+
fch.put = (url, options = {}) => fch(url, { ...options, method: "put" });
|
|
144
|
+
fch.del = (url, options = {}) => fch(url, { ...options, method: "delete" });
|
|
145
|
+
|
|
146
|
+
// Default values
|
|
147
|
+
fch.method = "get";
|
|
148
|
+
fch.headers = {};
|
|
149
|
+
|
|
150
|
+
// Default options
|
|
151
|
+
fch.dedupe = true;
|
|
152
|
+
fch.output = "body";
|
|
153
|
+
|
|
154
|
+
// Interceptors
|
|
155
|
+
fch.before = (request) => request;
|
|
156
|
+
fch.after = (response) => response;
|
|
157
|
+
fch.error = (error) => Promise.reject(error);
|
|
79
158
|
|
|
80
159
|
export default fch;
|
package/fetch.test.js
CHANGED
|
@@ -1,146 +1,370 @@
|
|
|
1
|
-
import fch
|
|
2
|
-
|
|
1
|
+
import fch from "./fetch.js";
|
|
2
|
+
import mock from "jest-fetch-mock";
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
mock.enableMocks();
|
|
5
|
+
|
|
6
|
+
const jsonHeaders = { headers: { "Content-Type": "application/json" } };
|
|
7
|
+
|
|
8
|
+
describe("fetch()", () => {
|
|
5
9
|
beforeEach(() => {
|
|
6
10
|
fetch.resetMocks();
|
|
7
11
|
});
|
|
8
12
|
|
|
9
|
-
it(
|
|
10
|
-
fetch.once(
|
|
11
|
-
const
|
|
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/");
|
|
12
49
|
|
|
13
|
-
expect(
|
|
50
|
+
expect(data).toEqual({ secret: "12345" });
|
|
14
51
|
expect(fetch.mock.calls.length).toEqual(1);
|
|
15
|
-
expect(fetch.mock.calls[0][0]).toEqual(
|
|
52
|
+
expect(fetch.mock.calls[0][0]).toEqual("https://google.com/");
|
|
16
53
|
});
|
|
17
54
|
|
|
18
|
-
it(
|
|
19
|
-
fetch.once(
|
|
20
|
-
|
|
21
|
-
expect(
|
|
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");
|
|
22
59
|
});
|
|
23
60
|
|
|
24
|
-
it(
|
|
25
|
-
fetch.once(
|
|
26
|
-
|
|
27
|
-
|
|
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;
|
|
28
69
|
});
|
|
29
70
|
|
|
30
|
-
it(
|
|
31
|
-
fetch.once(
|
|
32
|
-
|
|
33
|
-
expect(
|
|
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");
|
|
34
76
|
});
|
|
35
77
|
|
|
36
|
-
it(
|
|
37
|
-
fetch.once(
|
|
38
|
-
expect(await fch.
|
|
39
|
-
expect(fetch.mock.calls[0][1].method).toEqual(
|
|
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");
|
|
40
82
|
});
|
|
41
83
|
|
|
42
|
-
it(
|
|
43
|
-
fetch.once(
|
|
44
|
-
expect(await fch.
|
|
45
|
-
expect(fetch.mock.calls[0][1].method).toEqual(
|
|
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");
|
|
46
88
|
});
|
|
47
89
|
|
|
48
|
-
it(
|
|
49
|
-
fetch.once(
|
|
50
|
-
expect(await fch.
|
|
51
|
-
expect(fetch.mock.calls[0][1].method).toEqual(
|
|
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");
|
|
52
94
|
});
|
|
53
95
|
|
|
54
|
-
it(
|
|
55
|
-
fetch.once(
|
|
56
|
-
|
|
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);
|
|
57
105
|
|
|
58
|
-
expect(res).toEqual({ secret:
|
|
106
|
+
expect(res).toEqual({ secret: "12345" });
|
|
59
107
|
expect(fetch.mock.calls.length).toEqual(1);
|
|
60
|
-
expect(fetch.mock.calls[0][0]).toEqual(
|
|
108
|
+
expect(fetch.mock.calls[0][0]).toEqual("https://google.com/");
|
|
61
109
|
});
|
|
62
110
|
|
|
63
|
-
it(
|
|
64
|
-
fetch.once(JSON.stringify({ secret:
|
|
65
|
-
const res = await fch(
|
|
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() });
|
|
66
114
|
|
|
67
|
-
expect(res).toEqual({ secret:
|
|
115
|
+
expect(res).toEqual({ secret: "12345" });
|
|
68
116
|
expect(fetch.mock.calls.length).toEqual(1);
|
|
69
117
|
const [url, opts] = fetch.mock.calls[0];
|
|
70
118
|
expect(opts).toMatchObject({ body: expect.any(FormData) });
|
|
71
119
|
});
|
|
72
120
|
|
|
73
|
-
it(
|
|
74
|
-
fetch.once(JSON.stringify({ secret:
|
|
75
|
-
const res = await fch(
|
|
76
|
-
method:
|
|
77
|
-
body: { a:
|
|
78
|
-
headers: {
|
|
79
|
-
})
|
|
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
|
+
});
|
|
80
128
|
|
|
81
|
-
expect(res).toEqual({ secret:
|
|
129
|
+
expect(res).toEqual({ secret: "12345" });
|
|
82
130
|
expect(fetch.mock.calls.length).toEqual(1);
|
|
83
131
|
const [url, opts] = fetch.mock.calls[0];
|
|
84
|
-
expect(url).toEqual(
|
|
132
|
+
expect(url).toEqual("/");
|
|
85
133
|
expect(opts).toMatchObject({
|
|
86
|
-
method:
|
|
87
|
-
body: { a:
|
|
88
|
-
headers: {
|
|
134
|
+
method: "post",
|
|
135
|
+
body: { a: "b" },
|
|
136
|
+
headers: { "content-type": "xxx" },
|
|
89
137
|
});
|
|
90
138
|
});
|
|
91
139
|
|
|
92
|
-
it(
|
|
93
|
-
fetch.once(JSON.stringify({ secret:
|
|
94
|
-
const res = await fch(
|
|
140
|
+
it("will send JSON", async () => {
|
|
141
|
+
fetch.once(JSON.stringify({ secret: "12345" }), jsonHeaders);
|
|
142
|
+
const res = await fch("/", { method: "POST", body: { a: "b" } });
|
|
95
143
|
|
|
96
|
-
expect(res).toEqual({ secret:
|
|
144
|
+
expect(res).toEqual({ secret: "12345" });
|
|
97
145
|
expect(fetch.mock.calls.length).toEqual(1);
|
|
98
146
|
const [url, opts] = fetch.mock.calls[0];
|
|
99
|
-
expect(url).toEqual(
|
|
147
|
+
expect(url).toEqual("/");
|
|
100
148
|
expect(opts).toMatchObject({
|
|
101
|
-
method:
|
|
149
|
+
method: "post",
|
|
102
150
|
body: '{"a":"b"}',
|
|
103
|
-
headers: {
|
|
151
|
+
headers: { "content-type": "application/json; charset=utf-8" },
|
|
104
152
|
});
|
|
105
153
|
});
|
|
106
154
|
|
|
107
|
-
it(
|
|
108
|
-
fetch.once(
|
|
109
|
-
const res = await Promise.all([
|
|
155
|
+
it("can run in parallel", async () => {
|
|
156
|
+
fetch.once("a").once("b");
|
|
157
|
+
const res = await Promise.all([fch("/a"), fch("/b")]);
|
|
110
158
|
|
|
111
|
-
expect(res).toEqual([
|
|
159
|
+
expect(res).toEqual(["a", "b"]);
|
|
112
160
|
expect(fetch.mock.calls.length).toEqual(2);
|
|
113
161
|
});
|
|
114
162
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
fetch.once(JSON.stringify('a')).once(JSON.stringify('b'));
|
|
119
|
-
const res = await Promise.all(['/', '/'].map(url => fch(url).json()));
|
|
163
|
+
it("can set `accepts` insensitively", async () => {
|
|
164
|
+
fetch.once(JSON.stringify({ secret: "12345" }), jsonHeaders);
|
|
165
|
+
const res = await fch("/", { headers: { Accepts: "text/xml" } });
|
|
120
166
|
|
|
121
|
-
expect(
|
|
122
|
-
expect(fetch.mock.calls.length).toEqual(1);
|
|
167
|
+
expect(fetch.mock.calls[0][1].headers).toEqual({ accepts: "text/xml" });
|
|
123
168
|
});
|
|
124
169
|
|
|
125
|
-
it(
|
|
126
|
-
fetch.
|
|
127
|
-
|
|
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
|
+
});
|
|
128
179
|
|
|
129
|
-
|
|
130
|
-
|
|
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
|
+
});
|
|
131
185
|
});
|
|
132
186
|
|
|
133
|
-
it(
|
|
134
|
-
fetch.
|
|
135
|
-
await expect(fch(
|
|
136
|
-
message:
|
|
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",
|
|
137
191
|
});
|
|
138
192
|
});
|
|
193
|
+
});
|
|
139
194
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
+
},
|
|
144
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);
|
|
145
369
|
});
|
|
146
370
|
});
|
package/package.json
CHANGED
|
@@ -1,36 +1,40 @@
|
|
|
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": "2.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
|
-
"
|
|
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"
|
|
19
15
|
},
|
|
20
|
-
"
|
|
21
|
-
"
|
|
16
|
+
"keywords": [
|
|
17
|
+
"fetch",
|
|
18
|
+
"axios",
|
|
19
|
+
"http",
|
|
20
|
+
"https",
|
|
21
|
+
"async",
|
|
22
|
+
"ajax"
|
|
23
|
+
],
|
|
24
|
+
"main": "fetch.js",
|
|
25
|
+
"type": "module",
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=18.0.0"
|
|
22
28
|
},
|
|
23
29
|
"dependencies": {
|
|
24
|
-
"
|
|
25
|
-
"swear": "^1.0.0"
|
|
30
|
+
"swear": "^1.1.0"
|
|
26
31
|
},
|
|
27
32
|
"devDependencies": {
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"uglify-es": "^3.3.9"
|
|
33
|
+
"jest": "^28.0.1",
|
|
34
|
+
"jest-fetch-mock": "^3.0.3"
|
|
35
|
+
},
|
|
36
|
+
"jest": {
|
|
37
|
+
"testEnvironment": "jest-environment-node",
|
|
38
|
+
"transform": {}
|
|
35
39
|
}
|
|
36
40
|
}
|
package/readme.md
CHANGED
|
@@ -1,81 +1,256 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Fch [](https://www.npmjs.com/package/fch) [](https://github.com/franciscop/fetch/blob/master/fetch.js)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A library to make API calls easier. Similar to Axios, but tiny size and simpler API:
|
|
4
4
|
|
|
5
5
|
```js
|
|
6
|
-
import
|
|
7
|
-
|
|
8
|
-
// Example data: { "name": "Francisco" }
|
|
9
|
-
const url = 'https://api.jsonbin.io/b/5bc69ae7716f9364f8c58651';
|
|
6
|
+
import api from "fch";
|
|
10
7
|
|
|
11
|
-
|
|
12
|
-
// Using the Swear interface
|
|
13
|
-
const name = await fch(url).json().name;
|
|
14
|
-
console.log(name); // "Francisco"
|
|
8
|
+
api.baseUrl = "https://pokeapi.co/";
|
|
15
9
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
console.log(data.name); // "Francisco"
|
|
19
|
-
})();
|
|
10
|
+
const mew = await api("/pokemon/150");
|
|
11
|
+
console.log(mew);
|
|
20
12
|
```
|
|
21
13
|
|
|
14
|
+
`fch` is a better `fetch()`:
|
|
22
15
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
- Isomorphic fetch() so it works the same in the server as the browser.
|
|
27
|
-
- Automatic `JSON.stringify()` and `Content-Type: 'application/json'` for plain objects.
|
|
16
|
+
- Automatically `JSON.stringify()` and `Content-Type: 'application/json'` for plain objects.
|
|
17
|
+
- Automatically parse server response as json if it includes the headers, or text otherwise.
|
|
18
|
+
- Isomorphic fetch(); it works the same way in the browser and server.
|
|
28
19
|
- Await/Async Promise interface works as you know and love.
|
|
29
|
-
- Better error handling. `>= 400 and <= 100` will _reject_ the promise with an error instance.
|
|
30
|
-
- Advanced [
|
|
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.
|
|
31
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://...";`.
|
|
24
|
+
- Interceptors: `before` (the request), `after` (the response) and `error` (it fails).
|
|
25
|
+
- Deduplicates parallel GET requests.
|
|
26
|
+
- Configurable to return either just the body, or the full response.
|
|
27
|
+
- [TODO]: cache engine with "highs" and "lows", great for scrapping
|
|
28
|
+
- [TODO]: rate-limiting of requests (N-second, or N-parallel), great for scrapping
|
|
32
29
|
|
|
33
|
-
|
|
34
|
-
## Getting started
|
|
30
|
+
## Getting Started
|
|
35
31
|
|
|
36
32
|
Install it in your project:
|
|
37
33
|
|
|
38
|
-
```
|
|
34
|
+
```bash
|
|
39
35
|
npm install fch
|
|
40
36
|
```
|
|
41
37
|
|
|
42
38
|
Then import it to be able to use it in your code:
|
|
43
39
|
|
|
44
40
|
```js
|
|
45
|
-
|
|
46
|
-
|
|
41
|
+
import api from 'fch';
|
|
42
|
+
|
|
43
|
+
const data = await api.get('/');
|
|
47
44
|
```
|
|
48
45
|
|
|
49
|
-
Alternatively, include it straight from the CDN for front-end:
|
|
50
46
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
47
|
+
### Advanced promise interface
|
|
48
|
+
|
|
49
|
+
This library has a couple of aces up its sleeve. First, it [has a better Promise interface](https://www.npmjs.com/swear):
|
|
50
|
+
|
|
51
|
+
```js
|
|
52
|
+
import api from "fch";
|
|
53
|
+
|
|
54
|
+
// You can keep performing actions like it was sync
|
|
55
|
+
const firstFriendName = await api.get("/friends")[0].name;
|
|
56
|
+
console.log(firstFriendName);
|
|
56
57
|
```
|
|
57
58
|
|
|
59
|
+
### Define shared options
|
|
58
60
|
|
|
61
|
+
You can also define values straight away:
|
|
59
62
|
|
|
60
|
-
|
|
63
|
+
```js
|
|
64
|
+
import api from "fch";
|
|
61
65
|
|
|
62
|
-
|
|
66
|
+
api.baseUrl = "https://pokeapi.co/";
|
|
67
|
+
|
|
68
|
+
const mew = await api.get("/pokemon/150");
|
|
69
|
+
console.log(mew);
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
If you prefer Axios' style of outputting the whole response, you can do:
|
|
63
73
|
|
|
64
74
|
```js
|
|
65
|
-
//
|
|
66
|
-
|
|
67
|
-
const
|
|
68
|
-
|
|
75
|
+
// Default, already only returns the data on a successful call
|
|
76
|
+
api.output = "data";
|
|
77
|
+
const name = await api.get("/users/1").name;
|
|
78
|
+
|
|
79
|
+
// Axios-like
|
|
80
|
+
api.output = "response";
|
|
81
|
+
const name = await api.get("/users/1").data.name;
|
|
69
82
|
```
|
|
70
83
|
|
|
84
|
+
### Interceptors
|
|
85
|
+
|
|
86
|
+
You can also easily add the interceptors `before`, `after` and `error`:
|
|
87
|
+
|
|
71
88
|
```js
|
|
72
|
-
//
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
89
|
+
// Perform an action or request transformation before the request is sent
|
|
90
|
+
fch.before = async req => {
|
|
91
|
+
// Normalized request ready to be sent
|
|
92
|
+
...
|
|
93
|
+
return req;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
// Perform an action or data transformation after the request is finished
|
|
97
|
+
fch.after = async res => {
|
|
98
|
+
// Full response as just after the request is made
|
|
99
|
+
...
|
|
100
|
+
return res;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// Perform an action or data transformation when an error is thrown
|
|
104
|
+
fch.error = async err => {
|
|
105
|
+
// Need to re-throw if we want to throw on error
|
|
106
|
+
...
|
|
107
|
+
throw err;
|
|
108
|
+
|
|
109
|
+
// OR, resolve it as if it didn't fail
|
|
110
|
+
return err.response;
|
|
111
|
+
|
|
112
|
+
// OR, resolve it with a custom value
|
|
113
|
+
return { message: 'Request failed with a code ' + err.response.status };
|
|
114
|
+
};
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
## Dedupe
|
|
119
|
+
|
|
120
|
+
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
|
+
|
|
122
|
+
```js
|
|
123
|
+
fetch.mockOnce("a").mockOnce("b");
|
|
124
|
+
const res = await Promise.all([fch("/a"), fch("/a")]);
|
|
125
|
+
|
|
126
|
+
// Reuses the first response if two are launched in parallel
|
|
127
|
+
expect(res).toEqual(["a", "a"]);
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
You can disable this by setting either the global `fch.dedupe` option to `false` or by passing an option per request:
|
|
131
|
+
|
|
132
|
+
```js
|
|
133
|
+
// Globally set it for all calls
|
|
134
|
+
fch.dedupe = true; // [DEFAULT] Dedupes GET requests
|
|
135
|
+
fch.dedupe = false; // All fetch() calls trigger a network call
|
|
136
|
+
|
|
137
|
+
// Set it on a per-call basis
|
|
138
|
+
fch('/a', { dedupe: true }); // [DEFAULT] Dedupes GET requests
|
|
139
|
+
fch('/a', { dedupe: false }) // All fetch() calls trigger a network call
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
> We do not support deduping other methods right now besides `GET` right now
|
|
143
|
+
|
|
144
|
+
Note that opting out of deduping a request will _also_ make that request not be reusable, see this test for details:
|
|
145
|
+
|
|
146
|
+
```js
|
|
147
|
+
it("can opt out locally", async () => {
|
|
148
|
+
fetch.once("a").once("b").once("c");
|
|
149
|
+
const res = await Promise.all([
|
|
150
|
+
fch("/a"),
|
|
151
|
+
fch("/a", { dedupe: false }),
|
|
152
|
+
fch("/a"), // Reuses the 1st response, not the 2nd one
|
|
153
|
+
]);
|
|
154
|
+
|
|
155
|
+
expect(res).toEqual(["a", "b", "a"]);
|
|
156
|
+
expect(fetch.mock.calls.length).toEqual(2);
|
|
77
157
|
});
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
## How to
|
|
162
|
+
|
|
163
|
+
### Stop errors from throwing
|
|
164
|
+
|
|
165
|
+
While you can handle this on a per-request basis, if you want to overwrite the global behavior you can just do:
|
|
166
|
+
|
|
167
|
+
```js
|
|
168
|
+
import fch from 'fch';
|
|
169
|
+
fch.error = error => error.response;
|
|
170
|
+
|
|
171
|
+
const res = fch('/notfound');
|
|
172
|
+
expect(res.status).toBe(404);
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Just that, then when there's an error it'll just return as usual, e.g.:
|
|
176
|
+
|
|
177
|
+
```js
|
|
178
|
+
import fch from 'fch';
|
|
179
|
+
fch.error = error => error.response;
|
|
180
|
+
|
|
181
|
+
const res = fch('/notfound');
|
|
182
|
+
expect(res.status).toBe(404);
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Return the full response
|
|
186
|
+
|
|
187
|
+
By default a successful request will just return the data. However this one is configurable on a global level:
|
|
188
|
+
|
|
189
|
+
```js
|
|
190
|
+
import fch from 'fch';
|
|
191
|
+
fch.output = 'response'; // Valid values are 'body' (default) or 'response'
|
|
192
|
+
|
|
193
|
+
const res = await fch('/hello');
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Or on a per-request level:
|
|
197
|
+
|
|
198
|
+
```js
|
|
199
|
+
import fch from 'fch';
|
|
200
|
+
|
|
201
|
+
// Valid values are 'body' (default) or 'response'
|
|
202
|
+
const res = await fch('/hello', { output: 'response' });
|
|
203
|
+
|
|
204
|
+
// Does not affect others
|
|
205
|
+
const body = await fch('/hello');
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
### Set a base URL
|
|
210
|
+
|
|
211
|
+
There's a configuration parameter for that:
|
|
212
|
+
|
|
213
|
+
```js
|
|
214
|
+
import fch from 'fch';
|
|
215
|
+
fch.baseUrl = 'https://api.filemon.io/';
|
|
216
|
+
|
|
217
|
+
// Calls "https://api.filemon.io/blabla/"
|
|
218
|
+
const body = await fch.get('/blabla/');
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
### Set the authorization headers
|
|
223
|
+
|
|
224
|
+
You can set that globally as a header:
|
|
225
|
+
|
|
226
|
+
```js
|
|
227
|
+
import fch from 'fch';
|
|
228
|
+
fch.headers.Authorization = 'bearer abc';
|
|
229
|
+
|
|
230
|
+
const me = await fch('/users/me');
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Globally on a per-request basis, for example if you take the value from localStorage:
|
|
234
|
+
|
|
235
|
+
```js
|
|
236
|
+
import fch from 'fch';
|
|
237
|
+
|
|
238
|
+
// All the requests will add the Authorization header when the token is
|
|
239
|
+
// in localStorage
|
|
240
|
+
fch.before = req => {
|
|
241
|
+
if (localStorage.get('token')) {
|
|
242
|
+
req.headers.Authorization = 'bearer ' + localStorage.get('token');
|
|
243
|
+
}
|
|
244
|
+
return req;
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
const me = await fch('/users/me');
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
Or on a per-request basis, though we wouldn't recommend this:
|
|
251
|
+
|
|
252
|
+
```js
|
|
253
|
+
import fch from 'fch';
|
|
254
|
+
|
|
255
|
+
const me = await fch('/users/me', { headers: { Authorization: 'bearer abc' } });
|
|
81
256
|
```
|
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});
|