import { valkey } from "./valkey.ts"; /** * Implements a sliding window rate limiter backed by Valkey using Sorted Sets. * * @param key - The unique identifier for the limit (e.g. "rate:public:ip:192.168.1.1") * @param limit - Maximum requests allowed in the window. * @param windowMs - Size of the window in milliseconds. * @returns boolean - true if allowed, false if limit exceeded. */ export let checkRateLimit = async function checkRateLimit( key: string, limit: number, windowMs: number, ): Promise { const now = Date.now(); const windowStart = now - windowMs; // Use a multi block to ensure atomicity const multi = valkey.multi(); // 1. Remove all elements outside the current window multi.zremrangebyscore(key, 0, windowStart); // 2. Add the current request timestamp // Using the timestamp itself as the member and score. // To avoid collisions if multiple requests happen in the exact same millisecond, // we could append a random string, but for simple sliding window, // just the timestamp with a random suffix is safer. const member = `${now}-${crypto.randomUUID()}`; multi.zadd(key, now, member); // 3. Count elements in the current window multi.zcount(key, "-inf", "+inf"); // 4. Update the key's TTL to automatically clean up multi.pexpire(key, windowMs); try { const results = await multi.exec(); if (!results) { return false; // Fail closed } // The third command in multi is zcount const countResult = results[2]; if (countResult[0]) { // If there was an error executing zcount throw countResult[0]; } const currentCount = countResult[1] as number; return currentCount <= limit; } catch (error) { console.error("[RateLimit] Error executing multi block:", error); return false; // Fail closed if Valkey throws an error } }; export const rateLimitWrapper = { get checkRateLimit() { return checkRateLimit; }, set checkRateLimit(val: any) { checkRateLimit = val; }, };