diff --git a/src/app/api/aggregate/route.ts b/src/app/api/aggregate/route.ts new file mode 100644 index 00000000..c18aa18c --- /dev/null +++ b/src/app/api/aggregate/route.ts @@ -0,0 +1,51 @@ +// Vercel-edge CDN proxy for the aggregate blob. +// +// kv.openchainbench.com/aggregate/latest.json lives on the Paris VPS +// (no CDN). Vercel functions run in IAD1 (US East) and were timing out +// on the 7.5 MB payload at the old 8 s limit. This route re-exposes the +// blob through Vercel's own CDN: the first request fetches from VPS +// (slow), Vercel caches the response at IAD1's edge for 60 s, all +// subsequent calls within that window cost ~1 ms instead of ~4 s. +// +// aggregate-blob.ts can point AGGREGATE_BLOB_URL at this route instead +// of kv.openchainbench.com to get near-zero latency from any Vercel +// function in the same region. +export const runtime = "nodejs"; + +const UPSTREAM = "https://kv.openchainbench.com/aggregate/latest.json"; +const UPSTREAM_TIMEOUT_MS = 25_000; + +export async function GET() { + let upstream: Response; + try { + upstream = await fetch(UPSTREAM, { + signal: AbortSignal.timeout(UPSTREAM_TIMEOUT_MS), + headers: { "Accept-Encoding": "gzip, br" }, + cache: "no-store", + }); + } catch (err) { + return new Response( + JSON.stringify({ error: "upstream fetch failed", detail: String(err) }), + { status: 502, headers: { "Content-Type": "application/json" } }, + ); + } + + if (!upstream.ok) { + return new Response( + JSON.stringify({ error: "upstream error", status: upstream.status }), + { status: 502, headers: { "Content-Type": "application/json" } }, + ); + } + + const body = await upstream.arrayBuffer(); + return new Response(body, { + status: 200, + headers: { + "Content-Type": "application/json", + // Vercel CDN caches 60 s + 5 min SWR — aligns with the materialize + // worker's publish cadence (~60 s). The sitemap and homepage get a + // warm edge hit after the first request. + "Cache-Control": "public, s-maxage=60, stale-while-revalidate=300", + }, + }); +} diff --git a/src/lib/aggregate-blob.ts b/src/lib/aggregate-blob.ts index ace33ba6..57ea5efd 100644 --- a/src/lib/aggregate-blob.ts +++ b/src/lib/aggregate-blob.ts @@ -24,8 +24,15 @@ import type { Benchmark } from "@/types/benchmark"; import { loadSpecsUncached } from "@/lib/materialize/load"; import { overlayEditorial, slimBenchmarkForCache } from "@/lib/spec"; -const DEFAULT_URL = "https://kv.openchainbench.com/aggregate/latest.json"; -const FETCH_TIMEOUT_MS = 8_000; +// In production, use the self-hosted Vercel CDN proxy (/api/aggregate) +// so IAD1 functions pay ~1 ms (edge cache hit) instead of ~4 s crossing +// the Atlantic to kv.openchainbench.com (Paris VPS, no CDN). +// In dev/preview, hit the VPS directly — CDN caching doesn't apply. +const DEFAULT_URL = + process.env.VERCEL_ENV === "production" + ? "https://openchainbench.com/api/aggregate" + : "https://kv.openchainbench.com/aggregate/latest.json"; +const FETCH_TIMEOUT_MS = 20_000; const MIN_BENCHES = 40; type AggregateEnvelope = { @@ -56,6 +63,9 @@ async function fetchAndProject(): Promise { // Bypass Next's fetch memoization; the outer unstable_cache handles // cross-request caching, we don't want double-layered TTLs. cache: "no-store", + // Explicitly request compression so Vercel's Node.js fetch gets the + // 2MB gzip payload instead of the 7.5MB raw JSON. + headers: { "Accept-Encoding": "gzip, br" }, }); if (!res.ok) { console.warn(`[aggregate-blob] fetch ${url} → ${res.status}`); diff --git a/src/lib/sitemap-builder.ts b/src/lib/sitemap-builder.ts index ff9a3b28..fcef82ea 100644 --- a/src/lib/sitemap-builder.ts +++ b/src/lib/sitemap-builder.ts @@ -565,7 +565,7 @@ async function buildFullSitemap(): Promise { export async function buildSitemap(): Promise { try { const timeout = new Promise((_, reject) => - setTimeout(() => reject(new Error("sitemap build timeout")), 25_000), + setTimeout(() => reject(new Error("sitemap build timeout")), 45_000), ); return await Promise.race([buildFullSitemap(), timeout]); } catch (err) {