diff --git a/src/app/api/online/route.ts b/src/app/api/online/route.ts index fced5e15ce..761e923b08 100644 --- a/src/app/api/online/route.ts +++ b/src/app/api/online/route.ts @@ -5,17 +5,23 @@ // DB failure. import { apiJson } from "@/lib/api"; +import { cached } from "@/lib/cache"; import { prisma } from "@/lib/prisma"; export const dynamic = "force-dynamic"; export async function GET(_req: Request) { try { - const users = await prisma.user.findMany({ - where: { online: "1" }, - select: { username: true, look: true }, - take: 100, - }); + const users = await cached( + "online_users", + 10_000, + async () => + prisma.user.findMany({ + where: { online: "1" }, + select: { username: true, look: true }, + take: 100, + }), + ); return apiJson({ users }); } catch { diff --git a/src/lib/cache.ts b/src/lib/cache.ts index 620a1cc643..dcf8ded71e 100644 --- a/src/lib/cache.ts +++ b/src/lib/cache.ts @@ -1,17 +1,61 @@ -type CacheEntry = { data: T; expiresAt: number }; -const store = new Map>(); +import "server-only"; -export function cached( +import { redis } from "@/lib/redis"; + +type CacheEntry = { data: T; expiresAt: number }; +const memory = new Map>(); + +/** + * 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( key: string, ttlMs: number, fn: () => Promise, ): Promise { - 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 { + 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; - }); }