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
| Function | Lines | Complexity | Covered | CRAP | Mutation |
|---|---|---|---|---|---|
| logFailure | 30 to 38 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| readCache | 40 to 48 | 2 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| fetchEpisodes | 50 to 62 | 4 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| refresh | 64 to 84 | 4 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| isStale | 86 to 87 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| readPodcastFeed | 91 to 101 | 3 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| homePodcastEpisode | 104 to 109 | 2 | 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/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;
}