@agentuity/postgres 0.1.41

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.
Files changed (48) hide show
  1. package/AGENTS.md +124 -0
  2. package/README.md +297 -0
  3. package/dist/client.d.ts +224 -0
  4. package/dist/client.d.ts.map +1 -0
  5. package/dist/client.js +670 -0
  6. package/dist/client.js.map +1 -0
  7. package/dist/errors.d.ts +109 -0
  8. package/dist/errors.d.ts.map +1 -0
  9. package/dist/errors.js +115 -0
  10. package/dist/errors.js.map +1 -0
  11. package/dist/index.d.ts +41 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +47 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/patch.d.ts +65 -0
  16. package/dist/patch.d.ts.map +1 -0
  17. package/dist/patch.js +111 -0
  18. package/dist/patch.js.map +1 -0
  19. package/dist/postgres.d.ts +62 -0
  20. package/dist/postgres.d.ts.map +1 -0
  21. package/dist/postgres.js +63 -0
  22. package/dist/postgres.js.map +1 -0
  23. package/dist/reconnect.d.ts +31 -0
  24. package/dist/reconnect.d.ts.map +1 -0
  25. package/dist/reconnect.js +60 -0
  26. package/dist/reconnect.js.map +1 -0
  27. package/dist/registry.d.ts +71 -0
  28. package/dist/registry.d.ts.map +1 -0
  29. package/dist/registry.js +175 -0
  30. package/dist/registry.js.map +1 -0
  31. package/dist/transaction.d.ts +147 -0
  32. package/dist/transaction.d.ts.map +1 -0
  33. package/dist/transaction.js +287 -0
  34. package/dist/transaction.js.map +1 -0
  35. package/dist/types.d.ts +213 -0
  36. package/dist/types.d.ts.map +1 -0
  37. package/dist/types.js +2 -0
  38. package/dist/types.js.map +1 -0
  39. package/package.json +55 -0
  40. package/src/client.ts +776 -0
  41. package/src/errors.ts +154 -0
  42. package/src/index.ts +71 -0
  43. package/src/patch.ts +123 -0
  44. package/src/postgres.ts +65 -0
  45. package/src/reconnect.ts +74 -0
  46. package/src/registry.ts +194 -0
  47. package/src/transaction.ts +312 -0
  48. package/src/types.ts +250 -0
package/src/client.ts ADDED
@@ -0,0 +1,776 @@
1
+ import { SQL as BunSQL, type SQLQuery, type SQL } from 'bun';
2
+ import type { PostgresConfig, ConnectionStats, TransactionOptions, ReserveOptions } from './types';
3
+ import {
4
+ ConnectionClosedError,
5
+ PostgresError,
6
+ ReconnectFailedError,
7
+ QueryTimeoutError,
8
+ UnsupportedOperationError,
9
+ isRetryableError,
10
+ } from './errors';
11
+ import { computeBackoff, sleep, mergeReconnectConfig } from './reconnect';
12
+ import { Transaction, ReservedConnection } from './transaction';
13
+ import { registerClient, unregisterClient } from './registry';
14
+
15
+ /**
16
+ * Bun SQL options for PostgreSQL connections.
17
+ * We use a type assertion since the Bun types are a union of SQLite and Postgres options.
18
+ */
19
+ type BunPostgresOptions = SQL.PostgresOrMySQLOptions;
20
+
21
+ /**
22
+ * A resilient PostgreSQL client with automatic reconnection.
23
+ *
24
+ * Wraps Bun's native SQL driver and adds:
25
+ * - Automatic reconnection with exponential backoff
26
+ * - Connection state tracking
27
+ * - Transaction support
28
+ * - Reserved connection support
29
+ *
30
+ * Can be used as a tagged template literal for queries:
31
+ *
32
+ * @example
33
+ * ```typescript
34
+ * const client = new PostgresClient();
35
+ *
36
+ * // Simple query
37
+ * const users = await client`SELECT * FROM users`;
38
+ *
39
+ * // Parameterized query
40
+ * const user = await client`SELECT * FROM users WHERE id = ${userId}`;
41
+ *
42
+ * // Transaction
43
+ * const tx = await client.begin();
44
+ * await tx`INSERT INTO users (name) VALUES (${name})`;
45
+ * await tx.commit();
46
+ * ```
47
+ */
48
+ export class PostgresClient {
49
+ private _sql: InstanceType<typeof BunSQL> | null = null;
50
+ private _config: PostgresConfig;
51
+ private _initialized = false; // SQL client created (lazy connection)
52
+ private _connected = false; // Actual TCP connection verified
53
+ private _reconnecting = false;
54
+ private _closed = false;
55
+ private _shuttingDown = false;
56
+ private _signalHandlers: { signal: string; handler: () => void }[] = [];
57
+ private _reconnectPromise: Promise<void> | null = null;
58
+ private _connectPromise: Promise<void> | null = null;
59
+
60
+ private _stats: ConnectionStats = {
61
+ connected: false,
62
+ reconnecting: false,
63
+ totalConnections: 0,
64
+ reconnectAttempts: 0,
65
+ failedReconnects: 0,
66
+ lastConnectedAt: null,
67
+ lastDisconnectedAt: null,
68
+ lastReconnectAttemptAt: null,
69
+ };
70
+
71
+ /**
72
+ * Creates a new PostgresClient.
73
+ *
74
+ * Note: By default, the actual TCP connection is established lazily on first query.
75
+ * The `connected` property will be `false` until a query is executed or
76
+ * `waitForConnection()` is called. Set `preconnect: true` in config to
77
+ * establish the connection immediately.
78
+ *
79
+ * @param config - Connection configuration. Can be a connection URL string or a config object.
80
+ * If not provided, uses `process.env.DATABASE_URL`.
81
+ */
82
+ constructor(config?: string | PostgresConfig) {
83
+ if (typeof config === 'string') {
84
+ this._config = { url: config };
85
+ } else {
86
+ this._config = config ?? {};
87
+ }
88
+
89
+ // Initialize the SQL client (lazy - doesn't establish TCP connection yet)
90
+ this._initializeSql();
91
+
92
+ // Register shutdown signal handlers to prevent reconnection during app shutdown
93
+ this._registerShutdownHandlers();
94
+
95
+ // Register this client in the global registry for coordinated shutdown
96
+ registerClient(this);
97
+
98
+ // If preconnect is enabled, establish connection immediately
99
+ if (this._config.preconnect) {
100
+ const p = this._warmConnection();
101
+ // Attach no-op catch to suppress unhandled rejection warnings
102
+ // Later awaits will still observe the real rejection
103
+ p.catch(() => {});
104
+ this._connectPromise = p;
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Whether the client is currently connected.
110
+ */
111
+ get connected(): boolean {
112
+ return this._connected;
113
+ }
114
+
115
+ /**
116
+ * Whether the client is shutting down (won't attempt reconnection).
117
+ */
118
+ get shuttingDown(): boolean {
119
+ return this._shuttingDown;
120
+ }
121
+
122
+ /**
123
+ * Whether a reconnection attempt is in progress.
124
+ */
125
+ get reconnecting(): boolean {
126
+ return this._reconnecting;
127
+ }
128
+
129
+ /**
130
+ * Connection statistics.
131
+ */
132
+ get stats(): Readonly<ConnectionStats> {
133
+ return {
134
+ ...this._stats,
135
+ connected: this._connected,
136
+ reconnecting: this._reconnecting,
137
+ };
138
+ }
139
+
140
+ /**
141
+ * Execute a query using tagged template literal syntax.
142
+ * If reconnection is in progress, waits for it to complete before executing.
143
+ * Automatically retries on retryable errors.
144
+ *
145
+ * @example
146
+ * ```typescript
147
+ * const users = await client`SELECT * FROM users WHERE active = ${true}`;
148
+ * ```
149
+ */
150
+ query(strings: TemplateStringsArray, ...values: unknown[]): Promise<unknown[]> {
151
+ return this._executeWithRetry(async () => {
152
+ const sql = await this._ensureConnectedAsync();
153
+ return sql(strings, ...values);
154
+ });
155
+ }
156
+
157
+ /**
158
+ * Begin a new transaction.
159
+ *
160
+ * @param options - Transaction options (isolation level, read-only, deferrable)
161
+ * @returns A Transaction object for executing queries within the transaction
162
+ *
163
+ * @example
164
+ * ```typescript
165
+ * const tx = await client.begin();
166
+ * try {
167
+ * await tx`INSERT INTO users (name) VALUES (${name})`;
168
+ * await tx`UPDATE accounts SET balance = balance - ${amount} WHERE id = ${fromId}`;
169
+ * await tx.commit();
170
+ * } catch (error) {
171
+ * await tx.rollback();
172
+ * throw error;
173
+ * }
174
+ * ```
175
+ */
176
+ async begin(options?: TransactionOptions): Promise<Transaction> {
177
+ // Use async ensure to wait for connection/reconnect completion
178
+ // This ensures _warmConnection() updates connection stats before we proceed
179
+ const sql = await this._ensureConnectedAsync();
180
+
181
+ // Build BEGIN statement with options
182
+ let beginStatement = 'BEGIN';
183
+
184
+ if (options?.isolationLevel) {
185
+ beginStatement += ` ISOLATION LEVEL ${options.isolationLevel.toUpperCase()}`;
186
+ }
187
+
188
+ if (options?.readOnly) {
189
+ beginStatement += ' READ ONLY';
190
+ } else if (options?.readOnly === false) {
191
+ beginStatement += ' READ WRITE';
192
+ }
193
+
194
+ if (options?.deferrable === true) {
195
+ beginStatement += ' DEFERRABLE';
196
+ } else if (options?.deferrable === false) {
197
+ beginStatement += ' NOT DEFERRABLE';
198
+ }
199
+
200
+ // Execute BEGIN
201
+ const connection = await sql.unsafe(beginStatement);
202
+
203
+ return new Transaction(sql, connection);
204
+ }
205
+
206
+ /**
207
+ * Reserve an exclusive connection from the pool.
208
+ *
209
+ * **Note:** This feature is not currently supported because Bun's SQL driver
210
+ * does not expose connection-level pooling APIs. The underlying driver manages
211
+ * connections internally and does not allow reserving a specific connection.
212
+ *
213
+ * @param _options - Reserve options (unused)
214
+ * @throws {UnsupportedOperationError} Always throws - this operation is not supported
215
+ *
216
+ * @example
217
+ * ```typescript
218
+ * // This will throw UnsupportedOperationError
219
+ * const conn = await client.reserve();
220
+ * ```
221
+ */
222
+ async reserve(_options?: ReserveOptions): Promise<ReservedConnection> {
223
+ throw new UnsupportedOperationError({
224
+ operation: 'reserve',
225
+ reason:
226
+ "Bun's SQL driver does not expose connection-level pooling APIs. " +
227
+ 'Use transactions (begin/commit) for operations that require session-level state.',
228
+ });
229
+ }
230
+
231
+ /**
232
+ * Signal that the application is shutting down.
233
+ * This prevents reconnection attempts but doesn't immediately close the connection.
234
+ * Use this when you want to gracefully drain connections before calling close().
235
+ */
236
+ shutdown(): void {
237
+ this._shuttingDown = true;
238
+ }
239
+
240
+ /**
241
+ * Close the client and release all connections.
242
+ */
243
+ async close(): Promise<void> {
244
+ this._closed = true;
245
+ this._shuttingDown = true; // Also set shuttingDown to prevent any race conditions
246
+ this._connected = false;
247
+ this._reconnecting = false;
248
+
249
+ // Remove signal handlers
250
+ this._removeShutdownHandlers();
251
+
252
+ // Unregister from global registry
253
+ unregisterClient(this);
254
+
255
+ if (this._sql) {
256
+ await this._sql.close();
257
+ this._sql = null;
258
+ }
259
+ }
260
+
261
+ /**
262
+ * Access to raw SQL methods for advanced use cases.
263
+ * Returns the underlying Bun.SQL instance.
264
+ */
265
+ get raw(): InstanceType<typeof BunSQL> {
266
+ return this._ensureConnected();
267
+ }
268
+
269
+ /**
270
+ * Execute an unsafe (unparameterized) query.
271
+ * Use with caution - this bypasses SQL injection protection.
272
+ *
273
+ * @param query - The raw SQL query string
274
+ */
275
+ unsafe(query: string): SQLQuery {
276
+ const sql = this._ensureConnected();
277
+ return sql.unsafe(query);
278
+ }
279
+
280
+ /**
281
+ * Registers signal handlers to detect application shutdown.
282
+ * When shutdown is detected, reconnection is disabled.
283
+ */
284
+ private _registerShutdownHandlers(): void {
285
+ const shutdownHandler = () => {
286
+ this._shuttingDown = true;
287
+ };
288
+
289
+ // Listen for common shutdown signals
290
+ const signals = ['SIGTERM', 'SIGINT'] as const;
291
+ for (const signal of signals) {
292
+ process.on(signal, shutdownHandler);
293
+ this._signalHandlers.push({ signal, handler: shutdownHandler });
294
+ }
295
+ }
296
+
297
+ /**
298
+ * Removes signal handlers registered for shutdown detection.
299
+ */
300
+ private _removeShutdownHandlers(): void {
301
+ for (const { signal, handler } of this._signalHandlers) {
302
+ process.off(signal, handler);
303
+ }
304
+ this._signalHandlers = [];
305
+ }
306
+
307
+ /**
308
+ * Initializes the internal Bun.SQL client.
309
+ * Note: This creates the SQL client but doesn't establish the TCP connection yet.
310
+ * Bun's SQL driver uses lazy connections - the actual TCP connection is made on first query.
311
+ */
312
+ private _initializeSql(): void {
313
+ if (this._closed || this._initialized) {
314
+ return;
315
+ }
316
+
317
+ const url = this._config.url ?? process.env.DATABASE_URL;
318
+
319
+ // Build Bun.SQL options - use type assertion since Bun types are a union
320
+ const bunOptions: BunPostgresOptions = {
321
+ adapter: 'postgres',
322
+ };
323
+
324
+ if (url) {
325
+ bunOptions.url = url;
326
+ }
327
+
328
+ if (this._config.hostname) bunOptions.hostname = this._config.hostname;
329
+ if (this._config.port) bunOptions.port = this._config.port;
330
+ if (this._config.username) bunOptions.username = this._config.username;
331
+ if (this._config.password) bunOptions.password = this._config.password;
332
+ if (this._config.database) bunOptions.database = this._config.database;
333
+ if (this._config.max) bunOptions.max = this._config.max;
334
+ if (this._config.idleTimeout !== undefined) bunOptions.idleTimeout = this._config.idleTimeout;
335
+ if (this._config.connectionTimeout !== undefined)
336
+ bunOptions.connectionTimeout = this._config.connectionTimeout;
337
+
338
+ // Handle TLS configuration
339
+ if (this._config.tls !== undefined) {
340
+ if (typeof this._config.tls === 'boolean') {
341
+ bunOptions.tls = this._config.tls;
342
+ } else {
343
+ bunOptions.tls = this._config.tls;
344
+ }
345
+ }
346
+
347
+ // Set up onclose handler for reconnection
348
+ bunOptions.onclose = (err: Error | null) => {
349
+ this._handleClose(err ?? undefined);
350
+ };
351
+
352
+ this._sql = new BunSQL(bunOptions);
353
+ this._initialized = true;
354
+ // Note: _connected remains false until we verify the connection with a query
355
+ }
356
+
357
+ /**
358
+ * Warms the connection by executing a test query.
359
+ * This establishes the actual TCP connection and verifies it's working.
360
+ */
361
+ private async _warmConnection(): Promise<void> {
362
+ if (this._closed || this._connected) {
363
+ return;
364
+ }
365
+
366
+ if (!this._sql) {
367
+ this._initializeSql();
368
+ }
369
+
370
+ // Execute a test query to establish the TCP connection
371
+ // If this fails, the error will propagate to the caller
372
+ await this._sql!`SELECT 1`;
373
+ this._connected = true;
374
+ this._stats.totalConnections++;
375
+ this._stats.lastConnectedAt = new Date();
376
+ }
377
+
378
+ /**
379
+ * Re-initializes the SQL client for reconnection.
380
+ * Used internally during the reconnection loop.
381
+ */
382
+ private _reinitializeSql(): void {
383
+ this._initialized = false;
384
+ this._connected = false;
385
+ this._initializeSql();
386
+ }
387
+
388
+ /**
389
+ * Handles connection close events.
390
+ */
391
+ private _handleClose(error?: Error): void {
392
+ const wasConnected = this._connected;
393
+ this._connected = false;
394
+ this._stats.lastDisconnectedAt = new Date();
395
+
396
+ // Call user's onclose callback
397
+ this._config.onclose?.(error);
398
+
399
+ // Don't reconnect if explicitly closed OR if application is shutting down
400
+ if (this._closed || this._shuttingDown) {
401
+ return;
402
+ }
403
+
404
+ // Check if reconnection is enabled
405
+ const reconnectConfig = mergeReconnectConfig(this._config.reconnect);
406
+ if (!reconnectConfig.enabled) {
407
+ return;
408
+ }
409
+
410
+ // If there's an error, check if it's retryable
411
+ // If there's NO error (graceful close), still attempt reconnection
412
+ if (error && !isRetryableError(error)) {
413
+ return;
414
+ }
415
+
416
+ // Start reconnection if not already in progress
417
+ if (!this._reconnecting && wasConnected) {
418
+ this._startReconnect();
419
+ }
420
+ }
421
+
422
+ /**
423
+ * Starts the reconnection process.
424
+ */
425
+ private _startReconnect(): void {
426
+ if (this._reconnecting || this._closed || this._shuttingDown) {
427
+ return;
428
+ }
429
+
430
+ this._reconnecting = true;
431
+ this._reconnectPromise = this._reconnectLoop();
432
+ }
433
+
434
+ /**
435
+ * The main reconnection loop with exponential backoff.
436
+ */
437
+ private async _reconnectLoop(): Promise<void> {
438
+ const config = mergeReconnectConfig(this._config.reconnect);
439
+ let attempt = 0;
440
+ let lastError: Error | undefined;
441
+
442
+ while (attempt < config.maxAttempts && !this._closed && !this._shuttingDown) {
443
+ this._stats.reconnectAttempts++;
444
+ this._stats.lastReconnectAttemptAt = new Date();
445
+
446
+ // Notify about reconnection attempt
447
+ this._config.onreconnect?.(attempt + 1);
448
+
449
+ // Calculate backoff delay
450
+ const delay = computeBackoff(attempt, config);
451
+
452
+ // Wait before attempting
453
+ await sleep(delay);
454
+
455
+ if (this._closed) {
456
+ break;
457
+ }
458
+
459
+ try {
460
+ // Close existing connection if any
461
+ if (this._sql) {
462
+ try {
463
+ await this._sql.close();
464
+ } catch {
465
+ // Ignore close errors
466
+ }
467
+ this._sql = null;
468
+ }
469
+
470
+ // Attempt to reconnect - reinitialize and warm the connection
471
+ this._reinitializeSql();
472
+ await this._warmConnection();
473
+
474
+ // Success!
475
+ this._reconnecting = false;
476
+ this._reconnectPromise = null;
477
+ this._config.onreconnected?.();
478
+ return;
479
+ } catch (error) {
480
+ lastError =
481
+ error instanceof Error
482
+ ? error
483
+ : new PostgresError({
484
+ message: String(error),
485
+ });
486
+ this._stats.failedReconnects++;
487
+ attempt++;
488
+ }
489
+ }
490
+
491
+ // All attempts failed
492
+ this._reconnecting = false;
493
+ this._reconnectPromise = null;
494
+
495
+ // Only invoke callback if not explicitly closed/shutdown to avoid noisy/misleading callbacks
496
+ if (!this._closed && !this._shuttingDown) {
497
+ const finalError = new ReconnectFailedError({
498
+ attempts: attempt,
499
+ lastError,
500
+ });
501
+
502
+ this._config.onreconnectfailed?.(finalError);
503
+ }
504
+ }
505
+
506
+ /**
507
+ * Ensures the client is initialized and returns the SQL instance.
508
+ * This is the synchronous version - use _ensureConnectedAsync when you can await.
509
+ *
510
+ * Note: This returns the SQL instance even if `_connected` is false because
511
+ * Bun's SQL uses lazy connections. The actual connection will be established
512
+ * on first query. Use this for synchronous access to the SQL instance.
513
+ */
514
+ private _ensureConnected(): InstanceType<typeof BunSQL> {
515
+ if (this._closed) {
516
+ throw new ConnectionClosedError({
517
+ message: 'Client has been closed',
518
+ });
519
+ }
520
+
521
+ if (!this._sql) {
522
+ throw new ConnectionClosedError({
523
+ message: 'SQL client not initialized',
524
+ wasReconnecting: this._reconnecting,
525
+ });
526
+ }
527
+
528
+ return this._sql;
529
+ }
530
+
531
+ /**
532
+ * Ensures the client is connected and returns the SQL instance.
533
+ * If reconnection is in progress, waits for it to complete.
534
+ * If connection hasn't been established yet, warms it first.
535
+ */
536
+ private async _ensureConnectedAsync(): Promise<InstanceType<typeof BunSQL>> {
537
+ if (this._closed) {
538
+ throw new ConnectionClosedError({
539
+ message: 'Client has been closed',
540
+ });
541
+ }
542
+
543
+ // If preconnect is in progress, wait for it
544
+ if (this._connectPromise) {
545
+ try {
546
+ await this._connectPromise;
547
+ } catch (err) {
548
+ this._connectPromise = null;
549
+ throw err;
550
+ }
551
+ this._connectPromise = null;
552
+ }
553
+
554
+ // If reconnection is in progress, wait for it to complete
555
+ if (this._reconnecting && this._reconnectPromise) {
556
+ await this._reconnectPromise;
557
+ }
558
+
559
+ if (!this._sql) {
560
+ throw new ConnectionClosedError({
561
+ message: 'SQL client not initialized',
562
+ wasReconnecting: false,
563
+ });
564
+ }
565
+
566
+ // If not yet connected, warm the connection
567
+ if (!this._connected) {
568
+ await this._warmConnection();
569
+ }
570
+
571
+ return this._sql;
572
+ }
573
+
574
+ /**
575
+ * Executes an operation with retry logic for retryable errors.
576
+ * Waits for reconnection if one is in progress.
577
+ */
578
+ private async _executeWithRetry<T>(
579
+ operation: () => T | Promise<T>,
580
+ maxRetries: number = 3
581
+ ): Promise<T> {
582
+ let lastError: Error | undefined;
583
+
584
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
585
+ try {
586
+ // Wait for preconnect if in progress
587
+ if (this._connectPromise) {
588
+ try {
589
+ await this._connectPromise;
590
+ } catch (err) {
591
+ this._connectPromise = null;
592
+ throw err;
593
+ }
594
+ this._connectPromise = null;
595
+ }
596
+
597
+ // Wait for reconnection if in progress
598
+ if (this._reconnecting && this._reconnectPromise) {
599
+ await this._reconnectPromise;
600
+ }
601
+
602
+ if (!this._sql) {
603
+ throw new ConnectionClosedError({
604
+ message: 'SQL client not initialized',
605
+ wasReconnecting: this._reconnecting,
606
+ });
607
+ }
608
+
609
+ // If not yet connected, warm the connection
610
+ if (!this._connected) {
611
+ await this._warmConnection();
612
+ }
613
+
614
+ return await operation();
615
+ } catch (error) {
616
+ lastError = error instanceof Error ? error : new Error(String(error));
617
+
618
+ // If it's a retryable error and we have retries left, wait and retry
619
+ if (isRetryableError(error) && attempt < maxRetries) {
620
+ // Wait for reconnection to complete if it started
621
+ if (this._reconnecting && this._reconnectPromise) {
622
+ try {
623
+ await this._reconnectPromise;
624
+ } catch {
625
+ // Reconnection failed, will throw below
626
+ }
627
+ }
628
+ continue;
629
+ }
630
+
631
+ throw error;
632
+ }
633
+ }
634
+
635
+ throw lastError;
636
+ }
637
+
638
+ /**
639
+ * Wait for the connection to be established.
640
+ * If the connection hasn't been established yet (lazy connection), this will
641
+ * warm the connection by executing a test query.
642
+ * If reconnection is in progress, waits for it to complete.
643
+ *
644
+ * @param timeoutMs - Optional timeout in milliseconds
645
+ * @throws {ConnectionClosedError} If the client has been closed or connection fails
646
+ */
647
+ async waitForConnection(timeoutMs?: number): Promise<void> {
648
+ if (this._connected && this._sql) {
649
+ return;
650
+ }
651
+
652
+ if (this._closed) {
653
+ throw new ConnectionClosedError({
654
+ message: 'Client has been closed',
655
+ });
656
+ }
657
+
658
+ const connectOperation = async () => {
659
+ // Wait for preconnect if in progress
660
+ if (this._connectPromise) {
661
+ try {
662
+ await this._connectPromise;
663
+ } catch (err) {
664
+ this._connectPromise = null;
665
+ throw err;
666
+ }
667
+ this._connectPromise = null;
668
+ }
669
+
670
+ // Wait for reconnection if in progress
671
+ if (this._reconnecting && this._reconnectPromise) {
672
+ await this._reconnectPromise;
673
+ }
674
+
675
+ // If still not connected, warm the connection
676
+ if (!this._connected && this._sql) {
677
+ await this._warmConnection();
678
+ }
679
+
680
+ if (!this._connected || !this._sql) {
681
+ throw new ConnectionClosedError({
682
+ message: 'Failed to establish connection',
683
+ });
684
+ }
685
+ };
686
+
687
+ if (timeoutMs !== undefined) {
688
+ let timerId: ReturnType<typeof setTimeout> | undefined;
689
+ const timeoutPromise = new Promise<never>((_, reject) => {
690
+ timerId = setTimeout(
691
+ () =>
692
+ reject(
693
+ new QueryTimeoutError({
694
+ timeoutMs,
695
+ })
696
+ ),
697
+ timeoutMs
698
+ );
699
+ });
700
+ try {
701
+ await Promise.race([connectOperation(), timeoutPromise]);
702
+ } finally {
703
+ if (timerId !== undefined) {
704
+ clearTimeout(timerId);
705
+ }
706
+ }
707
+ } else {
708
+ await connectOperation();
709
+ }
710
+ }
711
+ }
712
+
713
+ /**
714
+ * Type for the callable PostgresClient that supports tagged template literals.
715
+ */
716
+ export type CallablePostgresClient = PostgresClient & {
717
+ (strings: TemplateStringsArray, ...values: unknown[]): Promise<unknown[]>;
718
+ };
719
+
720
+ /**
721
+ * Creates a PostgresClient that can be called as a tagged template literal.
722
+ *
723
+ * @param config - Connection configuration
724
+ * @returns A callable PostgresClient
725
+ *
726
+ * @internal
727
+ */
728
+ export function createCallableClient(config?: string | PostgresConfig): CallablePostgresClient {
729
+ const client = new PostgresClient(config);
730
+
731
+ // Create a callable function that delegates to client.query
732
+ const callable = function (
733
+ strings: TemplateStringsArray,
734
+ ...values: unknown[]
735
+ ): Promise<unknown[]> {
736
+ return client.query(strings, ...values);
737
+ } as unknown as CallablePostgresClient;
738
+
739
+ // Copy all properties and methods from the client to the callable
740
+ Object.setPrototypeOf(callable, PostgresClient.prototype);
741
+
742
+ // Define properties that delegate to the client
743
+ Object.defineProperties(callable, {
744
+ connected: {
745
+ get: () => client.connected,
746
+ enumerable: true,
747
+ },
748
+ reconnecting: {
749
+ get: () => client.reconnecting,
750
+ enumerable: true,
751
+ },
752
+ shuttingDown: {
753
+ get: () => client.shuttingDown,
754
+ enumerable: true,
755
+ },
756
+ stats: {
757
+ get: () => client.stats,
758
+ enumerable: true,
759
+ },
760
+ raw: {
761
+ get: () => client.raw,
762
+ enumerable: true,
763
+ },
764
+ });
765
+
766
+ // Bind methods to the client
767
+ callable.query = client.query.bind(client);
768
+ callable.begin = client.begin.bind(client);
769
+ callable.reserve = client.reserve.bind(client);
770
+ callable.close = client.close.bind(client);
771
+ callable.shutdown = client.shutdown.bind(client);
772
+ callable.unsafe = client.unsafe.bind(client);
773
+ callable.waitForConnection = client.waitForConnection.bind(client);
774
+
775
+ return callable;
776
+ }