@routup/rate-limit
Version:
Routup rate limiter.
188 lines (187 loc) • 5.88 kB
JavaScript
import { HeaderName, defineCoreHandler, getRequestIP } from "routup";
//#region src/utils/is-object.ts
function isObject(item) {
return !!item && typeof item === "object" && !Array.isArray(item);
}
//#endregion
//#region src/constants.ts
const RETRY_AGAIN_MESSAGE = "Too many requests, please try again later.";
//#endregion
//#region src/store/utils.ts
function calculateNextResetTime(windowMs) {
const resetTime = /* @__PURE__ */ new Date();
resetTime.setMilliseconds(resetTime.getMilliseconds() + windowMs);
return resetTime;
}
//#endregion
//#region src/store/memory.ts
var MemoryStore = class {
/**
* The duration of time before which all hit counts are reset (in milliseconds).
*/
windowMs;
/**
* The map that stores the number of hits for each client in memory.
*/
hits;
/**
* The time at which all hit counts will be reset.
*/
resetTime;
/**
* Reference to the active timer.
*/
interval;
/**
* Method that initializes the store.
*
* @param options {Options} - The options used to setup the middleware.
*/
init(options) {
this.windowMs = options.windowMs;
this.resetTime = calculateNextResetTime(this.windowMs);
this.hits = {};
this.interval = setInterval(async () => {
await this.resetAll();
}, this.windowMs);
if (this.interval.unref) this.interval.unref();
}
/**
* Method to increment a client's hit counter.
*
* @param key {string} - The identifier for a client.
*
* @returns {IncrementResponse} - The number of hits and reset time for that client.
*
* @public
*/
async increment(key) {
const totalHits = (this.hits[key] ?? 0) + 1;
this.hits[key] = totalHits;
return {
totalHits,
resetTime: this.resetTime
};
}
/**
* Method to decrement a client's hit counter.
*
* @param key {string} - The identifier for a client.
*
* @public
*/
async decrement(key) {
const current = this.hits[key];
if (current) this.hits[key] = current - 1;
}
/**
* Method to reset a client's hit counter.
*
* @param key {string} - The identifier for a client.
*
* @public
*/
async reset(key) {
delete this.hits[key];
}
/**
* Method to reset everyone's hit counter.
*
* @public
*/
/* istanbul ignore next */
async resetAll() {
this.hits = {};
this.resetTime = calculateNextResetTime(this.windowMs);
}
};
//#endregion
//#region src/utils/options.ts
function normalizeHandlerOptions(input = {}) {
const options = {
windowMs: 60 * 1e3,
max: 5,
message: RETRY_AGAIN_MESSAGE,
statusCode: 429,
skipFailedRequest: false,
skipSuccessfulRequest: false,
requestWasSuccessful: (_event, response) => response.status < 400,
skip: (_event) => false,
keyGenerator: (event) => getRequestIP(event, { trustProxy: true }) || "127.0.0.1",
async handler(event, _optionsUsed) {
const message = typeof options.message === "function" ? await options.message(event) : options.message;
event.response.status = options.statusCode;
return message ?? "Too many requests, please try again later.";
},
...input,
store: input.store || new MemoryStore()
};
return options;
}
//#endregion
//#region src/request.ts
const RateLimitSymbol = Symbol.for("@routup/rate-limit:ReqRateLimit");
function useRequestRateLimitInfo(event, key) {
if (RateLimitSymbol in event.store) {
if (typeof key === "string") return event.store[RateLimitSymbol][key];
return event.store[RateLimitSymbol];
}
return typeof key === "string" ? void 0 : {};
}
function setRequestRateLimitInfo(event, key, value) {
const existing = RateLimitSymbol in event.store ? event.store[RateLimitSymbol] : void 0;
if (isObject(key)) event.store[RateLimitSymbol] = existing ? {
...existing,
...key
} : key;
else if (existing) existing[key] = value;
else event.store[RateLimitSymbol] = { [key]: value };
}
//#endregion
//#region src/handler.ts
function createHandler(input) {
const options = normalizeHandlerOptions({ ...input || {} });
if (typeof options.store.init === "function") options.store.init(options);
return defineCoreHandler(async (event) => {
if (await options.skip(event)) return event.next();
const key = await options.keyGenerator(event);
const { totalHits, resetTime } = await options.store.increment(key);
const maxHits = await (typeof options.max === "function" ? options.max(event) : options.max);
setRequestRateLimitInfo(event, {
limit: maxHits,
current: totalHits,
remaining: Math.max(maxHits - totalHits, 0),
resetTime
});
event.response.headers.set(HeaderName.RATE_LIMIT_LIMIT, String(maxHits));
event.response.headers.set(HeaderName.RATE_LIMIT_REMAINING, String(Math.max(maxHits - totalHits, 0)));
if (resetTime) {
const deltaSeconds = Math.ceil((resetTime.getTime() - Date.now()) / 1e3);
event.response.headers.set(HeaderName.RATE_LIMIT_RESET, String(Math.max(0, deltaSeconds)));
}
if (maxHits && totalHits > maxHits) {
event.response.headers.set(HeaderName.RETRY_AFTER, String(Math.ceil(options.windowMs / 1e3)));
return options.handler(event, options);
}
const response = await event.next();
if (options.skipFailedRequest || options.skipSuccessfulRequest) {
const wasSuccessful = response ? options.requestWasSuccessful(event, response) : false;
if (options.skipFailedRequest && !wasSuccessful || options.skipSuccessfulRequest && wasSuccessful) {
await options.store.decrement(key);
setRequestRateLimitInfo(event, "remaining", Math.max(maxHits - totalHits + 1, 0));
}
}
return response;
});
}
//#endregion
//#region src/module.ts
function rateLimit(options) {
return createHandler(options);
}
//#endregion
//#region src/index.ts
var src_default = rateLimit;
//#endregion
export { MemoryStore, RETRY_AGAIN_MESSAGE, calculateNextResetTime, createHandler, src_default as default, isObject, normalizeHandlerOptions, rateLimit, setRequestRateLimitInfo, useRequestRateLimitInfo };
//# sourceMappingURL=index.mjs.map