ofexios
Version:
unjs/ofetch based HTTP client with similar API to axios for browser and Node.js
3 lines (2 loc) • 9.52 kB
JavaScript
import S from"callable-instance";import{Time as T}from"cosmokit";import{safeDestr as g}from"destr";export{fetch}from"ofetch";const x=(r=>{var t=typeof t>"u"?import.meta.require:t;let o=r?.ReadableStream,e=r?.TransformStream;if(typeof t<"u")try{const s=t("stream/web");o||(o=s.ReadableStream),e||(e=s.TransformStream)}catch{try{const s=t("web-streams-polyfill");o||(o=s.ReadableStream),e||(e=s.TransformStream)}catch{throw new TypeError("stream polyfill is not available")}}return{ReadableStream:o,TransformStream:e}})(void 0),L=x.TransformStream;var f=(r=>(r.BODY_USED="BODY_USED",r.NO_BODY_READER="NO_BODY_READER",r.TIMEOUT="TIMEOUT",r.NETWORK_ERROR="NETWORK_ERROR",r.BODY_NOT_ALLOWED="BODY_NOT_ALLOWED",r.HOOK_CONTEXT_CHANGED="HOOK_CONTEXT_CHANGED",r.ABORTED_BY_HOOK="ABORTED_BY_HOOK",r.INVALID_HOOK_CALLBACK="INVALID_HOOK_CALLBACK",r.UNEXPECTED_HOOK_RETURN="UNEXPECTED_HOOK_RETURN",r))(f||{});class d extends Error{constructor(t,o,e,s){super(o,s),this.name="FexiosError",this.code=t,this.context=e}}class b extends d{constructor(t,o,e){super(o.statusText,t,void 0,e),this.name="FexiosResponseError",this.response=o}}const U=r=>!(r instanceof b)&&r instanceof d;class m{constructor(t,o,e){this.rawResponse=t,this.data=o,this.ok=t.ok,this.status=t.status,this.statusText=t.statusText,this.headers=t.headers;for(const[s,i]of Object.entries(e||{}))this[s]=i}}const R=typeof Promise.withResolvers>"u"?function(){let r,t;return{promise:new Promise((o,e)=>{r=o,t=e}),resolve:r,reject:t}}:()=>Promise.withResolvers();function A(r){if(!r.ok)throw new b(`Request failed with status code ${r.status}`,r);return r}function v(r){let t=0;for(const s of r)t+=s.length;const o=new Uint8Array(t);let e=0;for(const s of r)o.set(s,e),e+=s.length;return o}async function E(r,t){let o=0;const e=[];for await(const s of r)e.push(s),o+=s.length,t?.(s.length,s);return v(e)}async function H(r,t,o){if(!t)return E(r,o);const e=new Uint8Array(t||0);let s=0,i;for await(const n of r){if(i){const[a,c]=await i;c.enqueue(n),s+=n.length,o?.(s,n);continue}if(s+n.length>e.length){console.warn(`readToUint8Array overflowed (${e.length}++${s+n.length-e.length}), fallback to concat`);const{promise:a,resolve:c}=R();i=a;const{promise:h,resolve:l,reject:u}=R();E(new ReadableStream({start(p){p.enqueue(e.subarray(0,s)),p.enqueue(n),s+=n.length,c([h,p])}})).then(l).catch(u);continue}e.set(n,s),o?.(s,n,e),s+=n.length}if(i){const[n,a]=await i;a.close();const c=await n;return o?.(s,void 0,c),c}return e}const B=new Set(["image/svg","application/xml","application/xhtml","application/html"]),C=/^application\/(?:[\w!#$%&*.^`~-]*\+)?json(;.+)?$/i;function k(r=""){if(!r)return"json";const t=r.split(";").shift()||"";return C.test(t)?"json":t==="application/octet-stream"?"stream":B.has(t)||t.startsWith("text/")?"text":"blob"}class w extends S{async request(t,o){let e=o=o||{};typeof t=="string"||t instanceof URL?e.url=t.toString():typeof t=="object"&&(e={...t,...e}),e=await this.emit("beforeInit",e);const s=o.baseURL||this.baseConfigs.baseURL||globalThis.location?.href,i=s?new URL(s,globalThis.location?.href):void 0,n=new URL(e.url.toString(),i);if(e.url=n.href,e.baseURL=i?i.href:n.origin,e.headers=this.mergeHeaders(this.baseConfigs.headers,o.headers),e.query=this.mergeQuery(this.baseConfigs.query,n.searchParams,o.query),n.search=new URLSearchParams(e.query).toString(),e.url=n.toString(),this.METHODS_WITHOUT_BODY.includes(e.method?.toLocaleLowerCase())&&e.body)throw new d(f.BODY_NOT_ALLOWED,`Request method "${e.method}" does not allow body`);e=await this.emit("beforeRequest",e);let a;typeof e.body<"u"&&e.body!==null&&(e.body instanceof Blob||e.body instanceof FormData||e.body instanceof URLSearchParams?a=e.body:typeof e.body=="object"?(a=JSON.stringify(e.body),e.headers["content-type"]="application/json; charset=UTF-8"):a=e.body),!o.headers?.["content-type"]&&a&&(a instanceof FormData||a instanceof URLSearchParams?typeof a=="string"&&typeof e.body=="object"?e.headers["content-type"]="application/json; charset=UTF-8":a instanceof Blob&&(e.headers["content-type"]=a.type):delete e.headers["content-type"]),e.body=a,e=await this.emit("afterBodyTransformed",e);const c=e.abortController||globalThis.AbortController?new AbortController:void 0,h=new Request(e.url,{method:e.method||"GET",credentials:e.credentials,cache:e.cache,mode:e.mode,headers:e.headers,body:e.body,signal:c?.signal});if(e.rawRequest=h,e=await this.emit("beforeActualFetch",e),e.url.startsWith("ws")){console.info("WebSocket:",e.url);const y=new WebSocket(e.url);return e.rawResponse=new Response,e.response=new m(e.rawResponse,y,{ok:!0,status:101,statusText:"Switching Protocols"}),e.data=y,e.headers=new Headers,this.emit("afterResponse",e)}const l=e.timeout||this.baseConfigs.timeout||T.minute,u=setTimeout(()=>{if(c?.abort(),!c)throw new d(f.TIMEOUT,`Request timed out after ${l}ms`,e)},l),p=await fetch(e.rawRequest).catch(y=>Promise.reject(new d(f.NETWORK_ERROR,y.error.message,e)));return e.rawResponse=p,e.response=await w.resolveResponseBody(p,e.responseType,(y,D)=>{console.info("Download progress:",y),o?.onProgress?.(y,D)}).finally(()=>{clearTimeout(u)}),e.data=e.response.data,e.headers=e.response.headers,this.emit("afterResponse",e)}mergeQuery(t,...o){const e=new URLSearchParams(t);for(const s of o)new URLSearchParams(s).forEach((i,n)=>{e.set(n,i)});return Object.fromEntries(e.entries())}mergeHeaders(t,...o){const e={},s=new Headers(t);for(const i of o)new Headers(i).forEach((n,a)=>{s.set(a,n)});return s.forEach((i,n)=>{e[n]=i}),e}async emit(t,o){const e=this.hooks.filter(s=>s.event===t);try{let s=0;for(const i of e){const n=`${t}#${i.action.name||`anonymous#${s}`}`,a=Symbol("FexiosHookContext");o[a]=a;const c=await i.action.call(this,o);if(c===!1)throw new d(f.ABORTED_BY_HOOK,`Request aborted by hook "${n}"`,o);if(typeof c=="object"&&c[a]===a)o=c;else{const h=globalThis["".concat("console")];try{throw new d(f.HOOK_CONTEXT_CHANGED,`Hook "${n}" should return the original FexiosContext or return false to abort the request, but got "${c}".`)}catch(l){h.warn(l.stack||l)}}delete o[a],s++}}catch(s){return Promise.reject(s)}return o}on(t,o,e=!1){if(typeof o!="function")throw new d(f.INVALID_HOOK_CALLBACK,`Hook should be a function, but got "${typeof o}"`);return this.hooks[e?"unshift":"push"]({event:t,action:o}),this}off(t,o){return this.hooks=this.hooks.filter(e=>e.event!==t||e.action!==o),this}createInterceptor(t){return{handlers:()=>this.hooks.filter(o=>o.event===t).map(o=>o.action),use:(o,e=!1)=>this.on(t,o,e),clear:()=>{this.hooks=this.hooks.filter(o=>o.event!==t)}}}createMethodShortcut(t){return Object.defineProperty(this,t,{value:(o,e,s)=>(this.METHODS_WITHOUT_BODY.includes(t.toLocaleLowerCase())?s=e:(s=s||{},s.body=e),this.request(o,{...s,method:t}))}),this}static async resolveResponseBody(t,o,e){if(t.bodyUsed)throw new d("BODY_USED","Response body has already been used or locked");const s=t.headers.get("content-type")||"";if(Number(t.headers.get("content-length")),(t.status===101||t.status===426||t.headers.get("upgrade"))&&typeof globalThis.WebSocket<"u"){const h=new WebSocket(t.url);return await new Promise((l,u)=>{h.onopen=l,h.onerror=u}),new m(t,h,{ok:!0,status:101,statusText:"Switching Protocols"})}if(s.startsWith("text/event-stream")&&!["text","json"].includes(o||"")&&typeof globalThis.EventSource<"u"){const h=new EventSource(t.url);return await new Promise((l,u)=>{h.onopen=l,h.onerror=u}),new m(t,h)}if(o==="stream")return new m(t,g(await t.text()));const i=o||k(t.headers.get("content-type")||"");if(!o&&i==="stream"){let h=0;const l=new L({transform(u,p){p.enqueue(u),h+=u.length,e?.(h,u)},flush(u){u.terminate()}});return new m(t,t.body?.pipeThrough(l))}if(o==="blob"||s.startsWith("image/")||s.startsWith("video/")||s.startsWith("audio/"))return new m(t,await t.blob());const n=t.body;if(!n)throw new d(f.NO_BODY_READER,"Failed to get ReadableStream from response body");const a=await H(n,+(t.headers.get("content-length")||0),e),c=new m(t,void 0);if(this.isText(a)?c.data=new TextDecoder().decode(a):c.data=new Blob([a],{type:t.headers.get("content-type")||void 0}),typeof c.data=="string"&&o!=="text"&&(o==="json"||s.startsWith("application/json")))try{c.data=g(c.data)}catch(h){console.warn("Failed to parse response data as JSON:",h)}return typeof c.data>"u"&&(c.data=a.length>0?a:void 0),A(c)}static isText(t,o=1024){if(!(t instanceof Uint8Array))throw new TypeError("Input must be a Uint8Array");const e=t.slice(0,o),s=new TextDecoder("utf-8",{fatal:!0});try{const i=s.decode(e),n=/[\x00-\x08\x0E-\x1F\x7F]/g,a=i.match(n);return!(a&&a.length/i.length>.1)}catch{return!1}}extends(t){const o=new w({...this.baseConfigs,...t});return o.hooks=[...this.hooks],o}static create(t){return new w(t)}constructor(t={}){super("request"),this.hooks=[],this.DEFAULT_CONFIGS={baseURL:"",timeout:T.minute,credentials:"same-origin",headers:{},query:{},responseType:void 0},this.ALL_METHODS=["get","post","put","patch","delete","head","options","trace"],this.METHODS_WITHOUT_BODY=["get","head","options","trace"],this.interceptors={request:this.createInterceptor("beforeRequest"),response:this.createInterceptor("afterResponse")},this.create=w.create,this.baseConfigs=t,this.ALL_METHODS.forEach(this.createMethodShortcut.bind(this))}}w.BLOB_MIME_TYPE=["image/","video/","audio/"];const O=w.create,_=O();export{w as Fexios,d as FexiosError,f as FexiosErrorCodes,m as FexiosResponse,b as FexiosResponseError,O as createFexios,_ as default,_ as fexios,U as isFexiosError};
//# sourceMappingURL=index.mjs.map