ai-chatbot/lib/public-api/performance.ts
2026-05-26 20:10:36 +08:00

144 lines
3.4 KiB
TypeScript

import { NextResponse } from "next/server";
type CacheStatus = "HIT" | "MISS" | "STALE" | "FALLBACK";
type PublicCacheEntry<T> = {
data: T;
expiresAt: number;
staleUntil: number;
};
const PUBLIC_API_CACHE_KEY = Symbol.for("ai-chatbot.public-api-cache");
function getStore<T>() {
const globalCache = globalThis as typeof globalThis & {
[PUBLIC_API_CACHE_KEY]?: Map<string, PublicCacheEntry<T>>;
};
if (!globalCache[PUBLIC_API_CACHE_KEY]) {
globalCache[PUBLIC_API_CACHE_KEY] = new Map<string, PublicCacheEntry<T>>();
}
return globalCache[PUBLIC_API_CACHE_KEY]!;
}
export function getEnvInt(
name: string,
fallback: number,
min: number,
max: number
) {
const value = Number.parseInt(process.env[name] || "", 10);
if (!Number.isFinite(value)) {
return fallback;
}
return Math.min(max, Math.max(min, value));
}
export function withTimeout<T>(
promise: Promise<T>,
timeoutMs: number,
label = "operation"
) {
let timer: ReturnType<typeof setTimeout> | undefined;
const timeout = new Promise<never>((_, reject) => {
timer = setTimeout(() => {
reject(new Error(`${label} timed out after ${timeoutMs}ms`));
}, timeoutMs);
});
return Promise.race([promise, timeout]).finally(() => {
if (timer) {
clearTimeout(timer);
}
});
}
export function publicJson(
data: unknown,
{
cacheStatus,
cacheControl = "private, max-age=30, stale-while-revalidate=120",
fallback = false,
startedAt,
status,
}: {
cacheStatus: CacheStatus;
cacheControl?: string;
fallback?: boolean;
startedAt: number;
status?: number;
}
) {
const durationMs = Math.max(0, Date.now() - startedAt);
const response = NextResponse.json(data, status ? { status } : undefined);
response.headers.set("Cache-Control", cacheControl);
response.headers.set("X-Cache", cacheStatus);
response.headers.set("X-Fallback", fallback ? "1" : "0");
response.headers.set("Server-Timing", `app;dur=${durationMs}`);
return response;
}
export async function cachedPublicJson<T extends Record<string, unknown>>({
cacheKey,
fallbackData,
load,
timeoutMs = 1800,
ttlMs = 30_000,
staleMs = 5 * 60_000,
cacheControl,
}: {
cacheKey: string;
fallbackData: T;
load: () => Promise<T>;
timeoutMs?: number;
ttlMs?: number;
staleMs?: number;
cacheControl?: string;
}) {
const startedAt = Date.now();
const store = getStore<T>();
const cached = store.get(cacheKey);
if (cached && cached.expiresAt > startedAt) {
return publicJson(cached.data, {
cacheStatus: "HIT",
cacheControl,
startedAt,
});
}
try {
const data = await withTimeout(load(), timeoutMs, cacheKey);
store.set(cacheKey, {
data,
expiresAt: Date.now() + ttlMs,
staleUntil: Date.now() + ttlMs + staleMs,
});
return publicJson(data, {
cacheStatus: "MISS",
cacheControl,
startedAt,
});
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.warn(`[Public API] ${cacheKey} fallback: ${message}`);
if (cached && cached.staleUntil > Date.now()) {
return publicJson(cached.data, {
cacheStatus: "STALE",
cacheControl,
fallback: true,
startedAt,
});
}
return publicJson(fallbackData, {
cacheStatus: "FALLBACK",
cacheControl,
fallback: true,
startedAt,
});
}
}