Skip to main content
Glama
rate-limiter.ts9.84 kB
/** * Rate limiting for expensive operations */ import { logger } from '../utils/logger'; export interface RateLimitConfig { windowMs: number; // Time window in milliseconds maxRequests: number; // Maximum requests per window skipSuccessfulRequests?: boolean; skipFailedRequests?: boolean; keyGenerator?: (operation: string, clientId?: string) => string; } export interface RateLimitResult { allowed: boolean; remainingRequests: number; resetTime: number; retryAfter?: number; } interface RateLimitEntry { count: number; resetTime: number; firstRequest: number; } export class RateLimiter { private static instance: RateLimiter; private store: Map<string, RateLimitEntry> = new Map(); private cleanupInterval: ReturnType<typeof setInterval> | null = null; // Default rate limits for different operations private readonly defaultLimits: Record<string, RateLimitConfig> = { // Color conversion operations - high limit convert_color: { windowMs: 60 * 1000, // 1 minute maxRequests: 1000, }, analyze_color: { windowMs: 60 * 1000, maxRequests: 500, }, // Palette generation - moderate limit generate_harmony_palette: { windowMs: 60 * 1000, maxRequests: 100, }, generate_contextual_palette: { windowMs: 60 * 1000, maxRequests: 50, }, generate_algorithmic_palette: { windowMs: 60 * 1000, maxRequests: 50, }, // Image processing - low limit (expensive) extract_palette_from_image: { windowMs: 60 * 1000, maxRequests: 10, }, // Visualization generation - moderate limit create_palette_html: { windowMs: 60 * 1000, maxRequests: 100, }, create_color_wheel_html: { windowMs: 60 * 1000, maxRequests: 50, }, create_gradient_html: { windowMs: 60 * 1000, maxRequests: 100, }, create_theme_preview_html: { windowMs: 60 * 1000, maxRequests: 50, }, // PNG generation - low limit (very expensive) create_palette_png: { windowMs: 60 * 1000, maxRequests: 20, }, create_gradient_png: { windowMs: 60 * 1000, maxRequests: 15, }, create_color_comparison_png: { windowMs: 60 * 1000, maxRequests: 10, }, // Accessibility tools - moderate limit check_contrast: { windowMs: 60 * 1000, maxRequests: 200, }, simulate_colorblindness: { windowMs: 60 * 1000, maxRequests: 100, }, optimize_for_accessibility: { windowMs: 60 * 1000, maxRequests: 50, }, // Utility operations - high limit mix_colors: { windowMs: 60 * 1000, maxRequests: 200, }, generate_color_variations: { windowMs: 60 * 1000, maxRequests: 100, }, sort_colors: { windowMs: 60 * 1000, maxRequests: 200, }, analyze_color_collection: { windowMs: 60 * 1000, maxRequests: 100, }, // Default limit for unknown operations default: { windowMs: 60 * 1000, maxRequests: 50, }, }; private constructor() { // Only start cleanup in non-test environments const isTestEnvironment = process.env['NODE_ENV'] === 'test' || process.env['JEST_WORKER_ID'] !== undefined || process.env['CI'] === 'true' || typeof jest !== 'undefined' || (typeof global !== 'undefined' && 'jest' in global); if (!isTestEnvironment) { this.cleanupInterval = setInterval( () => { this.cleanup(); }, 5 * 60 * 1000 ); } } public static getInstance(): RateLimiter { if (!RateLimiter.instance) { RateLimiter.instance = new RateLimiter(); } return RateLimiter.instance; } public checkRateLimit( operation: string, clientId = 'default', customConfig?: Partial<RateLimitConfig> ): RateLimitResult { const config = { ...this.getConfigForOperation(operation), ...customConfig, }; const key = config.keyGenerator ? config.keyGenerator(operation, clientId) : `${operation}:${clientId}`; const now = Date.now(); const entry = this.store.get(key); if (!entry) { // First request for this key this.store.set(key, { count: 1, resetTime: now + config.windowMs, firstRequest: now, }); logger.debug(`Rate limit initialized for ${operation}`, { tool: operation, remainingRequests: config.maxRequests - 1, }); return { allowed: true, remainingRequests: config.maxRequests - 1, resetTime: now + config.windowMs, }; } // Check if the window has expired if (now >= entry.resetTime) { // Reset the window this.store.set(key, { count: 1, resetTime: now + config.windowMs, firstRequest: now, }); logger.debug(`Rate limit window reset for ${operation}`, { tool: operation, remainingRequests: config.maxRequests - 1, }); return { allowed: true, remainingRequests: config.maxRequests - 1, resetTime: now + config.windowMs, }; } // Check if limit is exceeded if (entry.count >= config.maxRequests) { const retryAfter = Math.ceil((entry.resetTime - now) / 1000); logger.warn(`Rate limit exceeded for ${operation}`, { tool: operation, clientId, count: entry.count, limit: config.maxRequests, retryAfter, }); return { allowed: false, remainingRequests: 0, resetTime: entry.resetTime, retryAfter, }; } // Increment counter entry.count++; this.store.set(key, entry); const remainingRequests = config.maxRequests - entry.count; logger.debug(`Rate limit check passed for ${operation}`, { tool: operation, count: entry.count, remainingRequests, }); return { allowed: true, remainingRequests, resetTime: entry.resetTime, }; } private getConfigForOperation(operation: string): RateLimitConfig { const config = this.defaultLimits[operation]; if (config) { return config; } const defaultConfig = this.defaultLimits['default']; if (defaultConfig) { return defaultConfig; } // Fallback config if default is not found return { windowMs: 60 * 1000, maxRequests: 50, }; } public updateLimits( operation: string, config: Partial<RateLimitConfig> ): void { this.defaultLimits[operation] = { ...this.getConfigForOperation(operation), ...config, }; logger.info(`Updated rate limits for ${operation}`, { tool: operation, config, }); } public getRemainingRequests(operation: string, clientId = 'default'): number { const config = this.getConfigForOperation(operation); const key = `${operation}:${clientId}`; const entry = this.store.get(key); if (!entry) { return config.maxRequests; } const now = Date.now(); if (now >= entry.resetTime) { return config.maxRequests; } return Math.max(0, config.maxRequests - entry.count); } public getResetTime(operation: string, clientId = 'default'): number | null { const key = `${operation}:${clientId}`; const entry = this.store.get(key); if (!entry) { return null; } const now = Date.now(); if (now >= entry.resetTime) { return null; } return entry.resetTime; } private cleanup(): void { const now = Date.now(); let cleanedCount = 0; for (const [key, entry] of this.store.entries()) { if (now >= entry.resetTime) { this.store.delete(key); cleanedCount++; } } if (cleanedCount > 0) { logger.debug(`Cleaned up ${cleanedCount} expired rate limit entries`); } } public getStats(): { totalEntries: number; activeWindows: number; topOperations: Array<{ operation: string; requests: number }>; } { const now = Date.now(); const operationCounts: Record<string, number> = {}; let activeWindows = 0; for (const [key, entry] of this.store.entries()) { if (now < entry.resetTime) { activeWindows++; const operation = key.split(':')[0]; if (operation) { operationCounts[operation] = (operationCounts[operation] || 0) + entry.count; } } } const topOperations = Object.entries(operationCounts) .map(([operation, requests]) => ({ operation, requests })) .sort((a, b) => b.requests - a.requests) .slice(0, 10); return { totalEntries: this.store.size, activeWindows, topOperations, }; } public reset(operation?: string, clientId?: string): void { if (operation && clientId) { const key = `${operation}:${clientId}`; this.store.delete(key); logger.info(`Reset rate limit for ${operation}:${clientId}`); } else if (operation) { // Reset all entries for this operation const keysToDelete = Array.from(this.store.keys()).filter(key => key.startsWith(`${operation}:`) ); keysToDelete.forEach(key => this.store.delete(key)); logger.info(`Reset rate limits for operation ${operation}`); } else { // Reset all this.store.clear(); logger.info('Reset all rate limits'); } } public destroy(): void { if (this.cleanupInterval) { clearInterval(this.cleanupInterval); this.cleanupInterval = null; } this.store.clear(); // @ts-ignore - We need to reset the instance for testing RateLimiter.instance = undefined; } } // Export singleton instance export const rateLimiter = RateLimiter.getInstance();

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/keyurgolani/ColorMcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server