postgrejs 3.6.0 → 3.6.1
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/CHANGELOG.md +15 -1
- package/README.md +5 -1
- package/connection/intl-connection.js +4 -2
- package/connection/pool.d.ts +22 -0
- package/connection/pool.js +45 -3
- package/index.d.ts +1 -0
- package/index.js +1 -0
- package/package.json +1 -1
- package/protocol/connection-lost-error.d.ts +40 -0
- package/protocol/connection-lost-error.js +54 -0
- package/protocol/pg-socket.d.ts +7 -0
- package/protocol/pg-socket.js +26 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
## Changelog
|
|
2
2
|
|
|
3
|
-
### [v3.6.
|
|
3
|
+
### [v3.6.1](https://github.com/panates/postgrejs/compare/v3.6.0...v3.6.1) -
|
|
4
|
+
|
|
5
|
+
#### 🚀 New Features
|
|
6
|
+
|
|
7
|
+
- feat: report a pooled connection that dies @Eray Hanoğlu
|
|
8
|
+
|
|
9
|
+
#### 🛠 Refactoring and Updates
|
|
10
|
+
|
|
11
|
+
- refactor: give a lost connection its own error type @Eray Hanoğlu
|
|
12
|
+
|
|
13
|
+
#### 💬 General Changes
|
|
14
|
+
|
|
15
|
+
- ci: pin the runner image and stop naming the PostgreSQL it ships @Eray Hanoğlu
|
|
16
|
+
|
|
17
|
+
### [v3.6.0](https://github.com/panates/postgrejs/compare/v3.5.0...v3.6.0) - 20 September 2026
|
|
4
18
|
|
|
5
19
|
#### 🚀 New Features
|
|
6
20
|
|
package/README.md
CHANGED
|
@@ -74,7 +74,11 @@ Upgrading from 3.5? See [doc/MIGRATION-v3.5-to-v3.6.md](doc/MIGRATION-v3.5-to-v3
|
|
|
74
74
|
## Features
|
|
75
75
|
|
|
76
76
|
- **Connection Management:** Supports both single connection and advanced pooling, providing scalability and efficient
|
|
77
|
-
resource management.
|
|
77
|
+
resource management. A connection that dies rather than being closed says so: `'close'` carries the reason, and a
|
|
78
|
+
pooled one is reported on the pool's `'destroy'` and `'error'` events - so an admin kill or a failover is
|
|
79
|
+
distinguishable from an ordinary eviction, including when the connection was sitting idle with no query to reject.
|
|
80
|
+
The reason is a `ConnectionLostError` carrying the backend's `processID`, and `code` `'08006'` so branching on it
|
|
81
|
+
needs no `instanceof`.
|
|
78
82
|
- **Binary Wire Protocol:** Implements the full binary wire protocol for all PostgreSQL data types, ensuring robust and
|
|
79
83
|
efficient data handling.
|
|
80
84
|
- **Prepared Statements:** Named prepared statements for optimized query execution, and a per-connection cache that
|
|
@@ -96,7 +96,9 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
96
96
|
this._config = Object.freeze(getConnectionConfig(config));
|
|
97
97
|
this.socket = new PgSocket(this._config);
|
|
98
98
|
this.socket.on('error', err => this._onError(err));
|
|
99
|
-
|
|
99
|
+
// The reason travels with it: 'close' on its own cannot tell an
|
|
100
|
+
// ordinary shutdown from the backend being terminated under us.
|
|
101
|
+
this.socket.on('close', (reason) => this.emit('close', reason));
|
|
100
102
|
this.socket.on('notification', payload => this.emit('notification', payload));
|
|
101
103
|
this.socket.on('connecting', () => this.emit('connecting'));
|
|
102
104
|
this._onErrorSavePoint = 'SP_' + Math.round(Math.random() * 100000000);
|
|
@@ -163,7 +165,7 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
163
165
|
// IntlConnection TWICE per close() call - confirmed live: a single
|
|
164
166
|
// close() left two listeners registered on the far side (e.g. two
|
|
165
167
|
// NOTIFY deliveries after Pool's reconnect re-subscribed twice).
|
|
166
|
-
this.socket.once('close', resolve);
|
|
168
|
+
this.socket.once('close', () => resolve());
|
|
167
169
|
this.socket.sendTerminateMessage(() => {
|
|
168
170
|
this.socket.close();
|
|
169
171
|
});
|
package/connection/pool.d.ts
CHANGED
|
@@ -37,6 +37,14 @@ export declare class Pool extends SafeEventEmitter {
|
|
|
37
37
|
protected readonly _notificationListeners: SafeEventEmitter;
|
|
38
38
|
protected _notificationConnection?: Connection;
|
|
39
39
|
protected readonly _pipelineSlots: PipelineSlot[];
|
|
40
|
+
/**
|
|
41
|
+
* Why a connection is about to be destroyed, for the ones that were not
|
|
42
|
+
* destroyed on purpose. lightning-pool's own 'destroy' carries the
|
|
43
|
+
* resource and nothing else, so the reason is recorded on the way in -
|
|
44
|
+
* when the connection reports its close - and read back out when the
|
|
45
|
+
* pool reports the removal.
|
|
46
|
+
*/
|
|
47
|
+
protected readonly _destroyReasons: WeakMap<IntlConnection, Error>;
|
|
40
48
|
protected _pipelineMaxQueries: number;
|
|
41
49
|
protected _pipelineMaxConnections: number;
|
|
42
50
|
readonly config: PoolConfiguration;
|
|
@@ -91,6 +99,20 @@ export declare class Pool extends SafeEventEmitter {
|
|
|
91
99
|
listen(channel: string, callback: NotificationCallback): Promise<void>;
|
|
92
100
|
unListen(channel: string): Promise<void>;
|
|
93
101
|
unListenAll(): Promise<void>;
|
|
102
|
+
/**
|
|
103
|
+
* A pooled connection reported that its socket closed. `reason` is set
|
|
104
|
+
* only when that was not asked for - see PgSocket._handleClose().
|
|
105
|
+
*/
|
|
106
|
+
protected _onConnectionClosed(intlCon: IntlConnection, reason?: Error): void;
|
|
107
|
+
/**
|
|
108
|
+
* The pool has removed a connection. `destroy` fires the same way for an
|
|
109
|
+
* ordinary eviction (idle timeout, close(), a failed validate) as for a
|
|
110
|
+
* connection that died, so the reason is what tells them apart - an
|
|
111
|
+
* admin kill, a failover and a network fault were otherwise
|
|
112
|
+
* indistinguishable from a timeout, with nothing at all reported when
|
|
113
|
+
* the connection had no query in flight to reject.
|
|
114
|
+
*/
|
|
115
|
+
protected _onConnectionDestroyed(intlCon: IntlConnection): void;
|
|
94
116
|
protected _canPipeline(pipeline: boolean | undefined, sql: string, autoCommit: boolean | undefined, signal: AbortSignal | undefined): boolean;
|
|
95
117
|
/**
|
|
96
118
|
* Picks the pooled connection a pipelined query should ride on, or
|
package/connection/pool.js
CHANGED
|
@@ -13,6 +13,14 @@ export class Pool extends SafeEventEmitter {
|
|
|
13
13
|
_notificationListeners = new SafeEventEmitter();
|
|
14
14
|
_notificationConnection;
|
|
15
15
|
_pipelineSlots = [];
|
|
16
|
+
/**
|
|
17
|
+
* Why a connection is about to be destroyed, for the ones that were not
|
|
18
|
+
* destroyed on purpose. lightning-pool's own 'destroy' carries the
|
|
19
|
+
* resource and nothing else, so the reason is recorded on the way in -
|
|
20
|
+
* when the connection reports its close - and read back out when the
|
|
21
|
+
* pool reports the removal.
|
|
22
|
+
*/
|
|
23
|
+
_destroyReasons = new WeakMap();
|
|
16
24
|
_pipelineMaxQueries;
|
|
17
25
|
_pipelineMaxConnections;
|
|
18
26
|
config;
|
|
@@ -46,7 +54,7 @@ export class Pool extends SafeEventEmitter {
|
|
|
46
54
|
/* c8 ignore stop */
|
|
47
55
|
const intlCon = new IntlConnection(cfg);
|
|
48
56
|
await intlCon.connect();
|
|
49
|
-
intlCon.on('close', () => this.
|
|
57
|
+
intlCon.on('close', (reason) => this._onConnectionClosed(intlCon, reason));
|
|
50
58
|
/* c8 ignore start */
|
|
51
59
|
if (this.listenerCount('debug')) {
|
|
52
60
|
this.emit('debug', {
|
|
@@ -82,7 +90,7 @@ export class Pool extends SafeEventEmitter {
|
|
|
82
90
|
/* c8 ignore stop */
|
|
83
91
|
intlCon.owner = undefined;
|
|
84
92
|
intlCon.removeAllListeners();
|
|
85
|
-
intlCon.once('close', () => this.
|
|
93
|
+
intlCon.once('close', (reason) => this._onConnectionClosed(intlCon, reason));
|
|
86
94
|
intlCon._refCount = 0;
|
|
87
95
|
},
|
|
88
96
|
validate: async (intlCon) => {
|
|
@@ -104,7 +112,7 @@ export class Pool extends SafeEventEmitter {
|
|
|
104
112
|
this._pool.on('return', (...args) => this.emit('release', ...args));
|
|
105
113
|
this._pool.on('error', (...args) => this.emit('error', ...args));
|
|
106
114
|
this._pool.on('acquire', (...args) => this.emit('acquire', ...args));
|
|
107
|
-
this._pool.on('destroy', (
|
|
115
|
+
this._pool.on('destroy', (intlCon) => this._onConnectionDestroyed(intlCon));
|
|
108
116
|
}
|
|
109
117
|
/**
|
|
110
118
|
* Returns the number of connections that are currently acquired
|
|
@@ -313,6 +321,40 @@ export class Pool extends SafeEventEmitter {
|
|
|
313
321
|
await conn.close();
|
|
314
322
|
}
|
|
315
323
|
}
|
|
324
|
+
/**
|
|
325
|
+
* A pooled connection reported that its socket closed. `reason` is set
|
|
326
|
+
* only when that was not asked for - see PgSocket._handleClose().
|
|
327
|
+
*/
|
|
328
|
+
_onConnectionClosed(intlCon, reason) {
|
|
329
|
+
if (reason)
|
|
330
|
+
this._destroyReasons.set(intlCon, reason);
|
|
331
|
+
this._pool.destroy(intlCon);
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* The pool has removed a connection. `destroy` fires the same way for an
|
|
335
|
+
* ordinary eviction (idle timeout, close(), a failed validate) as for a
|
|
336
|
+
* connection that died, so the reason is what tells them apart - an
|
|
337
|
+
* admin kill, a failover and a network fault were otherwise
|
|
338
|
+
* indistinguishable from a timeout, with nothing at all reported when
|
|
339
|
+
* the connection had no query in flight to reject.
|
|
340
|
+
*/
|
|
341
|
+
_onConnectionDestroyed(intlCon) {
|
|
342
|
+
const reason = this._destroyReasons.get(intlCon);
|
|
343
|
+
if (reason)
|
|
344
|
+
this._destroyReasons.delete(intlCon);
|
|
345
|
+
this.emit('destroy', intlCon, reason);
|
|
346
|
+
if (!reason)
|
|
347
|
+
return;
|
|
348
|
+
// Reported on 'error' as well, which is where pg puts a pooled
|
|
349
|
+
// connection dying and where code ported from it listens. Safe to do
|
|
350
|
+
// unconditionally here: SafeEventEmitter drops an 'error' that has no
|
|
351
|
+
// listener instead of throwing, so this cannot turn a recovery the
|
|
352
|
+
// pool already made into a crashed process. The error names the pid
|
|
353
|
+
// and carries the socket error as its cause; no second argument, so
|
|
354
|
+
// the existing `(err, meta)` shape lightning-pool emits for a
|
|
355
|
+
// connection that could not be created stays unambiguous.
|
|
356
|
+
this.emit('error', reason);
|
|
357
|
+
}
|
|
316
358
|
_canPipeline(pipeline, sql, autoCommit, signal) {
|
|
317
359
|
return (pipeline === true &&
|
|
318
360
|
!signal &&
|
package/index.d.ts
CHANGED
|
@@ -23,6 +23,7 @@ export * from './interfaces/query-result.js';
|
|
|
23
23
|
export * from './interfaces/script-execute-options.js';
|
|
24
24
|
export * from './interfaces/script-result.js';
|
|
25
25
|
export * from './interfaces/statement-prepare-options.js';
|
|
26
|
+
export * from './protocol/connection-lost-error.js';
|
|
26
27
|
export * from './protocol/database-error.js';
|
|
27
28
|
export * from './protocol/smart-buffer.js';
|
|
28
29
|
export * from './types.js';
|
package/index.js
CHANGED
|
@@ -23,6 +23,7 @@ export * from './interfaces/query-result.js';
|
|
|
23
23
|
export * from './interfaces/script-execute-options.js';
|
|
24
24
|
export * from './interfaces/script-result.js';
|
|
25
25
|
export * from './interfaces/statement-prepare-options.js';
|
|
26
|
+
export * from './protocol/connection-lost-error.js';
|
|
26
27
|
export * from './protocol/database-error.js';
|
|
27
28
|
export * from './protocol/smart-buffer.js';
|
|
28
29
|
export * from './types.js';
|
package/package.json
CHANGED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A connection whose socket closed without anyone asking it to - the
|
|
3
|
+
* backend terminated by an administrator, a failover, a network fault.
|
|
4
|
+
*
|
|
5
|
+
* Reported on `Connection`'s `'close'` event as its reason, and for a
|
|
6
|
+
* pooled connection on `Pool`'s `'destroy'` (as the second argument) and
|
|
7
|
+
* `'error'`. `Pool` emits `'error'` for this and for a connection that
|
|
8
|
+
* could not be created in the first place, and those want different
|
|
9
|
+
* handling - this class is how a listener tells them apart:
|
|
10
|
+
*
|
|
11
|
+
* ```ts
|
|
12
|
+
* pool.on('error', err => {
|
|
13
|
+
* if (err.code === '08006') log.warn('pooled connection lost', err.processID);
|
|
14
|
+
* else log.error('could not open a connection', err);
|
|
15
|
+
* });
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* `code` is there so that branching does not have to mean `instanceof`,
|
|
19
|
+
* which fails across duplicated copies of the package and is awkward from
|
|
20
|
+
* JavaScript. It reads the same way `DatabaseError.code` does - a
|
|
21
|
+
* SQLSTATE - and `08006 connection_failure` is the one this is. The server
|
|
22
|
+
* never sends it: it cannot report that its own connection went away, so
|
|
23
|
+
* there is no ambiguity about where a code of this value came from.
|
|
24
|
+
*
|
|
25
|
+
* The message deliberately says nothing else: it matches what other
|
|
26
|
+
* drivers report for the same event, so code that branches on it keeps
|
|
27
|
+
* working. What happened to *this* connection is on the object instead -
|
|
28
|
+
* `processID` names the backend, and `cause` carries the socket error
|
|
29
|
+
* when there was one (a clean FIN leaves none).
|
|
30
|
+
*/
|
|
31
|
+
export declare class ConnectionLostError extends Error {
|
|
32
|
+
/**
|
|
33
|
+
* SQLSTATE 08006, `connection_failure`. Synthesized here rather than
|
|
34
|
+
* received - see the class doc.
|
|
35
|
+
*/
|
|
36
|
+
readonly code: string;
|
|
37
|
+
/** The backend's process id, as it was before the connection reset. */
|
|
38
|
+
readonly processID?: number;
|
|
39
|
+
constructor(processID?: number, cause?: unknown);
|
|
40
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A connection whose socket closed without anyone asking it to - the
|
|
3
|
+
* backend terminated by an administrator, a failover, a network fault.
|
|
4
|
+
*
|
|
5
|
+
* Reported on `Connection`'s `'close'` event as its reason, and for a
|
|
6
|
+
* pooled connection on `Pool`'s `'destroy'` (as the second argument) and
|
|
7
|
+
* `'error'`. `Pool` emits `'error'` for this and for a connection that
|
|
8
|
+
* could not be created in the first place, and those want different
|
|
9
|
+
* handling - this class is how a listener tells them apart:
|
|
10
|
+
*
|
|
11
|
+
* ```ts
|
|
12
|
+
* pool.on('error', err => {
|
|
13
|
+
* if (err.code === '08006') log.warn('pooled connection lost', err.processID);
|
|
14
|
+
* else log.error('could not open a connection', err);
|
|
15
|
+
* });
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* `code` is there so that branching does not have to mean `instanceof`,
|
|
19
|
+
* which fails across duplicated copies of the package and is awkward from
|
|
20
|
+
* JavaScript. It reads the same way `DatabaseError.code` does - a
|
|
21
|
+
* SQLSTATE - and `08006 connection_failure` is the one this is. The server
|
|
22
|
+
* never sends it: it cannot report that its own connection went away, so
|
|
23
|
+
* there is no ambiguity about where a code of this value came from.
|
|
24
|
+
*
|
|
25
|
+
* The message deliberately says nothing else: it matches what other
|
|
26
|
+
* drivers report for the same event, so code that branches on it keeps
|
|
27
|
+
* working. What happened to *this* connection is on the object instead -
|
|
28
|
+
* `processID` names the backend, and `cause` carries the socket error
|
|
29
|
+
* when there was one (a clean FIN leaves none).
|
|
30
|
+
*/
|
|
31
|
+
export class ConnectionLostError extends Error {
|
|
32
|
+
/**
|
|
33
|
+
* SQLSTATE 08006, `connection_failure`. Synthesized here rather than
|
|
34
|
+
* received - see the class doc.
|
|
35
|
+
*/
|
|
36
|
+
code = '08006';
|
|
37
|
+
/** The backend's process id, as it was before the connection reset. */
|
|
38
|
+
processID;
|
|
39
|
+
constructor(processID, cause) {
|
|
40
|
+
super('Connection terminated unexpectedly');
|
|
41
|
+
this.name = 'ConnectionLostError';
|
|
42
|
+
this.processID = processID;
|
|
43
|
+
// Assigned rather than passed to the constructor, as abortError()
|
|
44
|
+
// does - same reason, the option is newer than this package's floor.
|
|
45
|
+
if (cause !== undefined) {
|
|
46
|
+
try {
|
|
47
|
+
this.cause = cause;
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
/* c8 ignore next */
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
package/protocol/pg-socket.d.ts
CHANGED
|
@@ -13,6 +13,12 @@ export interface SocketError extends Error {
|
|
|
13
13
|
export declare class PgSocket extends SafeEventEmitter {
|
|
14
14
|
options: ConnectionConfiguration;
|
|
15
15
|
private _state;
|
|
16
|
+
/**
|
|
17
|
+
* The socket-level error that is about to end this connection, kept so
|
|
18
|
+
* the 'close' that follows can say what happened instead of arriving
|
|
19
|
+
* bare. Cleared by _reset().
|
|
20
|
+
*/
|
|
21
|
+
private _closeReason?;
|
|
16
22
|
private _socket?;
|
|
17
23
|
private _backend;
|
|
18
24
|
private _frontend;
|
|
@@ -282,6 +288,7 @@ export declare class PgSocket extends SafeEventEmitter {
|
|
|
282
288
|
/** Accepts this server, or drops it and moves to the next candidate. */
|
|
283
289
|
protected _applySessionAttrs(wanted: string, standby: boolean, readOnly: boolean): void;
|
|
284
290
|
protected _handleClose(): void;
|
|
291
|
+
private _buildCloseReason;
|
|
285
292
|
protected _handleError(err: unknown): void;
|
|
286
293
|
/** Rejects and clears every still-open capture, e.g. on close/error. */
|
|
287
294
|
private _failPendingCaptures;
|
package/protocol/pg-socket.js
CHANGED
|
@@ -8,6 +8,7 @@ import { ConnectionState } from '../constants.js';
|
|
|
8
8
|
import { SafeEventEmitter } from '../safe-event-emitter.js';
|
|
9
9
|
import { Backend } from './backend.js';
|
|
10
10
|
import { signatureHashOfCertificate } from './cert-signature.js';
|
|
11
|
+
import { ConnectionLostError } from './connection-lost-error.js';
|
|
11
12
|
import { DatabaseError } from './database-error.js';
|
|
12
13
|
import { Frontend } from './frontend.js';
|
|
13
14
|
import { Protocol } from './protocol.js';
|
|
@@ -17,6 +18,12 @@ const COMMAND_RESULT_PATTERN = /^([^\d]+)(?: (\d+)(?: (\d+))?)?$/;
|
|
|
17
18
|
export class PgSocket extends SafeEventEmitter {
|
|
18
19
|
options;
|
|
19
20
|
_state = ConnectionState.CLOSED;
|
|
21
|
+
/**
|
|
22
|
+
* The socket-level error that is about to end this connection, kept so
|
|
23
|
+
* the 'close' that follows can say what happened instead of arriving
|
|
24
|
+
* bare. Cleared by _reset().
|
|
25
|
+
*/
|
|
26
|
+
_closeReason;
|
|
20
27
|
_socket;
|
|
21
28
|
_backend = new Backend();
|
|
22
29
|
_frontend;
|
|
@@ -532,6 +539,7 @@ export class PgSocket extends SafeEventEmitter {
|
|
|
532
539
|
this._socket.removeAllListeners('close');
|
|
533
540
|
}
|
|
534
541
|
_reset() {
|
|
542
|
+
this._closeReason = undefined;
|
|
535
543
|
this._backend.reset();
|
|
536
544
|
this._sessionParameters = {};
|
|
537
545
|
this._processID = undefined;
|
|
@@ -627,14 +635,30 @@ export class PgSocket extends SafeEventEmitter {
|
|
|
627
635
|
this._connectToHost();
|
|
628
636
|
}
|
|
629
637
|
_handleClose() {
|
|
638
|
+
// close() is the only thing that sets CLOSING, so anything else here
|
|
639
|
+
// is the far end going away on its own - an admin terminating the
|
|
640
|
+
// backend, a failover, a network fault. Built before _reset() clears
|
|
641
|
+
// the process id, which is the one detail that makes the report
|
|
642
|
+
// actionable, and carrying the socket error as its cause when there
|
|
643
|
+
// was one (a bare FIN leaves none).
|
|
644
|
+
const reason = this._state === ConnectionState.CLOSING
|
|
645
|
+
? undefined
|
|
646
|
+
: this._buildCloseReason();
|
|
630
647
|
this._failPendingCaptures(new Error('Connection closed'));
|
|
631
648
|
this._reset();
|
|
632
649
|
this._socket = undefined;
|
|
633
650
|
this._state = ConnectionState.CLOSED;
|
|
634
|
-
this.emit('close');
|
|
651
|
+
this.emit('close', reason);
|
|
652
|
+
}
|
|
653
|
+
_buildCloseReason() {
|
|
654
|
+
return new ConnectionLostError(this._processID, this._closeReason);
|
|
635
655
|
}
|
|
636
656
|
_handleError(err) {
|
|
637
|
-
|
|
657
|
+
// Kept for the 'close' that follows: the socket reports the cause
|
|
658
|
+
// here and the fact of closing separately, and only the pair together
|
|
659
|
+
// says what actually happened.
|
|
660
|
+
this._closeReason = err instanceof Error ? err : new Error(String(err));
|
|
661
|
+
this._failPendingCaptures(this._closeReason);
|
|
638
662
|
if (this._state !== ConnectionState.READY) {
|
|
639
663
|
this._socket?.end();
|
|
640
664
|
}
|