Skip to the content

Capsid menu

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

The functions of cache-purge.server.ts
FunctionLinesComplexityCoveredCRAPMutation
combinePurges22 to 253No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
<anonymous:24>24 to 241No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
purgeTags33 to 675No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
purgePost72 to 741No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
purgePosts77 to 791No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
purgeProcedures82 to 841No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
purgeRegistry90 to 921No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
purgePublications99 to 1011No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
purgePages104 to 1061No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
purgeRoster113 to 1151No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
purgePhages121 to 1231No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
purgeLlms126 to 1281No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
purgeCv131 to 1331No data: no coverage report in this mapNo data: no coverage report in this mapNo 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);
}