UNPKG

recoder-shared

Version:

Shared types, utilities, and configurations for Recoder

612 lines (534 loc) 18.3 kB
/** * Error types and handling utilities */ export enum ErrorCode { // Authentication errors AUTHENTICATION_REQUIRED = 'AUTH_001', INVALID_TOKEN = 'AUTH_002', TOKEN_EXPIRED = 'AUTH_003', INSUFFICIENT_PERMISSIONS = 'AUTH_004', INVALID_CREDENTIALS = 'AUTH_005', // API errors API_CONNECTION_FAILED = 'API_001', API_TIMEOUT = 'API_002', API_RATE_LIMIT = 'API_003', API_SERVER_ERROR = 'API_004', API_INVALID_RESPONSE = 'API_005', // Validation errors VALIDATION_FAILED = 'VAL_001', INVALID_INPUT = 'VAL_002', MISSING_REQUIRED_FIELD = 'VAL_003', INVALID_FORMAT = 'VAL_004', VALUE_OUT_OF_RANGE = 'VAL_005', // File system errors FILE_NOT_FOUND = 'FS_001', FILE_ACCESS_DENIED = 'FS_002', FILE_ALREADY_EXISTS = 'FS_003', DIRECTORY_NOT_FOUND = 'FS_004', INVALID_PATH = 'FS_005', DISK_FULL = 'FS_006', // Project errors PROJECT_NOT_FOUND = 'PROJ_001', PROJECT_ALREADY_EXISTS = 'PROJ_002', INVALID_PROJECT_STRUCTURE = 'PROJ_003', UNSUPPORTED_FRAMEWORK = 'PROJ_004', DEPENDENCY_CONFLICT = 'PROJ_005', BUILD_FAILED = 'PROJ_006', // Template errors TEMPLATE_NOT_FOUND = 'TMPL_001', TEMPLATE_INVALID = 'TMPL_002', TEMPLATE_VERSION_MISMATCH = 'TMPL_003', TEMPLATE_RENDER_FAILED = 'TMPL_004', // Generation errors GENERATION_FAILED = 'GEN_001', INVALID_TEMPLATE_VARIABLES = 'GEN_002', OUTPUT_PATH_INVALID = 'GEN_003', CODE_QUALITY_CHECK_FAILED = 'GEN_004', // Configuration errors CONFIG_INVALID = 'CFG_001', CONFIG_MISSING = 'CFG_002', CONFIG_MIGRATION_FAILED = 'CFG_003', // Network errors NETWORK_ERROR = 'NET_001', CONNECTION_TIMEOUT = 'NET_002', DNS_RESOLUTION_FAILED = 'NET_003', // System errors SYSTEM_ERROR = 'SYS_001', MEMORY_LIMIT_EXCEEDED = 'SYS_002', PROCESS_TERMINATED = 'SYS_003', // Unknown/generic errors UNKNOWN_ERROR = 'UNK_001', OPERATION_CANCELLED = 'UNK_002', FEATURE_NOT_IMPLEMENTED = 'UNK_003', } export interface ErrorContext { operation?: string; resource?: string; details?: Record<string, any>; stack?: string; timestamp?: Date; userId?: string; sessionId?: string; httpStatus?: number; path?: string; attempts?: number; validationErrors?: any[]; context?: string; language?: string; promise?: string; } export class RecoderError extends Error { public readonly code: ErrorCode; public readonly context: ErrorContext; public readonly recoverable: boolean; public readonly userMessage: string; public readonly originalError?: Error; constructor( code: ErrorCode, message: string, context: ErrorContext = {}, recoverable: boolean = false, userMessage?: string, originalError?: Error ) { super(message); this.name = 'RecoderError'; this.code = code; this.context = { ...context, timestamp: new Date(), }; this.recoverable = recoverable; this.userMessage = userMessage || this.getDefaultUserMessage(code); this.originalError = originalError; // Maintain proper stack trace if (Error.captureStackTrace) { Error.captureStackTrace(this, RecoderError); } } private getDefaultUserMessage(code: ErrorCode): string { const messages: Record<ErrorCode, string> = { [ErrorCode.AUTHENTICATION_REQUIRED]: 'Please log in to continue', [ErrorCode.INVALID_TOKEN]: 'Your session has expired. Please log in again', [ErrorCode.TOKEN_EXPIRED]: 'Your session has expired. Please log in again', [ErrorCode.INSUFFICIENT_PERMISSIONS]: 'You do not have permission to perform this action', [ErrorCode.INVALID_CREDENTIALS]: 'Invalid username or password', [ErrorCode.API_CONNECTION_FAILED]: 'Unable to connect to the service. Please check your internet connection', [ErrorCode.API_TIMEOUT]: 'The request took too long to complete. Please try again', [ErrorCode.API_RATE_LIMIT]: 'Too many requests. Please wait a moment and try again', [ErrorCode.API_SERVER_ERROR]: 'Service temporarily unavailable. Please try again later', [ErrorCode.API_INVALID_RESPONSE]: 'Received invalid response from server', [ErrorCode.VALIDATION_FAILED]: 'Please check your input and try again', [ErrorCode.INVALID_INPUT]: 'The provided input is invalid', [ErrorCode.MISSING_REQUIRED_FIELD]: 'Please fill in all required fields', [ErrorCode.INVALID_FORMAT]: 'The format of the input is incorrect', [ErrorCode.VALUE_OUT_OF_RANGE]: 'The value is outside the allowed range', [ErrorCode.FILE_NOT_FOUND]: 'The requested file could not be found', [ErrorCode.FILE_ACCESS_DENIED]: 'Access to the file was denied', [ErrorCode.FILE_ALREADY_EXISTS]: 'A file with this name already exists', [ErrorCode.DIRECTORY_NOT_FOUND]: 'The specified directory does not exist', [ErrorCode.INVALID_PATH]: 'The file path is invalid', [ErrorCode.DISK_FULL]: 'Not enough disk space available', [ErrorCode.PROJECT_NOT_FOUND]: 'The project could not be found', [ErrorCode.PROJECT_ALREADY_EXISTS]: 'A project with this name already exists', [ErrorCode.INVALID_PROJECT_STRUCTURE]: 'The project structure is invalid', [ErrorCode.UNSUPPORTED_FRAMEWORK]: 'This framework is not supported', [ErrorCode.DEPENDENCY_CONFLICT]: 'There are conflicting dependencies', [ErrorCode.BUILD_FAILED]: 'The build process failed', [ErrorCode.TEMPLATE_NOT_FOUND]: 'The requested template could not be found', [ErrorCode.TEMPLATE_INVALID]: 'The template is invalid or corrupted', [ErrorCode.TEMPLATE_VERSION_MISMATCH]: 'The template version is incompatible', [ErrorCode.TEMPLATE_RENDER_FAILED]: 'Failed to render the template', [ErrorCode.GENERATION_FAILED]: 'Code generation failed', [ErrorCode.INVALID_TEMPLATE_VARIABLES]: 'Invalid template variables provided', [ErrorCode.OUTPUT_PATH_INVALID]: 'The output path is invalid', [ErrorCode.CODE_QUALITY_CHECK_FAILED]: 'Generated code failed quality checks', [ErrorCode.CONFIG_INVALID]: 'Configuration is invalid', [ErrorCode.CONFIG_MISSING]: 'Configuration file is missing', [ErrorCode.CONFIG_MIGRATION_FAILED]: 'Configuration migration failed', [ErrorCode.NETWORK_ERROR]: 'Network error occurred', [ErrorCode.CONNECTION_TIMEOUT]: 'Connection timed out', [ErrorCode.DNS_RESOLUTION_FAILED]: 'Unable to resolve the server address', [ErrorCode.SYSTEM_ERROR]: 'A system error occurred', [ErrorCode.MEMORY_LIMIT_EXCEEDED]: 'Memory limit exceeded', [ErrorCode.PROCESS_TERMINATED]: 'The process was terminated', [ErrorCode.UNKNOWN_ERROR]: 'An unknown error occurred', [ErrorCode.OPERATION_CANCELLED]: 'The operation was cancelled', [ErrorCode.FEATURE_NOT_IMPLEMENTED]: 'This feature is not yet implemented', }; return messages[code] || 'An error occurred'; } public toJSON(): object { return { name: this.name, message: this.message, code: this.code, context: this.context, recoverable: this.recoverable, userMessage: this.userMessage, stack: this.stack, originalError: this.originalError ? { name: this.originalError.name, message: this.originalError.message, stack: this.originalError.stack, } : undefined, }; } public override toString(): string { return `${this.name} [${this.code}]: ${this.message}`; } } /** * Specialized error classes */ export class AuthenticationError extends RecoderError { constructor(message: string, context: ErrorContext = {}, originalError?: Error) { super(ErrorCode.AUTHENTICATION_REQUIRED, message, context, false, undefined, originalError); this.name = 'AuthenticationError'; } } export class ValidationError extends RecoderError { constructor(message: string, context: ErrorContext = {}, originalError?: Error) { super(ErrorCode.VALIDATION_FAILED, message, context, true, undefined, originalError); this.name = 'ValidationError'; } } export class FileSystemError extends RecoderError { constructor(code: ErrorCode, message: string, context: ErrorContext = {}, originalError?: Error) { super(code, message, context, true, undefined, originalError); this.name = 'FileSystemError'; } } export class ProjectError extends RecoderError { constructor(code: ErrorCode, message: string, context: ErrorContext = {}, originalError?: Error) { super(code, message, context, true, undefined, originalError); this.name = 'ProjectError'; } } export class APIError extends RecoderError { constructor(code: ErrorCode, message: string, context: ErrorContext = {}, originalError?: Error) { super(code, message, context, true, undefined, originalError); this.name = 'APIError'; } } export class TemplateError extends RecoderError { constructor(code: ErrorCode, message: string, context: ErrorContext = {}, originalError?: Error) { super(code, message, context, true, undefined, originalError); this.name = 'TemplateError'; } } export class GenerationError extends RecoderError { constructor(code: ErrorCode, message: string, context: ErrorContext = {}, originalError?: Error) { super(code, message, context, false, undefined, originalError); this.name = 'GenerationError'; } } /** * Error handling utilities */ export class ErrorHandler { private static listeners: Array<(error: RecoderError) => void> = []; /** * Handle an error with proper logging and user feedback */ static handle(error: Error | RecoderError, context: ErrorContext = {}): RecoderError { let recoderError: RecoderError; if (error instanceof RecoderError) { recoderError = error; } else { recoderError = new RecoderError( ErrorCode.UNKNOWN_ERROR, error.message, { ...context, stack: error.stack }, false, undefined, error ); } // Notify listeners this.listeners.forEach(listener => { try { listener(recoderError); } catch (listenerError) { console.error('Error in error handler listener:', listenerError); } }); return recoderError; } /** * Create error from HTTP response */ static fromHTTPResponse(response: any, context: ErrorContext = {}): RecoderError { const status = response.status || response.statusCode || 500; const message = response.data?.message || response.message || 'HTTP request failed'; let code: ErrorCode; let recoverable = true; switch (status) { case 400: code = ErrorCode.INVALID_INPUT; break; case 401: code = ErrorCode.INVALID_TOKEN; recoverable = false; break; case 403: code = ErrorCode.INSUFFICIENT_PERMISSIONS; recoverable = false; break; case 404: code = ErrorCode.FILE_NOT_FOUND; break; case 408: code = ErrorCode.API_TIMEOUT; break; case 409: code = ErrorCode.FILE_ALREADY_EXISTS; break; case 429: code = ErrorCode.API_RATE_LIMIT; break; case 500: case 502: case 503: case 504: code = ErrorCode.API_SERVER_ERROR; break; default: code = ErrorCode.API_CONNECTION_FAILED; } return new APIError(code, message, { ...context, details: { ...context.details, httpStatus: status } }); } /** * Create error from file system operation */ static fromFileSystemError(error: any, operation: string, path: string): FileSystemError { let code: ErrorCode; const message = error.message || 'File system operation failed'; switch (error.code) { case 'ENOENT': code = ErrorCode.FILE_NOT_FOUND; break; case 'EACCES': case 'EPERM': code = ErrorCode.FILE_ACCESS_DENIED; break; case 'EEXIST': code = ErrorCode.FILE_ALREADY_EXISTS; break; case 'ENOTDIR': code = ErrorCode.DIRECTORY_NOT_FOUND; break; case 'EINVAL': code = ErrorCode.INVALID_PATH; break; case 'ENOSPC': code = ErrorCode.DISK_FULL; break; default: code = ErrorCode.SYSTEM_ERROR; } return new FileSystemError(code, message, { operation, details: { path } }, error); } /** * Add error listener */ static addListener(listener: (error: RecoderError) => void): void { this.listeners.push(listener); } /** * Remove error listener */ static removeListener(listener: (error: RecoderError) => void): void { const index = this.listeners.indexOf(listener); if (index > -1) { this.listeners.splice(index, 1); } } /** * Clear all error listeners */ static clearListeners(): void { this.listeners = []; } /** * Check if error is recoverable */ static isRecoverable(error: Error | RecoderError): boolean { if (error instanceof RecoderError) { return error.recoverable; } return true; // Assume recoverable for unknown errors } /** * Get user-friendly error message */ static getUserMessage(error: Error | RecoderError): string { if (error instanceof RecoderError) { return error.userMessage; } return 'An unexpected error occurred. Please try again.'; } /** * Format error for logging */ static formatForLogging(error: Error | RecoderError): string { if (error instanceof RecoderError) { return JSON.stringify(error.toJSON(), null, 2); } return `${error.name}: ${error.message}\n${error.stack}`; } /** * Check if error should be reported to telemetry */ static shouldReport(error: Error | RecoderError): boolean { if (error instanceof RecoderError) { // Don't report user errors or authentication errors return ![ ErrorCode.AUTHENTICATION_REQUIRED, ErrorCode.INVALID_CREDENTIALS, ErrorCode.VALIDATION_FAILED, ErrorCode.INVALID_INPUT, ErrorCode.MISSING_REQUIRED_FIELD, ErrorCode.OPERATION_CANCELLED, ].includes(error.code); } return true; // Report unknown errors } } /** * Async error handling utilities */ export class AsyncErrorHandler { /** * Wrap async function with error handling */ static wrap<T extends any[], R>( fn: (...args: T) => Promise<R>, context: ErrorContext = {} ): (...args: T) => Promise<R> { return async (...args: T): Promise<R> => { try { return await fn(...args); } catch (error) { throw ErrorHandler.handle(error as Error, context); } }; } /** * Execute with retry logic */ static async withRetry<T>( fn: () => Promise<T>, maxRetries: number = 3, delay: number = 1000, backoff: number = 2 ): Promise<T> { let lastError: Error; let currentDelay = delay; for (let attempt = 0; attempt <= maxRetries; attempt++) { try { return await fn(); } catch (error) { lastError = error as Error; // Don't retry non-recoverable errors if (!ErrorHandler.isRecoverable(lastError)) { throw ErrorHandler.handle(lastError); } // Don't retry on last attempt if (attempt === maxRetries) { break; } // Wait before retrying await new Promise(resolve => setTimeout(resolve, currentDelay)); currentDelay *= backoff; } } throw ErrorHandler.handle(lastError!, { details: { attempts: maxRetries + 1 } }); } /** * Execute with timeout */ static async withTimeout<T>( fn: () => Promise<T>, timeoutMs: number, timeoutMessage: string = 'Operation timed out' ): Promise<T> { return new Promise<T>((resolve, reject) => { const timeoutId = setTimeout(() => { reject(new RecoderError(ErrorCode.API_TIMEOUT, timeoutMessage)); }, timeoutMs); fn() .then(result => { clearTimeout(timeoutId); resolve(result); }) .catch(error => { clearTimeout(timeoutId); reject(ErrorHandler.handle(error as Error)); }); }); } } /** * Validation error helpers */ export class ValidationErrorBuilder { private errors: Array<{ field: string; message: string; code?: string }> = []; add(field: string, message: string, code?: string): this { this.errors.push({ field, message, code }); return this; } addIf(condition: boolean, field: string, message: string, code?: string): this { if (condition) { this.add(field, message, code); } return this; } hasErrors(): boolean { return this.errors.length > 0; } getErrors(): Array<{ field: string; message: string; code?: string }> { return [...this.errors]; } throw(): never { if (this.hasErrors()) { const message = this.errors.map(e => `${e.field}: ${e.message}`).join(', '); throw new ValidationError(message, { details: { validationErrors: this.errors } }); } throw new Error('No validation errors to throw'); } throwIf(): void { if (this.hasErrors()) { this.throw(); } } } /** * Global error handler setup */ export function setupGlobalErrorHandling(): void { // Handle unhandled promise rejections if (typeof process !== 'undefined' && process.on) { process.on('unhandledRejection', (reason: any, promise: Promise<any>) => { console.error('Unhandled Promise Rejection:', reason); const error = reason instanceof Error ? reason : new Error(String(reason)); const recoderError = ErrorHandler.handle(error, { details: { context: 'unhandledRejection' }, promise: promise.toString() }); // Could report to telemetry here console.error('Handled as:', recoderError.toString()); }); // Handle uncaught exceptions process.on('uncaughtException', (error: Error) => { console.error('Uncaught Exception:', error); const recoderError = ErrorHandler.handle(error, { details: { context: 'uncaughtException' } }); // Could report to telemetry here console.error('Handled as:', recoderError.toString()); // Exit process in case of uncaught exception process.exit(1); }); } }