UNPKG

rate-limit-memcached

Version:
157 lines (154 loc) 4.44 kB
import { promisify } from 'node:util'; import Memcached from 'memcached'; var __defProp = Object.defineProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField = (obj, key, value) => { __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); return value; }; const methods = [ "del", "get", "set", "add", "incr", "decr" ]; class MemcachedStore { /** * @constructor for `MemcachedStore`. * * @param options {Options} - The options used to configure the store's behaviour. */ constructor(options) { /** * The number of seconds to remember a client's requests. */ __publicField(this, "expiration"); /** * The text to prepend to the key. */ __publicField(this, "prefix"); /** * The `memcached` client to use. */ __publicField(this, "client"); /** * The promisifed functions from the `client` object. */ __publicField(this, "fns"); this.prefix = options?.prefix ?? "rl:"; if (options?.client) { for (const func of methods) { if (typeof options.client[func] !== "function") throw new Error("An invalid memcached client was passed to store."); } this.client = options.client; } else { this.client = new Memcached( options?.locations ?? ["localhost:11211"], options?.config ?? {} ); } this.fns = {}; for (const func of methods) { this.fns[func] = promisify(this.client[func]).bind(this.client); } } /** * Method that actually initializes the store. * * @param options {RateLimitConfiguration} - The options used to setup the middleware. * * @impl */ init(options) { this.expiration = options.windowMs / 1e3; } /** * 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 returns the name of the key used to store the reset timestamp * for the given key. * * @param key {string} - The key. * * @returns {string} - The expiry key's name. */ expiryKey(key) { return `${this.prefix}expiry:${key}`; } /** * 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 prefixedKey = this.prefixKey(key); let totalHits = await this.fns.incr(prefixedKey, 1); let expiresAt; if (totalHits === false) { try { await this.fns.add(prefixedKey, 1, this.expiration); totalHits = 1; expiresAt = Date.now() + this.expiration * 1e3; await this.fns.add( this.expiryKey(key), // The name of the key. expiresAt, // The value - the time at which the key expires. this.expiration // The key should be deleted by memcached after `window` seconds. ); } catch (caughtError) { const error = caughtError; if (/not(\s)?stored/i.test(error.message)) { totalHits = await this.fns.incr(prefixedKey, 1); expiresAt = await this.fns.get(this.expiryKey(key)); } else { throw error; } } } else { expiresAt = await this.fns.get(this.expiryKey(key)); } if (typeof totalHits !== "number") throw new Error( `Expected 'totalHits' to be a number, got ${totalHits} instead.` ); return { totalHits, // If `expiresAt` is undefined, assume the key expired sometime in between // reading the hits and expiry keys from memcached. resetTime: expiresAt ? new Date(expiresAt) : /* @__PURE__ */ new Date() }; } /** * Method to decrement a client's hit counter. * * @param key {string} - The identifier for a client */ async decrement(key) { await this.fns.decr(this.prefixKey(key), 1); } /** * Method to reset a client's hit counter. * * @param key {string} - The identifier for a client. */ async resetKey(key) { await this.fns.del(this.prefixKey(key)); await this.fns.del(this.expiryKey(key)); } } export { MemcachedStore };