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.

track cache hit latency separately, show both on stats page

cached (edge) vs cold (turso) avg latency in the same card.
cache hits were previously invisible — only turso round-trips
were recorded, making the displayed avg misleadingly high.

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

+31 -11
+3 -1
schema.sql
··· 45 45 CREATE TABLE IF NOT EXISTS metrics ( 46 46 hour INTEGER PRIMARY KEY, 47 47 searches INTEGER NOT NULL DEFAULT 0, 48 - total_ms REAL NOT NULL DEFAULT 0 48 + total_ms REAL NOT NULL DEFAULT 0, 49 + cache_hits INTEGER NOT NULL DEFAULT 0, 50 + cache_ms REAL NOT NULL DEFAULT 0 49 51 ); 50 52 51 53 CREATE TABLE IF NOT EXISTS snapshots (
+1 -1
src/enrichment.ts
··· 26 26 /** materialize stats data to KV so /stats never queries turso on the request path */ 27 27 export async function materializeStats(db: TursoDB, env: Env): Promise<void> { 28 28 const [metricsRes, snapshotRes, deltasRes] = await db.batch([ 29 - db.prepare("SELECT hour, searches, total_ms FROM metrics ORDER BY hour DESC LIMIT 2016"), 29 + db.prepare("SELECT hour, searches, total_ms, cache_hits, cache_ms FROM metrics ORDER BY hour DESC LIMIT 2016"), 30 30 db.prepare("SELECT hour, total, with_handles, with_avatars, hidden FROM snapshots ORDER BY hour ASC LIMIT 2000"), 31 31 db.prepare("SELECT bucket, actors_delta, handles_delta, avatars_delta FROM actor_deltas ORDER BY bucket DESC LIMIT 288"), 32 32 ], "read");
+4 -3
src/handlers/search.ts
··· 1 1 import type { TursoDB } from "../db"; 2 2 import type { ActorRow, Env } from "../types"; 3 3 import { json, sanitize, avatarUrl } from "../utils"; 4 - import { recordMetric, recordTrafficSource } from "../metrics"; 4 + import { recordMetric, recordCacheHit, recordTrafficSource } from "../metrics"; 5 5 import { throttledBackfill } from "../backfill"; 6 6 7 7 export async function handleSearch( ··· 28 28 return json({ actors: [] }); 29 29 } 30 30 31 + const t0 = Date.now(); 32 + 31 33 // cache API — edge cache for hot queries 32 34 const cacheKey = new Request( 33 35 `https://typeahead-cache/${encodeURIComponent(term)}:${limit}`, ··· 36 38 const cache = caches.default; 37 39 const cached = await cache.match(cacheKey); 38 40 if (cached) { 41 + ctx.waitUntil(recordCacheHit(db, Date.now() - t0)); 39 42 return cached; 40 43 } 41 - 42 - const t0 = Date.now(); 43 44 44 45 // 3-tier ranking: exact handle → handle prefix → FTS prefix 45 46 const ftsQuery = `"${term}"*`;
+6 -3
src/handlers/stats.ts
··· 19 19 20 20 // materialized data from KV (written by cron) — never hits turso 21 21 const kvData = await env.KV.get("stats_data", "json") as { 22 - metrics: { hour: number; searches: number; total_ms: number }[]; 22 + metrics: { hour: number; searches: number; total_ms: number; cache_hits: number; cache_ms: number }[]; 23 23 snapshots: { hour: number; total: number; with_handles: number; with_avatars: number; hidden: number }[]; 24 24 deltas: { bucket: number; actors_delta: number; handles_delta: number; avatars_delta: number }[]; 25 25 materialized_at: number; ··· 70 70 const withAvatars = latest.with_avatars; 71 71 const hiddenCount = dbSnapshots.length > 0 ? (dbSnapshots[dbSnapshots.length - 1].hidden ?? 0) : 0; 72 72 73 - const totalSearches = rows.reduce((s, r) => s + r.searches, 0); 73 + const totalSearches = rows.reduce((s, r) => s + r.searches + (r.cache_hits ?? 0), 0); 74 74 // avg latency over last 24h only (288 five-min buckets) so transient spikes age out quickly 75 75 const recentRows = rows.slice(0, 288); 76 76 const recentSearches = recentRows.reduce((s, r) => s + r.searches, 0); 77 77 const recentMs = recentRows.reduce((s, r) => s + r.total_ms, 0); 78 78 const avgLatency = recentSearches > 0 ? recentMs / recentSearches : 0; 79 + const recentCacheHits = recentRows.reduce((s, r) => s + (r.cache_hits ?? 0), 0); 80 + const recentCacheMs = recentRows.reduce((s, r) => s + (r.cache_ms ?? 0), 0); 81 + const avgCacheLatency = recentCacheHits > 0 ? recentCacheMs / recentCacheHits : 0; 79 82 const handlePct = total > 0 ? ((withHandles / total) * 100).toFixed(1) : "0"; 80 83 const avatarPct = total > 0 ? ((withAvatars / total) * 100).toFixed(1) : "0"; 81 84 82 85 const tProcess = performance.now(); 83 86 84 - const body = statsPage({ total, hiddenCount, rows, totalSearches, avgLatency, handlePct, avatarPct, snapshots, trafficSources }); 87 + const body = statsPage({ total, hiddenCount, rows, totalSearches, avgLatency, avgCacheLatency, handlePct, avatarPct, snapshots, trafficSources }); 85 88 86 89 const tRender = performance.now(); 87 90
+13
src/metrics.ts
··· 14 14 .run(); 15 15 } 16 16 17 + export async function recordCacheHit(db: TursoDB, ms: number): Promise<void> { 18 + const bucket = Math.floor(Date.now() / 300_000); 19 + await db.prepare( 20 + `INSERT INTO metrics (hour, cache_hits, cache_ms) 21 + VALUES (?1, 1, ?2) 22 + ON CONFLICT(hour) DO UPDATE SET 23 + cache_hits = cache_hits + 1, 24 + cache_ms = cache_ms + ?2` 25 + ) 26 + .bind(bucket, ms) 27 + .run(); 28 + } 29 + 17 30 /** fire-and-forget: record actor count deltas at 5-min granularity */ 18 31 export async function recordActorDelta( 19 32 db: TursoDB,
+4 -3
src/pages/stats.ts
··· 10 10 export interface StatsData { 11 11 total: number; 12 12 hiddenCount: number; 13 - rows: { hour: number; searches: number; total_ms: number }[]; 13 + rows: { hour: number; searches: number; total_ms: number; cache_hits: number; cache_ms: number }[]; 14 14 totalSearches: number; 15 15 avgLatency: number; 16 + avgCacheLatency: number; 16 17 handlePct: string; 17 18 avatarPct: string; 18 19 snapshots: SnapshotPoint[]; ··· 187 188 <div class="value">${d.totalSearches.toLocaleString()}</div> 188 189 </div> 189 190 <div class="metric"> 190 - <div class="label" data-tip="average response time for uncached searches (last 24h)">avg latency</div> 191 - <div class="value">${d.avgLatency.toFixed(1)} ms</div> 191 + <div class="label" data-tip="last 24h — cached (edge) vs uncached (turso)">avg latency</div> 192 + <div class="value" style="font-size:1.3rem">${d.avgCacheLatency.toFixed(0)} ms <span style="opacity:.4;font-size:.85rem">cached</span> · ${d.avgLatency.toFixed(0)} ms <span style="opacity:.4;font-size:.85rem">cold</span></div> 192 193 </div> 193 194 </div> 194 195