fch 4.1.5 → 5.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.min.js +2 -1
- package/package.json +11 -21
- package/readme.md +180 -91
package/index.min.js
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
|
|
1
|
+
var d=async e=>(e=await e,Array.isArray(e)?await Promise.all(e.map(d)):e),y=(e,n)=>(...a)=>(r=>r instanceof RegExp?r.test.bind(r):r)(e).call(n,...a),k=(e,n)=>async(a,r,t)=>({value:a,extra:await y(e,n)(a,r,t)}),D=({extra:e})=>e,T=({value:e})=>e,j={every:async(e,n,a)=>{for(let r=0;r<e.length;r++)if(!await y(n,a)(e[r],r,e))return!1;return!0},filter:async(e,n,a)=>(await d(e.map(k(n,a)))).filter(D).map(T),find:async(e,n,a)=>{for(let r=0;r<e.length;r++)if(await y(n,a)(e[r],r,e))return e[r]},findIndex:async(e,n,a)=>{for(let r=0;r<e.length;r++)if(await y(n,a)(e[r],r,e))return r;return-1},forEach:async(e,n,a)=>(await d(e.map(k(n,a))),e),reduce:async(e,n,a)=>{let r=a!==void 0;r||(a=e[0]);for(let t=r?0:1;t<e.length;t++)a=await y(n)(a,e[t],t,e);return a},reduceRight:async(e,n,a)=>{let r=a!==void 0;r||(a=e[e.length-1]);for(let t=e.length-(r?1:2);t>=0;t--)a=await y(n)(a,e[t],t,e);return a},some:async(e,n,a)=>{for(let r=0;r<e.length;r++)if(await y(n,a)(e[r],r,e))return!0;return!1}},C=(e,n)=>(a,r)=>r==="then"?(...t)=>d(e).then(...t):r==="catch"?(...t)=>p(d(e).catch(...t)):h(d(e).then(t=>typeof r=="symbol"?t[r]:r in n?h((...o)=>n[r](t,...o),n):typeof t=="number"&&r in n.number?h((...o)=>n.number[r](t,...o),n):typeof t=="string"&&r in n.string?h((...o)=>n.string[r](t,...o),n):Array.isArray(t)&&r in n.array?h((...o)=>n.array[r](t,...o),n):t[r]&&t[r].bind?h(t[r].bind(t),n):h(t[r],n)),n),F=(e,n)=>(a,r,t)=>h(d(e).then(o=>{return typeof o!="function"?(c=`You tried to call "${JSON.stringify(o)}" (${typeof o}) as a function, but it is not.`,Promise.reject(new Error(c))):o(...t);var c}),n),h=(e,n)=>new Proxy(()=>{},{get:C(e,n),apply:F(e,n)});function p(e,{number:n,string:a,array:r,...t}={}){return typeof e=="function"?(...o)=>p(Promise.all(o).then(c=>e(...c)),{number:n,string:a,array:r,...t}):new Proxy({},{get:C(e,{number:{...n},string:{...a},array:{...j,...r},...t})})}function w(e){e.expire||(e.expire=0);let n=new Map,a=async r=>{if(!n.has(r))return!1;let{time:t,expire:o}=n.get(r);return!(!o||new Date().getTime()-t>o*1e3)};return{get:async r=>{if(!await a(r))return null;let{data:o}=n.get(r);return o},set:async(r,t,o={})=>{let c=new Date().getTime(),f=o.EX||o.expire||e.expire;return n.set(r,{time:c,expire:f,data:t})},keys:async()=>[...await n.keys()],del:async r=>n.delete(r),exists:a,flushAll:()=>n.clear()}}var M=/(-?(?:\d+\.?\d*|\d*\.?\d+)(?:e[-+]?\d+)?)\s*([\p{L}]*)/iu;s.millisecond=s.ms=1;s.second=s.sec=s.s=s[""]=s.ms*1e3;s.minute=s.min=s.m=s.s*60;s.hour=s.hr=s.h=s.m*60;s.day=s.d=s.h*24;s.week=s.wk=s.w=s.d*7;s.year=s.yr=s.y=s.d*365.25;s.month=s.b=s.y/12;function s(e=""){if(!e)return 0;if(e===!0)return 60*60*1e3;if(typeof e=="number")return e;e=e.toLowerCase().replace(/[,_]/g,"");let[n,a,r]=M.exec(e)||[];r=s[r]||s[r.replace(/s$/,"")]||1e3;let t=r*parseFloat(a,10);return Math.abs(Math.round(t))}var O=e=>!e||e instanceof FormData||typeof(e.pipe||e.pipeTo)=="function"?!1:typeof e=="object"||Array.isArray(e),b=e=>{if(typeof e!="object")return e;for(let n in e)e[n]===void 0&&delete e[n];return e},g=class extends Error{constructor(n){let a="Error "+n.status;super(a),this.response=n,this.message=a}},B=(e,n,a)=>{let[r,t={}]=e.split("?"),o=new URLSearchParams(Object.fromEntries([...new URLSearchParams(b(n)),...new URLSearchParams(b(t))])).toString();return o&&(r=r+"?"+o),a?new URL(r.replace(/^\//,""),a).href:r},J=e=>{let n={};for(let[a,r]of Object.entries(e))n[a.toLowerCase()]=r;return n},v=async e=>{let n=e.headers.get("content-type"),a=n&&n.includes("application/json"),r=await e.clone().text();return a?JSON.parse(r):r},P=async e=>{let n={status:e.status,statusText:e.statusText,headers:{}};for(let a of e.headers.keys())n.headers[a.toLowerCase()]=e.headers.get(a);if(!e.ok)throw new g(e);return n.body=await v(e),n},L=(e,{ref:n,after:a,error:r,output:t})=>fetch(e.url,e).then(async o=>{if(n.res=o,o.ok&&t==="stream")return o.body;if(o.ok&&o[t]&&typeof o[t]=="function")return o[t]();let c=a(await P(o));if(t==="body")return c.body;if(t==="response")return c;if(t==="raw")return o.clone();throw new Error(`Invalid option output="${t}"`)}).catch(r),$=e=>e.method==="get",q=e=>e.method+":"+e.url,K=({store:e,...n}={},a)=>{let r={store:e?p(e):w({expire:s(n.expire)}),shouldCache:$,createKey:q,...n};return r.clear=()=>Promise.resolve(r.store).then(t=>t?.flushAll()),a&&(r.shouldCache=()=>!1),r};function x(e={}){let n={},a={},t=p(async(o="/",c={})=>{let{output:f,before:S,after:E,error:U,...i}={...t,...c},l={...t.cache},A=u=>["number","string","boolean"].includes(typeof u);if(l.expire=s([c.cache?.expire,c.cache,l?.expire,l].find(A)),i.url=B(o,{...t.query,...c.query},i.baseUrl??i.baseURL),i.method=i.method.toLowerCase()||"GET",i.headers=J({...t.headers,...c.headers}),i.body instanceof SubmitEvent&&(i.body=new FormData(i.body.target)),i.body instanceof HTMLFormElement&&(i.body=new FormData(i.body)),O(i.body)&&(i.body=JSON.stringify(b(i.body)),i.headers["content-type"]="application/json"),i=S(i),l.shouldCache(i)&&l.expire){let u=l.createKey(i);if(l.store?.then){let R=await l.store;l.store=R}if(await l.store.exists(u))return l.store.get(u);if(n[u])return n[u];let m;try{n[u]=L(i,{ref:a,cache:l,output:f,error:U,after:E}),m=await n[u]}finally{delete n[u]}return await l.store.set(u,m,{EX:l.expire}),m}return L(i,{ref:a,output:f,error:U,after:E})},{text:()=>a.res.text(),json:()=>a.res.json(),blob:()=>a.res.blob(),stream:()=>a.res.body,arrayBuffer:()=>a.res.arrayBuffer(),formData:()=>a.res.formData(),body:()=>v(a.res),clone:()=>a.res.clone(),raw:()=>a.res.clone(),response:()=>P(a.res.clone())});return t.url=e.url??"/",t.method=e.method??"get",t.query=e.query??{},t.headers=e.headers??{},t.baseUrl=e.baseUrl??e.baseURL??null,["number","string","boolean"].includes(typeof e.cache)&&(e.cache={expire:e.cache}),t.cache=K(e.cache,e.cache?.expire===!1||e.cache?.expire===0),t.output=e.output??"body",t.credentials=e.credentials??"include",t.before=e.before??(o=>o),t.after=e.after??(o=>o),t.error=e.error??(o=>Promise.reject(o)),t.get=(o,c)=>t(o,{method:"get",...c}),t.head=(o,c)=>t(o,{method:"head",...c}),t.post=(o,c,f)=>t(o,{method:"post",body:c,...f}),t.patch=(o,c,f)=>t(o,{method:"patch",body:c,...f}),t.put=(o,c,f)=>t(o,{method:"put",body:c,...f}),t.delete=(o,c)=>t(o,{method:"delete",...c}),t.del=t.delete,t.create=x,t}typeof window<"u"&&(window.fch=x());var _=x();export{x as create,_ as default,s as parse};
|
|
2
|
+
//# sourceMappingURL=index.min.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fch",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "5.1.0",
|
|
4
4
|
"description": "Fetch interface with better promises, deduplication, defaults, etc.",
|
|
5
5
|
"homepage": "https://github.com/franciscop/fetch",
|
|
6
6
|
"repository": "https://github.com/franciscop/fetch.git",
|
|
@@ -9,10 +9,10 @@
|
|
|
9
9
|
"author": "Francisco Presencia <public@francisco.io> (https://francisco.io/)",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"scripts": {
|
|
12
|
-
"build": "
|
|
12
|
+
"build": "esbuild src/index.js --bundle --minify --sourcemap --outfile=index.min.js --format=esm",
|
|
13
13
|
"size": "echo $(gzip -c index.min.js | wc -c) bytes",
|
|
14
14
|
"start": "node --experimental-vm-modules node_modules/jest/bin/jest.js --watch --coverage --detectOpenHandles",
|
|
15
|
-
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage --detectOpenHandles"
|
|
15
|
+
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage --detectOpenHandles && check-dts"
|
|
16
16
|
},
|
|
17
17
|
"keywords": [
|
|
18
18
|
"fetch",
|
|
@@ -22,40 +22,30 @@
|
|
|
22
22
|
"async",
|
|
23
23
|
"ajax"
|
|
24
24
|
],
|
|
25
|
-
"main": "
|
|
25
|
+
"main": "index.min.js",
|
|
26
26
|
"files": [
|
|
27
|
-
"
|
|
27
|
+
"index.min.js"
|
|
28
28
|
],
|
|
29
|
+
"types": "index.d.ts",
|
|
29
30
|
"type": "module",
|
|
30
31
|
"engines": {
|
|
31
32
|
"node": ">=18.0.0"
|
|
32
33
|
},
|
|
33
34
|
"devDependencies": {
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"babel-loader": "^8.2.2",
|
|
38
|
-
"babel-polyfill": "^6.26.0",
|
|
35
|
+
"check-dts": "^0.7.2",
|
|
36
|
+
"dotenv": "^16.3.1",
|
|
37
|
+
"esbuild": "^0.19.2",
|
|
39
38
|
"jest": "^28.1.0",
|
|
40
39
|
"jest-environment-jsdom": "^28.1.0",
|
|
41
40
|
"jest-fetch-mock": "^3.0.3",
|
|
42
|
-
"
|
|
43
|
-
"rollup-plugin-babel": "^4.4.0",
|
|
44
|
-
"rollup-plugin-node-resolve": "^5.2.0",
|
|
45
|
-
"rollup-plugin-terser": "^5.2.0",
|
|
41
|
+
"redis": "^4.6.10",
|
|
46
42
|
"swear": "^1.1.2"
|
|
47
43
|
},
|
|
48
44
|
"jest": {
|
|
49
45
|
"testEnvironment": "jest-environment-node",
|
|
50
46
|
"transform": {},
|
|
51
47
|
"setupFiles": [
|
|
52
|
-
"./setup.js"
|
|
53
|
-
]
|
|
54
|
-
},
|
|
55
|
-
"babel": {
|
|
56
|
-
"presets": [
|
|
57
|
-
"@babel/preset-env",
|
|
58
|
-
"@babel/preset-react"
|
|
48
|
+
"./test/setup.js"
|
|
59
49
|
]
|
|
60
50
|
}
|
|
61
51
|
}
|
package/readme.md
CHANGED
|
@@ -11,31 +11,30 @@ console.log(mew);
|
|
|
11
11
|
// As an API abstraction
|
|
12
12
|
const api = fch.create({ baseUrl: "https://pokeapi.co/" });
|
|
13
13
|
const mew = await api.get("/pokemon/150");
|
|
14
|
-
await api.patch("/pokemon/150", {
|
|
14
|
+
await api.patch("/pokemon/150", { type: "psychic" });
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
- Create instances with shared options across requests.
|
|
18
|
-
-
|
|
19
|
-
- Automatically decode JSON
|
|
20
|
-
- Await/Async Promises; `>= 400 and <= 100` will
|
|
18
|
+
- Configurable **cache** that works in-memory, with redis, or others.
|
|
19
|
+
- Automatically encode and decode JSON bodies.
|
|
20
|
+
- Await/Async Promises; `>= 400 and <= 100` will throw an error.
|
|
21
21
|
- Credentials: "include" by default
|
|
22
22
|
- Interceptors: `before` the request, `after` the response and catch with `error`.
|
|
23
|
-
-
|
|
24
|
-
- Works the same way in Node.js and the browser.
|
|
23
|
+
- Designed for both Node.js and the browser through its extensible cache system.
|
|
25
24
|
- No dependencies; include it with a simple `<script>` on the browser.
|
|
25
|
+
- Full Types definitions so you get nice autocomplete
|
|
26
26
|
|
|
27
27
|
```js
|
|
28
|
-
import
|
|
28
|
+
import fch from "fch";
|
|
29
|
+
|
|
30
|
+
const api = fch.create({ baseUrl, headers, ...options });
|
|
29
31
|
|
|
30
32
|
api.get(url, { headers, ...options });
|
|
31
33
|
api.head(url, { headers, ...options });
|
|
32
|
-
api.post(url,
|
|
33
|
-
api.patch(url,
|
|
34
|
-
api.put(url,
|
|
35
|
-
api.del(url, {
|
|
36
|
-
api.delete(url, { body, headers, ...options });
|
|
37
|
-
|
|
38
|
-
api.create({ url, body, headers, ...options });
|
|
34
|
+
api.post(url, body, { headers, ...options });
|
|
35
|
+
api.patch(url, body, { headers, ...options });
|
|
36
|
+
api.put(url, body, { headers, ...options });
|
|
37
|
+
api.del(url, { headers, ...options });
|
|
39
38
|
```
|
|
40
39
|
|
|
41
40
|
| Options | Default | Description |
|
|
@@ -47,7 +46,7 @@ api.create({ url, body, headers, ...options });
|
|
|
47
46
|
| [`query`](#query) | `{}` | Add query parameters to the URL |
|
|
48
47
|
| [`headers`](#headers) | `{}` | Shared headers across all requests |
|
|
49
48
|
| [`output`](#output) | `"body"` | The return value of the API call |
|
|
50
|
-
| [`
|
|
49
|
+
| [`cache`](#cache) | `{ expire: 0 }` | How long to reuse the response body |
|
|
51
50
|
| [`before`](#interceptors) | `req => req` | Process the request before sending it |
|
|
52
51
|
| [`after`](#interceptors) | `res => res` | Process the response before returning it |
|
|
53
52
|
| [`error`](#interceptors) | `err => throw err` | Process errors before returning them |
|
|
@@ -93,12 +92,14 @@ api.headers = {}; // Merged with the headers on a per-request basis
|
|
|
93
92
|
|
|
94
93
|
// Control simple variables
|
|
95
94
|
api.output = "body"; // Return the parsed body; use 'response' or 'stream' otherwise
|
|
96
|
-
api.
|
|
95
|
+
api.cache = { expires: 0 }; // Avoid sending GET requests that were already sent recently
|
|
97
96
|
|
|
98
97
|
// Interceptors
|
|
99
98
|
api.before = (req) => req;
|
|
100
99
|
api.after = (res) => res;
|
|
101
|
-
api.error = (err) =>
|
|
100
|
+
api.error = (err) => {
|
|
101
|
+
throw err;
|
|
102
|
+
};
|
|
102
103
|
```
|
|
103
104
|
|
|
104
105
|
They can all be defined globally as shown above, passed manually as the options argument, or be used when [creating a new instance](#create-an-instance).
|
|
@@ -111,8 +112,8 @@ The HTTP method to make the request. When using the shorthand, it defaults to `G
|
|
|
111
112
|
import api from "fch";
|
|
112
113
|
|
|
113
114
|
api.get("/cats");
|
|
114
|
-
api.post("/cats", {
|
|
115
|
-
api.put(`/cats/3`, {
|
|
115
|
+
api.post("/cats", { name: "snowball" });
|
|
116
|
+
api.put(`/cats/3`, { name: "snowball" });
|
|
116
117
|
```
|
|
117
118
|
|
|
118
119
|
You can use it with the plain function as an option parameter. The methods are all lowercase but the option as a parameter is case insensitive; it can be either uppercase or lowercase:
|
|
@@ -136,24 +137,19 @@ import api from "fch";
|
|
|
136
137
|
|
|
137
138
|
const cats = await api.get("/cats");
|
|
138
139
|
console.log(cats);
|
|
139
|
-
const { id } = await api.post("/cats", {
|
|
140
|
-
await api.
|
|
140
|
+
const { id } = await api.post("/cats", { name: "snowbll" });
|
|
141
|
+
await api.patch(`/cats/${id}`, { name: "snowball" });
|
|
141
142
|
```
|
|
142
143
|
|
|
143
144
|
### Url
|
|
144
145
|
|
|
145
|
-
Specify where to send the request to. It
|
|
146
|
+
Specify where to send the request to. It must be the first argument in all methods and the base call:
|
|
146
147
|
|
|
147
148
|
```js
|
|
148
149
|
import api from "fch";
|
|
149
150
|
|
|
150
|
-
// Recommended way of specifying the Url
|
|
151
|
-
await api.post("/hello", { body: "...", headers: {} });
|
|
152
|
-
|
|
153
|
-
// These are also valid if you prefer their style; we won't judge
|
|
154
151
|
await api("/hello", { method: "post", body: "...", headers: {} });
|
|
155
|
-
await api(
|
|
156
|
-
await api.post({ url: "/hello", headers: {}, body: "..." });
|
|
152
|
+
await api.post("/hello", body, { headers: {} });
|
|
157
153
|
```
|
|
158
154
|
|
|
159
155
|
It can be either absolute or relative, in which case it'll use the local one in the page. It's recommending to set `baseUrl`:
|
|
@@ -169,82 +165,83 @@ api.get("/hello");
|
|
|
169
165
|
|
|
170
166
|
### Body
|
|
171
167
|
|
|
172
|
-
|
|
168
|
+
> These docs refer to the REQUEST body, for the RESPONSE body see the [**Output docs**](#output).
|
|
169
|
+
|
|
170
|
+
The `body` can be a string, a plain object|array, a FormData instance, a ReadableStream, a SubmitEvent or a HTMLFormElement. If it's a plain array or object, it'll be stringified and the header `application/json` will be added:
|
|
173
171
|
|
|
174
172
|
```js
|
|
175
|
-
import api from
|
|
173
|
+
import api from "api";
|
|
176
174
|
|
|
177
175
|
// Sending plain text
|
|
178
|
-
await api.post(
|
|
176
|
+
await api.post("/houses", "plain text");
|
|
179
177
|
|
|
180
|
-
// Will JSON.stringify it internally
|
|
181
|
-
await api.post(
|
|
178
|
+
// Will JSON.stringify it internally and add the JSON headers
|
|
179
|
+
await api.post("/houses", { id: 1, name: "Cute Cottage" });
|
|
182
180
|
|
|
183
181
|
// Send it as FormData
|
|
184
|
-
form.onsubmit = e =>
|
|
185
|
-
await api.post('/houses', { body: new FormData(e.target) });
|
|
186
|
-
};
|
|
187
|
-
```
|
|
182
|
+
form.onsubmit = (e) => api.post("/houses", new FormData(e.target));
|
|
188
183
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
> See more info in [**Output**](#output)
|
|
194
|
-
|
|
195
|
-
```js
|
|
196
|
-
const body = await api.get("/cats");
|
|
197
|
-
console.log(body);
|
|
198
|
-
// [{ id: 1, }, ...]
|
|
184
|
+
// We have some helpers so you can just pass the Event or <form> itself!
|
|
185
|
+
form.onsubmit = (e) => api.post("/houses", e); // does not work with jquery BTW
|
|
186
|
+
form.onsubmit = (e) => api.post("/houses", e.target);
|
|
187
|
+
form.onsubmit = (e) => api.post("/houses", new FormData(e.target));
|
|
199
188
|
```
|
|
200
189
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
When the function returns the response (if you set `output: "response"` as an option), then the body can be accessed as `response.body`:
|
|
204
|
-
|
|
205
|
-
```js
|
|
206
|
-
const response = await api.get("/cats", { output: "response" });
|
|
207
|
-
console.log(response.body);
|
|
208
|
-
// [{ id: 1, }, ...]
|
|
209
|
-
```
|
|
190
|
+
The methods `GET`, `HEAD` and `DELETE` do not accept a body and it'll be ignored if you try to force it into the options.
|
|
210
191
|
|
|
211
192
|
### Query
|
|
212
193
|
|
|
213
|
-
You can easily pass
|
|
194
|
+
You can easily pass url query parameters by using the option `query`:
|
|
214
195
|
|
|
215
196
|
```js
|
|
216
197
|
api.get("/cats", { query: { limit: 3 } });
|
|
217
198
|
// /cats?limit=3
|
|
218
199
|
```
|
|
219
200
|
|
|
220
|
-
While rare, some times you might want to persist a query parameter across requests and always include it; in that case, you can define it globally and it'll be added to every request:
|
|
201
|
+
While rare, some times you might want to persist a query parameter across requests and always include it; in that case, you can define it globally and it'll be added to every request, or on an instance:
|
|
221
202
|
|
|
222
203
|
```js
|
|
223
|
-
import
|
|
224
|
-
|
|
204
|
+
import fch from "fch";
|
|
205
|
+
fch.query.myparam = "abc";
|
|
225
206
|
|
|
226
|
-
|
|
207
|
+
fch.get("/cats", { query: { limit: 3 } });
|
|
227
208
|
// /cats?limit=3&myparam=abc
|
|
209
|
+
|
|
210
|
+
const api = fch.create({ query: { sort: "asc" } });
|
|
211
|
+
api.get("/cats");
|
|
212
|
+
// /cats?sort=asc&myparam=abc
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
For POST or others, they go into the options as usual:
|
|
216
|
+
|
|
217
|
+
```js
|
|
218
|
+
fch.post("/cats", { my: "body" }, { query: { abc: "def" } });
|
|
219
|
+
// /cats?abc=def
|
|
228
220
|
```
|
|
229
221
|
|
|
230
222
|
### Headers
|
|
231
223
|
|
|
232
|
-
You can define headers
|
|
224
|
+
You can define headers in 4 ways:
|
|
225
|
+
|
|
226
|
+
- Globally, in which case they'll be added to every request
|
|
227
|
+
- On an instance, so they are added every time you use that instance
|
|
228
|
+
- Per-request, so that they are only added to the current request.
|
|
229
|
+
- In the `before` interceptor, which again can be globa, on an instance, or per-request
|
|
233
230
|
|
|
234
231
|
```js
|
|
235
|
-
import
|
|
232
|
+
import fch from "fch";
|
|
236
233
|
|
|
237
234
|
// Globally, so they are reused across all requests
|
|
238
|
-
|
|
235
|
+
fch.headers.a = "b";
|
|
239
236
|
|
|
240
237
|
// With an interceptor, in case you need dynamic headers per-request
|
|
241
|
-
|
|
238
|
+
fch.before = (req) => {
|
|
242
239
|
req.headers.c = "d";
|
|
243
240
|
return req;
|
|
244
241
|
};
|
|
245
242
|
|
|
246
243
|
// Set them for this single request:
|
|
247
|
-
|
|
244
|
+
fch.get("/hello", { headers: { e: "f" } });
|
|
248
245
|
// Total headers on the request:
|
|
249
246
|
// { a: 'b', c: 'd', e: 'f' }
|
|
250
247
|
```
|
|
@@ -252,6 +249,7 @@ api.get("/hello", { headers: { e: "f" } });
|
|
|
252
249
|
When to use each?
|
|
253
250
|
|
|
254
251
|
- If you need headers shared across all requests, like an API key, then the global one is the best place.
|
|
252
|
+
- If it's all the requests to only certain endpoint, like we are communicating to multiple endpoints use the instance
|
|
255
253
|
- When you need to extract them dynamically from somewhere it's better to use the .before() interceptor. An example would be the user Authorization token.
|
|
256
254
|
- When it changes on each request, it's not consistent or it's an one-off, use the option argument.
|
|
257
255
|
|
|
@@ -264,6 +262,8 @@ const cats = await api.get("/cats");
|
|
|
264
262
|
console.log(cats); // [{ id: 1, name: 'Whiskers', ... }, ...]
|
|
265
263
|
```
|
|
266
264
|
|
|
265
|
+
> Note: for your typical API, we recommend sending the proper headers from the API and **not** using the more advanced options below.
|
|
266
|
+
|
|
267
267
|
For more expressive control, you can use the **`output` option** (either as a default when [creating an instance](#create-an-instance) or with each call), or using a method:
|
|
268
268
|
|
|
269
269
|
```js
|
|
@@ -319,53 +319,142 @@ For example, return the raw body as a `ReadableStream` with the option `stream`:
|
|
|
319
319
|
```js
|
|
320
320
|
const stream = await api.get('/cats', { output: 'stream' });
|
|
321
321
|
stream.pipeTo(...);
|
|
322
|
+
// or
|
|
323
|
+
const stream = await api.get('/cats').stream();
|
|
324
|
+
stream.pipeTo(...);
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
### Cache
|
|
328
|
+
|
|
329
|
+
The cache (disabled by default) is a great method to reduce the number of API requests we make. While it's possible to modify the global fch to add a cache, we recommend to always use it per-instance or per-request. Options:
|
|
330
|
+
|
|
331
|
+
- `expire`: the amount of time the cached data will be valid for, it can be a number (seconds) or a string such as `1hour`, `1week`, etc. (based on [parse-duration](https://github.com/jkroso/parse-duration))
|
|
332
|
+
- `store`: the store where the cached data will be stored.
|
|
333
|
+
- `shouldCache`: a function that returns a boolean to determine whether the current data should go through the cache process.
|
|
334
|
+
- `createKey`: a function that takes the request and generates a unique key for that request, which will be the same for the next time that same request is made. Defaults to method+url.
|
|
335
|
+
|
|
336
|
+
> Note: cache should only be used through `fch.create({ cache: ... })`, not through the global instance.
|
|
337
|
+
|
|
338
|
+
To activate the cache, we just need to set a time as such:
|
|
339
|
+
|
|
340
|
+
```js
|
|
341
|
+
// This API has 1h by default:
|
|
342
|
+
const api = fch.create({
|
|
343
|
+
baseUrl: 'https://api.myweb.com/'
|
|
344
|
+
cache: '1h'
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
// This specific call will be cached for 20s
|
|
348
|
+
api.get('/somedata', { cache: '20s' });
|
|
322
349
|
```
|
|
323
350
|
|
|
324
|
-
|
|
351
|
+
Your cache will have a `store`; by default we create an in-memory store, but you can also use `redis` and it's fully compatible. Note now `cache` is now an object:
|
|
352
|
+
|
|
353
|
+
```js
|
|
354
|
+
import fch from "fch";
|
|
355
|
+
import { createClient } from "redis";
|
|
356
|
+
|
|
357
|
+
// You can either pass the store instance, or a promise that will
|
|
358
|
+
// return the instance. In this case we are doing the latter
|
|
359
|
+
const store = createClient().connect();
|
|
360
|
+
|
|
361
|
+
const api = fch.create({
|
|
362
|
+
cache: {
|
|
363
|
+
store: store,
|
|
364
|
+
expire: "1h",
|
|
365
|
+
},
|
|
366
|
+
});
|
|
367
|
+
```
|
|
325
368
|
|
|
326
|
-
|
|
369
|
+
That's the basic usage, but "invalidating cache" is not one of the complex topics in CS for no reason. Let's dig deeper. To clear the cache, you can call `cache.clear()` at any time:
|
|
327
370
|
|
|
328
371
|
```js
|
|
329
|
-
|
|
330
|
-
const res = await Promise.all([fch("/a"), fch("/a")]);
|
|
372
|
+
const api = fch.create({ cache: "1h" });
|
|
331
373
|
|
|
332
|
-
//
|
|
333
|
-
|
|
374
|
+
// Remove them all
|
|
375
|
+
await api.cache.clear();
|
|
334
376
|
```
|
|
335
377
|
|
|
336
|
-
You can
|
|
378
|
+
You can always access the store of the instance through `api.cache.store`, so we can do low-level operations on the store as such if needed:
|
|
337
379
|
|
|
338
380
|
```js
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
381
|
+
import fch from "fch";
|
|
382
|
+
import { createClient } from "redis";
|
|
383
|
+
|
|
384
|
+
// Initialize it straight away
|
|
385
|
+
const api = fch.create({
|
|
386
|
+
cache: {
|
|
387
|
+
store: createClient().connect(),
|
|
388
|
+
expire: "1h",
|
|
389
|
+
},
|
|
390
|
+
});
|
|
342
391
|
|
|
343
|
-
//
|
|
344
|
-
|
|
345
|
-
fch("/a", { dedupe: false }); // All fetch() calls trigger a network call
|
|
392
|
+
// Later on, maybe in a different place
|
|
393
|
+
await api.cache.store.flushDB();
|
|
346
394
|
```
|
|
347
395
|
|
|
348
|
-
|
|
396
|
+
Finally, the other two bits that are relevant for cache are `shouldCache` and `createKey`. For the most basic examples the default probably works, but you might want more advanced configuration:
|
|
349
397
|
|
|
350
|
-
|
|
398
|
+
```js
|
|
399
|
+
const api = fch.create({
|
|
400
|
+
cache: {
|
|
401
|
+
// Default shouldCache; Note the lowercase
|
|
402
|
+
shouldCache: (request) => request.method === "get",
|
|
403
|
+
|
|
404
|
+
// Default createKey;
|
|
405
|
+
createKey: (request) => request.method + ":" + request.url,
|
|
406
|
+
},
|
|
407
|
+
});
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
For example, if you want to differentiate the auth requests from the non-auth requests, you can do it so:
|
|
351
411
|
|
|
352
412
|
```js
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
413
|
+
import api from "./api";
|
|
414
|
+
|
|
415
|
+
const onLogin = (user) => {
|
|
416
|
+
// ... Do some other stuff
|
|
417
|
+
|
|
418
|
+
// Remove the old requests since we were not auth'ed yet
|
|
419
|
+
api.cache.clear();
|
|
360
420
|
|
|
361
|
-
|
|
362
|
-
|
|
421
|
+
// Create a key unique for this user
|
|
422
|
+
api.cache.createKey = (req) => user.id + ":" + req.method + ":" + req.url;
|
|
423
|
+
};
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
Or maybe you just want to NOT cache any of the requests that have an `Authorization` header, you can do so:
|
|
427
|
+
|
|
428
|
+
```js
|
|
429
|
+
const api = fch.create({
|
|
430
|
+
cache: {
|
|
431
|
+
expire: "1week",
|
|
432
|
+
|
|
433
|
+
// Note the lowercase in both! we normalize them to be lowercase
|
|
434
|
+
shouldCache: (req) => req.method === "get" && !req.headers.authorization,
|
|
435
|
+
},
|
|
363
436
|
});
|
|
364
437
|
```
|
|
365
438
|
|
|
439
|
+
It is this flexible since you can use fch both in the front-end and back-end, so usually in each of them you might want to follow a slightly different strategy.
|
|
440
|
+
|
|
441
|
+
#### Creating a store.
|
|
442
|
+
|
|
443
|
+
You might want to create a custom store. Please have a look at `src/store.js`, but basically you need an object that implements these methods:
|
|
444
|
+
|
|
445
|
+
```js
|
|
446
|
+
type Store = {
|
|
447
|
+
get: (key: string) => Promise<any>,
|
|
448
|
+
set: (key: string, value: any, options?: { EX: number }) => Promise<null>,
|
|
449
|
+
del: (key: string) => Promise<null>,
|
|
450
|
+
exists: (key: string) => Promise<boolean>,
|
|
451
|
+
flushAll: () => Promise<any>,
|
|
452
|
+
};
|
|
453
|
+
```
|
|
454
|
+
|
|
366
455
|
### Interceptors
|
|
367
456
|
|
|
368
|
-
You can also
|
|
457
|
+
You can also add the interceptors `before`, `after` and `error`:
|
|
369
458
|
|
|
370
459
|
- `before`: Called when the request is fully formed, but before actually launching it.
|
|
371
460
|
- `after`: Called just after the response is created and if there was no error, but before parsing anything else.
|