MyKindlebox/mykindlebox/server/routes/shows.ts

95 lines
3.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<CacheEntry> {
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<string, unknown>)),
});
}
// 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;