54 lines
1.9 KiB
JavaScript
54 lines
1.9 KiB
JavaScript
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--;
|
|
}
|
|
};
|
|
}
|