import { env, waitUntil } from "cloudflare:workers"; export const GITHUB_CACHE_TTL_MS = 5 * 60 * 1000; interface CachedValue { fetchedAt: number; value: T; } type Validator = (value: unknown) => value is T; function getWebsiteCache(): KVNamespace | null { return (env as { WEBSITE_CACHE?: KVNamespace }).WEBSITE_CACHE ?? null; } function isCachedValue(value: unknown, isValue: Validator): value is CachedValue { if (typeof value !== "object" || value === null) return false; const record = value as Record; return typeof record.fetchedAt === "number" && isValue(record.value); } async function readCachedValue( key: string, isValue: Validator, ): Promise | null> { const cache = getWebsiteCache(); if (!cache) return null; const cached = await cache.get(key, { cacheTtl: 60, type: "json" }); return isCachedValue(cached, isValue) ? cached : null; } async function writeCachedValue(key: string, value: T): Promise { const cache = getWebsiteCache(); if (!cache) return; await cache.put( key, JSON.stringify({ fetchedAt: Date.now(), value, }), ); } export async function getBlockingColdCache({ key, isValue, fetchFresh, }: { key: string; isValue: Validator; fetchFresh: () => Promise; }): Promise { const cached = await readCachedValue(key, isValue); if (cached) { if (Date.now() - cached.fetchedAt > GITHUB_CACHE_TTL_MS) { waitUntil( fetchFresh() .then((fresh) => writeCachedValue(key, fresh)) .catch(() => undefined), ); } return cached.value; } const fresh = await fetchFresh(); await writeCachedValue(key, fresh); return fresh; }