UNPKG

api-registry

Version:

Centralized HTTP API client for the browser and Node.js based on Fetch API

2 lines (1 loc) 5.48 kB
import{parse as t}from"rfc6570-uri-template";class e extends Error{constructor(t,e,s){super(e),this.response=t,this.cause=s}}function s(t){if(!["get","head"].includes(t.method.toLowerCase()))return`${Math.random()}${(new Date).getTime()}`;let e=t.url;e+="|"+t.method;let s="";return t.headers.forEach(((t,e)=>{s+=`(${e}, ${t})`})),s.length>0&&(e+="|"+s),e}const n=new Map;class i{open(t){let e=n.get(t);return null==e&&(e=new r,n.set(t,e)),e}}class r{constructor(){this._cache=new Map}async match(t){const e=s(t),n=this._cache.get(e);return null!=n?n[1]:void 0}async put(t,e){const n=s(t);this._cache.set(n,[t,e])}async keys(){const t=[];for(const e of this._cache.values())t.push(e[0]);return await Promise.resolve(t)}async delete(t){const e=s(t);return this._cache.delete(e)}}(function(t){!1===t.isSecureContext&&console.warn("Non-secure context. Memory cache will be used instead of Cache API"),null==t.caches&&(t.caches=new i)}).call(globalThis,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{});const a=new Map;class o{constructor(t,e){"string"==typeof t?(this._api=e,this._path=t,this._options=[],this._interceptors=[],this._ttl=0):(this._api=t._api,this._path=t._path,this._options=t._options,this._interceptors=t._interceptors,this._ttl=t._ttl)}async send(t,e){const n=await this.buildRequest(t,e);if(this._ttl<=0)return await this.fetchWithInterceptors(n,[...this._api.interceptors,...this._interceptors],t,e);const i=new URL(n.url).origin,r=await caches.open(i),o=s(n),h=(async()=>{const s=a.get(o);if(null!=s)return(await s).clone();const i=await r.match(n);if(null!=i){const t=i.headers.get("x-api-registry-fetched-on");if(null!=t&&parseFloat(t)+this._ttl>=(new Date).getTime())return i.clone()}const h=await this.fetchWithInterceptors(n,[...this._api.interceptors,...this._interceptors],t,e),c=h.clone(),l=new Headers(c.headers);l.append("x-api-registry-fetched-on",(new Date).getTime().toString());const u=new Response(await c.blob(),{headers:l,status:c.status,statusText:c.statusText});return await r.put(n,u),setTimeout((()=>{r.delete(n)}),this._ttl),h})();a.set(o,h);const c=await h;return a.delete(o),c}async getFullUrl(){const t=await this._api.getBaseUrl();if(null==t)throw new Error(`Base URL is not defined for the API "${this._api.name}"`);return`${t}${this._path.startsWith("{?")?"":"/"}${this._path}`}async buildRequest(e,s){let n;const i=await this.getFullUrl(),r=await this.reduceOptions(s);if(null==e)n=new Request(i,r);else{let s=i;try{s=t(i).expand(e)}catch{}const a=(r.method??"get").toLowerCase();if(["get","head"].includes(a))n=new Request(s,r);else{n=null!=r.body?new Request(s,{...r,headers:{...r.headers}}):new Request(s,{body:JSON.stringify(e),...r,headers:{"Content-Type":"application/json",...r.headers}})}}return n}async sendAndParse(t,s){const n=await this.send(t,s);if(!n.ok)throw new e(n,"request failed");try{return await n.json()}catch(t){throw new e(n,"response read failed",t)}}async reduceOptions(t){const e=(null==t?[...this._api.options,...this._options]:[...this._api.options,...this._options,t]).map((async t=>t instanceof Function?await t():await Promise.resolve(t))),s=await Promise.all(e);let n={};for(const t of s)n={...n,...t,headers:{...n.headers,...t.headers}};return n}async fetchWithInterceptors(t,e,s,n){const i=e.pop();return null!=i?await i(t,(async()=>await this.fetchWithInterceptors(await this.buildRequest(s,n),e,s,n))):await fetch(t)}}class h extends o{build(){return async t=>await super.send(void 0,t)}buildWithParse(){return async t=>await super.sendAndParse(void 0,t)}receives(){return new c(this,this._api)}returns(){return this}withOptions(t){return this._options.push(t),this}intercept(t){return this._interceptors.push(t),this}withCache(t){return this._ttl=t,this}}class c extends o{build(){return async(t,e)=>await super.send(t,e)}buildWithParse(){return async(t,e)=>await super.sendAndParse(t,e)}returns(){return this}withOptions(t){return this._options.push(t),this}intercept(t){return this._interceptors.push(t),this}withCache(t){return this._ttl=t,this}}class l{constructor(t){this._name=t,this._options=[],this._interceptors=[]}get name(){return this._name}async getBaseUrl(){if(null==this._baseURL)return this._baseURL;let t;return t=this._baseURL instanceof Function?await this._baseURL():this._baseURL,null!=t&&(t=t.replace(/\/$/,""),t=t.toLowerCase()),t}get baseURL(){return this._baseURL}set baseURL(t){this._baseURL=t}get options(){return this._options}get interceptors(){return this._interceptors}endpoint(t,e="GET"){t=(t=t.replace(/^\//,"")).replace(/\/$/,"");const s=new h(t,this);return"get"!==e.toLowerCase()&&s.withOptions({method:e}),s}withOptions(t){return this._options.push(t),this}intercept(t){return this._interceptors.push(t),this}}class u extends Response{async json(){return await super.json()}}const p=globalThis.apiRegistryV2??new class{constructor(){this._apis=new Map}api(t,e,s=!1){let n=this._apis.get(t);if(null==n)n=new l(t),n.baseURL=e,this._apis.set(t,n);else if(null==n.baseURL)n.baseURL=e;else if(s)null!=e&&(n.baseURL=e);else if(null!=e&&(e instanceof Function||n.baseURL instanceof Function||n.baseURL.toLowerCase().replace(/\/$/,"")!==e.toLocaleLowerCase().replace(/\/$/,"")))throw new Error(`API ${t} is already registered with another URL ${n.baseURL.toString()}`);return n}};globalThis.apiRegistryV2=p;export{l as JsonApi,p as JsonApiRegistry,h as JsonEndpoint,u as JsonResponse,e as JsonResponseError};