export type RequestRateLimitStore = Map; export type RequestRateLimitDecision = | { readonly ok: true } | { readonly ok: false; readonly retryAfterSeconds: number }; export type RequestRateLimitInput = Readonly<{ key: string; limit: number; windowMs: number; now?: number; store?: RequestRateLimitStore; }>; const state = globalThis as typeof globalThis & { jyotishaRequestRateLimits?: RequestRateLimitStore; }; export const requestRateLimits = { locationSearch: { limit: 20, windowMs: 60_000 }, locationTimezone: { limit: 30, windowMs: 60_000 }, dailyStarlanguage: { limit: 10, windowMs: 60_000 }, synastry: { limit: 8, windowMs: 60 * 60_000 }, sessionWrite: { limit: 40, windowMs: 60_000 }, } as const; export type RequestRateLimitBucket = keyof typeof requestRateLimits; function defaultStore(): RequestRateLimitStore { state.jyotishaRequestRateLimits ??= new Map(); return state.jyotishaRequestRateLimits; } export function consumeRequestRateLimit(input: RequestRateLimitInput): RequestRateLimitDecision { const now = input.now ?? Date.now(); const store = input.store ?? defaultStore(); const windowStart = now - input.windowMs; const recent = (store.get(input.key) ?? []).filter((timestamp) => timestamp > windowStart); if (recent.length >= input.limit) { const retryAfterSeconds = Math.max(1, Math.ceil((recent[0]! + input.windowMs - now) / 1000)); store.set(input.key, recent); return { ok: false, retryAfterSeconds }; } recent.push(now); store.set(input.key, recent); return { ok: true }; } export function consumeUserRequestRateLimit( bucket: RequestRateLimitBucket, userId: string, now?: number, ): RequestRateLimitDecision { const spec = requestRateLimits[bucket]; return consumeRequestRateLimit({ key: `${bucket}:${userId}`, limit: spec.limit, windowMs: spec.windowMs, now, }); }