rate-limit-memcached
Version:
A memcached store for the express-rate-limit middleware.
157 lines (154 loc) • 4.44 kB
JavaScript
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 };