UNPKG

@alwatr/fetch

Version:

`@alwatr/fetch` is an enhanced, lightweight, and dependency-free wrapper for the native `fetch` API. It provides modern features like caching strategies, request retries, timeouts, and intelligent duplicate request handling, all in a compact package.

5 lines (4 loc) 7.92 kB
/** 📦 @alwatr/fetch v7.1.3 */ __dev_mode__: console.debug("📦 @alwatr/fetch v7.1.3"); import{delay}from"@alwatr/delay";import{getGlobalThis}from"@alwatr/global-this";import{hasOwn}from"@alwatr/has-own";import{HttpStatusCodes,MimeTypes}from"@alwatr/http-primer";import{createLogger}from"@alwatr/logger";import{parseDuration}from"@alwatr/parse-duration";var FetchError=class extends Error{constructor(reason,message,response,data){super(message);this.name="FetchError";this.reason=reason;this.response=response;this.data=data}};var logger_=createLogger("@alwatr/fetch");var globalThis_=getGlobalThis();var cacheSupported=hasOwn(globalThis_,"caches");var duplicateRequestStorage_={};var defaultFetchOptions={method:"GET",headers:{},timeout:8e3,retry:3,retryDelay:1e3,removeDuplicate:"never",cacheStrategy:"network_only",cacheStorageName:"fetch_cache"};function _processOptions(url,options){logger_.logMethodArgs?.("_processOptions",{url,options});const options_={...defaultFetchOptions,...options,url};options_.window??=null;if(options_.removeDuplicate==="auto"){options_.removeDuplicate=cacheSupported?"until_load":"always"}if(options_.url.lastIndexOf("?")===-1&&options_.queryParams!=null){const queryParams=options_.queryParams;const queryArray=Object.keys(queryParams).map(key=>`${encodeURIComponent(key)}=${encodeURIComponent(String(queryParams[key]))}`);if(queryArray.length>0){options_.url+="?"+queryArray.join("&")}}if(options_.bodyJson!==void 0){options_.body=JSON.stringify(options_.bodyJson);options_.headers["content-type"]=MimeTypes.JSON}if(options_.bearerToken!==void 0){options_.headers.authorization=`Bearer ${options_.bearerToken}`}else if(options_.alwatrAuth!==void 0){options_.headers.authorization=`Alwatr ${options_.alwatrAuth.userId}:${options_.alwatrAuth.userToken}`}logger_.logProperty?.("fetch.options",options_);return options_}async function handleCacheStrategy_(options){if(options.cacheStrategy==="network_only"){return handleRemoveDuplicate_(options)}logger_.logMethod?.("handleCacheStrategy_");if(!cacheSupported){logger_.incident?.("fetch","fetch_cache_strategy_unsupported",{cacheSupported});options.cacheStrategy="network_only";return handleRemoveDuplicate_(options)}const cacheStorage=await caches.open(options.cacheStorageName);const request=new Request(options.url,options);switch(options.cacheStrategy){case"cache_first":{const cachedResponse=await cacheStorage.match(request);if(cachedResponse!=null){return cachedResponse}const response=await handleRemoveDuplicate_(options);if(response.ok){cacheStorage.put(request,response.clone())}return response}case"cache_only":{const cachedResponse=await cacheStorage.match(request);if(cachedResponse==null){throw new FetchError("cache_not_found","Resource not found in cache")}return cachedResponse}case"network_first":{try{const networkResponse=await handleRemoveDuplicate_(options);if(networkResponse.ok){cacheStorage.put(request,networkResponse.clone())}return networkResponse}catch(err){const cachedResponse=await cacheStorage.match(request);if(cachedResponse!=null){return cachedResponse}throw err}}case"update_cache":{const networkResponse=await handleRemoveDuplicate_(options);if(networkResponse.ok){cacheStorage.put(request,networkResponse.clone())}return networkResponse}case"stale_while_revalidate":{const cachedResponse=await cacheStorage.match(request);const fetchedResponsePromise=handleRemoveDuplicate_(options).then(networkResponse=>{if(networkResponse.ok){cacheStorage.put(request,networkResponse.clone());if(typeof options.revalidateCallback==="function"){setTimeout(options.revalidateCallback,0,networkResponse.clone())}}return networkResponse});return cachedResponse??fetchedResponsePromise}default:{return handleRemoveDuplicate_(options)}}}async function handleRemoveDuplicate_(options){if(options.removeDuplicate==="never"){return handleRetryPattern_(options)}logger_.logMethod?.("handleRemoveDuplicate_");const bodyString=typeof options.body==="string"?options.body:"";const cacheKey=`${options.method} ${options.url} ${bodyString}`;duplicateRequestStorage_[cacheKey]??=handleRetryPattern_(options);try{const response=await duplicateRequestStorage_[cacheKey];if(duplicateRequestStorage_[cacheKey]!=null){if(response.ok!==true||options.removeDuplicate==="until_load"){delete duplicateRequestStorage_[cacheKey]}}return response.clone()}catch(err){delete duplicateRequestStorage_[cacheKey];throw err}}async function handleRetryPattern_(options){if(!(options.retry>1)){return handleTimeout_(options)}logger_.logMethod?.("handleRetryPattern_");options.retry--;const externalAbortSignal=options.signal;try{const response=await handleTimeout_(options);if(!response.ok&&response.status>=HttpStatusCodes.Error_Server_500_Internal_Server_Error){throw new FetchError("http_error",`HTTP error! status: ${response.status} ${response.statusText}`,response)}return response}catch(err){logger_.accident("fetch","fetch_failed_retry",err);if(globalThis_.navigator?.onLine===false){logger_.accident("handleRetryPattern_","offline","Skip retry because offline");throw err}await delay.by(options.retryDelay);options.signal=externalAbortSignal;return handleRetryPattern_(options)}}function handleTimeout_(options){if(options.timeout===0){return globalThis_.fetch(options.url,options)}logger_.logMethod?.("handleTimeout_");return new Promise((resolved,reject)=>{const abortController=typeof AbortController==="function"?new AbortController:null;const externalAbortSignal=options.signal;options.signal=abortController?.signal;if(abortController!==null&&externalAbortSignal!=null){externalAbortSignal.addEventListener("abort",()=>abortController.abort(),{once:true})}const timeoutId=setTimeout(()=>{reject(new FetchError("timeout","fetch_timeout"));abortController?.abort("fetch_timeout")},parseDuration(options.timeout));globalThis_.fetch(options.url,options).then(response=>resolved(response)).catch(reason=>reject(reason)).finally(()=>{clearTimeout(timeoutId)})})}async function fetch(url,options={}){logger_.logMethodArgs?.("fetch",{url,options});const options_=_processOptions(url,options);try{const response=await handleCacheStrategy_(options_);if(!response.ok){throw new FetchError("http_error",`HTTP error! status: ${response.status} ${response.statusText}`,response)}return[response,null]}catch(err){let error;if(err instanceof FetchError){error=err;if(error.response!==void 0&&error.data===void 0){const bodyText=await error.response.text().catch(()=>"");if(bodyText.trim().length>0){try{error.data=JSON.parse(bodyText)}catch{error.data=bodyText}}}}else if(err instanceof Error){if(err.name==="AbortError"){error=new FetchError("aborted",err.message)}else{error=new FetchError("network_error",err.message)}}else{error=new FetchError("unknown_error",String(err??"unknown_error"))}logger_.error("fetch",error.reason,{error});return[null,error]}}async function fetchJson(url,options={}){logger_.logMethodArgs?.("fetchJson",{url,options});const[response,error]=await fetch(url,options);if(error){return[null,error]}const bodyText=await response.text().catch(()=>"");if(bodyText.trim().length===0){const parseError=new FetchError("json_parse_error","Response body is empty, cannot parse JSON",response,bodyText);logger_.error("fetchJson",parseError.reason,{error:parseError});return[null,parseError]}try{const data=JSON.parse(bodyText);if(options.requireJsonResponseWithOkTrue&&data.ok!==true){const parseError=new FetchError("json_response_error",'Response JSON "ok" property is not true',response,data);logger_.error("fetchJson",parseError.reason,{error:parseError});return[null,parseError]}return[data,null]}catch(err){const parseError=new FetchError("json_parse_error",err instanceof Error?err.message:"Failed to parse JSON response",response,bodyText);logger_.error("fetchJson",parseError.reason,{error:parseError});return[null,parseError]}}export{FetchError,cacheSupported,fetch,fetchJson}; //# sourceMappingURL=main.mjs.map