perf(website): cache GitHub release metadata in KV

This commit is contained in:
Mohamed Boudra
2026-05-07 23:22:48 +07:00
parent 90bf6571d6
commit 734e15e5a3
4 changed files with 143 additions and 55 deletions
+68
View File
@@ -0,0 +1,68 @@
import { env, waitUntil } from "cloudflare:workers";
export const GITHUB_CACHE_TTL_MS = 5 * 60 * 1000;
interface CachedValue<T> {
fetchedAt: number;
value: T;
}
type Validator<T> = (value: unknown) => value is T;
function getWebsiteCache(): KVNamespace | null {
return (env as { WEBSITE_CACHE?: KVNamespace }).WEBSITE_CACHE ?? null;
}
function isCachedValue<T>(value: unknown, isValue: Validator<T>): value is CachedValue<T> {
if (typeof value !== "object" || value === null) return false;
const record = value as Record<string, unknown>;
return typeof record.fetchedAt === "number" && isValue(record.value);
}
async function readCachedValue<T>(
key: string,
isValue: Validator<T>,
): Promise<CachedValue<T> | 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<T>(key: string, value: T): Promise<void> {
const cache = getWebsiteCache();
if (!cache) return;
await cache.put(
key,
JSON.stringify({
fetchedAt: Date.now(),
value,
}),
);
}
export async function getBlockingColdCache<T>({
key,
isValue,
fetchFresh,
}: {
key: string;
isValue: Validator<T>;
fetchFresh: () => Promise<T>;
}): Promise<T> {
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;
}
+46 -36
View File
@@ -1,5 +1,5 @@
import { createServerFn } from "@tanstack/react-start"; import { createServerFn } from "@tanstack/react-start";
import websitePackage from "../package.json"; import { getBlockingColdCache } from "./github-cache";
interface GitHubAsset { interface GitHubAsset {
name: string; name: string;
@@ -50,46 +50,56 @@ interface ReleaseInfo {
} }
const GITHUB_RELEASES_URL = "https://api.github.com/repos/getpaseo/paseo/releases?per_page=10"; const GITHUB_RELEASES_URL = "https://api.github.com/repos/getpaseo/paseo/releases?per_page=10";
const RELEASE_CACHE_KEY = "github-release:v1";
async function fetchLatestReadyRelease(): Promise<ReleaseInfo> { async function fetchLatestReadyRelease(): Promise<ReleaseInfo> {
const fallbackVersion = websitePackage.version.replace(/-.*$/, ""); const res = await fetch(GITHUB_RELEASES_URL, {
const fallback: ReleaseInfo = { headers: {
version: fallbackVersion, Accept: "application/vnd.github+json",
windowsX64Asset: `Paseo-Setup-${fallbackVersion}.exe`, "User-Agent": "paseo-website",
windowsArm64Asset: null, },
cf: {
cacheEverything: true,
cacheTtl: 60,
cacheKey: "github-releases-latest",
},
} as RequestInit);
if (!res.ok) throw new Error(`github releases ${res.status}`);
const releases = (await res.json()) as GitHubRelease[];
const ready = releases.find((r) => !r.prerelease && !r.draft && hasRequiredAssets(r));
if (!ready) throw new Error("no ready GitHub release found");
const win = pickWindowsAssets(ready.assets);
return {
version: versionFromTag(ready.tag_name),
windowsX64Asset: win.x64,
windowsArm64Asset: win.arm64,
}; };
}
try { function isReleaseInfo(value: unknown): value is ReleaseInfo {
const res = await fetch(GITHUB_RELEASES_URL, { if (typeof value !== "object" || value === null) return false;
headers: { const record = value as Record<string, unknown>;
Accept: "application/vnd.github+json", return (
"User-Agent": "paseo-website", typeof record.version === "string" &&
}, /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(record.version) &&
// Cloudflare Workers: cache the upstream response with stale-while-revalidate. (typeof record.windowsX64Asset === "string" || record.windowsX64Asset === null) &&
// Fresh for 60s, serve stale for up to 300s while revalidating in background. (typeof record.windowsArm64Asset === "string" || record.windowsArm64Asset === null) &&
// In non-Workers environments this is ignored. (record.windowsX64Asset === null ||
cf: { new RegExp(`^Paseo-Setup-${record.version.replaceAll(".", "\\.")}(?:-x64)?\\.exe$`).test(
cacheEverything: true, record.windowsX64Asset,
cacheTtl: 60, )) &&
cacheKey: "github-releases-latest", (record.windowsArm64Asset === null ||
}, new RegExp(`^Paseo-Setup-${record.version.replaceAll(".", "\\.")}-arm64\\.exe$`).test(
} as RequestInit); record.windowsArm64Asset,
if (!res.ok) return fallback; ))
);
const releases = (await res.json()) as GitHubRelease[];
const ready = releases.find((r) => !r.prerelease && !r.draft && hasRequiredAssets(r));
if (!ready) return fallback;
const win = pickWindowsAssets(ready.assets);
return {
version: versionFromTag(ready.tag_name),
windowsX64Asset: win.x64,
windowsArm64Asset: win.arm64,
};
} catch {
return fallback;
}
} }
export const getLatestRelease = createServerFn({ method: "GET" }).handler(async () => { export const getLatestRelease = createServerFn({ method: "GET" }).handler(async () => {
return fetchLatestReadyRelease(); return getBlockingColdCache({
key: RELEASE_CACHE_KEY,
isValue: isReleaseInfo,
fetchFresh: fetchLatestReadyRelease,
});
}); });
+25 -19
View File
@@ -1,4 +1,5 @@
import { createServerFn } from "@tanstack/react-start"; import { createServerFn } from "@tanstack/react-start";
import { getBlockingColdCache } from "./github-cache";
interface GitHubRepo { interface GitHubRepo {
stargazers_count: number; stargazers_count: number;
@@ -11,30 +12,35 @@ function formatStars(count: number): string {
} }
const GITHUB_REPO_URL = "https://api.github.com/repos/getpaseo/paseo"; const GITHUB_REPO_URL = "https://api.github.com/repos/getpaseo/paseo";
const STARS_CACHE_KEY = "github-stars:v1";
async function fetchStarCount(): Promise<string> { async function fetchStarCount(): Promise<string> {
try { const res = await fetch(GITHUB_REPO_URL, {
const res = await fetch(GITHUB_REPO_URL, { headers: {
headers: { Accept: "application/vnd.github+json",
Accept: "application/vnd.github+json", "User-Agent": "paseo-website",
"User-Agent": "paseo-website", },
}, cf: {
cf: { cacheEverything: true,
cacheEverything: true, cacheTtl: 60,
cacheTtl: 60, cacheKey: "github-repo-stars",
cacheKey: "github-repo-stars", },
}, } as RequestInit);
} as RequestInit); if (!res.ok) throw new Error(`github repo ${res.status}`);
if (!res.ok) return "";
const repo = (await res.json()) as GitHubRepo; const repo = (await res.json()) as GitHubRepo;
return formatStars(repo.stargazers_count); return formatStars(repo.stargazers_count);
} catch { }
return "";
} function isStars(value: unknown): value is string {
return typeof value === "string" && /^(\d+|\d+\.\d+k|\d+k)$/.test(value);
} }
export const getStarCount = createServerFn({ method: "GET" }).handler(async () => { export const getStarCount = createServerFn({ method: "GET" }).handler(async () => {
const stars = await fetchStarCount(); const stars = await getBlockingColdCache({
key: STARS_CACHE_KEY,
isValue: isStars,
fetchFresh: fetchStarCount,
});
return { stars }; return { stars };
}); });
+4
View File
@@ -11,3 +11,7 @@ routes = [
[observability] [observability]
enabled = true enabled = true
[[kv_namespaces]]
binding = "WEBSITE_CACHE"
id = "cb4d8779806544e9b673142a80c1eb5f"