Remove buildFontUrl, measureText, uncached, invalidateCache, fetchJsonWithFlareSolver, upsertPermission, deletePermission, bulkImportPermissions, clearAllPermissions, bulkDeletePermissions, bulkDeletePhotos, userReplyTicket, closeTicketByUser. Update staff-smoke-contract test for bulkDeletePhotos removal.
50 lines
1.2 KiB
TypeScript
50 lines
1.2 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;
|
|
}
|