From a3179b31eb21bfa7fc4db58ad027dd1bce57aa4d Mon Sep 17 00:00:00 2001 From: Zach Schultz Date: Tue, 13 Jan 2026 17:27:59 -0600 Subject: [PATCH] Add onidle callback option - fixes #1100 --- src/index.js | 3 +- tests/index.js | 101 +++++++++++++++++++++++++++++++++++++++++++++++ types/index.d.ts | 4 ++ 3 files changed, 107 insertions(+), 1 deletion(-) diff --git a/src/index.js b/src/index.js index c7fba3da..657aafc4 100644 --- a/src/index.js +++ b/src/index.js @@ -310,7 +310,7 @@ function Postgres(a, b) { queue.push(c) c.queue = queue queue === open - ? c.idleTimer.start() + ? (c.idleTimer.start(), options.onidle && options.onidle(c.id)) : c.idleTimer.cancel() return c } @@ -492,6 +492,7 @@ function parseOptions(a, b) { onnotify : o.onnotify, onclose : o.onclose, onparameter : o.onparameter, + onidle : o.onidle, socket : o.socket, transform : parseTransform(o.transform || { undefined: undefined }), parameters : {}, diff --git a/tests/index.js b/tests/index.js index 23e6c4d4..c07cb7a2 100644 --- a/tests/index.js +++ b/tests/index.js @@ -959,6 +959,107 @@ t('responds with server parameters (application_name)', async() => })`select 1`.catch(reject))] ) +t('onidle fires for each connection in pool', async() => { + const ids = [] + const sql = postgres({ + ...options, + max: 2, + onidle: (id) => ids.push(id) + }) + + // Run two queries concurrently to use both connections. + await Promise.all([ + sql`select pg_sleep(0.05)`, + sql`select pg_sleep(0.05)` + ]) + await sql.end() + + // Both connections should have fired onidle. + return [2, ids.length] +}) + +t('onidle not called when queries are queued', async() => { + let idleCount = 0 + const sql = postgres({ + ...options, + max: 1, + onidle: () => idleCount++ + }) + + // Run two queries concurrently on single connection. + await Promise.all([ + sql`select pg_sleep(0.05)`, + sql`select 1` + ]) + await sql.end() + + // Should only fire once after both queries complete and connection returns to idle. + return [1, idleCount] +}) + +t('onidle receives connection id', async() => { + const ids = [] + const sql = postgres({ + ...options, + max: 2, + onidle: (id) => ids.push(id) + }) + + // Use two connections. + await Promise.all([ + sql`select pg_sleep(0.05)`, + sql`select pg_sleep(0.05)` + ]) + await sql.end() + + // Should receive valid integer ids, and they should be different. + return [true, ids.length === 2 && ids.every(Number.isInteger) && ids[0] !== ids[1]] +}) + +t('onidle can be set after creation', async() => { + let called = false + const sql = postgres({ ...options, max: 1 }) + + sql.options.onidle = () => called = true + + await sql`select 1` + await sql.end() + + return [true, called] +}) + +t('onidle fires after reserved connection is released', async() => { + let idleCount = 0 + const sql = postgres({ + ...options, + max: 1, + onidle: () => idleCount++ + }) + + const reserved = await sql.reserve() + await reserved`select 1` + reserved.release() + await sql.end() + + return [1, idleCount] +}) + +t('onidle fires after transaction completes', async() => { + let idleCount = 0 + const sql = postgres({ + ...options, + max: 1, + onidle: () => idleCount++ + }) + + await sql.begin(async sql => { + await sql`select 1` + }) + await sql.end() + + return [1, idleCount] +}) + t('has server parameters', async() => { return ['postgres.js', (await sql`select 1`.then(() => sql.parameters.application_name))] }) diff --git a/types/index.d.ts b/types/index.d.ts index 4b796799..14a3b769 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -119,6 +119,10 @@ interface BaseOptions> { */ publications: string onclose: (connId: number) => void; + /** + * Called when a connection becomes idle and available for new queries + */ + onidle: (connId: number) => void; backoff: boolean | ((attemptNum: number) => number); max_lifetime: number | null; keep_alive: number | null;