GET /xrpc/app.bsky.actor.searchActorsTypeahead typeahead.waow.tech
16
fork

Configure Feed

Select the types of activity you want to include in your feed.

materialize stats to KV, keep pie chart live from turso

cron writes metrics/snapshots/deltas to KV hourly. /stats reads from
KV (edge-fast) + single turso query for real-time traffic pie chart.
~76ms cold miss, down from 4-6s.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

+38 -32
+16
src/enrichment.ts
··· 23 23 } 24 24 } 25 25 26 + /** materialize stats data to KV so /stats never queries turso on the request path */ 27 + export async function materializeStats(db: TursoDB, env: Env): Promise<void> { 28 + const [metricsRes, snapshotRes, deltasRes] = await db.batch([ 29 + db.prepare("SELECT hour, searches, total_ms FROM metrics ORDER BY hour DESC LIMIT 2016"), 30 + db.prepare("SELECT hour, total, with_handles, with_avatars, hidden FROM snapshots ORDER BY hour ASC LIMIT 2000"), 31 + db.prepare("SELECT bucket, actors_delta, handles_delta, avatars_delta FROM actor_deltas ORDER BY bucket DESC LIMIT 288"), 32 + ], "read"); 33 + 34 + await env.KV.put("stats_data", JSON.stringify({ 35 + metrics: metricsRes.results ?? [], 36 + snapshots: snapshotRes.results ?? [], 37 + deltas: deltasRes.results ?? [], 38 + materialized_at: Date.now(), 39 + })); 40 + } 41 + 26 42 /** lease-coordinated, two-phase enrichment: slingshot identity + getProfiles batch. 27 43 * returns { resolved, enriched } counts for delta tracking. */ 28 44 export async function enrichActors(db: TursoDB, env: Env): Promise<{ resolved: number; enriched: number }> {
+19 -30
src/handlers/stats.ts
··· 1 1 import type { TursoDB } from "../db"; 2 + import type { Env } from "../types"; 2 3 import { html } from "../utils"; 3 4 import { statsPage, type SnapshotPoint } from "../pages/stats"; 4 5 5 - const EDGE_CACHE_TTL = 60; // seconds — stats are backward-looking, 1 min staleness is invisible 6 + const EDGE_CACHE_TTL = 60; // seconds 6 7 7 - export async function handleStats(request: Request, db: TursoDB, ctx: ExecutionContext): Promise<Response> { 8 - // serve from CF edge cache when available (avoids Turso cold-start penalty) 8 + export async function handleStats(request: Request, db: TursoDB, env: Env, ctx: ExecutionContext): Promise<Response> { 9 9 const cache = caches.default; 10 10 const cacheKey = new Request(new URL("/stats", request.url).href); 11 11 const cached = await cache.match(cacheKey); 12 12 if (cached) { 13 - // override cache-control so browser doesn't cache the cached response 14 13 const resp = new Response(cached.body, cached); 15 14 resp.headers.set("Cache-Control", "no-store"); 16 15 return resp; ··· 18 17 19 18 const t0 = performance.now(); 20 19 21 - const [metricsRes, snapshotRes, trafficRes, deltasRes] = 22 - await db.batch([ 23 - db.prepare( 24 - "SELECT hour, searches, total_ms FROM metrics ORDER BY hour DESC LIMIT 2016" 25 - ), 26 - db.prepare( 27 - "SELECT hour, total, with_handles, with_avatars, hidden FROM snapshots ORDER BY hour ASC LIMIT 2000" 28 - ), 29 - db.prepare( 30 - "SELECT domain, hits FROM traffic_sources ORDER BY hits DESC LIMIT 10" 31 - ), 32 - db.prepare( 33 - "SELECT bucket, actors_delta, handles_delta, avatars_delta FROM actor_deltas ORDER BY bucket DESC LIMIT 288" 34 - ), 35 - ], "read"); 20 + // materialized data from KV (written by cron) — never hits turso 21 + const kvData = await env.KV.get("stats_data", "json") as { 22 + metrics: { hour: number; searches: number; total_ms: number }[]; 23 + snapshots: { hour: number; total: number; with_handles: number; with_avatars: number; hidden: number }[]; 24 + deltas: { bucket: number; actors_delta: number; handles_delta: number; avatars_delta: number }[]; 25 + materialized_at: number; 26 + } | null; 27 + 28 + // traffic sources: live from turso (7 rows, real-time) 29 + const trafficRes = await db.prepare( 30 + "SELECT domain, hits FROM traffic_sources ORDER BY hits DESC LIMIT 10" 31 + ).all<{ domain: string; hits: number }>(); 36 32 37 33 const tQuery = performance.now(); 38 34 39 - const rows = (metricsRes.results ?? []) as { 40 - hour: number; 41 - searches: number; 42 - total_ms: number; 43 - }[]; 44 - const dbSnapshots = (snapshotRes.results ?? []) as { hour: number; total: number; with_handles: number; with_avatars: number; hidden: number }[]; 45 - const trafficSources = (trafficRes.results ?? []) as { domain: string; hits: number }[]; 46 - const deltas = ((deltasRes.results ?? []) as { bucket: number; actors_delta: number; handles_delta: number; avatars_delta: number }[]).reverse(); 35 + const rows = kvData?.metrics ?? []; 36 + const dbSnapshots = kvData?.snapshots ?? []; 37 + const trafficSources = trafficRes.results ?? []; 38 + const deltas = [...(kvData?.deltas ?? [])].reverse(); 47 39 48 40 // build snapshot points with timestamps 49 41 const snapshots: SnapshotPoint[] = dbSnapshots.map((s) => ({ ··· 72 64 } 73 65 } 74 66 75 - // derive current counts from the latest point (snapshot + deltas) 76 67 const latest = snapshots[snapshots.length - 1] ?? { total: 0, with_handles: 0, with_avatars: 0 }; 77 68 const total = latest.total; 78 69 const withHandles = latest.with_handles; ··· 98 89 99 90 const timing = `query;dur=${queryMs}, process;dur=${processMs}, render;dur=${renderMs}, total;dur=${totalMs2}`; 100 91 101 - // edge cache entry — public, max-age controls the Cache API TTL 102 92 const cacheResponse = html(body, { 103 93 "Server-Timing": timing, 104 94 "Cache-Control": `public, max-age=${EDGE_CACHE_TTL}`, 105 95 }); 106 96 ctx.waitUntil(cache.put(cacheKey, cacheResponse)); 107 97 108 - // browser response — no caching so refreshes always get fresh data 109 98 return html(body, { 110 99 "Server-Timing": timing, 111 100 "Cache-Control": "no-store",
+3 -2
src/index.ts
··· 2 2 import { CORS_HEADERS } from "./types"; 3 3 import { getDb } from "./db"; 4 4 import { clientIP, json, html } from "./utils"; 5 - import { recordSnapshot } from "./enrichment"; 5 + import { recordSnapshot, materializeStats } from "./enrichment"; 6 6 import { enrichActors } from "./enrichment"; 7 7 import { refreshModeration } from "./cron"; 8 8 import { handleSearch } from "./handlers/search"; ··· 16 16 async scheduled(_event: ScheduledEvent, env: Env, _ctx: ExecutionContext): Promise<void> { 17 17 const db = await getDb(env); 18 18 await recordSnapshot(db); 19 + await materializeStats(db, env); 19 20 await refreshModeration(db, env); 20 21 await enrichActors(db, env); 21 22 }, ··· 43 44 const db = await getDb(env); 44 45 45 46 if (pathname === "/stats" && request.method === "GET") { 46 - return handleStats(request, db, ctx); 47 + return handleStats(request, db, env, ctx); 47 48 } 48 49 49 50 if (pathname === "/request-indexing" && request.method === "POST") {