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

- 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:
root
2026-04-06 20:33:46 +05:00
parent 75e6a870d3
commit b6904bcb6e
8 changed files with 131 additions and 33 deletions

View File

@@ -1,6 +1,6 @@
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
import { listAudioCache, listAudioJobs, type AudioCacheEntry, type AudioJob } from '$lib/server/pocketbase';
import { listAudioJobs, type AudioCacheEntry, type AudioJob } from '$lib/server/pocketbase';
import { log } from '$lib/server/logger';
export const load: PageServerLoad = async ({ locals }) => {
@@ -8,16 +8,20 @@ export const load: PageServerLoad = async ({ locals }) => {
redirect(302, '/');
}
const [entries, jobs] = await Promise.all([
listAudioCache().catch((e): AudioCacheEntry[] => {
log.warn('admin/audio', 'failed to load audio cache', { err: String(e) });
return [];
}),
listAudioJobs().catch((e): AudioJob[] => {
log.warn('admin/audio', 'failed to load audio jobs', { err: String(e) });
return [];
})
]);
const jobs = await listAudioJobs().catch((e): AudioJob[] => {
log.warn('admin/audio', 'failed to load audio jobs', { err: String(e) });
return [];
});
// Derive cache entries from done jobs — no second query needed.
const entries: AudioCacheEntry[] = jobs
.filter((j) => j.status === 'done')
.map((j) => ({
id: j.id,
cache_key: j.cache_key,
filename: `${j.cache_key}.mp3`,
updated: j.finished
}));
return { entries, jobs };
};

View File

@@ -1,6 +1,5 @@
<script lang="ts">
import { untrack } from 'svelte';
import { invalidateAll } from '$app/navigation';
import type { PageData } from './$types';
import type { AudioJob, AudioCacheEntry } from '$lib/server/pocketbase';
import * as m from '$lib/paraglide/messages.js';
@@ -21,8 +20,17 @@
$effect(() => {
if (!hasInFlight) return;
const id = setInterval(() => {
invalidateAll();
const id = setInterval(async () => {
const res = await fetch('/api/admin/audio-jobs').catch(() => null);
if (res?.ok) {
const body = await res.json().catch(() => null);
if (body?.jobs) {
jobs = body.jobs;
entries = (body.jobs as AudioJob[])
.filter((j) => j.status === 'done')
.map((j) => ({ id: j.id, cache_key: j.cache_key, filename: `${j.cache_key}.mp3`, updated: j.finished }));
}
}
}, 3000);
return () => clearInterval(id);
});

View File

@@ -1,6 +1,6 @@
import { redirect } from '@sveltejs/kit';
import type { Actions, PageServerLoad } from './$types';
import { listBooks, listTranslationJobs, type TranslationJob } from '$lib/server/pocketbase';
import { listBookSlugs, listTranslationJobs, type TranslationJob } from '$lib/server/pocketbase';
import { backendFetch } from '$lib/server/scraper';
import { log } from '$lib/server/logger';
@@ -10,8 +10,8 @@ export const load: PageServerLoad = async ({ locals }) => {
}
const [books, jobs] = await Promise.all([
listBooks().catch((e): Awaited<ReturnType<typeof listBooks>> => {
log.warn('admin/translation', 'failed to load books', { err: String(e) });
listBookSlugs().catch((e): Awaited<ReturnType<typeof listBookSlugs>> => {
log.warn('admin/translation', 'failed to load book slugs', { err: String(e) });
return [];
}),
listTranslationJobs().catch((e): TranslationJob[] => {

View File

@@ -1,6 +1,5 @@
<script lang="ts">
import { untrack } from 'svelte';
import { invalidateAll } from '$app/navigation';
import { enhance } from '$app/forms';
import type { PageData, ActionData } from './$types';
import type { TranslationJob } from '$lib/server/pocketbase';
@@ -19,8 +18,12 @@
$effect(() => {
if (!hasInFlight) return;
const id = setInterval(() => {
invalidateAll();
const id = setInterval(async () => {
const res = await fetch('/api/admin/translation-jobs').catch(() => null);
if (res?.ok) {
const body = await res.json().catch(() => null);
if (body?.jobs) jobs = body.jobs;
}
}, 3000);
return () => clearInterval(id);
});

View File

@@ -0,0 +1,16 @@
import { json, error } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { listAudioJobs } from '$lib/server/pocketbase';
/**
* GET /api/admin/audio-jobs
* Returns the current audio jobs list (served from 30 s Valkey cache).
* Used by the admin audio page for lightweight polling instead of invalidateAll().
*/
export const GET: RequestHandler = async ({ locals }) => {
if (!locals.user || locals.user.role !== 'admin') {
throw error(403, 'Forbidden');
}
const jobs = await listAudioJobs().catch(() => []);
return json({ jobs });
};

View File

@@ -0,0 +1,16 @@
import { json, error } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { listScrapingTasks } from '$lib/server/pocketbase';
/**
* GET /api/admin/scrape-tasks
* Returns the current scraping task list (served from 30 s Valkey cache).
* Used by the admin scrape page for lightweight polling instead of invalidateAll().
*/
export const GET: RequestHandler = async ({ locals }) => {
if (!locals.user || locals.user.role !== 'admin') {
throw error(403, 'Forbidden');
}
const tasks = await listScrapingTasks().catch(() => []);
return json({ tasks });
};

View File

@@ -0,0 +1,16 @@
import { json, error } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { listTranslationJobs } from '$lib/server/pocketbase';
/**
* GET /api/admin/translation-jobs
* Returns the current translation jobs list (served from 30 s Valkey cache).
* Used by the admin translation page for lightweight polling instead of invalidateAll().
*/
export const GET: RequestHandler = async ({ locals }) => {
if (!locals.user || locals.user.role !== 'admin') {
throw error(403, 'Forbidden');
}
const jobs = await listTranslationJobs().catch(() => []);
return json({ jobs });
};