@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) • 8.96 kB
JavaScript
/** 📦 @alwatr/fetch v7.1.3 */
__dev_mode__: console.debug("📦 @alwatr/fetch v7.1.3");
;var __defProp=Object.defineProperty;var __getOwnPropDesc=Object.getOwnPropertyDescriptor;var __getOwnPropNames=Object.getOwnPropertyNames;var __hasOwnProp=Object.prototype.hasOwnProperty;var __export=(target,all)=>{for(var name in all)__defProp(target,name,{get:all[name],enumerable:true})};var __copyProps=(to,from,except,desc)=>{if(from&&typeof from==="object"||typeof from==="function"){for(let key of __getOwnPropNames(from))if(!__hasOwnProp.call(to,key)&&key!==except)__defProp(to,key,{get:()=>from[key],enumerable:!(desc=__getOwnPropDesc(from,key))||desc.enumerable})}return to};var __toCommonJS=mod=>__copyProps(__defProp({},"__esModule",{value:true}),mod);var main_exports={};__export(main_exports,{FetchError:()=>FetchError,cacheSupported:()=>cacheSupported,fetch:()=>fetch,fetchJson:()=>fetchJson});module.exports=__toCommonJS(main_exports);var import_delay=require("@alwatr/delay");var import_global_this=require("@alwatr/global-this");var import_has_own=require("@alwatr/has-own");var import_http_primer=require("@alwatr/http-primer");var import_logger=require("@alwatr/logger");var import_parse_duration=require("@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_=(0,import_logger.createLogger)("@alwatr/fetch");var globalThis_=(0,import_global_this.getGlobalThis)();var cacheSupported=(0,import_has_own.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"]=import_http_primer.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>=import_http_primer.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 import_delay.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")},(0,import_parse_duration.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]}}0&&(module.exports={FetchError,cacheSupported,fetch,fetchJson});
//# sourceMappingURL=main.cjs.map