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 +1 -1
- package/package.json +2 -2
- package/src/cdn/cloudflare.js +5 -0
- package/src/cdn/fastly.js +8 -1
- package/src/cdn/vercel.js +6 -0
- package/src/isr.js +21 -1
- package/src/key.js +15 -3
- package/src/stores/redis-store.js +11 -1
- package/src/webhook.js +9 -3
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.
|
|
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.
|
|
33
|
+
"what-server": "^0.13.0"
|
|
34
34
|
},
|
|
35
35
|
"peerDependenciesMeta": {
|
|
36
36
|
"what-server": {
|
package/src/cdn/cloudflare.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
161
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
21
|
-
* @param
|
|
22
|
-
* @
|
|
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) {
|