101 lines
3.1 KiB
TypeScript
101 lines
3.1 KiB
TypeScript
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();
|
|
}
|