/** * CDC Error Types - Rich error types with actionable messages * * Provides specific error classes for different CDC failure scenarios, * with error codes, recovery suggestions, and debugging context. */ /** * CDC Error codes for programmatic handling */ export enum CDCErrorCode { // Connection errors (1xxx) CONNECTION_FAILED = 'CDC_1001', CONNECTION_TIMEOUT = 'CDC_1002', CONNECTION_LOST = 'CDC_1003', AUTHENTICATION_FAILED = 'CDC_1004', SSL_ERROR = 'CDC_1005', // Subscription errors (2xxx) SUBSCRIPTION_FAILED = 'CDC_2001', SUBSCRIPTION_NOT_FOUND = 'CDC_2002', INVALID_TABLE_NAME = 'CDC_2003', TABLE_NOT_FOUND = 'CDC_2004', INSUFFICIENT_PERMISSIONS = 'CDC_2005', REPLICATION_SLOT_ERROR = 'CDC_2006', // Protocol errors (3xxx) INVALID_MESSAGE = 'CDC_3001', PARSE_ERROR = 'CDC_3002', PROTOCOL_VERSION_MISMATCH = 'CDC_3003', // State errors (4xxx) INVALID_STATE_TRANSITION = 'CDC_4001', SUBSCRIPTION_ALREADY_ACTIVE = 'CDC_4002', SUBSCRIPTION_ALREADY_CLOSED = 'CDC_4003', // Circuit breaker errors (5xxx) CIRCUIT_OPEN = 'CDC_5001', MAX_RETRIES_EXCEEDED = 'CDC_5002', BACKPRESSURE = 'CDC_5003', // Resource errors (6xxx) MEMORY_LIMIT_EXCEEDED = 'CDC_6001', BUFFER_OVERFLOW = 'CDC_6002', TOO_MANY_SUBSCRIPTIONS = 'CDC_6003', // Unknown error UNKNOWN = 'CDC_9999', } /** * Base CDC error class with rich context */ export class CDCError extends Error { /** Unique error code for programmatic handling */ readonly code: CDCErrorCode /** Timestamp when error occurred */ readonly timestamp: Date /** Subscription ID if applicable */ readonly subscriptionId?: string | undefined /** Additional context for debugging */ readonly context: Record /** Human-readable recovery suggestion */ readonly suggestion?: string | undefined /** Whether this error is retryable */ readonly retryable: boolean /** Original error if this wraps another error */ override readonly cause?: Error | undefined constructor( message: string, options: { code: CDCErrorCode subscriptionId?: string | undefined context?: Record | undefined suggestion?: string | undefined retryable?: boolean | undefined cause?: Error | undefined } ) { super(message) this.name = 'CDCError' this.code = options.code this.timestamp = new Date() if (options.subscriptionId !== undefined) { this.subscriptionId = options.subscriptionId } this.context = options.context ?? {} if (options.suggestion !== undefined) { this.suggestion = options.suggestion } this.retryable = options.retryable ?? false if (options.cause !== undefined) { this.cause = options.cause } // Capture stack trace Error.captureStackTrace?.(this, CDCError) } /** * Create a human-readable string representation */ override toString(): string { const parts = [`[${this.code}] ${this.message}`] if (this.subscriptionId) { parts.push(`Subscription: ${this.subscriptionId}`) } if (this.suggestion) { parts.push(`Suggestion: ${this.suggestion}`) } return parts.join('\n') } /** * Convert to JSON for logging */ toJSON(): Record { return { name: this.name, code: this.code, message: this.message, timestamp: this.timestamp.toISOString(), subscriptionId: this.subscriptionId, context: this.context, suggestion: this.suggestion, retryable: this.retryable, stack: this.stack, } } } /** * Connection-related errors */ export class CDCConnectionError extends CDCError { constructor( message: string, options: Omit[1], 'code'> & { code?: CDCErrorCode | undefined } = {} ) { super(message, { ...options, code: options.code ?? CDCErrorCode.CONNECTION_FAILED, retryable: options.retryable ?? true, suggestion: options.suggestion ?? 'Check network connectivity and server availability. The client will automatically retry.', }) this.name = 'CDCConnectionError' } } /** * Connection timeout error */ export class CDCTimeoutError extends CDCConnectionError { /** Timeout duration in milliseconds */ readonly timeoutMs: number constructor( timeoutMs: number, options: { subscriptionId?: string context?: Record suggestion?: string retryable?: boolean cause?: Error } = {} ) { const baseContext = options.context ?? {} super(`Connection timed out after ${timeoutMs}ms`, { subscriptionId: options.subscriptionId, retryable: options.retryable, cause: options.cause, code: CDCErrorCode.CONNECTION_TIMEOUT, context: { ...baseContext, timeoutMs }, suggestion: options.suggestion ?? 'Increase the connection timeout or check for network latency issues.', }) this.name = 'CDCTimeoutError' this.timeoutMs = timeoutMs } } /** * Subscription-related errors */ export class CDCSubscriptionError extends CDCError { /** Table name if applicable */ readonly table?: string | undefined /** Schema name if applicable */ readonly schema?: string | undefined constructor( message: string, options: Omit[1], 'code'> & { code?: CDCErrorCode | undefined table?: string | undefined schema?: string | undefined } = {} ) { super(message, { ...options, code: options.code ?? CDCErrorCode.SUBSCRIPTION_FAILED, context: { ...options.context, table: options.table, schema: options.schema, }, }) this.name = 'CDCSubscriptionError' if (options.table !== undefined) { this.table = options.table } if (options.schema !== undefined) { this.schema = options.schema } } } /** * Invalid table name error */ export class CDCInvalidTableError extends CDCSubscriptionError { constructor( tableName: string, reason: string, options: { subscriptionId?: string context?: Record suggestion?: string retryable?: boolean cause?: Error table?: string schema?: string } = {} ) { const baseContext = options.context ?? {} super(`Invalid table name "${tableName}": ${reason}`, { subscriptionId: options.subscriptionId, cause: options.cause, table: options.table, schema: options.schema, code: CDCErrorCode.INVALID_TABLE_NAME, retryable: false, suggestion: 'Table names must start with a letter or underscore, contain only alphanumeric characters and underscores, and must not contain SQL injection patterns.', context: { ...baseContext, tableName, reason }, }) this.name = 'CDCInvalidTableError' } } /** * Circuit breaker error - when circuit is open */ export class CDCCircuitOpenError extends CDCError { /** When the circuit will reset */ readonly resetAt: Date /** Number of failures that caused the circuit to open */ readonly failureCount: number constructor( resetAt: Date, failureCount: number, options: { subscriptionId?: string context?: Record suggestion?: string retryable?: boolean cause?: Error } = {} ) { const resetInMs = resetAt.getTime() - Date.now() const baseContext = options.context ?? {} super( `Circuit breaker is open. Too many failures (${failureCount}). Will retry in ${Math.ceil(resetInMs / 1000)}s.`, { subscriptionId: options.subscriptionId, cause: options.cause, code: CDCErrorCode.CIRCUIT_OPEN, retryable: false, suggestion: 'Wait for the circuit breaker to reset before retrying. The connection has experienced too many failures.', context: { ...baseContext, resetAt: resetAt.toISOString(), failureCount, resetInMs, }, } ) this.name = 'CDCCircuitOpenError' this.resetAt = resetAt this.failureCount = failureCount } } /** * State machine transition error */ export class CDCStateError extends CDCError { /** Current state */ readonly currentState: string /** Attempted target state */ readonly targetState: string constructor( currentState: string, targetState: string, options: Omit[1], 'code'> = {} ) { super(`Invalid state transition from "${currentState}" to "${targetState}"`, { ...options, code: CDCErrorCode.INVALID_STATE_TRANSITION, retryable: false, context: { ...options.context, currentState, targetState }, }) this.name = 'CDCStateError' this.currentState = currentState this.targetState = targetState } } /** * Memory/resource limit error */ export class CDCResourceError extends CDCError { /** Type of resource that hit the limit */ readonly resourceType: string /** Current usage */ readonly current: number /** Maximum allowed */ readonly limit: number constructor( resourceType: string, current: number, limit: number, options: Omit[1], 'code'> & { code?: CDCErrorCode | undefined } = {} ) { super(`${resourceType} limit exceeded: ${current}/${limit}`, { ...options, code: options.code ?? CDCErrorCode.MEMORY_LIMIT_EXCEEDED, retryable: false, suggestion: 'Consider reducing batch size, closing unused subscriptions, or increasing resource limits.', context: { ...options.context, resourceType, current, limit, utilizationPercent: Math.round((current / limit) * 100), }, }) this.name = 'CDCResourceError' this.resourceType = resourceType this.current = current this.limit = limit } } /** * Parse error for malformed CDC messages */ export class CDCParseError extends CDCError { /** Raw data that failed to parse */ readonly rawData?: string | undefined constructor( message: string, rawData?: string, options: Omit[1], 'code'> = {} ) { super(message, { ...options, code: CDCErrorCode.PARSE_ERROR, retryable: false, suggestion: 'This indicates a protocol issue. Check for server/client version mismatch.', context: { ...options.context, rawDataPreview: rawData?.substring(0, 200), }, }) this.name = 'CDCParseError' if (rawData !== undefined) { this.rawData = rawData } } } /** * Factory function to create appropriate error from generic error */ export function wrapError(error: unknown, subscriptionId?: string): CDCError { if (error instanceof CDCError) { return error } if (error instanceof Error) { // Try to infer error type from message const message = error.message.toLowerCase() if (message.includes('timeout')) { return new CDCTimeoutError(0, { ...(subscriptionId !== undefined && { subscriptionId }), cause: error, }) } if (message.includes('connection') || message.includes('network')) { return new CDCConnectionError(error.message, { ...(subscriptionId !== undefined && { subscriptionId }), cause: error, }) } if (message.includes('parse') || message.includes('json')) { return new CDCParseError(error.message, undefined, { ...(subscriptionId !== undefined && { subscriptionId }), cause: error, }) } // Generic error return new CDCError(error.message, { code: CDCErrorCode.UNKNOWN, ...(subscriptionId !== undefined && { subscriptionId }), cause: error, retryable: true, }) } // Unknown error type return new CDCError(String(error), { code: CDCErrorCode.UNKNOWN, ...(subscriptionId !== undefined && { subscriptionId }), retryable: true, }) }