postgrejs 2.23.0 → 3.0.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 +162 -1
- package/LICENSE +26 -17
- package/README.md +195 -175
- package/connection/connection.d.ts +152 -13
- package/connection/connection.js +296 -49
- package/connection/copy-stream.d.ts +88 -0
- package/connection/copy-stream.js +244 -0
- package/connection/cursor.js +20 -13
- package/connection/intl-connection.d.ts +68 -3
- package/connection/intl-connection.js +414 -57
- package/connection/large-object.d.ts +65 -0
- package/connection/large-object.js +151 -0
- package/connection/logical-replication.d.ts +121 -0
- package/connection/logical-replication.js +279 -0
- package/connection/pool.d.ts +61 -4
- package/connection/pool.js +177 -29
- package/connection/portal.d.ts +18 -4
- package/connection/portal.js +53 -59
- package/connection/prepared-statement.d.ts +22 -1
- package/connection/prepared-statement.js +117 -97
- package/data-type-map.d.ts +2 -2
- package/data-type-map.js +4 -2
- package/data-types/bool-type.js +14 -10
- package/data-types/box-type.js +15 -8
- package/data-types/bytea-type.js +9 -5
- package/data-types/char-type.js +11 -5
- package/data-types/circle-type.js +15 -7
- package/data-types/date-type.js +25 -19
- package/data-types/float4-type.js +10 -3
- package/data-types/float8-type.js +12 -3
- package/data-types/int2-type.js +9 -4
- package/data-types/int2-vector-type.js +10 -4
- package/data-types/int4-type.js +11 -5
- package/data-types/int8-type.js +22 -7
- package/data-types/json-type.js +17 -8
- package/data-types/jsonb-type.js +20 -14
- package/data-types/lseg-type.js +16 -8
- package/data-types/numeric-type.d.ts +8 -0
- package/data-types/numeric-type.js +112 -8
- package/data-types/oid-type.d.ts +0 -1
- package/data-types/oid-type.js +9 -10
- package/data-types/oid-vector-type.d.ts +9 -0
- package/data-types/oid-vector-type.js +51 -0
- package/data-types/point-type.js +14 -6
- package/data-types/time-type.js +20 -13
- package/data-types/timestamp-type.js +32 -25
- package/data-types/timestamptz-type.d.ts +1 -1
- package/data-types/timestamptz-type.js +34 -21
- package/data-types/uuid-type.js +21 -11
- package/data-types/varchar-type.js +16 -4
- package/index.d.ts +5 -0
- package/index.js +5 -0
- package/interfaces/data-type.d.ts +5 -3
- package/interfaces/database-connection-params.d.ts +95 -0
- package/interfaces/query-options.d.ts +17 -0
- package/interfaces/script-execute-options.d.ts +17 -0
- package/package.json +3 -4
- package/protocol/backend.d.ts +6 -1
- package/protocol/backend.js +70 -45
- package/protocol/buffer-reader.d.ts +0 -2
- package/protocol/buffer-reader.js +3 -10
- package/protocol/cert-signature.d.ts +12 -0
- package/protocol/cert-signature.js +114 -0
- package/protocol/database-error.js +1 -0
- package/protocol/frontend.d.ts +16 -0
- package/protocol/frontend.js +58 -6
- package/protocol/pg-socket.d.ts +221 -10
- package/protocol/pg-socket.js +539 -102
- package/protocol/pgoutput.d.ts +76 -0
- package/protocol/pgoutput.js +155 -0
- package/protocol/protocol.d.ts +3 -4
- package/protocol/protocol.js +1 -1
- package/protocol/sasl.d.ts +11 -1
- package/protocol/sasl.js +35 -4
- package/protocol/smart-buffer.d.ts +0 -2
- package/protocol/smart-buffer.js +11 -16
- package/types.d.ts +5 -3
- package/util/abort-signal.d.ts +24 -0
- package/util/abort-signal.js +63 -0
- package/util/array-calculatedim.js +3 -1
- package/util/bigint-methods.js +1 -1
- package/util/connection-config.js +77 -3
- package/util/decode-binaryarray.d.ts +1 -1
- package/util/decode-binaryarray.js +15 -6
- package/util/encode-binaryarray.js +7 -3
- package/util/escape-identifier.d.ts +14 -0
- package/util/escape-identifier.js +32 -0
- package/util/escape-literal.js +3 -0
- package/util/fast-parseint.d.ts +1 -0
- package/util/fast-parseint.js +12 -17
- package/util/format-datetime.d.ts +10 -0
- package/util/format-datetime.js +71 -0
- package/util/get-parsers.js +31 -10
- package/util/parse-array.js +2 -2
- package/util/parse-bytea.d.ts +2 -0
- package/util/parse-bytea.js +83 -0
- package/util/parse-datetime.d.ts +3 -1
- package/util/parse-datetime.js +86 -17
- package/util/parse-row.d.ts +3 -1
- package/util/parse-row.js +60 -5
- package/util/parse-time.js +8 -1
- package/util/sql-tag.d.ts +60 -0
- package/util/sql-tag.js +202 -0
- package/util/starts-transaction.d.ts +38 -0
- package/util/starts-transaction.js +148 -0
- package/util/stringify-arrayliteral.js +13 -1
- package/util/wrap-row-description.js +13 -4
- package/util/convert-row-to-object.d.ts +0 -2
- package/util/convert-row-to-object.js +0 -9
package/protocol/pg-socket.js
CHANGED
|
@@ -2,10 +2,12 @@ import crypto from 'node:crypto';
|
|
|
2
2
|
import net from 'node:net';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import tls from 'node:tls';
|
|
5
|
+
import DoublyLinked from 'doublylinked';
|
|
5
6
|
import promisify from 'putil-promisify';
|
|
6
7
|
import { ConnectionState } from '../constants.js';
|
|
7
8
|
import { SafeEventEmitter } from '../safe-event-emitter.js';
|
|
8
9
|
import { Backend } from './backend.js';
|
|
10
|
+
import { signatureHashOfCertificate } from './cert-signature.js';
|
|
9
11
|
import { DatabaseError } from './database-error.js';
|
|
10
12
|
import { Frontend } from './frontend.js';
|
|
11
13
|
import { Protocol } from './protocol.js';
|
|
@@ -22,6 +24,13 @@ export class PgSocket extends SafeEventEmitter {
|
|
|
22
24
|
_saslSession;
|
|
23
25
|
_processID;
|
|
24
26
|
_secretKey;
|
|
27
|
+
_captureQueue = new DoublyLinked();
|
|
28
|
+
_pendingWrites = [];
|
|
29
|
+
_flushScheduled = false;
|
|
30
|
+
_hosts = [];
|
|
31
|
+
_hostIndex = 0;
|
|
32
|
+
_sessionAttrsChecked = false;
|
|
33
|
+
_standbyState;
|
|
25
34
|
constructor(options) {
|
|
26
35
|
super();
|
|
27
36
|
this.options = options;
|
|
@@ -45,8 +54,17 @@ export class PgSocket extends SafeEventEmitter {
|
|
|
45
54
|
connect() {
|
|
46
55
|
if (this._socket)
|
|
47
56
|
return;
|
|
48
|
-
this._state = ConnectionState.CONNECTING;
|
|
49
57
|
const options = this.options;
|
|
58
|
+
this._hosts = options.hosts?.length
|
|
59
|
+
? options.hosts
|
|
60
|
+
: [{ host: options.host || 'localhost', port: options.port }];
|
|
61
|
+
this._hostIndex = 0;
|
|
62
|
+
this._connectToHost();
|
|
63
|
+
}
|
|
64
|
+
_connectToHost() {
|
|
65
|
+
this._sessionAttrsChecked = false;
|
|
66
|
+
this._state = ConnectionState.CONNECTING;
|
|
67
|
+
const target = this._hosts[this._hostIndex];
|
|
50
68
|
const socket = (this._socket = new net.Socket());
|
|
51
69
|
const errorHandler = (err) => {
|
|
52
70
|
this._state = ConnectionState.CLOSED;
|
|
@@ -54,49 +72,128 @@ export class PgSocket extends SafeEventEmitter {
|
|
|
54
72
|
this._reset();
|
|
55
73
|
socket.destroy();
|
|
56
74
|
this._socket = undefined;
|
|
75
|
+
if (this._hostIndex + 1 < this._hosts.length) {
|
|
76
|
+
this._hostIndex++;
|
|
77
|
+
this._connectToHost();
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
57
80
|
this.emit('error', err);
|
|
58
81
|
};
|
|
59
82
|
const connectHandler = () => {
|
|
60
83
|
socket.setTimeout(0);
|
|
61
84
|
if (this.options.keepAlive || this.options.keepAlive == null)
|
|
62
85
|
socket.setKeepAlive(true);
|
|
63
|
-
|
|
64
|
-
|
|
86
|
+
this._negotiateTls(socket, target, readySocket => {
|
|
87
|
+
this._socket = readySocket;
|
|
65
88
|
this._removeListeners();
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
if (options.host && net.isIP(options.host) === 0)
|
|
69
|
-
tslOptions.servername = options.host;
|
|
70
|
-
const tlsSocket = (this._socket = tls.connect(tslOptions));
|
|
71
|
-
tlsSocket.once('error', errorHandler);
|
|
72
|
-
tlsSocket.once('secureConnect', () => {
|
|
73
|
-
this._removeListeners();
|
|
74
|
-
this._handleConnect();
|
|
75
|
-
});
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
if (x.toString() === 'N') {
|
|
79
|
-
if (options.requireSSL) {
|
|
80
|
-
return errorHandler(new Error('Server does not support SSL connections'));
|
|
81
|
-
}
|
|
82
|
-
this._removeListeners();
|
|
83
|
-
this._handleConnect();
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
return errorHandler(new Error('There was an error establishing an SSL connection'));
|
|
87
|
-
});
|
|
89
|
+
this._handleConnect();
|
|
90
|
+
}, errorHandler);
|
|
88
91
|
};
|
|
89
92
|
socket.setNoDelay(true);
|
|
90
|
-
socket.setTimeout(options.connectTimeoutMs || 30000, () => errorHandler(new Error('Connection timed out')));
|
|
93
|
+
socket.setTimeout(this.options.connectTimeoutMs || 30000, () => errorHandler(new Error('Connection timed out')));
|
|
91
94
|
socket.once('error', errorHandler);
|
|
92
95
|
socket.once('connect', connectHandler);
|
|
93
96
|
this.emit('connecting');
|
|
94
|
-
const port =
|
|
95
|
-
if (
|
|
96
|
-
socket.connect(path.join(
|
|
97
|
+
const port = target.port || DEFAULT_PORT_NUMBER;
|
|
98
|
+
if (target.host && target.host.startsWith('/')) {
|
|
99
|
+
socket.connect(path.join(target.host, '/.s.PGSQL.' + port));
|
|
97
100
|
}
|
|
98
101
|
else
|
|
99
|
-
socket.connect(
|
|
102
|
+
socket.connect(port, target.host || 'localhost');
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Negotiates TLS on an already TCP-connected `socket`, if configured -
|
|
106
|
+
* shared between the long-lived connection above (_connectToHost) and
|
|
107
|
+
* cancel() below, which needs the exact same host/TLS handling for its
|
|
108
|
+
* own short-lived socket, just without ever sending a StartupMessage
|
|
109
|
+
* afterwards. Resolves `onReady` with whichever socket ends up ready to
|
|
110
|
+
* speak the PostgreSQL protocol: the plain one, or its TLS upgrade.
|
|
111
|
+
*/
|
|
112
|
+
_negotiateTls(socket, target, onReady, onError) {
|
|
113
|
+
const options = this.options;
|
|
114
|
+
const startTls = () => {
|
|
115
|
+
const tlsOptions = { ...options.ssl, socket };
|
|
116
|
+
if (target.host && net.isIP(target.host) === 0)
|
|
117
|
+
tlsOptions.servername = target.host;
|
|
118
|
+
if (options.sslNegotiation === 'direct')
|
|
119
|
+
tlsOptions.ALPNProtocols = ['postgresql'];
|
|
120
|
+
const tlsSocket = tls.connect(tlsOptions);
|
|
121
|
+
tlsSocket.once('error', onError);
|
|
122
|
+
tlsSocket.once('secureConnect', () => onReady(tlsSocket));
|
|
123
|
+
};
|
|
124
|
+
const wantsSSL = !!options.ssl ||
|
|
125
|
+
!!options.requireSSL ||
|
|
126
|
+
options.sslNegotiation === 'direct';
|
|
127
|
+
if (!wantsSSL)
|
|
128
|
+
return onReady(socket);
|
|
129
|
+
if (options.sslNegotiation === 'direct') {
|
|
130
|
+
// Straight into the handshake: no SSLRequest, nothing in the clear.
|
|
131
|
+
return startTls();
|
|
132
|
+
}
|
|
133
|
+
socket.write(this._frontend.getSSLRequestMessage());
|
|
134
|
+
socket.once('data', x => {
|
|
135
|
+
const command = x.toString();
|
|
136
|
+
if (command === 'S')
|
|
137
|
+
return startTls();
|
|
138
|
+
if (command === 'N') {
|
|
139
|
+
if (options.requireSSL) {
|
|
140
|
+
return onError(new Error('Server does not support SSL connections'));
|
|
141
|
+
}
|
|
142
|
+
return onReady(socket);
|
|
143
|
+
}
|
|
144
|
+
return onError(new Error('There was an error establishing an SSL connection'));
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Asks the server to cancel whatever this session is currently running.
|
|
149
|
+
*
|
|
150
|
+
* Opens its own short-lived connection and closes it again: a backend busy
|
|
151
|
+
* with a query is not reading its own socket, so the request cannot travel
|
|
152
|
+
* down the connection it is meant to interrupt. The server answers nothing
|
|
153
|
+
* - it either finds a matching session and signals it or does not - so
|
|
154
|
+
* this resolves once the bytes are out, and the cancelled query reports
|
|
155
|
+
* the outcome itself, as an ordinary error on its own connection.
|
|
156
|
+
*
|
|
157
|
+
* No StartupMessage or authentication is ever sent here - the
|
|
158
|
+
* CancelRequest is the only message this socket carries, and it is
|
|
159
|
+
* self-authorized by the processID/secretKey pair BackendKeyData handed
|
|
160
|
+
* out to the session being cancelled, not by a separate login. TLS is
|
|
161
|
+
* still negotiated via _negotiateTls() when configured, because a
|
|
162
|
+
* `hostssl` pg_hba.conf rule can reject a plaintext connection outright
|
|
163
|
+
* before the server ever reads what request it carries - and the target
|
|
164
|
+
* is `_hosts[_hostIndex]`, the host this session actually ended up
|
|
165
|
+
* connected to, not `options.host`, which after a multi-host failover may
|
|
166
|
+
* name a candidate this session never used.
|
|
167
|
+
*/
|
|
168
|
+
cancel() {
|
|
169
|
+
const processID = this._processID;
|
|
170
|
+
const secretKey = this._secretKey;
|
|
171
|
+
// Nothing to cancel before the session is established.
|
|
172
|
+
if (processID == null || secretKey == null)
|
|
173
|
+
return Promise.resolve();
|
|
174
|
+
const data = this._frontend.getCancelRequestMessage(processID, secretKey);
|
|
175
|
+
const target = this._hosts[this._hostIndex];
|
|
176
|
+
return new Promise((resolve, reject) => {
|
|
177
|
+
const socket = new net.Socket();
|
|
178
|
+
const fail = (err) => {
|
|
179
|
+
socket.destroy();
|
|
180
|
+
reject(err);
|
|
181
|
+
};
|
|
182
|
+
const send = (readySocket) => {
|
|
183
|
+
readySocket.end(data, () => {
|
|
184
|
+
readySocket.destroy();
|
|
185
|
+
resolve();
|
|
186
|
+
});
|
|
187
|
+
};
|
|
188
|
+
socket.setNoDelay(true);
|
|
189
|
+
socket.once('error', fail);
|
|
190
|
+
socket.once('connect', () => this._negotiateTls(socket, target, send, fail));
|
|
191
|
+
const port = target.port || DEFAULT_PORT_NUMBER;
|
|
192
|
+
if (target.host && target.host.startsWith('/'))
|
|
193
|
+
socket.connect(path.join(target.host, '/.s.PGSQL.' + port));
|
|
194
|
+
else
|
|
195
|
+
socket.connect(port, target.host || 'localhost');
|
|
196
|
+
});
|
|
100
197
|
}
|
|
101
198
|
close() {
|
|
102
199
|
if (!this._socket || this._socket.destroyed) {
|
|
@@ -113,35 +210,150 @@ export class PgSocket extends SafeEventEmitter {
|
|
|
113
210
|
socket.once('close', () => this._handleClose());
|
|
114
211
|
socket.destroy();
|
|
115
212
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
this.emit('debug', { location: 'PgSocket.sendParseMessage', args });
|
|
119
|
-
this._send(this._frontend.getParseMessage(args), cb);
|
|
213
|
+
sendExecuteMessage(args, cb) {
|
|
214
|
+
return this._sendAndCapture(this._frontend.getExecuteMessage(args), cb, 'sendExecuteMessage', args);
|
|
120
215
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
this.emit('debug', { location: 'PgSocket.sendBindMessage', args });
|
|
124
|
-
this._send(this._frontend.getBindMessage(args), cb);
|
|
216
|
+
sendCloseMessage(args, cb) {
|
|
217
|
+
return this._sendAndCapture(this._frontend.getCloseMessage(args), cb, 'sendCloseMessage', args);
|
|
125
218
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
219
|
+
/**
|
|
220
|
+
* Sends Parse+Bind+Describe+Execute+Sync as a single write with a single
|
|
221
|
+
* FIFO capture entry, instead of 5 separate sendXMessage() round trips -
|
|
222
|
+
* the one-shot Extended Query fast path (IntlConnection.queryOnce()).
|
|
223
|
+
* Callers pass unnamed statement/portal (omit statement/portal/name) so
|
|
224
|
+
* no Close message is needed at all: PostgreSQL auto-clears an unnamed
|
|
225
|
+
* statement/portal at the next Parse/Bind referencing the unnamed name.
|
|
226
|
+
*/
|
|
227
|
+
sendExtendedQueryMessages(args, cb) {
|
|
228
|
+
const data = [
|
|
229
|
+
this._frontend.getParseMessage(args.parse),
|
|
230
|
+
this._frontend.getBindMessage(args.bind),
|
|
231
|
+
this._frontend.getDescribeMessage(args.describe),
|
|
232
|
+
this._frontend.getExecuteMessage(args.execute),
|
|
233
|
+
this._frontend.getSyncMessage(),
|
|
234
|
+
];
|
|
235
|
+
return this._sendAndCapture(data, cb, 'sendExtendedQueryMessages', args);
|
|
130
236
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
237
|
+
/**
|
|
238
|
+
* Parse + Describe(statement) + Sync as a single round trip - used by
|
|
239
|
+
* PreparedStatement.prepare() instead of a separate awaited Parse then a
|
|
240
|
+
* separate awaited Sync, and fetches the RowDescription/NoData in the
|
|
241
|
+
* same round trip so PreparedStatement can cache it instead of every
|
|
242
|
+
* later execute() re-Describing its own portal.
|
|
243
|
+
*/
|
|
244
|
+
sendPrepareMessages(args, cb) {
|
|
245
|
+
const data = [
|
|
246
|
+
this._frontend.getParseMessage(args.parse),
|
|
247
|
+
this._frontend.getDescribeMessage(args.describe),
|
|
248
|
+
this._frontend.getSyncMessage(),
|
|
249
|
+
];
|
|
250
|
+
return this._sendAndCapture(data, cb, 'sendPrepareMessages', args);
|
|
135
251
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
252
|
+
/**
|
|
253
|
+
* Bind + Execute + Sync as a single round trip, against an UNNAMED
|
|
254
|
+
* portal (args.bind.portal/args.execute.portal left unset) - used by
|
|
255
|
+
* PreparedStatement._execute() to reuse an already-prepared (named)
|
|
256
|
+
* statement without the per-call Describe/Close that Portal's own
|
|
257
|
+
* bind()/retrieveFields()/execute()/close() sequence needs for a
|
|
258
|
+
* freshly-created named portal. No Close message is needed: PostgreSQL
|
|
259
|
+
* auto-clears an unnamed portal at the next Bind naming it.
|
|
260
|
+
*/
|
|
261
|
+
sendBindExecuteMessages(args, cb) {
|
|
262
|
+
const data = [
|
|
263
|
+
this._frontend.getBindMessage(args.bind),
|
|
264
|
+
this._frontend.getExecuteMessage(args.execute),
|
|
265
|
+
this._frontend.getSyncMessage(),
|
|
266
|
+
];
|
|
267
|
+
return this._sendAndCapture(data, cb, 'sendBindExecuteMessages', args);
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Bind + Describe(portal) + Flush as a single round trip (NOT Sync - see
|
|
271
|
+
* Portal.bindAndRetrieveFields()'s doc comment for why a Sync here would
|
|
272
|
+
* be unsafe: it would commit an implicit/autocommit transaction and
|
|
273
|
+
* destroy the just-bound named portal before any fetch() could use it).
|
|
274
|
+
* Used by PreparedStatement._execute()'s cursor branch instead of two
|
|
275
|
+
* separately-awaited Portal.bind()/retrieveFields() round trips.
|
|
276
|
+
*/
|
|
277
|
+
sendBindDescribeMessages(args, cb) {
|
|
278
|
+
const data = [
|
|
279
|
+
this._frontend.getBindMessage(args.bind),
|
|
280
|
+
this._frontend.getDescribeMessage(args.describe),
|
|
281
|
+
this._frontend.getFlushMessage(),
|
|
282
|
+
];
|
|
283
|
+
return this._sendAndCapture(data, cb, 'sendBindDescribeMessages', args);
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Close + Sync under ONE capture, rather than a Close capture followed by
|
|
287
|
+
* a separate Sync capture. That split is unsafe after an ErrorResponse:
|
|
288
|
+
* the server then skips every message until Sync, so the only reply is
|
|
289
|
+
* the Sync's ReadyForQuery - which the FIFO hands to the *Close* capture
|
|
290
|
+
* (the older entry), making it fail with "unexpected response message
|
|
291
|
+
* (Z)" and leaving the Sync capture orphaned, to silently swallow the
|
|
292
|
+
* first message of whatever request comes next. With one capture,
|
|
293
|
+
* ReadyForQuery is simply this request's end marker whether or not the
|
|
294
|
+
* Close itself was honoured.
|
|
295
|
+
*/
|
|
296
|
+
sendCloseAndSyncMessages(args, cb) {
|
|
297
|
+
const data = [
|
|
298
|
+
this._frontend.getCloseMessage(args),
|
|
299
|
+
this._frontend.getSyncMessage(),
|
|
300
|
+
];
|
|
301
|
+
return this._sendAndCapture(data, cb, 'sendCloseAndSyncMessages', args);
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Close(portal) + Close(statement) + Sync as a single round trip - used
|
|
305
|
+
* by PreparedStatement._maybeCloseWithPortal() when a Cursor's close()
|
|
306
|
+
* brings the owning PreparedStatement's refcount to 0, instead of two
|
|
307
|
+
* separately-batched Close+Sync round trips (Portal.close() then
|
|
308
|
+
* PreparedStatement._close()). Sync here is safe/intended: this is final
|
|
309
|
+
* teardown, no portal needs to survive past it.
|
|
310
|
+
*/
|
|
311
|
+
sendClosePortalAndStatementMessages(args, cb) {
|
|
312
|
+
const data = [
|
|
313
|
+
this._frontend.getCloseMessage(args.portal),
|
|
314
|
+
this._frontend.getCloseMessage(args.statement),
|
|
315
|
+
this._frontend.getSyncMessage(),
|
|
316
|
+
];
|
|
317
|
+
return this._sendAndCapture(data, cb, 'sendClosePortalAndStatementMessages', args);
|
|
140
318
|
}
|
|
141
319
|
sendQueryMessage(sql, cb) {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
320
|
+
return this._sendAndCapture(this._frontend.getQueryMessage(sql), cb, 'sendQueryMessage', sql);
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* COPY IN data. Unlike every other send*() here this pushes nothing onto
|
|
324
|
+
* the capture queue: while a COPY is in progress the server answers no
|
|
325
|
+
* individual CopyData message, so the response still belongs to the
|
|
326
|
+
* Query capture that opened the copy.
|
|
327
|
+
*
|
|
328
|
+
* Returns false when the socket's buffer is full, exactly as
|
|
329
|
+
* net.Socket.write() does - the caller is expected to stop writing until
|
|
330
|
+
* `cb` fires, which is what CopyFromStream hands straight to its own
|
|
331
|
+
* Writable callback.
|
|
332
|
+
*/
|
|
333
|
+
sendCopyData(data, cb) {
|
|
334
|
+
return this._sendCopy(this._frontend.getCopyDataMessage(data), cb);
|
|
335
|
+
}
|
|
336
|
+
/** Ends a COPY IN normally; the server replies CommandComplete. */
|
|
337
|
+
sendCopyDone(cb) {
|
|
338
|
+
return this._sendCopy(this._frontend.getCopyDoneMessage(), cb);
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Aborts a COPY IN. The server discards the copy, reports `message` as an
|
|
342
|
+
* ErrorResponse and returns to its normal state - without this a failed
|
|
343
|
+
* import would leave the connection waiting for data forever.
|
|
344
|
+
*/
|
|
345
|
+
sendCopyFail(message, cb) {
|
|
346
|
+
return this._sendCopy(this._frontend.getCopyFailMessage(message), cb);
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Stops reading from the socket, so a COPY OUT consumer that cannot keep
|
|
350
|
+
* up doesn't buffer the whole export in memory. Safe to call repeatedly.
|
|
351
|
+
*/
|
|
352
|
+
pause() {
|
|
353
|
+
this._socket?.pause();
|
|
354
|
+
}
|
|
355
|
+
resume() {
|
|
356
|
+
this._socket?.resume();
|
|
145
357
|
}
|
|
146
358
|
sendFlushMessage(cb) {
|
|
147
359
|
if (this.listenerCount('debug'))
|
|
@@ -153,46 +365,31 @@ export class PgSocket extends SafeEventEmitter {
|
|
|
153
365
|
this.emit('debug', { location: 'PgSocket.sendTerminateMessage' });
|
|
154
366
|
this._send(this._frontend.getTerminateMessage(), cb);
|
|
155
367
|
}
|
|
156
|
-
sendSyncMessage() {
|
|
157
|
-
|
|
158
|
-
this.emit('debug', { location: 'PgSocket.sendSyncMessage' });
|
|
159
|
-
this._send(this._frontend.getSyncMessage());
|
|
368
|
+
sendSyncMessage(cb) {
|
|
369
|
+
return this._sendAndCapture(this._frontend.getSyncMessage(), cb, 'sendSyncMessage', undefined);
|
|
160
370
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
371
|
+
/**
|
|
372
|
+
* The only way anything outside this class can receive a correlated
|
|
373
|
+
* backend response. Sends `data` and pushes `cb` onto the FIFO capture
|
|
374
|
+
* queue in the same call, so a response can never arrive before its
|
|
375
|
+
* capturer is registered, and a capturer can never be registered without
|
|
376
|
+
* a message actually going out for it - there is no separate, decoupled
|
|
377
|
+
* "start listening" step to race or misuse.
|
|
378
|
+
*/
|
|
379
|
+
_sendAndCapture(data, cb, location, args) {
|
|
380
|
+
if (typeof cb !== 'function')
|
|
381
|
+
throw new TypeError(`${location}() requires a CaptureCallback`);
|
|
382
|
+
if (this.listenerCount('debug'))
|
|
383
|
+
this.emit('debug', { location: `PgSocket.${location}`, args });
|
|
168
384
|
return new Promise((resolve, reject) => {
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
};
|
|
178
|
-
const errorHandler = (err) => {
|
|
179
|
-
this.removeListener('close', closeHandler);
|
|
180
|
-
this.removeListener('message', msgHandler);
|
|
181
|
-
reject(err);
|
|
182
|
-
};
|
|
183
|
-
const closeHandler = () => {
|
|
184
|
-
this.removeListener('error', errorHandler);
|
|
185
|
-
this.removeListener('message', msgHandler);
|
|
186
|
-
reject(new Error('Connection closed'));
|
|
187
|
-
};
|
|
188
|
-
const msgHandler = (code, msg) => {
|
|
189
|
-
const x = callback(code, msg, done);
|
|
190
|
-
if (promisify.isPromise(x))
|
|
191
|
-
x.catch(err => done(err));
|
|
192
|
-
};
|
|
193
|
-
this.once('close', closeHandler);
|
|
194
|
-
this.once('error', errorHandler);
|
|
195
|
-
this.on('message', msgHandler);
|
|
385
|
+
const entry = { callback: cb, resolve, reject };
|
|
386
|
+
this._captureQueue.push(entry);
|
|
387
|
+
if (!this._send(data)) {
|
|
388
|
+
// Nothing else can have run between the push above and here (fully
|
|
389
|
+
// synchronous), so the entry just pushed is still the tail.
|
|
390
|
+
this._captureQueue.pop();
|
|
391
|
+
reject(new Error('Socket is not writable'));
|
|
392
|
+
}
|
|
196
393
|
});
|
|
197
394
|
}
|
|
198
395
|
_removeListeners() {
|
|
@@ -209,6 +406,9 @@ export class PgSocket extends SafeEventEmitter {
|
|
|
209
406
|
this._processID = undefined;
|
|
210
407
|
this._secretKey = undefined;
|
|
211
408
|
this._saslSession = undefined;
|
|
409
|
+
// Anything still queued belonged to the connection being reset - it
|
|
410
|
+
// must never reach a freshly connected socket.
|
|
411
|
+
this._pendingWrites = [];
|
|
212
412
|
}
|
|
213
413
|
_handleConnect() {
|
|
214
414
|
const socket = this._socket;
|
|
@@ -223,20 +423,99 @@ export class PgSocket extends SafeEventEmitter {
|
|
|
223
423
|
user: this.options.user || 'postgres',
|
|
224
424
|
database: this.options.database || '',
|
|
225
425
|
application_name: this.options.applicationName || '',
|
|
426
|
+
...(this.options.replication
|
|
427
|
+
? { replication: this.options.replication }
|
|
428
|
+
: undefined),
|
|
226
429
|
}));
|
|
227
430
|
}
|
|
431
|
+
/**
|
|
432
|
+
* Called once the server is ready. With `targetSessionAttrs` set, the
|
|
433
|
+
* session has to be checked before it is handed over: a server that does
|
|
434
|
+
* not match is dropped and the next candidate tried, which is how
|
|
435
|
+
* `read-write` finds whichever node is currently the primary.
|
|
436
|
+
*/
|
|
437
|
+
_finishConnect() {
|
|
438
|
+
const wanted = this.options.targetSessionAttrs;
|
|
439
|
+
if (!wanted || this._sessionAttrsChecked) {
|
|
440
|
+
this.emit('ready');
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
this._sessionAttrsChecked = true;
|
|
444
|
+
const reported = this._sessionParameters;
|
|
445
|
+
if (reported.in_hot_standby != null &&
|
|
446
|
+
reported.default_transaction_read_only != null) {
|
|
447
|
+
this._applySessionAttrs(wanted, reported.in_hot_standby === 'on', reported.default_transaction_read_only === 'on');
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
this.sendQueryMessage('select pg_catalog.pg_is_in_recovery()::text as a,' +
|
|
451
|
+
" current_setting('transaction_read_only') as b", (code, msg, done) => {
|
|
452
|
+
if (code === Protocol.BackendMessageCode.DataRow) {
|
|
453
|
+
// Two text columns, each length-prefixed within the row buffer.
|
|
454
|
+
const data = msg.data;
|
|
455
|
+
const aLen = data.readInt32BE(0);
|
|
456
|
+
const a = data.toString('utf8', 4, 4 + aLen);
|
|
457
|
+
const bOffset = 4 + aLen;
|
|
458
|
+
const bLen = data.readInt32BE(bOffset);
|
|
459
|
+
const b = data.toString('utf8', bOffset + 4, bOffset + 4 + bLen);
|
|
460
|
+
this._standbyState = { standby: a === 'true', readOnly: b === 'on' };
|
|
461
|
+
}
|
|
462
|
+
else if (code === Protocol.BackendMessageCode.ReadyForQuery) {
|
|
463
|
+
done(undefined);
|
|
464
|
+
const st = this._standbyState;
|
|
465
|
+
this._applySessionAttrs(wanted, !!st?.standby, !!st?.readOnly);
|
|
466
|
+
}
|
|
467
|
+
}).catch(err => this.emit('error', err));
|
|
468
|
+
}
|
|
469
|
+
/** Accepts this server, or drops it and moves to the next candidate. */
|
|
470
|
+
_applySessionAttrs(wanted, standby, readOnly) {
|
|
471
|
+
const rejected = (wanted === 'read-write' && readOnly) ||
|
|
472
|
+
(wanted === 'read-only' && !readOnly) ||
|
|
473
|
+
(wanted === 'primary' && standby) ||
|
|
474
|
+
(wanted === 'standby' && !standby) ||
|
|
475
|
+
// prefer-standby settles for a primary, but only once nothing else
|
|
476
|
+
// is left to try.
|
|
477
|
+
(wanted === 'prefer-standby' &&
|
|
478
|
+
!standby &&
|
|
479
|
+
this._hostIndex + 1 < this._hosts.length);
|
|
480
|
+
if (!rejected) {
|
|
481
|
+
this.emit('ready');
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
const target = this._hosts[this._hostIndex];
|
|
485
|
+
if (this._hostIndex + 1 >= this._hosts.length) {
|
|
486
|
+
this.close();
|
|
487
|
+
this.emit('error', new Error(`No server matched target_session_attrs "${wanted}" ` +
|
|
488
|
+
`(last tried ${target.host}:${target.port ?? this.options.port ?? DEFAULT_PORT_NUMBER})`));
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
this._hostIndex++;
|
|
492
|
+
const socket = this._socket;
|
|
493
|
+
this._removeListeners();
|
|
494
|
+
this._socket = undefined;
|
|
495
|
+
this._reset();
|
|
496
|
+
socket?.destroy();
|
|
497
|
+
this._connectToHost();
|
|
498
|
+
}
|
|
228
499
|
_handleClose() {
|
|
500
|
+
this._failPendingCaptures(new Error('Connection closed'));
|
|
229
501
|
this._reset();
|
|
230
502
|
this._socket = undefined;
|
|
231
503
|
this._state = ConnectionState.CLOSED;
|
|
232
504
|
this.emit('close');
|
|
233
505
|
}
|
|
234
506
|
_handleError(err) {
|
|
507
|
+
this._failPendingCaptures(err instanceof Error ? err : new Error(String(err)));
|
|
235
508
|
if (this._state !== ConnectionState.READY) {
|
|
236
509
|
this._socket?.end();
|
|
237
510
|
}
|
|
238
511
|
this.emit('error', err);
|
|
239
512
|
}
|
|
513
|
+
/** Rejects and clears every still-open capture, e.g. on close/error. */
|
|
514
|
+
_failPendingCaptures(err) {
|
|
515
|
+
const pending = this._captureQueue;
|
|
516
|
+
this._captureQueue = new DoublyLinked();
|
|
517
|
+
pending.forEach(entry => entry.reject(err));
|
|
518
|
+
}
|
|
240
519
|
_handleData(data) {
|
|
241
520
|
this._backend.parse(data, (code, payload) => {
|
|
242
521
|
try {
|
|
@@ -245,7 +524,7 @@ export class PgSocket extends SafeEventEmitter {
|
|
|
245
524
|
this._handleAuthenticationMessage(payload);
|
|
246
525
|
break;
|
|
247
526
|
case Protocol.BackendMessageCode.ErrorResponse:
|
|
248
|
-
this.
|
|
527
|
+
this._dispatch(code, new DatabaseError(payload));
|
|
249
528
|
break;
|
|
250
529
|
case Protocol.BackendMessageCode.NoticeResponse:
|
|
251
530
|
this.emit('notice', payload);
|
|
@@ -262,18 +541,18 @@ export class PgSocket extends SafeEventEmitter {
|
|
|
262
541
|
case Protocol.BackendMessageCode.ReadyForQuery:
|
|
263
542
|
if (this._state !== ConnectionState.READY) {
|
|
264
543
|
this._state = ConnectionState.READY;
|
|
265
|
-
this.
|
|
544
|
+
this._finishConnect();
|
|
266
545
|
}
|
|
267
546
|
else
|
|
268
|
-
this.
|
|
547
|
+
this._dispatch(code, payload);
|
|
269
548
|
break;
|
|
270
549
|
case Protocol.BackendMessageCode.CommandComplete: {
|
|
271
550
|
const msg = this._handleCommandComplete(payload);
|
|
272
|
-
this.
|
|
551
|
+
this._dispatch(code, msg);
|
|
273
552
|
break;
|
|
274
553
|
}
|
|
275
554
|
default:
|
|
276
|
-
this.
|
|
555
|
+
this._dispatch(code, payload);
|
|
277
556
|
}
|
|
278
557
|
}
|
|
279
558
|
catch (e) {
|
|
@@ -281,6 +560,44 @@ export class PgSocket extends SafeEventEmitter {
|
|
|
281
560
|
}
|
|
282
561
|
});
|
|
283
562
|
}
|
|
563
|
+
/**
|
|
564
|
+
* Routes one backend message to the capture at the front of the FIFO -
|
|
565
|
+
* the oldest still-open request, i.e. the one whose turn it is given the
|
|
566
|
+
* order messages were written to the socket. This *is* the request/
|
|
567
|
+
* response correlation, since the wire protocol has no id of its own.
|
|
568
|
+
* The same entry keeps receiving messages (ParseComplete, BindComplete,
|
|
569
|
+
* RowDescription, DataRow*, CommandComplete, ReadyForQuery, ...) until
|
|
570
|
+
* its callback calls `done()`, at which point it is dequeued and the
|
|
571
|
+
* next entry becomes the one messages are routed to.
|
|
572
|
+
*/
|
|
573
|
+
_dispatch(code, payload) {
|
|
574
|
+
const entry = this._captureQueue.head?.value;
|
|
575
|
+
if (!entry) {
|
|
576
|
+
if (payload instanceof DatabaseError) {
|
|
577
|
+
this.emit('error', payload);
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
this.emit('error', new Error(`PgSocket: received message code=${String(code)} with an empty capture queue`));
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
const done = (err, result) => {
|
|
584
|
+
if (this._captureQueue.head?.value !== entry)
|
|
585
|
+
return;
|
|
586
|
+
this._captureQueue.shift();
|
|
587
|
+
if (err)
|
|
588
|
+
entry.reject(err);
|
|
589
|
+
else
|
|
590
|
+
entry.resolve(result);
|
|
591
|
+
};
|
|
592
|
+
try {
|
|
593
|
+
const x = entry.callback(code, payload, done);
|
|
594
|
+
if (promisify.isPromise(x))
|
|
595
|
+
x.catch(err => done(err));
|
|
596
|
+
}
|
|
597
|
+
catch (err) {
|
|
598
|
+
done(err);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
284
601
|
_resolvePassword(cb) {
|
|
285
602
|
(async () => {
|
|
286
603
|
const pass = typeof this.options.password === 'function'
|
|
@@ -310,10 +627,21 @@ export class PgSocket extends SafeEventEmitter {
|
|
|
310
627
|
});
|
|
311
628
|
break;
|
|
312
629
|
case Protocol.AuthenticationMessageKind.SASL: {
|
|
313
|
-
|
|
314
|
-
|
|
630
|
+
const mode = this.options.channelBinding || 'prefer';
|
|
631
|
+
const tlsSocket = mode !== 'disable' && this._socket instanceof tls.TLSSocket
|
|
632
|
+
? this._socket
|
|
633
|
+
: undefined;
|
|
634
|
+
const offersBinding = !!msg.mechanisms?.includes('SCRAM-SHA-256-PLUS');
|
|
635
|
+
const useBinding = !!tlsSocket && offersBinding;
|
|
636
|
+
if (mode === 'require' && !useBinding) {
|
|
637
|
+
throw new Error(tlsSocket
|
|
638
|
+
? 'SASL: channelBinding is "require" but the server does not offer SCRAM-SHA-256-PLUS'
|
|
639
|
+
: 'SASL: channelBinding is "require" but the connection is not using TLS');
|
|
640
|
+
}
|
|
641
|
+
if (!useBinding && !msg.mechanisms?.includes('SCRAM-SHA-256')) {
|
|
642
|
+
throw new Error('SASL: Only mechanisms SCRAM-SHA-256 and SCRAM-SHA-256-PLUS are supported');
|
|
315
643
|
}
|
|
316
|
-
const saslSession = (this._saslSession = SASL.createSession(this.options.user || '', 'SCRAM-SHA-256'));
|
|
644
|
+
const saslSession = (this._saslSession = SASL.createSession(this.options.user || '', useBinding ? 'SCRAM-SHA-256-PLUS' : 'SCRAM-SHA-256', useBinding ? this._channelBindingData(tlsSocket) : undefined, !!tlsSocket));
|
|
317
645
|
this._send(this._frontend.getSASLMessage(saslSession));
|
|
318
646
|
break;
|
|
319
647
|
}
|
|
@@ -337,9 +665,26 @@ export class PgSocket extends SafeEventEmitter {
|
|
|
337
665
|
break;
|
|
338
666
|
}
|
|
339
667
|
default:
|
|
340
|
-
|
|
668
|
+
throw new Error(`Authentication method "${msg.kind}" is not supported. ` +
|
|
669
|
+
'Supported methods are cleartext password, MD5 and SCRAM-SHA-256 ' +
|
|
670
|
+
'(with or without channel binding).');
|
|
341
671
|
}
|
|
342
672
|
}
|
|
673
|
+
/**
|
|
674
|
+
* tls-server-end-point: the server certificate hashed with the algorithm
|
|
675
|
+
* its own signature used (RFC 5929), which is why the DER has to be read
|
|
676
|
+
* rather than one of Node's fixed fingerprints taken.
|
|
677
|
+
*/
|
|
678
|
+
_channelBindingData(socket) {
|
|
679
|
+
const cert = socket.getPeerX509Certificate?.();
|
|
680
|
+
if (!cert)
|
|
681
|
+
throw new Error('SASL: SCRAM-SHA-256-PLUS needs the server certificate, which this connection did not provide');
|
|
682
|
+
const der = cert.raw;
|
|
683
|
+
return crypto
|
|
684
|
+
.createHash(signatureHashOfCertificate(der))
|
|
685
|
+
.update(der)
|
|
686
|
+
.digest();
|
|
687
|
+
}
|
|
343
688
|
_handleParameterStatus(msg) {
|
|
344
689
|
this._sessionParameters[msg.name] = msg.value;
|
|
345
690
|
}
|
|
@@ -360,9 +705,101 @@ export class PgSocket extends SafeEventEmitter {
|
|
|
360
705
|
result.rowCount = parseInt(m[2], 10);
|
|
361
706
|
return result;
|
|
362
707
|
}
|
|
708
|
+
/**
|
|
709
|
+
* Queues `data` for the socket instead of writing it straight through:
|
|
710
|
+
* everything handed to _send() during the same synchronous burst (e.g.
|
|
711
|
+
* many concurrent query() calls fired via Promise.all over one
|
|
712
|
+
* connection) is flushed together on the next tick, corked, so Node
|
|
713
|
+
* turns the whole batch into a single underlying writev rather than one
|
|
714
|
+
* socket write per call. Measured against postgres.js, which coalesces
|
|
715
|
+
* the same way at the application level (its connection.js write()/
|
|
716
|
+
* nextWrite() pair, flushing on a 1KB threshold or setImmediate): 50
|
|
717
|
+
* concurrent prepared executes cost it 3 socket writes where this cost
|
|
718
|
+
* us 50, and that write path was the single largest non-idle item in a
|
|
719
|
+
* CPU profile of the scenario.
|
|
720
|
+
*
|
|
721
|
+
* nextTick rather than setImmediate, deliberately: it still batches a
|
|
722
|
+
* whole synchronous burst, but a lone query doesn't have to wait for the
|
|
723
|
+
* event loop's check phase before its bytes go out - which would trade
|
|
724
|
+
* away the sequential/single-query scenarios to win the concurrent ones.
|
|
725
|
+
*
|
|
726
|
+
* Returns whether the socket could accept the data at all; the write
|
|
727
|
+
* itself is reported through `cb` (and a socket that dies before the
|
|
728
|
+
* flush rejects every pending capture via _failPendingCaptures()).
|
|
729
|
+
*/
|
|
363
730
|
_send(data, cb) {
|
|
364
|
-
if (this._socket
|
|
365
|
-
|
|
731
|
+
if (!this._socket || !this._socket.writable)
|
|
732
|
+
return false;
|
|
733
|
+
if (Array.isArray(data)) {
|
|
734
|
+
const l = data.length;
|
|
735
|
+
for (let i = 0; i < l; i++)
|
|
736
|
+
this._pendingWrites.push({
|
|
737
|
+
data: data[i],
|
|
738
|
+
cb: i === l - 1 ? cb : undefined,
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
else
|
|
742
|
+
this._pendingWrites.push({ data, cb });
|
|
743
|
+
if (this._captureQueue.length > 1) {
|
|
744
|
+
if (!this._flushScheduled) {
|
|
745
|
+
this._flushScheduled = true;
|
|
746
|
+
process.nextTick(this._flushPendingWrites);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
else
|
|
750
|
+
this._flushPendingWrites();
|
|
751
|
+
return true;
|
|
752
|
+
}
|
|
753
|
+
/**
|
|
754
|
+
* Writes COPY bytes straight through instead of queueing them for the
|
|
755
|
+
* next tick like _send() does. Two reasons: during a copy the capture
|
|
756
|
+
* queue holds exactly one entry (the Query that opened it), so _send()'s
|
|
757
|
+
* "batch only when more than one request is in flight" test would never
|
|
758
|
+
* fire and every chunk would be written on its own anyway; and the
|
|
759
|
+
* caller needs write()'s own return value to know when to stop, which a
|
|
760
|
+
* deferred write cannot give it.
|
|
761
|
+
*/
|
|
762
|
+
_sendCopy(data, cb) {
|
|
763
|
+
const socket = this._socket;
|
|
764
|
+
if (!socket || !socket.writable)
|
|
765
|
+
return false;
|
|
766
|
+
this._flushPendingWrites();
|
|
767
|
+
if (!Array.isArray(data))
|
|
768
|
+
return socket.write(data, cb);
|
|
769
|
+
socket.cork();
|
|
770
|
+
try {
|
|
771
|
+
const l = data.length;
|
|
772
|
+
let writable = true;
|
|
773
|
+
let i;
|
|
774
|
+
for (i = 0; i < l; i++)
|
|
775
|
+
writable = socket.write(data[i], i === l - 1 ? cb : undefined);
|
|
776
|
+
return writable;
|
|
777
|
+
}
|
|
778
|
+
finally {
|
|
779
|
+
socket.uncork();
|
|
366
780
|
}
|
|
367
781
|
}
|
|
782
|
+
/** Arrow property, not a method: used as a bare process.nextTick callback. */
|
|
783
|
+
_flushPendingWrites = () => {
|
|
784
|
+
this._flushScheduled = false;
|
|
785
|
+
const pending = this._pendingWrites;
|
|
786
|
+
if (!pending.length)
|
|
787
|
+
return;
|
|
788
|
+
this._pendingWrites = [];
|
|
789
|
+
const socket = this._socket;
|
|
790
|
+
if (!socket || !socket.writable)
|
|
791
|
+
return;
|
|
792
|
+
socket.cork();
|
|
793
|
+
try {
|
|
794
|
+
const l = pending.length;
|
|
795
|
+
let i;
|
|
796
|
+
for (i = 0; i < l; i++) {
|
|
797
|
+
const { data, cb } = pending[i];
|
|
798
|
+
socket.write(data, cb);
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
finally {
|
|
802
|
+
socket.uncork();
|
|
803
|
+
}
|
|
804
|
+
};
|
|
368
805
|
}
|