import { Router } from 'express'; import { desc } from 'drizzle-orm'; import { db } from '../db/client'; import { arbaShowsCache } from '../db/schema'; import { toSnakeCase } from '../utils/case'; import { fetchArbaShows, ArbaShow } from '../lib/arba'; const router = Router(); // ARBA only updates Mon–Fri during business hours, so the cache is considered // fresh for a day; the first request after that triggers a re-scrape. const STALE_MS = 24 * 60 * 60 * 1000; interface CacheEntry { fetchedAt: string; shows: ArbaShow[]; } // Latest (and only) cache row, parsed back into objects — or null if never run. function readCache(): CacheEntry | null { const row = db.select().from(arbaShowsCache).orderBy(desc(arbaShowsCache.id)).limit(1).get(); if (!row) return null; return { fetchedAt: row.fetchedAt, shows: JSON.parse(row.data) as ArbaShow[] }; } // Scrape ARBA and replace the cache with the fresh result (one row only). async function refreshCache(): Promise { const shows = await fetchArbaShows(); const fetchedAt = new Date().toISOString(); const data = JSON.stringify(shows); db.transaction((tx) => { tx.delete(arbaShowsCache).run(); tx.insert(arbaShowsCache).values({ fetchedAt, data }).run(); }); return { fetchedAt, shows }; } function isStale(entry: CacheEntry): boolean { const fetchedMs = Date.parse(entry.fetchedAt); return Number.isNaN(fetchedMs) || Date.now() - fetchedMs > STALE_MS; } function respond(res: import('express').Response, entry: CacheEntry, stale: boolean): void { res.json({ fetched_at: entry.fetchedAt, stale, shows: entry.shows.map((show) => toSnakeCase(show as unknown as Record)), }); } // Return the cached ARBA listing, re-scraping first if the cache is missing or // older than STALE_MS. If a needed scrape fails but we still hold a cache, we // serve the stale cache (flagged) rather than erroring — the design doc's "cache // remains available until it can be refreshed". router.get('/arba', async (req, res, next) => { try { let entry = readCache(); if (!entry || isStale(entry)) { try { entry = await refreshCache(); respond(res, entry, false); return; } catch (err) { if (!entry) { res .status(502) .json({ error: `Could not load ARBA shows: ${(err as Error).message}` }); return; } // Fall through and serve the stale cache we already have. console.error('ARBA refresh failed, serving stale cache:', err); respond(res, entry, true); return; } } respond(res, entry, false); } catch (err) { next(err); } }); // Force a re-scrape. On failure the previous cache is left untouched. router.post('/arba/refresh', async (req, res) => { try { const entry = await refreshCache(); respond(res, entry, false); } catch (err) { res.status(502).json({ error: `ARBA refresh failed: ${(err as Error).message}` }); } }); export default router;