On-Demand Purge Engine (cache-revalidate.js)
Understand the programmatic cache invalidation API for purging and rebuilding paths and custom cache tags on-demand.
Key File Location: ./dinou/core/cache-revalidate.js💡 Overview
While Background ISR updates pages lazily on user visits, On-Demand Revalidation allows developers to force-purge and rebuild page caches immediately. This is crucial for blogs, e-commerce stores, and CMS integrations where database updates must reflect instantly to visitors without waiting for cache timers to tick down.
📊 Revalidation Flow
The diagram below illustrates how path-based updates differ from the recursive tag-based invalidation search:
⚡ Path Invalidation (revalidatePath)
Invoking revalidatePath("/some/route") normalizes the target path and immediately runs the build sequence:
- Relative Path Resolution: If the path is relative (e.g.
"./details"), the engine accessesgetContext(). It inspects the HTTPRefererheader to resolve the caller's active location and translates the path into an absolute system route. - Bailout Checking: The builder renders the path inside a compiler context. If the page performs database calls that read dynamic headers or cookies during this run, a bailout triggers, and static output generation skips.
🏷️ Tag Invalidation (revalidateTag)
Often, a database item spans multiple pages (e.g. a product card appears in the homepage, catalog, and product page). Tag-based invalidation allows purging all related pages with a single identifier:
- Fs Meta-Walking: The
walkMetadataFiles()function crawls thedist2/directory recursively, collecting allmetadata.jsonfiles. - Tag Validation: For each file, it checks if the
tagsarray (generated during static compilation) contains the queried tag string. - Path Extraction & Build: Resolves the folder path of matches back to system routes and triggers
revalidatePath(). - Concurred Awaiting: Promisifies all rebuild tasks and resolves them in parallel using
Promise.all().
🎯 Public API Entry Points (Where is it called?)
Developers do not invoke core/cache-revalidate.js directly. Instead, Dinou exposes these functions as part of its public API through the main package imports (CommonJS and ES Modules):
- CommonJS:
require("dinou/server") - ES Modules:
import { revalidatePath, revalidateTag } from "dinou/server"
When imported, the entry points dynamically load cache-revalidate.js and proxy the arguments to the internal handlers:
// Inside dinou/server.js (Public CJS Entry Point)
module.exports = {
revalidatePath: async function (path) {
const { revalidatePath: fn } = require("./core/cache-revalidate.js");
return fn(path);
},
revalidateTag: async function (tag) {
const { revalidateTag: fn } = require("./core/cache-revalidate.js");
return fn(tag);
},
};Revalidation APIs are mutative actions and should only be invoked inside Server Functions, custom Express route handlers (such as webhook listeners), or standalone Node.js cron and synchronization scripts.
⚙️ Complete Code Walkthrough
Below is the full, complete code of cache-revalidate.js:
const path = require("path");
const fs = require("fs").promises;
const { existsSync, copyFileSync } = require("fs");
const generateStaticPage = require("./generate-static-page");
const { buildStaticPage } = require("./build-static-pages");
const generateStaticRSC = require("./generate-static-rsc");
const { safeRename } = require("./safe-rename");
const { updateStatus } = require("./status-manifest");
const { getContext } = require("./request-context");
const { resolveRelativeUrl } = require("./url-resolver");
// 1. Recursive helper to scan cache folder
async function walkMetadataFiles(dir, fileList = []) {
try {
const files = await fs.readdir(dir);
for (const file of files) {
const filePath = path.join(dir, file);
const stat = await fs.stat(filePath);
if (stat.isDirectory()) {
await walkMetadataFiles(filePath, fileList);
} else if (file === "metadata.json") {
fileList.push(filePath);
}
}
} catch (err) {
// Ignore read errors for individual folders/files
}
return fileList;
}
// 2. Invalidate individual paths instantly
async function revalidatePath(reqPath) {
let targetPath = reqPath;
// Resolve relative paths if the request context is active
if (targetPath && !targetPath.startsWith("/") && !targetPath.includes("://")) {
const ctx = getContext();
let currentPathname = "/";
if (ctx && ctx.req) {
const referer = ctx.req.headers?.referer;
if (referer) {
try {
currentPathname = new URL(referer).pathname;
} catch (e) {}
} else {
currentPathname = ctx.req.path || "/";
}
}
targetPath = resolveRelativeUrl(targetPath, currentPathname);
}
let cleanPath = targetPath;
if (!cleanPath.startsWith("/")) {
cleanPath = "/" + cleanPath;
}
if (cleanPath !== "/" && cleanPath.endsWith("/")) {
cleanPath = cleanPath.slice(0, -1);
}
const dist2Folder = path.resolve(process.cwd(), "dist2");
const reqPathWithSlash = cleanPath.endsWith("/") ? cleanPath : cleanPath + "/";
// Backup current pages to avoid blank reads
try {
if (existsSync(path.join(dist2Folder, reqPathWithSlash, "index.html"))) {
copyFileSync(
path.join(dist2Folder, reqPathWithSlash, "index.html"),
path.join(dist2Folder, reqPathWithSlash, "index._old.html")
);
}
if (existsSync(path.join(dist2Folder, reqPathWithSlash, "rsc.rsc"))) {
copyFileSync(
path.join(dist2Folder, reqPathWithSlash, "rsc.rsc"),
path.join(dist2Folder, reqPathWithSlash, "rsc._old.rsc")
);
}
} catch (e) {
// Ignore copy errors
}
console.log(`[Revalidate] Starting on-demand revalidation for ${cleanPath}...`);
try {
const isDynamic = {};
await buildStaticPage(cleanPath, isDynamic);
if (isDynamic.value) {
console.log(`[Revalidate] Bailout detected for ${cleanPath}. Switching to dynamic.`);
return;
}
const rscResult = await generateStaticRSC(cleanPath);
if (!rscResult.success) {
console.warn(`⚠️ [Revalidate] RSC generation failed for ${cleanPath}.`);
if (rscResult.tempPath && existsSync(rscResult.tempPath)) {
await fs.unlink(rscResult.tempPath).catch(() => {});
}
return;
}
await safeRename(rscResult.tempPath, rscResult.finalPath);
const pageResult = await generateStaticPage(cleanPath);
if (pageResult.success) {
await safeRename(pageResult.tempPath, pageResult.finalPath);
updateStatus(cleanPath, pageResult.status);
console.log(`✅ [Revalidate] Successfully revalidated ${cleanPath} (Status: ${pageResult.status})`);
} else {
console.warn(`⚠️ [Revalidate] HTML generation failed for ${cleanPath}.`);
if (pageResult.tempPath && existsSync(pageResult.tempPath)) {
await fs.unlink(pageResult.tempPath).catch(() => {});
}
}
} catch (e) {
console.warn(`⚠️ [Revalidate] Failed to revalidate ${cleanPath}:`, e.message || e);
}
}
// 3. Invalidate pages matching a specific cache tag
async function revalidateTag(tag) {
console.log(`[Revalidate] Starting on-demand revalidation for tag: "${tag}"...`);
const dist2Folder = path.resolve(process.cwd(), "dist2");
if (!existsSync(dist2Folder)) return;
const metadataFiles = await walkMetadataFiles(dist2Folder);
const revalidatePromises = [];
for (const fileOfMeta of metadataFiles) {
try {
const content = await fs.readFile(fileOfMeta, "utf8");
const metadata = JSON.parse(content);
if (metadata && Array.isArray(metadata.tags) && metadata.tags.includes(tag)) {
// Resolve cache folder back to URL route
const relative = path.relative(dist2Folder, path.dirname(fileOfMeta));
const reqPath = "/" + relative.replace(/\\/g, "/");
revalidatePromises.push(revalidatePath(reqPath));
}
} catch (err) {
console.error(`[Revalidate] Error reading tags from ${fileOfMeta}:`, err);
}
}
// Await all invalidations in parallel
await Promise.all(revalidatePromises);
}
module.exports = {
revalidatePath,
revalidateTag,
};