perf: cache admin job lists + targeted polling for audio/translation pages
Some checks failed
Release / Test backend (push) Successful in 41s
Release / Check ui (push) Failing after 34s
Release / Upload source maps (push) Has been skipped
Release / Docker / ui (push) Has been skipped
Release / Docker / caddy (push) Successful in 40s
Release / Docker / runner (push) Has been cancelled
Release / Gitea Release (push) Has been cancelled
Release / Docker / backend (push) Has been cancelled
Some checks failed
Release / Test backend (push) Successful in 41s
Release / Check ui (push) Failing after 34s
Release / Upload source maps (push) Has been skipped
Release / Docker / ui (push) Has been skipped
Release / Docker / caddy (push) Successful in 40s
Release / Docker / runner (push) Has been cancelled
Release / Gitea Release (push) Has been cancelled
Release / Docker / backend (push) Has been cancelled
- Add 30s Valkey cache to listScrapingTasks, listAudioJobs, listTranslationJobs (use listN(500) instead of unbounded listAll to cap at one request) - Delete listAudioCache() — derive AudioCacheEntry[] from jobs in server load - Add listBookSlugs() with 10min cache — replaces full listBooks() in translation load - Add GET /api/admin/audio-jobs, /api/admin/translation-jobs, /api/admin/scrape-tasks (lightweight polling endpoints backed by the Valkey cache) - Replace invalidateAll() interval polling in audio+translation pages with targeted fetch to the new endpoints (avoids re-running full server load)
This commit is contained in:
@@ -248,6 +248,14 @@ const RATINGS_CACHE_TTL = 5 * 60; // 5 minutes
|
||||
const HOME_STATS_CACHE_KEY = 'home:stats';
|
||||
const HOME_STATS_CACHE_TTL = 10 * 60; // 10 minutes — counts don't need to be exact
|
||||
|
||||
const SCRAPING_TASKS_CACHE_KEY = 'admin:scraping_tasks';
|
||||
const AUDIO_JOBS_CACHE_KEY = 'admin:audio_jobs';
|
||||
const TRANSLATION_JOBS_CACHE_KEY = 'admin:translation_jobs';
|
||||
const ADMIN_JOBS_CACHE_TTL = 30; // 30 seconds — admin views poll frequently
|
||||
|
||||
const BOOK_SLUGS_CACHE_KEY = 'books:slugs';
|
||||
const BOOK_SLUGS_CACHE_TTL = 10 * 60; // 10 minutes — slugs change rarely
|
||||
|
||||
async function getAllRatings(): Promise<BookRating[]> {
|
||||
const cached = await cache.get<BookRating[]>(RATINGS_CACHE_KEY);
|
||||
if (cached) return cached;
|
||||
@@ -272,6 +280,31 @@ export async function listBooks(): Promise<Book[]> {
|
||||
return books;
|
||||
}
|
||||
|
||||
export interface BookSlug {
|
||||
slug: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns only the slug and title of every book. Cheaper than listBooks() —
|
||||
* used for datalist autocomplete in admin forms. Cached for 10 minutes.
|
||||
*/
|
||||
export async function listBookSlugs(): Promise<BookSlug[]> {
|
||||
const cached = await cache.get<BookSlug[]>(BOOK_SLUGS_CACHE_KEY);
|
||||
if (cached) return cached;
|
||||
// Re-use full books cache if already warm — avoids a second PocketBase call.
|
||||
const fullCached = await cache.get<Book[]>(BOOKS_CACHE_KEY);
|
||||
if (fullCached) {
|
||||
const slugs = fullCached.map((b) => ({ slug: b.slug, title: b.title }));
|
||||
await cache.set(BOOK_SLUGS_CACHE_KEY, slugs, BOOK_SLUGS_CACHE_TTL);
|
||||
return slugs;
|
||||
}
|
||||
const items = await listAll<BookSlug>('books', '', '+title').catch(() => [] as BookSlug[]);
|
||||
const slugs = items.map((b) => ({ slug: b.slug, title: b.title }));
|
||||
await cache.set(BOOK_SLUGS_CACHE_KEY, slugs, BOOK_SLUGS_CACHE_TTL);
|
||||
return slugs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch only the books whose slugs are in the given set.
|
||||
* Uses PocketBase filter `slug IN (...)` — a single request regardless of how
|
||||
@@ -1052,16 +1085,6 @@ export interface AudioCacheEntry {
|
||||
updated: string;
|
||||
}
|
||||
|
||||
export async function listAudioCache(): Promise<AudioCacheEntry[]> {
|
||||
const jobs = await listAll<AudioJob>('audio_jobs', 'status="done"', '-finished');
|
||||
return jobs.map((j) => ({
|
||||
id: j.id,
|
||||
cache_key: j.cache_key,
|
||||
filename: `${j.cache_key}.mp3`,
|
||||
updated: j.finished
|
||||
}));
|
||||
}
|
||||
|
||||
// ─── Scraping tasks ───────────────────────────────────────────────────────────
|
||||
|
||||
export interface ScrapingTask {
|
||||
@@ -1081,7 +1104,11 @@ export interface ScrapingTask {
|
||||
}
|
||||
|
||||
export async function listScrapingTasks(): Promise<ScrapingTask[]> {
|
||||
return listAll<ScrapingTask>('scraping_tasks', '', '-started');
|
||||
const cached = await cache.get<ScrapingTask[]>(SCRAPING_TASKS_CACHE_KEY);
|
||||
if (cached) return cached;
|
||||
const tasks = await listN<ScrapingTask>('scraping_tasks', 500, '', '-started');
|
||||
await cache.set(SCRAPING_TASKS_CACHE_KEY, tasks, ADMIN_JOBS_CACHE_TTL);
|
||||
return tasks;
|
||||
}
|
||||
|
||||
export async function getScrapingTask(id: string): Promise<ScrapingTask | null> {
|
||||
@@ -1103,7 +1130,11 @@ export interface AudioJob {
|
||||
}
|
||||
|
||||
export async function listAudioJobs(): Promise<AudioJob[]> {
|
||||
return listAll<AudioJob>('audio_jobs', '', '-started');
|
||||
const cached = await cache.get<AudioJob[]>(AUDIO_JOBS_CACHE_KEY);
|
||||
if (cached) return cached;
|
||||
const jobs = await listN<AudioJob>('audio_jobs', 500, '', '-started');
|
||||
await cache.set(AUDIO_JOBS_CACHE_KEY, jobs, ADMIN_JOBS_CACHE_TTL);
|
||||
return jobs;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1130,7 +1161,11 @@ export interface TranslationJob {
|
||||
}
|
||||
|
||||
export async function listTranslationJobs(): Promise<TranslationJob[]> {
|
||||
return listAll<TranslationJob>('translation_jobs', '', '-started');
|
||||
const cached = await cache.get<TranslationJob[]>(TRANSLATION_JOBS_CACHE_KEY);
|
||||
if (cached) return cached;
|
||||
const jobs = await listN<TranslationJob>('translation_jobs', 500, '', '-started');
|
||||
await cache.set(TRANSLATION_JOBS_CACHE_KEY, jobs, ADMIN_JOBS_CACHE_TTL);
|
||||
return jobs;
|
||||
}
|
||||
|
||||
export async function getAudioTime(
|
||||
|
||||
Reference in New Issue
Block a user