123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180 |
- 'use strict';
- Object.defineProperty(exports, '__esModule', { value: true });
- const scripts = {
- increment: `
- local totalHits = redis.call("INCR", KEYS[1])
- local timeToExpire = redis.call("PTTL", KEYS[1])
- if timeToExpire <= 0 or ARGV[1] == "1"
- then
- redis.call("PEXPIRE", KEYS[1], tonumber(ARGV[2]))
- timeToExpire = tonumber(ARGV[2])
- end
- return { totalHits, timeToExpire }
- `.replaceAll(/^\s+/gm, "").trim(),
- get: `
- local totalHits = redis.call("GET", KEYS[1])
- local timeToExpire = redis.call("PTTL", KEYS[1])
- return { totalHits, timeToExpire }
- `.replaceAll(/^\s+/gm, "").trim()
- };
- const toInt = (input) => {
- if (typeof input === "number")
- return input;
- return Number.parseInt((input ?? "").toString(), 10);
- };
- const parseScriptResponse = (results) => {
- if (!Array.isArray(results))
- throw new TypeError("Expected result to be array of values");
- if (results.length !== 2)
- throw new Error(`Expected 2 replies, got ${results.length}`);
- const totalHits = results[0] === false ? 0 : toInt(results[0]);
- const timeToExpire = toInt(results[1]);
- const resetTime = new Date(Date.now() + timeToExpire);
- return { totalHits, resetTime };
- };
- class RedisStore {
- /**
- * The function used to send raw commands to Redis.
- */
- sendCommand;
- /**
- * The text to prepend to the key in Redis.
- */
- prefix;
- /**
- * Whether to reset the expiry for a particular key whenever its hit count
- * changes.
- */
- resetExpiryOnChange;
- /**
- * Stores the loaded SHA1s of the LUA scripts used for executing the increment
- * and get key operations.
- */
- incrementScriptSha;
- getScriptSha;
- /**
- * The number of milliseconds to remember that user's requests.
- */
- windowMs;
- /**
- * @constructor for `RedisStore`.
- *
- * @param options {Options} - The configuration options for the store.
- */
- constructor(options) {
- this.sendCommand = options.sendCommand;
- this.prefix = options.prefix ?? "rl:";
- this.resetExpiryOnChange = options.resetExpiryOnChange ?? false;
- this.incrementScriptSha = this.loadIncrementScript();
- this.getScriptSha = this.loadGetScript();
- }
- /**
- * Loads the script used to increment a client's hit count.
- */
- async loadIncrementScript() {
- const result = await this.sendCommand("SCRIPT", "LOAD", scripts.increment);
- if (typeof result !== "string") {
- throw new TypeError("unexpected reply from redis client");
- }
- return result;
- }
- /**
- * Loads the script used to fetch a client's hit count and expiry time.
- */
- async loadGetScript() {
- const result = await this.sendCommand("SCRIPT", "LOAD", scripts.get);
- if (typeof result !== "string") {
- throw new TypeError("unexpected reply from redis client");
- }
- return result;
- }
- /**
- * Runs the increment command, and retries it if the script is not loaded.
- */
- async retryableIncrement(key) {
- const evalCommand = async () => this.sendCommand(
- "EVALSHA",
- await this.incrementScriptSha,
- "1",
- this.prefixKey(key),
- this.resetExpiryOnChange ? "1" : "0",
- this.windowMs.toString()
- );
- try {
- const result = await evalCommand();
- return result;
- } catch {
- this.incrementScriptSha = this.loadIncrementScript();
- return evalCommand();
- }
- }
- /**
- * Method to prefix the keys with the given text.
- *
- * @param key {string} - The key.
- *
- * @returns {string} - The text + the key.
- */
- prefixKey(key) {
- return `${this.prefix}${key}`;
- }
- /**
- * Method that actually initializes the store.
- *
- * @param options {RateLimitConfiguration} - The options used to setup the middleware.
- */
- init(options) {
- this.windowMs = options.windowMs;
- }
- /**
- * Method to fetch a client's hit count and reset time.
- *
- * @param key {string} - The identifier for a client.
- *
- * @returns {ClientRateLimitInfo | undefined} - The number of hits and reset time for that client.
- */
- async get(key) {
- const results = await this.sendCommand(
- "EVALSHA",
- await this.getScriptSha,
- "1",
- this.prefixKey(key)
- );
- return parseScriptResponse(results);
- }
- /**
- * 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
- */
- async increment(key) {
- const results = await this.retryableIncrement(key);
- return parseScriptResponse(results);
- }
- /**
- * Method to decrement a client's hit counter.
- *
- * @param key {string} - The identifier for a client
- */
- async decrement(key) {
- await this.sendCommand("DECR", this.prefixKey(key));
- }
- /**
- * Method to reset a client's hit counter.
- *
- * @param key {string} - The identifier for a client
- */
- async resetKey(key) {
- await this.sendCommand("DEL", this.prefixKey(key));
- }
- }
- exports.RedisStore = RedisStore;
- exports["default"] = RedisStore;
|