super-pancake-automation
Version:
A lightweight DOM-based UI automation framework using Chrome DevTools Protocol
451 lines (386 loc) • 13.4 kB
JavaScript
// Core error types for Super Pancake Automation Framework
export class SuperPancakeError extends Error {
constructor(message, code = 'UNKNOWN_ERROR', context = {}) {
super(message);
this.name = 'SuperPancakeError';
this.code = code;
this.context = context;
this.timestamp = new Date().toISOString();
// Maintain proper stack trace
if (Error.captureStackTrace) {
Error.captureStackTrace(this, SuperPancakeError);
}
}
toJSON() {
return {
name: this.name,
message: this.message,
code: this.code,
context: this.context,
timestamp: this.timestamp,
stack: this.stack
};
}
}
export class ElementNotFoundError extends SuperPancakeError {
constructor(selector, context = {}) {
super(`Element not found for selector: "${selector}"`, 'ELEMENT_NOT_FOUND', {
selector,
...context
});
}
}
export class TimeoutError extends SuperPancakeError {
constructor(operation, timeout, context = {}) {
super(`Operation "${operation}" timed out after ${timeout}ms`, 'TIMEOUT_ERROR', {
operation,
timeout,
...context
});
}
}
export class ValidationError extends SuperPancakeError {
constructor(parameter, expected, actual, context = {}) {
super(`Invalid ${parameter}: expected ${expected}, got ${actual}`, 'VALIDATION_ERROR', {
parameter,
expected,
actual,
...context
});
}
}
export class SessionError extends SuperPancakeError {
constructor(message, context = {}) {
super(message, 'SESSION_ERROR', context);
}
}
export class SecurityError extends SuperPancakeError {
constructor(message, context = {}) {
super(message, 'SECURITY_ERROR', context);
}
}
export class StackTraceError extends SuperPancakeError {
constructor(originalError, operation, context = {}) {
const message = `STACK_TRACE_ERROR in ${operation}: ${originalError?.message || 'Unknown error'}`;
super(message, 'STACK_TRACE_ERROR', {
originalError: originalError?.message,
originalStack: originalError?.stack,
operation,
...context
});
// Preserve original stack trace if available
if (originalError?.stack) {
this.originalStack = originalError.stack;
}
}
}
export class CircuitBreakerError extends SuperPancakeError {
constructor(operation, failures, context = {}) {
super(`Circuit breaker open for ${operation} after ${failures} failures`, 'CIRCUIT_BREAKER_OPEN', {
operation,
failures,
...context
});
}
}
export class RecoveryError extends SuperPancakeError {
constructor(operation, attempts, context = {}) {
super(`Recovery failed for ${operation} after ${attempts} attempts`, 'RECOVERY_FAILED', {
operation,
attempts,
...context
});
}
}
// Enhanced error recovery utilities
export function withRetry(fn, options = {}) {
const {
maxRetries = 3,
delay = 1000,
backoffMultiplier = 1.5,
maxDelay = 10000,
operation = 'unknown operation'
} = options;
return async function retryWrapper(...args) {
let lastError;
let currentDelay = delay;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const result = await fn(...args);
// Log successful recovery if this wasn't the first attempt
if (attempt > 1) {
console.log(`✅ Operation "${operation}" succeeded on attempt ${attempt}`);
}
return result;
} catch (error) {
lastError = error;
// Don't retry on certain error types
if (error instanceof ValidationError ||
error instanceof SecurityError ||
error instanceof CircuitBreakerError) {
throw error;
}
// Enhanced error logging
console.warn(`⚠️ Attempt ${attempt}/${maxRetries} failed for "${operation}": ${error.message}`);
if (attempt < maxRetries) {
console.log(`🔄 Retrying in ${currentDelay}ms...`);
await new Promise(resolve => setTimeout(resolve, currentDelay));
currentDelay = Math.min(currentDelay * backoffMultiplier, maxDelay);
}
}
}
// All retries failed - create comprehensive error
throw new RecoveryError(operation, maxRetries, {
originalError: lastError?.message,
originalCode: lastError?.code,
attempts: maxRetries,
totalDelay: delay * (Math.pow(backoffMultiplier, maxRetries) - 1) / (backoffMultiplier - 1)
});
};
}
// Comprehensive error wrapper to prevent STACK_TRACE_ERROR
export function withErrorRecovery(fn, operation = 'unknown operation') {
return async function errorRecoveryWrapper(...args) {
try {
return await fn(...args);
} catch (error) {
// Prevent STACK_TRACE_ERROR by catching and wrapping all errors
if (error.name === 'STACK_TRACE_ERROR' || !error.message) {
throw new StackTraceError(error, operation, {
args: args.length,
functionName: fn.name || 'anonymous'
});
}
// Enhance error context for better debugging
if (error instanceof SuperPancakeError) {
error.context = {
...error.context,
operation,
timestamp: new Date().toISOString(),
functionName: fn.name || 'anonymous'
};
throw error;
}
// Wrap unknown errors
throw new SuperPancakeError(
`Unexpected error in ${operation}: ${error.message}`,
'UNEXPECTED_ERROR',
{
originalError: error.message,
originalStack: error.stack,
operation,
functionName: fn.name || 'anonymous'
}
);
}
};
}
// Safe execution wrapper for critical operations
export function safeExecute(fn, fallback = null, operation = 'unknown operation') {
return async function safeExecuteWrapper(...args) {
try {
return await withErrorRecovery(fn, operation)(...args);
} catch (error) {
console.error(`🚨 Critical error in ${operation}:`, error.message);
if (fallback && typeof fallback === 'function') {
console.log(`🔄 Executing fallback for ${operation}...`);
try {
return await fallback(...args);
} catch (fallbackError) {
console.error(`❌ Fallback also failed for ${operation}:`, fallbackError.message);
throw new SuperPancakeError(
`Both primary and fallback operations failed for ${operation}`,
'COMPLETE_FAILURE',
{
primaryError: error.message,
fallbackError: fallbackError.message,
operation
}
);
}
}
throw error;
}
};
}
// Input validation utilities
export function validateSession(session) {
if (!session || typeof session.send !== 'function') {
throw new ValidationError('session', 'valid session object with send method', typeof session);
}
}
export function validateSelector(selector) {
if (!selector || typeof selector !== 'string' || selector.trim().length === 0) {
throw new ValidationError('selector', 'non-empty string', typeof selector);
}
// Basic CSS selector validation
if (selector.includes('<script') || selector.includes('javascript:')) {
throw new SecurityError('Selector contains potentially malicious content', { selector });
}
}
export function validateTimeout(timeout) {
if (timeout !== undefined && (!Number.isInteger(timeout) || timeout < 0)) {
throw new ValidationError('timeout', 'positive integer', timeout);
}
}
export function validateText(text, paramName = 'text') {
if (text === null || text === undefined) {
throw new ValidationError(paramName, 'string or number', typeof text);
}
// Check for potential code injection
const textStr = String(text);
if (textStr.includes('<script') || textStr.includes('javascript:') || textStr.includes('data:text/html')) {
throw new SecurityError(`${paramName} contains potentially malicious content`, { [paramName]: textStr });
}
}
export function validateFilePath(filePath) {
if (!filePath || typeof filePath !== 'string') {
throw new ValidationError('filePath', 'non-empty string', typeof filePath);
}
// Prevent directory traversal
if (filePath.includes('..') || filePath.includes('~')) {
throw new SecurityError('File path contains directory traversal patterns', { filePath });
}
}
export function sanitizeForExecution(value) {
if (typeof value === 'string') {
// Escape special characters for safe JavaScript execution
return value
.replace(/\\/g, '\\\\')
.replace(/'/g, "\\'")
.replace(/"/g, '\\"')
.replace(/\n/g, '\\n')
.replace(/\r/g, '\\r')
.replace(/\t/g, '\\t');
}
return value;
}
// Circuit Breaker Implementation
export class CircuitBreaker {
constructor(options = {}) {
this.name = options.name || 'unknown';
this.failureThreshold = options.failureThreshold || 5;
this.recoveryTimeout = options.recoveryTimeout || 30000; // 30 seconds
this.monitoringWindow = options.monitoringWindow || 60000; // 1 minute
// Circuit states: 'closed', 'open', 'half-open'
this.state = 'closed';
this.failures = 0;
this.successes = 0;
this.lastFailureTime = null;
this.lastSuccessTime = null;
// Request tracking
this.requests = [];
console.log(`🔧 Circuit breaker "${this.name}" initialized (threshold: ${this.failureThreshold})`);
}
async execute(fn, ...args) {
// Check if circuit should transition from open to half-open
if (this.state === 'open' && this.shouldAttemptRecovery()) {
this.state = 'half-open';
console.log(`🔄 Circuit breaker "${this.name}" transitioning to half-open`);
}
// Reject immediately if circuit is open
if (this.state === 'open') {
throw new CircuitBreakerError(this.name, this.failures, {
state: this.state,
lastFailureTime: this.lastFailureTime,
timeUntilRetry: this.recoveryTimeout - (Date.now() - this.lastFailureTime)
});
}
try {
const result = await fn(...args);
this.onSuccess();
return result;
} catch (error) {
this.onFailure(error);
throw error;
}
}
onSuccess() {
this.successes++;
this.lastSuccessTime = Date.now();
// Record successful request
this.requests.push({
timestamp: Date.now(),
success: true
});
// If in half-open state, close the circuit after successful execution
if (this.state === 'half-open') {
this.state = 'closed';
this.failures = 0;
console.log(`✅ Circuit breaker "${this.name}" closed after successful recovery`);
}
this.cleanupOldRequests();
}
onFailure(error) {
this.failures++;
this.lastFailureTime = Date.now();
// Record failed request
this.requests.push({
timestamp: Date.now(),
success: false,
error: error.message
});
console.warn(`⚠️ Circuit breaker "${this.name}" failure ${this.failures}/${this.failureThreshold}: ${error.message}`);
// Open circuit if failure threshold exceeded
if (this.failures >= this.failureThreshold) {
this.state = 'open';
console.error(`🚨 Circuit breaker "${this.name}" opened after ${this.failures} failures`);
}
this.cleanupOldRequests();
}
shouldAttemptRecovery() {
return this.lastFailureTime &&
(Date.now() - this.lastFailureTime) > this.recoveryTimeout;
}
cleanupOldRequests() {
const cutoff = Date.now() - this.monitoringWindow;
this.requests = this.requests.filter(req => req.timestamp > cutoff);
}
getStats() {
this.cleanupOldRequests();
const recentRequests = this.requests.length;
const recentFailures = this.requests.filter(req => !req.success).length;
const recentSuccesses = this.requests.filter(req => req.success).length;
const failureRate = recentRequests > 0 ? (recentFailures / recentRequests) * 100 : 0;
return {
name: this.name,
state: this.state,
totalFailures: this.failures,
totalSuccesses: this.successes,
recentRequests,
recentFailures,
recentSuccesses,
failureRate: Math.round(failureRate * 100) / 100,
lastFailureTime: this.lastFailureTime,
lastSuccessTime: this.lastSuccessTime,
failureThreshold: this.failureThreshold,
isHealthy: this.state === 'closed' && failureRate < 50
};
}
reset() {
this.state = 'closed';
this.failures = 0;
this.successes = 0;
this.lastFailureTime = null;
this.requests = [];
console.log(`🔄 Circuit breaker "${this.name}" reset`);
}
}
// Global circuit breaker registry
const circuitBreakers = new Map();
export function getCircuitBreaker(name, options = {}) {
if (!circuitBreakers.has(name)) {
circuitBreakers.set(name, new CircuitBreaker({ name, ...options }));
}
return circuitBreakers.get(name);
}
export function getAllCircuitBreakers() {
return Array.from(circuitBreakers.values()).map(cb => cb.getStats());
}
// Enhanced operation wrapper with circuit breaker
export function withCircuitBreaker(fn, breakerName, options = {}) {
const breaker = getCircuitBreaker(breakerName, options);
return async function circuitBreakerWrapper(...args) {
return await breaker.execute(fn, ...args);
};
}