fch 4.1.4 → 5.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.
Files changed (3) hide show
  1. package/index.min.js +2 -1
  2. package/package.json +10 -21
  3. package/readme.md +181 -91
package/index.min.js CHANGED
@@ -1 +1,2 @@
1
- const e=async r=>(r=await r,Array.isArray(r)?await Promise.all(r.map(e)):r),r=(e,r)=>(...t)=>(e=>e instanceof RegExp?e.test.bind(e):e)(e).call(r,...t),t=(e,t)=>async(n,a,o)=>({value:n,extra:await r(e,t)(n,a,o)}),n=({extra:e})=>e,a=({value:e})=>e,o={every:async(e,t,n)=>{for(let a=0;a<e.length;a++)if(!await r(t,n)(e[a],a,e))return!1;return!0},filter:async(r,o,s)=>(await e(r.map(t(o,s)))).filter(n).map(a),find:async(e,t,n)=>{for(let a=0;a<e.length;a++)if(await r(t,n)(e[a],a,e))return e[a]},findIndex:async(e,t,n)=>{for(let a=0;a<e.length;a++)if(await r(t,n)(e[a],a,e))return a;return-1},forEach:async(r,n,a)=>(await e(r.map(t(n,a))),r),reduce:async(e,t,n)=>{const a=void 0!==n;a||(n=e[0]);for(let o=a?0:1;o<e.length;o++)n=await r(t)(n,e[o],o,e);return n},reduceRight:async(e,t,n)=>{const a=void 0!==n;a||(n=e[e.length-1]);for(let o=e.length-(a?1:2);o>=0;o--)n=await r(t)(n,e[o],o,e);return n},some:async(e,t,n)=>{for(let a=0;a<e.length;a++)if(await r(t,n)(e[a],a,e))return!0;return!1}},s=(r,t)=>(n,a)=>"then"===a?(...t)=>e(r).then(...t):"catch"===a?(...t)=>l(e(r).catch(...t)):u(e(r).then(e=>"symbol"==typeof a?e[a]:a in t?u((...r)=>t[a](e,...r),t):"number"==typeof e&&a in t.number?u((...r)=>t.number[a](e,...r),t):"string"==typeof e&&a in t.string?u((...r)=>t.string[a](e,...r),t):Array.isArray(e)&&a in t.array?u((...r)=>t.array[a](e,...r),t):e[a]&&e[a].bind?u(e[a].bind(e),t):u(e[a],t)),t),i=(r,t)=>(n,a,o)=>u(e(r).then(e=>{return"function"!=typeof e?(r=`You tried to call "${JSON.stringify(e)}" (${typeof e}) as a function, but it is not.`,Promise.reject(new Error(r))):e(...o);var r}),t),u=(e,r)=>new Proxy(()=>{},{get:s(e,r),apply:i(e,r)});function l(e,{number:r,string:t,array:n,...a}={}){return"function"==typeof e?(...o)=>l(Promise.all(o).then(r=>e(...r)),{number:r,string:t,array:n,...a}):new Proxy({},{get:s(e,{number:{...r},string:{...t},array:{...o,...n},...a})})}const c=e=>{if("object"!=typeof e)return e;for(let r in e)void 0===e[r]&&delete e[r];return e};class d extends Error{constructor(e){const r="Error "+e.status;super(r),this.response=e,this.message=r}}const f=async e=>{const r=e.headers.get("content-type"),t=r&&r.includes("application/json"),n=await e.clone().text();return t?JSON.parse(n):n},y=async(e,r)=>{let t={status:e.status,statusText:e.statusText,headers:{}};for(let r of e.headers.keys())t.headers[r.toLowerCase()]=e.headers.get(r);if(!e.ok)throw new d(e);return t.body=await f(e),t},p=(e,{after:r,cache:t,error:n,output:a})=>{const o={},s={text:()=>o.res.text(),json:()=>o.res.json(),blob:()=>o.res.blob(),stream:()=>o.res.body,arrayBuffer:()=>o.res.arrayBuffer(),formData:()=>o.res.formData(),body:()=>f(o.res),clone:()=>o.res.clone(),raw:()=>o.res.clone(),response:()=>y(o.res.clone())};return l(fetch(e.url,e).then(async e=>{if(o.res=e,t&&t.clear(),e.ok&&"stream"===a)return e.body;if(e.ok&&e[a]&&"function"==typeof e[a])return e[a]();const n=r(await y(e));if("body"===a)return n.body;if("response"===a)return n;if("raw"===a)return e.clone();throw new Error(`Invalid option output="${a}"`)}),s)},h=(e={})=>{var r,t,n,a,o,s,i,u,l,d,f,y;const b=(()=>{const e=new Map;return r=>({save:t=>(e.set(r,t),t),get:()=>e.get(r),clear:()=>e.delete(r)})})(),w=(e="/",r={})=>{var t;"object"!=typeof r&&(r={}),r="string"==typeof e?{url:e,...r}:e;let{dedupe:n,output:a,before:o,after:s,error:i,...u}={...w,...r};u.url=((e,r,t)=>{let[n,a={}]=e.split("?");const o=new URLSearchParams(Object.fromEntries([...new URLSearchParams(c(r)),...new URLSearchParams(c(a))])).toString();if(o&&(n=n+"?"+o),!t)return n;return new URL(n.replace(/^\//,""),t).href})(u.url,{...w.query,...r.query},null!==(t=u.baseUrl)&&void 0!==t?t:u.baseURL),u.method=u.method.toLowerCase(),u.headers=(e=>{const r={};for(let[t,n]of Object.entries(e))r[t.toLowerCase()]=n;return r})({...w.headers,...r.headers});const l=!(!n||"get"!==u.method)&&b(u.url);var d;return!(d=u.body)||d.pipeTo||d instanceof FormData||"object"!=typeof d&&!Array.isArray(d)||(u.body=JSON.stringify(c(u.body)),u.headers["content-type"]="application/json"),u=o(u),l&&!u.signal?l.get()?l.get():l.save(p(u,{cache:l,output:a,error:i,after:s})):p(u,{output:a,error:i,after:s})};return w.url=null!==(r=e.url)&&void 0!==r?r:"/",w.method=null!==(t=e.method)&&void 0!==t?t:"get",w.query=null!==(n=e.query)&&void 0!==n?n:{},w.headers=null!==(a=e.headers)&&void 0!==a?a:{},w.baseUrl=null!==(o=null!==(s=e.baseUrl)&&void 0!==s?s:e.baseURL)&&void 0!==o?o:null,w.dedupe=null===(i=e.dedupe)||void 0===i||i,w.output=null!==(u=e.output)&&void 0!==u?u:"body",w.credentials=null!==(l=e.credentials)&&void 0!==l?l:"include",w.before=null!==(d=e.before)&&void 0!==d?d:e=>e,w.after=null!==(f=e.after)&&void 0!==f?f:e=>e,w.error=null!==(y=e.error)&&void 0!==y?y:e=>{throw e},["get","head","post","patch","put","delete","del"].map(e=>{w[e]=(r,t={})=>w(r,{...t,method:e})}),w.create=h,w};"undefined"!=typeof window&&(window.fch=h());var b=h();export default b;
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,M=({value:e})=>e,T={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(M),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}},v=(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:v(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:v(e,{number:{...n},string:{...a},array:{...T,...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 O=/(-?(?:\d+\.?\d*|\d*\.?\d+)(?:e[-+]?\d+)?)\s*([\p{L}]*)/giu;s.nanosecond=s.ns=1/1e6;s.\u00B5s=s.\u03BCs=s.us=s.microsecond=1/1e3;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.month=s.b=s.d*(365.25/12);s.year=s.yr=s.y=s.d*365.25;function C(e){return s[e]||s[e.toLowerCase().replace(/s$/,"")]}function s(e="",n="ms"){if(!e)return 0;if(typeof e=="boolean")return e?60*60:0;if(typeof e=="number")return e;var a=null;e=(e+"").replace(/(\d)[,_](\d)/g,"$1$2");var r=e[0]==="-";return e.replace(O,function(t,o,c){c=C(c),c&&(a=(a||0)+Math.abs(parseFloat(o,10))*c)}),Math.max(1,Math.round((a&&a/(C(n)||1)*(r?-1:1))/1e3))}var $=e=>!e||e instanceof FormData||typeof(e.pipe||e.pipeTo)=="function"?!1:typeof e=="object"||Array.isArray(e),g=e=>{if(typeof e!="object")return e;for(let n in e)e[n]===void 0&&delete e[n];return e},b=class extends Error{constructor(n){let a="Error "+n.status;super(a),this.response=n,this.message=a}},j=(e,n,a)=>{let[r,t={}]=e.split("?"),o=new URLSearchParams(Object.fromEntries([...new URLSearchParams(g(n)),...new URLSearchParams(g(t))])).toString();return o&&(r=r+"?"+o),a?new URL(r.replace(/^\//,""),a).href:r},B=e=>{let n={};for(let[a,r]of Object.entries(e))n[a.toLowerCase()]=r;return n},R=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},S=async(e,n)=>{let a={status:e.status,statusText:e.statusText,headers:{}};for(let r of e.headers.keys())a.headers[r.toLowerCase()]=e.headers.get(r);if(!e.ok)throw new b(e);return a.body=await R(e),a},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 S(o,r));if(t==="body")return c.body;if(t==="response")return c;if(t==="raw")return o.clone();throw new Error(`Invalid option output="${t}"`)}),J=e=>e.method==="get",N=e=>e.method+":"+e.url,q=(e={},n)=>{let a={store:w({expire:s(e.expire)}),shouldCache:J,createKey:N,...e};return a.clear=()=>a.store?.flushAll(),n&&(a.shouldCache=()=>!1),a};function x(e={}){let n={},a={},t=p(async(o="/",c={})=>{let{output:f,before:A,after:E,error:U,...i}={...t,...c},l={...t.cache},P=u=>["number","string","boolean"].includes(typeof u);if(l.expire=s([c.cache?.expire,c.cache,l?.expire,l].find(P)),i.url=j(o,{...t.query,...c.query},i.baseUrl??i.baseURL),i.method=i.method.toLowerCase()||"GET",i.headers=B({...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)),$(i.body)&&(i.body=JSON.stringify(g(i.body)),i.headers["content-type"]="application/json"),i=A(i),l.shouldCache(i)&&l.expire){let u=l.createKey(i);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:()=>R(a.res),clone:()=>a.res.clone(),raw:()=>a.res.clone(),response:()=>S(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=q(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=>{throw 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};
2
+ //# sourceMappingURL=index.min.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fch",
3
- "version": "4.1.4",
3
+ "version": "5.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",
@@ -9,10 +9,10 @@
9
9
  "author": "Francisco Presencia <public@francisco.io> (https://francisco.io/)",
10
10
  "license": "MIT",
11
11
  "scripts": {
12
- "build": "rollup -c",
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,29 @@
22
22
  "async",
23
23
  "ajax"
24
24
  ],
25
- "main": "./index.min.js",
25
+ "main": "index.min.js",
26
26
  "files": [
27
- "./index.min.js"
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
- "@babel/core": "^7.15.0",
35
- "@babel/preset-env": "^7.15.0",
36
- "@babel/preset-react": "^7.14.5",
37
- "babel-loader": "^8.2.2",
38
- "babel-polyfill": "^6.26.0",
35
+ "check-dts": "^0.7.2",
36
+ "esbuild": "^0.19.2",
39
37
  "jest": "^28.1.0",
40
38
  "jest-environment-jsdom": "^28.1.0",
41
39
  "jest-fetch-mock": "^3.0.3",
42
- "rollup": "^1.32.1",
43
- "rollup-plugin-babel": "^4.4.0",
44
- "rollup-plugin-node-resolve": "^5.2.0",
45
- "rollup-plugin-terser": "^5.2.0",
40
+ "redis": "^4.6.7",
46
41
  "swear": "^1.1.2"
47
42
  },
48
43
  "jest": {
49
44
  "testEnvironment": "jest-environment-node",
50
45
  "transform": {},
51
46
  "setupFiles": [
52
- "./setup.js"
53
- ]
54
- },
55
- "babel": {
56
- "presets": [
57
- "@babel/preset-env",
58
- "@babel/preset-react"
47
+ "./test/setup.js"
59
48
  ]
60
49
  }
61
50
  }
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", { body: { type: "psychic" } });
14
+ await api.patch("/pokemon/150", { type: "psychic" });
15
15
  ```
16
16
 
17
17
  - Create instances with shared options across requests.
18
- - Automatically encode object and array bodies as JSON.
19
- - Automatically decode JSON responses based on the headers.
20
- - Await/Async Promises; `>= 400 and <= 100` will _reject_ with an error.
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
- - Deduplicates parallel GET requests.
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 api from "fch";
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, { body, headers, ...options });
33
- api.patch(url, { body, headers, ...options });
34
- api.put(url, { body, headers, ...options });
35
- api.del(url, { body, headers, ...options });
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
- | [`dedupe`](#dedupe) | `true` | Reuse concurrently GET requests |
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.dedupe = true; // Avoid sending concurrent GET requests to the same path
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) => Promise.reject(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", { body: { name: "snowball" } });
115
- api.put(`/cats/3`, { body: { name: "snowball" } });
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", { body: { name: "snowbll" } });
140
- await api.put(`/cats/${id}`, { body: { name: "snowball" } });
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's normally the first argument, though technically you can use both styles:
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({ url: "/hello", method: "post", headers: {}, body: "..." });
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
- The `body` can be a string, a plain object|array or a FormData instance. If it's an array or object, it'll be stringified and the header `application/json` will be added. Otherwise it'll be sent as plain text:
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 'api';
173
+ import api from "api";
176
174
 
177
175
  // Sending plain text
178
- await api.post('/houses', { body: 'plain text' });
176
+ await api.post("/houses", "plain text");
179
177
 
180
- // Will JSON.stringify it internally, and add the JSON headers
181
- await api.post('/houses', { body: { id: 1, name: 'Cute Cottage' } });
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
- The methods `GET` and `HEAD` do not accept a body and it'll be ignored.
190
-
191
- The **response body** will be returned by default as the output of the call:
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
- When the server specifies the header `Content-Type` as `application/json`, then we'll attempt to parse the response body and return that as the variable. Otherwise, the plain text will be returned.
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 GET query parameters by using the option `query`:
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 api from "fch";
224
- api.query.myparam = "abc";
204
+ import fch from "fch";
205
+ fch.query.myparam = "abc";
225
206
 
226
- api.get("/cats", { query: { limit: 3 } });
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 globally, in which case they'll be added to every request, or locally, so that they are only added to the current request. You can also add them in the `before` callback:
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 api from "fch";
232
+ import fch from "fch";
236
233
 
237
234
  // Globally, so they are reused across all requests
238
- api.headers.a = "b";
235
+ fch.headers.a = "b";
239
236
 
240
237
  // With an interceptor, in case you need dynamic headers per-request
241
- api.before = (req) => {
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
- api.get("/hello", { headers: { e: "f" } });
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,143 @@ 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
- ### Dedupe
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
+ // Create a redis client
358
+ const store = createClient();
359
+ await store.connect();
360
+
361
+ const api = fch.create({
362
+ cache: {
363
+ store: store,
364
+ expire: "1h",
365
+ },
366
+ });
367
+ ```
325
368
 
326
- 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:
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
- fetch.mockOnce("a").mockOnce("b");
330
- const res = await Promise.all([fch("/a"), fch("/a")]);
372
+ const api = fch.create({ cache: "1h" });
331
373
 
332
- // Reuses the first response if two are launched in parallel
333
- expect(res).toEqual(["a", "a"]);
374
+ // Remove them all
375
+ await api.cache.clear();
334
376
  ```
335
377
 
336
- You can disable this by setting either the global `fch.dedupe` option to `false` or by passing an option per request:
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
- // Globally set it for all calls
340
- fch.dedupe = true; // [DEFAULT] Dedupes GET requests
341
- fch.dedupe = false; // All fetch() calls trigger a network call
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(),
388
+ expire: "1h",
389
+ },
390
+ });
391
+
392
+ // Connect it here now
393
+ await api.cache.store.connect();
342
394
 
343
- // Set it on a per-call basis
344
- fch("/a", { dedupe: true }); // [DEFAULT] Dedupes GET requests
345
- fch("/a", { dedupe: false }); // All fetch() calls trigger a network call
395
+ // Later on, maybe in a different place
396
+ await api.cache.store.flushDB();
346
397
  ```
347
398
 
348
- > We do not support deduping other methods besides `GET` right now
399
+ 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
400
 
350
- Note that opting out of deduping a request will _also_ make that request not be reusable, see this test for details:
401
+ ```js
402
+ const api = fch.create({
403
+ cache: {
404
+ // Default shouldCache; Note the lowercase
405
+ shouldCache: (request) => request.method === "get",
406
+
407
+ // Default createKey;
408
+ createKey: (request) => request.method + ":" + request.url,
409
+ },
410
+ });
411
+ ```
412
+
413
+ For example, if you want to differentiate the auth requests from the non-auth requests, you can do it so:
351
414
 
352
415
  ```js
353
- it("can opt out locally", async () => {
354
- fetch.once("a").once("b").once("c");
355
- const res = await Promise.all([
356
- fch("/a"),
357
- fch("/a", { dedupe: false }),
358
- fch("/a"), // Reuses the 1st response, not the 2nd one
359
- ]);
416
+ import api from "./api";
417
+
418
+ const onLogin = (user) => {
419
+ // ... Do some other stuff
420
+
421
+ // Remove the old requests since we were not auth'ed yet
422
+ api.cache.clear();
423
+
424
+ // Create a key unique for this user
425
+ api.cache.createKey = (req) => user.id + ":" + req.method + ":" + req.url;
426
+ };
427
+ ```
428
+
429
+ Or maybe you just want to NOT cache any of the requests that have an `Authorization` header, you can do so:
360
430
 
361
- expect(res).toEqual(["a", "b", "a"]);
362
- expect(fetch.mock.calls.length).toEqual(2);
431
+ ```js
432
+ const api = fch.create({
433
+ cache: {
434
+ expire: "1week",
435
+
436
+ // Note the lowercase in both! we normalize them to be lowercase
437
+ shouldCache: (req) => req.method === "get" && !req.headers.authorization,
438
+ },
363
439
  });
364
440
  ```
365
441
 
442
+ #### Creating a store.
443
+
444
+ 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:
445
+
446
+ ```js
447
+ type Store = {
448
+ get: (key: string) => Promise<any>,
449
+ set: (key: string, value: any, options?: { EX: number }) => Promise<null>,
450
+ del: (key: string) => Promise<null>,
451
+ exists: (key: string) => Promise<boolean>,
452
+ flushAll: () => Promise<any>,
453
+ };
454
+ ```
455
+
366
456
  ### Interceptors
367
457
 
368
- You can also easily add the interceptors `before`, `after` and `error`:
458
+ You can also add the interceptors `before`, `after` and `error`:
369
459
 
370
460
  - `before`: Called when the request is fully formed, but before actually launching it.
371
461
  - `after`: Called just after the response is created and if there was no error, but before parsing anything else.