Skip to the content

Capsid menu

app/lib/podcast/podcast.server.ts

98 lines and 7 functions in DrDustinEdwards/dustinedwards-info, at commit 1590ba4 on 2026-10-24. This file on GitHub.

Functions

The functions of podcast.server.ts
FunctionLinesComplexityCoveredCRAPMutation
logFailure30 to 381No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
readCache40 to 482No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
fetchEpisodes50 to 624No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
refresh64 to 844No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
isStale86 to 871No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
readPodcastFeed91 to 1013No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
homePodcastEpisode104 to 1092No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code

Imports

It imports app/lib/context.ts, app/lib/error-message.mjs, app/lib/podcast/feed.mjs, app/lib/site-origin.mjs. Imported by app/routes/home.tsx.

Source

import type { RouterContextProvider } from "react-router";

import { getEnv, getExecutionContext } from "~/lib/context";
import { PODCAST_FEED_URL, parsePodcastFeed, type PodcastEpisode } from "~/lib/podcast/feed.mjs";
import { errorMessage } from "~/lib/error-message.mjs";
import { SITE_USER_AGENT } from "~/lib/site-origin.mjs";

// The page never waits on the podcast host: loaders read KV only and refresh in `waitUntil` (this Worker
// has no cron). A failed refresh keeps the episodes and stamps `checkedAt`, so a down host is asked once
// per interval, not on every render.

const CACHE_KEY = "podcast:germomics:v1";
const REFRESH_AFTER_MS = 3 * 60 * 60 * 1000;
const FETCH_TIMEOUT_MS = 8000;

/**
 * `lastError` says why the last refresh failed; absent after one that landed. With no episodes ever
 * read, a failed refresh still answers (episodes empty, `fetchedAt` null, `lastError` set), so a
 * reader can tell "the read failed" from "not read yet", which is the only thing null means.
 */
type CachedFeed = {
  episodes: PodcastEpisode[];
  fetchedAt: string | null;
  checkedAt: string;
  lastError?: string;
};

// A failed refresh never takes the page down, but it is logged under one alert key and kept on the
// cached feed as `lastError`, so a host that has been down for a week is not read as a quiet podcast.
function logFailure(stage: string, detail: unknown) {
  console.error(
    JSON.stringify({
      alert: "podcast-refresh-failed",
      stage,
      detail: errorMessage(detail),
    }),
  );
}

async function readCache(kv: KVNamespace): Promise<CachedFeed | null> {
  try {
    return await kv.get<CachedFeed>(CACHE_KEY, "json");
  } catch (error) {
    // A KV hiccup must never take the page down.
    logFailure("kv-read", error);
    return null;
  }
}

async function fetchEpisodes(): Promise<{ episodes: PodcastEpisode[] } | { error: string }> {
  try {
    const res = await fetch(PODCAST_FEED_URL, {
      headers: { "user-agent": SITE_USER_AGENT, accept: "application/rss+xml, application/xml" },
      signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
    });
    if (!res.ok) return { error: `the feed answered HTTP ${res.status}` };
    const episodes = parsePodcastFeed(await res.text());
    return episodes.length > 0 ? { episodes } : { error: "the feed parsed to no episodes" };
  } catch (error) {
    return { error: errorMessage(error) };
  }
}

async function refresh(kv: KVNamespace, cached: CachedFeed | null): Promise<CachedFeed> {
  const now = new Date().toISOString();
  const fetched = await fetchEpisodes();
  let next: CachedFeed;
  if ("episodes" in fetched) {
    next = { episodes: fetched.episodes, fetchedAt: now, checkedAt: now };
  } else {
    logFailure("fetch", fetched.error);
    // Not stored when nothing was ever read: a cached empty feed would hold off the retry for the
    // whole refresh interval, where today every render retries a cold cache.
    if (!cached) return { episodes: [], fetchedAt: null, checkedAt: now, lastError: fetched.error };
    next = { ...cached, checkedAt: now, lastError: fetched.error };
  }
  try {
    await kv.put(CACHE_KEY, JSON.stringify(next));
  } catch (error) {
    // The next render tries again.
    logFailure("kv-write", error);
  }
  return next;
}

const isStale = (cached: CachedFeed) =>
  Date.now() - Date.parse(cached.checkedAt) > REFRESH_AFTER_MS;

// A public render never blocks on the podcast host: a stale or cold cache answers with what it has
// and the refresh runs in `waitUntil`.
async function readPodcastFeed(
  context: Readonly<RouterContextProvider>,
): Promise<CachedFeed | null> {
  const env = getEnv(context);
  const ctx = getExecutionContext(context);
  const kv = env.APP_KV;
  const cached = await readCache(kv);
  if (cached && !isStale(cached)) return cached;
  ctx.waitUntil(refresh(kv, cached));
  return cached;
}

// Always the latest episode in the feed: there is no stored choice to fall back from.
export async function homePodcastEpisode(
  context: Readonly<RouterContextProvider>,
): Promise<PodcastEpisode | null> {
  const feed = await readPodcastFeed(context);
  return feed?.episodes[0] ?? null;
}