what-isr 0.13.3 → 0.13.5

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.
package/dist/index.min.js CHANGED
@@ -1 +1 @@
1
- import{createHash as z}from"node:crypto";var L="\0",G=new Set(["fbclid","gclid","_","mc_cid","mc_eid"]),Q=["utm_"];function M(t){if(G.has(t))return!0;for(let e of Q)if(t.startsWith(e))return!0;return!1}function $(t){if(!t)return"/";let e=String(t).split("?")[0].split("#")[0];return e.length>1&&e.endsWith("/")&&(e=e.slice(0,-1)),e||"/"}function Z(t){if(!t)return[];if(typeof t=="string"){let r=t.startsWith("?")?t.slice(1):t;return r?[...new URLSearchParams(r).entries()]:[]}let e=[];for(let[r,s]of Object.entries(t))if(Array.isArray(s))for(let c of s)e.push([r,String(c)]);else e.push([r,String(s)]);return e}var T=t=>encodeURIComponent(String(t));function q(t){return Z(t).filter(([r])=>!M(r)).sort((r,s)=>r[0]===s[0]?r[1]<s[1]?-1:1:r[0]<s[0]?-1:1).map(([r,s])=>`${T(r)}=${T(s)}`).join("&")}function tt(t){let e={};if(!t)return e;for(let r of String(t).split(";")){let s=r.indexOf("=");s<0||(e[r.slice(0,s).trim()]=r.slice(s+1).trim())}return e}function O(t){let e=t||{};return e.vary!=null?e.vary:(e.config||{}).vary}function D(t){if(t==null)return[];if(typeof t=="string")return t?[t]:[];if(!Array.isArray(t))return null;let e=[];for(let r of t){if(typeof r!="string"||!r)return null;e.push(r)}return e}function P(t,e){let r=D(t);if(r===null)return null;if(!r.length)return{};if(!e||typeof e!="object")return null;let s={};for(let[h,l]of Object.entries(e))s[h.toLowerCase()]=Array.isArray(l)?l.join(","):l;let c=tt(s.cookie),d={};for(let h of r){let l=String(h),w=l.indexOf(":"),m=w<0?"header":l.slice(0,w).toLowerCase(),n=w<0?l:l.slice(w+1);if(m==="cookie")d[l]=c[n]??"";else if(m==="header")d[l]=s[n.toLowerCase()]??"";else return null}return d}function et(t){return t?Object.entries(t).sort((e,r)=>e[0]<r[0]?-1:1).map(([e,r])=>`${T(e)}=${T(r)}`).join("&"):""}function R({path:t,query:e,vary:r,headers:s}={}){let c=typeof r=="string"||Array.isArray(r),d=r==null||typeof r=="object"&&!Array.isArray(r),h=c?P(r,s):d?r:null;if(h===null)throw Object.assign(new Error("[what-isr] cannot build a cache key: `vary` is declared but could not be resolved against the request (unsupported shape, or no request headers were supplied)"),{code:"ERR_ISR_VARY_UNRESOLVED"});return[$(t),q(e),et(h)].join(L)}function C(t){let e=String(t),r=e.lastIndexOf(L);if(r<0)return e;let s=e.slice(r+1);return s.includes("=")?e.slice(0,r+1)+z("sha256").update(s).digest("hex"):e}function B(t){let e=z("sha256").update(t).digest("hex");return`${e.slice(0,2)}/${e.slice(2,4)}/${e.slice(4)}`}function I(t,e={},r=Date.now()){let s=e.mode==="hybrid"?60:0,c=e.revalidate!=null,d=Number(e.revalidate??s),h=Number.isFinite(d)?d:s,l=e.swr!=null?Number(e.swr):h,w=h>0?r+h*1e3:c?r:1/0;return{html:t.html||"",head:t.head||"",state:t.state??null,tags:t.tags||e.tags||[],path:t.path||e.path,status:t.status||200,partial:!!t.partial,private:!!(t.private||t.usedRequestHeaders),renderedAt:r,maxAge:h,swrWindow:l,expiresAt:w}}function N(t,e=Date.now()){return t.expiresAt===1/0||e<t.expiresAt}function V(t,e=Date.now()){return N(t,e)?!0:t.swrWindow==null?!1:e<t.expiresAt+t.swrWindow*1e3}function rt(t){let e=new Set;for(let r of t){let s=String(r),c=s.indexOf(":"),d=c<0?"header":s.slice(0,c).toLowerCase();e.add(d==="cookie"?"Cookie":c<0?s:s.slice(c+1))}return[...e].join(", ")}function W(t={},e={},r="MISS",s){let c={"X-What-Cache":r},d=s!==void 0?s:e.vary,h=D(d),l=h===null||h.length>0;if(h&&h.length>0&&(c.Vary=rt(h)),!((t.status==null||t.status===200)&&!l&&!t.private&&(t.maxAge>0||e.mode==="static"||e.mode==="hybrid")&&e.mode!=="server"))return c["Cache-Control"]="private, no-store",c;let n=t.partial?0:t.maxAge||0,o=t.swrWindow!=null?t.swrWindow:n;return c["Cache-Control"]=`public, s-maxage=${n}, stale-while-revalidate=${o}`,t.tags&&t.tags.length&&(c["Cache-Tag"]=t.tags.join(","),c["Surrogate-Key"]=t.tags.join(" ")),c}function _(t){if(typeof t!="string"||!t.startsWith("/")||/^[/\\]{2}/.test(t)||t.includes("\\"))return null;try{let e=new URL(t,"http://localhost");return e.origin!=="http://localhost"?null:e.pathname+e.search}catch{return null}}function nt({store:t,render:e,cdn:r,now:s=Date.now,logger:c=console}={}){let d=new Map;function h(a){return P(O(a),a.varyHeaders||a.headers)}function l(a){let g=h(a);if(g===null)throw Object.assign(new Error("[what-isr] route declares `vary` but the adapter supplied no request headers; refusing to cache"),{code:"ERR_ISR_VARY_NO_HEADERS"});return R({path:a.path,query:a.query,vary:g})}function w(a,g,S){let y=d.get(a);if(y)return y;let i=S||e,u=(async()=>{let p=await i(g,{}),x=I({...p,path:g.path},g.config||{},s());return x.status===200&&!x.private&&await t.set(a,x),x})().finally(()=>d.delete(a));return d.set(a,u),u}function m(a,g,S){return{html:a.html,head:a.head,state:a.state,status:a.status||200,cacheStatus:g,headers:W(a,S.config||{},g,O(S))}}async function n(a,g){let S=a.config||{},y=h(a);if(S.mode==="server"||y===null){y===null&&c.warn?.(`[what-isr] ${a.path} declares \`vary\` but no request headers were supplied; bypassing the cache`);let A=await(g||e)(a,{}),b=I({...A,path:a.path,private:y===null||A.private},S,s());return m(b,"BYPASS",a)}let i=R({path:a.path,query:a.query,vary:y}),u=await t.get(i),p=s();if(u&&N(u,p))return m(u,"HIT",a);if(u&&V(u,p))return w(i,a,g).catch(A=>c.error?.("[what-isr] background regenerate failed:",A)),m(u,"STALE",a);if(u&&S.onMiss==="stale-if-error")try{let A=await w(i,a,g);return m(A,"MISS",a)}catch(A){return c.error?.("[what-isr] regenerate failed, serving stale:",A),m(u,"STALE",a)}let x=await w(i,a,g);return m(x,"MISS",a)}async function o(a,{regenerate:g=!1,routeResolver:S}={}){let y=_(typeof a=="string"?a:"");if(y===null)return c.warn?.(`[what-isr] refusing to revalidate a non-local path: ${a}`),[];let i=$(y),u=await t.deleteByPath(i);if(r&&r.purge&&await r.purge([y]),g){let p=S?S(i):{path:i,query:{},config:{}};try{await w(l(p),p)}catch(x){c.error?.("[what-isr] regen after revalidatePath failed:",x)}}return u}async function f(a,{regenerate:g=!1,routeResolver:S}={}){let y=await t.deleteByTag(a);if(r&&r.purgeTags&&await r.purgeTags([a]),g&&S)for(let i of y){let u=S(i);if(u)try{await w(l(u),u)}catch{}}return y}return{handle:n,regenerate:a=>w(l(a),a),revalidatePath:o,revalidateTag:f,keyFor:l,store:t,_inFlight:d,_now:s,_cdn:r}}function at({maxEntries:t=1e3}={}){let e=new Map,r=new Map,s=new Map;function c(n,o,f){if(o==null)return;let a=n.get(o);a||n.set(o,a=new Set),a.add(f)}function d(n,o,f){if(o==null)return;let a=n.get(o);a&&(a.delete(f),a.size===0&&n.delete(o))}function h(n,o){if(o.tags)for(let f of o.tags)c(r,f,n);c(s,o.path,n)}function l(n,o){if(o.tags)for(let f of o.tags)d(r,f,n);d(s,o.path,n)}function w(n){let o=e.get(n);return o?(e.delete(n),l(n,o),!0):!1}function m(n,o){let f=n.get(o);if(!f)return[];let a=[...f];for(let g of a)w(g);return a}return{async get(n){let o=e.get(n);return o?(e.delete(n),e.set(n,o),o):null},async set(n,o){for(e.has(n)&&l(n,e.get(n)),e.set(n,o),h(n,o);e.size>t;){let f=e.keys().next().value;w(f)}},async delete(n){return w(n)},async deleteByTag(n){return m(r,n)},async deleteByPath(n){return m(s,n)},async clear(){e.clear(),r.clear(),s.clear()},async keys(){return[...e.keys()]}}}import{mkdir as st,writeFile as ot,readFile as it,rename as ct,rm as E,readdir as j,stat as J}from"node:fs/promises";import{join as v,dirname as ut}from"node:path";import{createHash as ft,randomBytes as lt}from"node:crypto";function H(t){return ft("sha256").update(String(t)).digest("hex")}function dt({dir:t}){let e=v(t,"entries"),r=v(t,"tags"),s=v(t,"paths"),c=i=>v(e,B(i)+".json"),d=(i,u)=>v(i,H(u)),h=(i,u,p)=>v(d(i,u),H(p)+".json"),l=(i,u)=>v(i,H(u)+".json");async function w(i,u){await st(ut(i),{recursive:!0});let p=`${i}.${process.pid}.${lt(6).toString("hex")}.tmp`;await ot(p,u),await ct(p,i)}async function m(i){try{return JSON.parse(await it(i,"utf8"))}catch{return null}}async function n(i,u,p){await w(h(i,u,p),JSON.stringify(p))}async function o(i,u,p){await E(h(i,u,p),{force:!0});let x=await f(i,u);if(!x)return;let A=x.filter(b=>b!==p);A.length!==x.length&&(A.length?await w(l(i,u),JSON.stringify(A)):await E(l(i,u),{force:!0}))}async function f(i,u){let p=await m(l(i,u));return Array.isArray(p)?p.filter(x=>typeof x=="string"):null}async function a(i,u){let p=new Set(await f(i,u)||[]),x=d(i,u),A;try{A=await j(x)}catch{return[...p]}for(let b of A){if(!b.endsWith(".json"))continue;let k=await m(v(x,b));typeof k=="string"&&p.add(k)}return[...p]}async function g(i,u){let p=await a(i,u);for(let x of p)await S(x);return await E(d(i,u),{recursive:!0,force:!0}),await E(l(i,u),{force:!0}),p}async function S(i){let u=await m(c(i));if(!u)return!1;await E(c(i),{force:!0});let p=u.entry||{};for(let x of p.tags||[])await o(r,x,i);return p.path&&await o(s,p.path,i),!0}async function y(){let i=[],u;try{u=await j(e)}catch{return i}for(let p of u){let x=v(e,p),A;try{A=await J(x)}catch{continue}if(A.isDirectory())for(let b of await j(x)){let k=v(x,b);try{if(!(await J(k)).isDirectory())continue}catch{continue}for(let K of await j(k)){if(!K.endsWith(".json"))continue;let F=await m(v(k,K));F&&F.key!=null&&i.push(F.key)}}}return i}return{async get(i){let u=await m(c(C(i)));return u?u.entry:null},async set(i,u){let p=C(i),x=await m(c(p));if(x&&x.entry){for(let A of x.entry.tags||[])await o(r,A,p);x.entry.path&&await o(s,x.entry.path,p)}await w(c(p),JSON.stringify({key:p,entry:u}));for(let A of u.tags||[])await n(r,A,p);u.path&&await n(s,u.path,p)},async delete(i){return S(C(i))},async deleteByTag(i){return g(r,i)},async deleteByPath(i){return g(s,i)},async clear(){await E(t,{recursive:!0,force:!0})},async keys(){return y()}}}function pt({client:t,namespace:e="what"}={}){if(!t)throw Object.assign(new Error("[what-isr] createRedisStore requires { client }"),{code:"ERR_ISR_MISSING_CLIENT"});let r=n=>`${e}:cache:${n}`,s=n=>`${e}:tag:${n}`,c=n=>`${e}:path:${n}`;function d(n,o=Date.now()){let f=n&&n.expiresAt;if(f==null||f===1/0||!Number.isFinite(f))return 0;let a=Number(n.swrWindow)||0;return Math.max(1,Math.ceil((f-o)/1e3)+a)}async function h(n){if(typeof t.scan=="function"){let o=new Set,f="0";do{let a=await t.scan(f,"MATCH",n,"COUNT",500),[g,S]=Array.isArray(a)?a:[a.cursor,a.keys];f=String(g);for(let y of S||[])o.add(y)}while(f!=="0");return[...o]}return typeof t.keys=="function"?await t.keys(n)||[]:[]}async function l(n){let o=await t.get(r(n));return o?JSON.parse(o):null}async function w(n,o){if(o){for(let f of o.tags||[])await t.srem(s(f),n);o.path&&await t.srem(c(o.path),n)}}async function m(n){let o=await t.smembers(n)||[];for(let f of o)await t.del(r(f));return await t.del(n),o}return{async get(n){return l(C(n))},async set(n,o){let f=C(n),a=await l(f);a&&await w(f,a),await t.set(r(f),JSON.stringify(o));let g=d(o);g>0&&typeof t.expire=="function"&&await t.expire(r(f),g);for(let S of o.tags||[])await t.sadd(s(S),f);o.path&&await t.sadd(c(o.path),f)},async delete(n){let o=C(n),f=await l(o);return await t.del(r(o)),await w(o,f),!!f},async deleteByTag(n){return m(s(n))},async deleteByPath(n){return m(c(n))},async clear(){for(let n of await h(`${e}:*`))await t.del(n)},async keys(){let n=`${e}:cache:`;return(await h(`${n}*`)).map(f=>f.slice(n.length))}}}async function ht(t,e={}){if(typeof t!="function")return{paths:[],fallback:!1};let r=await t(e);return{paths:r&&r.paths||[],fallback:r&&"fallback"in r?r.fallback:!1}}function yt(t,e={}){return t.replace(/[:*]([A-Za-z0-9_]+)/g,(r,s)=>{let c=e[s];return c==null?"":String(c)})}function gt(t,e){return t.some(r=>{let s=r.params||{},c=new Set([...Object.keys(s),...Object.keys(e)]);for(let d of c)if(String(s[d])!==String(e[d]))return!1;return!0})}function wt(t,e){return e?"serve":t==="blocking"?"render":t===!0?"skeleton":"notfound"}import{createHash as mt,timingSafeEqual as xt}from"node:crypto";function St(t,e){if(typeof t!="string"||typeof e!="string")return!1;let r=s=>mt("sha256").update(s).digest();return xt(r(t),r(e))}var At=100;function vt(t,e={}){let{secret:r,header:s="x-what-revalidate-secret",regenerate:c=!1,maxBatch:d=At}=e;return async function(l){let w=(l.headers||{})[s]||(l.headers||{})[s.toLowerCase()];if(!r||!St(w||"",r))return{status:401,body:{message:"Unauthorized"}};let m=l.body;if(!m||typeof m!="object")return{status:400,body:{message:"Invalid body"}};let{paths:n,tags:o}=m;if(!Array.isArray(n)&&!Array.isArray(o))return{status:400,body:{message:"Provide `paths` and/or `tags` arrays"}};let f=(Array.isArray(n)?n.length:0)+(Array.isArray(o)?o.length:0);if(f>d)return{status:400,body:{message:`Too many entries: ${f} exceeds the ${d} per-request limit`}};let a={paths:[],tags:[]};if(Array.isArray(n))for(let g of n)await t.revalidatePath(g,{regenerate:c}),a.paths.push(g);if(Array.isArray(o))for(let g of o)await t.revalidateTag(g,{regenerate:c}),a.tags.push(g);return{status:200,body:{revalidated:!0,...a}}}}function bt(t,e={}){let{maxConcurrent:r=4,random:s=Math.random,setTimer:c=setTimeout,clearTimer:d=clearTimeout,logger:h=console}=e,l=[],w=[],m=!1,n=0;function o(y){return Math.round(y*(1+s()*.1))}function f(y){y.timer=c(()=>a(y),o(y.intervalMs)),y.timer&&typeof y.timer.unref=="function"&&y.timer.unref()}function a(y){m&&(n<r?g(y):w.push(y))}async function g(y){n++;try{await t.regenerate(y.route)}catch(i){h.error?.("[what-isr] scheduled regenerate failed:",i)}finally{n--,m&&f(y),S()}}function S(){for(;m&&n<r&&w.length;)g(w.shift())}return{register(y,{intervalMs:i}){return l.push({route:y,intervalMs:i,timer:null}),this},start(){m=!0;for(let y of l)f(y);return this},stop(){m=!1;for(let y of l)y.timer!=null&&d(y.timer);return w.length=0,this},_tasks:l}}function U({zoneId:t,apiToken:e}={}){let r=`https://api.cloudflare.com/client/v4/zones/${t}/purge_cache`,s={Authorization:`Bearer ${e}`,"Content-Type":"application/json"};async function c(d){!t||!e||await fetch(r,{method:"POST",headers:s,body:JSON.stringify(d)})}return{purge:d=>c({files:d}),purgeTags:d=>c({tags:d})}}function X({serviceId:t,apiToken:e,baseUrl:r}={}){let s={"Fastly-Key":e??"",Accept:"application/json"};function c(h){if(!r)return null;let l=_(h);if(l===null)return null;try{return new URL(l,r).href}catch{return null}}async function d(h){!t||!e||await fetch(`https://api.fastly.com/service/${t}/purge/${encodeURIComponent(h)}`,{method:"POST",headers:s})}return{async purge(h){if(e)for(let l of h){let w=c(l);w&&await fetch(w,{method:"PURGE",headers:s})}},async purgeTags(h){for(let l of h)await d(l)}}}function Y({token:t,projectId:e,teamId:r}={}){let s={Authorization:`Bearer ${t}`,"Content-Type":"application/json"},c=r?`?teamId=${r}`:"";async function d(h){!t||!e||await fetch(`https://api.vercel.com/v1/projects/${e}/cache/purge${c}`,{method:"POST",headers:s,body:JSON.stringify({tags:h})})}return{purge:()=>Promise.resolve(),purgeTags:d}}export{W as buildCacheHeaders,yt as buildPath,R as cacheKey,nt as createCacheEngine,U as createCloudflareCDN,X as createFastlyCDN,dt as createFilesystemStore,at as createMemoryStore,pt as createRedisStore,vt as createRevalidateWebhook,bt as createScheduler,Y as createVercelCDN,wt as decideFallback,B as hashKey,N as isFresh,gt as isKnownParams,V as isServableStale,I as makeEntry,$ as normalizePath,q as normalizeQuery,D as normalizeVaryDeclaration,ht as resolveStaticPaths,P as resolveVary};
1
+ import{createHash as z}from"node:crypto";var L="\0",G=new Set(["fbclid","gclid","_","mc_cid","mc_eid"]),Q=["utm_"];function M(t){if(G.has(t))return!0;for(let e of Q)if(t.startsWith(e))return!0;return!1}function $(t){if(!t)return"/";let e=String(t).split("?")[0].split("#")[0];return e.length>1&&e.endsWith("/")&&(e=e.slice(0,-1)),e||"/"}function Z(t){if(!t)return[];if(typeof t=="string"){let r=t.startsWith("?")?t.slice(1):t;return r?[...new URLSearchParams(r).entries()]:[]}let e=[];for(let[r,o]of Object.entries(t))if(Array.isArray(o))for(let c of o)e.push([r,String(c)]);else e.push([r,String(o)]);return e}var T=t=>encodeURIComponent(String(t));function q(t){return Z(t).filter(([r])=>!M(r)).sort((r,o)=>r[0]===o[0]?0:r[0]<o[0]?-1:1).map(([r,o])=>`${T(r)}=${T(o)}`).join("&")}function tt(t){let e={};if(!t)return e;for(let r of String(t).split(";")){let o=r.indexOf("=");o<0||(e[r.slice(0,o).trim()]=r.slice(o+1).trim())}return e}function O(t){let e=t||{};return e.vary!=null?e.vary:(e.config||{}).vary}function D(t){if(t==null)return[];if(typeof t=="string")return t?[t]:[];if(!Array.isArray(t))return null;let e=[];for(let r of t){if(typeof r!="string"||!r)return null;e.push(r)}return e}function P(t,e){let r=D(t);if(r===null)return null;if(!r.length)return{};if(!e||typeof e!="object")return null;let o={};for(let[h,l]of Object.entries(e))o[h.toLowerCase()]=Array.isArray(l)?l.join(","):l;let c=tt(o.cookie),d={};for(let h of r){let l=String(h),w=l.indexOf(":"),m=w<0?"header":l.slice(0,w).toLowerCase(),n=w<0?l:l.slice(w+1);if(m==="cookie")d[l]=c[n]??"";else if(m==="header")d[l]=o[n.toLowerCase()]??"";else return null}return d}function et(t){return t?Object.entries(t).sort((e,r)=>e[0]<r[0]?-1:1).map(([e,r])=>`${T(e)}=${T(r)}`).join("&"):""}function R({path:t,query:e,vary:r,headers:o}={}){let c=typeof r=="string"||Array.isArray(r),d=r==null||typeof r=="object"&&!Array.isArray(r),h=c?P(r,o):d?r:null;if(h===null)throw Object.assign(new Error("[what-isr] cannot build a cache key: `vary` is declared but could not be resolved against the request (unsupported shape, or no request headers were supplied)"),{code:"ERR_ISR_VARY_UNRESOLVED"});return[$(t),q(e),et(h)].join(L)}function C(t){let e=String(t),r=e.lastIndexOf(L);if(r<0)return e;let o=e.slice(r+1);return o.includes("=")?e.slice(0,r+1)+z("sha256").update(o).digest("hex"):e}function B(t){let e=z("sha256").update(t).digest("hex");return`${e.slice(0,2)}/${e.slice(2,4)}/${e.slice(4)}`}function I(t,e={},r=Date.now()){let o=e.mode==="hybrid"?60:0,c=e.revalidate!=null,d=Number(e.revalidate??o),h=Number.isFinite(d)?d:o,l=e.swr!=null?Number(e.swr):h,w=h>0?r+h*1e3:c?r:1/0;return{html:t.html||"",head:t.head||"",state:t.state??null,tags:t.tags||e.tags||[],path:t.path||e.path,status:t.status||200,partial:!!t.partial,private:!!(t.private||t.usedRequestHeaders),renderedAt:r,maxAge:h,swrWindow:l,expiresAt:w}}function N(t,e=Date.now()){return t.expiresAt===1/0||e<t.expiresAt}function V(t,e=Date.now()){return N(t,e)?!0:t.swrWindow==null?!1:e<t.expiresAt+t.swrWindow*1e3}function rt(t){let e=new Set;for(let r of t){let o=String(r),c=o.indexOf(":"),d=c<0?"header":o.slice(0,c).toLowerCase();e.add(d==="cookie"?"Cookie":c<0?o:o.slice(c+1))}return[...e].join(", ")}function W(t={},e={},r="MISS",o){let c={"X-What-Cache":r},d=o!==void 0?o:e.vary,h=D(d),l=h===null||h.length>0;if(h&&h.length>0&&(c.Vary=rt(h)),!((t.status==null||t.status===200)&&!l&&!t.private&&((t.maxAge??0)>0||e.mode==="static"||e.mode==="hybrid")&&e.mode!=="server"))return c["Cache-Control"]="private, no-store",c;let n=t.partial?0:t.maxAge||0,s=t.swrWindow!=null?t.swrWindow:n;return c["Cache-Control"]=`public, s-maxage=${n}, stale-while-revalidate=${s}`,t.tags&&t.tags.length&&(c["Cache-Tag"]=t.tags.join(","),c["Surrogate-Key"]=t.tags.join(" ")),c}function _(t){if(typeof t!="string"||!t.startsWith("/")||/^[/\\]{2}/.test(t)||t.includes("\\"))return null;try{let e=new URL(t,"http://localhost");return e.origin!=="http://localhost"?null:e.pathname+e.search}catch{return null}}function nt({store:t,render:e,cdn:r,now:o=Date.now,logger:c=console}={}){let d=new Map;function h(a){return P(O(a),a.varyHeaders||a.headers)}function l(a){let g=h(a);if(g===null)throw Object.assign(new Error("[what-isr] route declares `vary` but the adapter supplied no request headers; refusing to cache"),{code:"ERR_ISR_VARY_NO_HEADERS"});return R({path:a.path,query:a.query,vary:g})}function w(a,g,S){let y=d.get(a);if(y)return y;let i=S||e,u=(async()=>{let p=await i(g,{}),x=I({...p,path:g.path},g.config||{},o());return x.status===200&&!x.private&&await t.set(a,x),x})().finally(()=>d.delete(a));return d.set(a,u),u}function m(a,g,S){return{html:a.html,head:a.head,state:a.state,status:a.status||200,cacheStatus:g,headers:W(a,S.config||{},g,O(S))}}async function n(a,g){let S=a.config||{},y=h(a);if(S.mode==="server"||y===null){y===null&&c.warn?.(`[what-isr] ${a.path} declares \`vary\` but no request headers were supplied; bypassing the cache`);let A=await(g||e)(a,{}),b=I({...A,path:a.path,private:y===null||A.private},S,o());return m(b,"BYPASS",a)}let i=R({path:a.path,query:a.query,vary:y}),u=await t.get(i),p=o();if(u&&N(u,p))return m(u,"HIT",a);if(u&&V(u,p))return w(i,a,g).catch(A=>c.error?.("[what-isr] background regenerate failed:",A)),m(u,"STALE",a);if(u&&S.onMiss==="stale-if-error")try{let A=await w(i,a,g);return m(A,"MISS",a)}catch(A){return c.error?.("[what-isr] regenerate failed, serving stale:",A),m(u,"STALE",a)}let x=await w(i,a,g);return m(x,"MISS",a)}async function s(a,{regenerate:g=!1,routeResolver:S}={}){let y=_(typeof a=="string"?a:"");if(y===null)return c.warn?.(`[what-isr] refusing to revalidate a non-local path: ${a}`),[];let i=$(y),u=await t.deleteByPath(i);if(r&&r.purge&&await r.purge([y]),g){let p=S?S(i):{path:i,query:{},config:{}};try{await w(l(p),p)}catch(x){c.error?.("[what-isr] regen after revalidatePath failed:",x)}}return u}async function f(a,{regenerate:g=!1,routeResolver:S}={}){let y=await t.deleteByTag(a);if(r&&r.purgeTags&&await r.purgeTags([a]),g&&S)for(let i of y){let u=S(i);if(u)try{await w(l(u),u)}catch{}}return y}return{handle:n,regenerate:a=>w(l(a),a),revalidatePath:s,revalidateTag:f,keyFor:l,store:t,_inFlight:d,_now:o,_cdn:r}}function at({maxEntries:t=1e3}={}){let e=new Map,r=new Map,o=new Map;function c(n,s,f){if(s==null)return;let a=n.get(s);a||n.set(s,a=new Set),a.add(f)}function d(n,s,f){if(s==null)return;let a=n.get(s);a&&(a.delete(f),a.size===0&&n.delete(s))}function h(n,s){if(s.tags)for(let f of s.tags)c(r,f,n);c(o,s.path,n)}function l(n,s){if(s.tags)for(let f of s.tags)d(r,f,n);d(o,s.path,n)}function w(n){let s=e.get(n);return s?(e.delete(n),l(n,s),!0):!1}function m(n,s){let f=n.get(s);if(!f)return[];let a=[...f];for(let g of a)w(g);return a}return{async get(n){let s=e.get(n);return s?(e.delete(n),e.set(n,s),s):null},async set(n,s){for(e.has(n)&&l(n,e.get(n)),e.set(n,s),h(n,s);e.size>t;){let f=e.keys().next().value;w(f)}},async delete(n){return w(n)},async deleteByTag(n){return m(r,n)},async deleteByPath(n){return m(o,n)},async clear(){e.clear(),r.clear(),o.clear()},async keys(){return[...e.keys()]}}}import{mkdir as st,writeFile as ot,readFile as it,rename as ct,rm as E,readdir as j,stat as J}from"node:fs/promises";import{join as v,dirname as ut}from"node:path";import{createHash as ft,randomBytes as lt}from"node:crypto";function H(t){return ft("sha256").update(String(t)).digest("hex")}function dt({dir:t}){let e=v(t,"entries"),r=v(t,"tags"),o=v(t,"paths"),c=i=>v(e,B(i)+".json"),d=(i,u)=>v(i,H(u)),h=(i,u,p)=>v(d(i,u),H(p)+".json"),l=(i,u)=>v(i,H(u)+".json");async function w(i,u){await st(ut(i),{recursive:!0});let p=`${i}.${process.pid}.${lt(6).toString("hex")}.tmp`;await ot(p,u),await ct(p,i)}async function m(i){try{return JSON.parse(await it(i,"utf8"))}catch{return null}}async function n(i,u,p){await w(h(i,u,p),JSON.stringify(p))}async function s(i,u,p){await E(h(i,u,p),{force:!0});let x=await f(i,u);if(!x)return;let A=x.filter(b=>b!==p);A.length!==x.length&&(A.length?await w(l(i,u),JSON.stringify(A)):await E(l(i,u),{force:!0}))}async function f(i,u){let p=await m(l(i,u));return Array.isArray(p)?p.filter(x=>typeof x=="string"):null}async function a(i,u){let p=new Set(await f(i,u)||[]),x=d(i,u),A;try{A=await j(x)}catch{return[...p]}for(let b of A){if(!b.endsWith(".json"))continue;let k=await m(v(x,b));typeof k=="string"&&p.add(k)}return[...p]}async function g(i,u){let p=await a(i,u);for(let x of p)await S(x);return await E(d(i,u),{recursive:!0,force:!0}),await E(l(i,u),{force:!0}),p}async function S(i){let u=await m(c(i));if(!u)return!1;await E(c(i),{force:!0});let p=u.entry||{};for(let x of p.tags||[])await s(r,x,i);return p.path&&await s(o,p.path,i),!0}async function y(){let i=[],u;try{u=await j(e)}catch{return i}for(let p of u){let x=v(e,p),A;try{A=await J(x)}catch{continue}if(A.isDirectory())for(let b of await j(x)){let k=v(x,b);try{if(!(await J(k)).isDirectory())continue}catch{continue}for(let K of await j(k)){if(!K.endsWith(".json"))continue;let F=await m(v(k,K));F&&F.key!=null&&i.push(F.key)}}}return i}return{async get(i){let u=await m(c(C(i)));return u?u.entry:null},async set(i,u){let p=C(i),x=await m(c(p));if(x&&x.entry){for(let A of x.entry.tags||[])await s(r,A,p);x.entry.path&&await s(o,x.entry.path,p)}await w(c(p),JSON.stringify({key:p,entry:u}));for(let A of u.tags||[])await n(r,A,p);u.path&&await n(o,u.path,p)},async delete(i){return S(C(i))},async deleteByTag(i){return g(r,i)},async deleteByPath(i){return g(o,i)},async clear(){await E(t,{recursive:!0,force:!0})},async keys(){return y()}}}function pt({client:t,namespace:e="what"}={}){if(!t)throw Object.assign(new Error("[what-isr] createRedisStore requires { client }"),{code:"ERR_ISR_MISSING_CLIENT"});let r=n=>`${e}:cache:${n}`,o=n=>`${e}:tag:${n}`,c=n=>`${e}:path:${n}`;function d(n,s=Date.now()){let f=n&&n.expiresAt;if(f==null||f===1/0||!Number.isFinite(f))return 0;let a=Number(n.swrWindow)||0;return Math.max(1,Math.ceil((f-s)/1e3)+a)}async function h(n){if(typeof t.scan=="function"){let s=new Set,f="0";do{let a=await t.scan(f,"MATCH",n,"COUNT",500),[g,S]=Array.isArray(a)?a:[a.cursor,a.keys];f=String(g);for(let y of S||[])s.add(y)}while(f!=="0");return[...s]}return typeof t.keys=="function"?await t.keys(n)||[]:[]}async function l(n){let s=await t.get(r(n));return s?JSON.parse(s):null}async function w(n,s){if(s){for(let f of s.tags||[])await t.srem(o(f),n);s.path&&await t.srem(c(s.path),n)}}async function m(n){let s=await t.smembers(n)||[];for(let f of s)await t.del(r(f));return await t.del(n),s}return{async get(n){return l(C(n))},async set(n,s){let f=C(n),a=await l(f);a&&await w(f,a),await t.set(r(f),JSON.stringify(s));let g=d(s);g>0&&typeof t.expire=="function"&&await t.expire(r(f),g);for(let S of s.tags||[])await t.sadd(o(S),f);s.path&&await t.sadd(c(s.path),f)},async delete(n){let s=C(n),f=await l(s);return await t.del(r(s)),await w(s,f),!!f},async deleteByTag(n){return m(o(n))},async deleteByPath(n){return m(c(n))},async clear(){for(let n of await h(`${e}:*`))await t.del(n)},async keys(){let n=`${e}:cache:`;return(await h(`${n}*`)).map(f=>f.slice(n.length))}}}async function ht(t,e={}){if(typeof t!="function")return{paths:[],fallback:!1};let r=await t(e);return{paths:r&&r.paths||[],fallback:r&&"fallback"in r?r.fallback:!1}}function yt(t,e={}){return t.replace(/[:*]([A-Za-z0-9_]+)/g,(r,o)=>{let c=e[o];return c==null?"":String(c)})}function gt(t,e){return t.some(r=>{let o=r.params||{},c=new Set([...Object.keys(o),...Object.keys(e)]);for(let d of c)if(String(o[d])!==String(e[d]))return!1;return!0})}function wt(t,e){return e?"serve":t==="blocking"?"render":t===!0?"skeleton":"notfound"}import{createHash as mt,timingSafeEqual as xt}from"node:crypto";function St(t,e){if(typeof t!="string"||typeof e!="string")return!1;let r=o=>mt("sha256").update(o).digest();return xt(r(t),r(e))}var At=100;function vt(t,e={}){let{secret:r,header:o="x-what-revalidate-secret",regenerate:c=!1,maxBatch:d=At}=e;return async function(l){let w=(l.headers||{})[o]||(l.headers||{})[o.toLowerCase()];if(!r||!St(w||"",r))return{status:401,body:{message:"Unauthorized"}};let m=l.body;if(!m||typeof m!="object")return{status:400,body:{message:"Invalid body"}};let{paths:n,tags:s}=m;if(!Array.isArray(n)&&!Array.isArray(s))return{status:400,body:{message:"Provide `paths` and/or `tags` arrays"}};let f=(Array.isArray(n)?n.length:0)+(Array.isArray(s)?s.length:0);if(f>d)return{status:400,body:{message:`Too many entries: ${f} exceeds the ${d} per-request limit`}};let a={paths:[],tags:[]};if(Array.isArray(n))for(let g of n)await t.revalidatePath(g,{regenerate:c}),a.paths.push(g);if(Array.isArray(s))for(let g of s)await t.revalidateTag(g,{regenerate:c}),a.tags.push(g);return{status:200,body:{revalidated:!0,...a}}}}function bt(t,e={}){let{maxConcurrent:r=4,random:o=Math.random,setTimer:c=setTimeout,clearTimer:d=clearTimeout,logger:h=console}=e,l=[],w=[],m=!1,n=0;function s(y){return Math.round(y*(1+o()*.1))}function f(y){y.timer=c(()=>a(y),s(y.intervalMs)),y.timer&&typeof y.timer.unref=="function"&&y.timer.unref()}function a(y){m&&(n<r?g(y):w.push(y))}async function g(y){n++;try{await t.regenerate(y.route)}catch(i){h.error?.("[what-isr] scheduled regenerate failed:",i)}finally{n--,m&&f(y),S()}}function S(){for(;m&&n<r&&w.length;)g(w.shift())}return{register(y,{intervalMs:i}){return l.push({route:y,intervalMs:i,timer:null}),this},start(){m=!0;for(let y of l)f(y);return this},stop(){m=!1;for(let y of l)y.timer!=null&&d(y.timer);return w.length=0,this},_tasks:l}}function U({zoneId:t,apiToken:e}={}){let r=`https://api.cloudflare.com/client/v4/zones/${t}/purge_cache`,o={Authorization:`Bearer ${e}`,"Content-Type":"application/json"};async function c(d){!t||!e||await fetch(r,{method:"POST",headers:o,body:JSON.stringify(d)})}return{purge:d=>c({files:d}),purgeTags:d=>c({tags:d})}}function X({serviceId:t,apiToken:e,baseUrl:r}={}){let o={"Fastly-Key":e??"",Accept:"application/json"};function c(h){if(!r)return null;let l=_(h);if(l===null)return null;try{return new URL(l,r).href}catch{return null}}async function d(h){!t||!e||await fetch(`https://api.fastly.com/service/${t}/purge/${encodeURIComponent(h)}`,{method:"POST",headers:o})}return{async purge(h){if(e)for(let l of h){let w=c(l);w&&await fetch(w,{method:"PURGE",headers:o})}},async purgeTags(h){for(let l of h)await d(l)}}}function Y({token:t,projectId:e,teamId:r}={}){let o={Authorization:`Bearer ${t}`,"Content-Type":"application/json"},c=r?`?teamId=${r}`:"";async function d(h){!t||!e||await fetch(`https://api.vercel.com/v1/projects/${e}/cache/purge${c}`,{method:"POST",headers:o,body:JSON.stringify({tags:h})})}return{purge:()=>Promise.resolve(),purgeTags:d}}export{W as buildCacheHeaders,yt as buildPath,R as cacheKey,nt as createCacheEngine,U as createCloudflareCDN,X as createFastlyCDN,dt as createFilesystemStore,at as createMemoryStore,pt as createRedisStore,vt as createRevalidateWebhook,bt as createScheduler,Y as createVercelCDN,wt as decideFallback,B as hashKey,N as isFresh,gt as isKnownParams,V as isServableStale,I as makeEntry,$ as normalizePath,q as normalizeQuery,D as normalizeVaryDeclaration,ht as resolveStaticPaths,P as resolveVary};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "what-isr",
3
- "version": "0.13.3",
3
+ "version": "0.13.5",
4
4
  "description": "What Framework - Origin-first ISR cache engine: stale-while-revalidate, on-demand & poll regeneration, no CDN required",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -30,7 +30,7 @@
30
30
  "author": "ZVN DEV (https://zvndev.com)",
31
31
  "license": "MIT",
32
32
  "peerDependencies": {
33
- "what-server": "^0.13.3"
33
+ "what-server": "^0.13.5"
34
34
  },
35
35
  "peerDependenciesMeta": {
36
36
  "what-server": {
package/src/headers.js CHANGED
@@ -18,8 +18,8 @@ function varyHeaderNames(vary) {
18
18
  }
19
19
 
20
20
  /**
21
- * @param {object} entry { maxAge, swrWindow, tags?, partial?, private?, status? }
22
- * @param {object} config route page config { mode, vary?, ... }
21
+ * @param {{ maxAge?: number, swrWindow?: number, tags?: string[], partial?: boolean, private?: boolean, status?: number }} [entry]
22
+ * @param {{ mode?: string, vary?: any }} [config]
23
23
  * @param {string} cacheStatus HIT | STALE | MISS | BYPASS
24
24
  * @param {string[]} [vary] The declared vary the cache key used. Callers that
25
25
  * omit it fall back to config.vary, which is only correct when the routeMatch
@@ -49,7 +49,7 @@ export function buildCacheHeaders(entry = {}, config = {}, cacheStatus = 'MISS',
49
49
  const cacheable = isOk
50
50
  && !varies
51
51
  && !entry.private
52
- && (entry.maxAge > 0 || config.mode === 'static' || config.mode === 'hybrid')
52
+ && ((entry.maxAge ?? 0) > 0 || config.mode === 'static' || config.mode === 'hybrid')
53
53
  && config.mode !== 'server';
54
54
 
55
55
  if (!cacheable) {
package/src/key.js CHANGED
@@ -60,11 +60,13 @@ function queryToEntries(query) {
60
60
  // and must be different keys.
61
61
  const enc = (s) => encodeURIComponent(String(s));
62
62
 
63
- /** Normalize a query (object or string) → sorted "k=v&k=v", tracking params dropped. */
63
+ /** Normalize a query → key-sorted "k=v&k=v", preserving repeated value order. */
64
64
  export function normalizeQuery(query) {
65
65
  const entries = queryToEntries(query)
66
66
  .filter(([k]) => !shouldStrip(k))
67
- .sort((a, b) => (a[0] === b[0] ? (a[1] < b[1] ? -1 : 1) : a[0] < b[0] ? -1 : 1));
67
+ // Stable sort groups names without changing the order a loader observes
68
+ // for repeated values; sorting values would cache a different request.
69
+ .sort((a, b) => (a[0] === b[0] ? 0 : a[0] < b[0] ? -1 : 1));
68
70
  return entries.map(([k, v]) => `${enc(k)}=${enc(v)}`).join('&');
69
71
  }
70
72