UNPKG

@web3r/flowerkit

Version:

Tree-shakable JavaScript and TypeScript utility library for frontend/browser apps: DOM, events, arrays, objects, strings, date, JSON, and network helpers (ESM/CJS, SSR-friendly).

53 lines (52 loc) 7.26 kB
import{getWindow,getDocument}from"ssr-window";import{bubble}from"../../evt/bubble/index.mjs";import{getObjFromFormData}from"../../obj/getObjFromFormData/index.mjs";import{getFormDataFromObj}from"../getFormDataFromObj/index.mjs";import{getUrlWithQueryParams}from"../getUrlWithQueryParams/index.mjs"; /** * Performs an HTTP request (`fetch`) with handy defaults, content-type handling, * query param building, and optional bubbling of a "getFromServer" event. * * @template T The expected response data type. * @param {Object} [props] Request parameters (all optional). * @param {string} [props.url] The URL to request. Defaults to current window location or './'. * @param {("auto"|"application/json"|"application/x-www-form-urlencoded"|"multipart/form-data")} [props.contentType="auto"] Content type header. If "auto", sets based on data/method. * @param {boolean} [props.isBubble=true] Whether to bubble a "getFromServer" event after success. * @param {number} [props.timeout=15000] Timeout in milliseconds (use Infinity to disable). * @param {("GET"|"PUT"|"POST"|"DELETE"|"HEAD"|"CONNECT"|"OPTIONS"|"TRACE")} [props.method="GET"] HTTP method. * @param {RequestMode} [props.mode="cors"] Fetch mode. * @param {AbortSignal|null} [props.signal=null] AbortSignal for cancellation. * @param {Record<string,unknown>|FormData|null} [props.data=null] Request data. For GET-like methods, appended as query params. * @param {function(T): T} [props.getSuccessResp] Transform function for successful response. Defaults to identity function. * @param {function(Response): Promise<T>} [props.getResp] Custom response parser. If provided, overrides `type`. * @param {("text"|"json"|"blob"|"arrayBuffer")} [props.type="json"] Response body parsing type (used when `getResp` not provided). * @param {Record<string,string>} [props.headers={}] Additional headers. * @param {number[]} [props.allowedCodes=[]] Array of HTTP status codes to treat as success even if not 2xx. * @param {RequestCredentials} [props.credentials="same-origin"] Credentials mode. * @param {RequestRedirect} [props.redirect="follow"] Redirect mode. * @param {RequestCache} [props.cache="default"] Cache mode. * @param {ReferrerPolicy} [props.referrerPolicy="no-referrer-when-downgrade"] Referrer policy. * @param {Omit<RequestInit, "method"|"headers"|"body"|"signal"|"mode"|"credentials"|"redirect"|"cache"|"referrerPolicy">} [props.fetchProps={}] Additional fetch options. * @returns {Promise<T>} Promise with parsed response (type depends on `type` option). * @throws {TypeError} getFromServer: url must be a string * @throws {TypeError} getFromServer: allowedCodes must be an array of integers * @throws {TypeError} getFromServer: data must be a plain object, FormData, or null * @throws {TypeError} getFromServer: timeout must be a non-negative number or Infinity * @see https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API * @example * const user = await getFromServer<{ userId: number }>({ url: "/api/user?id=1", method: "GET" }); */const getFromServer=async(props={})=>{const{contentType:contentType="auto",isBubble:isBubble=true,timeout:timeout=15000,method:method="GET",mode:mode="cors",signal:signal=null,data:data=null,getResp:getResp,type:type="json",url:url=getWindow().location.href||"./",headers:headers={},allowedCodes:allowedCodes=[],credentials:credentials="same-origin",redirect:redirect="follow",referrerPolicy:referrerPolicy="no-referrer-when-downgrade",cache:cache="default",fetchProps:fetchProps={}}=props;const getSuccessResp=props.getSuccessResp??(resp=>resp);const methodNormalized=String(method).toUpperCase();const methodsWithBody=new Set(["POST","PUT","DELETE","PATCH"]);const methodsNoBody=new Set(["GET","HEAD","CONNECT","OPTIONS","TRACE"]);const isFormData=v=>typeof FormData!=="undefined"&&v instanceof FormData;const isPlainObject=v=>Object.prototype.toString.call(v)==="[object Object]";if(typeof url!=="string")throw new TypeError("getFromServer: url must be a string");if(!Array.isArray(allowedCodes)||!allowedCodes.every(c=>Number.isInteger(c)))throw new TypeError("getFromServer: allowedCodes must be an array of integers");if(typeof timeout!=="number"||Number.isFinite(timeout)&&timeout<0||Number.isNaN(timeout))throw new TypeError("getFromServer: timeout must be a non-negative number or Infinity");if(data!==null&&!isFormData(data)&&!isPlainObject(data))throw new TypeError("getFromServer: data must be a plain object, FormData, or null");let timer=null;let isTimedOut=false;const requestController=new AbortController;const externalAbortListener=()=>{requestController.abort()};if(signal)if(signal.aborted)externalAbortListener();else signal.addEventListener("abort",externalAbortListener,{once:true});const getDataAsObject=()=>isFormData(data)?getObjFromFormData(data):data??{}; /** * Produces request body based on contentType and data * @private * @returns {BodyInit | null} */const getBody=()=>{if(!methodsWithBody.has(methodNormalized))return null;switch(true){case contentType==="application/json":return JSON.stringify(getDataAsObject());case contentType==="application/x-www-form-urlencoded":{const params=new URLSearchParams;Object.entries(getDataAsObject()).forEach(([key,value])=>{params.set(key,String(value??""))});return params.toString()}case contentType==="multipart/form-data":return isFormData(data)?data:getFormDataFromObj(getDataAsObject());case contentType==="auto":return isFormData(data)?data:getFormDataFromObj(getDataAsObject());default:return null}}; /** * URL builder (adds query params for GET-like methods) * @private */const getUrl=()=>methodsNoBody.has(methodNormalized)&&data!==null?getUrlWithQueryParams(url,isFormData(data)?data:data):url; /** * Response parser * @private */const getResponse=async resp=>{if(typeof getResp==="function")return await getResp(resp);const{ok:ok,status:status}=resp;if(ok||allowedCodes.length>0&&allowedCodes.includes(status))switch(type){case"arrayBuffer":return await resp.arrayBuffer();case"json":return await resp.json();case"blob":return await resp.blob();default:return await resp.text()}throw resp}; /** * Headers builder * @private */const getHeaders=()=>{const result={...headers||{}};if(contentType==="multipart/form-data"){Object.keys(result).forEach(key=>{if(key.toLowerCase()==="content-type")delete result[key]});return result}if(["application/json","application/x-www-form-urlencoded"].includes(contentType))result["Content-Type"]=contentType;return result};if(timeout&&timeout!==Infinity)timer=setTimeout(()=>{isTimedOut=true;requestController.abort()},timeout);const fetchParams={...fetchProps,method:methodNormalized,body:getBody(),mode:mode,signal:requestController.signal,credentials:credentials,redirect:redirect,cache:cache,referrerPolicy:referrerPolicy,headers:getHeaders()};try{const resp=await fetch(getUrl(),fetchParams);const parsed=await getResponse(resp);if(isBubble&&typeof window!=="undefined")bubble(getDocument(),getFromServer.name,parsed);return getSuccessResp(parsed)}catch(error){if(isTimedOut)throw 408;throw error}finally{if(timer)clearTimeout(timer);if(signal)signal.removeEventListener("abort",externalAbortListener)}};export{getFromServer}; //# sourceMappingURL=index.mjs.map