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.

add hourly cron for actor count snapshots

snapshot collection was piggybacking on uncached searches, so hours
with no traffic produced no data points. now a cron trigger runs
every hour at :00 and calls recordSnapshot directly. also removed
the KV-gated snapshot logic from recordMetric since the cron handles
it unconditionally.

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

+24 -20
+23 -20
src/index.ts
··· 115 115 116 116 // --- end backfill --- 117 117 118 + /** record an actor-count snapshot for the current hour (idempotent) */ 119 + async function recordSnapshot(env: Env): Promise<void> { 120 + const hour = Math.floor(Date.now() / 3_600_000); 121 + const row = await env.DB.prepare( 122 + `SELECT COUNT(*) AS total, 123 + SUM(CASE WHEN handle != '' THEN 1 ELSE 0 END) AS with_handles, 124 + SUM(CASE WHEN avatar_url != '' THEN 1 ELSE 0 END) AS with_avatars 125 + FROM actors` 126 + ).first<{ total: number; with_handles: number; with_avatars: number }>(); 127 + if (row) { 128 + await env.DB.prepare( 129 + `INSERT OR REPLACE INTO snapshots (hour, total, with_handles, with_avatars) 130 + VALUES (?1, ?2, ?3, ?4)` 131 + ) 132 + .bind(hour, row.total, row.with_handles, row.with_avatars) 133 + .run(); 134 + } 135 + } 136 + 118 137 /** fire-and-forget: increment hourly search count + accumulate response time */ 119 138 async function recordMetric(env: Env, ms: number): Promise<void> { 120 139 const hour = Math.floor(Date.now() / 3_600_000); ··· 127 146 ) 128 147 .bind(hour, ms) 129 148 .run(); 130 - 131 - // hourly actor count snapshot (first search of each hour wins) 132 - const snapKey = `snap:${hour}`; 133 - if (!(await env.KV.get(snapKey))) { 134 - const row = await env.DB.prepare( 135 - `SELECT COUNT(*) AS total, 136 - SUM(CASE WHEN handle != '' THEN 1 ELSE 0 END) AS with_handles, 137 - SUM(CASE WHEN avatar_url != '' THEN 1 ELSE 0 END) AS with_avatars 138 - FROM actors` 139 - ).first<{ total: number; with_handles: number; with_avatars: number }>(); 140 - if (row) { 141 - await env.DB.prepare( 142 - `INSERT OR IGNORE INTO snapshots (hour, total, with_handles, with_avatars) 143 - VALUES (?1, ?2, ?3, ?4)` 144 - ) 145 - .bind(hour, row.total, row.with_handles, row.with_avatars) 146 - .run(); 147 - } 148 - await env.KV.put(snapKey, "1", { expirationTtl: 7200 }); 149 - } 150 149 } 151 150 152 151 async function handleSearch( ··· 929 928 } 930 929 931 930 export default { 931 + async scheduled(_event: ScheduledEvent, env: Env, _ctx: ExecutionContext): Promise<void> { 932 + await recordSnapshot(env); 933 + }, 934 + 932 935 async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> { 933 936 if (request.method === "OPTIONS") { 934 937 return new Response(null, { status: 204, headers: CORS_HEADERS });
+1
wrangler.jsonc
··· 3 3 "main": "src/index.ts", 4 4 "compatibility_date": "2024-12-01", 5 5 "compatibility_flags": ["nodejs_compat"], 6 + "triggers": { "crons": ["0 * * * *"] }, 6 7 "d1_databases": [ 7 8 { 8 9 "binding": "DB",