/** * Rate Limiter for RPC calls * * Prevents overwhelming RPC endpoints with too many concurrent requests. * Useful for wallet discovery, batch operations, and API calls. */ export interface RateLimiterOptions { /** Maximum concurrent operations */ maxConcurrent?: number; /** Minimum delay between operations (ms) */ delayMs?: number; /** Maximum queue size (0 = unlimited) */ maxQueueSize?: number; } /** * Simple rate limiter with concurrency control * * @example * ```typescript * const limiter = new RateLimiter({ maxConcurrent: 5, delayMs: 200 }); * * // Schedule multiple operations * const results = await Promise.all( * addresses.map(addr => * limiter.schedule(() => provider.getBalance(addr)) * ) * ); * ``` */ export class RateLimiter { private running = 0; private queue: Array<() => void> = []; private lastExecutionTime = 0; constructor( protected options: RateLimiterOptions = {} ) { this.options.maxConcurrent = options.maxConcurrent || 5; this.options.delayMs = options.delayMs || 100; this.options.maxQueueSize = options.maxQueueSize || 0; // 0 = unlimited } /** * Schedule an async operation with rate limiting * * @param fn - Async function to execute * @returns Promise resolving to function result * @throws Error if queue is full */ async schedule(fn: () => Promise): Promise { // Check queue size if (this.options.maxQueueSize! > 0 && this.queue.length >= this.options.maxQueueSize!) { throw new Error(`Rate limiter queue full (${this.queue.length}/${this.options.maxQueueSize})`); } // Wait for available slot await this.waitForSlot(); this.running++; try { // Enforce minimum delay between operations const now = Date.now(); const timeSinceLastExecution = now - this.lastExecutionTime; if (timeSinceLastExecution < this.options.delayMs!) { await new Promise(resolve => setTimeout(resolve, this.options.delayMs! - timeSinceLastExecution) ); } this.lastExecutionTime = Date.now(); // Execute the function return await fn(); } finally { this.running--; this.processQueue(); } } /** * Wait for an available execution slot */ private async waitForSlot(): Promise { if (this.running < this.options.maxConcurrent!) { return; } return new Promise(resolve => { this.queue.push(resolve); }); } /** * Process queued operations */ private processQueue(): void { if (this.queue.length > 0 && this.running < this.options.maxConcurrent!) { const resolve = this.queue.shift(); if (resolve) { resolve(); } } } /** * Get current stats */ getStats() { return { running: this.running, queued: this.queue.length, maxConcurrent: this.options.maxConcurrent }; } /** * Clear the queue */ clear(): void { this.queue = []; } } /** * Adaptive rate limiter that adjusts based on errors * * Automatically backs off when rate limit errors are detected. */ export class AdaptiveRateLimiter extends RateLimiter { private consecutiveErrors = 0; private baseDelayMs: number; constructor(options: RateLimiterOptions = {}) { super(options); this.baseDelayMs = options.delayMs || 100; } async schedule(fn: () => Promise): Promise { try { const result = await super.schedule(fn); // Success - reset error count this.consecutiveErrors = 0; this.options.delayMs = this.baseDelayMs; return result; } catch (error: any) { // Check if it's a rate limit error if (this.isRateLimitError(error)) { this.consecutiveErrors++; // Exponential backoff this.options.delayMs = Math.min( this.baseDelayMs * Math.pow(2, this.consecutiveErrors), 10000 // Max 10 seconds ); console.warn( `Rate limit detected. Backing off to ${this.options.delayMs}ms delay` ); } throw error; } } private isRateLimitError(error: any): boolean { const message = error.message?.toLowerCase() || ''; return ( message.includes('rate limit') || message.includes('too many requests') || message.includes('429') || error.code === 'RATE_LIMIT' || error.code === 429 ); } }