singulio-postgres 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +866 -0
- package/dist/client.d.ts +52 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +127 -0
- package/dist/health.d.ts +44 -0
- package/dist/health.d.ts.map +1 -0
- package/dist/health.js +182 -0
- package/dist/index.d.ts +40 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +45 -0
- package/dist/pool.d.ts +52 -0
- package/dist/pool.d.ts.map +1 -0
- package/dist/pool.js +216 -0
- package/dist/rls.d.ts +53 -0
- package/dist/rls.d.ts.map +1 -0
- package/dist/rls.js +134 -0
- package/dist/transaction.d.ts +54 -0
- package/dist/transaction.d.ts.map +1 -0
- package/dist/transaction.js +138 -0
- package/dist/types.d.ts +121 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/vector.d.ts +74 -0
- package/dist/vector.d.ts.map +1 -0
- package/dist/vector.js +223 -0
- package/package.json +97 -0
- package/src/client.ts +153 -0
- package/src/health.ts +226 -0
- package/src/index.ts +110 -0
- package/src/pool.ts +268 -0
- package/src/rls.ts +169 -0
- package/src/transaction.ts +207 -0
- package/src/types.ts +142 -0
- package/src/vector.ts +312 -0
package/src/health.ts
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @singulio/postgres - Health Check Utilities
|
|
3
|
+
* Kubernetes readiness/liveness probes for PostgreSQL
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { sql } from 'bun';
|
|
7
|
+
import type { HealthCheckResult, Logger } from './types.js';
|
|
8
|
+
import type { PostgresPool } from './pool.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Simple health check - verifies database connectivity
|
|
12
|
+
*/
|
|
13
|
+
export async function healthCheck(logger?: Logger): Promise<HealthCheckResult> {
|
|
14
|
+
const start = performance.now();
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
const result = await sql`SELECT 1 as ok`;
|
|
18
|
+
const latencyMs = performance.now() - start;
|
|
19
|
+
|
|
20
|
+
const healthy = result.length > 0 && result[0].ok === 1;
|
|
21
|
+
|
|
22
|
+
logger?.debug('Health check completed', { healthy, latencyMs: latencyMs.toFixed(2) });
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
healthy,
|
|
26
|
+
latencyMs,
|
|
27
|
+
};
|
|
28
|
+
} catch (error) {
|
|
29
|
+
const latencyMs = performance.now() - start;
|
|
30
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
31
|
+
|
|
32
|
+
logger?.error('Health check failed', { error: errorMessage, latencyMs: latencyMs.toFixed(2) });
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
healthy: false,
|
|
36
|
+
latencyMs,
|
|
37
|
+
error: errorMessage,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Extended health check with pool statistics
|
|
44
|
+
*/
|
|
45
|
+
export async function healthCheckWithPool(
|
|
46
|
+
pool: PostgresPool,
|
|
47
|
+
logger?: Logger
|
|
48
|
+
): Promise<HealthCheckResult> {
|
|
49
|
+
const start = performance.now();
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
// Query through pool
|
|
53
|
+
const result = await pool.query<{ ok: number }>('SELECT 1 as ok');
|
|
54
|
+
const latencyMs = performance.now() - start;
|
|
55
|
+
|
|
56
|
+
const healthy = result.rows.length > 0 && result.rows[0].ok === 1;
|
|
57
|
+
const poolStats = pool.stats();
|
|
58
|
+
|
|
59
|
+
logger?.debug('Pool health check completed', {
|
|
60
|
+
healthy,
|
|
61
|
+
latencyMs: latencyMs.toFixed(2),
|
|
62
|
+
...poolStats,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
healthy,
|
|
67
|
+
latencyMs,
|
|
68
|
+
poolStats,
|
|
69
|
+
};
|
|
70
|
+
} catch (error) {
|
|
71
|
+
const latencyMs = performance.now() - start;
|
|
72
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
73
|
+
const poolStats = pool.stats();
|
|
74
|
+
|
|
75
|
+
logger?.error('Pool health check failed', {
|
|
76
|
+
error: errorMessage,
|
|
77
|
+
latencyMs: latencyMs.toFixed(2),
|
|
78
|
+
...poolStats,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
healthy: false,
|
|
83
|
+
latencyMs,
|
|
84
|
+
poolStats,
|
|
85
|
+
error: errorMessage,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Deep health check - verifies database is accepting writes
|
|
92
|
+
*/
|
|
93
|
+
export async function deepHealthCheck(logger?: Logger): Promise<HealthCheckResult> {
|
|
94
|
+
const start = performance.now();
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
// Check we can read and write
|
|
98
|
+
await sql`
|
|
99
|
+
CREATE TEMP TABLE IF NOT EXISTS _health_check (
|
|
100
|
+
id serial PRIMARY KEY,
|
|
101
|
+
ts timestamptz DEFAULT NOW()
|
|
102
|
+
)
|
|
103
|
+
`;
|
|
104
|
+
|
|
105
|
+
await sql`INSERT INTO _health_check DEFAULT VALUES`;
|
|
106
|
+
const result = await sql`SELECT COUNT(*) as count FROM _health_check`;
|
|
107
|
+
await sql`DROP TABLE IF EXISTS _health_check`;
|
|
108
|
+
|
|
109
|
+
const latencyMs = performance.now() - start;
|
|
110
|
+
const healthy = result.length > 0 && (result[0].count as number) > 0;
|
|
111
|
+
|
|
112
|
+
logger?.debug('Deep health check completed', {
|
|
113
|
+
healthy,
|
|
114
|
+
latencyMs: latencyMs.toFixed(2),
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
healthy,
|
|
119
|
+
latencyMs,
|
|
120
|
+
};
|
|
121
|
+
} catch (error) {
|
|
122
|
+
const latencyMs = performance.now() - start;
|
|
123
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
124
|
+
|
|
125
|
+
// Cleanup on error
|
|
126
|
+
try {
|
|
127
|
+
await sql`DROP TABLE IF EXISTS _health_check`;
|
|
128
|
+
} catch {
|
|
129
|
+
// Ignore cleanup errors
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
logger?.error('Deep health check failed', {
|
|
133
|
+
error: errorMessage,
|
|
134
|
+
latencyMs: latencyMs.toFixed(2),
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
healthy: false,
|
|
139
|
+
latencyMs,
|
|
140
|
+
error: errorMessage,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Check PostgreSQL version
|
|
147
|
+
*/
|
|
148
|
+
export async function getVersion(): Promise<string> {
|
|
149
|
+
const result = await sql`SELECT version()`;
|
|
150
|
+
return result[0]?.version ?? 'unknown';
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Check if database is in recovery mode (replica)
|
|
155
|
+
*/
|
|
156
|
+
export async function isInRecovery(): Promise<boolean> {
|
|
157
|
+
const result = await sql`SELECT pg_is_in_recovery() as in_recovery`;
|
|
158
|
+
return result[0]?.in_recovery === true;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Get database size
|
|
163
|
+
*/
|
|
164
|
+
export async function getDatabaseSize(dbName?: string): Promise<string> {
|
|
165
|
+
const result = await sql.unsafe(
|
|
166
|
+
`SELECT pg_size_pretty(pg_database_size(${dbName ? '$1' : 'current_database()'}))::text as size`,
|
|
167
|
+
dbName ? [dbName] : []
|
|
168
|
+
);
|
|
169
|
+
return result[0]?.size ?? 'unknown';
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Get connection count
|
|
174
|
+
*/
|
|
175
|
+
export async function getConnectionCount(): Promise<{
|
|
176
|
+
active: number;
|
|
177
|
+
idle: number;
|
|
178
|
+
total: number;
|
|
179
|
+
maxConnections: number;
|
|
180
|
+
}> {
|
|
181
|
+
const result = await sql`
|
|
182
|
+
SELECT
|
|
183
|
+
COUNT(*) FILTER (WHERE state = 'active') as active,
|
|
184
|
+
COUNT(*) FILTER (WHERE state = 'idle') as idle,
|
|
185
|
+
COUNT(*) as total,
|
|
186
|
+
(SELECT setting::int FROM pg_settings WHERE name = 'max_connections') as max_connections
|
|
187
|
+
FROM pg_stat_activity
|
|
188
|
+
WHERE datname = current_database()
|
|
189
|
+
`;
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
active: Number(result[0]?.active) || 0,
|
|
193
|
+
idle: Number(result[0]?.idle) || 0,
|
|
194
|
+
total: Number(result[0]?.total) || 0,
|
|
195
|
+
maxConnections: Number(result[0]?.max_connections) || 100,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Create an HTTP health check handler for Bun.serve
|
|
201
|
+
*/
|
|
202
|
+
export function createHealthHandler(pool?: PostgresPool) {
|
|
203
|
+
return async (req: Request): Promise<Response> => {
|
|
204
|
+
const url = new URL(req.url);
|
|
205
|
+
|
|
206
|
+
if (url.pathname === '/health' || url.pathname === '/healthz') {
|
|
207
|
+
const result = pool ? await healthCheckWithPool(pool) : await healthCheck();
|
|
208
|
+
|
|
209
|
+
return new Response(JSON.stringify(result), {
|
|
210
|
+
status: result.healthy ? 200 : 503,
|
|
211
|
+
headers: { 'Content-Type': 'application/json' },
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (url.pathname === '/ready' || url.pathname === '/readyz') {
|
|
216
|
+
const result = await deepHealthCheck();
|
|
217
|
+
|
|
218
|
+
return new Response(JSON.stringify(result), {
|
|
219
|
+
status: result.healthy ? 200 : 503,
|
|
220
|
+
headers: { 'Content-Type': 'application/json' },
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return new Response('Not Found', { status: 404 });
|
|
225
|
+
};
|
|
226
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @singulio/postgres
|
|
3
|
+
* PostgreSQL 18.x client for Bun with RLS, pooling, transactions, and pgvector support
|
|
4
|
+
*
|
|
5
|
+
* A thin wrapper around bun:postgres native client providing:
|
|
6
|
+
* - Connection pooling with configurable limits
|
|
7
|
+
* - Row-Level Security (RLS) helpers for multi-tenant isolation
|
|
8
|
+
* - Transaction support with auto-rollback
|
|
9
|
+
* - pgvector extension for AI/ML embeddings
|
|
10
|
+
* - Kubernetes health check utilities
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* import { createClient, createPool, withTenant, vectorSearch } from '@singulio/postgres';
|
|
15
|
+
*
|
|
16
|
+
* // Simple client
|
|
17
|
+
* const client = createClient(process.env.DATABASE_URL);
|
|
18
|
+
* const users = await client.queryAll('SELECT * FROM users');
|
|
19
|
+
*
|
|
20
|
+
* // With RLS tenant context
|
|
21
|
+
* const tenantData = await withTenant({ tenantId: 'tenant-123' }, async () => {
|
|
22
|
+
* return client.queryAll('SELECT * FROM contacts'); // RLS filters by tenant
|
|
23
|
+
* });
|
|
24
|
+
*
|
|
25
|
+
* // Vector similarity search
|
|
26
|
+
* const similar = await vectorSearch('documents', 'embedding', queryVector, {
|
|
27
|
+
* operator: '<=>', // Cosine similarity
|
|
28
|
+
* limit: 10,
|
|
29
|
+
* });
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
// Types
|
|
34
|
+
export type {
|
|
35
|
+
PostgresConfig,
|
|
36
|
+
PoolConfig,
|
|
37
|
+
RLSContext,
|
|
38
|
+
QueryResult,
|
|
39
|
+
QueryRow,
|
|
40
|
+
FieldInfo,
|
|
41
|
+
IsolationLevel,
|
|
42
|
+
TransactionOptions,
|
|
43
|
+
HealthCheckResult,
|
|
44
|
+
PoolStats,
|
|
45
|
+
VectorDistanceOperator,
|
|
46
|
+
VectorIndexType,
|
|
47
|
+
VectorSearchOptions,
|
|
48
|
+
Logger,
|
|
49
|
+
} from './types.js';
|
|
50
|
+
|
|
51
|
+
// Client
|
|
52
|
+
export { PostgresClient, createClient } from './client.js';
|
|
53
|
+
|
|
54
|
+
// Pool
|
|
55
|
+
export { PostgresPool, createPool } from './pool.js';
|
|
56
|
+
|
|
57
|
+
// RLS
|
|
58
|
+
export {
|
|
59
|
+
setRLSContext,
|
|
60
|
+
clearRLSContext,
|
|
61
|
+
getRLSContext,
|
|
62
|
+
withTenant,
|
|
63
|
+
queryWithTenant,
|
|
64
|
+
RLSClient,
|
|
65
|
+
createRLSClient,
|
|
66
|
+
} from './rls.js';
|
|
67
|
+
|
|
68
|
+
// Transactions
|
|
69
|
+
export {
|
|
70
|
+
transaction,
|
|
71
|
+
serializableTransaction,
|
|
72
|
+
readOnlyTransaction,
|
|
73
|
+
savepoint,
|
|
74
|
+
rollbackToSavepoint,
|
|
75
|
+
releaseSavepoint,
|
|
76
|
+
type TransactionQuery,
|
|
77
|
+
} from './transaction.js';
|
|
78
|
+
|
|
79
|
+
// Vector (pgvector)
|
|
80
|
+
export {
|
|
81
|
+
formatVector,
|
|
82
|
+
parseVector,
|
|
83
|
+
vectorDimension,
|
|
84
|
+
normalizeVector,
|
|
85
|
+
getDistanceOperator,
|
|
86
|
+
getDistanceOperatorName,
|
|
87
|
+
ensureVectorExtension,
|
|
88
|
+
createVectorColumn,
|
|
89
|
+
createVectorIndex,
|
|
90
|
+
vectorSearch,
|
|
91
|
+
insertWithVector,
|
|
92
|
+
updateVector,
|
|
93
|
+
batchInsertWithVectors,
|
|
94
|
+
type Vector,
|
|
95
|
+
} from './vector.js';
|
|
96
|
+
|
|
97
|
+
// Health
|
|
98
|
+
export {
|
|
99
|
+
healthCheck,
|
|
100
|
+
healthCheckWithPool,
|
|
101
|
+
deepHealthCheck,
|
|
102
|
+
getVersion,
|
|
103
|
+
isInRecovery,
|
|
104
|
+
getDatabaseSize,
|
|
105
|
+
getConnectionCount,
|
|
106
|
+
createHealthHandler,
|
|
107
|
+
} from './health.js';
|
|
108
|
+
|
|
109
|
+
// Re-export bun:postgres sql for direct usage
|
|
110
|
+
export { sql } from 'bun';
|
package/src/pool.ts
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @singulio/postgres - Connection Pool Manager
|
|
3
|
+
* Manages PostgreSQL connections with configurable pooling
|
|
4
|
+
*
|
|
5
|
+
* Note: Bun's native sql driver handles connection pooling internally.
|
|
6
|
+
* This module provides a higher-level API with explicit pool management
|
|
7
|
+
* for scenarios requiring fine-grained control.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { sql } from 'bun';
|
|
11
|
+
import type { PoolConfig, PoolStats, QueryResult, QueryRow, Logger } from './types.js';
|
|
12
|
+
|
|
13
|
+
interface PooledConnection {
|
|
14
|
+
id: number;
|
|
15
|
+
createdAt: number;
|
|
16
|
+
lastUsedAt: number;
|
|
17
|
+
inUse: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Internal pool config with required pool fields */
|
|
21
|
+
interface InternalPoolConfig extends PoolConfig {
|
|
22
|
+
min: number;
|
|
23
|
+
max: number;
|
|
24
|
+
idleTimeout: number;
|
|
25
|
+
connectionTimeout: number;
|
|
26
|
+
maxLifetime: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Connection pool manager for PostgreSQL
|
|
31
|
+
*/
|
|
32
|
+
export class PostgresPool {
|
|
33
|
+
private config: InternalPoolConfig;
|
|
34
|
+
private logger?: Logger;
|
|
35
|
+
private connections: Map<number, PooledConnection> = new Map();
|
|
36
|
+
private nextId = 0;
|
|
37
|
+
private waitQueue: Array<{
|
|
38
|
+
resolve: (conn: PooledConnection) => void;
|
|
39
|
+
reject: (err: Error) => void;
|
|
40
|
+
timer: Timer;
|
|
41
|
+
}> = [];
|
|
42
|
+
private closed = false;
|
|
43
|
+
private idleCheckTimer?: Timer;
|
|
44
|
+
|
|
45
|
+
constructor(config: PoolConfig, logger?: Logger) {
|
|
46
|
+
this.config = {
|
|
47
|
+
min: 2,
|
|
48
|
+
max: 20,
|
|
49
|
+
idleTimeout: 30000,
|
|
50
|
+
connectionTimeout: 10000,
|
|
51
|
+
maxLifetime: 3600000,
|
|
52
|
+
...config,
|
|
53
|
+
};
|
|
54
|
+
this.logger = logger;
|
|
55
|
+
|
|
56
|
+
// Start idle connection checker
|
|
57
|
+
this.idleCheckTimer = setInterval(() => this.checkIdleConnections(), 10000);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Initialize the pool with minimum connections
|
|
62
|
+
*/
|
|
63
|
+
async initialize(): Promise<void> {
|
|
64
|
+
this.logger?.info('Initializing PostgreSQL pool', {
|
|
65
|
+
min: this.config.min,
|
|
66
|
+
max: this.config.max,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Warm up minimum connections
|
|
70
|
+
for (let i = 0; i < this.config.min; i++) {
|
|
71
|
+
await this.createConnection();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
this.logger?.info('PostgreSQL pool initialized', {
|
|
75
|
+
connections: this.connections.size,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private async createConnection(): Promise<PooledConnection> {
|
|
80
|
+
const conn: PooledConnection = {
|
|
81
|
+
id: this.nextId++,
|
|
82
|
+
createdAt: Date.now(),
|
|
83
|
+
lastUsedAt: Date.now(),
|
|
84
|
+
inUse: false,
|
|
85
|
+
};
|
|
86
|
+
this.connections.set(conn.id, conn);
|
|
87
|
+
return conn;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private async acquireConnection(): Promise<PooledConnection> {
|
|
91
|
+
// Find an idle connection
|
|
92
|
+
for (const conn of this.connections.values()) {
|
|
93
|
+
if (!conn.inUse) {
|
|
94
|
+
// Check if connection is too old
|
|
95
|
+
if (Date.now() - conn.createdAt > this.config.maxLifetime) {
|
|
96
|
+
await this.destroyConnection(conn);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
conn.inUse = true;
|
|
100
|
+
conn.lastUsedAt = Date.now();
|
|
101
|
+
return conn;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Create new connection if under max
|
|
106
|
+
if (this.connections.size < this.config.max) {
|
|
107
|
+
const conn = await this.createConnection();
|
|
108
|
+
conn.inUse = true;
|
|
109
|
+
return conn;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Wait for a connection
|
|
113
|
+
return new Promise((resolve, reject) => {
|
|
114
|
+
const timer = setTimeout(() => {
|
|
115
|
+
const idx = this.waitQueue.findIndex(w => w.resolve === resolve);
|
|
116
|
+
if (idx >= 0) this.waitQueue.splice(idx, 1);
|
|
117
|
+
reject(new Error(`Connection timeout after ${this.config.connectionTimeout}ms`));
|
|
118
|
+
}, this.config.connectionTimeout);
|
|
119
|
+
|
|
120
|
+
this.waitQueue.push({ resolve, reject, timer });
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private releaseConnection(conn: PooledConnection): void {
|
|
125
|
+
conn.inUse = false;
|
|
126
|
+
conn.lastUsedAt = Date.now();
|
|
127
|
+
|
|
128
|
+
// Check waiting queue
|
|
129
|
+
if (this.waitQueue.length > 0) {
|
|
130
|
+
const waiter = this.waitQueue.shift()!;
|
|
131
|
+
clearTimeout(waiter.timer);
|
|
132
|
+
conn.inUse = true;
|
|
133
|
+
waiter.resolve(conn);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private async destroyConnection(conn: PooledConnection): Promise<void> {
|
|
138
|
+
this.connections.delete(conn.id);
|
|
139
|
+
this.logger?.debug('Connection destroyed', { id: conn.id });
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private async checkIdleConnections(): Promise<void> {
|
|
143
|
+
if (this.closed) return;
|
|
144
|
+
|
|
145
|
+
const now = Date.now();
|
|
146
|
+
for (const conn of this.connections.values()) {
|
|
147
|
+
// Don't close if we're at minimum
|
|
148
|
+
if (this.connections.size <= this.config.min) break;
|
|
149
|
+
|
|
150
|
+
if (!conn.inUse && now - conn.lastUsedAt > this.config.idleTimeout) {
|
|
151
|
+
await this.destroyConnection(conn);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Execute a query using a pooled connection
|
|
158
|
+
*/
|
|
159
|
+
async query<T = QueryRow>(queryText: string, params?: unknown[]): Promise<QueryResult<T>> {
|
|
160
|
+
if (this.closed) {
|
|
161
|
+
throw new Error('Pool is closed');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const conn = await this.acquireConnection();
|
|
165
|
+
const start = performance.now();
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
const result = await sql.unsafe(queryText, params ?? []);
|
|
169
|
+
const latency = performance.now() - start;
|
|
170
|
+
|
|
171
|
+
this.logger?.debug('Pool query executed', {
|
|
172
|
+
connId: conn.id,
|
|
173
|
+
query: queryText.substring(0, 100),
|
|
174
|
+
rows: result.length,
|
|
175
|
+
latencyMs: latency.toFixed(2),
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
rows: result as T[],
|
|
180
|
+
rowCount: result.length,
|
|
181
|
+
};
|
|
182
|
+
} finally {
|
|
183
|
+
this.releaseConnection(conn);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Execute with exclusive connection for multi-statement operations
|
|
189
|
+
*/
|
|
190
|
+
async withConnection<T>(
|
|
191
|
+
fn: (query: (sql: string, params?: unknown[]) => Promise<QueryResult>) => Promise<T>
|
|
192
|
+
): Promise<T> {
|
|
193
|
+
if (this.closed) {
|
|
194
|
+
throw new Error('Pool is closed');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const conn = await this.acquireConnection();
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
return await fn(async (queryText: string, params?: unknown[]) => {
|
|
201
|
+
const result = await sql.unsafe(queryText, params ?? []);
|
|
202
|
+
return {
|
|
203
|
+
rows: result,
|
|
204
|
+
rowCount: result.length,
|
|
205
|
+
};
|
|
206
|
+
});
|
|
207
|
+
} finally {
|
|
208
|
+
this.releaseConnection(conn);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Get current pool statistics
|
|
214
|
+
*/
|
|
215
|
+
stats(): PoolStats {
|
|
216
|
+
let idle = 0;
|
|
217
|
+
let active = 0;
|
|
218
|
+
|
|
219
|
+
for (const conn of this.connections.values()) {
|
|
220
|
+
if (conn.inUse) {
|
|
221
|
+
active++;
|
|
222
|
+
} else {
|
|
223
|
+
idle++;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
total: this.connections.size,
|
|
229
|
+
idle,
|
|
230
|
+
active,
|
|
231
|
+
pending: this.waitQueue.length,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Close the pool and all connections
|
|
237
|
+
*/
|
|
238
|
+
async close(): Promise<void> {
|
|
239
|
+
this.closed = true;
|
|
240
|
+
|
|
241
|
+
if (this.idleCheckTimer) {
|
|
242
|
+
clearInterval(this.idleCheckTimer);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Reject all waiting
|
|
246
|
+
for (const waiter of this.waitQueue) {
|
|
247
|
+
clearTimeout(waiter.timer);
|
|
248
|
+
waiter.reject(new Error('Pool is closing'));
|
|
249
|
+
}
|
|
250
|
+
this.waitQueue = [];
|
|
251
|
+
|
|
252
|
+
// Destroy all connections
|
|
253
|
+
for (const conn of this.connections.values()) {
|
|
254
|
+
await this.destroyConnection(conn);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
this.logger?.info('PostgreSQL pool closed');
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Create and initialize a connection pool
|
|
263
|
+
*/
|
|
264
|
+
export async function createPool(config: PoolConfig, logger?: Logger): Promise<PostgresPool> {
|
|
265
|
+
const pool = new PostgresPool(config, logger);
|
|
266
|
+
await pool.initialize();
|
|
267
|
+
return pool;
|
|
268
|
+
}
|