express-rate-limiter-ts
Version:
A powerful, flexible and modern rate limiter middleware for Express.js written in TypeScript.
96 lines (95 loc) • 3.64 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.RedisStore = void 0;
const ioredis_1 = __importDefault(require("ioredis"));
// RedisStore: Fully working, async, TTL and ban supported store for production.
class RedisStore {
constructor(options) {
this.statsKey = 'rateLimiter:stats';
this.banPrefix = 'rateLimiter:ban:';
this.countPrefix = 'rateLimiter:count:';
this.windowMs = options.windowMs;
this.client = new ioredis_1.default(options.url);
}
// Increment counter and check limit (async)
async incr(key) {
const redisKey = this.countPrefix + key;
// Increment counter, set TTL if key does not exist
const current = await this.client.incr(redisKey);
if (current === 1) {
await this.client.pexpire(redisKey, this.windowMs);
}
const ttl = await this.client.pttl(redisKey);
return {
current,
remaining: 0, // Remaining limit will be calculated in middleware
resetTime: new Date(Date.now() + (ttl > 0 ? ttl : this.windowMs)),
exceeded: false,
};
}
// Read counter (async)
async get(key) {
const redisKey = this.countPrefix + key;
const count = parseInt(await this.client.get(redisKey) || '0', 10);
const ttl = await this.client.pttl(redisKey);
return { count, resetTime: Date.now() + (ttl > 0 ? ttl : this.windowMs) };
}
// Reset counter
async reset(key) {
await this.client.del(this.countPrefix + key);
}
// Record statistic (optional)
async recordStat(route) {
await this.client.hincrby(this.statsKey, route, 1);
await this.client.hset(this.statsKey + ':last', route, Date.now());
}
// Get statistics
async getStats() {
const counts = await this.client.hgetall(this.statsKey);
const lasts = await this.client.hgetall(this.statsKey + ':last');
const stats = {};
for (const route in counts) {
stats[route] = {
count: parseInt(counts[route], 10),
lastTriggered: new Date(Number(lasts[route]) || Date.now()),
};
}
return stats;
}
// Ban
async ban(key, seconds, reason) {
await this.client.set(this.banPrefix + key, reason, 'EX', seconds);
}
// Check ban
async isBanned(key) {
const reason = await this.client.get(this.banPrefix + key);
if (reason) {
const ttl = await this.client.ttl(this.banPrefix + key);
return { banned: true, banExpiresAt: new Date(Date.now() + ttl * 1000), reason };
}
return { banned: false };
}
// Get banned keys (scan all, should be optimized for large data)
async getBanned() {
const keys = await this.client.keys(this.banPrefix + '*');
const result = {};
for (const fullKey of keys) {
const key = fullKey.replace(this.banPrefix, '');
const reason = await this.client.get(fullKey);
const ttl = await this.client.ttl(fullKey);
if (reason && ttl > 0) {
result[key] = { banExpiresAt: new Date(Date.now() + ttl * 1000), reason };
}
}
return result;
}
// Unban
async unban(key) {
await this.client.del(this.banPrefix + key);
await this.client.del(this.countPrefix + key);
}
}
exports.RedisStore = RedisStore;