/** * Custom error types for Brainy operations * Provides better error classification and handling */ export type BrainyErrorType = | 'TIMEOUT' | 'NETWORK' | 'STORAGE' | 'NOT_FOUND' | 'RETRY_EXHAUSTED' | 'VALIDATION' | 'FIELD_NOT_INDEXED' /** * Custom error class for Brainy operations * Provides error type classification and retry information */ export class BrainyError extends Error { public readonly type: BrainyErrorType public readonly retryable: boolean public readonly originalError?: Error public readonly attemptNumber?: number public readonly maxRetries?: number constructor( message: string, type: BrainyErrorType, retryable: boolean = false, originalError?: Error, attemptNumber?: number, maxRetries?: number ) { super(message) this.name = 'BrainyError' this.type = type this.retryable = retryable this.originalError = originalError this.attemptNumber = attemptNumber this.maxRetries = maxRetries // Maintain proper stack trace for where our error was thrown (only available on V8) if (Error.captureStackTrace) { Error.captureStackTrace(this, BrainyError) } } /** * Create a timeout error */ static timeout(operation: string, timeoutMs: number, originalError?: Error): BrainyError { return new BrainyError( `Operation '${operation}' timed out after ${timeoutMs}ms`, 'TIMEOUT', true, originalError ) } /** * Create a network error */ static network(message: string, originalError?: Error): BrainyError { return new BrainyError( `Network error: ${message}`, 'NETWORK', true, originalError ) } /** * Create a storage error */ static storage(message: string, originalError?: Error): BrainyError { return new BrainyError( `Storage error: ${message}`, 'STORAGE', true, originalError ) } /** * Create a not found error */ static notFound(resource: string): BrainyError { return new BrainyError( `Resource not found: ${resource}`, 'NOT_FOUND', false ) } /** * Create a retry exhausted error */ static retryExhausted(operation: string, maxRetries: number, lastError?: Error): BrainyError { return new BrainyError( `Operation '${operation}' failed after ${maxRetries} retry attempts`, 'RETRY_EXHAUSTED', false, lastError, maxRetries, maxRetries ) } /** * Create a "field is not indexed" error. Thrown by metadata-index reads * when a `where` clause names a field that has neither a column-store * entry nor a sparse-index entry. Callers in `find()` evaluation catch * this, translate the offending clause to an empty result, and log so * the silent-empty behavior is replaced with a loud one. Use * `brain.explain({ where: {...} })` to discover this before running. */ static fieldNotIndexed(field: string): BrainyError { return new BrainyError( `Field "${field}" is not indexed. find()/where will not match any entities. ` + `Likely causes: (1) the writer registered the field in memory but has not flushed; ` + `(2) the field name is mistyped; (3) no entity has ever held this field. ` + `Run brain.explain({ where: { ${field}: ... } }) for the diagnostic.`, 'FIELD_NOT_INDEXED', false ) } /** * Create a validation error */ static validation(parameter: string, constraint: string, value?: any): BrainyError { return new BrainyError( `Invalid ${parameter}: ${constraint}`, 'VALIDATION', false ) } /** * Check if an error is retryable */ static isRetryable(error: Error): boolean { if (error instanceof BrainyError) { return error.retryable } // Check for common retryable error patterns const message = error.message.toLowerCase() const name = error.name.toLowerCase() // Network-related errors that are typically retryable if ( message.includes('timeout') || message.includes('network') || message.includes('connection') || message.includes('econnreset') || message.includes('enotfound') || message.includes('etimedout') || name.includes('timeout') ) { return true } // AWS SDK specific retryable errors if ( message.includes('throttling') || message.includes('rate limit') || message.includes('service unavailable') || message.includes('internal server error') || message.includes('bad gateway') || message.includes('gateway timeout') ) { return true } return false } /** * Convert a generic error to a BrainyError with appropriate classification */ static fromError(error: Error, operation?: string): BrainyError { if (error instanceof BrainyError) { return error } const message = error.message.toLowerCase() const name = error.name.toLowerCase() // Classify the error based on common patterns if (message.includes('timeout') || name.includes('timeout')) { return BrainyError.timeout(operation || 'unknown', 0, error) } if ( message.includes('network') || message.includes('connection') || message.includes('econnreset') || message.includes('enotfound') || message.includes('etimedout') ) { return BrainyError.network(error.message, error) } if ( message.includes('nosuchkey') || message.includes('not found') || message.includes('does not exist') ) { return BrainyError.notFound(operation || 'resource') } if ( message.includes('invalid') || message.includes('validation') || message.includes('cannot be null') || message.includes('must be') ) { return new BrainyError(error.message, 'VALIDATION', false, error) } // Default to storage error for unclassified errors return BrainyError.storage(error.message, error) } }