fch 6.0.0 → 6.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.d.ts +2 -8
- package/index.min.js +1 -1
- package/package.json +1 -1
- package/readme.md +38 -87
package/index.d.ts
CHANGED
|
@@ -5,12 +5,6 @@ type Store = {
|
|
|
5
5
|
has?: (key: string) => Promise<boolean>;
|
|
6
6
|
clear?: () => Promise<any>;
|
|
7
7
|
};
|
|
8
|
-
type Cache = boolean | number | string | Store | {
|
|
9
|
-
expire?: number | string;
|
|
10
|
-
store?: Store;
|
|
11
|
-
shouldCache?: (request: any) => boolean;
|
|
12
|
-
createKey?: (request: any) => string;
|
|
13
|
-
};
|
|
14
8
|
type Headers = {
|
|
15
9
|
[name: string]: string;
|
|
16
10
|
};
|
|
@@ -28,7 +22,7 @@ type Options = {
|
|
|
28
22
|
headers?: Headers;
|
|
29
23
|
baseUrl?: string;
|
|
30
24
|
baseURL?: string;
|
|
31
|
-
cache?:
|
|
25
|
+
cache?: Store;
|
|
32
26
|
output?: string;
|
|
33
27
|
credentials?: string;
|
|
34
28
|
before?: (req: any) => any;
|
|
@@ -63,7 +57,7 @@ interface FchInstance {
|
|
|
63
57
|
headers: Headers;
|
|
64
58
|
baseUrl: string | null;
|
|
65
59
|
baseURL: string | null;
|
|
66
|
-
cache:
|
|
60
|
+
cache: Store | null;
|
|
67
61
|
output: string;
|
|
68
62
|
credentials: string;
|
|
69
63
|
before?: (req: any) => any;
|
package/index.min.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var
|
|
1
|
+
var S=async(i)=>(i=await i,Array.isArray(i)?await Promise.all(i.map(S)):i),N=(i,w)=>(...x)=>((n)=>n instanceof RegExp?n.test.bind(n):n)(i).call(w,...x),O=(i,w)=>async(x,n,m)=>({value:x,extra:await N(i,w)(x,n,m)}),_=({extra:i})=>i,D=({value:i})=>i,Q={every:async(i,w,x)=>{for(let n=0;n<i.length;n++)if(!await N(w,x)(i[n],n,i))return!1;return!0},filter:async(i,w,x)=>(await S(i.map(O(w,x)))).filter(_).map(D),find:async(i,w,x)=>{for(let n=0;n<i.length;n++)if(await N(w,x)(i[n],n,i))return i[n]},findIndex:async(i,w,x)=>{for(let n=0;n<i.length;n++)if(await N(w,x)(i[n],n,i))return n;return-1},forEach:async(i,w,x)=>(await S(i.map(O(w,x))),i),reduce:async(i,w,x)=>{let n=x!==void 0;n||(x=i[0]);for(let m=n?0:1;m<i.length;m++)x=await N(w)(x,i[m],m,i);return x},reduceRight:async(i,w,x)=>{let n=x!==void 0;n||(x=i[i.length-1]);for(let m=i.length-(n?1:2);m>=0;m--)x=await N(w)(x,i[m],m,i);return x},some:async(i,w,x)=>{for(let n=0;n<i.length;n++)if(await N(w,x)(i[n],n,i))return!0;return!1}},T=(i,w)=>(x,n)=>n==="then"?(...m)=>S(i).then(...m):n==="catch"?(...m)=>Y(S(i).catch(...m)):v(S(i).then((m)=>typeof n=="symbol"?m[n]:(n in w)?v((...P)=>w[n](m,...P),w):typeof m=="number"&&(n in w.number)?v((...P)=>w.number[n](m,...P),w):typeof m=="string"&&(n in w.string)?v((...P)=>w.string[n](m,...P),w):Array.isArray(m)&&(n in w.array)?v((...P)=>w.array[n](m,...P),w):m[n]&&m[n].bind?v(m[n].bind(m),w):v(m[n],w)),w),F=(i,w)=>(x,n,m)=>v(S(i).then((P)=>{return typeof P!="function"?(g=`You tried to call "${JSON.stringify(P)}" (${typeof P}) as a function, but it is not.`,Promise.reject(Error(g))):P(...m);var g}),w),v=(i,w)=>new Proxy(()=>{},{get:T(i,w),apply:F(i,w)});function Y(i,{number:w,string:x,array:n,...m}={}){return typeof i=="function"?(...P)=>Y(Promise.all(P).then((g)=>i(...g)),{number:w,string:x,array:n,...m}):new Proxy({},{get:T(i,{number:{...w},string:{...x},array:{...Q,...n},...m})})}var H=(i)=>{if(!i)return!1;if(i instanceof FormData)return!1;if(typeof(i.pipe||i.pipeTo)==="function")return!1;return typeof i==="object"||Array.isArray(i)},G=(i)=>{if(typeof i!=="object")return i;for(let w in i)if(i[w]===void 0)delete i[w];return i};class W extends Error{response;constructor(i){let w="Error "+i.status;super(w);this.response=i,this.message=w}}var R=(i,w,x)=>{let[n,m=""]=i.split("?"),P=new URLSearchParams(Object.fromEntries([...new URLSearchParams(G(w)),...new URLSearchParams(G(m))])).toString();if(P)n=n+"?"+P;if(!x)return n;return new URL(n.replace(/^\//,""),x).href},B=(i)=>{let w={};for(let[x,n]of Object.entries(i))w[x.toLowerCase()]=n;return w},X=async(i)=>{let w=i.headers.get("content-type"),x=w&&w.includes("application/json"),n=await i.clone().text();return x?JSON.parse(n):n},Z=async(i)=>{let w={status:i.status,statusText:i.statusText,headers:{},body:void 0};if(i.headers.forEach((x,n)=>{w.headers[n.toLowerCase()]=x}),!i.ok)throw new W(i);return w.body=await X(i),w},V=(i,{ref:w,after:x,error:n,output:m})=>{return fetch(i.url,i).then(async(P)=>{if(w.res=P,P.ok&&m==="stream")return P.body;if(P.ok&&P[m]&&typeof P[m]==="function")return P[m]();let g=x(await Z(P));if(m==="body")return g.body;else if(m==="response")return g;else if(m==="raw")return P.clone();else throw Error(`Invalid option output="${m}"`)}).catch(n)};function I(i={}){let w={},x={},m=Y(async(P="/",g={})=>{let{output:$,before:J,after:K,error:L,cache:z,...A}={...m,...g};if(A.url=R(P,{...m.query,...g.query},A.baseUrl??A.baseURL),A.method=(A.method||"get").toLowerCase(),A.headers=B({...m.headers,...g.headers}),typeof SubmitEvent<"u"&&A.body instanceof SubmitEvent||typeof HTMLFormElement<"u"&&A.body instanceof HTMLFormElement)A.body=new FormData(A.body);if(H(A.body))A.body=JSON.stringify(G(A.body)),A.headers["content-type"]="application/json";if(A=J?J(A):A,!z||A.method!=="get")return V(A,{ref:x,output:$,error:L,after:K});let E=A.method+":"+A.url,M=await z.get(E);if(M)return M;if(w[E])return w[E];let C;try{w[E]=V(A,{ref:x,output:$,error:L,after:K}),C=await w[E]}finally{delete w[E]}return await z.set(E,C),C},{text:()=>x.res.clone().text(),json:()=>x.res.clone().json(),blob:()=>x.res.clone().blob(),stream:()=>x.res.clone().body,arrayBuffer:()=>x.res.clone().arrayBuffer(),formData:()=>x.res.clone().formData(),body:()=>X(x.res.clone()),clone:()=>x.res.clone(),raw:()=>x.res.clone(),response:()=>Z(x.res.clone())});return m.url=i.url??"/",m.method=i.method??"get",m.query=i.query??{},m.headers=i.headers??{},m.baseUrl=i.baseUrl??i.baseURL??null,m.baseURL=i.baseUrl??i.baseURL??null,m.cache=i.cache??null,m.output=i.output??"body",m.credentials=i.credentials??"include",m.before=i.before??((P)=>P),m.after=i.after??((P)=>P),m.error=i.error??((P)=>Promise.reject(P)),m.get=(P,g)=>m(P,{method:"get",...g}),m.head=(P,g)=>m(P,{method:"head",...g}),m.post=(P,g,$)=>m(P,{method:"post",body:g,...$}),m.patch=(P,g,$)=>m(P,{method:"patch",body:g,...$}),m.put=(P,g,$)=>m(P,{method:"put",body:g,...$}),m.delete=(P,g)=>m(P,{method:"delete",...g}),m.del=m.delete,m.create=I,m}if(typeof window<"u")window.fch=I();var j=I();export{j as default,I as create};
|
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Fch [](https://www.npmjs.com/package/fch) [](https://github.com/franciscop/fetch/
|
|
1
|
+
# Fch [](https://www.npmjs.com/package/fch) [](https://github.com/franciscop/fetch/actions) [](https://bundlephobia.com/package/fch)
|
|
2
2
|
|
|
3
3
|
A tiny library to make API calls easier. Similar to Axios, but tiny size and simpler API:
|
|
4
4
|
|
|
@@ -17,7 +17,7 @@ await api.patch("/pokemon/150", { type: "psychic" });
|
|
|
17
17
|
- Create instances with shared options across requests.
|
|
18
18
|
- Configurable **cache** that works in-memory, with redis, or others.
|
|
19
19
|
- Automatically encode and decode JSON bodies.
|
|
20
|
-
- Await/Async Promises;
|
|
20
|
+
- Await/Async Promises; non-OK responses (status < 200 or >= 300) will throw an error.
|
|
21
21
|
- Credentials: "include" by default
|
|
22
22
|
- Interceptors: `before` the request, `after` the response and catch with `error`.
|
|
23
23
|
- Designed for both Node.js and the browser through its extensible cache system.
|
|
@@ -156,9 +156,9 @@ It can be either absolute or relative, in which case it'll use the local one in
|
|
|
156
156
|
|
|
157
157
|
```js
|
|
158
158
|
import api from "fch";
|
|
159
|
-
api.baseUrl = "https
|
|
159
|
+
api.baseUrl = "https://api.filemon.io/";
|
|
160
160
|
api.get("/hello");
|
|
161
|
-
// Called https
|
|
161
|
+
// Called https://api.filemon.io/hello
|
|
162
162
|
```
|
|
163
163
|
|
|
164
164
|
> Note: with Node.js you need to either set an absolute baseUrl or make the URL absolute
|
|
@@ -170,10 +170,10 @@ api.get("/hello");
|
|
|
170
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:
|
|
171
171
|
|
|
172
172
|
```js
|
|
173
|
-
import
|
|
173
|
+
import fch from "fch";
|
|
174
174
|
|
|
175
175
|
// Sending plain text
|
|
176
|
-
await
|
|
176
|
+
await fch.post("/houses", "plain text");
|
|
177
177
|
|
|
178
178
|
// Will JSON.stringify it internally and add the JSON headers
|
|
179
179
|
await api.post("/houses", { id: 1, name: "Cute Cottage" });
|
|
@@ -182,12 +182,12 @@ await api.post("/houses", { id: 1, name: "Cute Cottage" });
|
|
|
182
182
|
form.onsubmit = (e) => api.post("/houses", new FormData(e.target));
|
|
183
183
|
|
|
184
184
|
// We have some helpers so you can just pass the Event or <form> itself!
|
|
185
|
-
form.onsubmit = (e) => api.post("/houses", e);
|
|
185
|
+
form.onsubmit = (e) => api.post("/houses", e);
|
|
186
186
|
form.onsubmit = (e) => api.post("/houses", e.target);
|
|
187
187
|
form.onsubmit = (e) => api.post("/houses", new FormData(e.target));
|
|
188
188
|
```
|
|
189
189
|
|
|
190
|
-
The methods `GET
|
|
190
|
+
The methods `GET` and `HEAD` do not accept a body. For `DELETE`, while not common, you can pass a body via the options parameter if needed: `fch.delete('/resource', { body: {...} })`.
|
|
191
191
|
|
|
192
192
|
### Query
|
|
193
193
|
|
|
@@ -295,7 +295,7 @@ There are few options that can be specified:
|
|
|
295
295
|
- `output: "response"`: return the full response with the properties `body`, `headers`, `status`. The body will be parsed as JSON or plain TEXT depending on the headers. If you want the raw `response`, use `raw` or `clone` instead (see below in "raw" or "clone").
|
|
296
296
|
- `output: "stream"`: return a [web ReadableStream](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream) of the body as the result of the promise.
|
|
297
297
|
- `output: "arrayBuffer"`\*: returns an arrayBuffer of the response body.
|
|
298
|
-
- `output: "blob"`\*: returns
|
|
298
|
+
- `output: "blob"`\*: returns a Blob of the response body.
|
|
299
299
|
- `output: "clone"`\*: returns the raw Response, with the raw body. See also `raw` below.
|
|
300
300
|
- `output: "formData"`\* (might be unavailable): returns an instance of FormData with all the parsed data.
|
|
301
301
|
- `output: "json"`\*: attempts to parse the response as JSON.
|
|
@@ -326,21 +326,29 @@ stream.pipeTo(...);
|
|
|
326
326
|
|
|
327
327
|
### Cache
|
|
328
328
|
|
|
329
|
-
|
|
329
|
+
> We use **[polystore](https://polystore.dev/)** for Cache management.
|
|
330
|
+
|
|
331
|
+
The cache (disabled by default) is a great method to reduce the number of API requests we make. Provide a [polystore](https://polystore.dev/) Store instance directly to `fch.create()` (or per request) and fch will cache GET responses using whatever persistence that store offers. Set `cache` to `null` on a call to skip caching entirely. Legacy cache helpers such as `shouldCache`, `generateKey`, or expiry options have been removed; fch now relies purely on the semantics of the Polystore Store you pass in.
|
|
332
|
+
|
|
330
333
|
|
|
331
|
-
> Note: cache should only be used through `fch.create({ cache: ... })`, not through the global instance.
|
|
332
334
|
|
|
333
|
-
|
|
335
|
+
While a GET request is in flight, fch keeps an internal map of ongoing requests so additional calls reuse the same promise instead of triggering duplicate fetches. As soon as the request resolves, the result is written to your store (if available) and the entry is removed from the in-flight map.
|
|
336
|
+
|
|
337
|
+
To activate the cache, create a polystore store and pass it to fch:
|
|
334
338
|
|
|
335
339
|
```js
|
|
336
|
-
|
|
340
|
+
import kv from "polystore";
|
|
341
|
+
|
|
342
|
+
// This API reuses responses using an in-memory Map:
|
|
343
|
+
const cache = kv(new Map());
|
|
337
344
|
const api = fch.create({
|
|
338
|
-
|
|
339
|
-
|
|
345
|
+
cache,
|
|
346
|
+
baseUrl: "https://api.myweb.com/",
|
|
340
347
|
});
|
|
341
348
|
|
|
342
|
-
//
|
|
343
|
-
|
|
349
|
+
// Provide a different store per request if needed
|
|
350
|
+
const shortCache = kv(new Map());
|
|
351
|
+
api.get("/somedata", { cache: shortCache });
|
|
344
352
|
```
|
|
345
353
|
|
|
346
354
|
For more control, you can use **polystore** to create a custom cache store with different backends (in-memory, Redis, localStorage, etc.):
|
|
@@ -349,12 +357,12 @@ For more control, you can use **polystore** to create a custom cache store with
|
|
|
349
357
|
import fch from "fch";
|
|
350
358
|
import kv from "polystore";
|
|
351
359
|
|
|
352
|
-
// In-memory cache
|
|
353
|
-
const cache = kv(new Map())
|
|
360
|
+
// In-memory cache backed by a Map
|
|
361
|
+
const cache = kv(new Map());
|
|
354
362
|
|
|
355
363
|
const api = fch.create({
|
|
356
|
-
baseUrl:
|
|
357
|
-
cache
|
|
364
|
+
baseUrl: "https://api.myweb.com/",
|
|
365
|
+
cache,
|
|
358
366
|
});
|
|
359
367
|
```
|
|
360
368
|
|
|
@@ -365,12 +373,12 @@ import fch from "fch";
|
|
|
365
373
|
import kv from "polystore";
|
|
366
374
|
import { createClient } from "redis";
|
|
367
375
|
|
|
368
|
-
// Redis cache
|
|
376
|
+
// Redis-backed cache store
|
|
369
377
|
const redis = await createClient().connect();
|
|
370
|
-
const cache = kv(redis)
|
|
378
|
+
const cache = kv(redis);
|
|
371
379
|
|
|
372
380
|
const api = fch.create({
|
|
373
|
-
cache
|
|
381
|
+
cache,
|
|
374
382
|
});
|
|
375
383
|
```
|
|
376
384
|
|
|
@@ -379,7 +387,7 @@ That's the basic usage, but "invalidating cache" is not one of the complex topic
|
|
|
379
387
|
```js
|
|
380
388
|
import kv from "polystore";
|
|
381
389
|
|
|
382
|
-
const cache = kv(new Map())
|
|
390
|
+
const cache = kv(new Map());
|
|
383
391
|
const api = fch.create({ cache });
|
|
384
392
|
|
|
385
393
|
// Remove them all
|
|
@@ -394,7 +402,7 @@ import kv from "polystore";
|
|
|
394
402
|
import { createClient } from "redis";
|
|
395
403
|
|
|
396
404
|
const redis = await createClient().connect();
|
|
397
|
-
const cache = kv(redis)
|
|
405
|
+
const cache = kv(redis);
|
|
398
406
|
|
|
399
407
|
const api = fch.create({ cache });
|
|
400
408
|
|
|
@@ -402,64 +410,6 @@ const api = fch.create({ cache });
|
|
|
402
410
|
await redis.flushDB();
|
|
403
411
|
```
|
|
404
412
|
|
|
405
|
-
For advanced cache configuration, you can pass an object with additional options like `shouldCache` and `createKey`:
|
|
406
|
-
|
|
407
|
-
```js
|
|
408
|
-
import kv from "polystore";
|
|
409
|
-
|
|
410
|
-
const cache = kv(new Map()).expires("1h");
|
|
411
|
-
|
|
412
|
-
const api = fch.create({
|
|
413
|
-
cache: {
|
|
414
|
-
store: cache,
|
|
415
|
-
// Default shouldCache; Note the lowercase
|
|
416
|
-
shouldCache: (request) => request.method === "get",
|
|
417
|
-
|
|
418
|
-
// Default createKey;
|
|
419
|
-
createKey: (request) => request.method + ":" + request.url,
|
|
420
|
-
},
|
|
421
|
-
});
|
|
422
|
-
```
|
|
423
|
-
|
|
424
|
-
For example, if you want to differentiate the auth requests from the non-auth requests, you can do it so:
|
|
425
|
-
|
|
426
|
-
```js
|
|
427
|
-
import kv from "polystore";
|
|
428
|
-
|
|
429
|
-
const cache = kv(new Map()).expires("1h");
|
|
430
|
-
|
|
431
|
-
const api = fch.create({
|
|
432
|
-
cache: {
|
|
433
|
-
store: cache,
|
|
434
|
-
// Create a key unique for each user
|
|
435
|
-
createKey: (req) => user.id + ":" + req.method + ":" + req.url,
|
|
436
|
-
}
|
|
437
|
-
});
|
|
438
|
-
|
|
439
|
-
const onLogin = (user) => {
|
|
440
|
-
// Remove the old requests since we were not auth'ed yet
|
|
441
|
-
cache.clear();
|
|
442
|
-
};
|
|
443
|
-
```
|
|
444
|
-
|
|
445
|
-
Or maybe you just want to NOT cache any of the requests that have an `Authorization` header, you can do so:
|
|
446
|
-
|
|
447
|
-
```js
|
|
448
|
-
import kv from "polystore";
|
|
449
|
-
|
|
450
|
-
const cache = kv(new Map()).expires("1week");
|
|
451
|
-
|
|
452
|
-
const api = fch.create({
|
|
453
|
-
cache: {
|
|
454
|
-
store: cache,
|
|
455
|
-
// Note the lowercase in both! we normalize them to be lowercase
|
|
456
|
-
shouldCache: (req) => req.method === "get" && !req.headers.authorization,
|
|
457
|
-
},
|
|
458
|
-
});
|
|
459
|
-
```
|
|
460
|
-
|
|
461
|
-
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.
|
|
462
|
-
|
|
463
413
|
#### Creating a custom store
|
|
464
414
|
|
|
465
415
|
You can create a custom store that works with fch by implementing the polystore-compatible interface. See the [polystore documentation](https://polystore.dev/) for details on creating custom stores and adapters.
|
|
@@ -581,8 +531,9 @@ import fch from "fch";
|
|
|
581
531
|
// All the requests will add the Authorization header when the token is
|
|
582
532
|
// in localStorage
|
|
583
533
|
fch.before = (req) => {
|
|
584
|
-
|
|
585
|
-
|
|
534
|
+
const token = localStorage.getItem("token");
|
|
535
|
+
if (token) {
|
|
536
|
+
req.headers.Authorization = "bearer " + token;
|
|
586
537
|
}
|
|
587
538
|
return req;
|
|
588
539
|
};
|
|
@@ -696,7 +647,7 @@ axios.interceptors.request.use(fn);
|
|
|
696
647
|
fch.before = fn;
|
|
697
648
|
```
|
|
698
649
|
|
|
699
|
-
API size is also strikingly different, with **7.8kb** for Axios and **
|
|
650
|
+
API size is also strikingly different, with **7.8kb** for Axios and **2.1kb** for fch (gzipped).
|
|
700
651
|
|
|
701
652
|
As disadvantages, I can think of two major ones for `fch`:
|
|
702
653
|
|