Files
libnovel/ui/src/lib/server/cache.ts
Admin 69818089a6
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
perf(ui): eliminate full listBooks() scan on every page load
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
2026-03-26 16:14:47 +05:00

73 lines
1.8 KiB
TypeScript

/**
* Generic Valkey (Redis-compatible) cache.
*
* Reuses the same ioredis singleton from presignCache.ts but exposes a
* simple typed get/set/invalidate API for arbitrary JSON values.
*
* Usage:
* const books = await cache.get<Book[]>('books:all');
* await cache.set('books:all', books, 5 * 60);
* await cache.invalidate('books:all');
*/
import Redis from 'ioredis';
let _client: Redis | null = null;
function client(): Redis {
if (!_client) {
const url = process.env.VALKEY_URL ?? 'redis://valkey:6379';
_client = new Redis(url, {
lazyConnect: false,
enableOfflineQueue: true,
maxRetriesPerRequest: 2
});
_client.on('error', (err: Error) => {
console.error('[cache] Valkey error:', err.message);
});
}
return _client;
}
/** Return the cached value for key, or null if absent / expired / error. */
export async function get<T>(key: string): Promise<T | null> {
try {
const raw = await client().get(key);
if (!raw) return null;
return JSON.parse(raw) as T;
} catch {
return null;
}
}
/**
* Store a value under key for ttlSeconds seconds.
* Silently no-ops on Valkey errors so callers never crash.
*/
export async function set<T>(key: string, value: T, ttlSeconds: number): Promise<void> {
try {
await client().set(key, JSON.stringify(value), 'EX', ttlSeconds);
} catch {
// non-fatal
}
}
/** Delete a key immediately (e.g. after a write that invalidates it). */
export async function invalidate(key: string): Promise<void> {
try {
await client().del(key);
} catch {
// non-fatal
}
}
/** Invalidate all keys matching a glob pattern (e.g. 'books:*'). */
export async function invalidatePattern(pattern: string): Promise<void> {
try {
const keys = await client().keys(pattern);
if (keys.length > 0) await client().del(...keys);
} catch {
// non-fatal
}
}