pgserve 2.3.0 → 2.4.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/src/pg-wire.js DELETED
@@ -1,869 +0,0 @@
1
- /**
2
- * PostgreSQL Wire Protocol Client
3
- *
4
- * Native Bun implementation for PostgreSQL connections using Bun.connect().
5
- * Implements: Startup, Authentication (MD5/trust), Simple Query, COPY TO/FROM.
6
- *
7
- * Protocol Reference: https://www.postgresql.org/docs/current/protocol.html
8
- */
9
-
10
- import { createHash } from 'crypto';
11
-
12
- // PostgreSQL Protocol Version 3.0
13
- const PROTOCOL_VERSION = 196608;
14
-
15
- // Frontend (client → server) message codes
16
- const FE = {
17
- Query: 0x51, // 'Q' - Simple query
18
- Terminate: 0x58, // 'X' - Connection termination
19
- PasswordMessage: 0x70, // 'p' - Password response
20
- CopyData: 0x64, // 'd' - COPY data chunk
21
- CopyDone: 0x63, // 'c' - COPY complete
22
- CopyFail: 0x66, // 'f' - COPY failed
23
- };
24
-
25
- // Backend (server → client) message codes
26
- const BE = {
27
- AuthenticationRequest: 0x52, // 'R' - Authentication request/ok
28
- BackendKeyData: 0x4b, // 'K' - Process ID and secret key
29
- ParameterStatus: 0x53, // 'S' - Server parameter change
30
- ReadyForQuery: 0x5a, // 'Z' - Ready for next query
31
- RowDescription: 0x54, // 'T' - Column metadata
32
- DataRow: 0x44, // 'D' - Row data
33
- CommandComplete: 0x43, // 'C' - Command finished
34
- ErrorResponse: 0x45, // 'E' - Error
35
- NoticeResponse: 0x4e, // 'N' - Warning/notice
36
- CopyOutResponse: 0x48, // 'H' - COPY TO started
37
- CopyInResponse: 0x47, // 'G' - COPY FROM ready
38
- CopyDone: 0x63, // 'c' - Server COPY complete
39
- EmptyQueryResponse: 0x49, // 'I' - Empty query
40
- };
41
-
42
- // Authentication types
43
- const AUTH = {
44
- Ok: 0,
45
- CleartextPassword: 3,
46
- MD5Password: 5,
47
- SASL: 10,
48
- SASLContinue: 11,
49
- SASLFinal: 12,
50
- };
51
-
52
- /**
53
- * PostgreSQL Wire Protocol Client
54
- *
55
- * @example
56
- * const client = new PgWireClient({
57
- * hostname: '127.0.0.1',
58
- * port: 5432,
59
- * database: 'mydb',
60
- * username: 'postgres',
61
- * password: 'postgres'
62
- * });
63
- * await client.connect();
64
- * const rows = await client.query('SELECT * FROM users');
65
- * client.close();
66
- */
67
- export class PgWireClient {
68
- constructor(options = {}) {
69
- this.hostname = options.hostname || '127.0.0.1';
70
- this.port = options.port || 5432;
71
- this.unix = options.unix || null;
72
- this.database = options.database || 'postgres';
73
- this.username = options.username || 'postgres';
74
- this.password = options.password || 'postgres';
75
-
76
- this.socket = null;
77
- this.buffer = Buffer.alloc(0);
78
- this.state = 'disconnected';
79
-
80
- // Promise management for async operations
81
- this._connectResolve = null;
82
- this._connectReject = null;
83
- this._queryResolve = null;
84
- this._queryReject = null;
85
- this._copyResolve = null;
86
- this._copyReject = null;
87
-
88
- // Query result accumulation
89
- this._columns = [];
90
- this._rows = [];
91
- this._commandTag = '';
92
-
93
- // COPY streaming
94
- this._copyChunks = [];
95
- this._copyCallback = null;
96
-
97
- // Backend key data (for cancel requests)
98
- this.processId = null;
99
- this.secretKey = null;
100
- }
101
-
102
- /**
103
- * Connect to PostgreSQL server
104
- * @returns {Promise<void>}
105
- */
106
- async connect() {
107
- return new Promise((resolve, reject) => {
108
- this._connectResolve = resolve;
109
- this._connectReject = reject;
110
-
111
- const connectOpts = this.unix
112
- ? { unix: this.unix }
113
- : { hostname: this.hostname, port: this.port };
114
-
115
- const client = this;
116
-
117
- Bun.connect({
118
- ...connectOpts,
119
- socket: {
120
- open(socket) {
121
- client.socket = socket;
122
- client.state = 'startup';
123
- // Send startup message
124
- const startup = client._buildStartupMessage();
125
- socket.write(startup);
126
- },
127
- data(socket, data) {
128
- client._onData(data);
129
- },
130
- close() {
131
- client._onClose();
132
- },
133
- error(socket, err) {
134
- client._onError(err);
135
- },
136
- drain() {
137
- // Handle backpressure if needed
138
- }
139
- }
140
- }).catch(reject);
141
- });
142
- }
143
-
144
- /**
145
- * Build PostgreSQL startup message
146
- * @private
147
- */
148
- _buildStartupMessage() {
149
- const params = `user\0${this.username}\0database\0${this.database}\0\0`;
150
- const len = 4 + 4 + params.length;
151
- const buf = Buffer.alloc(len);
152
- buf.writeUInt32BE(len, 0);
153
- buf.writeUInt32BE(PROTOCOL_VERSION, 4);
154
- buf.write(params, 8);
155
- return buf;
156
- }
157
-
158
- /**
159
- * Handle incoming data
160
- * @private
161
- */
162
- _onData(data) {
163
- // Append to buffer
164
- this.buffer = Buffer.concat([this.buffer, Buffer.from(data)]);
165
-
166
- // Process complete messages
167
- this._processMessages();
168
- }
169
-
170
- /**
171
- * Process buffered messages
172
- * @private
173
- */
174
- _processMessages() {
175
- while (this.buffer.length >= 5) {
176
- const type = this.buffer[0];
177
- const length = this.buffer.readUInt32BE(1);
178
- const totalLength = 1 + length;
179
-
180
- if (this.buffer.length < totalLength) {
181
- break; // Wait for more data
182
- }
183
-
184
- const payload = this.buffer.subarray(5, totalLength);
185
- this.buffer = this.buffer.subarray(totalLength);
186
-
187
- this._handleMessage(type, payload);
188
- }
189
- }
190
-
191
- /**
192
- * Handle a single message
193
- * @private
194
- */
195
- _handleMessage(type, payload) {
196
- switch (type) {
197
- case BE.AuthenticationRequest:
198
- this._handleAuth(payload);
199
- break;
200
-
201
- case BE.BackendKeyData:
202
- this.processId = payload.readUInt32BE(0);
203
- this.secretKey = payload.readUInt32BE(4);
204
- break;
205
-
206
- case BE.ParameterStatus:
207
- // Server parameter - ignore for now
208
- break;
209
-
210
- case BE.ReadyForQuery:
211
- this._handleReadyForQuery(payload);
212
- break;
213
-
214
- case BE.RowDescription:
215
- this._handleRowDescription(payload);
216
- break;
217
-
218
- case BE.DataRow:
219
- this._handleDataRow(payload);
220
- break;
221
-
222
- case BE.CommandComplete:
223
- this._commandTag = payload.toString('utf8', 0, payload.indexOf(0));
224
- break;
225
-
226
- case BE.ErrorResponse:
227
- this._handleError(payload);
228
- break;
229
-
230
- case BE.NoticeResponse:
231
- // Warning - ignore for now
232
- break;
233
-
234
- case BE.EmptyQueryResponse:
235
- // Empty query - no rows
236
- break;
237
-
238
- case BE.CopyOutResponse:
239
- this._handleCopyOutResponse(payload);
240
- break;
241
-
242
- case BE.CopyInResponse:
243
- this._handleCopyInResponse(payload);
244
- break;
245
-
246
- case BE.CopyDone:
247
- // Server confirms COPY complete
248
- break;
249
-
250
- case FE.CopyData:
251
- // This is actually BE CopyData (same code)
252
- this._handleCopyData(payload);
253
- break;
254
- }
255
- }
256
-
257
- /**
258
- * Handle authentication request
259
- * @private
260
- */
261
- _handleAuth(payload) {
262
- const authType = payload.readUInt32BE(0);
263
-
264
- switch (authType) {
265
- case AUTH.Ok:
266
- this.state = 'authenticated';
267
- break;
268
-
269
- case AUTH.CleartextPassword:
270
- this._sendPassword(this.password);
271
- break;
272
-
273
- case AUTH.MD5Password:
274
- const salt = payload.subarray(4, 8);
275
- const hash = this._md5Auth(this.username, this.password, salt);
276
- this._sendPassword('md5' + hash);
277
- break;
278
-
279
- case AUTH.SASL:
280
- // SCRAM-SHA-256 not yet implemented
281
- this._rejectConnect(new Error('SCRAM-SHA-256 authentication not yet implemented'));
282
- break;
283
-
284
- default:
285
- this._rejectConnect(new Error(`Unsupported authentication type: ${authType}`));
286
- }
287
- }
288
-
289
- /**
290
- * Compute MD5 password hash
291
- * @private
292
- */
293
- _md5Auth(user, password, salt) {
294
- // md5(md5(password + user) + salt)
295
- const inner = createHash('md5')
296
- .update(password + user)
297
- .digest('hex');
298
- return createHash('md5')
299
- .update(inner)
300
- .update(salt)
301
- .digest('hex');
302
- }
303
-
304
- /**
305
- * Send password message
306
- * @private
307
- */
308
- _sendPassword(password) {
309
- const buf = Buffer.alloc(1 + 4 + password.length + 1);
310
- buf[0] = FE.PasswordMessage;
311
- buf.writeUInt32BE(4 + password.length + 1, 1);
312
- buf.write(password + '\0', 5);
313
- this.socket.write(buf);
314
- }
315
-
316
- /**
317
- * Handle ReadyForQuery message
318
- * @private
319
- */
320
- _handleReadyForQuery(payload) {
321
- const _txStatus = String.fromCharCode(payload[0]); // 'I', 'T', or 'E'
322
-
323
- if (this.state === 'authenticated' || this.state === 'startup') {
324
- this.state = 'ready';
325
- this._resolveConnect();
326
- } else if (this.state === 'query') {
327
- this.state = 'ready';
328
- this._resolveQuery({
329
- rows: this._rows,
330
- columns: this._columns,
331
- command: this._commandTag
332
- });
333
- } else if (this.state === 'copyTo') {
334
- this.state = 'ready';
335
- this._resolveCopy();
336
- } else if (this.state === 'copyFrom') {
337
- this.state = 'ready';
338
- this._resolveCopy();
339
- }
340
- }
341
-
342
- /**
343
- * Handle RowDescription message
344
- * @private
345
- */
346
- _handleRowDescription(payload) {
347
- const fieldCount = payload.readUInt16BE(0);
348
- const columns = [];
349
- let offset = 2;
350
-
351
- for (let i = 0; i < fieldCount; i++) {
352
- const nameEnd = payload.indexOf(0, offset);
353
- const name = payload.toString('utf8', offset, nameEnd);
354
- offset = nameEnd + 1;
355
-
356
- const _tableOid = payload.readUInt32BE(offset);
357
- offset += 4;
358
- const _columnId = payload.readUInt16BE(offset);
359
- offset += 2;
360
- const typeOid = payload.readUInt32BE(offset);
361
- offset += 4;
362
- const _typeLen = payload.readInt16BE(offset);
363
- offset += 2;
364
- const _typeMod = payload.readInt32BE(offset);
365
- offset += 4;
366
- const format = payload.readUInt16BE(offset);
367
- offset += 2;
368
-
369
- columns.push({ name, typeOid, format });
370
- }
371
-
372
- this._columns = columns;
373
- }
374
-
375
- /**
376
- * Handle DataRow message
377
- * @private
378
- */
379
- _handleDataRow(payload) {
380
- const columnCount = payload.readUInt16BE(0);
381
- const row = {};
382
- let offset = 2;
383
-
384
- for (let i = 0; i < columnCount; i++) {
385
- const valueLen = payload.readInt32BE(offset);
386
- offset += 4;
387
-
388
- if (valueLen === -1) {
389
- row[this._columns[i].name] = null;
390
- } else {
391
- const value = payload.toString('utf8', offset, offset + valueLen);
392
- offset += valueLen;
393
- row[this._columns[i].name] = this._parseValue(value, this._columns[i].typeOid);
394
- }
395
- }
396
-
397
- this._rows.push(row);
398
- }
399
-
400
- /**
401
- * Parse value based on PostgreSQL type OID
402
- * @private
403
- */
404
- _parseValue(value, typeOid) {
405
- // Basic type conversion
406
- switch (typeOid) {
407
- case 23: // int4
408
- case 21: // int2
409
- case 20: // int8
410
- return parseInt(value, 10);
411
- case 700: // float4
412
- case 701: // float8
413
- case 1700: // numeric
414
- return parseFloat(value);
415
- case 16: // bool
416
- return value === 't' || value === 'true';
417
- case 1114: // timestamp
418
- case 1184: // timestamptz
419
- return new Date(value);
420
- default:
421
- return value;
422
- }
423
- }
424
-
425
- /**
426
- * Handle error response
427
- * @private
428
- */
429
- _handleError(payload) {
430
- const error = this._parseErrorFields(payload);
431
- const err = new Error(error.message || 'PostgreSQL error');
432
- err.code = error.code;
433
- err.detail = error.detail;
434
- err.severity = error.severity;
435
-
436
- if (this.state === 'startup' || this.state === 'authenticated') {
437
- this._rejectConnect(err);
438
- } else if (this.state === 'query') {
439
- this._rejectQuery(err);
440
- } else if (this.state === 'copyTo' || this.state === 'copyFrom') {
441
- this._rejectCopy(err);
442
- }
443
- }
444
-
445
- /**
446
- * Parse error response fields
447
- * @private
448
- */
449
- _parseErrorFields(payload) {
450
- const fields = {};
451
- let offset = 0;
452
-
453
- while (offset < payload.length) {
454
- const fieldType = String.fromCharCode(payload[offset]);
455
- if (fieldType === '\0') break;
456
- offset++;
457
-
458
- const valueEnd = payload.indexOf(0, offset);
459
- const value = payload.toString('utf8', offset, valueEnd);
460
- offset = valueEnd + 1;
461
-
462
- switch (fieldType) {
463
- case 'S': fields.severity = value; break;
464
- case 'C': fields.code = value; break;
465
- case 'M': fields.message = value; break;
466
- case 'D': fields.detail = value; break;
467
- case 'H': fields.hint = value; break;
468
- case 'P': fields.position = value; break;
469
- }
470
- }
471
-
472
- return fields;
473
- }
474
-
475
- /**
476
- * Handle CopyOutResponse (COPY TO started)
477
- * @private
478
- */
479
- _handleCopyOutResponse(_payload) {
480
- this.state = 'copyTo';
481
- this._copyChunks = [];
482
- // Format and column info available in _payload but not needed for binary copy
483
- }
484
-
485
- /**
486
- * Handle CopyInResponse (COPY FROM ready)
487
- * @private
488
- */
489
- _handleCopyInResponse(_payload) {
490
- this.state = 'copyFrom';
491
- // Server is ready to receive COPY data
492
- if (this._copyCallback) {
493
- this._copyCallback();
494
- }
495
- }
496
-
497
- /**
498
- * Handle CopyData message (for COPY TO)
499
- * @private
500
- */
501
- _handleCopyData(payload) {
502
- if (this.state === 'copyTo') {
503
- this._copyChunks.push(Buffer.from(payload));
504
- }
505
- }
506
-
507
- /**
508
- * Handle connection close
509
- * @private
510
- */
511
- _onClose() {
512
- this.state = 'disconnected';
513
- this.socket = null;
514
- }
515
-
516
- /**
517
- * Handle socket error
518
- * @private
519
- */
520
- _onError(err) {
521
- if (this._connectReject) {
522
- this._connectReject(err);
523
- } else if (this._queryReject) {
524
- this._queryReject(err);
525
- } else if (this._copyReject) {
526
- this._copyReject(err);
527
- }
528
- }
529
-
530
- // Promise resolution helpers
531
- _resolveConnect() {
532
- if (this._connectResolve) {
533
- this._connectResolve();
534
- this._connectResolve = null;
535
- this._connectReject = null;
536
- }
537
- }
538
-
539
- _rejectConnect(err) {
540
- if (this._connectReject) {
541
- this._connectReject(err);
542
- this._connectResolve = null;
543
- this._connectReject = null;
544
- }
545
- }
546
-
547
- _resolveQuery(result) {
548
- if (this._queryResolve) {
549
- this._queryResolve(result);
550
- this._queryResolve = null;
551
- this._queryReject = null;
552
- }
553
- }
554
-
555
- _rejectQuery(err) {
556
- if (this._queryReject) {
557
- this._queryReject(err);
558
- this._queryResolve = null;
559
- this._queryReject = null;
560
- }
561
- }
562
-
563
- _resolveCopy() {
564
- if (this._copyResolve) {
565
- this._copyResolve();
566
- this._copyResolve = null;
567
- this._copyReject = null;
568
- }
569
- }
570
-
571
- _rejectCopy(err) {
572
- if (this._copyReject) {
573
- this._copyReject(err);
574
- this._copyResolve = null;
575
- this._copyReject = null;
576
- }
577
- }
578
-
579
- /**
580
- * Execute a simple query
581
- * @param {string} sql - SQL query
582
- * @returns {Promise<{rows: Object[], columns: Object[], command: string}>}
583
- */
584
- async query(sql) {
585
- if (this.state !== 'ready') {
586
- throw new Error(`Cannot query: client is ${this.state}`);
587
- }
588
-
589
- return new Promise((resolve, reject) => {
590
- this._queryResolve = resolve;
591
- this._queryReject = reject;
592
- this._columns = [];
593
- this._rows = [];
594
- this._commandTag = '';
595
- this.state = 'query';
596
-
597
- // Send Query message
598
- const buf = Buffer.alloc(1 + 4 + Buffer.byteLength(sql, 'utf8') + 1);
599
- buf[0] = FE.Query;
600
- buf.writeUInt32BE(4 + Buffer.byteLength(sql, 'utf8') + 1, 1);
601
- buf.write(sql + '\0', 5);
602
- this.socket.write(buf);
603
- });
604
- }
605
-
606
- /**
607
- * Execute COPY TO STDOUT - returns async iterator of binary chunks
608
- * @param {string} sql - COPY TO query
609
- * @returns {AsyncGenerator<Buffer>}
610
- */
611
- async *copyTo(sql) {
612
- if (this.state !== 'ready') {
613
- throw new Error(`Cannot copyTo: client is ${this.state}`);
614
- }
615
-
616
- // Reset copy state
617
- this._copyChunks = [];
618
- this.state = 'copyTo';
619
-
620
- // Send Query message
621
- const buf = Buffer.alloc(1 + 4 + Buffer.byteLength(sql, 'utf8') + 1);
622
- buf[0] = FE.Query;
623
- buf.writeUInt32BE(4 + Buffer.byteLength(sql, 'utf8') + 1, 1);
624
- buf.write(sql + '\0', 5);
625
- this.socket.write(buf);
626
-
627
- // Yield chunks as they arrive
628
- while (this.state === 'copyTo') {
629
- // Wait for data or state change
630
- await this._waitForData();
631
-
632
- // Yield accumulated chunks
633
- while (this._copyChunks.length > 0) {
634
- yield this._copyChunks.shift();
635
- }
636
- }
637
- }
638
-
639
- /**
640
- * Wait for more data or state change
641
- * @private
642
- */
643
- _waitForData() {
644
- return new Promise(resolve => {
645
- if (this._copyChunks.length > 0 || this.state !== 'copyTo') {
646
- resolve();
647
- return;
648
- }
649
-
650
- // Poll for changes (simple approach)
651
- const check = () => {
652
- if (this._copyChunks.length > 0 || this.state !== 'copyTo') {
653
- resolve();
654
- } else {
655
- setTimeout(check, 1);
656
- }
657
- };
658
- setTimeout(check, 1);
659
- });
660
- }
661
-
662
- /**
663
- * Execute COPY FROM STDIN - accepts async iterator of binary chunks
664
- * @param {string} sql - COPY FROM query
665
- * @param {AsyncIterable<Buffer>} dataIterator - Iterator yielding data chunks
666
- * @returns {Promise<void>}
667
- */
668
- async copyFrom(sql, dataIterator) {
669
- if (this.state !== 'ready') {
670
- throw new Error(`Cannot copyFrom: client is ${this.state}`);
671
- }
672
-
673
- return new Promise(async (resolve, reject) => {
674
- this._copyResolve = resolve;
675
- this._copyReject = reject;
676
-
677
- // Wait for CopyInResponse
678
- this._copyCallback = async () => {
679
- try {
680
- // Send CopyData messages
681
- for await (const chunk of dataIterator) {
682
- const msg = Buffer.alloc(1 + 4 + chunk.length);
683
- msg[0] = FE.CopyData;
684
- msg.writeUInt32BE(4 + chunk.length, 1);
685
- chunk.copy(msg, 5);
686
- this.socket.write(msg);
687
- }
688
-
689
- // Send CopyDone
690
- const done = Buffer.alloc(5);
691
- done[0] = FE.CopyDone;
692
- done.writeUInt32BE(4, 1);
693
- this.socket.write(done);
694
- } catch (err) {
695
- // Send CopyFail
696
- const errMsg = err.message || 'Copy failed';
697
- const fail = Buffer.alloc(1 + 4 + Buffer.byteLength(errMsg, 'utf8') + 1);
698
- fail[0] = FE.CopyFail;
699
- fail.writeUInt32BE(4 + Buffer.byteLength(errMsg, 'utf8') + 1, 1);
700
- fail.write(errMsg + '\0', 5);
701
- this.socket.write(fail);
702
- reject(err);
703
- }
704
-
705
- this._copyCallback = null;
706
- };
707
-
708
- this.state = 'copyFrom';
709
-
710
- // Send Query message
711
- const buf = Buffer.alloc(1 + 4 + Buffer.byteLength(sql, 'utf8') + 1);
712
- buf[0] = FE.Query;
713
- buf.writeUInt32BE(4 + Buffer.byteLength(sql, 'utf8') + 1, 1);
714
- buf.write(sql + '\0', 5);
715
- this.socket.write(buf);
716
- });
717
- }
718
-
719
- /**
720
- * Close the connection gracefully
721
- */
722
- close() {
723
- if (this.socket) {
724
- // Send Terminate message
725
- const terminate = Buffer.alloc(5);
726
- terminate[0] = FE.Terminate;
727
- terminate.writeUInt32BE(4, 1);
728
- this.socket.write(terminate);
729
- this.socket.end();
730
- this.socket = null;
731
- }
732
- this.state = 'disconnected';
733
- }
734
-
735
- /**
736
- * Check if client is connected and ready
737
- */
738
- get isReady() {
739
- return this.state === 'ready';
740
- }
741
- }
742
-
743
- /**
744
- * PostgreSQL Connection Pool
745
- *
746
- * Manages a pool of PgWireClient connections.
747
- *
748
- * @example
749
- * const pool = new PgWirePool({ hostname: '127.0.0.1', port: 5432, max: 8 });
750
- * const client = await pool.connect();
751
- * const result = await client.query('SELECT 1');
752
- * pool.release(client);
753
- * await pool.end();
754
- */
755
- export class PgWirePool {
756
- constructor(options = {}) {
757
- this.options = options;
758
- this.max = options.max || 10;
759
- this.available = [];
760
- this.inUse = new Set();
761
- this.waiters = [];
762
- this.closed = false;
763
- }
764
-
765
- /**
766
- * Acquire a connection from the pool
767
- * @returns {Promise<PgWireClient>}
768
- */
769
- async connect() {
770
- if (this.closed) {
771
- throw new Error('Pool is closed');
772
- }
773
-
774
- // Return available connection
775
- if (this.available.length > 0) {
776
- const client = this.available.pop();
777
- this.inUse.add(client);
778
- return client;
779
- }
780
-
781
- // Create new connection if under limit
782
- if (this.inUse.size < this.max) {
783
- const client = new PgWireClient(this.options);
784
- await client.connect();
785
- this.inUse.add(client);
786
- return client;
787
- }
788
-
789
- // Wait for available connection
790
- return new Promise((resolve, reject) => {
791
- this.waiters.push({ resolve, reject });
792
- });
793
- }
794
-
795
- /**
796
- * Release a connection back to the pool
797
- * @param {PgWireClient} client
798
- */
799
- release(client) {
800
- if (!this.inUse.has(client)) {
801
- return;
802
- }
803
-
804
- this.inUse.delete(client);
805
-
806
- // Check for waiting requests
807
- if (this.waiters.length > 0 && client.isReady) {
808
- const waiter = this.waiters.shift();
809
- this.inUse.add(client);
810
- waiter.resolve(client);
811
- return;
812
- }
813
-
814
- // Return to pool if still usable
815
- if (client.isReady && !this.closed) {
816
- this.available.push(client);
817
- } else {
818
- client.close();
819
- }
820
- }
821
-
822
- /**
823
- * Execute a query using a pooled connection
824
- * @param {string} sql - SQL query
825
- * @returns {Promise<{rows: Object[], columns: Object[], command: string}>}
826
- */
827
- async query(sql) {
828
- const client = await this.connect();
829
- try {
830
- return await client.query(sql);
831
- } finally {
832
- this.release(client);
833
- }
834
- }
835
-
836
- /**
837
- * Close all connections and the pool
838
- */
839
- async end() {
840
- this.closed = true;
841
-
842
- // Reject waiting requests
843
- for (const waiter of this.waiters) {
844
- waiter.reject(new Error('Pool is closing'));
845
- }
846
- this.waiters = [];
847
-
848
- // Close all connections
849
- for (const client of [...this.available, ...this.inUse]) {
850
- client.close();
851
- }
852
- this.available = [];
853
- this.inUse.clear();
854
- }
855
-
856
- /**
857
- * Get pool statistics
858
- */
859
- get stats() {
860
- return {
861
- available: this.available.length,
862
- inUse: this.inUse.size,
863
- waiting: this.waiters.length,
864
- max: this.max
865
- };
866
- }
867
- }
868
-
869
- export { FE, BE, AUTH };