Files
EpicNext-Cms/src/lib/cache.ts
T
openhands d1807ca814
CI / check (push) Successful in 31s
CI / release (push) Skipped
CI / deploy (push) Successful in 1m27s
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
2026-07-31 15:09:56 +02:00

62 lines
1.4 KiB
TypeScript

import "server-only";
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 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 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 */
}
}
}