@stacksjs/router
Version:
The Stacks framework router.
74 lines (73 loc) • 2.4 kB
JavaScript
import { HttpError } from "@stacksjs/error-handling";
import { RateLimitError, RateLimiter, defaultIdentity } from "ts-rate-limiter";
import { getCurrentRequest } from "./request-context";
const PERIOD_SECONDS = {
second: 1,
minute: 60,
hour: 3600,
day: 86400
}, limiterCache = new Map;
function getLimiter(max, windowMs) {
const cacheKey = `${windowMs}:${max}`;
let limiter = limiterCache.get(cacheKey);
if (!limiter) {
limiter = new RateLimiter({
windowMs,
maxRequests: max,
algorithm: "fixed-window",
standardHeaders: !1,
legacyHeaders: !1
});
limiterCache.set(cacheKey, limiter);
}
return limiter;
}
function resolveIdentity(explicit) {
if (explicit !== void 0)
return explicit;
const req = getCurrentRequest();
return req ? defaultIdentity(req) : "anon";
}
export function rateLimit(key, max, options = {}) {
const id = resolveIdentity(options.identity), bucketKey = `${key}:${id}`, run = async (windowMs) => {
const limiter = getLimiter(max, windowMs);
try {
await limiter.enforce(bucketKey);
} catch (err) {
if (err instanceof RateLimitError)
throw Object.assign(new HttpError(429, "Too many requests", {
key,
max,
retryAfter: err.retryAfter
}), { headers: err.toHeaders() });
throw err;
}
};
return {
async per(period) {
const seconds = PERIOD_SECONDS[period];
if (!seconds)
throw Error(`rateLimit().per: unknown period '${period}'`);
await run(seconds * 1000);
},
async over(ttlSeconds) {
if (!Number.isFinite(ttlSeconds) || ttlSeconds <= 0)
throw Error(`rateLimit().over: ttl must be a positive number, got ${ttlSeconds}`);
await run(ttlSeconds * 1000);
}
};
}
export async function rateLimitStatus(key, max, windowSeconds, options = {}) {
const id = resolveIdentity(options.identity), bucketKey = `${key}:${id}`, result = await getLimiter(max, windowSeconds * 1000).peek(bucketKey);
if (!result)
return null;
return {
count: result.current,
limit: result.limit,
remaining: Math.max(0, result.limit - result.current)
};
}
export async function clearRateLimit(key, max, windowSeconds, options = {}) {
const id = resolveIdentity(options.identity), bucketKey = `${key}:${id}`;
await getLimiter(max, windowSeconds * 1000).reset(bucketKey);
}