what-isr 0.12.4 → 0.13.0

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 H}from"node:crypto";var L="\0",G=new Set(["fbclid","gclid","_","mc_cid","mc_eid"]),Q=["utm_"];function Z(t){if(G.has(t))return!0;for(let e of Q)if(t.startsWith(e))return!0;return!1}function P(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 M(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 M(t).filter(([r])=>!Z(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 _(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 j(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),g=l.indexOf(":"),m=g<0?"header":l.slice(0,g).toLowerCase(),n=g<0?l:l.slice(g+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 E({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?j(r,s):d?r:null;if(h===null)throw 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)");return[P(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)+H("sha256").update(s).digest("hex"):e}function R(t){let e=H("sha256").update(t).digest("hex");return`${e.slice(0,2)}/${e.slice(2,4)}/${e.slice(4)}`}function F(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,g=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:g}}function N(t,e=Date.now()){return t.expiresAt===1/0||e<t.expiresAt}function W(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 K(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 I(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 j(_(a),a.varyHeaders||a.headers)}function l(a){let w=h(a);if(w===null)throw new Error("[what-isr] route declares `vary` but the adapter supplied no request headers; refusing to cache");return E({path:a.path,query:a.query,vary:w})}function g(a,w,S){let y=d.get(a);if(y)return y;let i=S||e,u=(async()=>{let p=await i(w,{}),x=F({...p,path:w.path},w.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,w,S){return{html:a.html,head:a.head,state:a.state,status:a.status||200,cacheStatus:w,headers:K(a,S.config||{},w,_(S))}}async function n(a,w){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 v=await(w||e)(a,{}),b=F({...v,path:a.path,private:y===null||v.private},S,s());return m(b,"BYPASS",a)}let i=E({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&&W(u,p))return g(i,a,w).catch(v=>c.error?.("[what-isr] background regenerate failed:",v)),m(u,"STALE",a);if(u&&S.onMiss==="stale-if-error")try{let v=await g(i,a,w);return m(v,"MISS",a)}catch(v){return c.error?.("[what-isr] regenerate failed, serving stale:",v),m(u,"STALE",a)}let x=await g(i,a,w);return m(x,"MISS",a)}async function o(a,{regenerate:w=!1,routeResolver:S}={}){let y=I(typeof a=="string"?a:"");if(y===null)return c.warn?.(`[what-isr] refusing to revalidate a non-local path: ${a}`),[];let i=P(y),u=await t.deleteByPath(i);if(r&&r.purge&&await r.purge([y]),w){let p=S?S(i):{path:i,query:{},config:{}};try{await g(l(p),p)}catch(x){c.error?.("[what-isr] regen after revalidatePath failed:",x)}}return u}async function f(a,{regenerate:w=!1,routeResolver:S}={}){let y=await t.deleteByTag(a);if(r&&r.purgeTags&&await r.purgeTags([a]),w&&S)for(let i of y){let u=S(i);if(u)try{await g(l(u),u)}catch{}}return y}return{handle:n,regenerate:a=>g(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 g(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 w of a)g(w);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;g(f)}},async delete(n){return g(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 $,readdir as O,stat as J}from"node:fs/promises";import{join as A,dirname as ut}from"node:path";import{createHash as ft,randomBytes as lt}from"node:crypto";function V(t){return ft("sha256").update(String(t)).digest("hex")}function dt({dir:t}){let e=A(t,"entries"),r=A(t,"tags"),s=A(t,"paths"),c=i=>A(e,R(i)+".json"),d=(i,u)=>A(i,V(u)),h=(i,u,p)=>A(d(i,u),V(p)+".json"),l=(i,u)=>A(i,V(u)+".json");async function g(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 g(h(i,u,p),JSON.stringify(p))}async function o(i,u,p){await $(h(i,u,p),{force:!0});let x=await f(i,u);if(!x)return;let v=x.filter(b=>b!==p);v.length!==x.length&&(v.length?await g(l(i,u),JSON.stringify(v)):await $(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),v;try{v=await O(x)}catch{return[...p]}for(let b of v){if(!b.endsWith(".json"))continue;let k=await m(A(x,b));typeof k=="string"&&p.add(k)}return[...p]}async function w(i,u){let p=await a(i,u);for(let x of p)await S(x);return await $(d(i,u),{recursive:!0,force:!0}),await $(l(i,u),{force:!0}),p}async function S(i){let u=await m(c(i));if(!u)return!1;await $(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 O(e)}catch{return i}for(let p of u){let x=A(e,p),v;try{v=await J(x)}catch{continue}if(v.isDirectory())for(let b of await O(x)){let k=A(x,b);try{if(!(await J(k)).isDirectory())continue}catch{continue}for(let z of await O(k)){if(!z.endsWith(".json"))continue;let B=await m(A(k,z));B&&B.key!=null&&i.push(B.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 v of x.entry.tags||[])await o(r,v,p);x.entry.path&&await o(s,x.entry.path,p)}await g(c(p),JSON.stringify({key:p,entry:u}));for(let v of u.tags||[])await n(r,v,p);u.path&&await n(s,u.path,p)},async delete(i){return S(C(i))},async deleteByTag(i){return w(r,i)},async deleteByPath(i){return w(s,i)},async clear(){await $(t,{recursive:!0,force:!0})},async keys(){return y()}}}function pt({client:t,namespace:e="what"}={}){if(!t)throw new Error("[what-isr] createRedisStore requires { 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),[w,S]=Array.isArray(a)?a:[a.cursor,a.keys];f=String(w);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 g(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 g(f,a),await t.set(r(f),JSON.stringify(o));let w=d(o);w>0&&typeof t.expire=="function"&&await t.expire(r(f),w);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 g(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 wt(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 gt(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 vt=100;function At(t,e={}){let{secret:r,header:s="x-what-revalidate-secret",regenerate:c=!1,maxBatch:d=vt}=e;return async function(l){let g=(l.headers||{})[s]||(l.headers||{})[s.toLowerCase()];if(!r||!St(g||"",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 w of n)await t.revalidatePath(w,{regenerate:c}),a.paths.push(w);if(Array.isArray(o))for(let w of o)await t.revalidateTag(w,{regenerate:c}),a.tags.push(w);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=[],g=[],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?w(y):g.push(y))}async function w(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&&g.length;)w(g.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 g.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=I(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 g=c(l);g&&await fetch(g,{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{K as buildCacheHeaders,yt as buildPath,E as cacheKey,nt as createCacheEngine,U as createCloudflareCDN,X as createFastlyCDN,dt as createFilesystemStore,at as createMemoryStore,pt as createRedisStore,At as createRevalidateWebhook,bt as createScheduler,Y as createVercelCDN,gt as decideFallback,R as hashKey,N as isFresh,wt as isKnownParams,W as isServableStale,F as makeEntry,P as normalizePath,q as normalizeQuery,D as normalizeVaryDeclaration,ht as resolveStaticPaths,j 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,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};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "what-isr",
3
- "version": "0.12.4",
3
+ "version": "0.13.0",
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.12.4"
33
+ "what-server": "^0.13.0"
34
34
  },
35
35
  "peerDependenciesMeta": {
36
36
  "what-server": {
@@ -1,6 +1,11 @@
1
1
  // Cloudflare CDN adapter — edge purge by URL or cache-tag (Enterprise tag purge).
2
2
  // Uses global fetch (Node 18+); credentials injected by the deploy adapter.
3
3
 
4
+ /**
5
+ * @param {object} [credentials]
6
+ * @param {string} [credentials.zoneId]
7
+ * @param {string} [credentials.apiToken]
8
+ */
4
9
  export function createCloudflareCDN({ zoneId, apiToken } = {}) {
5
10
  const endpoint = `https://api.cloudflare.com/client/v4/zones/${zoneId}/purge_cache`;
6
11
  const headers = { Authorization: `Bearer ${apiToken}`, 'Content-Type': 'application/json' };
package/src/cdn/fastly.js CHANGED
@@ -2,8 +2,15 @@
2
2
 
3
3
  import { safeLocalPath } from '../local-path.js';
4
4
 
5
+ /**
6
+ * @param {object} [credentials]
7
+ * @param {string} [credentials.serviceId]
8
+ * @param {string} [credentials.apiToken]
9
+ * @param {string} [credentials.baseUrl]
10
+ */
5
11
  export function createFastlyCDN({ serviceId, apiToken, baseUrl } = {}) {
6
- const headers = { 'Fastly-Key': apiToken, Accept: 'application/json' };
12
+ /** @type {Record<string, string>} */
13
+ const headers = { 'Fastly-Key': apiToken ?? '', Accept: 'application/json' };
7
14
 
8
15
  // A URL purge is a request to an arbitrary host carrying the Fastly API key,
9
16
  // so only local paths resolved against the configured baseUrl are ever sent.
package/src/cdn/vercel.js CHANGED
@@ -2,6 +2,12 @@
2
2
  // a thin on-demand purge via the project API when a token is supplied; it
3
3
  // no-ops (header-only mode) without one.
4
4
 
5
+ /**
6
+ * @param {object} [credentials]
7
+ * @param {string} [credentials.token]
8
+ * @param {string} [credentials.projectId]
9
+ * @param {string} [credentials.teamId]
10
+ */
5
11
  export function createVercelCDN({ token, projectId, teamId } = {}) {
6
12
  const headers = { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' };
7
13
  const qs = teamId ? `?teamId=${teamId}` : '';
package/src/isr.js CHANGED
@@ -13,6 +13,14 @@ import { makeEntry, isFresh, isServableStale } from './stores/store-interface.js
13
13
  import { buildCacheHeaders } from './headers.js';
14
14
  import { safeLocalPath } from './local-path.js';
15
15
 
16
+ /**
17
+ * @param {object} [options]
18
+ * @param {any} [options.store] cache store (see stores/store-interface.js)
19
+ * @param {(routeMatch: any, ctx?: any) => any} [options.render] injected by the adapter
20
+ * @param {any} [options.cdn] optional CDN adapter for edge fan-out
21
+ * @param {() => number} [options.now]
22
+ * @param {any} [options.logger]
23
+ */
16
24
  export function createCacheEngine({ store, render, cdn, now = Date.now, logger = console } = {}) {
17
25
  const inFlight = new Map(); // key -> Promise<entry> (dedupe)
18
26
 
@@ -26,7 +34,11 @@ export function createCacheEngine({ store, render, cdn, now = Date.now, logger =
26
34
  function keyFor(routeMatch) {
27
35
  const vary = varyFor(routeMatch);
28
36
  if (vary === null) {
29
- throw new Error('[what-isr] route declares `vary` but the adapter supplied no request headers; refusing to cache');
37
+ // ERROR_CODES.ISR_VARY_NO_HEADERS
38
+ throw Object.assign(
39
+ new Error('[what-isr] route declares `vary` but the adapter supplied no request headers; refusing to cache'),
40
+ { code: 'ERR_ISR_VARY_NO_HEADERS' },
41
+ );
30
42
  }
31
43
  return cacheKey({ path: routeMatch.path, query: routeMatch.query, vary });
32
44
  }
@@ -113,6 +125,10 @@ export function createCacheEngine({ store, render, cdn, now = Date.now, logger =
113
125
 
114
126
  // --- On-demand invalidation (origin purge + optional CDN fan-out) ---
115
127
 
128
+ /**
129
+ * @param {string} path
130
+ * @param {{ regenerate?: boolean, routeResolver?: (path: string) => any }} [options]
131
+ */
116
132
  async function revalidatePath(path, { regenerate: regen = false, routeResolver } = {}) {
117
133
  // Never let a caller-supplied target reach the CDN adapter: a purge carries
118
134
  // the CDN API token, so an absolute URL would exfiltrate it (SSRF).
@@ -138,6 +154,10 @@ export function createCacheEngine({ store, render, cdn, now = Date.now, logger =
138
154
  return deleted;
139
155
  }
140
156
 
157
+ /**
158
+ * @param {string} tag
159
+ * @param {{ regenerate?: boolean, routeResolver?: (key: string) => any }} [options]
160
+ */
141
161
  async function revalidateTag(tag, { regenerate: regen = false, routeResolver } = {}) {
142
162
  const deleted = await store.deleteByTag(tag);
143
163
  if (cdn && cdn.purgeTags) await cdn.purgeTags([tag]);
package/src/key.js CHANGED
@@ -157,8 +157,16 @@ function varyString(vary) {
157
157
  .join('&');
158
158
  }
159
159
 
160
- /** Build the canonical cache key for a request. `vary` may be a resolved
161
- * name -> value map, or a declared list resolved here against `headers`. */
160
+ /**
161
+ * Build the canonical cache key for a request. `vary` may be a resolved
162
+ * name -> value map, or a declared list resolved here against `headers`.
163
+ *
164
+ * @param {object} [request]
165
+ * @param {string} [request.path]
166
+ * @param {string|Record<string, any>} [request.query]
167
+ * @param {string|string[]|Record<string, any>} [request.vary]
168
+ * @param {Record<string, any>} [request.headers]
169
+ */
162
170
  export function cacheKey({ path, query, vary, headers } = {}) {
163
171
  // A string or an array is a DECLARATION and has to be resolved against the
164
172
  // request. A plain object is an already-resolved name -> value map (what the
@@ -169,7 +177,11 @@ export function cacheKey({ path, query, vary, headers } = {}) {
169
177
  const isResolvedMap = vary == null || (typeof vary === 'object' && !Array.isArray(vary));
170
178
  const resolved = isDeclaration ? resolveVary(vary, headers) : (isResolvedMap ? vary : null);
171
179
  if (resolved === null) {
172
- throw 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)');
180
+ // ERROR_CODES.ISR_VARY_UNRESOLVED
181
+ throw Object.assign(
182
+ 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)'),
183
+ { code: 'ERR_ISR_VARY_UNRESOLVED' },
184
+ );
173
185
  }
174
186
  return [normalizePath(path), normalizeQuery(query), varyString(resolved)].join(KEY_SEP);
175
187
  }
@@ -5,8 +5,18 @@
5
5
 
6
6
  import { redactVary } from '../key.js';
7
7
 
8
+ /**
9
+ * @param {object} [options]
10
+ * @param {any} [options.client] ioredis / node-redis shaped client
11
+ * @param {string} [options.namespace]
12
+ */
8
13
  export function createRedisStore({ client, namespace = 'what' } = {}) {
9
- if (!client) throw new Error('[what-isr] createRedisStore requires { client }');
14
+ // ERROR_CODES.ISR_MISSING_CLIENT
15
+ if (!client) {
16
+ throw Object.assign(new Error('[what-isr] createRedisStore requires { client }'), {
17
+ code: 'ERR_ISR_MISSING_CLIENT',
18
+ });
19
+ }
10
20
 
11
21
  const ck = (key) => `${namespace}:cache:${key}`;
12
22
  const tk = (tag) => `${namespace}:tag:${tag}`;
package/src/webhook.js CHANGED
@@ -17,9 +17,14 @@ function safeEqual(a, b) {
17
17
  const MAX_BATCH = 100;
18
18
 
19
19
  /**
20
- * @param engine cache engine (revalidatePath / revalidateTag)
21
- * @param options { secret, header='x-what-revalidate-secret', regenerate=false, maxBatch=100 }
22
- * @returns async (reqLike:{headers, body:{paths?,tags?}}) -> { status, body }
20
+ * @param {{ revalidatePath: Function, revalidateTag: Function }} engine cache engine
21
+ * @param {object} [options]
22
+ * @param {string} [options.secret] shared secret the caller must present
23
+ * @param {string} [options.header] header carrying the secret
24
+ * @param {boolean} [options.regenerate] re-render each invalidated entry
25
+ * @param {number} [options.maxBatch] cap on paths + tags per request
26
+ * @returns {(reqLike: { headers?: Record<string, any>, body?: any }) =>
27
+ * Promise<{ status: number, body: any }>}
23
28
  */
24
29
  export function createRevalidateWebhook(engine, options = {}) {
25
30
  const { secret, header = 'x-what-revalidate-secret', regenerate = false, maxBatch = MAX_BATCH } = options;
@@ -46,6 +51,7 @@ export function createRevalidateWebhook(engine, options = {}) {
46
51
  return { status: 400, body: { message: `Too many entries: ${total} exceeds the ${maxBatch} per-request limit` } };
47
52
  }
48
53
 
54
+ /** @type {{ paths: any[], tags: any[] }} */
49
55
  const revalidated = { paths: [], tags: [] };
50
56
  if (Array.isArray(paths)) {
51
57
  for (const p of paths) {