UNPKG

@routup/rate-limit

Version:
188 lines (187 loc) 5.88 kB
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