oss-ratelimit
Version:
Flexible rate limiting library with Redis for TypeScript applications
1 lines • 30.3 kB
Source Map (JSON)
{"version":3,"sources":["../src/index.ts","../src/rate-limit.ts","../src/utils.ts"],"sourcesContent":["import { type RedisClientType, createClient } from 'redis';\nimport { Ratelimit } from './rate-limit';\nimport {\n FixedWindowOptions,\n LimiterType,\n RatelimitConfig,\n SlidingWindowOptions,\n TimeWindow,\n TokenBucketOptions,\n} from './types';\nimport { parseTimeWindow } from './utils';\nexport { Ratelimit };\n// Enhanced error handling\nexport class RatelimitError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'RatelimitError';\n }\n}\n\n// Limiter functions\nexport const fixedWindow = (limit: number, window: TimeWindow): FixedWindowOptions => {\n return {\n limit,\n interval: parseTimeWindow(window),\n };\n};\n\nexport const slidingWindow = (limit: number, window: TimeWindow): SlidingWindowOptions => {\n return {\n limit,\n interval: parseTimeWindow(window),\n };\n};\n\nexport const tokenBucket = (\n refillRate: number,\n interval: TimeWindow,\n limit: number\n): TokenBucketOptions => {\n return {\n refillRate,\n interval: parseTimeWindow(interval),\n limit,\n };\n};\n\nlet redisClient: RedisClientType | undefined;\nexport const getRedisSingleClient = (envRedisKey: string) => {\n if (redisClient) {\n return redisClient;\n }\n redisClient = createClient({\n url: process.env[envRedisKey] || 'redis://localhost:6379',\n socket: {\n reconnectStrategy: (retries) => Math.min(retries * 50, 1000),\n },\n }) as RedisClientType;\n redisClient.on('error', (err) => console.error('Redis Client Error', err));\n // Connect automatically\n (async () => {\n try {\n if (!redisClient.isOpen || !(await redisClient.ping())) {\n await redisClient.connect();\n }\n } catch (error) {\n console.error('Failed to connect to Redis:', error);\n }\n })();\n return redisClient;\n};\n// Factory function\nexport interface CreateRateLimiterProps extends Omit<RatelimitConfig, 'redis'> {}\n\nexport const createSingletonRateLimiter = (\n props?: CreateRateLimiterProps & {\n limiter?: LimiterType;\n envRedisKey?: string;\n }\n) => {\n const redis = getRedisSingleClient(props?.envRedisKey ?? 'REDIS_URL');\n return createRateLimiter(redis, props);\n};\n\nexport const createRateLimiter = (\n redis: RedisClientType,\n props?: CreateRateLimiterProps & {\n limiter?: LimiterType;\n envRedisKey?: string;\n }\n) => {\n return new Ratelimit({\n redis,\n limiter: props?.limiter ?? slidingWindow(10, '10 s'),\n prefix: props?.prefix ?? 'open-ratelimit',\n analytics: props?.analytics ?? false,\n timeout: props?.timeout ?? 1000,\n ephemeralCache: props?.ephemeralCache ?? true,\n ephemeralCacheTTL: props?.ephemeralCacheTTL ?? 60000,\n });\n};\n","import { RedisClientType } from 'redis';\nimport { RatelimitError } from '.';\nimport {\n FixedWindowOptions,\n LimiterType,\n RatelimitConfig,\n RatelimitResponse,\n SlidingWindowOptions,\n TokenBucketOptions,\n} from './types';\n// Function to detect limiter type\nconst getLimiterType = (limiter: LimiterType): string => {\n if ('refillRate' in limiter) return 'tokenBucket';\n // Both fixed and sliding window have same properties, so we'll use an explicit property\n return 'interval' in limiter ? 'slidingWindow' : 'fixedWindow';\n};\n\n// Memory cache for handling Redis outages\nclass EphemeralCache {\n private cache: Map<string, { count: number; expires: number }>;\n private ttl: number;\n\n constructor(ttlMs = 60000) {\n this.cache = new Map();\n this.ttl = ttlMs;\n // Clean expired items periodically\n setInterval(() => this.cleanup(), Math.min(ttlMs, 60000));\n }\n\n get(key: string): number {\n const now = Date.now();\n const item = this.cache.get(key);\n if (!item || item.expires < now) return 0;\n return item.count;\n }\n\n set(key: string, count: number, windowMs: number): void {\n this.cache.set(key, {\n count,\n expires: Date.now() + Math.min(windowMs, this.ttl),\n });\n }\n\n increment(key: string, windowMs: number): number {\n const now = Date.now();\n const item = this.cache.get(key) || { count: 0, expires: now + windowMs };\n if (item.expires < now) {\n item.count = 1;\n item.expires = now + windowMs;\n } else {\n item.count++;\n }\n this.cache.set(key, item);\n return item.count;\n }\n\n cleanup(): void {\n const now = Date.now();\n for (const [key, item] of this.cache.entries()) {\n if (item.expires < now) {\n this.cache.delete(key);\n }\n }\n }\n}\n\n// Main rate limiter class\nexport class Ratelimit {\n private redisPromise: Promise<RedisClientType>;\n private limiter: LimiterType;\n private limiterType: string;\n private prefix: string;\n private analytics: boolean;\n private timeout: number;\n private ephemeralCache?: EphemeralCache;\n\n constructor(config: RatelimitConfig) {\n // Handle both direct client and promise\n this.redisPromise = Promise.resolve(config.redis);\n this.limiter = config.limiter;\n this.limiterType = getLimiterType(config.limiter);\n this.prefix = config.prefix || 'ratelimit';\n this.analytics = config.analytics ?? false;\n this.timeout = config.timeout ?? 1000;\n\n // Create ephemeral cache if requested\n if (config.ephemeralCache) {\n this.ephemeralCache = new EphemeralCache(config.ephemeralCacheTTL);\n }\n }\n\n // Get the Redis client with timeout protection\n private async getRedis(): Promise<RedisClientType> {\n try {\n const timeoutPromise = new Promise<never>((_, reject) => {\n setTimeout(\n () => reject(new RatelimitError(`Redis connection timed out after ${this.timeout}ms`)),\n this.timeout\n );\n });\n\n const redis = await Promise.race([this.redisPromise, timeoutPromise]);\n\n // Check if Redis is connected\n if (!redis.isOpen || !(await redis.ping())) {\n await redis.connect();\n }\n\n return redis;\n } catch (error) {\n throw new RatelimitError(\n `Failed to connect to Redis: ${error instanceof Error ? error.message : String(error)}`\n );\n }\n }\n\n // Apply rate limit\n async limit(identifier: string): Promise<RatelimitResponse> {\n const now = Date.now();\n const key = `${this.prefix}:${identifier}`;\n\n // Try ephemeral cache first if available\n if (this.ephemeralCache && this.limiterType === 'slidingWindow') {\n const windowMs = (this.limiter as SlidingWindowOptions).interval;\n const limit = (this.limiter as SlidingWindowOptions).limit;\n\n try {\n // Try Redis first\n return await this.applySlidingWindowLimit(key, now);\n } catch (error) {\n // Fall back to ephemeral cache on Redis failure\n console.warn(\n `Redis error, using ephemeral cache: ${\n error instanceof Error ? error.message : String(error)\n }`\n );\n\n const count = this.ephemeralCache.increment(key, windowMs);\n const success = count <= limit;\n\n return {\n success,\n limit,\n remaining: Math.max(0, limit - count),\n reset: now + windowMs,\n retryAfter: success ? 0 : Math.ceil(windowMs / 1000),\n };\n }\n }\n\n // Apply appropriate limiter\n try {\n switch (this.limiterType) {\n case 'slidingWindow':\n return await this.applySlidingWindowLimit(key, now);\n case 'fixedWindow':\n return await this.applyFixedWindowLimit(key, now);\n case 'tokenBucket':\n return await this.applyTokenBucketLimit(key, now);\n default:\n throw new RatelimitError(`Unknown limiter type: ${this.limiterType}`);\n }\n } catch (error) {\n // If Redis fails and no ephemeral cache, fail open with a warning\n console.error(\n `Rate limiting error: ${error instanceof Error ? error.message : String(error)}`\n );\n\n // Get limit based on limiter type\n const limit = 'limit' in this.limiter ? this.limiter.limit : 10;\n\n return {\n success: true, // Fail open\n limit,\n remaining: limit - 1,\n reset: now + 60000, // Arbitrary 1-minute reset\n };\n }\n }\n\n // Sliding window implementation\n private async applySlidingWindowLimit(key: string, now: number): Promise<RatelimitResponse> {\n const redis = await this.getRedis();\n const { limit, interval } = this.limiter as SlidingWindowOptions;\n const windowStart = now - interval;\n\n const luaScript = `\n local key = KEYS[1]\n local analyticsKey = KEYS[2]\n local now = tonumber(ARGV[1])\n local windowMs = tonumber(ARGV[2])\n local maxRequests = tonumber(ARGV[3])\n local doAnalytics = tonumber(ARGV[4])\n local windowStart = now - windowMs\n \n -- Remove counts older than the current window\n redis.call('ZREMRANGEBYSCORE', key, 0, windowStart)\n \n -- Get current count\n local count = redis.call('ZCARD', key)\n \n local success = count < maxRequests\n \n -- Add current timestamp if successful\n if success then\n redis.call('ZADD', key, now, now .. ':' .. math.random())\n count = count + 1\n end\n \n -- Set expiration to keep memory usage bounded\n redis.call('PEXPIRE', key, windowMs * 2)\n \n -- Analytics if requested\n if doAnalytics == 1 then\n -- Record request timestamp for analytics\n redis.call('ZADD', analyticsKey, now, now)\n redis.call('PEXPIRE', analyticsKey, windowMs * 2)\n end\n \n -- Calculate when the oldest request expires\n local oldestTimestamp = 0\n if count >= maxRequests then\n local oldest = redis.call('ZRANGE', key, 0, 0, 'WITHSCORES')\n if #oldest >= 2 then\n oldestTimestamp = tonumber(oldest[2])\n end\n end\n \n -- Calculate pending and throughput if analytics enabled\n local pending = 0\n local throughput = 0\n if doAnalytics == 1 then\n pending = count\n -- Calculate requests in the last second\n local secondAgo = now - 1000\n throughput = redis.call('ZCOUNT', analyticsKey, secondAgo, '+inf')\n end\n \n -- Return results\n return {\n success and 1 or 0, \n maxRequests, \n math.max(0, maxRequests - count), \n now + windowMs,\n oldestTimestamp > 0 and math.ceil((oldestTimestamp + windowMs - now) / 1000) or 0,\n pending,\n throughput\n }\n `;\n\n try {\n const analyticsKey = `${key}:analytics`;\n const result = await redis.eval(luaScript, {\n keys: [key, analyticsKey],\n arguments: [\n now.toString(),\n interval.toString(),\n limit.toString(),\n this.analytics ? '1' : '0',\n ],\n });\n\n if (!Array.isArray(result)) {\n throw new RatelimitError('Invalid response from Redis');\n }\n\n const response: RatelimitResponse = {\n success: Boolean(result[0]),\n limit: Number(result[1]),\n remaining: Number(result[2]),\n reset: Number(result[3]),\n };\n\n // Add conditional properties\n const retryAfter = Number(result[4]);\n if (retryAfter > 0) response.retryAfter = retryAfter;\n\n if (this.analytics) {\n response.pending = Number(result[5]);\n response.throughput = Number(result[6]);\n }\n\n // Store in ephemeral cache if available\n if (this.ephemeralCache) {\n this.ephemeralCache.set(key, limit - response.remaining, interval);\n }\n\n return response;\n } catch (error) {\n throw new RatelimitError(\n `Sliding window limit error: ${error instanceof Error ? error.message : String(error)}`\n );\n }\n }\n\n // Fixed window implementation\n private async applyFixedWindowLimit(key: string, now: number): Promise<RatelimitResponse> {\n const redis = await this.getRedis();\n const { limit, interval } = this.limiter as FixedWindowOptions;\n\n // Create window key with fixed time boundary\n const windowKey = `${key}:${Math.floor(now / interval)}`;\n\n const luaScript = `\n local key = KEYS[1]\n local analyticsKey = KEYS[2]\n local limit = tonumber(ARGV[1])\n local windowMs = tonumber(ARGV[2])\n local doAnalytics = tonumber(ARGV[3])\n local now = tonumber(ARGV[4])\n \n -- Increment counter for this window\n local count = redis.call('INCR', key)\n \n -- Set expiration if this is first request in window\n if count == 1 then\n redis.call('PEXPIRE', key, windowMs * 2)\n end\n \n local success = count <= limit\n \n -- Analytics\n if doAnalytics == 1 then\n redis.call('ZADD', analyticsKey, now, now)\n redis.call('PEXPIRE', analyticsKey, windowMs)\n end\n \n -- Calculate remaining time in window\n local ttl = redis.call('PTTL', key)\n if ttl < 0 then ttl = windowMs end\n \n -- Calculate throughput if analytics enabled\n local throughput = 0\n if doAnalytics == 1 then\n -- Calculate requests in the last second\n local secondAgo = now - 1000\n throughput = redis.call('ZCOUNT', analyticsKey, secondAgo, '+inf')\n end\n \n return {\n success and 1 or 0,\n limit,\n math.max(0, limit - count),\n now + ttl,\n success and 0 or math.ceil(ttl / 1000),\n count,\n throughput\n }\n `;\n\n try {\n const analyticsKey = `${key}:analytics`;\n const result = await redis.eval(luaScript, {\n keys: [windowKey, analyticsKey],\n arguments: [\n limit.toString(),\n interval.toString(),\n this.analytics ? '1' : '0',\n now.toString(),\n ],\n });\n\n if (!Array.isArray(result)) {\n throw new RatelimitError('Invalid response from Redis');\n }\n\n const response: RatelimitResponse = {\n success: Boolean(result[0]),\n limit: Number(result[1]),\n remaining: Number(result[2]),\n reset: Number(result[3]),\n };\n\n const retryAfter = Number(result[4]);\n if (retryAfter > 0) response.retryAfter = retryAfter;\n\n if (this.analytics) {\n response.pending = Number(result[5]);\n response.throughput = Number(result[6]);\n }\n\n return response;\n } catch (error) {\n throw new RatelimitError(\n `Fixed window limit error: ${error instanceof Error ? error.message : String(error)}`\n );\n }\n }\n\n // Token bucket implementation\n private async applyTokenBucketLimit(key: string, now: number): Promise<RatelimitResponse> {\n const redis = await this.getRedis();\n const { limit, refillRate, interval } = this.limiter as TokenBucketOptions;\n\n const luaScript = `\n local key = KEYS[1]\n local analyticsKey = KEYS[2]\n local now = tonumber(ARGV[1])\n local refillRate = tonumber(ARGV[2])\n local refillInterval = tonumber(ARGV[3])\n local bucketCapacity = tonumber(ARGV[4])\n local doAnalytics = tonumber(ARGV[5])\n \n -- Get current bucket state\n local bucketInfo = redis.call('HMGET', key, 'tokens', 'lastRefill')\n local tokens = tonumber(bucketInfo[1]) or bucketCapacity\n local lastRefill = tonumber(bucketInfo[2]) or 0\n \n -- Calculate token refill\n local elapsedTime = now - lastRefill\n local tokensToAdd = math.floor(elapsedTime * (refillRate / refillInterval))\n \n if tokensToAdd > 0 then\n -- Add tokens based on elapsed time\n tokens = math.min(bucketCapacity, tokens + tokensToAdd)\n lastRefill = now\n end\n \n -- Try to consume a token\n local success = tokens >= 1\n if success then\n tokens = tokens - 1\n end\n \n -- Save updated bucket state\n redis.call('HMSET', key, 'tokens', tokens, 'lastRefill', lastRefill)\n redis.call('PEXPIRE', key, refillInterval * 2)\n \n -- Analytics\n if doAnalytics == 1 then\n redis.call('ZADD', analyticsKey, now, now)\n redis.call('PEXPIRE', analyticsKey, refillInterval)\n end\n \n -- Calculate time until next token refill\n local timeToNextToken = success and 0 or math.ceil((1 - tokens) * (refillInterval / refillRate))\n \n -- Calculate throughput if analytics enabled\n local throughput = 0\n if doAnalytics == 1 then\n -- Calculate requests in the last second\n local secondAgo = now - 1000\n throughput = redis.call('ZCOUNT', analyticsKey, secondAgo, '+inf')\n end\n \n return {\n success and 1 or 0,\n bucketCapacity,\n tokens,\n now + (refillInterval / refillRate),\n timeToNextToken,\n bucketCapacity - tokens,\n throughput\n }\n `;\n\n try {\n const analyticsKey = `${key}:analytics`;\n const result = await redis.eval(luaScript, {\n keys: [key, analyticsKey],\n arguments: [\n now.toString(),\n refillRate.toString(),\n interval.toString(),\n limit.toString(),\n this.analytics ? '1' : '0',\n ],\n });\n\n if (!Array.isArray(result)) {\n throw new RatelimitError('Invalid response from Redis');\n }\n\n const response: RatelimitResponse = {\n success: Boolean(result[0]),\n limit: Number(result[1]),\n remaining: Number(result[2]),\n reset: Number(result[3]),\n };\n\n const retryAfter = Number(result[4]);\n if (retryAfter > 0) response.retryAfter = retryAfter;\n\n if (this.analytics) {\n response.pending = Number(result[5]);\n response.throughput = Number(result[6]);\n }\n\n return response;\n } catch (error) {\n throw new RatelimitError(\n `Token bucket limit error: ${error instanceof Error ? error.message : String(error)}`\n );\n }\n }\n // Modified block method to prevent infinite loop\n async block(identifier: string, maxWaitMs = 5000): Promise<RatelimitResponse> {\n const startTime = Date.now();\n let attempts = 0;\n const maxAttempts = 100; // Safeguard against infinite loops\n\n while (attempts < maxAttempts) {\n attempts++;\n const response = await this.limit(identifier);\n\n if (response.success) {\n return response;\n }\n\n const currentTime = Date.now();\n if (currentTime - startTime >= maxWaitMs) {\n throw new RatelimitError(\n `Rate limit exceeded for ${identifier} after waiting ${currentTime - startTime}ms`\n );\n }\n\n const waitTime = Math.max(\n 50,\n Math.min(500, response.retryAfter ? (response.retryAfter * 1000) / 2 : 100)\n );\n\n await new Promise((resolve) => setTimeout(resolve, waitTime));\n }\n\n // This is a safeguard in case we reach max attempts\n throw new RatelimitError(`Rate limit exceeded for ${identifier} after ${maxAttempts} attempts`);\n }\n // Reset rate limit for an identifier\n async reset(identifier: string): Promise<void> {\n try {\n const redis = await this.getRedis();\n const key = `${this.prefix}:${identifier}`;\n\n await redis.del(key);\n await redis.del(`${key}:analytics`);\n\n // Also clear ephemeral cache if available\n if (this.ephemeralCache) {\n this.ephemeralCache.set(key, 0, 0);\n }\n } catch (error) {\n throw new RatelimitError(\n `Failed to reset rate limit: ${error instanceof Error ? error.message : String(error)}`\n );\n }\n }\n}\n","import { RatelimitError } from './index';\nimport { TimeWindow } from './types';\n\n// Parse time window to milliseconds with proper error handling\nexport const parseTimeWindow = (window: TimeWindow): number => {\n try {\n const [valueStr, unit] = window.split(' ');\n const value = Number.parseInt(valueStr || 's', 10);\n\n if (Number.isNaN(value) || value <= 0) {\n throw new RatelimitError(`Invalid time value: ${valueStr}`);\n }\n\n switch (unit) {\n case 'ms':\n return value;\n case 's':\n return value * 1000;\n case 'm':\n return value * 60 * 1000;\n case 'h':\n return value * 60 * 60 * 1000;\n case 'd':\n return value * 24 * 60 * 60 * 1000;\n default:\n throw new RatelimitError(`Invalid time unit: ${unit}`);\n }\n } catch (error) {\n if (error instanceof RatelimitError) throw error;\n throw new RatelimitError(`Failed to parse time window: ${window}`);\n }\n};\n"],"mappings":"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","names":["createClient","getLimiterType","limiter","EphemeralCache","ttlMs","key","now","item","count","windowMs","Ratelimit","config","timeoutPromise","_","reject","RatelimitError","redis","error","identifier","limit","success","interval","windowStart","luaScript","analyticsKey","result","response","retryAfter","windowKey","refillRate","maxWaitMs","startTime","attempts","maxAttempts","currentTime","waitTime","resolve","parseTimeWindow","window","valueStr","unit","value","RatelimitError","error","RatelimitError","message","fixedWindow","limit","window","parseTimeWindow","slidingWindow","tokenBucket","refillRate","interval","redisClient","getRedisSingleClient","envRedisKey","createClient","retries","err","error","createSingletonRateLimiter","props","redis","createRateLimiter","Ratelimit"]}