import type { ModuleDb } from './platform/types.js' export type ModuleDbClientQuery = ( sql: string, params?: unknown[], ) => Promise<{ rows: unknown[] }> export interface ModuleDbTxConnection { query: ModuleDbClientQuery rawQuery: ModuleDbClientQuery } export interface CreateModuleDbOptions { query: ModuleDbClientQuery withClient: (fn: (conn: ModuleDbTxConnection) => Promise) => Promise /** 默认 30_000;非法值(≤0/NaN/Infinity)回落默认,无"关闭"语义 */ defaultTimeoutMs?: number /** 模块可请求的 statement_timeout 上限,默认 120_000;同时用作事务空转超时 */ maxTimeoutMs?: number } const DEFAULT_TIMEOUT_MS = 30_000 const MAX_TIMEOUT_MS = 120_000 const MIN_TIMEOUT_MS = 1_000 function normalizeTimeout(value: number | undefined): number | null { if (value === undefined || !Number.isFinite(value) || value <= 0) return null return Math.floor(value) } /** * 钳制超时:模块传入值收敛到 [1s, max],非法值回落默认。 * 不提供关闭超时的途径(安全优先,防止长事务占满共享连接池)。 */ function resolveTimeouts( options: Pick, txOptions?: { timeoutMs?: number }, ): { statementTimeoutMs: number; idleTimeoutMs: number } { const max = normalizeTimeout(options.maxTimeoutMs) ?? MAX_TIMEOUT_MS const fallback = Math.min(normalizeTimeout(options.defaultTimeoutMs) ?? DEFAULT_TIMEOUT_MS, max) const requested = normalizeTimeout(txOptions?.timeoutMs) ?? fallback return { statementTimeoutMs: Math.min(Math.max(requested, MIN_TIMEOUT_MS), max), idleTimeoutMs: max, } } export function createModuleDb(options: CreateModuleDbOptions): ModuleDb { const db: ModuleDb = { query: options.query, async transaction(fn, txOptions) { return options.withClient(async ({ query, rawQuery }) => { await rawQuery('BEGIN') const { statementTimeoutMs, idleTimeoutMs } = resolveTimeouts(options, txOptions) // SET LOCAL 必须在事务内;不支持参数绑定,内联毫秒整数。 // statement_timeout 限单条 SQL;idle_in_transaction_session_timeout // 兜底回调空转(statement_timeout 管不住 SQL 之间的 await)。 await rawQuery(`SET LOCAL statement_timeout = ${statementTimeoutMs}`) await rawQuery(`SET LOCAL idle_in_transaction_session_timeout = ${idleTimeoutMs}`) const tx: ModuleDb = { query, transaction: async () => { throw new Error('Nested transactions are not supported') }, } try { const result = await fn(tx) await rawQuery('COMMIT') return result } catch (err) { try { await rawQuery('ROLLBACK') } catch { // 忽略 rollback 二次错误,保留原始 err } throw err } }) }, } return db }