import { osloDay } from "@/lib/date"; import { purgeOwnCache, setInternalRevalidateSecret, } from "@/lib/internalRevalidate"; import { CMS_CACHE_TAG, TODAY_CACHE_TAG, secondsUntilOsloMidnight, } from "@/lib/revalidation"; // How often the clock is checked. Timers alone can't be trusted to fire at // midnight: they stand still while a host or VM is paused const CHECK_INTERVAL_MS = 30_000; // Until the first purge has gone through. The server is already listening // when register() runs, so this is only for a purge that fails const STARTUP_RETRY_MS = 1_000; function keepCacheCurrent() { // the Oslo day the cache was last purged for; undefined until the startup // purge has gone through let purgedDay: string | undefined; let repeatTodayPurge = false; let failures = 0; async function check() { try { await purgeWhatIsDue(); } catch (error) { // nothing in there is expected to throw, but if something does the // loop must survive it: without it the cache never rolls over console.error("Checking the page cache failed", error); } finally { schedule(); } } async function purgeWhatIsDue() { const day = osloDay(); let error: string | null = null; if (purgedDay === undefined) { // Pages prerendered by the build, or left on disk by an earlier run, // may predate CMS edits and midnights this process never saw error = (await purgeOwnCache(CMS_CACHE_TAG)) ?? (await purgeOwnCache(TODAY_CACHE_TAG)); if (!error) purgedDay = day; } else if (day !== purgedDay) { error = await purgeOwnCache(TODAY_CACHE_TAG); if (!error) { purgedDay = day; repeatTodayPurge = true; } } else if (repeatTodayPurge) { // Next stamps a page when its render finishes, so a page that was // being rendered across the purge looks newer than it and survives. // A second purge one interval later catches those error = await purgeOwnCache(TODAY_CACHE_TAG); if (!error) repeatTodayPurge = false; } if (error) { failures += 1; // report the first failure once running, and then now and again; at // startup only if it goes on for a minute if (failures === 1 ? purgedDay !== undefined : failures % 60 === 0) { console.error(`Purging the page cache keeps failing: ${error}`); } } else { failures = 0; } } function schedule() { const delay = purgedDay === undefined ? STARTUP_RETRY_MS : // one second past midnight, so the purge can't land on the old day Math.min(CHECK_INTERVAL_MS, (secondsUntilOsloMidnight() + 1) * 1000); setTimeout(check, delay).unref(); } void check(); } export function register() { // only in the running production server: not during `next build`, in // `next dev` (nothing is kept between requests there) or on the edge if ( process.env.NEXT_RUNTIME !== "nodejs" || process.env.NODE_ENV !== "production" || process.env.NEXT_PHASE === "phase-production-build" ) { return; } setInternalRevalidateSecret(crypto.randomUUID()); keepCacheCurrent(); }