UNPKG

ai-seo

Version:

AI-native JSON-LD schema utility with automated URL-to-Schema generation, intelligent content analysis, caching, rate limiting, performance monitoring, and AI optimization (ChatGPT, Voice). Complete automation & scale features. Zero runtime dependencies.

410 lines (352 loc) 9.32 kB
/** * Rate Limiter - v1.12.0 * Intelligent rate limiting with backoff strategies * * Features: * - Configurable rate limits * - Exponential backoff * - Request queuing * - Per-domain limits * - Statistics tracking */ export class RateLimiter { constructor(options = {}) { this.options = { maxRequests: options.maxRequests || 10, // Requests per window windowMs: options.windowMs || 60000, // 1 minute default strategy: options.strategy || 'fixed', // 'fixed' or 'sliding' backoff: options.backoff || 'exponential', // 'exponential' or 'linear' maxRetries: options.maxRetries || 3, maxQueueSize: options.maxQueueSize || 1000, executionTimeoutMs: options.executionTimeoutMs || 30000, enabled: options.enabled !== false }; // Track requests per domain this.domains = new Map(); // Request queue this._queue = []; this._processing = false; // Statistics this.stats = { total: 0, allowed: 0, limited: 0, queued: 0, retries: 0, rejected: 0, timedOut: 0 }; } /** * Check if request is allowed * @param {string} url - URL to check * @returns {Object} Result with allowed status */ async check(url) { if (!this.options.enabled) { this.stats.allowed++; return { allowed: true, wait: 0 }; } const domain = this._extractDomain(url); const now = Date.now(); if (!this.domains.has(domain)) { this.domains.set(domain, { requests: [], retryCount: 0, lastRequest: 0 }); } const domainData = this.domains.get(domain); // Clean old requests this._cleanOldRequests(domainData, now); // Check if limit reached if (domainData.requests.length >= this.options.maxRequests) { this.stats.limited++; const oldestRequest = domainData.requests[0]; const wait = this.options.windowMs - (now - oldestRequest); return { allowed: false, wait, retryAfter: now + wait }; } // Allow request domainData.requests.push(now); domainData.lastRequest = now; this.stats.allowed++; this.stats.total++; return { allowed: true, wait: 0 }; } /** * Wait for rate limit availability * @param {string} url - URL to wait for * @returns {Promise<void>} */ async waitFor(url) { while (true) { const result = await this.check(url); if (result.allowed) { return; } await new Promise(resolve => setTimeout(resolve, Math.max(0, result.wait))); } } /** * Execute function with rate limiting * @param {string} url - URL for rate limiting * @param {Function} fn - Function to execute * @returns {Promise<*>} Function result */ async execute(url, fn) { await this.waitFor(url); return fn(); } /** * Queue request for later execution * @param {string} url - URL * @param {Function} fn - Function to execute * @returns {Promise<*>} Queued promise */ async enqueue(url, fn) { this.stats.queued++; return new Promise((resolve, reject) => { if (this._queue.length >= this.options.maxQueueSize) { this.stats.rejected++; reject(new Error('Rate limiter queue is full')); return; } this._queue.push({ url, fn, resolve, reject, attempts: 0, addedAt: Date.now() }); this._processQueue(); }); } /** * Process queued requests * @private */ async _processQueue() { if (this._processing || this._queue.length === 0) { return; } this._processing = true; try { while (this._queue.length > 0) { const item = this._queue[0]; try { await this.waitFor(item.url); const result = await this._withTimeout( Promise.resolve().then(() => item.fn()), this.options.executionTimeoutMs ); item.resolve(result); this._queue.shift(); } catch (error) { item.attempts++; if (error && error.name === 'RateLimiterTimeoutError') { this.stats.timedOut++; } if (item.attempts >= this.options.maxRetries) { item.reject(error); this._queue.shift(); } else { this.stats.retries++; // Calculate backoff wait time const backoffWait = this._calculateBackoff(item.attempts); await new Promise(resolve => setTimeout(resolve, backoffWait)); } } } } finally { this._processing = false; } } /** * Calculate backoff wait time * @private */ _calculateBackoff(attempts) { if (this.options.backoff === 'exponential') { return Math.min(1000 * Math.pow(2, attempts), 30000); // Max 30 seconds } else { return Math.min(1000 * attempts, 10000); // Max 10 seconds } } _withTimeout(promise, timeoutMs) { if (!timeoutMs || timeoutMs <= 0) { return promise; } let timeoutId; const timeoutPromise = new Promise((_, reject) => { timeoutId = setTimeout(() => { const error = new Error(`Queue item timed out after ${timeoutMs}ms`); error.name = 'RateLimiterTimeoutError'; reject(error); }, timeoutMs); }); return Promise.race([promise, timeoutPromise]).finally(() => { clearTimeout(timeoutId); }); } /** * Extract domain from URL * @private */ _extractDomain(url) { try { const urlObj = new URL(url); return urlObj.hostname; } catch (error) { return 'unknown'; } } /** * Clean old requests from tracking * @private */ _cleanOldRequests(domainData, now) { const cutoff = now - this.options.windowMs; domainData.requests = domainData.requests.filter(time => time > cutoff); } /** * Get statistics * @returns {Object} Statistics */ getStats() { return { ...this.stats, queueSize: this._queue.length, domains: this.domains.size, limitRate: this.stats.total > 0 ? ((this.stats.limited / this.stats.total) * 100).toFixed(2) + '%' : '0%' }; } /** * Get domain statistics * @param {string} domain - Domain name * @returns {Object} Domain stats */ getDomainStats(domain) { if (!this.domains.has(domain)) { return null; } const data = this.domains.get(domain); const now = Date.now(); this._cleanOldRequests(data, now); return { domain, currentRequests: data.requests.length, maxRequests: this.options.maxRequests, remaining: this.options.maxRequests - data.requests.length, lastRequest: data.lastRequest, timeSinceLastRequest: now - data.lastRequest }; } /** * Reset domain rate limit * @param {string} domain - Domain to reset */ resetDomain(domain) { this.domains.delete(domain); } /** * Reset all rate limits */ resetAll() { this.domains.clear(); this._queue = []; this.stats = { total: 0, allowed: 0, limited: 0, queued: 0, retries: 0, rejected: 0, timedOut: 0 }; } /** * Clear queue */ clearQueue() { for (const item of this._queue) { item.reject(new Error('Queue cleared')); } this._queue = []; } /** * Get queue size * @returns {number} Queue size */ getQueueSize() { return this._queue.length; } /** * Batch process URLs with rate limiting * @param {string[]} urls - URLs to process * @param {Function} fn - Function to call for each URL * @param {Object} options - Processing options * @returns {Promise<Array>} Results */ async batchProcess(urls, fn, options = {}) { const { concurrency = 3, onProgress = null } = options; const results = []; const active = new Set(); for (let i = 0; i < urls.length; i++) { const url = urls[i]; // Wait if we're at concurrency limit while (active.size >= concurrency) { await Promise.race(active); } const promise = this.execute(url, () => fn(url)) .then(result => { results[i] = result; active.delete(promise); if (onProgress) { onProgress(url, i + 1, urls.length); } return result; }) .catch(error => { results[i] = { error: error.message, url }; active.delete(promise); return results[i]; }); active.add(promise); } // Wait for remaining promises await Promise.all(active); return results; } } // Global rate limiter instance let globalLimiter = null; /** * Get global rate limiter instance * @param {Object} options - Limiter options * @returns {RateLimiter} Rate limiter instance */ export function getRateLimiter(options = {}) { if (!globalLimiter) { globalLimiter = new RateLimiter(options); } return globalLimiter; } /** * Reset global rate limiter */ export function resetRateLimiter() { if (globalLimiter) { globalLimiter.resetAll(); } globalLimiter = null; }