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
@@ -5,17 +5,23 @@
|
|||||||
// DB failure.
|
// DB failure.
|
||||||
|
|
||||||
import { apiJson } from "@/lib/api";
|
import { apiJson } from "@/lib/api";
|
||||||
|
import { cached } from "@/lib/cache";
|
||||||
import { prisma } from "@/lib/prisma";
|
import { prisma } from "@/lib/prisma";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
export async function GET(_req: Request) {
|
export async function GET(_req: Request) {
|
||||||
try {
|
try {
|
||||||
const users = await prisma.user.findMany({
|
const users = await cached(
|
||||||
where: { online: "1" },
|
"online_users",
|
||||||
select: { username: true, look: true },
|
10_000,
|
||||||
take: 100,
|
async () =>
|
||||||
});
|
prisma.user.findMany({
|
||||||
|
where: { online: "1" },
|
||||||
|
select: { username: true, look: true },
|
||||||
|
take: 100,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
return apiJson({ users });
|
return apiJson({ users });
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
+53
-9
@@ -1,17 +1,61 @@
|
|||||||
type CacheEntry<T> = { data: T; expiresAt: number };
|
import "server-only";
|
||||||
const store = new Map<string, CacheEntry<unknown>>();
|
|
||||||
|
|
||||||
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,
|
key: string,
|
||||||
ttlMs: number,
|
ttlMs: number,
|
||||||
fn: () => Promise<T>,
|
fn: () => Promise<T>,
|
||||||
): 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()) {
|
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