UNPKG

tdpw

Version:

CLI tool for uploading Playwright test reports to TestDino platform with Azure storage support

180 lines 6.57 kB
"use strict"; /** * Retry utility with exponential backoff and circuit breaker * Handles network failures and temporary service unavailability */ Object.defineProperty(exports, "__esModule", { value: true }); exports.CircuitBreaker = void 0; exports.withRetry = withRetry; exports.withFileUploadRetry = withFileUploadRetry; exports.createApiCircuitBreaker = createApiCircuitBreaker; const types_1 = require("../types"); /** * Default retry configuration */ const DEFAULT_OPTIONS = { maxAttempts: 3, baseDelay: 1000, maxDelay: 30000, factor: 2, timeout: 120000, // 2 minutes as specified shouldRetry: (error) => { // Retry on network errors, timeouts, and 5xx server errors if (error instanceof types_1.NetworkError) return true; if (error.message.includes('timeout')) return true; if (error.message.includes('ECONNRESET')) return true; if (error.message.includes('ENOTFOUND')) return true; if (error.message.includes('fetch failed')) return true; // Check for HTTP status codes that warrant retry if (error.message.match(/50[0-9]/)) return true; // 500-509 if (error.message.includes('429')) return true; // Rate limiting return false; }, }; /** * Execute an operation with retry logic and exponential backoff */ async function withRetry(operation, options = {}) { const config = { ...DEFAULT_OPTIONS, ...options }; let lastError; for (let attempt = 1; attempt <= config.maxAttempts; attempt++) { try { // Wrap operation with timeout const result = await Promise.race([ operation(), new Promise((_, reject) => { setTimeout(() => { reject(new Error(`Operation timeout after ${config.timeout}ms`)); }, config.timeout); }), ]); return result; } catch (error) { lastError = error; // Check if we should retry this error if (!config.shouldRetry(lastError)) { throw lastError; } // If this is the last attempt, throw the error if (attempt === config.maxAttempts) { throw new types_1.NetworkError(`Operation failed after ${config.maxAttempts} attempts: ${lastError.message}`, lastError); } // Calculate delay with exponential backoff const delay = Math.min(config.baseDelay * config.factor ** (attempt - 1), config.maxDelay); console.warn(`⚠️ Attempt ${attempt}/${config.maxAttempts} failed: ${lastError.message}`); console.warn(` Retrying in ${delay}ms...`); // Wait before next attempt await new Promise(resolve => setTimeout(resolve, delay)); } } // This should never be reached, but TypeScript requires it throw lastError; } /** * Specialized retry for file upload operations * Uses different retry logic optimized for large file uploads */ async function withFileUploadRetry(operation, _fileName) { return withRetry(operation, { maxAttempts: 3, baseDelay: 2000, // Longer initial delay for file operations maxDelay: 60000, // Allow longer delays for large files timeout: 300000, // 5 minutes for file uploads shouldRetry: (error) => { // More lenient retry for file uploads const message = error.message.toLowerCase(); // Retry on common file upload failures if (message.includes('network')) return true; if (message.includes('timeout')) return true; if (message.includes('connection')) return true; if (message.includes('upload')) return true; if (message.includes('blob')) return true; if (message.includes('storage')) return true; // Don't retry on authentication or permission errors if (message.includes('unauthorized') || message.includes('403')) return false; if (message.includes('forbidden')) return false; return DEFAULT_OPTIONS.shouldRetry(error); }, }); } /** * Circuit breaker pattern for external service calls * Prevents cascading failures when external services are down */ class CircuitBreaker { operation; options; failureCount = 0; lastFailureTime = 0; isOpen = false; constructor(operation, options) { this.operation = operation; this.options = options; } async execute() { // Check if circuit should be reset if (this.isOpen && Date.now() - this.lastFailureTime > this.options.resetTimeout) { this.isOpen = false; this.failureCount = 0; this.options.monitor?.('half-open'); } // Fail fast if circuit is open if (this.isOpen) { throw new Error('Circuit breaker is OPEN - service temporarily unavailable'); } try { const result = await this.operation(); // Reset on success if (this.failureCount > 0) { this.failureCount = 0; this.options.monitor?.('closed'); } return result; } catch (error) { this.failureCount++; this.lastFailureTime = Date.now(); // Open circuit if threshold reached if (this.failureCount >= this.options.failureThreshold) { this.isOpen = true; this.options.monitor?.('open'); } throw error; } } } exports.CircuitBreaker = CircuitBreaker; /** * Helper to create a circuit breaker for API calls */ function createApiCircuitBreaker(operation, serviceName) { return new CircuitBreaker(operation, { failureThreshold: 5, // Open after 5 failures resetTimeout: 60000, // Try again after 1 minute monitor: (state) => { if (state === 'open') { console.warn(`🚨 Circuit breaker OPEN for ${serviceName} - service unavailable`); } else if (state === 'closed') { console.log(`✅ Circuit breaker CLOSED for ${serviceName} - service recovered`); } }, }); } //# sourceMappingURL=retry.js.map