UNPKG

a11yanalyze

Version:

A command-line tool for developers and QA engineers to test web pages and websites for WCAG 2.2 AA accessibility compliance

404 lines 14.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ErrorResilienceManager = exports.RetryManager = exports.TimeoutManager = exports.CircuitBreaker = exports.CircuitBreakerState = void 0; /** * Circuit breaker states */ var CircuitBreakerState; (function (CircuitBreakerState) { CircuitBreakerState["CLOSED"] = "CLOSED"; CircuitBreakerState["OPEN"] = "OPEN"; CircuitBreakerState["HALF_OPEN"] = "HALF_OPEN"; })(CircuitBreakerState || (exports.CircuitBreakerState = CircuitBreakerState = {})); /** * Circuit breaker for preventing cascading failures */ class CircuitBreaker { constructor(threshold, timeout, name) { this.threshold = threshold; this.timeout = timeout; this.name = name; this.state = CircuitBreakerState.CLOSED; this.failureCount = 0; this.lastFailureTime = 0; this.nextAttemptTime = 0; } async execute(operation) { if (this.state === CircuitBreakerState.OPEN) { if (Date.now() >= this.nextAttemptTime) { this.state = CircuitBreakerState.HALF_OPEN; } else { throw new Error(`Circuit breaker ${this.name} is OPEN. Next attempt at ${new Date(this.nextAttemptTime)}`); } } try { const result = await operation(); this.onSuccess(); return result; } catch (error) { this.onFailure(); throw error; } } onSuccess() { this.failureCount = 0; this.state = CircuitBreakerState.CLOSED; } onFailure() { this.failureCount++; this.lastFailureTime = Date.now(); if (this.failureCount >= this.threshold) { this.state = CircuitBreakerState.OPEN; this.nextAttemptTime = Date.now() + this.timeout; } } getState() { return this.state; } getFailureCount() { return this.failureCount; } reset() { this.failureCount = 0; this.state = CircuitBreakerState.CLOSED; this.nextAttemptTime = 0; } } exports.CircuitBreaker = CircuitBreaker; /** * Adaptive timeout manager */ class TimeoutManager { constructor(config) { this.metrics = new Map(); this.config = config; } /** * Get timeout for operation with adaptive adjustment */ getTimeout(operationType) { const baseTimeout = this.config.operationTimeouts[operationType] || 5000; if (!this.config.adaptiveTimeouts) { return baseTimeout; } const metrics = this.metrics.get(operationType); if (!metrics) { return baseTimeout; } // Adjust timeout based on historical performance const adaptiveFactor = this.calculateAdaptiveFactor(metrics); const adaptiveTimeout = Math.max(baseTimeout * adaptiveFactor, baseTimeout * 0.5 // Minimum 50% of base timeout ); return Math.min(adaptiveTimeout, baseTimeout * 3); // Maximum 300% of base timeout } /** * Record operation execution metrics */ recordExecution(operationType, executionTime, success, timedOut = false) { const existing = this.metrics.get(operationType) || { successCount: 0, failureCount: 0, averageExecutionTime: 0, lastExecutionTime: 0, timeouts: 0, }; const totalOperations = existing.successCount + existing.failureCount; if (success) { existing.successCount++; } else { existing.failureCount++; } if (timedOut) { existing.timeouts++; } // Update average execution time existing.averageExecutionTime = (existing.averageExecutionTime * totalOperations + executionTime) / (totalOperations + 1); existing.lastExecutionTime = executionTime; this.metrics.set(operationType, existing); } /** * Calculate adaptive factor based on metrics */ calculateAdaptiveFactor(metrics) { const totalOperations = metrics.successCount + metrics.failureCount; if (totalOperations < 5) { return 1.0; // Not enough data for adaptation } const successRate = metrics.successCount / totalOperations; const timeoutRate = metrics.timeouts / totalOperations; // Increase timeout if high failure/timeout rate if (successRate < 0.8 || timeoutRate > 0.2) { return 1.5; } // Decrease timeout if consistently fast and successful if (successRate > 0.95 && timeoutRate < 0.05) { return 0.8; } return 1.0; } getMetrics() { return new Map(this.metrics); } reset() { this.metrics.clear(); } } exports.TimeoutManager = TimeoutManager; /** * Retry manager with configurable strategies */ class RetryManager { constructor(config) { this.config = config; } /** * Execute operation with retry logic */ async executeWithRetry(operation, strategy = {}, operationType) { const retryConfig = { maxAttempts: strategy.maxAttempts ?? this.config.maxRetries, delay: strategy.delay ?? this.config.baseRetryDelay, backoffType: strategy.backoffType ?? 'exponential', jitter: strategy.jitter ?? true, retryableErrors: strategy.retryableErrors ?? ['timeout', 'network', 'runtime'], }; let lastError; let attempt = 0; while (attempt < retryConfig.maxAttempts) { try { const startTime = Date.now(); const result = await operation(); return result; } catch (error) { lastError = error; attempt++; if (attempt >= retryConfig.maxAttempts) { break; } if (!this.isRetryableError(error, retryConfig.retryableErrors)) { throw error; } const delay = this.calculateDelay(attempt, retryConfig); await this.sleep(delay); } } throw new Error(`Operation ${operationType} failed after ${attempt} attempts. Last error: ${lastError?.message || 'Unknown error'}`); } /** * Check if error is retryable */ isRetryableError(error, retryableTypes) { const errorMessage = error.message.toLowerCase(); return retryableTypes.some(type => errorMessage.includes(type.toLowerCase()) || error.name.toLowerCase().includes(type.toLowerCase())); } /** * Calculate delay for retry attempt */ calculateDelay(attempt, strategy) { let delay; switch (strategy.backoffType) { case 'linear': delay = strategy.delay * attempt; break; case 'exponential': delay = strategy.delay * Math.pow(this.config.backoffMultiplier, attempt - 1); break; case 'fixed': default: delay = strategy.delay; break; } // Apply jitter to prevent thundering herd if (strategy.jitter) { delay = delay * (0.5 + Math.random() * 0.5); } return Math.min(delay, this.config.maxRetryDelay); } /** * Sleep for specified duration */ sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } } exports.RetryManager = RetryManager; /** * Comprehensive error resilience manager */ class ErrorResilienceManager { constructor(config = {}) { this.circuitBreakers = new Map(); this.activeOperations = new Set(); this.config = { maxRetries: 3, baseRetryDelay: 1000, maxRetryDelay: 10000, backoffMultiplier: 2, circuitBreakerThreshold: 5, circuitBreakerTimeout: 30000, operationTimeouts: { navigation: 30000, pageReady: 10000, ruleExecution: 15000, metadataExtraction: 5000, screenshot: 10000, }, adaptiveTimeouts: true, maxConcurrentOperations: 10, ...config, }; this.timeoutManager = new TimeoutManager(this.config); this.retryManager = new RetryManager(this.config); } /** * Execute operation with full resilience features */ async executeResilient(operation, operationType, options = {}) { const { useCircuitBreaker = true, useRetry = true, useTimeout = true, retryStrategy = {}, } = options; // Check concurrent operation limit if (this.activeOperations.size >= this.config.maxConcurrentOperations) { throw new Error(`Maximum concurrent operations (${this.config.maxConcurrentOperations}) exceeded`); } const operationId = `${operationType}-${Date.now()}-${Math.random()}`; this.activeOperations.add(operationId); try { const wrappedOperation = async () => { const startTime = Date.now(); let result; let timedOut = false; try { if (useTimeout) { const timeout = this.timeoutManager.getTimeout(operationType); result = await Promise.race([ operation(), this.createTimeoutPromise(timeout, operationType), ]); } else { result = await operation(); } const executionTime = Date.now() - startTime; this.timeoutManager.recordExecution(operationType, executionTime, true, false); return result; } catch (error) { const executionTime = Date.now() - startTime; timedOut = error instanceof Error && error.message.includes('timeout'); this.timeoutManager.recordExecution(operationType, executionTime, false, timedOut); throw error; } }; let finalOperation = wrappedOperation; // Wrap with retry logic if (useRetry) { const retryOperation = () => this.retryManager.executeWithRetry(wrappedOperation, retryStrategy, operationType); finalOperation = retryOperation; } // Wrap with circuit breaker if (useCircuitBreaker) { const circuitBreaker = this.getOrCreateCircuitBreaker(operationType); return await circuitBreaker.execute(finalOperation); } return await finalOperation(); } finally { this.activeOperations.delete(operationId); } } /** * Create timeout promise */ createTimeoutPromise(timeout, operationType) { return new Promise((_, reject) => { setTimeout(() => { reject(new Error(`Operation ${operationType} timed out after ${timeout}ms`)); }, timeout); }); } /** * Get or create circuit breaker for operation type */ getOrCreateCircuitBreaker(operationType) { if (!this.circuitBreakers.has(operationType)) { this.circuitBreakers.set(operationType, new CircuitBreaker(this.config.circuitBreakerThreshold, this.config.circuitBreakerTimeout, operationType)); } return this.circuitBreakers.get(operationType); } /** * Get resilience status and metrics */ getStatus() { const circuitBreakerStatus = Array.from(this.circuitBreakers.entries()).map(([name, cb]) => ({ name, state: cb.getState(), failureCount: cb.getFailureCount(), })); return { config: this.config, activeOperations: this.activeOperations.size, circuitBreakers: circuitBreakerStatus, operationMetrics: Object.fromEntries(this.timeoutManager.getMetrics()), }; } /** * Reset all resilience components */ reset() { this.circuitBreakers.forEach(cb => cb.reset()); this.timeoutManager.reset(); this.activeOperations.clear(); } /** * Update configuration */ updateConfig(newConfig) { this.config = { ...this.config, ...newConfig }; this.timeoutManager = new TimeoutManager(this.config); this.retryManager = new RetryManager(this.config); } /** * Categorize and enrich error information */ categorizeError(error, context) { const message = error.message.toLowerCase(); let type = 'runtime'; if (message.includes('timeout') || message.includes('time out') || message.includes('timed out')) { type = 'timeout'; } else if (message.includes('network') || message.includes('connection') || message.includes('dns') || message.includes('unreachable')) { type = 'network'; } else if (message.includes('parse') || message.includes('syntax') || message.includes('invalid html') || message.includes('malformed')) { type = 'parsing'; } return { type, message: error.message, details: JSON.stringify({ context, originalError: error.name, stack: error.stack?.split('\n').slice(0, 3).join('\n'), // Truncated stack timestamp: new Date().toISOString(), }), }; } /** * Create error with retry information */ createRetryableError(originalError, attempt, maxAttempts) { const error = new Error(`${originalError.message} (attempt ${attempt}/${maxAttempts})`); error.name = `Retryable${originalError.name}`; error.stack = originalError.stack; return error; } } exports.ErrorResilienceManager = ErrorResilienceManager; //# sourceMappingURL=error-resilience.js.map