import { apiError } from "./errors.js"; /** * Limits for the expensive endpoints: parsing an uploaded Word file can take a LibreOffice * conversion and seconds of CPU, so one client must not be able to occupy the server. */ /** * At most `max` requests per client address in each `windowMs`; the rest get 429. * `skip(req)` exempts a request, such as a health check, from counting. */ export function rateLimit({ max, windowMs, message, skip = () => false }) { const windows = new Map(); let swept = 0; return (req, res, next) => { if (skip(req)) return next(); const now = Date.now(); // Finished windows are dropped, at most once a second, so the map holds only active clients // and a crowd of addresses does not make every request walk the whole map. if (now - swept >= 1000) { swept = now; for (const [key, w] of windows) if (now - w.start >= windowMs) windows.delete(key); } let w = windows.get(req.ip); if (!w || now - w.start >= windowMs) windows.set(req.ip, (w = { start: now, count: 0 })); w.count++; if (w.count <= max) return next(); res.set("Retry-After", String(Math.ceil((w.start + windowMs - now) / 1000))); res.status(429).json({ error: message, code: "too_many_requests" }); }; } /** * Runs at most `limit` tasks at once; up to `queue` more wait for a free slot, * and beyond that a task is refused with a 429 error carrying `message`. */ export function semaphore({ limit, queue, message }) { let active = 0; const waiting = []; return async function run(task) { if (active < limit) active++; else if (waiting.length < queue) await new Promise((resolve) => waiting.push(resolve)); else throw apiError(429, "busy", message); try { return await task(); } finally { // The slot passes straight to the next waiting task, or frees up. const next = waiting.shift(); if (next) next(); else active--; } }; }