/** * PostgreSQL connection pool singleton. * All database access flows through this pool. * * Pool configuration env vars (task 2.1 / 2.2): * DB_POOL_MAX — maximum connections (default 20) * DB_POOL_MIN — minimum connections (default 2) * DB_POOL_IDLE_TIMEOUT_MS — idle connection timeout in ms (default 30000) * DB_POOL_CONNECTION_TIMEOUT_MS — connection acquisition timeout in ms (default 5000) */ import { Pool } from 'pg'; import { dbQueryDurationSeconds, dbPoolActiveConnections, dbPoolWaitingRequests, } from '../metrics/registry.js'; let pool: Pool | null = null; /** * Returns the singleton pg Pool instance. * Initialises the pool on first call using DATABASE_URL and optional pool * tuning env vars. * * Prometheus gauges `agentidp_db_pool_active_connections` and * `agentidp_db_pool_waiting_requests` are updated via pool events (task 2.3). * * @returns The PostgreSQL connection pool. * @throws Error if DATABASE_URL is not set. */ export function getPool(): Pool { if (!pool) { const connectionString = process.env['DATABASE_URL']; if (!connectionString) { throw new Error('DATABASE_URL environment variable is required'); } const max = parseInt(process.env['DB_POOL_MAX'] ?? '20', 10); const min = parseInt(process.env['DB_POOL_MIN'] ?? '2', 10); const idleTimeoutMillis = parseInt(process.env['DB_POOL_IDLE_TIMEOUT_MS'] ?? '30000', 10); const connectionTimeoutMillis = parseInt( process.env['DB_POOL_CONNECTION_TIMEOUT_MS'] ?? '5000', 10, ); pool = new Pool({ connectionString, max, min, idleTimeoutMillis, connectionTimeoutMillis, }); pool.on('error', (err: Error) => { // eslint-disable-next-line no-console console.error('Unexpected pg pool error', err); }); // Track active connections and waiting requests via pool events (task 2.3). pool.on('acquire', () => { if (pool) { dbPoolActiveConnections.set(pool.totalCount - pool.idleCount); dbPoolWaitingRequests.set(pool.waitingCount); } }); pool.on('remove', () => { if (pool) { dbPoolActiveConnections.set(pool.totalCount - pool.idleCount); dbPoolWaitingRequests.set(pool.waitingCount); } }); pool.on('connect', () => { if (pool) { dbPoolActiveConnections.set(pool.totalCount - pool.idleCount); dbPoolWaitingRequests.set(pool.waitingCount); } }); // Wrap pool.query to record duration in Prometheus. // The pg Pool.query method is heavily overloaded — the only safe approach // without TypeScript errors is a typed-any wrapper on the shim itself. // We capture originalQuery as `(...args: any[]) => Promise` to satisfy // TypeScript's spread-into-rest constraint; this is the one sanctioned use of // `any` in this file. // eslint-disable-next-line @typescript-eslint/no-explicit-any const originalQuery = pool.query.bind(pool) as (...args: any[]) => Promise; // eslint-disable-next-line @typescript-eslint/no-explicit-any (pool as any).query = async (...args: any[]): Promise => { const end = dbQueryDurationSeconds.startTimer({ operation: 'query' }); try { return await originalQuery(...args); } finally { end(); } }; } return pool; } /** * Closes the pool and resets the singleton. * Used for graceful shutdown and tests. * * @returns Promise that resolves when the pool is closed. */ export async function closePool(): Promise { if (pool) { await pool.end(); pool = null; } }