Files
psu-schedule/server/limits.js

43 lines
1.5 KiB
JavaScript

/**
* 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. */
export function rateLimit({ max, windowMs, message }) {
const windows = new Map();
return (req, res, next) => {
const now = Date.now();
// Finished windows are dropped on the way, so the map holds only active clients.
for (const [key, w] of windows) if (now - w.start >= windowMs) windows.delete(key);
const w = windows.get(req.ip) ?? { start: now, count: 0 };
w.count++;
windows.set(req.ip, w);
if (w.count <= max) return next();
res.set("Retry-After", String(Math.ceil((w.start + windowMs - now) / 1000)));
res.status(429).json({ error: message });
};
}
/**
* 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 Object.assign(new Error(message), { status: 429 });
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--;
}
};
}