grab-url 1.0.16 → 1.0.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,2 +1,2 @@
1
- "use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const e=require("./log.cjs.js");async function t(r,i={}){var{headers:s,response:l={},method:a=(i.post?"POST":i.put?"PUT":i.patch?"PATCH":"GET"),cache:d=!1,cacheForTime:c=60,timeout:u=30,baseURL:f="undefined"!=typeof process&&process.env.SERVER_API_URL||"/api/",cancelOngoingIfNew:p=!1,cancelNewIfOngoing:g=!1,rateLimit:m=0,debug:b=!1,infiniteScroll:y=null,setDefaults:w=!1,retryAttempts:h=0,logger:v=e.log,onRequest:T=null,onResponse:S=null,onError:L=null,onStream:x=null,repeatEvery:O=null,repeat:E=0,debounce:N=0,regrabOnStale:j=!1,regrabOnFocus:k=!1,regrabOnNetwork:P=!1,post:D=!1,put:q=!1,patch:J=!1,body:A=null,...F}={..."undefined"!=typeof window?window?.grab?.defaults:globalThis?.grab?.defaults||{},...i};let R=e=>r.startsWith(e);R("http:")||R("https:")?f="":R("/")||f.endsWith("/")?R("/")&&f.endsWith("/")&&(r=r.slice(1)):r="/"+r;try{if(N>0)return await o(async()=>{await t(r,{...i,debounce:0})},1e3*N);if(E>1){for(let e=0;e<E;e++)await t(r,{...i,repeat:0});return l}if(O)return setInterval(async()=>{await t(r,{...i,repeat:0,repeatEvery:null})},1e3*O),l;if(i?.setDefaults)return void("undefined"!=typeof window?window.grab.defaults={...i,setDefaults:void 0}:void 0!==globalThis.grab&&(globalThis.grab.defaults={...i,setDefaults:void 0}));if("undefined"!=typeof window){const e=async()=>await t(r,{...i,cache:!1});j&&d&&setTimeout(e,1e3*c),P&&window.addEventListener("online",e),k&&(window.addEventListener("focus",e),document.addEventListener("visibilitychange",async()=>{"visible"===document.visibilityState&&await e()}))}let n="function"==typeof l?l:null;l&&!n||(l={});var[$,I,H]=y||[];if(y?.length&&void 0!==H&&"undefined"!=typeof window){let o="string"==typeof H?document.querySelector(H):H;o?window.scrollListener&&void 0!==o&&"function"==typeof o.removeEventListener&&o.removeEventListener("scroll",window.scrollListener):e.log("paginateDOM not found",{color:"red"}),window.scrollListener=async e=>{const o=e.target;localStorage.setItem("scroll",JSON.stringify([o.scrollTop,o.scrollLeft,H])),o.scrollHeight-o.scrollTop<=o.clientHeight+200&&await t(r,{...i,cache:!1,[$]:h?.currentPage+1})},o&&o.addEventListener("scroll",window.scrollListener)}let w=JSON.stringify($?{...F,[$]:void 0}:F),h=t?.log?.find(e=>e.request==w&&e.path==r);if($){let e=h?.currentPage+1||F?.[$]||1;h||(l[I]=[],e=1),h&&(h.currentPage=e),F={...F,[$]:e}}else{for(let e of Object.keys(l))l[e]=void 0;if(d&&(!c||h?.lastFetchTime>Date.now()-1e3*c)){for(let e of Object.keys(h.res))l[e]=h.res[e];n&&(l=n(l))}}if(n?n({isLoading:!0}):"object"==typeof l&&(l.isLoading=!0),n&&(l=n(l)),m>0&&h?.lastFetchTime&&h.lastFetchTime>Date.now()-1e3*m)throw new Error(`Fetch rate limit exceeded for ${r}. \n Wait ${m}s between requests.`);if(h?.controller)if(p)h.controller.abort();else if(g)return{isLoading:!0};void 0!==t.log&&t.log?.unshift({path:r,request:w,lastFetchTime:Date.now(),controller:new AbortController});let L={method:a,headers:{"Content-Type":"application/json",Accept:"application/json",...s},body:F.body,redirect:"follow",cache:d?"force-cache":"no-store",signal:p?t.log[0]?.controller?.signal:AbortSignal.timeout(1e3*u)},D="";["POST","PUT","PATCH"].includes(a)?L.body=F.body||JSON.stringify(F):D=(Object.keys(F).length?"?":"")+new URLSearchParams(F).toString(),"function"==typeof T&&([r,l,F,L]=T(r,l,F,L));let q=null,J=new Date,A=t.mock?.[r],R=e=>new Promise(t=>setTimeout(t,1e3*e||0));if(!A||A.params&&A.method!=a||A.params&&w!=JSON.stringify(A.params)){if(q=await fetch(f+r+D,L).catch(e=>{throw new Error(e.message)}),!q.ok)throw new Error(`HTTP error: ${q.status} ${q.statusText}`);let e=q.headers.get("content-type");x?await x(q.body):q=await(e?e.includes("application/json")?q&&q.json():e.includes("application/pdf")||e.includes("application/octet-stream")?q.blob():q.text():q.json()).catch(e=>{throw new Error("Error parsing response: "+e)})}else await R(A.delay),q="function"==typeof A.response?A.response(F):A.response;"function"==typeof S&&([r,l,F,L]=S(r,l,F,L)),n?n({isLoading:void 0}):"object"==typeof l&&delete l?.isLoading,delete h?.controller;const C=((Number(new Date)-Number(J))/1e3).toFixed(1);if(b&&v("Path:"+f+r+D+"\n"+JSON.stringify(i,null,2)+"\nTime: "+C+"s\nResponse: "+e.printJSONStructure(q)),"object"==typeof q){for(let e of Object.keys(q))l[e]=I==e&&l[e]?.length?[...l[e],...q[e]]:q[e];void 0!==l&&(l.data=q)}else n?n({data:q,...q}):"object"==typeof l&&(l.data=q);return void 0!==t.log&&t.log?.unshift({path:r,request:JSON.stringify({...F,paginateKey:void 0}),response:l,lastFetchTime:Date.now()}),n&&(l=n(l)),l}catch(C){let e="Error: "+C.message+"\nPath:"+f+r+"\n";return"function"==typeof L&&L(C.message,f+r,F),i.retryAttempts>0?await t(r,{...i,retryAttempts:--i.retryAttempts}):(!C.message.includes("signal")&&i.debug&&(v(e,{color:"red"}),b&&"undefined"!=typeof document&&n(e)),l.error=C.message,"function"==typeof l?(l.data=l({isLoading:void 0,error:C.message}),l=l.data):delete l?.isLoading,void 0!==t.log&&t.log?.unshift({path:r,request:JSON.stringify(F),error:C.message}),l)}}t.instance=(e={})=>(o,n={})=>t(o,{...e,...n});const o=async(e,t)=>{let o;return async function(...n){clearTimeout(o),o=setTimeout(async()=>{clearTimeout(o),await e(...n)},t)}};function n(e){if("undefined"==typeof document)return;let t,o=document.getElementById("alert-overlay");o||(o=document.body.appendChild(document.createElement("div")),o.id="alert-overlay",o.setAttribute("style","position:fixed;inset:0;z-index:9999;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center"),o.innerHTML='<div id="alert-box" style="background:#fff;padding:1.5em 2em;border-radius:8px;box-shadow:0 2px 16px #0003;min-width:220px;max-height:80vh;position:relative;display:flex;flex-direction:column;">\n <button id="close-alert" style="position:absolute;top:12px;right:20px;font-size:1.5em;background:none;border:none;cursor:pointer;color:black;">&times;</button>\n <div id="alert-list" style="overflow:auto;flex:1;"></div>\n </div>',o.addEventListener("click",e=>e.target==o&&o.remove()),document.getElementById("close-alert").onclick=()=>o.remove()),t=o.querySelector("#alert-list"),t.innerHTML+=`<div style="border-bottom:1px solid #333; font-size:1.2em;margin:0.5em 0;">${e}</div>`}function r(){document.addEventListener("keydown",o=>{if("i"===o.key&&o.ctrlKey&&o.altKey){let o=" ";for(let n of t.log)o+=`<div style="margin-bottom:1em; border-bottom:1px solid #ccc; padding-bottom:1em;">\n <b>Path:</b> ${n.path}<br>\n <b>Request:</b> ${e.printJSONStructure(n.request,0,"html")}<br>\n <b>Response:</b> ${e.printJSONStructure(n.response,0,"html")}<br> \n <b>Time:</b> ${new Date(n.lastFetchTime).toLocaleString()}\n </div>`;n(o)}})}"undefined"!=typeof window?(window.log=e.log,window.grab=t,window.grab.log=[],window.grab.mock={},window.grab.defaults={},r(),document.addEventListener("DOMContentLoaded",()=>{let[e,t,o]=JSON.parse(localStorage.getItem("scroll"))||[];e&&(document.querySelector(o).scrollTop=e,document.querySelector(o).scrollLeft=t)})):"undefined"!=typeof globalThis&&(t.log=[],t.mock={},t.defaults={},globalThis.log=e.log,globalThis.grab=t.instance()),exports.log=e.log,exports.printJSONStructure=e.printJSONStructure,exports.default=t,exports.grab=t,exports.setupDevTools=r,exports.showAlert=n;
1
+ "use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const e=require("./log.cjs.js");async function t(r,i){var{headers:s,response:l={},method:a=(i?.post?"POST":i?.put?"PUT":i?.patch?"PATCH":"GET"),cache:d=!1,cacheForTime:c=60,timeout:u=30,baseURL:f="undefined"!=typeof process&&process.env.SERVER_API_URL||"/api/",cancelOngoingIfNew:p=!1,cancelNewIfOngoing:g=!1,rateLimit:m=0,debug:b=!1,infiniteScroll:y=null,setDefaults:w=!1,retryAttempts:h=0,logger:v=e.log,onRequest:T=null,onResponse:S=null,onError:L=null,onStream:x=null,repeatEvery:O=null,repeat:E=0,debounce:N=0,regrabOnStale:j=!1,regrabOnFocus:k=!1,regrabOnNetwork:P=!1,post:D=!1,put:q=!1,patch:J=!1,body:A=null,...F}={..."undefined"!=typeof window?window?.grab?.defaults:globalThis?.grab?.defaults||{},...i};let R=e=>r.startsWith(e);R("http:")||R("https:")?f="":R("/")||f.endsWith("/")?R("/")&&f.endsWith("/")&&(r=r.slice(1)):r="/"+r;try{if(N>0)return await o(async()=>{await t(r,{...i,debounce:0})},1e3*N);if(E>1){for(let e=0;e<E;e++)await t(r,{...i,repeat:0});return l}if(O)return setInterval(async()=>{await t(r,{...i,repeat:0,repeatEvery:null})},1e3*O),l;if(i?.setDefaults)return void("undefined"!=typeof window?window.grab.defaults={...i,setDefaults:void 0}:void 0!==globalThis.grab&&(globalThis.grab.defaults={...i,setDefaults:void 0}));if("undefined"!=typeof window){const e=async()=>await t(r,{...i,cache:!1});j&&d&&setTimeout(e,1e3*c),P&&window.addEventListener("online",e),k&&(window.addEventListener("focus",e),document.addEventListener("visibilitychange",async()=>{"visible"===document.visibilityState&&await e()}))}let n="function"==typeof l?l:null;l&&!n||(l={});var[$,I,H]=y||[];if(y?.length&&void 0!==H&&"undefined"!=typeof window){let o="string"==typeof H?document.querySelector(H):H;o?window.scrollListener&&void 0!==o&&"function"==typeof o.removeEventListener&&o.removeEventListener("scroll",window.scrollListener):e.log("paginateDOM not found",{color:"red"}),window.scrollListener=async e=>{const o=e.target;localStorage.setItem("scroll",JSON.stringify([o.scrollTop,o.scrollLeft,H])),o.scrollHeight-o.scrollTop<=o.clientHeight+200&&await t(r,{...i,cache:!1,[$]:h?.currentPage+1})},o&&o.addEventListener("scroll",window.scrollListener)}let w=JSON.stringify($?{...F,[$]:void 0}:F),h=t?.log?.find(e=>e.request==w&&e.path==r);if($){let e=h?.currentPage+1||F?.[$]||1;h||(l[I]=[],e=1),h&&(h.currentPage=e),F={...F,[$]:e}}else{for(let e of Object.keys(l))l[e]=void 0;if(d&&(!c||h?.lastFetchTime>Date.now()-1e3*c)){for(let e of Object.keys(h.res))l[e]=h.res[e];n&&(l=n(l))}}if(n?n({isLoading:!0}):"object"==typeof l&&(l.isLoading=!0),n&&(l=n(l)),m>0&&h?.lastFetchTime&&h.lastFetchTime>Date.now()-1e3*m)throw new Error(`Fetch rate limit exceeded for ${r}. \n Wait ${m}s between requests.`);if(h?.controller)if(p)h.controller.abort();else if(g)return{isLoading:!0};void 0!==t.log&&t.log?.unshift({path:r,request:w,lastFetchTime:Date.now(),controller:new AbortController});let L={method:a,headers:{"Content-Type":"application/json",Accept:"application/json",...s},body:F.body,redirect:"follow",cache:d?"force-cache":"no-store",signal:p?t.log[0]?.controller?.signal:AbortSignal.timeout(1e3*u)},D="";["POST","PUT","PATCH"].includes(a)?L.body=F.body||JSON.stringify(F):D=(Object.keys(F).length?"?":"")+new URLSearchParams(F).toString(),"function"==typeof T&&([r,l,F,L]=T(r,l,F,L));let q=null,J=new Date,A=t.mock?.[r],R=e=>new Promise(t=>setTimeout(t,1e3*e||0));if(!A||A.params&&A.method!=a||A.params&&w!=JSON.stringify(A.params)){if(q=await fetch(f+r+D,L).catch(e=>{throw new Error(e.message)}),!q.ok)throw new Error(`HTTP error: ${q.status} ${q.statusText}`);let e=q.headers.get("content-type");x?await x(q.body):q=await(e?e.includes("application/json")?q&&q.json():e.includes("application/pdf")||e.includes("application/octet-stream")?q.blob():q.text():q.json()).catch(e=>{throw new Error("Error parsing response: "+e)})}else await R(A.delay),q="function"==typeof A.response?A.response(F):A.response;"function"==typeof S&&([r,l,F,L]=S(r,l,F,L)),n?n({isLoading:void 0}):"object"==typeof l&&delete l?.isLoading,delete h?.controller;const C=((Number(new Date)-Number(J))/1e3).toFixed(1);if(b&&v("Path:"+f+r+D+"\n"+JSON.stringify(i,null,2)+"\nTime: "+C+"s\nResponse: "+e.printJSONStructure(q)),"object"==typeof q){for(let e of Object.keys(q))l[e]=I==e&&l[e]?.length?[...l[e],...q[e]]:q[e];void 0!==l&&(l.data=q)}else n?n({data:q,...q}):"object"==typeof l&&(l.data=q);return void 0!==t.log&&t.log?.unshift({path:r,request:JSON.stringify({...F,paginateKey:void 0}),response:l,lastFetchTime:Date.now()}),n&&(l=n(l)),l}catch(C){let e="Error: "+C.message+"\nPath:"+f+r+"\n";return"function"==typeof L&&L(C.message,f+r,F),i.retryAttempts>0?await t(r,{...i,retryAttempts:--i.retryAttempts}):(!C.message.includes("signal")&&i.debug&&(v(e,{color:"red"}),b&&"undefined"!=typeof document&&n(e)),l.error=C.message,"function"==typeof l?(l.data=l({isLoading:void 0,error:C.message}),l=l.data):delete l?.isLoading,void 0!==t.log&&t.log?.unshift({path:r,request:JSON.stringify(F),error:C.message}),l)}}t.instance=(e={})=>(o,n={})=>t(o,{...e,...n});const o=async(e,t)=>{let o;return async function(...n){clearTimeout(o),o=setTimeout(async()=>{clearTimeout(o),await e(...n)},t)}};function n(e){if("undefined"==typeof document)return;let t,o=document.getElementById("alert-overlay");o||(o=document.body.appendChild(document.createElement("div")),o.id="alert-overlay",o.setAttribute("style","position:fixed;inset:0;z-index:9999;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center"),o.innerHTML='<div id="alert-box" style="background:#fff;padding:1.5em 2em;border-radius:8px;box-shadow:0 2px 16px #0003;min-width:220px;max-height:80vh;position:relative;display:flex;flex-direction:column;">\n <button id="close-alert" style="position:absolute;top:12px;right:20px;font-size:1.5em;background:none;border:none;cursor:pointer;color:black;">&times;</button>\n <div id="alert-list" style="overflow:auto;flex:1;"></div>\n </div>',o.addEventListener("click",e=>e.target==o&&o.remove()),document.getElementById("close-alert").onclick=()=>o.remove()),t=o.querySelector("#alert-list"),t.innerHTML+=`<div style="border-bottom:1px solid #333; font-size:1.2em;margin:0.5em 0;">${e}</div>`}function r(){document.addEventListener("keydown",o=>{if("i"===o.key&&o.ctrlKey&&o.altKey){let o=" ";for(let n of t.log)o+=`<div style="margin-bottom:1em; border-bottom:1px solid #ccc; padding-bottom:1em;">\n <b>Path:</b> ${n.path}<br>\n <b>Request:</b> ${e.printJSONStructure(n.request,0,"html")}<br>\n <b>Response:</b> ${e.printJSONStructure(n.response,0,"html")}<br> \n <b>Time:</b> ${new Date(n.lastFetchTime).toLocaleString()}\n </div>`;n(o)}})}"undefined"!=typeof window?(window.log=e.log,window.grab=t,window.grab.log=[],window.grab.mock={},window.grab.defaults={},r(),document.addEventListener("DOMContentLoaded",()=>{let[e,t,o]=JSON.parse(localStorage.getItem("scroll"))||[];e&&(document.querySelector(o).scrollTop=e,document.querySelector(o).scrollLeft=t)})):"undefined"!=typeof globalThis&&(t.log=[],t.mock={},t.defaults={},globalThis.log=e.log,globalThis.grab=t.instance()),exports.log=e.log,exports.printJSONStructure=e.printJSONStructure,exports.default=t,exports.grab=t,exports.setupDevTools=r,exports.showAlert=n;
2
2
  //# sourceMappingURL=grab-api.cjs.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"grab-api.cjs.js","sources":["../src/grab-api.ts"],"sourcesContent":["import {\n printJSONStructure,\n log,\n type LogOptions,\n} from \"./log-json\";\n\n/**\n * ### GRAB: Generate Request to API from Browser\n * ![GrabAPILogo](https://i.imgur.com/Rwl5P3p.png)\n *\n * 1. **GRAB is the FBEST Request Manager: Functionally Brilliant, Elegantly Simple Tool**: One Function, no dependencies,\n * minimalist syntax, [more features than alternatives](https://grab.js.org/docs/Comparisons)\n * 2. **Auto-JSON Convert**: Pass parameters and get response or error in JSON, handling other data types as is.\n * 3. **isLoading Status**: Sets `.isLoading=true` on the pre-initialized response object so you can show a \"Loading...\" in any framework\n * 4. **Debug Logging**: Adds global `log()` and prints colored JSON structure, response, timing for requests in test.\n * 5. **Mock Server Support**: Configure `window.grab.mock` for development and testing environments\n * 6. **Cancel Duplicates**: Prevent this request if one is ongoing to same path & params, or cancel the ongoing request.\n * 7. **Timeout & Retry**: Customizable request timeout, default 30s, and auto-retry on error\n * 8. **DevTools**: `Ctrl+I` overlays webpage with devtools showing all requests and responses, timing, and JSON structure.\n * 9. **Request History**: Stores all request and response data in global `grab.log` object\n * 10. **Pagination Infinite Scroll**: Built-in pagination for infinite scroll to auto-load and merge next result page, with scroll position recovery.\n * 11. **Base URL Based on Environment**: Configure `grab.defaults.baseURL` once at the top, overide with `SERVER_API_URL` in `.env`.\n * 12. **Frontend Cache**: Set cache headers and retrieve from frontend memory for repeat requests to static data.\n * 13. **Regrab On Error**: Regrab on timeout error, or on window refocus, or on network change, or on stale data.\n * 14. **Framework Agnostic**: Alternatives like TanStack work only in component initialization and depend on React & others.\n * 15. **Globals**: Adds to window in browser or global in Node.js so you only import once: `grab()`, `log()`, `grab.log`, `grab.mock`, `grab.defaults`\n * 16. **TypeScript Tooltips**: Developers can hover over option names and autocomplete TypeScript.\n * 17. **Request Stategies**: [🎯 Examples](https://grab.js.org/docs/Examples) show common stategies like debounce, repeat, proxy, unit tests, interceptors, file upload, etc\n * 18. **Rate Limiting**: Built-in rate limiting to prevent multi-click cascading responses, require to wait seconds between requests.\n * 19. **Repeat**: Repeat request this many times, or repeat every X seconds to poll for updates.\n * 20. **Loading Icons**: Import from `grab-url/icons` to get enhanced animated loading icons.\n *\n * @param {string} path The full URL path OR relative path on this server after `grab.defaults.baseURL`\n * @param {object} [options={}] Request params for GET or body for POST/PUT/PATCH and utility options\n * @param {string} [options.method] default=\"GET\" The HTTP method to use\n * @param {object} [options.response] Pre-initialized object which becomes response JSON, no need for `.data`.\n * isLoading and error may also be set on this object. May omit and use return if load status is not needed.\n * @param {boolean} [options.cancelOngoingIfNew] default=false Cancel previous requests to same path\n * @param {boolean} [options.cancelNewIfOngoing] default=false Cancel if a request to path is in progress\n * @param {boolean} [options.cache] default=false Whether to cache the request and from frontend cache\n * @param {boolean} [options.debug] default=false Whether to log the request and response\n * @param {number} [options.timeout] default=30 The timeout for the request in seconds\n * @param {number} [options.cacheForTime] default=60 Seconds to consider data stale and invalidate cache\n * @param {number} [options.rateLimit] default=0 If set, how many seconds to wait between requests\n * @param {string} [options.baseURL] default='/api/' base url prefix, override with SERVER_API_URL env\n * @param {boolean} [options.setDefaults] default=false Pass this with options to set\n * those options as defaults for all requests.\n * @param {number} [options.retryAttempts] default=0 Retry failed requests this many times\n * @param {array} [options.infiniteScroll] default=null [page key, response field to concatenate, element with results]\n * @param {number} [options.repeat] default=0 Repeat request this many times\n * @param {number} [options.repeatEvery] default=null Repeat request every seconds\n * @param {function} [options.logger] default=log Custom logger to override the built-in color JSON log()\n * @param {function} [options.onRequest] Set with defaults to modify each request data.\n * Takes and returns in order: path, response, params, fetchParams\n * @param {function} [options.onResponse] Set with defaults to modify each request data.\n * Takes and returns in order: path, response, params, fetchParams\n * @param {function} [options.onStream] Set with defaults to process the response as a stream (i.e., for instant unzip)\n * @param {function} [options.onError] Set with defaults to modify the error data. Takes: error, path, params\n * @param {number} [options.debounce] default=0 Seconds to debounce request, wait to execute so that other requests may override\n * @param {boolean} [options.regrabOnStale] default=false Refetch when cache is past cacheForTime\n * @param {boolean} [options.regrabOnFocus] default=false Refetch on window refocus\n * @param {boolean} [options.regrabOnNetwork] default=false Refetch on network change\n * @param {any} [...params] All other params become GET params, POST body, and other methods.\n * @returns {Promise<Object>} The response object with resulting data or .error if error.\n * @author [vtempest (2025)](https://github.com/vtempest/GRAB-URL)\n * @see [🎯 Examples](https://grab.js.org/docs/Examples) [📑 Docs](https://grab.js.org)\n */\nexport default async function grab<TResponse = any, TParams = any>(\n path: string,\n options: GrabOptions<TResponse, TParams> = {} as any\n): Promise<GrabResponse<TResponse>> {\n var {\n headers,\n response = {} as any, // Pre-initialized object to set the response in. isLoading and error are also set on this object.\n method = options.post // set post: true for POST, omit for GET\n ? \"POST\"\n : options.put\n ? \"PUT\"\n : options.patch\n ? \"PATCH\"\n : \"GET\",\n cache = false, // Enable/disable frontend caching\n cacheForTime = 60, // Seconds to consider data stale and invalidate cache\n timeout = 30, // Request timeout in seconds\n baseURL = (typeof process !== \"undefined\" && process.env.SERVER_API_URL) ||\n \"/api/\", // Use env var or default to /api/\n cancelOngoingIfNew = false, // Cancel previous request for same path\n cancelNewIfOngoing = false, // Don't make new request if one is ongoing\n rateLimit = 0, // Minimum seconds between requests\n debug = false, // Auto-enable debug on localhost\n // typeof window !== \"undefined\" && window?.location?.hostname?.includes(\"localhost\"),\n infiniteScroll = null, // page key, response field to concatenate, element with results\n setDefaults = false, // Set these options as defaults for future requests\n retryAttempts = 0, // Retry failed requests once\n logger = log, // Custom logger to override the built-in color JSON log()\n onRequest = null, // Hook to modify request data before request is made\n onResponse = null, // Hook to modify request data after request is made\n onError = null, // Hook to modify request data after request is made\n onStream = null, // Hook to process the response as a stream (i.e., for instant unarchiving)\n repeatEvery = null, // Repeat request every seconds\n repeat = 0, // Repeat request this many times\n debounce = 0, // Seconds to debounce request, wait to execute so that other requests may override\n regrabOnStale = false, // Refetch when cache is past cacheForTime\n regrabOnFocus = false, // Refetch on window refocus\n regrabOnNetwork = false, // Refetch on network change\n post = false,\n put = false,\n patch = false,\n body = null,\n ...params // All other params become request params/query\n } = {\n // Destructure options with defaults, merging with any globally set defaults\n ...(typeof window !== \"undefined\"\n ? window?.grab?.defaults\n : (globalThis)?.grab?.defaults || {}),\n ...options,\n };\n\n // Handle URL construction\n // Ensures proper joining of baseURL and path\n let s = (t) => path.startsWith(t);\n if (s(\"http:\") || s(\"https:\")) baseURL = \"\";\n else if (!s(\"/\") && !baseURL.endsWith(\"/\")) path = \"/\" + path;\n else if (s(\"/\") && baseURL.endsWith(\"/\")) path = path.slice(1);\n\n try {\n //handle debounce\n if (debounce > 0)\n return (await debouncer(async () => {\n await grab(path, { ...options, debounce: 0 });\n }, debounce * 1000)) as GrabResponse;\n\n // Handle repeat options:\n // - repeat: Makes the same request multiple times sequentially\n // - repeatEvery: Makes the request periodically on an interval\n if (repeat > 1) {\n for (let i = 0; i < repeat; i++) {\n await grab(path, { ...options, repeat: 0 });\n }\n return response;\n }\n if (repeatEvery) {\n setInterval(async () => {\n await grab(path, { ...options, repeat: 0, repeatEvery: null });\n }, repeatEvery * 1000);\n return response;\n }\n\n // Store the provided options as new defaults if setDefaults flag is set\n // This allows configuring default options that apply to all future requests\n if (options?.setDefaults) {\n if (typeof window !== \"undefined\")\n window.grab.defaults = { ...options, setDefaults: undefined };\n else if (typeof globalThis.grab !== \"undefined\")\n globalThis.grab.defaults = {\n ...options,\n setDefaults: undefined,\n };\n\n return;\n }\n\n // regrab on stale, on window refocus, on network\n if (typeof window !== \"undefined\") {\n const regrab = async () => await grab(path, { ...options, cache: false });\n if (regrabOnStale && cache) setTimeout(regrab, 1000 * cacheForTime);\n if (regrabOnNetwork) window.addEventListener(\"online\", regrab);\n if (regrabOnFocus) {\n window.addEventListener(\"focus\", regrab);\n document.addEventListener(\"visibilitychange\", async () => {\n if (document.visibilityState === \"visible\") await regrab();\n });\n }\n }\n\n // Handle response parameter which can be either an object to populate\n // or a function to call with results (e.g. React setState)\n let resFunction = typeof response === \"function\" ? response : null;\n if (!response || resFunction) response = {};\n\n var [paginateKey, paginateResult, paginateElement] = infiniteScroll || [];\n\n // Configure infinite scroll behavior if enabled\n // Attaches scroll listener to specified element that triggers next page load\n if (infiniteScroll?.length && typeof paginateElement !== \"undefined\"\n && typeof window !== \"undefined\") {\n let paginateDOM =\n typeof paginateElement === \"string\"\n ? document.querySelector(paginateElement)\n : paginateElement;\n\n if (!paginateDOM) log(\"paginateDOM not found\", { color: \"red\" });\n else if (window.scrollListener\n && typeof paginateDOM !== \"undefined\"\n && typeof paginateDOM.removeEventListener === \"function\")\n paginateDOM.removeEventListener(\"scroll\", window.scrollListener);\n\n // Your modified scroll listener with position saving\n window.scrollListener = async (event) => {\n const t = event.target as HTMLElement;\n\n // Save scroll position whenever user scrolls\n localStorage.setItem(\n \"scroll\",\n JSON.stringify([t.scrollTop, t.scrollLeft, paginateElement])\n );\n\n if (t.scrollHeight - t.scrollTop <= t.clientHeight + 200) {\n await grab(path, {\n ...options,\n cache: false,\n [paginateKey]: priorRequest?.currentPage + 1,\n });\n }\n };\n\n if (paginateDOM)\n paginateDOM.addEventListener(\"scroll\", window.scrollListener);\n }\n\n // Check request history for a previous request with same path/params\n // Used for caching and pagination. Ignores pagination params when comparing.\n let paramsAsText = JSON.stringify(\n paginateKey ? { ...params, [paginateKey]: undefined } : params\n );\n let priorRequest = grab?.log?.find(\n (e) => e.request == paramsAsText && e.path == path\n );\n\n // Handle response data management based on pagination settings\n if (!paginateKey) {\n // Clear any existing response data\n for (let key of Object.keys(response)) response[key] = undefined;\n\n // For non-paginated requests:\n // Return cached response if caching enabled and identical request exists\n // after returning cache, proceed with call to revalidate ensure data is up to date\n if (\n cache &&\n (!cacheForTime ||\n priorRequest?.lastFetchTime > Date.now() - 1000 * cacheForTime)\n ) {\n // set response to cache data\n for (let key of Object.keys(priorRequest.res))\n response[key] = priorRequest.res[key];\n if (resFunction) response = resFunction(response);\n\n // if (!cacheValidate) return response;\n }\n } else {\n // For paginated requests:\n // Track current page number and append results to existing data\n let pageNumber =\n priorRequest?.currentPage + 1 || params?.[paginateKey] || 1;\n\n // Clear response if this is a new request with new params\n if (!priorRequest) {\n response[paginateResult] = [];\n pageNumber = 1;\n }\n\n // Update page tracking\n if (priorRequest) priorRequest.currentPage = pageNumber;\n params = { ...params, [paginateKey]: pageNumber };\n }\n\n // Set loading state on response object\n if (resFunction) resFunction({ isLoading: true });\n else if (typeof response === \"object\") response.isLoading = true;\n\n if (resFunction) response = resFunction(response);\n\n // Enforce rate limiting between requests if configured\n if (\n rateLimit > 0 &&\n priorRequest?.lastFetchTime &&\n priorRequest.lastFetchTime > Date.now() - 1000 * rateLimit\n )\n throw new Error(`Fetch rate limit exceeded for ${path}. \n Wait ${rateLimit}s between requests.`);\n\n // Handle request cancellation based on configuration:\n // - cancelOngoingIfNew: Cancels any in-progress request for same path\n // - cancelNewIfOngoing: Prevents new request if one is already in progress\n if (priorRequest?.controller)\n if (cancelOngoingIfNew) priorRequest.controller.abort();\n else if (cancelNewIfOngoing) return { isLoading: true } as GrabResponse;\n\n // Track new request in history log\n if (typeof grab.log != \"undefined\")\n grab.log?.unshift({\n path,\n request: paramsAsText,\n lastFetchTime: Date.now(),\n controller: new AbortController(),\n });\n\n // Configure fetch request parameters including headers, cache settings,\n // and timeout/cancellation signals\n let fetchParams = {\n method,\n headers: {\n \"Content-Type\": \"application/json\",\n Accept: \"application/json\",\n ...headers,\n },\n body: params.body,\n redirect: \"follow\" as RequestRedirect,\n cache: cache ? \"force-cache\" : (\"no-store\" as RequestCache),\n signal: cancelOngoingIfNew\n ? grab.log[0]?.controller?.signal\n : AbortSignal.timeout(timeout * 1000),\n } as RequestInit;\n\n // Format request parameters based on HTTP method\n // POST/PUT/PATCH send data in request body\n // GET/DELETE append data as URL query parameters\n let paramsGETRequest = \"\";\n if ([\"POST\", \"PUT\", \"PATCH\"].includes(method))\n fetchParams.body = params.body || JSON.stringify(params);\n else\n paramsGETRequest =\n (Object.keys(params).length ? \"?\" : \"\") +\n new URLSearchParams(params).toString();\n\n // Execute pre-request hook if configured\n // Allows modifying request data before sending\n if (typeof onRequest === \"function\")\n [path, response, params, fetchParams] = onRequest(\n path,\n response,\n params,\n fetchParams\n );\n\n // Process request through mock handler if configured\n // Otherwise make actual API request\n let res = null,\n startTime = new Date(),\n mockHandler = grab.mock?.[path] as GrabMockHandler;\n\n let wait = (s) => new Promise((res) => setTimeout(res, s * 1000 || 0));\n\n if (\n mockHandler &&\n (!mockHandler.params || mockHandler.method == method) &&\n (!mockHandler.params ||\n paramsAsText == JSON.stringify(mockHandler.params))\n ) {\n await wait(mockHandler.delay);\n\n res =\n typeof mockHandler.response === \"function\"\n ? mockHandler.response(params)\n : mockHandler.response;\n } else {\n // Make actual API request and handle response based on content type\n res = await fetch(baseURL + path + paramsGETRequest, fetchParams).catch(\n (e) => {\n throw new Error(e.message);\n }\n );\n\n if (!res.ok)\n throw new Error(`HTTP error: ${res.status} ${res.statusText}`);\n\n // Convert browser ReadableStream to Node.js stream\n let type = res.headers.get(\"content-type\");\n\n if (onStream) await onStream(res.body);\n else\n res = await (type\n ? type.includes(\"application/json\")\n ? res && res.json()\n : type.includes(\"application/pdf\") ||\n type.includes(\"application/octet-stream\")\n ? res.blob()\n : res.text()\n : res.json()\n ).catch((e) => {\n throw new Error(\"Error parsing response: \" + e);\n });\n }\n\n // Execute post-request hook if configured\n // Allows modifying response data before processing\n if (typeof onResponse === \"function\")\n [path, response, params, fetchParams] = onResponse(\n path,\n response,\n params,\n fetchParams\n );\n\n // Clear request tracking states\n if (resFunction) resFunction({ isLoading: undefined });\n else if (typeof response === \"object\") delete response?.isLoading;\n\n delete priorRequest?.controller;\n\n // Log debug information if enabled\n // Includes request details, timing, and response structure\n const elapsedTime = (\n (Number(new Date()) - Number(startTime)) /\n 1000\n ).toFixed(1);\n if (debug) {\n logger(\n \"Path:\" +\n baseURL +\n path +\n paramsGETRequest +\n \"\\n\" +\n JSON.stringify(options, null, 2) +\n \"\\nTime: \" +\n elapsedTime +\n \"s\\nResponse: \" +\n printJSONStructure(res)\n );\n // console.log(res);\n }\n\n // if (typeof res === \"undefined\") return;\n\n // Update response object with results\n // For paginated requests, concatenates with existing results\n if (typeof res === \"object\") {\n for (let key of Object.keys(res))\n response[key] =\n paginateResult == key && response[key]?.length\n ? [...response[key], ...res[key]]\n : res[key];\n\n if (typeof response !== \"undefined\") response.data = res; // for axios compat\n } else if (resFunction) resFunction({ data: res, ...res });\n else if (typeof response === \"object\") response.data = res;\n\n // Store request/response in history log\n if (typeof grab.log != \"undefined\")\n grab.log?.unshift({\n path,\n request: JSON.stringify({ ...params, paginateKey: undefined }),\n response,\n lastFetchTime: Date.now(),\n });\n\n if (resFunction) response = resFunction(response);\n\n return response;\n } catch (error) {\n // Handle any errors that occurred during request processing\n let errorMessage =\n \"Error: \" + error.message + \"\\nPath:\" + baseURL + path + \"\\n\";\n // JSON.stringify(params);\n\n // if onError hook is passed\n if (typeof onError === \"function\")\n onError(error.message, baseURL + path, params);\n\n // Retry request if retries are configured and attempts remain\n if (options.retryAttempts > 0)\n return await grab(path, {\n ...options,\n retryAttempts: --options.retryAttempts,\n });\n\n // Update error state in response object\n // Do not show errors for duplicate aborted requests\n if (!error.message.includes(\"signal\") && options.debug) {\n logger(errorMessage, { color: \"red\" });\n if (debug && typeof document !== \"undefined\") showAlert(errorMessage);\n }\n response.error = error.message;\n if (typeof response === \"function\") {\n response.data = response({ isLoading: undefined, error: error.message });\n response = response.data;\n } else delete response?.isLoading;\n\n // Log error in request history\n if (typeof grab.log != \"undefined\")\n grab.log?.unshift({\n path,\n request: JSON.stringify(params),\n error: error.message,\n });\n\n // if (typeof options.response === \"function\")\n // response = options.response(response);\n return response;\n }\n}\n\n/**\n * Creates a new instance of grab with default options\n * to apply to all requests made by this instance\n * @param {Object} defaults - options for all requests by instance\n * @returns {Function} grab() function using those options\n */\ngrab.instance =\n (defaults = {}) =>\n (path, options = {}) =>\n grab(path, { ...defaults, ...options });\n\n// delays execution so that future calls may override and only executes last one\nconst debouncer = async (func, wait) => {\n let timeout;\n return async function executedFunction(...args) {\n const later = async () => {\n clearTimeout(timeout);\n await func(...args);\n };\n clearTimeout(timeout);\n timeout = setTimeout(later, wait);\n };\n};\n\n// Add globals to window in browser, or global in Node.js\nif (typeof window !== \"undefined\") {\n window.log = log;\n window.grab = grab;\n\n window.grab.log = [];\n window.grab.mock = {};\n window.grab.defaults = {};\n\n //Ctrl+I setup dev tools\n setupDevTools();\n\n // Restore scroll position when page loads or component mounts\n document.addEventListener(\"DOMContentLoaded\", () => {\n let [scrollTop, scrollLeft, paginateElement] =\n JSON.parse(localStorage.getItem(\"scroll\")) || [];\n if (!scrollTop) return;\n document.querySelector(paginateElement).scrollTop = scrollTop;\n document.querySelector(paginateElement).scrollLeft = scrollLeft;\n });\n} else if (typeof globalThis !== \"undefined\") {\n grab.log = [];\n grab.mock = {};\n grab.defaults = {};\n globalThis.log = log;\n globalThis.grab = grab.instance();\n}\n\n\n\n/**\n * Shows message in a modal overlay with scrollable message stack\n * and is easier to dismiss unlike alert() which blocks window.\n * Creates a semi-transparent overlay with a white box containing the message.\n * @param {string} msg - The message to display\n */\nexport function showAlert(msg) {\n if (typeof document === \"undefined\") return;\n let o = document.getElementById(\"alert-overlay\"),\n list;\n\n // Create overlay and alert box if they don't exist\n if (!o) {\n o = document.body.appendChild(document.createElement(\"div\"));\n o.id = \"alert-overlay\";\n o.setAttribute(\n \"style\",\n \"position:fixed;inset:0;z-index:9999;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center\"\n );\n o.innerHTML = `<div id=\"alert-box\" style=\"background:#fff;padding:1.5em 2em;border-radius:8px;box-shadow:0 2px 16px #0003;min-width:220px;max-height:80vh;position:relative;display:flex;flex-direction:column;\">\n <button id=\"close-alert\" style=\"position:absolute;top:12px;right:20px;font-size:1.5em;background:none;border:none;cursor:pointer;color:black;\">&times;</button>\n <div id=\"alert-list\" style=\"overflow:auto;flex:1;\"></div>\n </div>`;\n\n // Add click handlers to close overlay\n o.addEventListener(\"click\", (e) => e.target == o && o.remove());\n document.getElementById(\"close-alert\").onclick = () => o.remove();\n }\n\n list = o.querySelector(\"#alert-list\");\n\n // Add new message to list\n list.innerHTML += `<div style=\"border-bottom:1px solid #333; font-size:1.2em;margin:0.5em 0;\">${msg}</div>`;\n}\n\n/**\n * Sets up development tools for debugging API requests\n * Adds a keyboard shortcut (Ctrl+Alt+I) that shows a modal with request history\n * Each request entry shows:\n * - Request path\n * - Request details\n * - Response data\n * - Timestamp\n */\nexport function setupDevTools() {\n // Keyboard shortcut (Ctrl+Alt+I) to toggle debug view\n document.addEventListener(\"keydown\", (e) => {\n if (e.key === \"i\" && e.ctrlKey && e.altKey) {\n // Create HTML of the grab.log requests\n let html = \" \";\n for (let request of grab.log) {\n html += `<div style=\"margin-bottom:1em; border-bottom:1px solid #ccc; padding-bottom:1em;\">\n <b>Path:</b> ${request.path}<br>\n <b>Request:</b> ${printJSONStructure(request.request, 0, 'html')}<br>\n <b>Response:</b> ${printJSONStructure(request.response, 0, 'html')}<br> \n <b>Time:</b> ${new Date(request.lastFetchTime).toLocaleString()}\n </div>`;\n }\n showAlert(html);\n }\n });\n}\n\n\n\n/***************** TYPESCRIPT INTERFACES *****************/\n\n// Core response object that gets populated with API response data\nexport type GrabResponse<TResponse = any> = TResponse & {\n /** Indicates if request is currently in progress */\n isLoading?: boolean;\n /** Error message if request failed */\n error?: string;\n /** Binary or text response data (JSON is set to the root)*/\n data?: TResponse | any;\n /** The actual response data - type depends on API endpoint */\n [key: string]: unknown;\n};\n\nexport type GrabOptions<TResponse = any, TParams = any> = TParams & {\n /** include headers and authorization in the request */\n headers?: Record<string, string>;\n /** Pre-initialized object which becomes response JSON, no need for .data */\n response?: TResponse | ((params: TParams) => TResponse) | any;\n /** default=\"GET\" The HTTP method to use */\n method?: \"GET\" | \"POST\" | \"PUT\" | \"PATCH\" | \"DELETE\" | \"OPTIONS\" | \"HEAD\";\n /** default=false Whether to cache the request and from frontend cache */\n cache?: boolean;\n /** default=60 Seconds to consider data stale and invalidate cache */\n cacheForTime?: number;\n /** default=30 The timeout for the request in seconds */\n timeout?: number;\n /** default='/api/' base url prefix, override with SERVER_API_URL env */\n baseURL?: string;\n /** default=true Cancel previous requests to same path */\n cancelOngoingIfNew?: boolean;\n /** default=false Cancel if a request to path is in progress */\n cancelNewIfOngoing?: boolean;\n /** default=false If set, how many seconds to wait between requests */\n rateLimit?: number;\n /** default=false Whether to log the request and response */\n debug?: boolean;\n /** default=null [page key, response field to concatenate, element with results] */\n infiniteScroll?: [string, string, string | HTMLElement];\n /** default=false Pass this with options to set those options as defaults for all requests */\n setDefaults?: boolean;\n /** default=0 Retry failed requests this many times */\n retryAttempts?: number;\n /** default=log Custom logger to override the built-in color JSON log() */\n logger?: (...args: any[]) => void;\n /** Set with defaults to modify each request data. Takes and returns in order: path, response, params, fetchParams */\n onRequest?: (...args: any[]) => any;\n /** Set with defaults to modify each request data. Takes and returns in order: path, response, params, fetchParams */\n onResponse?: (...args: any[]) => any;\n /** Set with defaults to modify each request data. Takes and returns in order: error, path, params */\n onError?: (...args: any[]) => any;\n /** Set with defaults to process the response as a stream (i.e., for instant unzip) */\n onStream?: (...args: any[]) => any;\n /** default=0 Repeat request this many times */\n repeat?: number;\n /** default=null Repeat request every seconds */\n repeatEvery?: number;\n /** default=0 Seconds to debounce request, wait to execute so that other requests may override */\n debounce?: number;\n /** default=false Refetch when cache is past cacheForTime */\n regrabOnStale?: boolean;\n /** default=false Refetch on window refocus */\n regrabOnFocus?: boolean;\n /** default=false Refetch on network change */\n regrabOnNetwork?: boolean;\n /** shortcut for method: \"POST\" */\n post?: boolean;\n /** shortcut for method: \"PUT\" */\n put?: boolean;\n /** shortcut for method: \"PATCH\" */\n patch?: boolean;\n /** default=null The body of the POST/PUT/PATCH request (can be passed into main)*/\n body?: any;\n /** All other params become GET params, POST body, and other methods */\n [key: string]: TParams | any;\n};\n\n// Combined options and parameters interface\n\n// Mock server configuration for testing\nexport interface GrabMockHandler<TParams = any, TResponse = any> {\n /** Mock response data or function that returns response */\n response: TResponse | ((params: TParams) => TResponse);\n /** HTTP method this mock should respond to */\n method?: \"GET\" | \"POST\" | \"PUT\" | \"PATCH\" | \"DELETE\" | \"OPTIONS\" | \"HEAD\";\n /** Request parameters this mock should match */\n params?: TParams;\n /** Delay in seconds before returning mock response */\n delay?: number;\n}\n\n// Request log entry for debugging and history\nexport interface GrabLogEntry {\n /** API path that was requested */\n path: string;\n /** Stringified request parameters */\n request: string;\n /** Response data (only present for successful requests) */\n response?: any;\n /** Error message (only present for failed requests) */\n error?: string;\n /** Timestamp when request was made */\n lastFetchTime: number;\n /** Abort controller for request cancellation */\n controller?: AbortController;\n /** Current page number for paginated requests */\n currentPage?: number;\n}\n\n// Global grab configuration and state\nexport interface GrabGlobal {\n /** Default options applied to all requests */\n defaults?: Partial<GrabOptions>;\n /** Request history and debugging info */\n log?: GrabLogEntry[];\n /** Mock server handlers for testing */\n mock?: Record<string, GrabMockHandler>;\n /** Create a separate instance of grab with separate default options */\n instance?: (defaultOptions?: Partial<GrabOptions>) => GrabFunction;\n}\n\n// Main grab function signature with overloads for different use cases\nexport interface GrabFunction {\n /**\n * ### GRAB: Generate Request to API from Browser\n * ![grabAPILogo](https://i.imgur.com/Rwl5P3p.png)\n * Make API request with path\n * @returns {Promise<Object>} The response object with resulting data or .error if error.\n * @author [vtempest (2025)](https://github.com/vtempest/GRAB-URL)\n * @see [🎯 Examples](https://grab.js.org/docs/Examples) [📑 Docs](https://grab.js.org/lib)\n */\n <TResponse = any, TParams = Record<string, any>>(path: string, options?: GrabOptions<TResponse, TParams>): Promise<\n GrabResponse<TResponse>\n >;\n\n /**\n * ### GRAB: Generate Request to API from Browser\n * ![grabAPILogo](https://i.imgur.com/Rwl5P3p.png)\n * Make API request with path and options/parameters\n * @returns {Promise<Object>} The response object with resulting data or .error if error.\n * @author [vtempest (2025)](https://github.com/vtempest/GRAB-URL)\n * @see [🎯 Examples](https://grab.js.org/docs/Examples) [📑 Docs](https://grab.js.org/lib)\n */\n <TResponse = any, TParams = Record<string, any>>(\n path: string,\n config: GrabOptions<TResponse, TParams>\n ): Promise<GrabResponse<TResponse>>;\n\n /** Default options applied to all requests */\n defaults?: Partial<GrabOptions>;\n\n /** Request history and debugging info for all requests */\n log?: GrabLogEntry[];\n\n /** Mock server handlers for testing */\n mock?: Record<string, GrabMockHandler>;\n\n /** Create a separate instance of grab with separate default options */\n instance?: (defaultOptions?: Partial<GrabOptions>) => GrabFunction;\n}\n\n// Log function for debugging\nexport interface LogFunction {\n /**\n * Log messages with custom styling\n * @param message - Message to log (string or object)\n */\n (message: string | object, options?: LogOptions): void;\n}\n\n// Utility function to describe JSON structure\nexport interface printJSONStructureFunction {\n /**\n * Generate TypeDoc-like description of JSON object structure\n * @param obj - The JSON object to describe\n * @returns String representation of object structure\n */\n (obj: any): string;\n}\n\ndeclare global {\n // Browser globals\n interface Window {\n grab: GrabFunction;\n log: LogFunction;\n scrollListener: (event: Event) => void; // replace 'any' with the actual type if you know it (e.g., a function type)\n }\n\n // Node.js globals\n namespace NodeJS {\n interface Global {\n grab: GrabFunction;\n log: LogFunction;\n }\n }\n\n // Global variables available after script inclusion\n var log: LogFunction;\n var grab: GrabFunction;\n}\n\nexport { grab, log, printJSONStructure };\n"],"names":["async","grab","path","options","headers","response","method","post","put","patch","cache","cacheForTime","timeout","baseURL","process","env","SERVER_API_URL","cancelOngoingIfNew","cancelNewIfOngoing","rateLimit","debug","infiniteScroll","setDefaults","retryAttempts","logger","log","onRequest","onResponse","onError","onStream","repeatEvery","repeat","debounce","regrabOnStale","regrabOnFocus","regrabOnNetwork","body","params","window","defaults","globalThis","s","t","startsWith","endsWith","slice","debouncer","i","setInterval","regrab","setTimeout","addEventListener","document","visibilityState","resFunction","paginateKey","paginateResult","paginateElement","length","paginateDOM","querySelector","scrollListener","removeEventListener","color","event","target","localStorage","setItem","JSON","stringify","scrollTop","scrollLeft","scrollHeight","clientHeight","priorRequest","currentPage","paramsAsText","find","e","request","pageNumber","key","Object","keys","lastFetchTime","Date","now","res","isLoading","Error","controller","abort","unshift","AbortController","fetchParams","Accept","redirect","signal","AbortSignal","paramsGETRequest","includes","URLSearchParams","toString","startTime","mockHandler","mock","wait","Promise","fetch","catch","message","ok","status","statusText","type","get","json","blob","text","delay","elapsedTime","Number","toFixed","printJSONStructure","data","error","errorMessage","instance","func","args","clearTimeout","showAlert","msg","list","o","getElementById","appendChild","createElement","id","setAttribute","innerHTML","remove","onclick","setupDevTools","ctrlKey","altKey","html","toLocaleString","parse","getItem"],"mappings":"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"}
1
+ {"version":3,"file":"grab-api.cjs.js","sources":["../src/grab-api.ts"],"sourcesContent":["import {\n printJSONStructure,\n log,\n type LogOptions,\n} from \"./log-json\";\n\n/**\n * ### GRAB: Generate Request to API from Browser\n * ![GrabAPILogo](https://i.imgur.com/Rwl5P3p.png)\n *\n * 1. **GRAB is the FBEST Request Manager: Functionally Brilliant, Elegantly Simple Tool**: One Function, no dependencies,\n * minimalist syntax, [more features than alternatives](https://grab.js.org/docs/Comparisons)\n * 2. **Auto-JSON Convert**: Pass parameters and get response or error in JSON, handling other data types as is.\n * 3. **isLoading Status**: Sets `.isLoading=true` on the pre-initialized response object so you can show a \"Loading...\" in any framework\n * 4. **Debug Logging**: Adds global `log()` and prints colored JSON structure, response, timing for requests in test.\n * 5. **Mock Server Support**: Configure `window.grab.mock` for development and testing environments\n * 6. **Cancel Duplicates**: Prevent this request if one is ongoing to same path & params, or cancel the ongoing request.\n * 7. **Timeout & Retry**: Customizable request timeout, default 30s, and auto-retry on error\n * 8. **DevTools**: `Ctrl+I` overlays webpage with devtools showing all requests and responses, timing, and JSON structure.\n * 9. **Request History**: Stores all request and response data in global `grab.log` object\n * 10. **Pagination Infinite Scroll**: Built-in pagination for infinite scroll to auto-load and merge next result page, with scroll position recovery.\n * 11. **Base URL Based on Environment**: Configure `grab.defaults.baseURL` once at the top, overide with `SERVER_API_URL` in `.env`.\n * 12. **Frontend Cache**: Set cache headers and retrieve from frontend memory for repeat requests to static data.\n * 13. **Regrab On Error**: Regrab on timeout error, or on window refocus, or on network change, or on stale data.\n * 14. **Framework Agnostic**: Alternatives like TanStack work only in component initialization and depend on React & others.\n * 15. **Globals**: Adds to window in browser or global in Node.js so you only import once: `grab()`, `log()`, `grab.log`, `grab.mock`, `grab.defaults`\n * 16. **TypeScript Tooltips**: Developers can hover over option names and autocomplete TypeScript.\n * 17. **Request Stategies**: [🎯 Examples](https://grab.js.org/docs/Examples) show common stategies like debounce, repeat, proxy, unit tests, interceptors, file upload, etc\n * 18. **Rate Limiting**: Built-in rate limiting to prevent multi-click cascading responses, require to wait seconds between requests.\n * 19. **Repeat**: Repeat request this many times, or repeat every X seconds to poll for updates.\n * 20. **Loading Icons**: Import from `grab-url/icons` to get enhanced animated loading icons.\n *\n * @param {string} path The full URL path OR relative path on this server after `grab.defaults.baseURL`\n * @param {object} [options={}] Request params for GET or body for POST/PUT/PATCH and utility options\n * @param {string} [options.method] default=\"GET\" The HTTP method to use\n * @param {object} [options.response] Pre-initialized object which becomes response JSON, no need for `.data`.\n * isLoading and error may also be set on this object. May omit and use return if load status is not needed.\n * @param {boolean} [options.cancelOngoingIfNew] default=false Cancel previous requests to same path\n * @param {boolean} [options.cancelNewIfOngoing] default=false Cancel if a request to path is in progress\n * @param {boolean} [options.cache] default=false Whether to cache the request and from frontend cache\n * @param {boolean} [options.debug] default=false Whether to log the request and response\n * @param {number} [options.timeout] default=30 The timeout for the request in seconds\n * @param {number} [options.cacheForTime] default=60 Seconds to consider data stale and invalidate cache\n * @param {number} [options.rateLimit] default=0 If set, how many seconds to wait between requests\n * @param {string} [options.baseURL] default='/api/' base url prefix, override with SERVER_API_URL env\n * @param {boolean} [options.setDefaults] default=false Pass this with options to set\n * those options as defaults for all requests.\n * @param {number} [options.retryAttempts] default=0 Retry failed requests this many times\n * @param {array} [options.infiniteScroll] default=null [page key, response field to concatenate, element with results]\n * @param {number} [options.repeat] default=0 Repeat request this many times\n * @param {number} [options.repeatEvery] default=null Repeat request every seconds\n * @param {function} [options.logger] default=log Custom logger to override the built-in color JSON log()\n * @param {function} [options.onRequest] Set with defaults to modify each request data.\n * Takes and returns in order: path, response, params, fetchParams\n * @param {function} [options.onResponse] Set with defaults to modify each request data.\n * Takes and returns in order: path, response, params, fetchParams\n * @param {function} [options.onStream] Set with defaults to process the response as a stream (i.e., for instant unzip)\n * @param {function} [options.onError] Set with defaults to modify the error data. Takes: error, path, params\n * @param {number} [options.debounce] default=0 Seconds to debounce request, wait to execute so that other requests may override\n * @param {boolean} [options.regrabOnStale] default=false Refetch when cache is past cacheForTime\n * @param {boolean} [options.regrabOnFocus] default=false Refetch on window refocus\n * @param {boolean} [options.regrabOnNetwork] default=false Refetch on network change\n * @param {any} [...params] All other params become GET params, POST body, and other methods.\n * @returns {Promise<Object>} The response object with resulting data or .error if error.\n * @author [vtempest (2025)](https://github.com/vtempest/GRAB-URL)\n * @see [🎯 Examples](https://grab.js.org/docs/Examples) [📑 Docs](https://grab.js.org)\n */\nexport default async function grab<TResponse = any, TParams = any>(\n path: string,\n options?: GrabOptions<TResponse, TParams>\n): Promise<GrabResponse<TResponse>> {\n var {\n headers,\n response = {} as any, // Pre-initialized object to set the response in. isLoading and error are also set on this object.\n method = options?.post // set post: true for POST, omit for GET\n ? \"POST\"\n : options?.put\n ? \"PUT\"\n : options?.patch\n ? \"PATCH\"\n : \"GET\",\n cache = false, // Enable/disable frontend caching\n cacheForTime = 60, // Seconds to consider data stale and invalidate cache\n timeout = 30, // Request timeout in seconds\n baseURL = (typeof process !== \"undefined\" && process.env.SERVER_API_URL) ||\n \"/api/\", // Use env var or default to /api/\n cancelOngoingIfNew = false, // Cancel previous request for same path\n cancelNewIfOngoing = false, // Don't make new request if one is ongoing\n rateLimit = 0, // Minimum seconds between requests\n debug = false, // Auto-enable debug on localhost\n // typeof window !== \"undefined\" && window?.location?.hostname?.includes(\"localhost\"),\n infiniteScroll = null, // page key, response field to concatenate, element with results\n setDefaults = false, // Set these options as defaults for future requests\n retryAttempts = 0, // Retry failed requests once\n logger = log, // Custom logger to override the built-in color JSON log()\n onRequest = null, // Hook to modify request data before request is made\n onResponse = null, // Hook to modify request data after request is made\n onError = null, // Hook to modify request data after request is made\n onStream = null, // Hook to process the response as a stream (i.e., for instant unarchiving)\n repeatEvery = null, // Repeat request every seconds\n repeat = 0, // Repeat request this many times\n debounce = 0, // Seconds to debounce request, wait to execute so that other requests may override\n regrabOnStale = false, // Refetch when cache is past cacheForTime\n regrabOnFocus = false, // Refetch on window refocus\n regrabOnNetwork = false, // Refetch on network change\n post = false,\n put = false,\n patch = false,\n body = null,\n ...params // All other params become request params/query\n } = {\n // Destructure options with defaults, merging with any globally set defaults\n ...(typeof window !== \"undefined\"\n ? window?.grab?.defaults\n : (globalThis)?.grab?.defaults || {}),\n ...options,\n };\n\n // Handle URL construction\n // Ensures proper joining of baseURL and path\n let s = (t) => path.startsWith(t);\n if (s(\"http:\") || s(\"https:\")) baseURL = \"\";\n else if (!s(\"/\") && !baseURL.endsWith(\"/\")) path = \"/\" + path;\n else if (s(\"/\") && baseURL.endsWith(\"/\")) path = path.slice(1);\n\n try {\n //handle debounce\n if (debounce > 0)\n return (await debouncer(async () => {\n await grab(path, { ...options, debounce: 0 });\n }, debounce * 1000)) as GrabResponse;\n\n // Handle repeat options:\n // - repeat: Makes the same request multiple times sequentially\n // - repeatEvery: Makes the request periodically on an interval\n if (repeat > 1) {\n for (let i = 0; i < repeat; i++) {\n await grab(path, { ...options, repeat: 0 });\n }\n return response;\n }\n if (repeatEvery) {\n setInterval(async () => {\n await grab(path, { ...options, repeat: 0, repeatEvery: null });\n }, repeatEvery * 1000);\n return response;\n }\n\n // Store the provided options as new defaults if setDefaults flag is set\n // This allows configuring default options that apply to all future requests\n if (options?.setDefaults) {\n if (typeof window !== \"undefined\")\n window.grab.defaults = { ...options, setDefaults: undefined };\n else if (typeof globalThis.grab !== \"undefined\")\n globalThis.grab.defaults = {\n ...options,\n setDefaults: undefined,\n };\n\n return;\n }\n\n // regrab on stale, on window refocus, on network\n if (typeof window !== \"undefined\") {\n const regrab = async () => await grab(path, { ...options, cache: false });\n if (regrabOnStale && cache) setTimeout(regrab, 1000 * cacheForTime);\n if (regrabOnNetwork) window.addEventListener(\"online\", regrab);\n if (regrabOnFocus) {\n window.addEventListener(\"focus\", regrab);\n document.addEventListener(\"visibilitychange\", async () => {\n if (document.visibilityState === \"visible\") await regrab();\n });\n }\n }\n\n // Handle response parameter which can be either an object to populate\n // or a function to call with results (e.g. React setState)\n let resFunction = typeof response === \"function\" ? response : null;\n if (!response || resFunction) response = {};\n\n var [paginateKey, paginateResult, paginateElement] = infiniteScroll || [];\n\n // Configure infinite scroll behavior if enabled\n // Attaches scroll listener to specified element that triggers next page load\n if (infiniteScroll?.length && typeof paginateElement !== \"undefined\"\n && typeof window !== \"undefined\") {\n let paginateDOM =\n typeof paginateElement === \"string\"\n ? document.querySelector(paginateElement)\n : paginateElement;\n\n if (!paginateDOM) log(\"paginateDOM not found\", { color: \"red\" });\n else if (window.scrollListener\n && typeof paginateDOM !== \"undefined\"\n && typeof paginateDOM.removeEventListener === \"function\")\n paginateDOM.removeEventListener(\"scroll\", window.scrollListener);\n\n // Your modified scroll listener with position saving\n window.scrollListener = async (event) => {\n const t = event.target as HTMLElement;\n\n // Save scroll position whenever user scrolls\n localStorage.setItem(\n \"scroll\",\n JSON.stringify([t.scrollTop, t.scrollLeft, paginateElement])\n );\n\n if (t.scrollHeight - t.scrollTop <= t.clientHeight + 200) {\n await grab(path, {\n ...options,\n cache: false,\n [paginateKey]: priorRequest?.currentPage + 1,\n });\n }\n };\n\n if (paginateDOM)\n paginateDOM.addEventListener(\"scroll\", window.scrollListener);\n }\n\n // Check request history for a previous request with same path/params\n // Used for caching and pagination. Ignores pagination params when comparing.\n let paramsAsText = JSON.stringify(\n paginateKey ? { ...params, [paginateKey]: undefined } : params\n );\n let priorRequest = grab?.log?.find(\n (e) => e.request == paramsAsText && e.path == path\n );\n\n // Handle response data management based on pagination settings\n if (!paginateKey) {\n // Clear any existing response data\n for (let key of Object.keys(response)) response[key] = undefined;\n\n // For non-paginated requests:\n // Return cached response if caching enabled and identical request exists\n // after returning cache, proceed with call to revalidate ensure data is up to date\n if (\n cache &&\n (!cacheForTime ||\n priorRequest?.lastFetchTime > Date.now() - 1000 * cacheForTime)\n ) {\n // set response to cache data\n for (let key of Object.keys(priorRequest.res))\n response[key] = priorRequest.res[key];\n if (resFunction) response = resFunction(response);\n\n // if (!cacheValidate) return response;\n }\n } else {\n // For paginated requests:\n // Track current page number and append results to existing data\n let pageNumber =\n priorRequest?.currentPage + 1 || params?.[paginateKey] || 1;\n\n // Clear response if this is a new request with new params\n if (!priorRequest) {\n response[paginateResult] = [];\n pageNumber = 1;\n }\n\n // Update page tracking\n if (priorRequest) priorRequest.currentPage = pageNumber;\n params = { ...params, [paginateKey]: pageNumber };\n }\n\n // Set loading state on response object\n if (resFunction) resFunction({ isLoading: true });\n else if (typeof response === \"object\") response.isLoading = true;\n\n if (resFunction) response = resFunction(response);\n\n // Enforce rate limiting between requests if configured\n if (\n rateLimit > 0 &&\n priorRequest?.lastFetchTime &&\n priorRequest.lastFetchTime > Date.now() - 1000 * rateLimit\n )\n throw new Error(`Fetch rate limit exceeded for ${path}. \n Wait ${rateLimit}s between requests.`);\n\n // Handle request cancellation based on configuration:\n // - cancelOngoingIfNew: Cancels any in-progress request for same path\n // - cancelNewIfOngoing: Prevents new request if one is already in progress\n if (priorRequest?.controller)\n if (cancelOngoingIfNew) priorRequest.controller.abort();\n else if (cancelNewIfOngoing) return { isLoading: true } as GrabResponse;\n\n // Track new request in history log\n if (typeof grab.log != \"undefined\")\n grab.log?.unshift({\n path,\n request: paramsAsText,\n lastFetchTime: Date.now(),\n controller: new AbortController(),\n });\n\n // Configure fetch request parameters including headers, cache settings,\n // and timeout/cancellation signals\n let fetchParams = {\n method,\n headers: {\n \"Content-Type\": \"application/json\",\n Accept: \"application/json\",\n ...headers,\n },\n body: params.body,\n redirect: \"follow\" as RequestRedirect,\n cache: cache ? \"force-cache\" : (\"no-store\" as RequestCache),\n signal: cancelOngoingIfNew\n ? grab.log[0]?.controller?.signal\n : AbortSignal.timeout(timeout * 1000),\n } as RequestInit;\n\n // Format request parameters based on HTTP method\n // POST/PUT/PATCH send data in request body\n // GET/DELETE append data as URL query parameters\n let paramsGETRequest = \"\";\n if ([\"POST\", \"PUT\", \"PATCH\"].includes(method))\n fetchParams.body = params.body || JSON.stringify(params);\n else\n paramsGETRequest =\n (Object.keys(params).length ? \"?\" : \"\") +\n new URLSearchParams(params).toString();\n\n // Execute pre-request hook if configured\n // Allows modifying request data before sending\n if (typeof onRequest === \"function\")\n [path, response, params, fetchParams] = onRequest(\n path,\n response,\n params,\n fetchParams\n );\n\n // Process request through mock handler if configured\n // Otherwise make actual API request\n let res = null,\n startTime = new Date(),\n mockHandler = grab.mock?.[path] as GrabMockHandler;\n\n let wait = (s) => new Promise((res) => setTimeout(res, s * 1000 || 0));\n\n if (\n mockHandler &&\n (!mockHandler.params || mockHandler.method == method) &&\n (!mockHandler.params ||\n paramsAsText == JSON.stringify(mockHandler.params))\n ) {\n await wait(mockHandler.delay);\n\n res =\n typeof mockHandler.response === \"function\"\n ? mockHandler.response(params)\n : mockHandler.response;\n } else {\n // Make actual API request and handle response based on content type\n res = await fetch(baseURL + path + paramsGETRequest, fetchParams).catch(\n (e) => {\n throw new Error(e.message);\n }\n );\n\n if (!res.ok)\n throw new Error(`HTTP error: ${res.status} ${res.statusText}`);\n\n // Convert browser ReadableStream to Node.js stream\n let type = res.headers.get(\"content-type\");\n\n if (onStream) await onStream(res.body);\n else\n res = await (type\n ? type.includes(\"application/json\")\n ? res && res.json()\n : type.includes(\"application/pdf\") ||\n type.includes(\"application/octet-stream\")\n ? res.blob()\n : res.text()\n : res.json()\n ).catch((e) => {\n throw new Error(\"Error parsing response: \" + e);\n });\n }\n\n // Execute post-request hook if configured\n // Allows modifying response data before processing\n if (typeof onResponse === \"function\")\n [path, response, params, fetchParams] = onResponse(\n path,\n response,\n params,\n fetchParams\n );\n\n // Clear request tracking states\n if (resFunction) resFunction({ isLoading: undefined });\n else if (typeof response === \"object\") delete response?.isLoading;\n\n delete priorRequest?.controller;\n\n // Log debug information if enabled\n // Includes request details, timing, and response structure\n const elapsedTime = (\n (Number(new Date()) - Number(startTime)) /\n 1000\n ).toFixed(1);\n if (debug) {\n logger(\n \"Path:\" +\n baseURL +\n path +\n paramsGETRequest +\n \"\\n\" +\n JSON.stringify(options, null, 2) +\n \"\\nTime: \" +\n elapsedTime +\n \"s\\nResponse: \" +\n printJSONStructure(res)\n );\n // console.log(res);\n }\n\n // if (typeof res === \"undefined\") return;\n\n // Update response object with results\n // For paginated requests, concatenates with existing results\n if (typeof res === \"object\") {\n for (let key of Object.keys(res))\n response[key] =\n paginateResult == key && response[key]?.length\n ? [...response[key], ...res[key]]\n : res[key];\n\n if (typeof response !== \"undefined\") response.data = res; // for axios compat\n } else if (resFunction) resFunction({ data: res, ...res });\n else if (typeof response === \"object\") response.data = res;\n\n // Store request/response in history log\n if (typeof grab.log != \"undefined\")\n grab.log?.unshift({\n path,\n request: JSON.stringify({ ...params, paginateKey: undefined }),\n response,\n lastFetchTime: Date.now(),\n });\n\n if (resFunction) response = resFunction(response);\n\n return response;\n } catch (error) {\n // Handle any errors that occurred during request processing\n let errorMessage =\n \"Error: \" + error.message + \"\\nPath:\" + baseURL + path + \"\\n\";\n // JSON.stringify(params);\n\n // if onError hook is passed\n if (typeof onError === \"function\")\n onError(error.message, baseURL + path, params);\n\n // Retry request if retries are configured and attempts remain\n if (options.retryAttempts > 0)\n return await grab(path, {\n ...options,\n retryAttempts: --options.retryAttempts,\n });\n\n // Update error state in response object\n // Do not show errors for duplicate aborted requests\n if (!error.message.includes(\"signal\") && options.debug) {\n logger(errorMessage, { color: \"red\" });\n if (debug && typeof document !== \"undefined\") showAlert(errorMessage);\n }\n response.error = error.message;\n if (typeof response === \"function\") {\n response.data = response({ isLoading: undefined, error: error.message });\n response = response.data;\n } else delete response?.isLoading;\n\n // Log error in request history\n if (typeof grab.log != \"undefined\")\n grab.log?.unshift({\n path,\n request: JSON.stringify(params),\n error: error.message,\n });\n\n // if (typeof options.response === \"function\")\n // response = options.response(response);\n return response;\n }\n}\n\n/**\n * Creates a new instance of grab with default options\n * to apply to all requests made by this instance\n * @param {Object} defaults - options for all requests by instance\n * @returns {Function} grab() function using those options\n */\ngrab.instance =\n (defaults = {}) =>\n (path, options = {}) =>\n grab(path, { ...defaults, ...options });\n\n// delays execution so that future calls may override and only executes last one\nconst debouncer = async (func, wait) => {\n let timeout;\n return async function executedFunction(...args) {\n const later = async () => {\n clearTimeout(timeout);\n await func(...args);\n };\n clearTimeout(timeout);\n timeout = setTimeout(later, wait);\n };\n};\n\n// Add globals to window in browser, or global in Node.js\nif (typeof window !== \"undefined\") {\n window.log = log;\n window.grab = grab;\n\n window.grab.log = [];\n window.grab.mock = {};\n window.grab.defaults = {};\n\n //Ctrl+I setup dev tools\n setupDevTools();\n\n // Restore scroll position when page loads or component mounts\n document.addEventListener(\"DOMContentLoaded\", () => {\n let [scrollTop, scrollLeft, paginateElement] =\n JSON.parse(localStorage.getItem(\"scroll\")) || [];\n if (!scrollTop) return;\n document.querySelector(paginateElement).scrollTop = scrollTop;\n document.querySelector(paginateElement).scrollLeft = scrollLeft;\n });\n} else if (typeof globalThis !== \"undefined\") {\n grab.log = [];\n grab.mock = {};\n grab.defaults = {};\n globalThis.log = log;\n globalThis.grab = grab.instance();\n}\n\n\n\n/**\n * Shows message in a modal overlay with scrollable message stack\n * and is easier to dismiss unlike alert() which blocks window.\n * Creates a semi-transparent overlay with a white box containing the message.\n * @param {string} msg - The message to display\n */\nexport function showAlert(msg) {\n if (typeof document === \"undefined\") return;\n let o = document.getElementById(\"alert-overlay\"),\n list;\n\n // Create overlay and alert box if they don't exist\n if (!o) {\n o = document.body.appendChild(document.createElement(\"div\"));\n o.id = \"alert-overlay\";\n o.setAttribute(\n \"style\",\n \"position:fixed;inset:0;z-index:9999;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center\"\n );\n o.innerHTML = `<div id=\"alert-box\" style=\"background:#fff;padding:1.5em 2em;border-radius:8px;box-shadow:0 2px 16px #0003;min-width:220px;max-height:80vh;position:relative;display:flex;flex-direction:column;\">\n <button id=\"close-alert\" style=\"position:absolute;top:12px;right:20px;font-size:1.5em;background:none;border:none;cursor:pointer;color:black;\">&times;</button>\n <div id=\"alert-list\" style=\"overflow:auto;flex:1;\"></div>\n </div>`;\n\n // Add click handlers to close overlay\n o.addEventListener(\"click\", (e) => e.target == o && o.remove());\n document.getElementById(\"close-alert\").onclick = () => o.remove();\n }\n\n list = o.querySelector(\"#alert-list\");\n\n // Add new message to list\n list.innerHTML += `<div style=\"border-bottom:1px solid #333; font-size:1.2em;margin:0.5em 0;\">${msg}</div>`;\n}\n\n/**\n * Sets up development tools for debugging API requests\n * Adds a keyboard shortcut (Ctrl+Alt+I) that shows a modal with request history\n * Each request entry shows:\n * - Request path\n * - Request details\n * - Response data\n * - Timestamp\n */\nexport function setupDevTools() {\n // Keyboard shortcut (Ctrl+Alt+I) to toggle debug view\n document.addEventListener(\"keydown\", (e) => {\n if (e.key === \"i\" && e.ctrlKey && e.altKey) {\n // Create HTML of the grab.log requests\n let html = \" \";\n for (let request of grab.log) {\n html += `<div style=\"margin-bottom:1em; border-bottom:1px solid #ccc; padding-bottom:1em;\">\n <b>Path:</b> ${request.path}<br>\n <b>Request:</b> ${printJSONStructure(request.request, 0, 'html')}<br>\n <b>Response:</b> ${printJSONStructure(request.response, 0, 'html')}<br> \n <b>Time:</b> ${new Date(request.lastFetchTime).toLocaleString()}\n </div>`;\n }\n showAlert(html);\n }\n });\n}\n\n\n\n/***************** TYPESCRIPT INTERFACES *****************/\n\n// Core response object that gets populated with API response data\nexport type GrabResponse<TResponse = any> = TResponse & {\n /** Indicates if request is currently in progress */\n isLoading?: boolean;\n /** Error message if request failed */\n error?: string;\n /** Binary or text response data (JSON is set to the root)*/\n data?: TResponse | any;\n /** The actual response data - type depends on API endpoint */\n [key: string]: unknown;\n};\n\nexport type GrabOptions<TResponse = any, TParams = any> = TParams & {\n /** include headers and authorization in the request */\n headers?: Record<string, string>;\n /** Pre-initialized object which becomes response JSON, no need for .data */\n response?: TResponse | ((params: TParams) => TResponse) | any;\n /** default=\"GET\" The HTTP method to use */\n method?: \"GET\" | \"POST\" | \"PUT\" | \"PATCH\" | \"DELETE\" | \"OPTIONS\" | \"HEAD\";\n /** default=false Whether to cache the request and from frontend cache */\n cache?: boolean;\n /** default=60 Seconds to consider data stale and invalidate cache */\n cacheForTime?: number;\n /** default=30 The timeout for the request in seconds */\n timeout?: number;\n /** default='/api/' base url prefix, override with SERVER_API_URL env */\n baseURL?: string;\n /** default=true Cancel previous requests to same path */\n cancelOngoingIfNew?: boolean;\n /** default=false Cancel if a request to path is in progress */\n cancelNewIfOngoing?: boolean;\n /** default=false If set, how many seconds to wait between requests */\n rateLimit?: number;\n /** default=false Whether to log the request and response */\n debug?: boolean;\n /** default=null [page key, response field to concatenate, element with results] */\n infiniteScroll?: [string, string, string | HTMLElement];\n /** default=false Pass this with options to set those options as defaults for all requests */\n setDefaults?: boolean;\n /** default=0 Retry failed requests this many times */\n retryAttempts?: number;\n /** default=log Custom logger to override the built-in color JSON log() */\n logger?: (...args: any[]) => void;\n /** Set with defaults to modify each request data. Takes and returns in order: path, response, params, fetchParams */\n onRequest?: (...args: any[]) => any;\n /** Set with defaults to modify each request data. Takes and returns in order: path, response, params, fetchParams */\n onResponse?: (...args: any[]) => any;\n /** Set with defaults to modify each request data. Takes and returns in order: error, path, params */\n onError?: (...args: any[]) => any;\n /** Set with defaults to process the response as a stream (i.e., for instant unzip) */\n onStream?: (...args: any[]) => any;\n /** default=0 Repeat request this many times */\n repeat?: number;\n /** default=null Repeat request every seconds */\n repeatEvery?: number;\n /** default=0 Seconds to debounce request, wait to execute so that other requests may override */\n debounce?: number;\n /** default=false Refetch when cache is past cacheForTime */\n regrabOnStale?: boolean;\n /** default=false Refetch on window refocus */\n regrabOnFocus?: boolean;\n /** default=false Refetch on network change */\n regrabOnNetwork?: boolean;\n /** shortcut for method: \"POST\" */\n post?: boolean;\n /** shortcut for method: \"PUT\" */\n put?: boolean;\n /** shortcut for method: \"PATCH\" */\n patch?: boolean;\n /** default=null The body of the POST/PUT/PATCH request (can be passed into main)*/\n body?: any;\n /** All other params become GET params, POST body, and other methods */\n [key: string]: TParams | any;\n};\n\n// Combined options and parameters interface\n\n// Mock server configuration for testing\nexport interface GrabMockHandler<TParams = any, TResponse = any> {\n /** Mock response data or function that returns response */\n response: TResponse | ((params: TParams) => TResponse);\n /** HTTP method this mock should respond to */\n method?: \"GET\" | \"POST\" | \"PUT\" | \"PATCH\" | \"DELETE\" | \"OPTIONS\" | \"HEAD\";\n /** Request parameters this mock should match */\n params?: TParams;\n /** Delay in seconds before returning mock response */\n delay?: number;\n}\n\n// Request log entry for debugging and history\nexport interface GrabLogEntry {\n /** API path that was requested */\n path: string;\n /** Stringified request parameters */\n request: string;\n /** Response data (only present for successful requests) */\n response?: any;\n /** Error message (only present for failed requests) */\n error?: string;\n /** Timestamp when request was made */\n lastFetchTime: number;\n /** Abort controller for request cancellation */\n controller?: AbortController;\n /** Current page number for paginated requests */\n currentPage?: number;\n}\n\n// Global grab configuration and state\nexport interface GrabGlobal {\n /** Default options applied to all requests */\n defaults?: Partial<GrabOptions>;\n /** Request history and debugging info */\n log?: GrabLogEntry[];\n /** Mock server handlers for testing */\n mock?: Record<string, GrabMockHandler>;\n /** Create a separate instance of grab with separate default options */\n instance?: (defaultOptions?: Partial<GrabOptions>) => GrabFunction;\n}\n\n// Main grab function signature with overloads for different use cases\nexport interface GrabFunction {\n /**\n * ### GRAB: Generate Request to API from Browser\n * ![grabAPILogo](https://i.imgur.com/Rwl5P3p.png)\n * Make API request with path\n * @returns {Promise<Object>} The response object with resulting data or .error if error.\n * @author [vtempest (2025)](https://github.com/vtempest/GRAB-URL)\n * @see [🎯 Examples](https://grab.js.org/docs/Examples) [📑 Docs](https://grab.js.org/lib)\n */\n <TResponse = any, TParams = Record<string, any>>(path: string, options?: GrabOptions<TResponse, TParams>): Promise<\n GrabResponse<TResponse>\n >;\n\n /**\n * ### GRAB: Generate Request to API from Browser\n * ![grabAPILogo](https://i.imgur.com/Rwl5P3p.png)\n * Make API request with path and options/parameters\n * @returns {Promise<Object>} The response object with resulting data or .error if error.\n * @author [vtempest (2025)](https://github.com/vtempest/GRAB-URL)\n * @see [🎯 Examples](https://grab.js.org/docs/Examples) [📑 Docs](https://grab.js.org/lib)\n */\n <TResponse = any, TParams = Record<string, any>>(\n path: string,\n config: GrabOptions<TResponse, TParams>\n ): Promise<GrabResponse<TResponse>>;\n\n /** Default options applied to all requests */\n defaults?: Partial<GrabOptions>;\n\n /** Request history and debugging info for all requests */\n log?: GrabLogEntry[];\n\n /** Mock server handlers for testing */\n mock?: Record<string, GrabMockHandler>;\n\n /** Create a separate instance of grab with separate default options */\n instance?: (defaultOptions?: Partial<GrabOptions>) => GrabFunction;\n}\n\n// Log function for debugging\nexport interface LogFunction {\n /**\n * Log messages with custom styling\n * @param message - Message to log (string or object)\n */\n (message: string | object, options?: LogOptions): void;\n}\n\n// Utility function to describe JSON structure\nexport interface printJSONStructureFunction {\n /**\n * Generate TypeDoc-like description of JSON object structure\n * @param obj - The JSON object to describe\n * @returns String representation of object structure\n */\n (obj: any): string;\n}\n\ndeclare global {\n // Browser globals\n interface Window {\n grab: GrabFunction;\n log: LogFunction;\n scrollListener: (event: Event) => void; // replace 'any' with the actual type if you know it (e.g., a function type)\n }\n\n // Node.js globals\n namespace NodeJS {\n interface Global {\n grab: GrabFunction;\n log: LogFunction;\n }\n }\n\n // Global variables available after script inclusion\n var log: LogFunction;\n var grab: GrabFunction;\n}\n\nexport { grab, log, printJSONStructure };\n"],"names":["async","grab","path","options","headers","response","method","post","put","patch","cache","cacheForTime","timeout","baseURL","process","env","SERVER_API_URL","cancelOngoingIfNew","cancelNewIfOngoing","rateLimit","debug","infiniteScroll","setDefaults","retryAttempts","logger","log","onRequest","onResponse","onError","onStream","repeatEvery","repeat","debounce","regrabOnStale","regrabOnFocus","regrabOnNetwork","body","params","window","defaults","globalThis","s","t","startsWith","endsWith","slice","debouncer","i","setInterval","regrab","setTimeout","addEventListener","document","visibilityState","resFunction","paginateKey","paginateResult","paginateElement","length","paginateDOM","querySelector","scrollListener","removeEventListener","color","event","target","localStorage","setItem","JSON","stringify","scrollTop","scrollLeft","scrollHeight","clientHeight","priorRequest","currentPage","paramsAsText","find","e","request","pageNumber","key","Object","keys","lastFetchTime","Date","now","res","isLoading","Error","controller","abort","unshift","AbortController","fetchParams","Accept","redirect","signal","AbortSignal","paramsGETRequest","includes","URLSearchParams","toString","startTime","mockHandler","mock","wait","Promise","fetch","catch","message","ok","status","statusText","type","get","json","blob","text","delay","elapsedTime","Number","toFixed","printJSONStructure","data","error","errorMessage","instance","func","args","clearTimeout","showAlert","msg","list","o","getElementById","appendChild","createElement","id","setAttribute","innerHTML","remove","onclick","setupDevTools","ctrlKey","altKey","html","toLocaleString","parse","getItem"],"mappings":"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"}
@@ -98,7 +98,7 @@ declare enum ColorName {
98
98
  * @author [vtempest (2025)](https://github.com/vtempest/GRAB-URL)
99
99
  * @see [🎯 Examples](https://grab.js.org/docs/Examples) [📑 Docs](https://grab.js.org)
100
100
  */
101
- declare function grab_2<TResponse = any, TParams = any>(path: string, options: GrabOptions<TResponse, TParams>): Promise<GrabResponse<TResponse>>;
101
+ declare function grab_2<TResponse = any, TParams = any>(path: string, options?: GrabOptions<TResponse, TParams>): Promise<GrabResponse<TResponse>>;
102
102
 
103
103
  declare namespace grab_2 {
104
104
  var instance: (defaults?: {}) => (path: any, options?: {}) => Promise<any>;
@@ -264,7 +264,8 @@ export declare type GrabResponse<TResponse = any> = TResponse & {
264
264
  * If true, uses `console.debug` (hidden in production). If false, uses `console.log`.
265
265
  *
266
266
  */
267
- export declare function log(message?: string | object, options?: LogOptions): boolean;
267
+ declare function log_2(message?: string | object, options?: LogOptions): boolean;
268
+ export { log_2 as log }
268
269
 
269
270
  export declare interface LogFunction {
270
271
  /**
@@ -1,2 +1,2 @@
1
- import{log as e,printJSONStructure as t}from"./log.es.js";async function o(r,a={}){var{headers:s,response:l={},method:d=(a.post?"POST":a.put?"PUT":a.patch?"PATCH":"GET"),cache:c=!1,cacheForTime:u=60,timeout:f=30,baseURL:p="undefined"!=typeof process&&process.env.SERVER_API_URL||"/api/",cancelOngoingIfNew:g=!1,cancelNewIfOngoing:m=!1,rateLimit:y=0,debug:b=!1,infiniteScroll:w=null,setDefaults:h=!1,retryAttempts:v=0,logger:T=e,onRequest:L=null,onResponse:S=null,onError:E=null,onStream:x=null,repeatEvery:O=null,repeat:k=0,debounce:P=0,regrabOnStale:j=!1,regrabOnFocus:D=!1,regrabOnNetwork:N=!1,post:q=!1,put:A=!1,patch:F=!1,body:R=null,...$}={..."undefined"!=typeof window?window?.grab?.defaults:globalThis?.grab?.defaults||{},...a};let I=e=>r.startsWith(e);I("http:")||I("https:")?p="":I("/")||p.endsWith("/")?I("/")&&p.endsWith("/")&&(r=r.slice(1)):r="/"+r;try{if(P>0)return await n(async()=>{await o(r,{...a,debounce:0})},1e3*P);if(k>1){for(let e=0;e<k;e++)await o(r,{...a,repeat:0});return l}if(O)return setInterval(async()=>{await o(r,{...a,repeat:0,repeatEvery:null})},1e3*O),l;if(a?.setDefaults)return void("undefined"!=typeof window?window.grab.defaults={...a,setDefaults:void 0}:void 0!==globalThis.grab&&(globalThis.grab.defaults={...a,setDefaults:void 0}));if("undefined"!=typeof window){const e=async()=>await o(r,{...a,cache:!1});j&&c&&setTimeout(e,1e3*u),N&&window.addEventListener("online",e),D&&(window.addEventListener("focus",e),document.addEventListener("visibilitychange",async()=>{"visible"===document.visibilityState&&await e()}))}let i="function"==typeof l?l:null;l&&!i||(l={});var[J,H,C]=w||[];if(w?.length&&void 0!==C&&"undefined"!=typeof window){let t="string"==typeof C?document.querySelector(C):C;t?window.scrollListener&&void 0!==t&&"function"==typeof t.removeEventListener&&t.removeEventListener("scroll",window.scrollListener):e("paginateDOM not found",{color:"red"}),window.scrollListener=async e=>{const t=e.target;localStorage.setItem("scroll",JSON.stringify([t.scrollTop,t.scrollLeft,C])),t.scrollHeight-t.scrollTop<=t.clientHeight+200&&await o(r,{...a,cache:!1,[J]:v?.currentPage+1})},t&&t.addEventListener("scroll",window.scrollListener)}let h=JSON.stringify(J?{...$,[J]:void 0}:$),v=o?.log?.find(e=>e.request==h&&e.path==r);if(J){let e=v?.currentPage+1||$?.[J]||1;v||(l[H]=[],e=1),v&&(v.currentPage=e),$={...$,[J]:e}}else{for(let e of Object.keys(l))l[e]=void 0;if(c&&(!u||v?.lastFetchTime>Date.now()-1e3*u)){for(let e of Object.keys(v.res))l[e]=v.res[e];i&&(l=i(l))}}if(i?i({isLoading:!0}):"object"==typeof l&&(l.isLoading=!0),i&&(l=i(l)),y>0&&v?.lastFetchTime&&v.lastFetchTime>Date.now()-1e3*y)throw new Error(`Fetch rate limit exceeded for ${r}. \n Wait ${y}s between requests.`);if(v?.controller)if(g)v.controller.abort();else if(m)return{isLoading:!0};void 0!==o.log&&o.log?.unshift({path:r,request:h,lastFetchTime:Date.now(),controller:new AbortController});let E={method:d,headers:{"Content-Type":"application/json",Accept:"application/json",...s},body:$.body,redirect:"follow",cache:c?"force-cache":"no-store",signal:g?o.log[0]?.controller?.signal:AbortSignal.timeout(1e3*f)},q="";["POST","PUT","PATCH"].includes(d)?E.body=$.body||JSON.stringify($):q=(Object.keys($).length?"?":"")+new URLSearchParams($).toString(),"function"==typeof L&&([r,l,$,E]=L(r,l,$,E));let A=null,F=/* @__PURE__ */new Date,R=o.mock?.[r],I=e=>new Promise(t=>setTimeout(t,1e3*e||0));if(!R||R.params&&R.method!=d||R.params&&h!=JSON.stringify(R.params)){if(A=await fetch(p+r+q,E).catch(e=>{throw new Error(e.message)}),!A.ok)throw new Error(`HTTP error: ${A.status} ${A.statusText}`);let e=A.headers.get("content-type");x?await x(A.body):A=await(e?e.includes("application/json")?A&&A.json():e.includes("application/pdf")||e.includes("application/octet-stream")?A.blob():A.text():A.json()).catch(e=>{throw new Error("Error parsing response: "+e)})}else await I(R.delay),A="function"==typeof R.response?R.response($):R.response;"function"==typeof S&&([r,l,$,E]=S(r,l,$,E)),i?i({isLoading:void 0}):"object"==typeof l&&delete l?.isLoading,delete v?.controller;const U=((Number(/* @__PURE__ */new Date)-Number(F))/1e3).toFixed(1);if(b&&T("Path:"+p+r+q+"\n"+JSON.stringify(a,null,2)+"\nTime: "+U+"s\nResponse: "+t(A)),"object"==typeof A){for(let e of Object.keys(A))l[e]=H==e&&l[e]?.length?[...l[e],...A[e]]:A[e];void 0!==l&&(l.data=A)}else i?i({data:A,...A}):"object"==typeof l&&(l.data=A);return void 0!==o.log&&o.log?.unshift({path:r,request:JSON.stringify({...$,paginateKey:void 0}),response:l,lastFetchTime:Date.now()}),i&&(l=i(l)),l}catch(U){let e="Error: "+U.message+"\nPath:"+p+r+"\n";return"function"==typeof E&&E(U.message,p+r,$),a.retryAttempts>0?await o(r,{...a,retryAttempts:--a.retryAttempts}):(!U.message.includes("signal")&&a.debug&&(T(e,{color:"red"}),b&&"undefined"!=typeof document&&i(e)),l.error=U.message,"function"==typeof l?(l.data=l({isLoading:void 0,error:U.message}),l=l.data):delete l?.isLoading,void 0!==o.log&&o.log?.unshift({path:r,request:JSON.stringify($),error:U.message}),l)}}o.instance=(e={})=>(t,n={})=>o(t,{...e,...n});const n=async(e,t)=>{let o;return async function(...n){clearTimeout(o),o=setTimeout(async()=>{clearTimeout(o),await e(...n)},t)}};function i(e){if("undefined"==typeof document)return;let t,o=document.getElementById("alert-overlay");o||(o=document.body.appendChild(document.createElement("div")),o.id="alert-overlay",o.setAttribute("style","position:fixed;inset:0;z-index:9999;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center"),o.innerHTML='<div id="alert-box" style="background:#fff;padding:1.5em 2em;border-radius:8px;box-shadow:0 2px 16px #0003;min-width:220px;max-height:80vh;position:relative;display:flex;flex-direction:column;">\n <button id="close-alert" style="position:absolute;top:12px;right:20px;font-size:1.5em;background:none;border:none;cursor:pointer;color:black;">&times;</button>\n <div id="alert-list" style="overflow:auto;flex:1;"></div>\n </div>',o.addEventListener("click",e=>e.target==o&&o.remove()),document.getElementById("close-alert").onclick=()=>o.remove()),t=o.querySelector("#alert-list"),t.innerHTML+=`<div style="border-bottom:1px solid #333; font-size:1.2em;margin:0.5em 0;">${e}</div>`}function r(){document.addEventListener("keydown",e=>{if("i"===e.key&&e.ctrlKey&&e.altKey){let e=" ";for(let n of o.log)e+=`<div style="margin-bottom:1em; border-bottom:1px solid #ccc; padding-bottom:1em;">\n <b>Path:</b> ${n.path}<br>\n <b>Request:</b> ${t(n.request,0,"html")}<br>\n <b>Response:</b> ${t(n.response,0,"html")}<br> \n <b>Time:</b> ${new Date(n.lastFetchTime).toLocaleString()}\n </div>`;i(e)}})}"undefined"!=typeof window?(window.log=e,window.grab=o,window.grab.log=[],window.grab.mock={},window.grab.defaults={},r(),document.addEventListener("DOMContentLoaded",()=>{let[e,t,o]=JSON.parse(localStorage.getItem("scroll"))||[];e&&(document.querySelector(o).scrollTop=e,document.querySelector(o).scrollLeft=t)})):"undefined"!=typeof globalThis&&(o.log=[],o.mock={},o.defaults={},globalThis.log=e,globalThis.grab=o.instance());export{o as default,o as grab,e as log,t as printJSONStructure,r as setupDevTools,i as showAlert};
1
+ import{log as e,printJSONStructure as t}from"./log.es.js";async function o(r,a){var{headers:s,response:l={},method:d=(a?.post?"POST":a?.put?"PUT":a?.patch?"PATCH":"GET"),cache:c=!1,cacheForTime:u=60,timeout:f=30,baseURL:p="undefined"!=typeof process&&process.env.SERVER_API_URL||"/api/",cancelOngoingIfNew:g=!1,cancelNewIfOngoing:m=!1,rateLimit:y=0,debug:b=!1,infiniteScroll:w=null,setDefaults:h=!1,retryAttempts:v=0,logger:T=e,onRequest:L=null,onResponse:S=null,onError:E=null,onStream:x=null,repeatEvery:O=null,repeat:k=0,debounce:P=0,regrabOnStale:j=!1,regrabOnFocus:D=!1,regrabOnNetwork:N=!1,post:q=!1,put:A=!1,patch:F=!1,body:R=null,...$}={..."undefined"!=typeof window?window?.grab?.defaults:globalThis?.grab?.defaults||{},...a};let I=e=>r.startsWith(e);I("http:")||I("https:")?p="":I("/")||p.endsWith("/")?I("/")&&p.endsWith("/")&&(r=r.slice(1)):r="/"+r;try{if(P>0)return await n(async()=>{await o(r,{...a,debounce:0})},1e3*P);if(k>1){for(let e=0;e<k;e++)await o(r,{...a,repeat:0});return l}if(O)return setInterval(async()=>{await o(r,{...a,repeat:0,repeatEvery:null})},1e3*O),l;if(a?.setDefaults)return void("undefined"!=typeof window?window.grab.defaults={...a,setDefaults:void 0}:void 0!==globalThis.grab&&(globalThis.grab.defaults={...a,setDefaults:void 0}));if("undefined"!=typeof window){const e=async()=>await o(r,{...a,cache:!1});j&&c&&setTimeout(e,1e3*u),N&&window.addEventListener("online",e),D&&(window.addEventListener("focus",e),document.addEventListener("visibilitychange",async()=>{"visible"===document.visibilityState&&await e()}))}let i="function"==typeof l?l:null;l&&!i||(l={});var[J,H,C]=w||[];if(w?.length&&void 0!==C&&"undefined"!=typeof window){let t="string"==typeof C?document.querySelector(C):C;t?window.scrollListener&&void 0!==t&&"function"==typeof t.removeEventListener&&t.removeEventListener("scroll",window.scrollListener):e("paginateDOM not found",{color:"red"}),window.scrollListener=async e=>{const t=e.target;localStorage.setItem("scroll",JSON.stringify([t.scrollTop,t.scrollLeft,C])),t.scrollHeight-t.scrollTop<=t.clientHeight+200&&await o(r,{...a,cache:!1,[J]:v?.currentPage+1})},t&&t.addEventListener("scroll",window.scrollListener)}let h=JSON.stringify(J?{...$,[J]:void 0}:$),v=o?.log?.find(e=>e.request==h&&e.path==r);if(J){let e=v?.currentPage+1||$?.[J]||1;v||(l[H]=[],e=1),v&&(v.currentPage=e),$={...$,[J]:e}}else{for(let e of Object.keys(l))l[e]=void 0;if(c&&(!u||v?.lastFetchTime>Date.now()-1e3*u)){for(let e of Object.keys(v.res))l[e]=v.res[e];i&&(l=i(l))}}if(i?i({isLoading:!0}):"object"==typeof l&&(l.isLoading=!0),i&&(l=i(l)),y>0&&v?.lastFetchTime&&v.lastFetchTime>Date.now()-1e3*y)throw new Error(`Fetch rate limit exceeded for ${r}. \n Wait ${y}s between requests.`);if(v?.controller)if(g)v.controller.abort();else if(m)return{isLoading:!0};void 0!==o.log&&o.log?.unshift({path:r,request:h,lastFetchTime:Date.now(),controller:new AbortController});let E={method:d,headers:{"Content-Type":"application/json",Accept:"application/json",...s},body:$.body,redirect:"follow",cache:c?"force-cache":"no-store",signal:g?o.log[0]?.controller?.signal:AbortSignal.timeout(1e3*f)},q="";["POST","PUT","PATCH"].includes(d)?E.body=$.body||JSON.stringify($):q=(Object.keys($).length?"?":"")+new URLSearchParams($).toString(),"function"==typeof L&&([r,l,$,E]=L(r,l,$,E));let A=null,F=/* @__PURE__ */new Date,R=o.mock?.[r],I=e=>new Promise(t=>setTimeout(t,1e3*e||0));if(!R||R.params&&R.method!=d||R.params&&h!=JSON.stringify(R.params)){if(A=await fetch(p+r+q,E).catch(e=>{throw new Error(e.message)}),!A.ok)throw new Error(`HTTP error: ${A.status} ${A.statusText}`);let e=A.headers.get("content-type");x?await x(A.body):A=await(e?e.includes("application/json")?A&&A.json():e.includes("application/pdf")||e.includes("application/octet-stream")?A.blob():A.text():A.json()).catch(e=>{throw new Error("Error parsing response: "+e)})}else await I(R.delay),A="function"==typeof R.response?R.response($):R.response;"function"==typeof S&&([r,l,$,E]=S(r,l,$,E)),i?i({isLoading:void 0}):"object"==typeof l&&delete l?.isLoading,delete v?.controller;const U=((Number(/* @__PURE__ */new Date)-Number(F))/1e3).toFixed(1);if(b&&T("Path:"+p+r+q+"\n"+JSON.stringify(a,null,2)+"\nTime: "+U+"s\nResponse: "+t(A)),"object"==typeof A){for(let e of Object.keys(A))l[e]=H==e&&l[e]?.length?[...l[e],...A[e]]:A[e];void 0!==l&&(l.data=A)}else i?i({data:A,...A}):"object"==typeof l&&(l.data=A);return void 0!==o.log&&o.log?.unshift({path:r,request:JSON.stringify({...$,paginateKey:void 0}),response:l,lastFetchTime:Date.now()}),i&&(l=i(l)),l}catch(U){let e="Error: "+U.message+"\nPath:"+p+r+"\n";return"function"==typeof E&&E(U.message,p+r,$),a.retryAttempts>0?await o(r,{...a,retryAttempts:--a.retryAttempts}):(!U.message.includes("signal")&&a.debug&&(T(e,{color:"red"}),b&&"undefined"!=typeof document&&i(e)),l.error=U.message,"function"==typeof l?(l.data=l({isLoading:void 0,error:U.message}),l=l.data):delete l?.isLoading,void 0!==o.log&&o.log?.unshift({path:r,request:JSON.stringify($),error:U.message}),l)}}o.instance=(e={})=>(t,n={})=>o(t,{...e,...n});const n=async(e,t)=>{let o;return async function(...n){clearTimeout(o),o=setTimeout(async()=>{clearTimeout(o),await e(...n)},t)}};function i(e){if("undefined"==typeof document)return;let t,o=document.getElementById("alert-overlay");o||(o=document.body.appendChild(document.createElement("div")),o.id="alert-overlay",o.setAttribute("style","position:fixed;inset:0;z-index:9999;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center"),o.innerHTML='<div id="alert-box" style="background:#fff;padding:1.5em 2em;border-radius:8px;box-shadow:0 2px 16px #0003;min-width:220px;max-height:80vh;position:relative;display:flex;flex-direction:column;">\n <button id="close-alert" style="position:absolute;top:12px;right:20px;font-size:1.5em;background:none;border:none;cursor:pointer;color:black;">&times;</button>\n <div id="alert-list" style="overflow:auto;flex:1;"></div>\n </div>',o.addEventListener("click",e=>e.target==o&&o.remove()),document.getElementById("close-alert").onclick=()=>o.remove()),t=o.querySelector("#alert-list"),t.innerHTML+=`<div style="border-bottom:1px solid #333; font-size:1.2em;margin:0.5em 0;">${e}</div>`}function r(){document.addEventListener("keydown",e=>{if("i"===e.key&&e.ctrlKey&&e.altKey){let e=" ";for(let n of o.log)e+=`<div style="margin-bottom:1em; border-bottom:1px solid #ccc; padding-bottom:1em;">\n <b>Path:</b> ${n.path}<br>\n <b>Request:</b> ${t(n.request,0,"html")}<br>\n <b>Response:</b> ${t(n.response,0,"html")}<br> \n <b>Time:</b> ${new Date(n.lastFetchTime).toLocaleString()}\n </div>`;i(e)}})}"undefined"!=typeof window?(window.log=e,window.grab=o,window.grab.log=[],window.grab.mock={},window.grab.defaults={},r(),document.addEventListener("DOMContentLoaded",()=>{let[e,t,o]=JSON.parse(localStorage.getItem("scroll"))||[];e&&(document.querySelector(o).scrollTop=e,document.querySelector(o).scrollLeft=t)})):"undefined"!=typeof globalThis&&(o.log=[],o.mock={},o.defaults={},globalThis.log=e,globalThis.grab=o.instance());export{o as default,o as grab,e as log,t as printJSONStructure,r as setupDevTools,i as showAlert};
2
2
  //# sourceMappingURL=grab-api.es.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"grab-api.es.js","sources":["../src/grab-api.ts"],"sourcesContent":["import {\n printJSONStructure,\n log,\n type LogOptions,\n} from \"./log-json\";\n\n/**\n * ### GRAB: Generate Request to API from Browser\n * ![GrabAPILogo](https://i.imgur.com/Rwl5P3p.png)\n *\n * 1. **GRAB is the FBEST Request Manager: Functionally Brilliant, Elegantly Simple Tool**: One Function, no dependencies,\n * minimalist syntax, [more features than alternatives](https://grab.js.org/docs/Comparisons)\n * 2. **Auto-JSON Convert**: Pass parameters and get response or error in JSON, handling other data types as is.\n * 3. **isLoading Status**: Sets `.isLoading=true` on the pre-initialized response object so you can show a \"Loading...\" in any framework\n * 4. **Debug Logging**: Adds global `log()` and prints colored JSON structure, response, timing for requests in test.\n * 5. **Mock Server Support**: Configure `window.grab.mock` for development and testing environments\n * 6. **Cancel Duplicates**: Prevent this request if one is ongoing to same path & params, or cancel the ongoing request.\n * 7. **Timeout & Retry**: Customizable request timeout, default 30s, and auto-retry on error\n * 8. **DevTools**: `Ctrl+I` overlays webpage with devtools showing all requests and responses, timing, and JSON structure.\n * 9. **Request History**: Stores all request and response data in global `grab.log` object\n * 10. **Pagination Infinite Scroll**: Built-in pagination for infinite scroll to auto-load and merge next result page, with scroll position recovery.\n * 11. **Base URL Based on Environment**: Configure `grab.defaults.baseURL` once at the top, overide with `SERVER_API_URL` in `.env`.\n * 12. **Frontend Cache**: Set cache headers and retrieve from frontend memory for repeat requests to static data.\n * 13. **Regrab On Error**: Regrab on timeout error, or on window refocus, or on network change, or on stale data.\n * 14. **Framework Agnostic**: Alternatives like TanStack work only in component initialization and depend on React & others.\n * 15. **Globals**: Adds to window in browser or global in Node.js so you only import once: `grab()`, `log()`, `grab.log`, `grab.mock`, `grab.defaults`\n * 16. **TypeScript Tooltips**: Developers can hover over option names and autocomplete TypeScript.\n * 17. **Request Stategies**: [🎯 Examples](https://grab.js.org/docs/Examples) show common stategies like debounce, repeat, proxy, unit tests, interceptors, file upload, etc\n * 18. **Rate Limiting**: Built-in rate limiting to prevent multi-click cascading responses, require to wait seconds between requests.\n * 19. **Repeat**: Repeat request this many times, or repeat every X seconds to poll for updates.\n * 20. **Loading Icons**: Import from `grab-url/icons` to get enhanced animated loading icons.\n *\n * @param {string} path The full URL path OR relative path on this server after `grab.defaults.baseURL`\n * @param {object} [options={}] Request params for GET or body for POST/PUT/PATCH and utility options\n * @param {string} [options.method] default=\"GET\" The HTTP method to use\n * @param {object} [options.response] Pre-initialized object which becomes response JSON, no need for `.data`.\n * isLoading and error may also be set on this object. May omit and use return if load status is not needed.\n * @param {boolean} [options.cancelOngoingIfNew] default=false Cancel previous requests to same path\n * @param {boolean} [options.cancelNewIfOngoing] default=false Cancel if a request to path is in progress\n * @param {boolean} [options.cache] default=false Whether to cache the request and from frontend cache\n * @param {boolean} [options.debug] default=false Whether to log the request and response\n * @param {number} [options.timeout] default=30 The timeout for the request in seconds\n * @param {number} [options.cacheForTime] default=60 Seconds to consider data stale and invalidate cache\n * @param {number} [options.rateLimit] default=0 If set, how many seconds to wait between requests\n * @param {string} [options.baseURL] default='/api/' base url prefix, override with SERVER_API_URL env\n * @param {boolean} [options.setDefaults] default=false Pass this with options to set\n * those options as defaults for all requests.\n * @param {number} [options.retryAttempts] default=0 Retry failed requests this many times\n * @param {array} [options.infiniteScroll] default=null [page key, response field to concatenate, element with results]\n * @param {number} [options.repeat] default=0 Repeat request this many times\n * @param {number} [options.repeatEvery] default=null Repeat request every seconds\n * @param {function} [options.logger] default=log Custom logger to override the built-in color JSON log()\n * @param {function} [options.onRequest] Set with defaults to modify each request data.\n * Takes and returns in order: path, response, params, fetchParams\n * @param {function} [options.onResponse] Set with defaults to modify each request data.\n * Takes and returns in order: path, response, params, fetchParams\n * @param {function} [options.onStream] Set with defaults to process the response as a stream (i.e., for instant unzip)\n * @param {function} [options.onError] Set with defaults to modify the error data. Takes: error, path, params\n * @param {number} [options.debounce] default=0 Seconds to debounce request, wait to execute so that other requests may override\n * @param {boolean} [options.regrabOnStale] default=false Refetch when cache is past cacheForTime\n * @param {boolean} [options.regrabOnFocus] default=false Refetch on window refocus\n * @param {boolean} [options.regrabOnNetwork] default=false Refetch on network change\n * @param {any} [...params] All other params become GET params, POST body, and other methods.\n * @returns {Promise<Object>} The response object with resulting data or .error if error.\n * @author [vtempest (2025)](https://github.com/vtempest/GRAB-URL)\n * @see [🎯 Examples](https://grab.js.org/docs/Examples) [📑 Docs](https://grab.js.org)\n */\nexport default async function grab<TResponse = any, TParams = any>(\n path: string,\n options: GrabOptions<TResponse, TParams> = {} as any\n): Promise<GrabResponse<TResponse>> {\n var {\n headers,\n response = {} as any, // Pre-initialized object to set the response in. isLoading and error are also set on this object.\n method = options.post // set post: true for POST, omit for GET\n ? \"POST\"\n : options.put\n ? \"PUT\"\n : options.patch\n ? \"PATCH\"\n : \"GET\",\n cache = false, // Enable/disable frontend caching\n cacheForTime = 60, // Seconds to consider data stale and invalidate cache\n timeout = 30, // Request timeout in seconds\n baseURL = (typeof process !== \"undefined\" && process.env.SERVER_API_URL) ||\n \"/api/\", // Use env var or default to /api/\n cancelOngoingIfNew = false, // Cancel previous request for same path\n cancelNewIfOngoing = false, // Don't make new request if one is ongoing\n rateLimit = 0, // Minimum seconds between requests\n debug = false, // Auto-enable debug on localhost\n // typeof window !== \"undefined\" && window?.location?.hostname?.includes(\"localhost\"),\n infiniteScroll = null, // page key, response field to concatenate, element with results\n setDefaults = false, // Set these options as defaults for future requests\n retryAttempts = 0, // Retry failed requests once\n logger = log, // Custom logger to override the built-in color JSON log()\n onRequest = null, // Hook to modify request data before request is made\n onResponse = null, // Hook to modify request data after request is made\n onError = null, // Hook to modify request data after request is made\n onStream = null, // Hook to process the response as a stream (i.e., for instant unarchiving)\n repeatEvery = null, // Repeat request every seconds\n repeat = 0, // Repeat request this many times\n debounce = 0, // Seconds to debounce request, wait to execute so that other requests may override\n regrabOnStale = false, // Refetch when cache is past cacheForTime\n regrabOnFocus = false, // Refetch on window refocus\n regrabOnNetwork = false, // Refetch on network change\n post = false,\n put = false,\n patch = false,\n body = null,\n ...params // All other params become request params/query\n } = {\n // Destructure options with defaults, merging with any globally set defaults\n ...(typeof window !== \"undefined\"\n ? window?.grab?.defaults\n : (globalThis)?.grab?.defaults || {}),\n ...options,\n };\n\n // Handle URL construction\n // Ensures proper joining of baseURL and path\n let s = (t) => path.startsWith(t);\n if (s(\"http:\") || s(\"https:\")) baseURL = \"\";\n else if (!s(\"/\") && !baseURL.endsWith(\"/\")) path = \"/\" + path;\n else if (s(\"/\") && baseURL.endsWith(\"/\")) path = path.slice(1);\n\n try {\n //handle debounce\n if (debounce > 0)\n return (await debouncer(async () => {\n await grab(path, { ...options, debounce: 0 });\n }, debounce * 1000)) as GrabResponse;\n\n // Handle repeat options:\n // - repeat: Makes the same request multiple times sequentially\n // - repeatEvery: Makes the request periodically on an interval\n if (repeat > 1) {\n for (let i = 0; i < repeat; i++) {\n await grab(path, { ...options, repeat: 0 });\n }\n return response;\n }\n if (repeatEvery) {\n setInterval(async () => {\n await grab(path, { ...options, repeat: 0, repeatEvery: null });\n }, repeatEvery * 1000);\n return response;\n }\n\n // Store the provided options as new defaults if setDefaults flag is set\n // This allows configuring default options that apply to all future requests\n if (options?.setDefaults) {\n if (typeof window !== \"undefined\")\n window.grab.defaults = { ...options, setDefaults: undefined };\n else if (typeof globalThis.grab !== \"undefined\")\n globalThis.grab.defaults = {\n ...options,\n setDefaults: undefined,\n };\n\n return;\n }\n\n // regrab on stale, on window refocus, on network\n if (typeof window !== \"undefined\") {\n const regrab = async () => await grab(path, { ...options, cache: false });\n if (regrabOnStale && cache) setTimeout(regrab, 1000 * cacheForTime);\n if (regrabOnNetwork) window.addEventListener(\"online\", regrab);\n if (regrabOnFocus) {\n window.addEventListener(\"focus\", regrab);\n document.addEventListener(\"visibilitychange\", async () => {\n if (document.visibilityState === \"visible\") await regrab();\n });\n }\n }\n\n // Handle response parameter which can be either an object to populate\n // or a function to call with results (e.g. React setState)\n let resFunction = typeof response === \"function\" ? response : null;\n if (!response || resFunction) response = {};\n\n var [paginateKey, paginateResult, paginateElement] = infiniteScroll || [];\n\n // Configure infinite scroll behavior if enabled\n // Attaches scroll listener to specified element that triggers next page load\n if (infiniteScroll?.length && typeof paginateElement !== \"undefined\"\n && typeof window !== \"undefined\") {\n let paginateDOM =\n typeof paginateElement === \"string\"\n ? document.querySelector(paginateElement)\n : paginateElement;\n\n if (!paginateDOM) log(\"paginateDOM not found\", { color: \"red\" });\n else if (window.scrollListener\n && typeof paginateDOM !== \"undefined\"\n && typeof paginateDOM.removeEventListener === \"function\")\n paginateDOM.removeEventListener(\"scroll\", window.scrollListener);\n\n // Your modified scroll listener with position saving\n window.scrollListener = async (event) => {\n const t = event.target as HTMLElement;\n\n // Save scroll position whenever user scrolls\n localStorage.setItem(\n \"scroll\",\n JSON.stringify([t.scrollTop, t.scrollLeft, paginateElement])\n );\n\n if (t.scrollHeight - t.scrollTop <= t.clientHeight + 200) {\n await grab(path, {\n ...options,\n cache: false,\n [paginateKey]: priorRequest?.currentPage + 1,\n });\n }\n };\n\n if (paginateDOM)\n paginateDOM.addEventListener(\"scroll\", window.scrollListener);\n }\n\n // Check request history for a previous request with same path/params\n // Used for caching and pagination. Ignores pagination params when comparing.\n let paramsAsText = JSON.stringify(\n paginateKey ? { ...params, [paginateKey]: undefined } : params\n );\n let priorRequest = grab?.log?.find(\n (e) => e.request == paramsAsText && e.path == path\n );\n\n // Handle response data management based on pagination settings\n if (!paginateKey) {\n // Clear any existing response data\n for (let key of Object.keys(response)) response[key] = undefined;\n\n // For non-paginated requests:\n // Return cached response if caching enabled and identical request exists\n // after returning cache, proceed with call to revalidate ensure data is up to date\n if (\n cache &&\n (!cacheForTime ||\n priorRequest?.lastFetchTime > Date.now() - 1000 * cacheForTime)\n ) {\n // set response to cache data\n for (let key of Object.keys(priorRequest.res))\n response[key] = priorRequest.res[key];\n if (resFunction) response = resFunction(response);\n\n // if (!cacheValidate) return response;\n }\n } else {\n // For paginated requests:\n // Track current page number and append results to existing data\n let pageNumber =\n priorRequest?.currentPage + 1 || params?.[paginateKey] || 1;\n\n // Clear response if this is a new request with new params\n if (!priorRequest) {\n response[paginateResult] = [];\n pageNumber = 1;\n }\n\n // Update page tracking\n if (priorRequest) priorRequest.currentPage = pageNumber;\n params = { ...params, [paginateKey]: pageNumber };\n }\n\n // Set loading state on response object\n if (resFunction) resFunction({ isLoading: true });\n else if (typeof response === \"object\") response.isLoading = true;\n\n if (resFunction) response = resFunction(response);\n\n // Enforce rate limiting between requests if configured\n if (\n rateLimit > 0 &&\n priorRequest?.lastFetchTime &&\n priorRequest.lastFetchTime > Date.now() - 1000 * rateLimit\n )\n throw new Error(`Fetch rate limit exceeded for ${path}. \n Wait ${rateLimit}s between requests.`);\n\n // Handle request cancellation based on configuration:\n // - cancelOngoingIfNew: Cancels any in-progress request for same path\n // - cancelNewIfOngoing: Prevents new request if one is already in progress\n if (priorRequest?.controller)\n if (cancelOngoingIfNew) priorRequest.controller.abort();\n else if (cancelNewIfOngoing) return { isLoading: true } as GrabResponse;\n\n // Track new request in history log\n if (typeof grab.log != \"undefined\")\n grab.log?.unshift({\n path,\n request: paramsAsText,\n lastFetchTime: Date.now(),\n controller: new AbortController(),\n });\n\n // Configure fetch request parameters including headers, cache settings,\n // and timeout/cancellation signals\n let fetchParams = {\n method,\n headers: {\n \"Content-Type\": \"application/json\",\n Accept: \"application/json\",\n ...headers,\n },\n body: params.body,\n redirect: \"follow\" as RequestRedirect,\n cache: cache ? \"force-cache\" : (\"no-store\" as RequestCache),\n signal: cancelOngoingIfNew\n ? grab.log[0]?.controller?.signal\n : AbortSignal.timeout(timeout * 1000),\n } as RequestInit;\n\n // Format request parameters based on HTTP method\n // POST/PUT/PATCH send data in request body\n // GET/DELETE append data as URL query parameters\n let paramsGETRequest = \"\";\n if ([\"POST\", \"PUT\", \"PATCH\"].includes(method))\n fetchParams.body = params.body || JSON.stringify(params);\n else\n paramsGETRequest =\n (Object.keys(params).length ? \"?\" : \"\") +\n new URLSearchParams(params).toString();\n\n // Execute pre-request hook if configured\n // Allows modifying request data before sending\n if (typeof onRequest === \"function\")\n [path, response, params, fetchParams] = onRequest(\n path,\n response,\n params,\n fetchParams\n );\n\n // Process request through mock handler if configured\n // Otherwise make actual API request\n let res = null,\n startTime = new Date(),\n mockHandler = grab.mock?.[path] as GrabMockHandler;\n\n let wait = (s) => new Promise((res) => setTimeout(res, s * 1000 || 0));\n\n if (\n mockHandler &&\n (!mockHandler.params || mockHandler.method == method) &&\n (!mockHandler.params ||\n paramsAsText == JSON.stringify(mockHandler.params))\n ) {\n await wait(mockHandler.delay);\n\n res =\n typeof mockHandler.response === \"function\"\n ? mockHandler.response(params)\n : mockHandler.response;\n } else {\n // Make actual API request and handle response based on content type\n res = await fetch(baseURL + path + paramsGETRequest, fetchParams).catch(\n (e) => {\n throw new Error(e.message);\n }\n );\n\n if (!res.ok)\n throw new Error(`HTTP error: ${res.status} ${res.statusText}`);\n\n // Convert browser ReadableStream to Node.js stream\n let type = res.headers.get(\"content-type\");\n\n if (onStream) await onStream(res.body);\n else\n res = await (type\n ? type.includes(\"application/json\")\n ? res && res.json()\n : type.includes(\"application/pdf\") ||\n type.includes(\"application/octet-stream\")\n ? res.blob()\n : res.text()\n : res.json()\n ).catch((e) => {\n throw new Error(\"Error parsing response: \" + e);\n });\n }\n\n // Execute post-request hook if configured\n // Allows modifying response data before processing\n if (typeof onResponse === \"function\")\n [path, response, params, fetchParams] = onResponse(\n path,\n response,\n params,\n fetchParams\n );\n\n // Clear request tracking states\n if (resFunction) resFunction({ isLoading: undefined });\n else if (typeof response === \"object\") delete response?.isLoading;\n\n delete priorRequest?.controller;\n\n // Log debug information if enabled\n // Includes request details, timing, and response structure\n const elapsedTime = (\n (Number(new Date()) - Number(startTime)) /\n 1000\n ).toFixed(1);\n if (debug) {\n logger(\n \"Path:\" +\n baseURL +\n path +\n paramsGETRequest +\n \"\\n\" +\n JSON.stringify(options, null, 2) +\n \"\\nTime: \" +\n elapsedTime +\n \"s\\nResponse: \" +\n printJSONStructure(res)\n );\n // console.log(res);\n }\n\n // if (typeof res === \"undefined\") return;\n\n // Update response object with results\n // For paginated requests, concatenates with existing results\n if (typeof res === \"object\") {\n for (let key of Object.keys(res))\n response[key] =\n paginateResult == key && response[key]?.length\n ? [...response[key], ...res[key]]\n : res[key];\n\n if (typeof response !== \"undefined\") response.data = res; // for axios compat\n } else if (resFunction) resFunction({ data: res, ...res });\n else if (typeof response === \"object\") response.data = res;\n\n // Store request/response in history log\n if (typeof grab.log != \"undefined\")\n grab.log?.unshift({\n path,\n request: JSON.stringify({ ...params, paginateKey: undefined }),\n response,\n lastFetchTime: Date.now(),\n });\n\n if (resFunction) response = resFunction(response);\n\n return response;\n } catch (error) {\n // Handle any errors that occurred during request processing\n let errorMessage =\n \"Error: \" + error.message + \"\\nPath:\" + baseURL + path + \"\\n\";\n // JSON.stringify(params);\n\n // if onError hook is passed\n if (typeof onError === \"function\")\n onError(error.message, baseURL + path, params);\n\n // Retry request if retries are configured and attempts remain\n if (options.retryAttempts > 0)\n return await grab(path, {\n ...options,\n retryAttempts: --options.retryAttempts,\n });\n\n // Update error state in response object\n // Do not show errors for duplicate aborted requests\n if (!error.message.includes(\"signal\") && options.debug) {\n logger(errorMessage, { color: \"red\" });\n if (debug && typeof document !== \"undefined\") showAlert(errorMessage);\n }\n response.error = error.message;\n if (typeof response === \"function\") {\n response.data = response({ isLoading: undefined, error: error.message });\n response = response.data;\n } else delete response?.isLoading;\n\n // Log error in request history\n if (typeof grab.log != \"undefined\")\n grab.log?.unshift({\n path,\n request: JSON.stringify(params),\n error: error.message,\n });\n\n // if (typeof options.response === \"function\")\n // response = options.response(response);\n return response;\n }\n}\n\n/**\n * Creates a new instance of grab with default options\n * to apply to all requests made by this instance\n * @param {Object} defaults - options for all requests by instance\n * @returns {Function} grab() function using those options\n */\ngrab.instance =\n (defaults = {}) =>\n (path, options = {}) =>\n grab(path, { ...defaults, ...options });\n\n// delays execution so that future calls may override and only executes last one\nconst debouncer = async (func, wait) => {\n let timeout;\n return async function executedFunction(...args) {\n const later = async () => {\n clearTimeout(timeout);\n await func(...args);\n };\n clearTimeout(timeout);\n timeout = setTimeout(later, wait);\n };\n};\n\n// Add globals to window in browser, or global in Node.js\nif (typeof window !== \"undefined\") {\n window.log = log;\n window.grab = grab;\n\n window.grab.log = [];\n window.grab.mock = {};\n window.grab.defaults = {};\n\n //Ctrl+I setup dev tools\n setupDevTools();\n\n // Restore scroll position when page loads or component mounts\n document.addEventListener(\"DOMContentLoaded\", () => {\n let [scrollTop, scrollLeft, paginateElement] =\n JSON.parse(localStorage.getItem(\"scroll\")) || [];\n if (!scrollTop) return;\n document.querySelector(paginateElement).scrollTop = scrollTop;\n document.querySelector(paginateElement).scrollLeft = scrollLeft;\n });\n} else if (typeof globalThis !== \"undefined\") {\n grab.log = [];\n grab.mock = {};\n grab.defaults = {};\n globalThis.log = log;\n globalThis.grab = grab.instance();\n}\n\n\n\n/**\n * Shows message in a modal overlay with scrollable message stack\n * and is easier to dismiss unlike alert() which blocks window.\n * Creates a semi-transparent overlay with a white box containing the message.\n * @param {string} msg - The message to display\n */\nexport function showAlert(msg) {\n if (typeof document === \"undefined\") return;\n let o = document.getElementById(\"alert-overlay\"),\n list;\n\n // Create overlay and alert box if they don't exist\n if (!o) {\n o = document.body.appendChild(document.createElement(\"div\"));\n o.id = \"alert-overlay\";\n o.setAttribute(\n \"style\",\n \"position:fixed;inset:0;z-index:9999;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center\"\n );\n o.innerHTML = `<div id=\"alert-box\" style=\"background:#fff;padding:1.5em 2em;border-radius:8px;box-shadow:0 2px 16px #0003;min-width:220px;max-height:80vh;position:relative;display:flex;flex-direction:column;\">\n <button id=\"close-alert\" style=\"position:absolute;top:12px;right:20px;font-size:1.5em;background:none;border:none;cursor:pointer;color:black;\">&times;</button>\n <div id=\"alert-list\" style=\"overflow:auto;flex:1;\"></div>\n </div>`;\n\n // Add click handlers to close overlay\n o.addEventListener(\"click\", (e) => e.target == o && o.remove());\n document.getElementById(\"close-alert\").onclick = () => o.remove();\n }\n\n list = o.querySelector(\"#alert-list\");\n\n // Add new message to list\n list.innerHTML += `<div style=\"border-bottom:1px solid #333; font-size:1.2em;margin:0.5em 0;\">${msg}</div>`;\n}\n\n/**\n * Sets up development tools for debugging API requests\n * Adds a keyboard shortcut (Ctrl+Alt+I) that shows a modal with request history\n * Each request entry shows:\n * - Request path\n * - Request details\n * - Response data\n * - Timestamp\n */\nexport function setupDevTools() {\n // Keyboard shortcut (Ctrl+Alt+I) to toggle debug view\n document.addEventListener(\"keydown\", (e) => {\n if (e.key === \"i\" && e.ctrlKey && e.altKey) {\n // Create HTML of the grab.log requests\n let html = \" \";\n for (let request of grab.log) {\n html += `<div style=\"margin-bottom:1em; border-bottom:1px solid #ccc; padding-bottom:1em;\">\n <b>Path:</b> ${request.path}<br>\n <b>Request:</b> ${printJSONStructure(request.request, 0, 'html')}<br>\n <b>Response:</b> ${printJSONStructure(request.response, 0, 'html')}<br> \n <b>Time:</b> ${new Date(request.lastFetchTime).toLocaleString()}\n </div>`;\n }\n showAlert(html);\n }\n });\n}\n\n\n\n/***************** TYPESCRIPT INTERFACES *****************/\n\n// Core response object that gets populated with API response data\nexport type GrabResponse<TResponse = any> = TResponse & {\n /** Indicates if request is currently in progress */\n isLoading?: boolean;\n /** Error message if request failed */\n error?: string;\n /** Binary or text response data (JSON is set to the root)*/\n data?: TResponse | any;\n /** The actual response data - type depends on API endpoint */\n [key: string]: unknown;\n};\n\nexport type GrabOptions<TResponse = any, TParams = any> = TParams & {\n /** include headers and authorization in the request */\n headers?: Record<string, string>;\n /** Pre-initialized object which becomes response JSON, no need for .data */\n response?: TResponse | ((params: TParams) => TResponse) | any;\n /** default=\"GET\" The HTTP method to use */\n method?: \"GET\" | \"POST\" | \"PUT\" | \"PATCH\" | \"DELETE\" | \"OPTIONS\" | \"HEAD\";\n /** default=false Whether to cache the request and from frontend cache */\n cache?: boolean;\n /** default=60 Seconds to consider data stale and invalidate cache */\n cacheForTime?: number;\n /** default=30 The timeout for the request in seconds */\n timeout?: number;\n /** default='/api/' base url prefix, override with SERVER_API_URL env */\n baseURL?: string;\n /** default=true Cancel previous requests to same path */\n cancelOngoingIfNew?: boolean;\n /** default=false Cancel if a request to path is in progress */\n cancelNewIfOngoing?: boolean;\n /** default=false If set, how many seconds to wait between requests */\n rateLimit?: number;\n /** default=false Whether to log the request and response */\n debug?: boolean;\n /** default=null [page key, response field to concatenate, element with results] */\n infiniteScroll?: [string, string, string | HTMLElement];\n /** default=false Pass this with options to set those options as defaults for all requests */\n setDefaults?: boolean;\n /** default=0 Retry failed requests this many times */\n retryAttempts?: number;\n /** default=log Custom logger to override the built-in color JSON log() */\n logger?: (...args: any[]) => void;\n /** Set with defaults to modify each request data. Takes and returns in order: path, response, params, fetchParams */\n onRequest?: (...args: any[]) => any;\n /** Set with defaults to modify each request data. Takes and returns in order: path, response, params, fetchParams */\n onResponse?: (...args: any[]) => any;\n /** Set with defaults to modify each request data. Takes and returns in order: error, path, params */\n onError?: (...args: any[]) => any;\n /** Set with defaults to process the response as a stream (i.e., for instant unzip) */\n onStream?: (...args: any[]) => any;\n /** default=0 Repeat request this many times */\n repeat?: number;\n /** default=null Repeat request every seconds */\n repeatEvery?: number;\n /** default=0 Seconds to debounce request, wait to execute so that other requests may override */\n debounce?: number;\n /** default=false Refetch when cache is past cacheForTime */\n regrabOnStale?: boolean;\n /** default=false Refetch on window refocus */\n regrabOnFocus?: boolean;\n /** default=false Refetch on network change */\n regrabOnNetwork?: boolean;\n /** shortcut for method: \"POST\" */\n post?: boolean;\n /** shortcut for method: \"PUT\" */\n put?: boolean;\n /** shortcut for method: \"PATCH\" */\n patch?: boolean;\n /** default=null The body of the POST/PUT/PATCH request (can be passed into main)*/\n body?: any;\n /** All other params become GET params, POST body, and other methods */\n [key: string]: TParams | any;\n};\n\n// Combined options and parameters interface\n\n// Mock server configuration for testing\nexport interface GrabMockHandler<TParams = any, TResponse = any> {\n /** Mock response data or function that returns response */\n response: TResponse | ((params: TParams) => TResponse);\n /** HTTP method this mock should respond to */\n method?: \"GET\" | \"POST\" | \"PUT\" | \"PATCH\" | \"DELETE\" | \"OPTIONS\" | \"HEAD\";\n /** Request parameters this mock should match */\n params?: TParams;\n /** Delay in seconds before returning mock response */\n delay?: number;\n}\n\n// Request log entry for debugging and history\nexport interface GrabLogEntry {\n /** API path that was requested */\n path: string;\n /** Stringified request parameters */\n request: string;\n /** Response data (only present for successful requests) */\n response?: any;\n /** Error message (only present for failed requests) */\n error?: string;\n /** Timestamp when request was made */\n lastFetchTime: number;\n /** Abort controller for request cancellation */\n controller?: AbortController;\n /** Current page number for paginated requests */\n currentPage?: number;\n}\n\n// Global grab configuration and state\nexport interface GrabGlobal {\n /** Default options applied to all requests */\n defaults?: Partial<GrabOptions>;\n /** Request history and debugging info */\n log?: GrabLogEntry[];\n /** Mock server handlers for testing */\n mock?: Record<string, GrabMockHandler>;\n /** Create a separate instance of grab with separate default options */\n instance?: (defaultOptions?: Partial<GrabOptions>) => GrabFunction;\n}\n\n// Main grab function signature with overloads for different use cases\nexport interface GrabFunction {\n /**\n * ### GRAB: Generate Request to API from Browser\n * ![grabAPILogo](https://i.imgur.com/Rwl5P3p.png)\n * Make API request with path\n * @returns {Promise<Object>} The response object with resulting data or .error if error.\n * @author [vtempest (2025)](https://github.com/vtempest/GRAB-URL)\n * @see [🎯 Examples](https://grab.js.org/docs/Examples) [📑 Docs](https://grab.js.org/lib)\n */\n <TResponse = any, TParams = Record<string, any>>(path: string, options?: GrabOptions<TResponse, TParams>): Promise<\n GrabResponse<TResponse>\n >;\n\n /**\n * ### GRAB: Generate Request to API from Browser\n * ![grabAPILogo](https://i.imgur.com/Rwl5P3p.png)\n * Make API request with path and options/parameters\n * @returns {Promise<Object>} The response object with resulting data or .error if error.\n * @author [vtempest (2025)](https://github.com/vtempest/GRAB-URL)\n * @see [🎯 Examples](https://grab.js.org/docs/Examples) [📑 Docs](https://grab.js.org/lib)\n */\n <TResponse = any, TParams = Record<string, any>>(\n path: string,\n config: GrabOptions<TResponse, TParams>\n ): Promise<GrabResponse<TResponse>>;\n\n /** Default options applied to all requests */\n defaults?: Partial<GrabOptions>;\n\n /** Request history and debugging info for all requests */\n log?: GrabLogEntry[];\n\n /** Mock server handlers for testing */\n mock?: Record<string, GrabMockHandler>;\n\n /** Create a separate instance of grab with separate default options */\n instance?: (defaultOptions?: Partial<GrabOptions>) => GrabFunction;\n}\n\n// Log function for debugging\nexport interface LogFunction {\n /**\n * Log messages with custom styling\n * @param message - Message to log (string or object)\n */\n (message: string | object, options?: LogOptions): void;\n}\n\n// Utility function to describe JSON structure\nexport interface printJSONStructureFunction {\n /**\n * Generate TypeDoc-like description of JSON object structure\n * @param obj - The JSON object to describe\n * @returns String representation of object structure\n */\n (obj: any): string;\n}\n\ndeclare global {\n // Browser globals\n interface Window {\n grab: GrabFunction;\n log: LogFunction;\n scrollListener: (event: Event) => void; // replace 'any' with the actual type if you know it (e.g., a function type)\n }\n\n // Node.js globals\n namespace NodeJS {\n interface Global {\n grab: GrabFunction;\n log: LogFunction;\n }\n }\n\n // Global variables available after script inclusion\n var log: LogFunction;\n var grab: GrabFunction;\n}\n\nexport { grab, log, printJSONStructure };\n"],"names":["async","grab","path","options","headers","response","method","post","put","patch","cache","cacheForTime","timeout","baseURL","process","env","SERVER_API_URL","cancelOngoingIfNew","cancelNewIfOngoing","rateLimit","debug","infiniteScroll","setDefaults","retryAttempts","logger","log","onRequest","onResponse","onError","onStream","repeatEvery","repeat","debounce","regrabOnStale","regrabOnFocus","regrabOnNetwork","body","params","window","defaults","globalThis","s","t","startsWith","endsWith","slice","debouncer","i","setInterval","regrab","setTimeout","addEventListener","document","visibilityState","resFunction","paginateKey","paginateResult","paginateElement","length","paginateDOM","querySelector","scrollListener","removeEventListener","color","event","target","localStorage","setItem","JSON","stringify","scrollTop","scrollLeft","scrollHeight","clientHeight","priorRequest","currentPage","paramsAsText","find","e","request","pageNumber","key","Object","keys","lastFetchTime","Date","now","res","isLoading","Error","controller","abort","unshift","AbortController","fetchParams","Accept","redirect","signal","AbortSignal","paramsGETRequest","includes","URLSearchParams","toString","startTime","mockHandler","mock","wait","Promise","fetch","catch","message","ok","status","statusText","type","get","json","blob","text","delay","elapsedTime","Number","toFixed","printJSONStructure","data","error","errorMessage","instance","func","args","clearTimeout","showAlert","msg","list","o","getElementById","appendChild","createElement","id","setAttribute","innerHTML","remove","onclick","setupDevTools","ctrlKey","altKey","html","toLocaleString","parse","getItem"],"mappings":"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"}
1
+ {"version":3,"file":"grab-api.es.js","sources":["../src/grab-api.ts"],"sourcesContent":["import {\n printJSONStructure,\n log,\n type LogOptions,\n} from \"./log-json\";\n\n/**\n * ### GRAB: Generate Request to API from Browser\n * ![GrabAPILogo](https://i.imgur.com/Rwl5P3p.png)\n *\n * 1. **GRAB is the FBEST Request Manager: Functionally Brilliant, Elegantly Simple Tool**: One Function, no dependencies,\n * minimalist syntax, [more features than alternatives](https://grab.js.org/docs/Comparisons)\n * 2. **Auto-JSON Convert**: Pass parameters and get response or error in JSON, handling other data types as is.\n * 3. **isLoading Status**: Sets `.isLoading=true` on the pre-initialized response object so you can show a \"Loading...\" in any framework\n * 4. **Debug Logging**: Adds global `log()` and prints colored JSON structure, response, timing for requests in test.\n * 5. **Mock Server Support**: Configure `window.grab.mock` for development and testing environments\n * 6. **Cancel Duplicates**: Prevent this request if one is ongoing to same path & params, or cancel the ongoing request.\n * 7. **Timeout & Retry**: Customizable request timeout, default 30s, and auto-retry on error\n * 8. **DevTools**: `Ctrl+I` overlays webpage with devtools showing all requests and responses, timing, and JSON structure.\n * 9. **Request History**: Stores all request and response data in global `grab.log` object\n * 10. **Pagination Infinite Scroll**: Built-in pagination for infinite scroll to auto-load and merge next result page, with scroll position recovery.\n * 11. **Base URL Based on Environment**: Configure `grab.defaults.baseURL` once at the top, overide with `SERVER_API_URL` in `.env`.\n * 12. **Frontend Cache**: Set cache headers and retrieve from frontend memory for repeat requests to static data.\n * 13. **Regrab On Error**: Regrab on timeout error, or on window refocus, or on network change, or on stale data.\n * 14. **Framework Agnostic**: Alternatives like TanStack work only in component initialization and depend on React & others.\n * 15. **Globals**: Adds to window in browser or global in Node.js so you only import once: `grab()`, `log()`, `grab.log`, `grab.mock`, `grab.defaults`\n * 16. **TypeScript Tooltips**: Developers can hover over option names and autocomplete TypeScript.\n * 17. **Request Stategies**: [🎯 Examples](https://grab.js.org/docs/Examples) show common stategies like debounce, repeat, proxy, unit tests, interceptors, file upload, etc\n * 18. **Rate Limiting**: Built-in rate limiting to prevent multi-click cascading responses, require to wait seconds between requests.\n * 19. **Repeat**: Repeat request this many times, or repeat every X seconds to poll for updates.\n * 20. **Loading Icons**: Import from `grab-url/icons` to get enhanced animated loading icons.\n *\n * @param {string} path The full URL path OR relative path on this server after `grab.defaults.baseURL`\n * @param {object} [options={}] Request params for GET or body for POST/PUT/PATCH and utility options\n * @param {string} [options.method] default=\"GET\" The HTTP method to use\n * @param {object} [options.response] Pre-initialized object which becomes response JSON, no need for `.data`.\n * isLoading and error may also be set on this object. May omit and use return if load status is not needed.\n * @param {boolean} [options.cancelOngoingIfNew] default=false Cancel previous requests to same path\n * @param {boolean} [options.cancelNewIfOngoing] default=false Cancel if a request to path is in progress\n * @param {boolean} [options.cache] default=false Whether to cache the request and from frontend cache\n * @param {boolean} [options.debug] default=false Whether to log the request and response\n * @param {number} [options.timeout] default=30 The timeout for the request in seconds\n * @param {number} [options.cacheForTime] default=60 Seconds to consider data stale and invalidate cache\n * @param {number} [options.rateLimit] default=0 If set, how many seconds to wait between requests\n * @param {string} [options.baseURL] default='/api/' base url prefix, override with SERVER_API_URL env\n * @param {boolean} [options.setDefaults] default=false Pass this with options to set\n * those options as defaults for all requests.\n * @param {number} [options.retryAttempts] default=0 Retry failed requests this many times\n * @param {array} [options.infiniteScroll] default=null [page key, response field to concatenate, element with results]\n * @param {number} [options.repeat] default=0 Repeat request this many times\n * @param {number} [options.repeatEvery] default=null Repeat request every seconds\n * @param {function} [options.logger] default=log Custom logger to override the built-in color JSON log()\n * @param {function} [options.onRequest] Set with defaults to modify each request data.\n * Takes and returns in order: path, response, params, fetchParams\n * @param {function} [options.onResponse] Set with defaults to modify each request data.\n * Takes and returns in order: path, response, params, fetchParams\n * @param {function} [options.onStream] Set with defaults to process the response as a stream (i.e., for instant unzip)\n * @param {function} [options.onError] Set with defaults to modify the error data. Takes: error, path, params\n * @param {number} [options.debounce] default=0 Seconds to debounce request, wait to execute so that other requests may override\n * @param {boolean} [options.regrabOnStale] default=false Refetch when cache is past cacheForTime\n * @param {boolean} [options.regrabOnFocus] default=false Refetch on window refocus\n * @param {boolean} [options.regrabOnNetwork] default=false Refetch on network change\n * @param {any} [...params] All other params become GET params, POST body, and other methods.\n * @returns {Promise<Object>} The response object with resulting data or .error if error.\n * @author [vtempest (2025)](https://github.com/vtempest/GRAB-URL)\n * @see [🎯 Examples](https://grab.js.org/docs/Examples) [📑 Docs](https://grab.js.org)\n */\nexport default async function grab<TResponse = any, TParams = any>(\n path: string,\n options?: GrabOptions<TResponse, TParams>\n): Promise<GrabResponse<TResponse>> {\n var {\n headers,\n response = {} as any, // Pre-initialized object to set the response in. isLoading and error are also set on this object.\n method = options?.post // set post: true for POST, omit for GET\n ? \"POST\"\n : options?.put\n ? \"PUT\"\n : options?.patch\n ? \"PATCH\"\n : \"GET\",\n cache = false, // Enable/disable frontend caching\n cacheForTime = 60, // Seconds to consider data stale and invalidate cache\n timeout = 30, // Request timeout in seconds\n baseURL = (typeof process !== \"undefined\" && process.env.SERVER_API_URL) ||\n \"/api/\", // Use env var or default to /api/\n cancelOngoingIfNew = false, // Cancel previous request for same path\n cancelNewIfOngoing = false, // Don't make new request if one is ongoing\n rateLimit = 0, // Minimum seconds between requests\n debug = false, // Auto-enable debug on localhost\n // typeof window !== \"undefined\" && window?.location?.hostname?.includes(\"localhost\"),\n infiniteScroll = null, // page key, response field to concatenate, element with results\n setDefaults = false, // Set these options as defaults for future requests\n retryAttempts = 0, // Retry failed requests once\n logger = log, // Custom logger to override the built-in color JSON log()\n onRequest = null, // Hook to modify request data before request is made\n onResponse = null, // Hook to modify request data after request is made\n onError = null, // Hook to modify request data after request is made\n onStream = null, // Hook to process the response as a stream (i.e., for instant unarchiving)\n repeatEvery = null, // Repeat request every seconds\n repeat = 0, // Repeat request this many times\n debounce = 0, // Seconds to debounce request, wait to execute so that other requests may override\n regrabOnStale = false, // Refetch when cache is past cacheForTime\n regrabOnFocus = false, // Refetch on window refocus\n regrabOnNetwork = false, // Refetch on network change\n post = false,\n put = false,\n patch = false,\n body = null,\n ...params // All other params become request params/query\n } = {\n // Destructure options with defaults, merging with any globally set defaults\n ...(typeof window !== \"undefined\"\n ? window?.grab?.defaults\n : (globalThis)?.grab?.defaults || {}),\n ...options,\n };\n\n // Handle URL construction\n // Ensures proper joining of baseURL and path\n let s = (t) => path.startsWith(t);\n if (s(\"http:\") || s(\"https:\")) baseURL = \"\";\n else if (!s(\"/\") && !baseURL.endsWith(\"/\")) path = \"/\" + path;\n else if (s(\"/\") && baseURL.endsWith(\"/\")) path = path.slice(1);\n\n try {\n //handle debounce\n if (debounce > 0)\n return (await debouncer(async () => {\n await grab(path, { ...options, debounce: 0 });\n }, debounce * 1000)) as GrabResponse;\n\n // Handle repeat options:\n // - repeat: Makes the same request multiple times sequentially\n // - repeatEvery: Makes the request periodically on an interval\n if (repeat > 1) {\n for (let i = 0; i < repeat; i++) {\n await grab(path, { ...options, repeat: 0 });\n }\n return response;\n }\n if (repeatEvery) {\n setInterval(async () => {\n await grab(path, { ...options, repeat: 0, repeatEvery: null });\n }, repeatEvery * 1000);\n return response;\n }\n\n // Store the provided options as new defaults if setDefaults flag is set\n // This allows configuring default options that apply to all future requests\n if (options?.setDefaults) {\n if (typeof window !== \"undefined\")\n window.grab.defaults = { ...options, setDefaults: undefined };\n else if (typeof globalThis.grab !== \"undefined\")\n globalThis.grab.defaults = {\n ...options,\n setDefaults: undefined,\n };\n\n return;\n }\n\n // regrab on stale, on window refocus, on network\n if (typeof window !== \"undefined\") {\n const regrab = async () => await grab(path, { ...options, cache: false });\n if (regrabOnStale && cache) setTimeout(regrab, 1000 * cacheForTime);\n if (regrabOnNetwork) window.addEventListener(\"online\", regrab);\n if (regrabOnFocus) {\n window.addEventListener(\"focus\", regrab);\n document.addEventListener(\"visibilitychange\", async () => {\n if (document.visibilityState === \"visible\") await regrab();\n });\n }\n }\n\n // Handle response parameter which can be either an object to populate\n // or a function to call with results (e.g. React setState)\n let resFunction = typeof response === \"function\" ? response : null;\n if (!response || resFunction) response = {};\n\n var [paginateKey, paginateResult, paginateElement] = infiniteScroll || [];\n\n // Configure infinite scroll behavior if enabled\n // Attaches scroll listener to specified element that triggers next page load\n if (infiniteScroll?.length && typeof paginateElement !== \"undefined\"\n && typeof window !== \"undefined\") {\n let paginateDOM =\n typeof paginateElement === \"string\"\n ? document.querySelector(paginateElement)\n : paginateElement;\n\n if (!paginateDOM) log(\"paginateDOM not found\", { color: \"red\" });\n else if (window.scrollListener\n && typeof paginateDOM !== \"undefined\"\n && typeof paginateDOM.removeEventListener === \"function\")\n paginateDOM.removeEventListener(\"scroll\", window.scrollListener);\n\n // Your modified scroll listener with position saving\n window.scrollListener = async (event) => {\n const t = event.target as HTMLElement;\n\n // Save scroll position whenever user scrolls\n localStorage.setItem(\n \"scroll\",\n JSON.stringify([t.scrollTop, t.scrollLeft, paginateElement])\n );\n\n if (t.scrollHeight - t.scrollTop <= t.clientHeight + 200) {\n await grab(path, {\n ...options,\n cache: false,\n [paginateKey]: priorRequest?.currentPage + 1,\n });\n }\n };\n\n if (paginateDOM)\n paginateDOM.addEventListener(\"scroll\", window.scrollListener);\n }\n\n // Check request history for a previous request with same path/params\n // Used for caching and pagination. Ignores pagination params when comparing.\n let paramsAsText = JSON.stringify(\n paginateKey ? { ...params, [paginateKey]: undefined } : params\n );\n let priorRequest = grab?.log?.find(\n (e) => e.request == paramsAsText && e.path == path\n );\n\n // Handle response data management based on pagination settings\n if (!paginateKey) {\n // Clear any existing response data\n for (let key of Object.keys(response)) response[key] = undefined;\n\n // For non-paginated requests:\n // Return cached response if caching enabled and identical request exists\n // after returning cache, proceed with call to revalidate ensure data is up to date\n if (\n cache &&\n (!cacheForTime ||\n priorRequest?.lastFetchTime > Date.now() - 1000 * cacheForTime)\n ) {\n // set response to cache data\n for (let key of Object.keys(priorRequest.res))\n response[key] = priorRequest.res[key];\n if (resFunction) response = resFunction(response);\n\n // if (!cacheValidate) return response;\n }\n } else {\n // For paginated requests:\n // Track current page number and append results to existing data\n let pageNumber =\n priorRequest?.currentPage + 1 || params?.[paginateKey] || 1;\n\n // Clear response if this is a new request with new params\n if (!priorRequest) {\n response[paginateResult] = [];\n pageNumber = 1;\n }\n\n // Update page tracking\n if (priorRequest) priorRequest.currentPage = pageNumber;\n params = { ...params, [paginateKey]: pageNumber };\n }\n\n // Set loading state on response object\n if (resFunction) resFunction({ isLoading: true });\n else if (typeof response === \"object\") response.isLoading = true;\n\n if (resFunction) response = resFunction(response);\n\n // Enforce rate limiting between requests if configured\n if (\n rateLimit > 0 &&\n priorRequest?.lastFetchTime &&\n priorRequest.lastFetchTime > Date.now() - 1000 * rateLimit\n )\n throw new Error(`Fetch rate limit exceeded for ${path}. \n Wait ${rateLimit}s between requests.`);\n\n // Handle request cancellation based on configuration:\n // - cancelOngoingIfNew: Cancels any in-progress request for same path\n // - cancelNewIfOngoing: Prevents new request if one is already in progress\n if (priorRequest?.controller)\n if (cancelOngoingIfNew) priorRequest.controller.abort();\n else if (cancelNewIfOngoing) return { isLoading: true } as GrabResponse;\n\n // Track new request in history log\n if (typeof grab.log != \"undefined\")\n grab.log?.unshift({\n path,\n request: paramsAsText,\n lastFetchTime: Date.now(),\n controller: new AbortController(),\n });\n\n // Configure fetch request parameters including headers, cache settings,\n // and timeout/cancellation signals\n let fetchParams = {\n method,\n headers: {\n \"Content-Type\": \"application/json\",\n Accept: \"application/json\",\n ...headers,\n },\n body: params.body,\n redirect: \"follow\" as RequestRedirect,\n cache: cache ? \"force-cache\" : (\"no-store\" as RequestCache),\n signal: cancelOngoingIfNew\n ? grab.log[0]?.controller?.signal\n : AbortSignal.timeout(timeout * 1000),\n } as RequestInit;\n\n // Format request parameters based on HTTP method\n // POST/PUT/PATCH send data in request body\n // GET/DELETE append data as URL query parameters\n let paramsGETRequest = \"\";\n if ([\"POST\", \"PUT\", \"PATCH\"].includes(method))\n fetchParams.body = params.body || JSON.stringify(params);\n else\n paramsGETRequest =\n (Object.keys(params).length ? \"?\" : \"\") +\n new URLSearchParams(params).toString();\n\n // Execute pre-request hook if configured\n // Allows modifying request data before sending\n if (typeof onRequest === \"function\")\n [path, response, params, fetchParams] = onRequest(\n path,\n response,\n params,\n fetchParams\n );\n\n // Process request through mock handler if configured\n // Otherwise make actual API request\n let res = null,\n startTime = new Date(),\n mockHandler = grab.mock?.[path] as GrabMockHandler;\n\n let wait = (s) => new Promise((res) => setTimeout(res, s * 1000 || 0));\n\n if (\n mockHandler &&\n (!mockHandler.params || mockHandler.method == method) &&\n (!mockHandler.params ||\n paramsAsText == JSON.stringify(mockHandler.params))\n ) {\n await wait(mockHandler.delay);\n\n res =\n typeof mockHandler.response === \"function\"\n ? mockHandler.response(params)\n : mockHandler.response;\n } else {\n // Make actual API request and handle response based on content type\n res = await fetch(baseURL + path + paramsGETRequest, fetchParams).catch(\n (e) => {\n throw new Error(e.message);\n }\n );\n\n if (!res.ok)\n throw new Error(`HTTP error: ${res.status} ${res.statusText}`);\n\n // Convert browser ReadableStream to Node.js stream\n let type = res.headers.get(\"content-type\");\n\n if (onStream) await onStream(res.body);\n else\n res = await (type\n ? type.includes(\"application/json\")\n ? res && res.json()\n : type.includes(\"application/pdf\") ||\n type.includes(\"application/octet-stream\")\n ? res.blob()\n : res.text()\n : res.json()\n ).catch((e) => {\n throw new Error(\"Error parsing response: \" + e);\n });\n }\n\n // Execute post-request hook if configured\n // Allows modifying response data before processing\n if (typeof onResponse === \"function\")\n [path, response, params, fetchParams] = onResponse(\n path,\n response,\n params,\n fetchParams\n );\n\n // Clear request tracking states\n if (resFunction) resFunction({ isLoading: undefined });\n else if (typeof response === \"object\") delete response?.isLoading;\n\n delete priorRequest?.controller;\n\n // Log debug information if enabled\n // Includes request details, timing, and response structure\n const elapsedTime = (\n (Number(new Date()) - Number(startTime)) /\n 1000\n ).toFixed(1);\n if (debug) {\n logger(\n \"Path:\" +\n baseURL +\n path +\n paramsGETRequest +\n \"\\n\" +\n JSON.stringify(options, null, 2) +\n \"\\nTime: \" +\n elapsedTime +\n \"s\\nResponse: \" +\n printJSONStructure(res)\n );\n // console.log(res);\n }\n\n // if (typeof res === \"undefined\") return;\n\n // Update response object with results\n // For paginated requests, concatenates with existing results\n if (typeof res === \"object\") {\n for (let key of Object.keys(res))\n response[key] =\n paginateResult == key && response[key]?.length\n ? [...response[key], ...res[key]]\n : res[key];\n\n if (typeof response !== \"undefined\") response.data = res; // for axios compat\n } else if (resFunction) resFunction({ data: res, ...res });\n else if (typeof response === \"object\") response.data = res;\n\n // Store request/response in history log\n if (typeof grab.log != \"undefined\")\n grab.log?.unshift({\n path,\n request: JSON.stringify({ ...params, paginateKey: undefined }),\n response,\n lastFetchTime: Date.now(),\n });\n\n if (resFunction) response = resFunction(response);\n\n return response;\n } catch (error) {\n // Handle any errors that occurred during request processing\n let errorMessage =\n \"Error: \" + error.message + \"\\nPath:\" + baseURL + path + \"\\n\";\n // JSON.stringify(params);\n\n // if onError hook is passed\n if (typeof onError === \"function\")\n onError(error.message, baseURL + path, params);\n\n // Retry request if retries are configured and attempts remain\n if (options.retryAttempts > 0)\n return await grab(path, {\n ...options,\n retryAttempts: --options.retryAttempts,\n });\n\n // Update error state in response object\n // Do not show errors for duplicate aborted requests\n if (!error.message.includes(\"signal\") && options.debug) {\n logger(errorMessage, { color: \"red\" });\n if (debug && typeof document !== \"undefined\") showAlert(errorMessage);\n }\n response.error = error.message;\n if (typeof response === \"function\") {\n response.data = response({ isLoading: undefined, error: error.message });\n response = response.data;\n } else delete response?.isLoading;\n\n // Log error in request history\n if (typeof grab.log != \"undefined\")\n grab.log?.unshift({\n path,\n request: JSON.stringify(params),\n error: error.message,\n });\n\n // if (typeof options.response === \"function\")\n // response = options.response(response);\n return response;\n }\n}\n\n/**\n * Creates a new instance of grab with default options\n * to apply to all requests made by this instance\n * @param {Object} defaults - options for all requests by instance\n * @returns {Function} grab() function using those options\n */\ngrab.instance =\n (defaults = {}) =>\n (path, options = {}) =>\n grab(path, { ...defaults, ...options });\n\n// delays execution so that future calls may override and only executes last one\nconst debouncer = async (func, wait) => {\n let timeout;\n return async function executedFunction(...args) {\n const later = async () => {\n clearTimeout(timeout);\n await func(...args);\n };\n clearTimeout(timeout);\n timeout = setTimeout(later, wait);\n };\n};\n\n// Add globals to window in browser, or global in Node.js\nif (typeof window !== \"undefined\") {\n window.log = log;\n window.grab = grab;\n\n window.grab.log = [];\n window.grab.mock = {};\n window.grab.defaults = {};\n\n //Ctrl+I setup dev tools\n setupDevTools();\n\n // Restore scroll position when page loads or component mounts\n document.addEventListener(\"DOMContentLoaded\", () => {\n let [scrollTop, scrollLeft, paginateElement] =\n JSON.parse(localStorage.getItem(\"scroll\")) || [];\n if (!scrollTop) return;\n document.querySelector(paginateElement).scrollTop = scrollTop;\n document.querySelector(paginateElement).scrollLeft = scrollLeft;\n });\n} else if (typeof globalThis !== \"undefined\") {\n grab.log = [];\n grab.mock = {};\n grab.defaults = {};\n globalThis.log = log;\n globalThis.grab = grab.instance();\n}\n\n\n\n/**\n * Shows message in a modal overlay with scrollable message stack\n * and is easier to dismiss unlike alert() which blocks window.\n * Creates a semi-transparent overlay with a white box containing the message.\n * @param {string} msg - The message to display\n */\nexport function showAlert(msg) {\n if (typeof document === \"undefined\") return;\n let o = document.getElementById(\"alert-overlay\"),\n list;\n\n // Create overlay and alert box if they don't exist\n if (!o) {\n o = document.body.appendChild(document.createElement(\"div\"));\n o.id = \"alert-overlay\";\n o.setAttribute(\n \"style\",\n \"position:fixed;inset:0;z-index:9999;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center\"\n );\n o.innerHTML = `<div id=\"alert-box\" style=\"background:#fff;padding:1.5em 2em;border-radius:8px;box-shadow:0 2px 16px #0003;min-width:220px;max-height:80vh;position:relative;display:flex;flex-direction:column;\">\n <button id=\"close-alert\" style=\"position:absolute;top:12px;right:20px;font-size:1.5em;background:none;border:none;cursor:pointer;color:black;\">&times;</button>\n <div id=\"alert-list\" style=\"overflow:auto;flex:1;\"></div>\n </div>`;\n\n // Add click handlers to close overlay\n o.addEventListener(\"click\", (e) => e.target == o && o.remove());\n document.getElementById(\"close-alert\").onclick = () => o.remove();\n }\n\n list = o.querySelector(\"#alert-list\");\n\n // Add new message to list\n list.innerHTML += `<div style=\"border-bottom:1px solid #333; font-size:1.2em;margin:0.5em 0;\">${msg}</div>`;\n}\n\n/**\n * Sets up development tools for debugging API requests\n * Adds a keyboard shortcut (Ctrl+Alt+I) that shows a modal with request history\n * Each request entry shows:\n * - Request path\n * - Request details\n * - Response data\n * - Timestamp\n */\nexport function setupDevTools() {\n // Keyboard shortcut (Ctrl+Alt+I) to toggle debug view\n document.addEventListener(\"keydown\", (e) => {\n if (e.key === \"i\" && e.ctrlKey && e.altKey) {\n // Create HTML of the grab.log requests\n let html = \" \";\n for (let request of grab.log) {\n html += `<div style=\"margin-bottom:1em; border-bottom:1px solid #ccc; padding-bottom:1em;\">\n <b>Path:</b> ${request.path}<br>\n <b>Request:</b> ${printJSONStructure(request.request, 0, 'html')}<br>\n <b>Response:</b> ${printJSONStructure(request.response, 0, 'html')}<br> \n <b>Time:</b> ${new Date(request.lastFetchTime).toLocaleString()}\n </div>`;\n }\n showAlert(html);\n }\n });\n}\n\n\n\n/***************** TYPESCRIPT INTERFACES *****************/\n\n// Core response object that gets populated with API response data\nexport type GrabResponse<TResponse = any> = TResponse & {\n /** Indicates if request is currently in progress */\n isLoading?: boolean;\n /** Error message if request failed */\n error?: string;\n /** Binary or text response data (JSON is set to the root)*/\n data?: TResponse | any;\n /** The actual response data - type depends on API endpoint */\n [key: string]: unknown;\n};\n\nexport type GrabOptions<TResponse = any, TParams = any> = TParams & {\n /** include headers and authorization in the request */\n headers?: Record<string, string>;\n /** Pre-initialized object which becomes response JSON, no need for .data */\n response?: TResponse | ((params: TParams) => TResponse) | any;\n /** default=\"GET\" The HTTP method to use */\n method?: \"GET\" | \"POST\" | \"PUT\" | \"PATCH\" | \"DELETE\" | \"OPTIONS\" | \"HEAD\";\n /** default=false Whether to cache the request and from frontend cache */\n cache?: boolean;\n /** default=60 Seconds to consider data stale and invalidate cache */\n cacheForTime?: number;\n /** default=30 The timeout for the request in seconds */\n timeout?: number;\n /** default='/api/' base url prefix, override with SERVER_API_URL env */\n baseURL?: string;\n /** default=true Cancel previous requests to same path */\n cancelOngoingIfNew?: boolean;\n /** default=false Cancel if a request to path is in progress */\n cancelNewIfOngoing?: boolean;\n /** default=false If set, how many seconds to wait between requests */\n rateLimit?: number;\n /** default=false Whether to log the request and response */\n debug?: boolean;\n /** default=null [page key, response field to concatenate, element with results] */\n infiniteScroll?: [string, string, string | HTMLElement];\n /** default=false Pass this with options to set those options as defaults for all requests */\n setDefaults?: boolean;\n /** default=0 Retry failed requests this many times */\n retryAttempts?: number;\n /** default=log Custom logger to override the built-in color JSON log() */\n logger?: (...args: any[]) => void;\n /** Set with defaults to modify each request data. Takes and returns in order: path, response, params, fetchParams */\n onRequest?: (...args: any[]) => any;\n /** Set with defaults to modify each request data. Takes and returns in order: path, response, params, fetchParams */\n onResponse?: (...args: any[]) => any;\n /** Set with defaults to modify each request data. Takes and returns in order: error, path, params */\n onError?: (...args: any[]) => any;\n /** Set with defaults to process the response as a stream (i.e., for instant unzip) */\n onStream?: (...args: any[]) => any;\n /** default=0 Repeat request this many times */\n repeat?: number;\n /** default=null Repeat request every seconds */\n repeatEvery?: number;\n /** default=0 Seconds to debounce request, wait to execute so that other requests may override */\n debounce?: number;\n /** default=false Refetch when cache is past cacheForTime */\n regrabOnStale?: boolean;\n /** default=false Refetch on window refocus */\n regrabOnFocus?: boolean;\n /** default=false Refetch on network change */\n regrabOnNetwork?: boolean;\n /** shortcut for method: \"POST\" */\n post?: boolean;\n /** shortcut for method: \"PUT\" */\n put?: boolean;\n /** shortcut for method: \"PATCH\" */\n patch?: boolean;\n /** default=null The body of the POST/PUT/PATCH request (can be passed into main)*/\n body?: any;\n /** All other params become GET params, POST body, and other methods */\n [key: string]: TParams | any;\n};\n\n// Combined options and parameters interface\n\n// Mock server configuration for testing\nexport interface GrabMockHandler<TParams = any, TResponse = any> {\n /** Mock response data or function that returns response */\n response: TResponse | ((params: TParams) => TResponse);\n /** HTTP method this mock should respond to */\n method?: \"GET\" | \"POST\" | \"PUT\" | \"PATCH\" | \"DELETE\" | \"OPTIONS\" | \"HEAD\";\n /** Request parameters this mock should match */\n params?: TParams;\n /** Delay in seconds before returning mock response */\n delay?: number;\n}\n\n// Request log entry for debugging and history\nexport interface GrabLogEntry {\n /** API path that was requested */\n path: string;\n /** Stringified request parameters */\n request: string;\n /** Response data (only present for successful requests) */\n response?: any;\n /** Error message (only present for failed requests) */\n error?: string;\n /** Timestamp when request was made */\n lastFetchTime: number;\n /** Abort controller for request cancellation */\n controller?: AbortController;\n /** Current page number for paginated requests */\n currentPage?: number;\n}\n\n// Global grab configuration and state\nexport interface GrabGlobal {\n /** Default options applied to all requests */\n defaults?: Partial<GrabOptions>;\n /** Request history and debugging info */\n log?: GrabLogEntry[];\n /** Mock server handlers for testing */\n mock?: Record<string, GrabMockHandler>;\n /** Create a separate instance of grab with separate default options */\n instance?: (defaultOptions?: Partial<GrabOptions>) => GrabFunction;\n}\n\n// Main grab function signature with overloads for different use cases\nexport interface GrabFunction {\n /**\n * ### GRAB: Generate Request to API from Browser\n * ![grabAPILogo](https://i.imgur.com/Rwl5P3p.png)\n * Make API request with path\n * @returns {Promise<Object>} The response object with resulting data or .error if error.\n * @author [vtempest (2025)](https://github.com/vtempest/GRAB-URL)\n * @see [🎯 Examples](https://grab.js.org/docs/Examples) [📑 Docs](https://grab.js.org/lib)\n */\n <TResponse = any, TParams = Record<string, any>>(path: string, options?: GrabOptions<TResponse, TParams>): Promise<\n GrabResponse<TResponse>\n >;\n\n /**\n * ### GRAB: Generate Request to API from Browser\n * ![grabAPILogo](https://i.imgur.com/Rwl5P3p.png)\n * Make API request with path and options/parameters\n * @returns {Promise<Object>} The response object with resulting data or .error if error.\n * @author [vtempest (2025)](https://github.com/vtempest/GRAB-URL)\n * @see [🎯 Examples](https://grab.js.org/docs/Examples) [📑 Docs](https://grab.js.org/lib)\n */\n <TResponse = any, TParams = Record<string, any>>(\n path: string,\n config: GrabOptions<TResponse, TParams>\n ): Promise<GrabResponse<TResponse>>;\n\n /** Default options applied to all requests */\n defaults?: Partial<GrabOptions>;\n\n /** Request history and debugging info for all requests */\n log?: GrabLogEntry[];\n\n /** Mock server handlers for testing */\n mock?: Record<string, GrabMockHandler>;\n\n /** Create a separate instance of grab with separate default options */\n instance?: (defaultOptions?: Partial<GrabOptions>) => GrabFunction;\n}\n\n// Log function for debugging\nexport interface LogFunction {\n /**\n * Log messages with custom styling\n * @param message - Message to log (string or object)\n */\n (message: string | object, options?: LogOptions): void;\n}\n\n// Utility function to describe JSON structure\nexport interface printJSONStructureFunction {\n /**\n * Generate TypeDoc-like description of JSON object structure\n * @param obj - The JSON object to describe\n * @returns String representation of object structure\n */\n (obj: any): string;\n}\n\ndeclare global {\n // Browser globals\n interface Window {\n grab: GrabFunction;\n log: LogFunction;\n scrollListener: (event: Event) => void; // replace 'any' with the actual type if you know it (e.g., a function type)\n }\n\n // Node.js globals\n namespace NodeJS {\n interface Global {\n grab: GrabFunction;\n log: LogFunction;\n }\n }\n\n // Global variables available after script inclusion\n var log: LogFunction;\n var grab: GrabFunction;\n}\n\nexport { grab, log, printJSONStructure };\n"],"names":["async","grab","path","options","headers","response","method","post","put","patch","cache","cacheForTime","timeout","baseURL","process","env","SERVER_API_URL","cancelOngoingIfNew","cancelNewIfOngoing","rateLimit","debug","infiniteScroll","setDefaults","retryAttempts","logger","log","onRequest","onResponse","onError","onStream","repeatEvery","repeat","debounce","regrabOnStale","regrabOnFocus","regrabOnNetwork","body","params","window","defaults","globalThis","s","t","startsWith","endsWith","slice","debouncer","i","setInterval","regrab","setTimeout","addEventListener","document","visibilityState","resFunction","paginateKey","paginateResult","paginateElement","length","paginateDOM","querySelector","scrollListener","removeEventListener","color","event","target","localStorage","setItem","JSON","stringify","scrollTop","scrollLeft","scrollHeight","clientHeight","priorRequest","currentPage","paramsAsText","find","e","request","pageNumber","key","Object","keys","lastFetchTime","Date","now","res","isLoading","Error","controller","abort","unshift","AbortController","fetchParams","Accept","redirect","signal","AbortSignal","paramsGETRequest","includes","URLSearchParams","toString","startTime","mockHandler","mock","wait","Promise","fetch","catch","message","ok","status","statusText","type","get","json","blob","text","delay","elapsedTime","Number","toFixed","printJSONStructure","data","error","errorMessage","instance","func","args","clearTimeout","showAlert","msg","list","o","getElementById","appendChild","createElement","id","setAttribute","innerHTML","remove","onclick","setupDevTools","ctrlKey","altKey","html","toLocaleString","parse","getItem"],"mappings":"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"}
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "grab-url",
3
3
  "description": "📥 Generate Request to API from Browser",
4
4
  "type": "module",
5
- "version": "1.0.16",
5
+ "version": "1.0.18",
6
6
  "author": "vtempest",
7
7
  "license": "rights.institute/prosper",
8
8
  "repository": {
package/src/grab-api.ts CHANGED
@@ -67,16 +67,16 @@ import {
67
67
  */
68
68
  export default async function grab<TResponse = any, TParams = any>(
69
69
  path: string,
70
- options: GrabOptions<TResponse, TParams> = {} as any
70
+ options?: GrabOptions<TResponse, TParams>
71
71
  ): Promise<GrabResponse<TResponse>> {
72
72
  var {
73
73
  headers,
74
74
  response = {} as any, // Pre-initialized object to set the response in. isLoading and error are also set on this object.
75
- method = options.post // set post: true for POST, omit for GET
75
+ method = options?.post // set post: true for POST, omit for GET
76
76
  ? "POST"
77
- : options.put
77
+ : options?.put
78
78
  ? "PUT"
79
- : options.patch
79
+ : options?.patch
80
80
  ? "PATCH"
81
81
  : "GET",
82
82
  cache = false, // Enable/disable frontend caching