/**
* RateLimiter — Token bucket rate limiting
*
* Per-client, per-operation rate limiting with persistent storage.
*/
export class RateLimiter {
constructor (opts = {}) {
this.db = opts.db || null
this.limits = opts.limits || {
auth: { tokens: 1000, refillRate: 100, refillInterval: 60 * 1000 },
api: { tokens: 10000, refillRate: 1000, refillInterval: 60 * 1000 }
}
this.cacheTimeout = opts.cacheTimeout || 10 * 1000
this._buckets = new Map()
this._cleanupInterval = setInterval(() => this._cleanup(), 60 * 1000)
}
/**
* Check rate limit for client/operation
* @param {string} clientId - Client identifier (IP, user ID, etc.)
* @param {string} operation - Operation type
* @returns {Promise<Object>} { allowed, remaining, retryAfter?, resetAt? }
*/
async check (clientId, operation = 'api') {
const key = `${clientId}:${operation}`
const limit = this.limits[operation] || this.limits.api
let bucket = this._buckets.get(key)
if (!bucket) {
bucket = {
tokens: limit.tokens,
lastRefill: Date.now(),
operation
}
this._buckets.set(key, bucket)
}
// Refill tokens
const now = Date.now()
const elapsed = now - bucket.lastRefill
const refills = Math.floor(elapsed / limit.refillInterval)
if (refills > 0) {
bucket.tokens = Math.min(limit.tokens, bucket.tokens + refills * limit.refillRate)
bucket.lastRefill = now
}
// Check
if (bucket.tokens <= 0) {
const retryAfter = Math.ceil(limit.refillInterval / 1000)
return {
allowed: false,
remaining: 0,
retryAfter,
resetAt: bucket.lastRefill + limit.refillInterval
}
}
// Consume token
bucket.tokens--
return {
allowed: true,
remaining: bucket.tokens
}
}
/**
* Cleanup old buckets
*/
_cleanup () {
const cutoff = Date.now() - 10 * 60 * 1000 // 10 min
for (const [key, bucket] of this._buckets) {
if (bucket.lastRefill < cutoff) {
this._buckets.delete(key)
}
}
}
/**
* Get stats
*/
async stats () {
return {
activeBuckets: this._buckets.size
}
}
destroy () {
clearInterval(this._cleanupInterval)
this._buckets.clear()
}
}
export default RateLimiter