app/lib/cache-purge.server.ts
116 lines and 13 functions in DrDustinEdwards/dustinedwards-info, at commit 1590ba4 on 2026-10-24. This file on GitHub.
Functions
| Function | Lines | Complexity | Covered | CRAP | Mutation |
|---|---|---|---|---|---|
| combinePurges | 22 to 25 | 3 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:24> | 24 to 24 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| purgeTags | 33 to 67 | 5 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| purgePost | 72 to 74 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| purgePosts | 77 to 79 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| purgeProcedures | 82 to 84 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| purgeRegistry | 90 to 92 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| purgePublications | 99 to 101 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| purgePages | 104 to 106 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| purgeRoster | 113 to 115 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| purgePhages | 121 to 123 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| purgeLlms | 126 to 128 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| purgeCv | 131 to 133 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
Imports
It imports app/kb/registry/route.ts, app/lib/cv/route.ts, app/lib/error-message.mjs, app/lib/llms/validate.mjs, app/lib/pages/route.ts, app/lib/publications/paths.mjs, app/lib/seo.ts. Imported by app/kb/procedures/primer-dependents.server.ts, app/kb/procedures/save.server.ts, app/kb/registry/save.server.ts, app/lib/cv/pdf.server.ts, app/lib/cv/save.server.ts, app/lib/dictionary/save.server.ts, app/lib/editor/post-rows.server.ts, app/lib/editor/publish.server.ts, app/lib/llms/save.server.ts, app/lib/operator/sync-tools.server.ts, app/lib/pages/save.server.ts, app/lib/phages/save.server.ts, app/lib/publications/save.server.ts, app/lib/roster/save.server.ts, app/lib/webmention/decide.server.ts.
Source
// aislop-ignore-next-line ai-slop/hallucinated-import -- a Workers built-in, not an npm package
import { cache } from "cloudflare:workers";
import { LLMS_CACHE_TAG } from "~/lib/llms/validate.mjs";
import { CV_CACHE_TAG } from "~/lib/cv/route";
import { CONTENT_PAGES_CACHE_TAG } from "~/lib/pages/route";
import { PUBLICATIONS_CACHE_TAG } from "~/lib/publications/paths.mjs";
import { REGISTRY_CACHE_TAG } from "~/kb/registry/route";
import { cacheTags } from "~/lib/seo";
import { errorMessage } from "~/lib/error-message.mjs";
// A purge never throws and never blocks a write: a failed invalidation is a stale page and a log
// line, not a 500 after a publish that already succeeded.
/**
* `true` purged, `false` failed (refused or threw: public pages stay stale until expiry), `null`
* skipped because this runtime has no purge API (Miniflare, local dev). Only `false` is a problem.
*/
export type PurgeOutcome = boolean | null;
/** Several purges read as one: any failure fails, all done is done, otherwise skipped. */
export function combinePurges(...outcomes: PurgeOutcome[]): PurgeOutcome {
if (outcomes.includes(false)) return false;
return outcomes.every((outcome) => outcome === true) ? true : null;
}
type PurgeResult = { success: boolean; errors?: Array<{ code: number; message: string }> };
/**
* @param tags the cache tags to invalidate
* @param why one phrase naming the write that caused this, for the log line
*/
async function purgeTags(tags: string[], why: string): Promise<PurgeOutcome> {
// Miniflare has no Workers Cache, so the API may be absent; a TypeError here would fail a save.
const purge = (cache as typeof cache & { purge?: (o: unknown) => Promise<PurgeResult> })?.purge;
if (typeof purge !== "function") {
console.log(JSON.stringify({ event: "cache-purge-skipped", why, reason: "no purge API on this runtime", tags }));
return null;
}
try {
// `purge` resolves `{ success: false }` on a refusal (usually the rate limit) rather than rejecting.
const result = await purge({ tags });
if (!result?.success) {
console.error(
JSON.stringify({
alert: "cache-purge-refused",
why,
tags,
errors: result?.errors ?? [],
}),
);
return false;
}
return true;
} catch (error) {
console.error(
JSON.stringify({
alert: "cache-purge-threw",
why,
tags,
detail: errorMessage(error),
}),
);
return false;
}
}
/**
* @param slug the post whose page changed
*/
export async function purgePost(slug: string, why: string): Promise<PurgeOutcome> {
return purgeTags([`post:${slug}`], why);
}
// Not `purgeEverything`, which would also drop the hand-authored pages no publish changes.
export async function purgePosts(why: string): Promise<PurgeOutcome> {
return purgeTags([cacheTags()], why);
}
/** Every procedure page, sheet and twin carries this one tag (app/kb/procedures/route.ts). */
export async function purgeProcedures(why: string): Promise<PurgeOutcome> {
return purgeTags(["procedures"], why);
}
/**
* Every registry page, listing and twin carries this one tag (app/kb/registry/route.ts). A protocol page links
* the registry's items by their own addresses and states none of their facts, so no other tag moves.
*/
export async function purgeRegistry(why: string): Promise<PurgeOutcome> {
return purgeTags([REGISTRY_CACHE_TAG], why);
}
/**
* The publication pages, exports and twins carry their own tag. The home page, the search page and the
* blog listings carry `posts` and show papers too (the three newest, the count, a search record), so a
* save purges both.
*/
export async function purgePublications(why: string): Promise<PurgeOutcome> {
return purgeTags([PUBLICATIONS_CACHE_TAG, cacheTags()], why);
}
/** Every content page and its twin carries this one tag (app/lib/pages/route.ts). */
export async function purgePages(why: string): Promise<PurgeOutcome> {
return purgeTags([CONTENT_PAGES_CACHE_TAG], why);
}
/**
* The roster is embedded in two pages: the list on /teaching/phage-discovery, which carries the content
* pages' tag, and the counts on the home page, which carries `posts` (app/routes/home.tsx). A cohort save
* purges both, because purging only the program page would leave the home page's counts stale.
*/
export async function purgeRoster(why: string): Promise<PurgeOutcome> {
return purgeTags([CONTENT_PAGES_CACHE_TAG, cacheTags()], why);
}
/**
* The phage table is embedded in one page, /research/phages: its HTML and its markdown twin both carry the
* content-pages tag, so a phage save purges that tag (and only it). A phage appears nowhere else.
*/
export async function purgePhages(why: string): Promise<PurgeOutcome> {
return purgeTags([CONTENT_PAGES_CACHE_TAG], why);
}
/** /llms.txt carries this one tag (app/routes/llms.ts). */
export async function purgeLlms(why: string): Promise<PurgeOutcome> {
return purgeTags([LLMS_CACHE_TAG], why);
}
/** The CV page, its charts and its markdown twin carry this one tag (app/lib/cv/route.ts). */
export async function purgeCv(why: string): Promise<PurgeOutcome> {
return purgeTags([CV_CACHE_TAG], why);
}