/** Pure error classification for database failures (no server-only import so it is unit-testable). */ /** Thrown when the database itself is unreachable (connection refused, auth, pool exhausted…). */ export class DatabaseUnavailableError extends Error { readonly code: string | undefined; constructor(cause: unknown) { const e = cause as { message?: string; code?: string } | undefined; super(`Database unavailable${e?.code ? ` (${e.code})` : ''}: ${e?.message ?? String(cause)}`); this.name = 'DatabaseUnavailableError'; this.code = e?.code; } } // Node socket errors + postgres.js connection errors + PostgreSQL SQLSTATE classes 08 (connection // exception), 28 (auth), 3D (bad database), 53 (insufficient resources) and 57P (operator intervention). const CONNECTION_CODES = new Set(['ECONNREFUSED', 'ECONNRESET', 'ETIMEDOUT', 'ENOTFOUND', 'EHOSTUNREACH', 'EPIPE', 'EAI_AGAIN', 'CONNECT_TIMEOUT', 'CONNECTION_CLOSED', 'CONNECTION_ENDED', 'CONNECTION_DESTROYED']); const CONNECTION_SQLSTATE = /^(08|28|3D|53|57P)/; const CONNECTION_MESSAGE = /ECONNREFUSED|ECONNRESET|ETIMEDOUT|ENOTFOUND|connection (refused|terminated|closed|ended)|CONNECT_TIMEOUT|too many connections|password authentication failed/i; /** True when the error means "the database is unreachable" rather than "this query/table is wrong". */ export function isConnectionError(err: unknown, depth = 0): boolean { if (!err || typeof err !== 'object' || depth > 4) return false; const e = err as { code?: unknown; errno?: unknown; message?: unknown; cause?: unknown }; const code = typeof e.code === 'string' ? e.code : typeof e.errno === 'string' ? e.errno : ''; if (code && (CONNECTION_CODES.has(code) || CONNECTION_SQLSTATE.test(code))) return true; if (typeof e.message === 'string' && CONNECTION_MESSAGE.test(e.message)) return true; return e.cause != null && e.cause !== err ? isConnectionError(e.cause, depth + 1) : false; }