grab-url 1.0.7 → 1.0.8

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";function e(e="",o={}){let{color:n=null,style:r="color: #66ccff; font-size: 10pt;",hideInProduction:i,startSpinner:a=!1,stopSpinner:s=!1}=o;void 0===i&&(i="undefined"!=typeof window&&(null==window?void 0:window.location.hostname.includes("localhost"))),"object"==typeof e&&(e=l(e)+"\n\n"+JSON.stringify(e,null,2)),n&&void 0!==typeof process&&(e=(t[n]||"")+e+t.reset);var c=0;return a?(global||globalThis).interval=setInterval(()=>{process.stdout.write((t[n]||"")+"\r"+"⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏".split("")[c=++c%10]+" "+e+t.reset)},50):s?(clearInterval((global||globalThis).interval),process.stdout.write("\r"+(e||"✔ Done")+" ".repeat(e.length+20)+"\n")):"string"==typeof r?(1==r.split(" ").length||n?r=`color: ${n||r}; font-size: 11pt;`:r.match(/^#[0-9a-fA-F]{6}$/)&&(r=`color: ${r}; font-size: 11pt;`),i?console.debug((r?"%c":"")+(e||""),r):console.log((r?"%c":"")+(e||""),r)):"object"==typeof r&&console.log(e,...r),!0}Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const t={reset:"",black:"",red:"",green:"",yellow:"",blue:"",magenta:"",cyan:"",white:"",gray:"",brightRed:"",brightGreen:"",brightYellow:"",brightBlue:"",brightMagenta:"",brightCyan:"",brightWhite:"",bgRed:"",bgGreen:"",bgYellow:"",bgBlue:"",bgMagenta:"",bgCyan:"",bgWhite:"",bgGray:""};function o(e){return"string"==typeof e?t.yellow:"number"==typeof e?t.cyan:"boolean"==typeof e?t.magenta:"function"==typeof e?t.red:null===e?t.gray:Array.isArray(e)?t.blue:"object"==typeof e?t.green:t.white}function n(e){return"string"==typeof e?'""':"number"==typeof e?"number":"boolean"==typeof e?"bool":"function"==typeof e?"function":null===e?"null":Array.isArray(e)?e.length?"["+n(e[0])+"]":"[]":"object"==typeof e?"{...}":typeof e}function l(e,r=0){const i=" ".repeat(r);if("object"!=typeof e||null===e){return o(e)+n(e)+t.reset}if(Array.isArray(e)){let o=t.blue+"["+t.reset;return e.length&&(o+="\n"),e.forEach((t,n)=>{o+=i+" "+l(t,r+1),n<e.length-1&&(o+=","),o+="\n"}),o+=i+t.blue+"]"+t.reset,o}let a=t.green+"{"+t.reset;const s=Object.keys(e);return s.length&&(a+="\n"),s.forEach((c,d)=>{const u=e[c],f=o(u);a+=i+" ","object"!=typeof u||null===u||Array.isArray(u)?Array.isArray(u)?a+=f+c+t.reset+": "+l(u,r+1):a+=f+c+": "+n(u)+t.reset:a+=f+c+t.reset+": "+l(u,r+1),d<s.length-1&&(a+=","),a+="\n"}),a+=i+t.green+"}"+t.reset,a}function r(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>`}async function i(t,o){var n,r,s,c,d,u,f,g,p,b;let{headers:y,response:m={},method:w=(o.post?"POST":o.put?"PUT":o.patch?"PATCH":"GET"),cache:h=!1,cacheForTime:v=60,timeout:T=30,baseURL:S="undefined"!=typeof process&&process.env.SERVER_API_URL||"/api/",cancelOngoingIfNew:x=!1,cancelNewIfOngoing:L=!1,rateLimit:O=0,debug:E=!1,infiniteScroll:j=null,setDefaults:k=!1,retryAttempts:A=0,logger:P=e,onRequest:N=null,onResponse:D=null,onError:R=null,onStream:$=null,repeatEvery:q=null,repeat:F=0,debounce:I=0,regrabOnStale:J=!1,regrabOnFocus:C=!1,regrabOnNetwork:M=!1,post:H=!1,put:z=!1,patch:W=!1,body:U=null,...B}={..."undefined"!=typeof window?null==(n=null==window?void 0:window.grab)?void 0:n.defaults:(null==(s=null==(r=global||globalThis)?void 0:r.grab)?void 0:s.defaults)||{},...o},G=e=>t.startsWith(e);if(G("http:")||G("https:")?S="":G("/")||S.endsWith("/")?G("/")&&S.endsWith("/")&&(t=t.slice(1)):t="/"+t,I>0)return await a(async()=>{await i(t,{...o,debounce:0})},1e3*I);if(F>1){for(let e=0;e<F;e++)await i(t,{...o,repeat:0});return m}if(q)return setInterval(async()=>{await i(t,{...o,repeat:0,repeatEvery:null})},1e3*q),m;if(null==o?void 0:o.setDefaults)return void("undefined"!=typeof window?window.grab.defaults={...o,setDefaults:void 0}:void 0!==(global||globalThis).grab&&((global||globalThis).grab.defaults={...o,setDefaults:void 0}));if(void 0!==typeof window){const e=async()=>await i(t,{...o,cache:!1});J&&h&&setTimeout(e,1e3*v),M&&window.addEventListener("online",e),C&&(window.addEventListener("focus",e),document.addEventListener("visibilitychange",async()=>{"visible"===document.visibilityState&&await e()}))}let _="function"==typeof m?m:null;m&&!_||(m={});var[K,Y,V]=j||[];if((null==j?void 0:j.length)&&void 0!==V&&"undefined"!=typeof window){let n="string"==typeof V?document.querySelector(V):V;n||e("paginateDOM not found",{color:"red"}),window.scrollListener&&n.removeEventListener("scroll",window.scrollListener),window.scrollListener=async e=>{const n=e.target;localStorage.setItem("scroll",JSON.stringify([n.scrollTop,n.scrollLeft,V])),n.scrollHeight-n.scrollTop<=n.clientHeight+200&&await i(t,{...o,cache:!1,[K]:(null==X?void 0:X.currentPage)+1})},n&&n.addEventListener("scroll",window.scrollListener)}let Q=JSON.stringify(K?{...B,[K]:void 0}:B),X=null==(c=null==i?void 0:i.log)?void 0:c.find(e=>e.request==Q&&e.path==t);if(K){let e=(null==X?void 0:X.currentPage)+1||(null==B?void 0:B[K])||1;X||(m[Y]=[],e=1),X&&(X.currentPage=e),B[K]=e}else{for(let e of Object.keys(m))m[e]=void 0;if(h&&(!v||(null==X?void 0:X.lastFetchTime)>Date.now()-1e3*v)){for(let e of Object.keys(X.res))m[e]=X.res[e];_&&(m=_(m))}}if(_?_({isLoading:!0}):"object"==typeof m&&(m.isLoading=!0),_&&(m=_(m)),O>0&&(null==X?void 0:X.lastFetchTime)&&X.lastFetchTime>Date.now()-1e3*O)throw new Error(`Fetch rate limit exceeded for ${t}. \n Wait ${O}s between requests.`);if(null==X?void 0:X.controller)if(x)X.controller.abort();else if(L)return{isLoading:!0};void 0!==i.log&&(null==(d=i.log)||d.unshift({path:t,request:Q,lastFetchTime:Date.now(),controller:new AbortController}));let Z={method:w,headers:{"Content-Type":"application/json",Accept:"application/json",...y},body:B.body,redirect:"follow",cache:h?"force-cache":"no-store",signal:x?null==(f=null==(u=i.log[0])?void 0:u.controller)?void 0:f.signal:AbortSignal.timeout(1e3*T)},ee="";["POST","PUT","PATCH"].includes(w)?Z.body=B.body||JSON.stringify(B):ee=(Object.keys(B).length?"?":"")+new URLSearchParams(B).toString(),"function"==typeof N&&([t,m,B,Z]=N(t,m,B,Z));let te=null,oe=new Date,ne=null==(g=i.mock)?void 0:g[t];if(!ne||ne.params&&ne.method!=w||ne.params&&Q!=JSON.stringify(ne.params)){if(te=await fetch(S+t+ee,Z).catch(e=>{throw new Error(e)}),!te.ok)throw new Error(`HTTP error: ${te.status} ${te.statusText}`);let e=te.headers.get("content-type");$?await $(te.body):te=await(e?e.includes("application/json")?te&&te.json():e.includes("application/pdf")||e.includes("application/octet-stream")?te.blob():te.text():te.json()).catch(e=>{throw new Error("Error parsing response: "+e)})}else await(le=ne.delay,new Promise(e=>setTimeout(e,1e3*le||0))),te="function"==typeof ne.response?ne.response(B):ne.response;var le;"function"==typeof D&&([t,m,B,Z]=D(t,m,B,Z)),_?_({isLoading:void 0}):"object"==typeof m&&(null==m||delete m.isLoading),null==X||delete X.controller;const re=((Number(new Date)-Number(oe))/1e3).toFixed(1);if(E&&P("Path:"+S+t+ee+"\n"+JSON.stringify(o,null,2)+"\nTime: "+re+"s\nResponse: "+l(te)),"object"==typeof te){for(let e of Object.keys(te))m[e]=Y==e&&(null==(p=m[e])?void 0:p.length)?[...m[e],...te[e]]:te[e];void 0!==m&&(m.data=te)}else _?_({data:te,...te}):"object"==typeof m&&(m.data=te);return void 0!==i.log&&(null==(b=i.log)||b.unshift({path:t,request:JSON.stringify({...B,paginateKey:void 0}),response:m,lastFetchTime:Date.now()})),_&&(m=_(m)),m}i.instance=(e={})=>(t,o={})=>i(t,{...e,...o});const a=async(e,t)=>{let o;return async function(...n){clearTimeout(o),o=setTimeout(async()=>{clearTimeout(o),await e(...n)},t)}};"undefined"!=typeof window?(window.log=e,window.grab=i,window.grab.log=[],window.grab.mock={},window.grab.defaults={},document.addEventListener("keydown",e=>{if("i"===e.key&&e.ctrlKey){let e=" ";for(let t of grab.log)e+=`<div style="margin-bottom:1em; border-bottom:1px solid #ccc; padding-bottom:1em;">\n <b>Path:</b> ${t.path}<br>\n <b>Request:</b> ${t.request}<br>\n <b>Response:</b> ${JSON.stringify(t.response,null,2)}<br>\n <b>Time:</b> ${new Date(t.lastFetchTime).toLocaleString()}\n </div>`;r(e)}}),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 global?(i.log=[],i.mock={},i.defaults={},global.log=e,global.grab=i.instance()):"undefined"!=typeof globalThis&&(i.log=[],i.mock={},i.defaults={},globalThis.log=e,globalThis.grab=i.instance()),exports.default=i,exports.grab=i,exports.log=e,exports.printJSONStructure=l,exports.showAlert=r;
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:l,response:s={},method:a=(i.post?"POST":i.put?"PUT":i.patch?"PATCH":"GET"),cache:d=!1,cacheForTime:c=60,timeout:u=30,baseURL:g="undefined"!=typeof process&&process.env.SERVER_API_URL||"/api/",cancelOngoingIfNew:f=!1,cancelNewIfOngoing:p=!1,rateLimit:b=0,debug:m=!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:k=0,regrabOnStale:N=!1,regrabOnFocus:j=!1,regrabOnNetwork:P=!1,post:D=!1,put:q=!1,patch:J=!1,body:A=null,...F}={..."undefined"!=typeof window?window?.grab?.defaults:(global||globalThis)?.grab?.defaults||{},...i};let R=e=>r.startsWith(e);R("http:")||R("https:")?g="":R("/")||g.endsWith("/")?R("/")&&g.endsWith("/")&&(r=r.slice(1)):r="/"+r;try{if(k>0)return await o(async()=>{await t(r,{...i,debounce:0})},1e3*k);if(E>1){for(let e=0;e<E;e++)await t(r,{...i,repeat:0});return s}if(O)return setInterval(async()=>{await t(r,{...i,repeat:0,repeatEvery:null})},1e3*O),s;if(i?.setDefaults)return void("undefined"!=typeof window?window.grab.defaults={...i,setDefaults:void 0}:void 0!==(global||globalThis).grab&&((global||globalThis).grab.defaults={...i,setDefaults:void 0}));if("undefined"!=typeof window){const e=async()=>await t(r,{...i,cache:!1});N&&d&&setTimeout(e,1e3*c),P&&window.addEventListener("online",e),j&&(window.addEventListener("focus",e),document.addEventListener("visibilitychange",async()=>{"visible"===document.visibilityState&&await e()}))}let n="function"==typeof s?s:null;s&&!n||(s={});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||(s[I]=[],e=1),h&&(h.currentPage=e),F={...F,[$]:e}}else{for(let e of Object.keys(s))s[e]=void 0;if(d&&(!c||h?.lastFetchTime>Date.now()-1e3*c)){for(let e of Object.keys(h.res))s[e]=h.res[e];n&&(s=n(s))}}if(n?n({isLoading:!0}):"object"==typeof s&&(s.isLoading=!0),n&&(s=n(s)),b>0&&h?.lastFetchTime&&h.lastFetchTime>Date.now()-1e3*b)throw new Error(`Fetch rate limit exceeded for ${r}. \n Wait ${b}s between requests.`);if(h?.controller)if(f)h.controller.abort();else if(p)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",...l},body:F.body,redirect:"follow",cache:d?"force-cache":"no-store",signal:f?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,s,F,L]=T(r,s,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(g+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,s,F,L]=S(r,s,F,L)),n?n({isLoading:void 0}):"object"==typeof s&&delete s?.isLoading,delete h?.controller;const C=((Number(new Date)-Number(J))/1e3).toFixed(1);if(m&&v("Path:"+g+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))s[e]=I==e&&s[e]?.length?[...s[e],...q[e]]:q[e];void 0!==s&&(s.data=q)}else n?n({data:q,...q}):"object"==typeof s&&(s.data=q);return void 0!==t.log&&t.log?.unshift({path:r,request:JSON.stringify({...F,paginateKey:void 0}),response:s,lastFetchTime:Date.now()}),n&&(s=n(s)),s}catch(C){let e="Error: "+C.message+"\nPath:"+g+r+"\n";return"function"==typeof L&&L(C.message,g+r,F),i.retryAttempts>0?await t(r,{...i,retryAttempts:--i.retryAttempts}):(!C.message.includes("signal")&&i.debug&&(v(e,{color:"red"}),m&&"undefined"!=typeof document&&n(e)),s.error=C.message,"function"==typeof s?(s.data=s({isLoading:void 0,error:C.message}),s=s.data):delete s?.isLoading,void 0!==t.log&&t.log?.unshift({path:r,request:JSON.stringify(F),error:C.message}),s)}}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 global?(t.log=[],t.mock={},t.defaults={},global.log=e.log,global.grab=t.instance()):"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/log.ts","../src/grab-api.ts"],"sourcesContent":["/**\n * ### Colorized Log With JSON Structure\n * ![Debug log](https://i.imgur.com/R8Qp6Vg.png)\n * Logs messages to the console with custom styling,\n * prints JSON with description of structure layout,\n * and showing debug output in development only.\n * @param {string|object} message - The message to log. If an object is provided, it will be stringified.\n * @param {string|string[]} [options.style] default='color: blue; font-size: 11pt;' - CSS style string\n * @param {boolean} [options.hideInProduction] - default = auto-detects based on hostname.\n * If true, uses `console.debug` (hidden in production). If false, uses `console.log`.\n *\n */\nexport function log(message: string|object = \"\", options: LogOptions = {}) {\n let {\n color = null,\n style = \"color: #66ccff; font-size: 10pt;\",\n hideInProduction = undefined,\n startSpinner = false,\n stopSpinner = false,\n } = options;\n\n // Auto-detect if we should hide logs in production based on hostname\n if (typeof hideInProduction === \"undefined\")\n hideInProduction =\n typeof window !== \"undefined\" &&\n window?.location.hostname.includes(\"localhost\");\n\n // For objects, print both the structure visualization and full JSON\n if (typeof message === \"object\")\n message =\n printJSONStructure(message) + \"\\n\\n\" + JSON.stringify(message, null, 2);\n\n //colorize in terminal (%c is only in browser)\n if (color && typeof process !== undefined)\n message = (colors[color] || \"\") + message + colors.reset;\n\n // Displays an animated spinner in the terminal with the provided text.\n var i = 0;\n\n if (startSpinner)\n (global || globalThis).interval = setInterval(() => {\n process.stdout.write(\n (colors[color] || \"\") +\n \"\\r\" +\n \"⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏\".split(\"\")[(i = ++i % 10)] +\n \" \" +\n message +\n colors.reset\n );\n }, 50);\n else if (stopSpinner) {\n clearInterval((global || globalThis).interval);\n process.stdout.write(\n \"\\r\" + (message || \"✔ Done\") + \" \".repeat(message.length + 20) + \"\\n\"\n );\n } else if (typeof style === \"string\") {\n // check if style is a one word color code or named color\n //test if style is valid as a CSS color name\n if (style.split(\" \").length == 1 || color) {\n style = `color: ${color || style}; font-size: 11pt;`;\n } else {\n // check if style is valid as a CSS color code\n if (style.match(/^#[0-9a-fA-F]{6}$/)) {\n style = `color: ${style}; font-size: 11pt;`;\n }\n }\n // Use console.debug for production-hidden logs, console.log otherwise\n if (hideInProduction)\n console.debug((style ? \"%c\" : \"\") + (message || \"\"), style);\n else console.log((style ? \"%c\" : \"\") + (message || \"\"), style);\n } else if (typeof style === \"object\") console.log(message, ...(style as any));\n return true;\n}\n\nexport interface LogOptions {\n /** CSS style string or array of CSS strings for browser console styling */\n style?: string | string[];\n /** Optional color name or code for terminal environments */\n color?: keyof typeof colors | null;\n /** If true, hides log in production (auto-detects by hostname if undefined) */\n hideInProduction?: boolean;\n /** Start a spinner (for CLI tools, optional) */\n startSpinner?: boolean;\n /** Stop a spinner (for CLI tools, optional) */\n stopSpinner?: boolean;\n}\n\n// ANSI escape codes for terminal colors when running in Node.js\nexport const colors = {\n reset: \"\\x1b[0m\", // Reset to default color\n black: \"\\x1b[30m\",\n red: \"\\x1b[31m\", // Functions, errors\n green: \"\\x1b[32m\", // Object braces, success\n yellow: \"\\x1b[33m\", // Strings, warnings\n blue: \"\\x1b[34m\", // Array brackets, info\n magenta: \"\\x1b[35m\", // Booleans\n cyan: \"\\x1b[36m\", // Numbers\n white: \"\\x1b[37m\", // Default color, plain text\n gray: \"\\x1b[90m\", // Null, undefined, subtle\n // Bright variants\n brightRed: \"\\x1b[91m\",\n brightGreen: \"\\x1b[92m\",\n brightYellow: \"\\x1b[93m\",\n brightBlue: \"\\x1b[94m\",\n brightMagenta: \"\\x1b[95m\",\n brightCyan: \"\\x1b[96m\",\n brightWhite: \"\\x1b[97m\",\n // Background colors (optional)\n bgRed: \"\\x1b[41m\",\n bgGreen: \"\\x1b[42m\",\n bgYellow: \"\\x1b[43m\",\n bgBlue: \"\\x1b[44m\",\n bgMagenta: \"\\x1b[45m\",\n bgCyan: \"\\x1b[46m\",\n bgWhite: \"\\x1b[47m\",\n bgGray: \"\\x1b[100m\",\n};\n\n/**\n * Determines the appropriate color code for a given value type\n * Used for consistent color coding in the structure visualization\n */\nfunction getColorForType(value) {\n if (typeof value === \"string\") return colors.yellow;\n if (typeof value === \"number\") return colors.cyan;\n if (typeof value === \"boolean\") return colors.magenta;\n if (typeof value === \"function\") return colors.red;\n if (value === null) return colors.gray;\n if (Array.isArray(value)) return colors.blue;\n if (typeof value === \"object\") return colors.green;\n return colors.white;\n}\n\n/**\n * Returns a string representation of the value's type\n * Used to show simplified type information in the structure visualization\n */\nfunction getTypeString(value) {\n if (typeof value === \"string\") return '\"\"';\n if (typeof value === \"number\") return \"number\";\n if (typeof value === \"boolean\") return \"bool\";\n if (typeof value === \"function\") return \"function\";\n if (value === null) return \"null\";\n if (Array.isArray(value)) {\n if (value.length) return \"[\" + getTypeString(value[0]) + \"]\";\n else return \"[]\";\n }\n if (typeof value === \"object\") return \"{...}\";\n return typeof value;\n}\n\n/**\n * Creates a colored visualization of a JSON object's structure\n * Shows the shape and types of the data rather than actual values\n * Recursively processes nested objects and arrays\n */\nexport function printJSONStructure(obj, indent = 0) {\n const pad = \" \".repeat(indent);\n\n // Handle primitive values and null\n if (typeof obj !== \"object\" || obj === null) {\n const color = getColorForType(obj);\n return color + getTypeString(obj) + colors.reset;\n }\n\n // Handle arrays with special bracket formatting\n if (Array.isArray(obj)) {\n let result = colors.blue + \"[\" + colors.reset;\n if (obj.length) result += \"\\n\";\n obj.forEach((item, idx) => {\n result += pad + \" \" + printJSONStructure(item, indent + 1);\n if (idx < obj.length - 1) result += \",\";\n result += \"\\n\";\n });\n result += pad + colors.blue + \"]\" + colors.reset;\n return result;\n }\n\n // Handle objects with special brace and property formatting\n let result = colors.green + \"{\" + colors.reset;\n const keys = Object.keys(obj);\n if (keys.length) result += \"\\n\";\n keys.forEach((key, index) => {\n const value = obj[key];\n const color = getColorForType(value);\n result += pad + \" \";\n\n // Handle nested objects recursively\n if (typeof value === \"object\" && value !== null && !Array.isArray(value)) {\n result +=\n color +\n key +\n colors.reset +\n \": \" +\n printJSONStructure(value, indent + 1);\n }\n // Handle nested arrays recursively\n else if (Array.isArray(value)) {\n result +=\n color +\n key +\n colors.reset +\n \": \" +\n printJSONStructure(value, indent + 1);\n }\n // Handle primitive values\n else {\n result += color + key + \": \" + getTypeString(value) + colors.reset;\n }\n if (index < keys.length - 1) result += \",\";\n result += \"\\n\";\n });\n result += pad + colors.green + \"}\" + colors.reset;\n\n // Only log at top level of recursion\n if (indent === 0) {\n // console.log(result);\n }\n return result;\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+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+I) to toggle debug view\n document.addEventListener(\"keydown\", (e) => {\n if (e.key === \"i\" && e.ctrlKey) {\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> ${request.request}<br>\n <b>Response:</b> ${JSON.stringify(request.response, null, 2)}<br>\n <b>Time:</b> ${new Date(request.lastFetchTime).toLocaleString()}\n </div>`;\n }\n showAlert(html);\n }\n });\n}\n\n/**\n * Displays an animated spinner in the terminal with the provided text.\n * The spinner animates in-place until the returned function is called,\n * which stops the spinner and prints a success message.\n * @param {string} text - The text to display next to the spinner animation.\n * @returns {(success?: string) => void} Stop function with optional message.\n * @example\n * const stopSpinner = showSpinnerInTerminal('Downloading...');\n * setTimeout(() => {\n * stopSpinner('Success!');\n * }, 2000);\n */\nexport function showSpinnerInTerminal(text) {\n let i = 0,\n interval = setInterval(() => {\n process.stdout.write(\n \"\\r\" + \"⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏\".split(\"\")[(i = ++i % 10)] + \" \" + text\n );\n }, 50);\n\n return function (success = \"✔ Done!\") {\n clearInterval(interval);\n process.stdout.write(\"\\r\" + success + \" \".repeat(text.length) + \"\\n\");\n };\n}\n","import {\n printJSONStructure,\n log,\n showAlert,\n setupDevTools,\n type LogOptions,\n} from \"./log\";\n\n/**\n * TODO\n * - react tests\n * - grab error popup and dev tool\n * - show net log in alert\n * - progress\n * - pagination working\n * - tests in stackblitz\n * - loading icons\n * - cache revalidation\n */\n\n/**\n * ### GRAB: Generate Request to API from Browser\n * ![GrabAPILogo](https://i.imgur.com/qrQWkeb.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/guide/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/guide/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/guide/Examples) [📑 Docs](https://grab.js.org)\n * @example import grab from 'grab-url';\n * let res = {};\n * await grab('search', {\n * response: res,\n * query: \"search words\"\n * })\n */\nexport default async function grab<TResponse = any, TParams = any>(\n path: string,\n options: GrabOptions<TResponse, TParams>\n): Promise<GrabResponse<TResponse>> {\n let {\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 : (global || 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 (global || globalThis).grab !== \"undefined\")\n (global || 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\n if (window.scrollListener) \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 // @ts-ignore\n 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);\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 (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 // @ts-ignore\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 global !== \"undefined\") {\n grab.log = [];\n grab.mock = {};\n grab.defaults = {};\n global.log = log;\n global.grab = grab.instance();\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/***************** 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/qrQWkeb.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/guide/Examples) [📑 Docs](https://grab.js.org/lib)\n */\n <TResponse = any, TParams = Record<string, any>>(path: string): Promise<\n GrabResponse<TResponse>\n >;\n\n /**\n * ### GRAB: Generate Request to API from Browser\n * ![grabAPILogo](https://i.imgur.com/qrQWkeb.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/guide/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\n// Helper type for creating typed API clients\n// export type TypedGrabFunction = <\n// TResponse = any,\n// TParams = Record<string, any>\n// >(\n// path: string,\n// config?: GrabOptions<TResponse, TParams>\n// ) => Promise<GrabResponse<TResponse>>;\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, showAlert, printJSONStructure };\n"],"names":["log","message","options","color","style","hideInProduction","startSpinner","stopSpinner","window","location","hostname","includes","printJSONStructure","JSON","stringify","process","colors","reset","i","global","globalThis","interval","setInterval","stdout","write","split","clearInterval","repeat","length","match","console","debug","black","red","green","yellow","blue","magenta","cyan","white","gray","brightRed","brightGreen","brightYellow","brightBlue","brightMagenta","brightCyan","brightWhite","bgRed","bgGreen","bgYellow","bgBlue","bgMagenta","bgCyan","bgWhite","bgGray","getColorForType","value","Array","isArray","getTypeString","obj","indent","pad","result","forEach","item","idx","keys","Object","key","index","showAlert","msg","document","list","o","getElementById","body","appendChild","createElement","id","setAttribute","innerHTML","addEventListener","e","target","remove","onclick","querySelector","async","grab","path","headers","response","method","post","put","patch","cache","cacheForTime","timeout","baseURL","env","SERVER_API_URL","cancelOngoingIfNew","cancelNewIfOngoing","rateLimit","infiniteScroll","setDefaults","retryAttempts","logger","onRequest","onResponse","onError","onStream","repeatEvery","debounce","regrabOnStale","regrabOnFocus","regrabOnNetwork","params","_a","defaults","_c","_b","s","t","startsWith","endsWith","slice","debouncer","regrab","setTimeout","visibilityState","resFunction","paginateKey","paginateResult","paginateElement","paginateDOM","scrollListener","removeEventListener","event","localStorage","setItem","scrollTop","scrollLeft","scrollHeight","clientHeight","priorRequest","currentPage","paramsAsText","find","request","pageNumber","lastFetchTime","Date","now","res","isLoading","Error","controller","abort","unshift","AbortController","fetchParams","Accept","redirect","signal","AbortSignal","paramsGETRequest","URLSearchParams","toString","startTime","mockHandler","mock","fetch","catch","ok","status","statusText","type","get","json","blob","text","delay","Promise","elapsedTime","Number","toFixed","_i","data","instance","func","wait","args","clearTimeout","ctrlKey","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/xWD7gyV.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 : (global || 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 (global || globalThis).grab !== \"undefined\")\n (global || 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 global !== \"undefined\") {\n grab.log = [];\n grab.mock = {};\n grab.defaults = {};\n global.log = log;\n global.grab = grab.instance();\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/xWD7gyV.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/xWD7gyV.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","global","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,48 +1,48 @@
1
- declare const colors: {
2
- reset: string;
3
- black: string;
4
- red: string;
5
- green: string;
6
- yellow: string;
7
- blue: string;
8
- magenta: string;
9
- cyan: string;
10
- white: string;
11
- gray: string;
12
- brightRed: string;
13
- brightGreen: string;
14
- brightYellow: string;
15
- brightBlue: string;
16
- brightMagenta: string;
17
- brightCyan: string;
18
- brightWhite: string;
19
- bgRed: string;
20
- bgGreen: string;
21
- bgYellow: string;
22
- bgBlue: string;
23
- bgMagenta: string;
24
- bgCyan: string;
25
- bgWhite: string;
26
- bgGray: string;
27
- };
28
-
29
1
  /**
30
- * TODO
31
- * - react tests
32
- * - grab error popup and dev tool
33
- * - show net log in alert
34
- * - progress
35
- * - pagination working
36
- * - tests in stackblitz
37
- * - loading icons
38
- * - cache revalidation
2
+ * Available color names
39
3
  */
4
+ declare enum ColorName {
5
+ RESET = "reset",
6
+ BLACK = "black",
7
+ RED = "red",
8
+ GREEN = "green",
9
+ YELLOW = "yellow",
10
+ BLUE = "blue",
11
+ MAGENTA = "magenta",
12
+ CYAN = "cyan",
13
+ WHITE = "white",
14
+ GRAY = "gray",
15
+ BRIGHT_RED = "brightRed",
16
+ BRIGHT_GREEN = "brightGreen",
17
+ BRIGHT_YELLOW = "brightYellow",
18
+ BRIGHT_BLUE = "brightBlue",
19
+ BRIGHT_MAGENTA = "brightMagenta",
20
+ BRIGHT_CYAN = "brightCyan",
21
+ BRIGHT_WHITE = "brightWhite",
22
+ BG_RED = "bgRed",
23
+ BG_GREEN = "bgGreen",
24
+ BG_YELLOW = "bgYellow",
25
+ BG_BLUE = "bgBlue",
26
+ BG_MAGENTA = "bgMagenta",
27
+ BG_CYAN = "bgCyan",
28
+ BG_WHITE = "bgWhite",
29
+ BG_GRAY = "bgGray",
30
+ BG_BLACK = "bgBlack",
31
+ BG_BRIGHT_RED = "bgBrightRed",
32
+ BG_BRIGHT_GREEN = "bgBrightGreen",
33
+ BG_BRIGHT_YELLOW = "bgBrightYellow",
34
+ BG_BRIGHT_BLUE = "bgBrightBlue",
35
+ BG_BRIGHT_MAGENTA = "bgBrightMagenta",
36
+ BG_BRIGHT_CYAN = "bgBrightCyan",
37
+ BG_BRIGHT_WHITE = "bgBrightWhite"
38
+ }
39
+
40
40
  /**
41
41
  * ### GRAB: Generate Request to API from Browser
42
- * ![GrabAPILogo](https://i.imgur.com/qrQWkeb.png)
42
+ * ![GrabAPILogo](https://i.imgur.com/xWD7gyV.png)
43
43
  *
44
44
  * 1. **GRAB is the FBEST Request Manager: Functionally Brilliant, Elegantly Simple Tool**: One Function, no dependencies,
45
- * minimalist syntax, [more features than alternatives](https://grab.js.org/guide/Comparisons)
45
+ * minimalist syntax, [more features than alternatives](https://grab.js.org/docs/Comparisons)
46
46
  * 2. **Auto-JSON Convert**: Pass parameters and get response or error in JSON, handling other data types as is.
47
47
  * 3. **isLoading Status**: Sets `.isLoading=true` on the pre-initialized response object so you can show a "Loading..." in any framework
48
48
  * 4. **Debug Logging**: Adds global `log()` and prints colored JSON structure, response, timing for requests in test.
@@ -58,7 +58,7 @@ declare const colors: {
58
58
  * 14. **Framework Agnostic**: Alternatives like TanStack work only in component initialization and depend on React & others.
59
59
  * 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`
60
60
  * 16. **TypeScript Tooltips**: Developers can hover over option names and autocomplete TypeScript.
61
- * 17. **Request Stategies**: [🎯 Examples](https://grab.js.org/guide/Examples) show common stategies like debounce, repeat, proxy, unit tests, interceptors, file upload, etc
61
+ * 17. **Request Stategies**: [🎯 Examples](https://grab.js.org/docs/Examples) show common stategies like debounce, repeat, proxy, unit tests, interceptors, file upload, etc
62
62
  * 18. **Rate Limiting**: Built-in rate limiting to prevent multi-click cascading responses, require to wait seconds between requests.
63
63
  * 19. **Repeat**: Repeat request this many times, or repeat every X seconds to poll for updates.
64
64
  * 20. **Loading Icons**: Import from `grab-url/icons` to get enhanced animated loading icons.
@@ -96,13 +96,7 @@ declare const colors: {
96
96
  * @param {any} [...params] All other params become GET params, POST body, and other methods.
97
97
  * @returns {Promise<Object>} The response object with resulting data or .error if error.
98
98
  * @author [vtempest (2025)](https://github.com/vtempest/GRAB-URL)
99
- * @see [🎯 Examples](https://grab.js.org/guide/Examples) [📑 Docs](https://grab.js.org)
100
- * @example import grab from 'grab-url';
101
- * let res = {};
102
- * await grab('search', {
103
- * response: res,
104
- * query: "search words"
105
- * })
99
+ * @see [🎯 Examples](https://grab.js.org/docs/Examples) [📑 Docs](https://grab.js.org)
106
100
  */
107
101
  declare function grab_2<TResponse = any, TParams = any>(path: string, options: GrabOptions<TResponse, TParams>): Promise<GrabResponse<TResponse>>;
108
102
 
@@ -118,20 +112,20 @@ export { grab_2 as grab }
118
112
  export declare interface GrabFunction {
119
113
  /**
120
114
  * ### GRAB: Generate Request to API from Browser
121
- * ![grabAPILogo](https://i.imgur.com/qrQWkeb.png)
115
+ * ![grabAPILogo](https://i.imgur.com/xWD7gyV.png)
122
116
  * Make API request with path
123
117
  * @returns {Promise<Object>} The response object with resulting data or .error if error.
124
118
  * @author [vtempest (2025)](https://github.com/vtempest/GRAB-URL)
125
- * @see [🎯 Examples](https://grab.js.org/guide/Examples) [📑 Docs](https://grab.js.org/lib)
119
+ * @see [🎯 Examples](https://grab.js.org/docs/Examples) [📑 Docs](https://grab.js.org/lib)
126
120
  */
127
- <TResponse = any, TParams = Record<string, any>>(path: string): Promise<GrabResponse<TResponse>>;
121
+ <TResponse = any, TParams = Record<string, any>>(path: string, options?: GrabOptions<TResponse, TParams>): Promise<GrabResponse<TResponse>>;
128
122
  /**
129
123
  * ### GRAB: Generate Request to API from Browser
130
- * ![grabAPILogo](https://i.imgur.com/qrQWkeb.png)
124
+ * ![grabAPILogo](https://i.imgur.com/xWD7gyV.png)
131
125
  * Make API request with path and options/parameters
132
126
  * @returns {Promise<Object>} The response object with resulting data or .error if error.
133
127
  * @author [vtempest (2025)](https://github.com/vtempest/GRAB-URL)
134
- * @see [🎯 Examples](https://grab.js.org/guide/Examples) [📑 Docs](https://grab.js.org/lib)
128
+ * @see [🎯 Examples](https://grab.js.org/docs/Examples) [📑 Docs](https://grab.js.org/lib)
135
129
  */
136
130
  <TResponse = any, TParams = Record<string, any>>(path: string, config: GrabOptions<TResponse, TParams>): Promise<GrabResponse<TResponse>>;
137
131
  /** Default options applied to all requests */
@@ -285,7 +279,7 @@ declare interface LogOptions {
285
279
  /** CSS style string or array of CSS strings for browser console styling */
286
280
  style?: string | string[];
287
281
  /** Optional color name or code for terminal environments */
288
- color?: keyof typeof colors | null;
282
+ color?: ColorName | ColorName[] | string | string[];
289
283
  /** If true, hides log in production (auto-detects by hostname if undefined) */
290
284
  hideInProduction?: boolean;
291
285
  /** Start a spinner (for CLI tools, optional) */
@@ -298,8 +292,12 @@ declare interface LogOptions {
298
292
  * Creates a colored visualization of a JSON object's structure
299
293
  * Shows the shape and types of the data rather than actual values
300
294
  * Recursively processes nested objects and arrays
295
+ * @param {object} obj - The JSON object to visualize
296
+ * @param {number} indent - The number of spaces to indent the object
297
+ * @param {ColorFormat} colorFormat - The color format to use
298
+ * @returns {string} The colored visualization of the JSON object
301
299
  */
302
- export declare function printJSONStructure(obj: any, indent?: number): string;
300
+ export declare function printJSONStructure(obj: any, indent?: number, colorFormat?: 'html' | 'ansi'): string;
303
301
 
304
302
  export declare interface printJSONStructureFunction {
305
303
  /**
@@ -310,6 +308,17 @@ export declare interface printJSONStructureFunction {
310
308
  (obj: any): string;
311
309
  }
312
310
 
311
+ /**
312
+ * Sets up development tools for debugging API requests
313
+ * Adds a keyboard shortcut (Ctrl+Alt+I) that shows a modal with request history
314
+ * Each request entry shows:
315
+ * - Request path
316
+ * - Request details
317
+ * - Response data
318
+ * - Timestamp
319
+ */
320
+ export declare function setupDevTools(): void;
321
+
313
322
  /**
314
323
  * Shows message in a modal overlay with scrollable message stack
315
324
  * and is easier to dismiss unlike alert() which blocks window.