ts-rate-limiter
Version:
High-performance, flexible rate limiting for TypeScript and Bun
107 lines (85 loc) • 3.05 kB
TypeScript
import type { MemoryStorageOptions, StorageProvider } from '../types';
export declare class MemoryStorage implements StorageProvider {
private records: Map<string, { count: number, resetTime: number }>
private timestamps: Map<string, number[]>
private cleanupTimer: NodeJS.Timeout | null
private enableAutoCleanup: boolean
private cleanupIntervalMs: number
constructor(options?: MemoryStorageOptions) {
this.records = new Map()
this.timestamps = new Map()
const defaultConfig = config.memoryStorage || {}
this.enableAutoCleanup = options?.enableAutoCleanup ?? defaultConfig.enableAutoCleanup ?? false
this.cleanupIntervalMs = options?.cleanupIntervalMs ?? defaultConfig.cleanupIntervalMs ?? 60 * 1000
this.cleanupTimer = null
if (this.enableAutoCleanup) {
this.startCleanupTimer()
}
}
async increment(key: string, windowMs: number): Promise<{ count: number, resetTime: number }> {
const now = Date.now()
const record = this.records.get(key)
if (!record || now > record.resetTime) {
const newRecord = {
count: 1,
resetTime: now + windowMs,
}
this.records.set(key, newRecord)
this.timestamps.set(key, [now])
return newRecord
}
record.count += 1
const timestamps = this.timestamps.get(key) || []
timestamps.push(now)
this.timestamps.set(key, timestamps)
return record
}
async reset(key: string): Promise<void> {
this.records.delete(key)
this.timestamps.delete(key)
}
async getCount(key: string): Promise<number> {
const record = this.records.get(key)
return record?.count || 0
}
async getSlidingWindowCount(key: string, windowMs: number): Promise<number> {
const timestamps = this.timestamps.get(key) || []
const now = Date.now()
const windowStart = now - windowMs
return timestamps.filter(time => time > windowStart).length
}
async batchIncrement(keys: string[], windowMs: number): Promise<Map<string, { count: number, resetTime: number }>> {
const results = new Map<string, { count: number, resetTime: number }>()
for (const key of keys) {
const result = await this.increment(key, windowMs)
results.set(key, result)
}
return results
}
cleanExpired(): void {
const now = Date.now()
for (const [key, record] of this.records.entries()) {
if (now > record.resetTime) {
this.records.delete(key)
}
}
const maxAge = now - 3600000
for (const [key, timestamps] of this.timestamps.entries()) {
const filtered = timestamps.filter(time => time > maxAge)
if (filtered.length === 0) {
this.timestamps.delete(key)
}
else if (filtered.length !== timestamps.length) {
this.timestamps.set(key, filtered)
}
}
}
dispose(): void {
if (this.cleanupTimer) {
clearInterval(this.cleanupTimer)
}
}
private startCleanupTimer(): void {
this.cleanupTimer = setInterval(() => this.cleanExpired(), this.cleanupIntervalMs)
}
}