feat: Implement caching and rate limiting for API requests

This commit is contained in:
2026-10-18 17:20:29 +03:00
parent 3dc6cc4e9e
commit 6b68cec34a
12 changed files with 282 additions and 27 deletions

52
test/limits.test.js Normal file
View File

@@ -0,0 +1,52 @@
import test from "node:test";
import assert from "node:assert/strict";
import { rateLimit, semaphore } from "../server/limits.js";
test("Импорт: не больше двух разборов сразу, лишние ждут или получают 429", async () => {
const run = semaphore({ limit: 2, queue: 1, message: "Занято" });
let active = 0,
peak = 0;
const release = [];
const task = () =>
run(async () => {
peak = Math.max(peak, ++active);
await new Promise((resolve) => release.push(resolve));
active--;
});
const running = [task(), task(), task()];
await assert.rejects(task(), (e) => e.status === 429 && e.message === "Занято");
// Each finished task hands its slot to the one waiting.
while (release.length) {
release.shift()();
await new Promise((resolve) => setImmediate(resolve));
}
await Promise.all(running);
assert.equal(peak, 2);
// All slots are free again.
assert.equal(await run(async () => "ok"), "ok");
});
test("Импорт: лимит запросов считается по адресу клиента", () => {
const limit = rateLimit({ max: 2, windowMs: 60_000, message: "Подождите" });
const call = (ip) => {
let status = 200,
body = null,
passed = false;
const res = {
set() {},
status(code) {
status = code;
return this;
},
json(value) {
body = value;
},
};
limit({ ip }, res, () => (passed = true));
return { status, body, passed };
};
assert.ok(call("10.0.0.1").passed);
assert.ok(call("10.0.0.1").passed);
assert.deepEqual(call("10.0.0.1"), { status: 429, body: { error: "Подождите" }, passed: false });
assert.ok(call("10.0.0.2").passed);
});