feat(yt): cache based on lookback instead of constant period
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { sse } from "@/lib/db/sse-endpoints";
|
||||
|
||||
export const revalidate = 900;
|
||||
import { youtubeCache } from "@/lib/adaptive-cache";
|
||||
|
||||
export type YoutubeLiveInfo =
|
||||
| {
|
||||
@@ -60,13 +59,10 @@ export async function GET() {
|
||||
|
||||
if (!apiKey || !channelId) return NextResponse.json<YoutubeLiveInfo>("none");
|
||||
|
||||
const response = await fetch(
|
||||
const response = await youtubeCache.fetch(
|
||||
`https://www.googleapis.com/youtube/v3/search?part=snippet&channelId=${channelId}&eventType=live&type=video&maxResults=1&key=${apiKey}`,
|
||||
{
|
||||
next: {
|
||||
revalidate: 900, // refresh every 15 minutes
|
||||
},
|
||||
cache: "force-cache",
|
||||
cache: "no-store",
|
||||
},
|
||||
);
|
||||
|
||||
@@ -74,7 +70,10 @@ export async function GET() {
|
||||
throw new Error(`YouTube API error: ${response.statusText}`);
|
||||
|
||||
const data: APISearchResponse = await response.json();
|
||||
if (data.items.length <= 0) return NextResponse.json<YoutubeLiveInfo>("none");
|
||||
|
||||
if (data.items.length === 0)
|
||||
return NextResponse.json<YoutubeLiveInfo>("none");
|
||||
|
||||
const live = data.items[0];
|
||||
|
||||
const res: YoutubeLiveInfo = {
|
||||
@@ -85,5 +84,6 @@ export async function GET() {
|
||||
};
|
||||
|
||||
sse.active.pub("live", res);
|
||||
|
||||
return NextResponse.json<YoutubeLiveInfo>(res);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
// gpt'd at 3am, used as-is by another module
|
||||
|
||||
interface AdaptiveCacheOptions {
|
||||
/** Maximum requests allowed per period. */
|
||||
maxRequests: number;
|
||||
|
||||
/** Usually one day. */
|
||||
period: number;
|
||||
|
||||
/** Rolling window used to estimate request rate. Defaults to period / 2. */
|
||||
lookback?: number;
|
||||
|
||||
/** Enable caching once projected usage exceeds this fraction. Defaults to 0.5. */
|
||||
threshold?: number;
|
||||
|
||||
/** How long cached entries live. */
|
||||
ttl: number;
|
||||
|
||||
/** Cache key generator. */
|
||||
key?(req: Request): string;
|
||||
}
|
||||
|
||||
export class AdaptiveCache {
|
||||
private readonly cache = new Map<
|
||||
string,
|
||||
{
|
||||
expires: number;
|
||||
response: Response;
|
||||
}
|
||||
>();
|
||||
|
||||
private readonly requests: number[] = [];
|
||||
|
||||
constructor(private readonly options: AdaptiveCacheOptions) {
|
||||
options.lookback ??= options.period / 2;
|
||||
options.threshold ??= 0.5;
|
||||
options.key ??= (req) => req.url;
|
||||
}
|
||||
|
||||
async fetch(
|
||||
input: RequestInfo | URL,
|
||||
init?: RequestInit,
|
||||
fetcher: typeof fetch = fetch,
|
||||
): Promise<Response> {
|
||||
const req = input instanceof Request ? input : new Request(input, init);
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
this.cleanup(now);
|
||||
|
||||
const key = this.options.key!(req);
|
||||
|
||||
if (this.shouldCache()) {
|
||||
const cached = this.cache.get(key);
|
||||
if (cached && cached.expires > now) return cached.response.clone();
|
||||
}
|
||||
|
||||
const res = await fetcher(req);
|
||||
|
||||
if (res.ok) {
|
||||
this.requests.push(now);
|
||||
|
||||
this.cache.set(key, {
|
||||
expires: now + this.options.ttl,
|
||||
response: res.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
private shouldCache() {
|
||||
const projected =
|
||||
this.requests.length * (this.options.period / this.options.lookback!);
|
||||
|
||||
return projected >= this.options.maxRequests * this.options.threshold!;
|
||||
}
|
||||
|
||||
private cleanup(now: number) {
|
||||
const cutoff = now - this.options.lookback!;
|
||||
|
||||
while (this.requests.length && this.requests[0] < cutoff)
|
||||
this.requests.shift();
|
||||
|
||||
for (const [key, value] of this.cache)
|
||||
if (value.expires <= now) this.cache.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
export const youtubeCache = new AdaptiveCache({
|
||||
maxRequests: 100, // YouTube search.list requests/day
|
||||
period: 24 * 60 * 60_000,
|
||||
ttl: 15 * 60_000,
|
||||
});
|
||||
Reference in New Issue
Block a user