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
JavaScript
"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