perf(ui): eliminate full listBooks() scan on every page load
Some checks failed
Release / Test backend (push) Failing after 10s
Release / Docker / backend (push) Has been skipped
Release / Docker / runner (push) Has been skipped
Release / Docker / caddy (push) Failing after 10s
Release / Check ui (push) Successful in 32s
CI / Test backend (pull_request) Successful in 39s
CI / Check ui (pull_request) Successful in 48s
Release / Upload source maps (push) Successful in 2m17s
CI / Docker / caddy (pull_request) Successful in 2m44s
Release / Docker / ui (push) Successful in 2m31s
Release / Gitea Release (push) Has been skipped
CI / Docker / runner (pull_request) Successful in 1m38s
CI / Docker / ui (pull_request) Successful in 1m28s
CI / Docker / backend (pull_request) Successful in 2m11s
Some checks failed
Release / Test backend (push) Failing after 10s
Release / Docker / backend (push) Has been skipped
Release / Docker / runner (push) Has been skipped
Release / Docker / caddy (push) Failing after 10s
Release / Check ui (push) Successful in 32s
CI / Test backend (pull_request) Successful in 39s
CI / Check ui (pull_request) Successful in 48s
Release / Upload source maps (push) Successful in 2m17s
CI / Docker / caddy (pull_request) Successful in 2m44s
Release / Docker / ui (push) Successful in 2m31s
Release / Gitea Release (push) Has been skipped
CI / Docker / runner (pull_request) Successful in 1m38s
CI / Docker / ui (pull_request) Successful in 1m28s
CI / Docker / backend (pull_request) Successful in 2m11s
The home, library, and /books routes were fetching all 15k books from PocketBase on every SSR request (31 sequential HTTP calls per request). Changes: - Add src/lib/server/cache.ts: generic Valkey JSON cache - Add getBooksBySlugs(): single PB query fetching only requested slugs, with fallback to the 5-min Valkey cache populated by listBooks() - listBooks(): now caches results in Valkey for 5 min (safety net for admin routes that still need the full list) - Home + /api/home: replaced listBooks()+filter with getBooksBySlugs() on progress slugs only — typically 1 PB request instead of 31 - /books + /api/library: same pattern using progress+saved slug union
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import type { PageServerLoad } from './$types';
|
||||
import {
|
||||
listBooks,
|
||||
getBooksBySlugs,
|
||||
recentlyAddedBooks,
|
||||
allProgress,
|
||||
getHomeStats,
|
||||
@@ -10,14 +10,15 @@ import { log } from '$lib/server/logger';
|
||||
import type { Book, Progress } from '$lib/server/pocketbase';
|
||||
|
||||
export const load: PageServerLoad = async ({ locals }) => {
|
||||
let allBooks: Book[] = [];
|
||||
// Step 1: fetch progress + recent books + stats in parallel.
|
||||
// We intentionally do NOT call listBooks() here — we only need books that
|
||||
// appear in the user's progress list, which is a tiny subset of 15k books.
|
||||
let recentBooks: Book[] = [];
|
||||
let progressList: Progress[] = [];
|
||||
let stats = { totalBooks: 0, totalChapters: 0 };
|
||||
|
||||
try {
|
||||
[allBooks, recentBooks, progressList, stats] = await Promise.all([
|
||||
listBooks(),
|
||||
[recentBooks, progressList, stats] = await Promise.all([
|
||||
recentlyAddedBooks(8),
|
||||
allProgress(locals.sessionId, locals.user?.id),
|
||||
getHomeStats()
|
||||
@@ -26,8 +27,14 @@ export const load: PageServerLoad = async ({ locals }) => {
|
||||
log.error('home', 'failed to load home data', { err: String(e) });
|
||||
}
|
||||
|
||||
// Build slug → book lookup
|
||||
const bookMap = new Map<string, Book>(allBooks.map((b) => [b.slug, b]));
|
||||
// Step 2: fetch only the books we actually need for continue-reading.
|
||||
// This is O(progress entries) instead of O(15k books).
|
||||
const progressSlugs = progressList.map((p) => p.slug);
|
||||
const progressBooks = progressSlugs.length > 0
|
||||
? await getBooksBySlugs(progressSlugs).catch(() => [] as Book[])
|
||||
: [];
|
||||
|
||||
const bookMap = new Map<string, Book>(progressBooks.map((b) => [b.slug, b]));
|
||||
|
||||
// Continue reading: progress entries joined with book data, most recent first
|
||||
const continueReading = progressList
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import {
|
||||
listBooks,
|
||||
getBooksBySlugs,
|
||||
recentlyAddedBooks,
|
||||
allProgress,
|
||||
getHomeStats,
|
||||
@@ -17,14 +17,12 @@ import type { Book, Progress } from '$lib/server/pocketbase';
|
||||
* Requires authentication (enforced by layout guard).
|
||||
*/
|
||||
export const GET: RequestHandler = async ({ locals }) => {
|
||||
let allBooks: Book[] = [];
|
||||
let recentBooks: Book[] = [];
|
||||
let progressList: Progress[] = [];
|
||||
let stats = { totalBooks: 0, totalChapters: 0 };
|
||||
|
||||
try {
|
||||
[allBooks, recentBooks, progressList, stats] = await Promise.all([
|
||||
listBooks(),
|
||||
[recentBooks, progressList, stats] = await Promise.all([
|
||||
recentlyAddedBooks(8),
|
||||
allProgress(locals.sessionId, locals.user?.id),
|
||||
getHomeStats()
|
||||
@@ -33,7 +31,13 @@ export const GET: RequestHandler = async ({ locals }) => {
|
||||
log.error('api/home', 'failed to load home data', { err: String(e) });
|
||||
}
|
||||
|
||||
const bookMap = new Map<string, Book>(allBooks.map((b) => [b.slug, b]));
|
||||
// Fetch only the books we actually need for continue-reading.
|
||||
const progressSlugs = progressList.map((p) => p.slug);
|
||||
const progressBooks = progressSlugs.length > 0
|
||||
? await getBooksBySlugs(progressSlugs).catch(() => [] as Book[])
|
||||
: [];
|
||||
|
||||
const bookMap = new Map<string, Book>(progressBooks.map((b) => [b.slug, b]));
|
||||
|
||||
const continueReading = progressList
|
||||
.filter((p) => bookMap.has(p.slug))
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { listBooks, allProgress, getSavedSlugs } from '$lib/server/pocketbase';
|
||||
import { getBooksBySlugs, allProgress, getSavedSlugs } from '$lib/server/pocketbase';
|
||||
import { log } from '$lib/server/logger';
|
||||
import type { Book } from '$lib/server/pocketbase';
|
||||
|
||||
/**
|
||||
* GET /api/library
|
||||
@@ -11,23 +12,25 @@ import { log } from '$lib/server/logger';
|
||||
* Response shape mirrors LibraryItem in the iOS APIClient.
|
||||
*/
|
||||
export const GET: RequestHandler = async ({ locals }) => {
|
||||
let allBooks: Awaited<ReturnType<typeof listBooks>>;
|
||||
let progressList: Awaited<ReturnType<typeof allProgress>>;
|
||||
let savedSlugs: Set<string>;
|
||||
let progressList: Awaited<ReturnType<typeof allProgress>> = [];
|
||||
let savedSlugs: Set<string> = new Set();
|
||||
|
||||
try {
|
||||
[allBooks, progressList, savedSlugs] = await Promise.all([
|
||||
listBooks(),
|
||||
[progressList, savedSlugs] = await Promise.all([
|
||||
allProgress(locals.sessionId, locals.user?.id),
|
||||
getSavedSlugs(locals.sessionId, locals.user?.id)
|
||||
]);
|
||||
} catch (e) {
|
||||
log.error('api/library', 'failed to load library data', { err: String(e) });
|
||||
allBooks = [];
|
||||
progressList = [];
|
||||
savedSlugs = new Set();
|
||||
}
|
||||
|
||||
// Fetch only the books the user actually has in their library.
|
||||
const progressSlugs = new Set(progressList.map((p) => p.slug));
|
||||
const allNeededSlugs = new Set([...progressSlugs, ...savedSlugs]);
|
||||
const books = allNeededSlugs.size > 0
|
||||
? await getBooksBySlugs(allNeededSlugs).catch(() => [] as Book[])
|
||||
: [];
|
||||
|
||||
const progressMap: Record<string, number> = {};
|
||||
const progressUpdatedMap: Record<string, string> = {};
|
||||
for (const p of progressList) {
|
||||
@@ -35,9 +38,6 @@ export const GET: RequestHandler = async ({ locals }) => {
|
||||
progressUpdatedMap[p.slug] = p.updated;
|
||||
}
|
||||
|
||||
const progressSlugs = new Set(progressList.map((p) => p.slug));
|
||||
const books = allBooks.filter((b) => progressSlugs.has(b.slug) || savedSlugs.has(b.slug));
|
||||
|
||||
const withProgress = books.filter((b) => progressSlugs.has(b.slug));
|
||||
const savedOnly = books
|
||||
.filter((b) => !progressSlugs.has(b.slug))
|
||||
|
||||
@@ -1,48 +1,42 @@
|
||||
import { error } from '@sveltejs/kit';
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { listBooks, allProgress, getSavedSlugs } from '$lib/server/pocketbase';
|
||||
import { getBooksBySlugs, allProgress, getSavedSlugs } from '$lib/server/pocketbase';
|
||||
import { log } from '$lib/server/logger';
|
||||
import type { Book } from '$lib/server/pocketbase';
|
||||
|
||||
export const load: PageServerLoad = async ({ locals }) => {
|
||||
let allBooks: Awaited<ReturnType<typeof listBooks>>;
|
||||
let progressList: Awaited<ReturnType<typeof allProgress>>;
|
||||
let savedSlugs: Set<string>;
|
||||
let progressList: Awaited<ReturnType<typeof allProgress>> = [];
|
||||
let savedSlugs: Set<string> = new Set();
|
||||
|
||||
try {
|
||||
[allBooks, progressList, savedSlugs] = await Promise.all([
|
||||
listBooks(),
|
||||
[progressList, savedSlugs] = await Promise.all([
|
||||
allProgress(locals.sessionId, locals.user?.id),
|
||||
getSavedSlugs(locals.sessionId, locals.user?.id)
|
||||
]);
|
||||
} catch (e) {
|
||||
log.error('books', 'failed to load library data', { err: String(e) });
|
||||
allBooks = [];
|
||||
progressList = [];
|
||||
savedSlugs = new Set();
|
||||
}
|
||||
|
||||
// Fetch only the books the user actually has in their library.
|
||||
const progressSlugs = new Set(progressList.map((p) => p.slug));
|
||||
const allNeededSlugs = new Set([...progressSlugs, ...savedSlugs]);
|
||||
const books = allNeededSlugs.size > 0
|
||||
? await getBooksBySlugs(allNeededSlugs).catch(() => [] as Book[])
|
||||
: [];
|
||||
|
||||
// Build a quick lookup: slug → last chapter read
|
||||
const progressMap: Record<string, number> = {};
|
||||
const progressUpdatedMap: Record<string, string> = {};
|
||||
for (const p of progressList) {
|
||||
progressMap[p.slug] = p.chapter;
|
||||
progressUpdatedMap[p.slug] = p.updated;
|
||||
}
|
||||
|
||||
// Library = books the user has started reading OR explicitly saved
|
||||
const progressSlugs = new Set(progressList.map((p) => p.slug));
|
||||
const books = allBooks.filter((b) => progressSlugs.has(b.slug) || savedSlugs.has(b.slug));
|
||||
|
||||
// Sort: books with progress first (most-recently-read order is implicit via progressList),
|
||||
// then saved-only books alphabetically.
|
||||
// Sort: books with progress first (most-recently-read), then saved-only alphabetically.
|
||||
const withProgress = books.filter((b) => progressSlugs.has(b.slug));
|
||||
const savedOnly = books
|
||||
.filter((b) => !progressSlugs.has(b.slug))
|
||||
.sort((a, b) => (a.title ?? '').localeCompare(b.title ?? ''));
|
||||
|
||||
// Re-sort withProgress by most recent progress update
|
||||
const progressUpdatedMap: Record<string, string> = {};
|
||||
for (const p of progressList) {
|
||||
progressUpdatedMap[p.slug] = p.updated;
|
||||
}
|
||||
withProgress.sort((a, b) => {
|
||||
const ta = progressUpdatedMap[a.slug] ?? '';
|
||||
const tb = progressUpdatedMap[b.slug] ?? '';
|
||||
|
||||
Reference in New Issue
Block a user