perf: cache online API endpoints with Redis-first cache
- Upgrade lib/cache.ts: Redis-first cached() with in-memory fallback (was in-memory only, broken across PM2 instances) - Cache /api/online user list (10s TTL, was uncached per-request) eliminates DB query on every poll request - Add uncached() invalidation helper for write-after-cache patterns - 0 tsc errors, 583 tests passing
This commit is contained in:
1 parent
ef5e706ee1
commit
d1807ca814
2 files changed
+64
-14
No files matched your search
+53
-9
@@ -1,17 +1,61 @@
|
||||
type CacheEntry<T> = { data: T; expiresAt: number };
|
||||
const store = new Map<string, CacheEntry<unknown>>();
|
||||
import "server-only";
|
||||
|
||||
export function cached<T>(
|
||||
import { redis } from "@/lib/redis";
|
||||
|
||||
type CacheEntry<T> = { data: T; expiresAt: number };
|
||||
const memory = new Map<string, CacheEntry<unknown>>();
|
||||
|
||||
/**
|
||||
* Redis-first cached query with an in-memory fallback.
|
||||
* Use for read-heavy endpoints polled by the browser (online count, etc.).
|
||||
*/
|
||||
export async function cached<T>(
|
||||
key: string,
|
||||
ttlMs: number,
|
||||
fn: () => Promise<T>,
|
||||
): Promise<T> {
|
||||
const existing = store.get(key);
|
||||
const ttlSec = Math.ceil(ttlMs / 1000);
|
||||
|
||||
// Redis path (shared across instances).
|
||||
if (redis && redis.status !== "end") {
|
||||
try {
|
||||
const cached = await redis.get(key);
|
||||
if (cached !== null && cached !== undefined) {
|
||||
return JSON.parse(cached) as T;
|
||||
}
|
||||
} catch {
|
||||
/* fall through to DB / memory */
|
||||
}
|
||||
}
|
||||
|
||||
// In-memory fallback (single-instance fast path).
|
||||
const existing = memory.get(key);
|
||||
if (existing && existing.expiresAt > Date.now()) {
|
||||
return Promise.resolve(existing.data as T);
|
||||
return existing.data as T;
|
||||
}
|
||||
|
||||
const data = await fn();
|
||||
|
||||
if (redis && redis.status !== "end") {
|
||||
try {
|
||||
await redis.setex(key, ttlSec, JSON.stringify(data));
|
||||
} catch {
|
||||
/* non-critical: memory cache still works */
|
||||
}
|
||||
}
|
||||
memory.set(key, { data, expiresAt: Date.now() + ttlMs });
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/** Invalidate a single cache key across Redis + memory (call after writes). */
|
||||
export async function uncached(key: string): Promise<void> {
|
||||
memory.delete(key);
|
||||
if (redis && redis.status !== "end") {
|
||||
try {
|
||||
await redis.del(key);
|
||||
} catch {
|
||||
/* non-critical */
|
||||
}
|
||||
}
|
||||
return fn().then((data) => {
|
||||
store.set(key, { data, expiresAt: Date.now() + ttlMs });
|
||||
return data;
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user