zet-lib 1.3.40 → 1.3.42

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/lib/Pool.js CHANGED
@@ -1,437 +1,437 @@
1
- 'use strict'
2
- var __createBinding =
3
- (this && this.__createBinding) ||
4
- (Object.create
5
- ? function (o, m, k, k2) {
6
- if (k2 === undefined) k2 = k
7
- var desc = Object.getOwnPropertyDescriptor(m, k)
8
- if (!desc || ('get' in desc ? !m.__esModule : desc.writable || desc.configurable)) {
9
- desc = {
10
- enumerable: true,
11
- get: function () {
12
- return m[k]
13
- },
14
- }
15
- }
16
- Object.defineProperty(o, k2, desc)
17
- }
18
- : function (o, m, k, k2) {
19
- if (k2 === undefined) k2 = k
20
- o[k2] = m[k]
21
- })
22
- var __setModuleDefault =
23
- (this && this.__setModuleDefault) ||
24
- (Object.create
25
- ? function (o, v) {
26
- Object.defineProperty(o, 'default', { enumerable: true, value: v })
27
- }
28
- : function (o, v) {
29
- o['default'] = v
30
- })
31
- var __importStar =
32
- (this && this.__importStar) ||
33
- function (mod) {
34
- if (mod && mod.__esModule) return mod
35
- var result = {}
36
- if (mod != null) for (var k in mod) if (k !== 'default' && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k)
37
- __setModuleDefault(result, mod)
38
- return result
39
- }
40
- Object.defineProperty(exports, '__esModule', { value: true })
41
- exports.Pool = void 0
42
- const events_1 = require('events')
43
- const fs = __importStar(require('fs'))
44
- const path = __importStar(require('path'))
45
- const promises_1 = require('timers-promises')
46
- const pg_1 = require('pg')
47
- function pgToString(value) {
48
- return value.toString()
49
- }
50
- pg_1.types.setTypeParser(1082, pgToString) // date
51
- const uuid_1 = require('uuid')
52
- const ErrorWithCode_1 = require('./ErrorWithCode')
53
- class Pool extends events_1.EventEmitter {
54
- /**
55
- * Gets the number of queued requests waiting for a database connection
56
- */
57
- get waitingCount() {
58
- return this.connectionQueue.length
59
- }
60
- /**
61
- * Gets the number of idle connections
62
- */
63
- get idleCount() {
64
- return this.idleConnections.length
65
- }
66
- /**
67
- * Gets the total number of connections in the pool
68
- */
69
- get totalCount() {
70
- return this.connections.length
71
- }
72
- options
73
- // Internal event emitter used to handle queued connection requests
74
- connectionQueueEventEmitter
75
- connections = []
76
- // Should self order by idle timeout ascending
77
- idleConnections = []
78
- connectionQueue = []
79
- isEnding = false
80
- constructor(options) {
81
- // eslint-disable-next-line constructor-super
82
- super()
83
- const defaultOptions = {
84
- poolSize: 10,
85
- idleTimeoutMillis: 10000,
86
- waitForAvailableConnectionTimeoutMillis: 90000,
87
- connectionTimeoutMillis: 5000,
88
- retryConnectionMaxRetries: 5,
89
- retryConnectionWaitMillis: 100,
90
- retryConnectionErrorCodes: ['ENOTFOUND', 'EAI_AGAIN', 'ERR_PG_CONNECT_TIMEOUT', 'timeout expired'],
91
- reconnectOnDatabaseIsStartingError: true,
92
- waitForDatabaseStartupMillis: 0,
93
- databaseStartupTimeoutMillis: 90000,
94
- reconnectOnReadOnlyTransactionError: true,
95
- waitForReconnectReadOnlyTransactionMillis: 0,
96
- readOnlyTransactionReconnectTimeoutMillis: 90000,
97
- reconnectOnConnectionError: true,
98
- waitForReconnectConnectionMillis: 0,
99
- connectionReconnectTimeoutMillis: 90000,
100
- namedParameterFindRegExp: /@([\w])+\b/g,
101
- getNamedParameterReplaceRegExp(namedParameter) {
102
- // eslint-disable-next-line security/detect-non-literal-regexp
103
- return new RegExp(`@${namedParameter}\\b`, 'gm')
104
- },
105
- getNamedParameterName(namedParameterWithSymbols) {
106
- // Remove leading @ symbol
107
- return namedParameterWithSymbols.substring(1)
108
- },
109
- }
110
- const { ssl, ...otherOptions } = options
111
- this.options = { ...defaultOptions, ...otherOptions }
112
- if (ssl === 'aws-rds') {
113
- this.options.ssl = {
114
- rejectUnauthorized: true,
115
- // eslint-disable-next-line security/detect-non-literal-fs-filename
116
- ca: fs.readFileSync(path.join(__dirname, './certs/rds-global-bundle.pem')),
117
- minVersion: 'TLSv1.2',
118
- }
119
- } else {
120
- this.options.ssl = ssl
121
- }
122
- this.connectionQueueEventEmitter = new events_1.EventEmitter()
123
- }
124
- /**
125
- * Gets a client connection from the pool.
126
- * Note: You must call `.release()` when finished with the client connection object. That will release the connection back to the pool to be used by other requests.
127
- */
128
- async connect() {
129
- if (this.isEnding) {
130
- throw new ErrorWithCode_1.ErrorWithCode('Cannot use pool after calling end() on the pool', 'ERR_PG_CONNECT_POOL_ENDED')
131
- }
132
- const idleConnection = this.idleConnections.shift()
133
- if (idleConnection) {
134
- if (idleConnection.idleTimeoutTimer) {
135
- clearTimeout(idleConnection.idleTimeoutTimer)
136
- }
137
- this.emit('idleConnectionActivated')
138
- return idleConnection
139
- }
140
- const id = (0, uuid_1.v4)()
141
- if (this.connections.length < this.options.poolSize) {
142
- this.connections.push(id)
143
- try {
144
- const connection = await this._createConnection(id)
145
- return connection
146
- } catch (ex) {
147
- // Remove the connection id since we failed to connect
148
- const connectionIndex = this.connections.indexOf(id)
149
- if (connectionIndex > -1) {
150
- this.connections.splice(connectionIndex, 1)
151
- }
152
- throw ex
153
- }
154
- }
155
- this.emit('connectionRequestQueued')
156
- this.connectionQueue.push(id)
157
- let connectionTimeoutTimer = null
158
- return await Promise.race([
159
- new Promise((resolve) => {
160
- this.connectionQueueEventEmitter.on(`connection_${id}`, (client) => {
161
- if (connectionTimeoutTimer) {
162
- clearTimeout(connectionTimeoutTimer)
163
- }
164
- this.connectionQueueEventEmitter.removeAllListeners(`connection_${id}`)
165
- this.emit('connectionRequestDequeued')
166
- resolve(client)
167
- })
168
- }),
169
- (async () => {
170
- connectionTimeoutTimer = await (0, promises_1.setTimeout)(this.options.waitForAvailableConnectionTimeoutMillis)
171
- this.connectionQueueEventEmitter.removeAllListeners(`connection_${id}`)
172
- // Remove this connection attempt from the connection queue
173
- const index = this.connectionQueue.indexOf(id)
174
- if (index > -1) {
175
- this.connectionQueue.splice(index, 1)
176
- }
177
- throw new ErrorWithCode_1.ErrorWithCode('Timed out while waiting for available connection in pool', 'ERR_PG_CONNECT_POOL_CONNECTION_TIMEOUT')
178
- })(),
179
- ])
180
- }
181
- /**
182
- * Gets a connection to the database and executes the specified query. This method will release the connection back to the pool when the query has finished.
183
- * @param {string} text
184
- * @param {object | object[]} values - If an object, keys represent named parameters in the query
185
- */
186
- query(text, values) {
187
- /* eslint-enable @typescript-eslint/no-explicit-any */
188
- if (Array.isArray(values)) {
189
- return this._query(text, values)
190
- }
191
- if (!values || !Object.keys(values).length) {
192
- return this._query(text)
193
- }
194
- // eslint-disable-next-line @typescript-eslint/prefer-regexp-exec
195
- const tokenMatches = text.match(this.options.namedParameterFindRegExp)
196
- if (!tokenMatches) {
197
- throw new ErrorWithCode_1.ErrorWithCode('Did not find named parameters in in the query. Expected named parameter form is @foo', 'ERR_PG_QUERY_NO_NAMED_PARAMETERS')
198
- }
199
- // Get unique token names
200
- // https://stackoverflow.com/a/45886147/3085
201
- const tokens = Array.from(new Set(tokenMatches.map(this.options.getNamedParameterName)))
202
- const missingParameters = []
203
- for (const token of tokens) {
204
- if (!(token in values)) {
205
- missingParameters.push(token)
206
- }
207
- }
208
- if (missingParameters.length) {
209
- throw new ErrorWithCode_1.ErrorWithCode(`Missing query parameter(s): ${missingParameters.join(', ')}`, 'ERR_PG_QUERY_MISSING_QUERY_PARAMETER')
210
- }
211
- let sql = text.slice()
212
- const params = []
213
- let tokenIndex = 1
214
- for (const token of tokens) {
215
- sql = sql.replace(this.options.getNamedParameterReplaceRegExp(token), `$${tokenIndex}`)
216
- params.push(values[token])
217
- tokenIndex += 1
218
- }
219
- return this._query(sql, params)
220
- }
221
- /**
222
- * Drains the pool of all active client connections and prevents additional connections
223
- */
224
- end() {
225
- this.isEnding = true
226
- return this.drainIdleConnections()
227
- }
228
- /**
229
- * Drains the pool of all idle client connections.
230
- */
231
- async drainIdleConnections() {
232
- await Promise.all([...this.idleConnections].map((idleConnection) => this._removeConnection(idleConnection)))
233
- }
234
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
235
- async _query(text, values, reconnectQueryStartTime) {
236
- const connection = await this.connect()
237
- let removeConnection = false
238
- let timeoutError
239
- let connectionError
240
- try {
241
- const results = await connection.query(text, values)
242
- return results
243
- } catch (ex) {
244
- const { message } = ex
245
- if (this.options.reconnectOnReadOnlyTransactionError && /cannot execute [\s\w]+ in a read-only transaction/giu.test(message)) {
246
- timeoutError = ex
247
- removeConnection = true
248
- } else if (this.options.reconnectOnConnectionError && /Client has encountered a connection error and is not queryable/giu.test(message)) {
249
- connectionError = ex
250
- removeConnection = true
251
- } else {
252
- throw ex
253
- }
254
- } finally {
255
- await connection.release(removeConnection)
256
- }
257
- // If we get here, that means that the query was attempted with a read-only connection.
258
- // This can happen when the cluster fails over to a read-replica
259
- if (timeoutError) {
260
- this.emit('queryDeniedForReadOnlyTransaction')
261
- } else if (connectionError) {
262
- // This can happen when a cluster fails over
263
- this.emit('queryDeniedForConnectionError')
264
- }
265
- // Clear all idle connections and try the query again with a fresh connection
266
- await this.drainIdleConnections()
267
- if (!reconnectQueryStartTime) {
268
- // eslint-disable-next-line no-param-reassign
269
- reconnectQueryStartTime = process.hrtime()
270
- }
271
- if (timeoutError && this.options.waitForReconnectReadOnlyTransactionMillis > 0) {
272
- await (0, promises_1.setTimeout)(this.options.waitForReconnectReadOnlyTransactionMillis)
273
- }
274
- if (connectionError && this.options.waitForReconnectConnectionMillis > 0) {
275
- await (0, promises_1.setTimeout)(this.options.waitForReconnectConnectionMillis)
276
- }
277
- const diff = process.hrtime(reconnectQueryStartTime)
278
- const timeSinceLastRun = Number((diff[0] * 1e3 + diff[1] * 1e-6).toFixed(3))
279
- if (timeoutError && timeSinceLastRun > this.options.readOnlyTransactionReconnectTimeoutMillis) {
280
- throw timeoutError
281
- }
282
- if (connectionError && timeSinceLastRun > this.options.connectionReconnectTimeoutMillis) {
283
- throw connectionError
284
- }
285
- const results = await this._query(text, values, reconnectQueryStartTime)
286
- return results
287
- }
288
- /**
289
- * Creates a new client connection to add to the pool
290
- * @param {string} connectionId
291
- * @param {number} [retryAttempt=0]
292
- * @param {bigint} [createConnectionStartTime] - High-resolution time (in nanoseconds) for when the connection was created
293
- * @param {[number,number]} [databaseStartupStartTime] - hrtime when the db was first listed as starting up
294
- */
295
- async _createConnection(connectionId, retryAttempt = 0, createConnectionStartTime = process.hrtime.bigint(), databaseStartupStartTime) {
296
- const client = new pg_1.Client(this.options)
297
- client.uniqueId = connectionId
298
- /**
299
- * Releases the client connection back to the pool, to be used by another query.
300
- *
301
- * @param {boolean} [removeConnection=false]
302
- */
303
- client.release = async (removeConnection = false) => {
304
- if (this.isEnding || removeConnection) {
305
- await this._removeConnection(client)
306
- return
307
- }
308
- const id = this.connectionQueue.shift()
309
- // Return the connection to be used by a queued request
310
- if (id) {
311
- this.connectionQueueEventEmitter.emit(`connection_${id}`, client)
312
- } else if (this.options.idleTimeoutMillis > 0) {
313
- client.idleTimeoutTimer = setTimeout(() => {
314
- // eslint-disable-next-line no-void
315
- void this._removeConnection(client)
316
- }, this.options.idleTimeoutMillis)
317
- this.idleConnections.push(client)
318
- this.emit('connectionIdle')
319
- } else {
320
- await this._removeConnection(client)
321
- }
322
- }
323
- client.errorHandler = (err) => {
324
- // fire and forget, we will always emit the error.
325
- // eslint-disable-next-line no-void
326
- void this._removeConnection(client).finally(() => this.emit('error', err, client))
327
- }
328
- client.on('error', client.errorHandler)
329
- let connectionTimeoutTimer = null
330
- const { connectionTimeoutMillis } = this.options
331
- try {
332
- await Promise.race([
333
- (async function connectClient() {
334
- try {
335
- await client.connect()
336
- } finally {
337
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
338
- if (connectionTimeoutTimer) {
339
- clearTimeout(connectionTimeoutTimer)
340
- }
341
- }
342
- })(),
343
- (async function connectTimeout() {
344
- connectionTimeoutTimer = await (0, promises_1.setTimeout)(connectionTimeoutMillis)
345
- throw new ErrorWithCode_1.ErrorWithCode('Timed out trying to connect to postgres', 'ERR_PG_CONNECT_TIMEOUT')
346
- })(),
347
- ])
348
- this.emit('connectionAddedToPool', {
349
- connectionId,
350
- retryAttempt,
351
- startTime: createConnectionStartTime,
352
- })
353
- } catch (ex) {
354
- const { connection } = client
355
- if (connection) {
356
- // Force a disconnect of the socket, if it exists.
357
- connection.stream.destroy()
358
- }
359
- await client.end()
360
- const { message, code } = ex
361
- let retryConnection = false
362
- if (this.options.retryConnectionMaxRetries) {
363
- if (code) {
364
- retryConnection = this.options.retryConnectionErrorCodes.includes(code)
365
- } else {
366
- for (const errorCode of this.options.retryConnectionErrorCodes) {
367
- if (message.includes(errorCode)) {
368
- retryConnection = true
369
- break
370
- }
371
- }
372
- }
373
- }
374
- if (retryConnection && retryAttempt < this.options.retryConnectionMaxRetries) {
375
- this.emit('retryConnectionOnError')
376
- if (this.options.retryConnectionWaitMillis > 0) {
377
- await (0, promises_1.setTimeout)(this.options.retryConnectionWaitMillis)
378
- }
379
- const connectionAfterRetry = await this._createConnection(connectionId, retryAttempt + 1, createConnectionStartTime, databaseStartupStartTime)
380
- return connectionAfterRetry
381
- }
382
- if (this.options.reconnectOnDatabaseIsStartingError && /the database system is starting up/giu.test(message)) {
383
- this.emit('waitingForDatabaseToStart')
384
- if (!databaseStartupStartTime) {
385
- // eslint-disable-next-line no-param-reassign
386
- databaseStartupStartTime = process.hrtime()
387
- }
388
- if (this.options.waitForDatabaseStartupMillis > 0) {
389
- await (0, promises_1.setTimeout)(this.options.waitForDatabaseStartupMillis)
390
- }
391
- const diff = process.hrtime(databaseStartupStartTime)
392
- const timeSinceFirstConnectAttempt = Number((diff[0] * 1e3 + diff[1] * 1e-6).toFixed(3))
393
- if (timeSinceFirstConnectAttempt > this.options.databaseStartupTimeoutMillis) {
394
- throw ex
395
- }
396
- const connectionAfterRetry = await this._createConnection(connectionId, 0, createConnectionStartTime, databaseStartupStartTime)
397
- return connectionAfterRetry
398
- }
399
- throw ex
400
- }
401
- return client
402
- }
403
- /**
404
- * Removes the client connection from the pool and tries to gracefully shut it down
405
- * @param {PoolClient} client
406
- */
407
- async _removeConnection(client) {
408
- client.removeListener('error', client.errorHandler)
409
- // Ignore any errors when ending the connection
410
- client.on('error', () => {
411
- // NOOP
412
- })
413
- if (client.idleTimeoutTimer) {
414
- clearTimeout(client.idleTimeoutTimer)
415
- }
416
- const idleConnectionIndex = this.idleConnections.findIndex((connection) => connection.uniqueId === client.uniqueId)
417
- if (idleConnectionIndex > -1) {
418
- this.idleConnections.splice(idleConnectionIndex, 1)
419
- this.emit('connectionRemovedFromIdlePool')
420
- }
421
- const connectionIndex = this.connections.indexOf(client.uniqueId)
422
- if (connectionIndex > -1) {
423
- this.connections.splice(connectionIndex, 1)
424
- }
425
- try {
426
- await client.end()
427
- } catch (ex) {
428
- const { message } = ex
429
- if (!/This socket has been ended by the other party/giu.test(message)) {
430
- this.emit('error', ex)
431
- }
432
- }
433
- this.emit('connectionRemovedFromPool')
434
- }
435
- }
436
- exports.Pool = Pool
437
- //# sourceMappingURL=index.js.map
1
+ 'use strict'
2
+ var __createBinding =
3
+ (this && this.__createBinding) ||
4
+ (Object.create
5
+ ? function (o, m, k, k2) {
6
+ if (k2 === undefined) k2 = k
7
+ var desc = Object.getOwnPropertyDescriptor(m, k)
8
+ if (!desc || ('get' in desc ? !m.__esModule : desc.writable || desc.configurable)) {
9
+ desc = {
10
+ enumerable: true,
11
+ get: function () {
12
+ return m[k]
13
+ },
14
+ }
15
+ }
16
+ Object.defineProperty(o, k2, desc)
17
+ }
18
+ : function (o, m, k, k2) {
19
+ if (k2 === undefined) k2 = k
20
+ o[k2] = m[k]
21
+ })
22
+ var __setModuleDefault =
23
+ (this && this.__setModuleDefault) ||
24
+ (Object.create
25
+ ? function (o, v) {
26
+ Object.defineProperty(o, 'default', { enumerable: true, value: v })
27
+ }
28
+ : function (o, v) {
29
+ o['default'] = v
30
+ })
31
+ var __importStar =
32
+ (this && this.__importStar) ||
33
+ function (mod) {
34
+ if (mod && mod.__esModule) return mod
35
+ var result = {}
36
+ if (mod != null) for (var k in mod) if (k !== 'default' && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k)
37
+ __setModuleDefault(result, mod)
38
+ return result
39
+ }
40
+ Object.defineProperty(exports, '__esModule', { value: true })
41
+ exports.Pool = void 0
42
+ const events_1 = require('events')
43
+ const fs = __importStar(require('fs'))
44
+ const path = __importStar(require('path'))
45
+ const promises_1 = require('timers-promises')
46
+ const pg_1 = require('pg')
47
+ function pgToString(value) {
48
+ return value.toString()
49
+ }
50
+ pg_1.types.setTypeParser(1082, pgToString) // date
51
+ const uuid_1 = require('uuid')
52
+ const ErrorWithCode_1 = require('./ErrorWithCode')
53
+ class Pool extends events_1.EventEmitter {
54
+ /**
55
+ * Gets the number of queued requests waiting for a database connection
56
+ */
57
+ get waitingCount() {
58
+ return this.connectionQueue.length
59
+ }
60
+ /**
61
+ * Gets the number of idle connections
62
+ */
63
+ get idleCount() {
64
+ return this.idleConnections.length
65
+ }
66
+ /**
67
+ * Gets the total number of connections in the pool
68
+ */
69
+ get totalCount() {
70
+ return this.connections.length
71
+ }
72
+ options
73
+ // Internal event emitter used to handle queued connection requests
74
+ connectionQueueEventEmitter
75
+ connections = []
76
+ // Should self order by idle timeout ascending
77
+ idleConnections = []
78
+ connectionQueue = []
79
+ isEnding = false
80
+ constructor(options) {
81
+ // eslint-disable-next-line constructor-super
82
+ super()
83
+ const defaultOptions = {
84
+ poolSize: 10,
85
+ idleTimeoutMillis: 10000,
86
+ waitForAvailableConnectionTimeoutMillis: 90000,
87
+ connectionTimeoutMillis: 5000,
88
+ retryConnectionMaxRetries: 5,
89
+ retryConnectionWaitMillis: 100,
90
+ retryConnectionErrorCodes: ['ENOTFOUND', 'EAI_AGAIN', 'ERR_PG_CONNECT_TIMEOUT', 'timeout expired'],
91
+ reconnectOnDatabaseIsStartingError: true,
92
+ waitForDatabaseStartupMillis: 0,
93
+ databaseStartupTimeoutMillis: 90000,
94
+ reconnectOnReadOnlyTransactionError: true,
95
+ waitForReconnectReadOnlyTransactionMillis: 0,
96
+ readOnlyTransactionReconnectTimeoutMillis: 90000,
97
+ reconnectOnConnectionError: true,
98
+ waitForReconnectConnectionMillis: 0,
99
+ connectionReconnectTimeoutMillis: 90000,
100
+ namedParameterFindRegExp: /@([\w])+\b/g,
101
+ getNamedParameterReplaceRegExp(namedParameter) {
102
+ // eslint-disable-next-line security/detect-non-literal-regexp
103
+ return new RegExp(`@${namedParameter}\\b`, 'gm')
104
+ },
105
+ getNamedParameterName(namedParameterWithSymbols) {
106
+ // Remove leading @ symbol
107
+ return namedParameterWithSymbols.substring(1)
108
+ },
109
+ }
110
+ const { ssl, ...otherOptions } = options
111
+ this.options = { ...defaultOptions, ...otherOptions }
112
+ if (ssl === 'aws-rds') {
113
+ this.options.ssl = {
114
+ rejectUnauthorized: true,
115
+ // eslint-disable-next-line security/detect-non-literal-fs-filename
116
+ ca: fs.readFileSync(path.join(__dirname, './certs/rds-global-bundle.pem')),
117
+ minVersion: 'TLSv1.2',
118
+ }
119
+ } else {
120
+ this.options.ssl = ssl
121
+ }
122
+ this.connectionQueueEventEmitter = new events_1.EventEmitter()
123
+ }
124
+ /**
125
+ * Gets a client connection from the pool.
126
+ * Note: You must call `.release()` when finished with the client connection object. That will release the connection back to the pool to be used by other requests.
127
+ */
128
+ async connect() {
129
+ if (this.isEnding) {
130
+ throw new ErrorWithCode_1.ErrorWithCode('Cannot use pool after calling end() on the pool', 'ERR_PG_CONNECT_POOL_ENDED')
131
+ }
132
+ const idleConnection = this.idleConnections.shift()
133
+ if (idleConnection) {
134
+ if (idleConnection.idleTimeoutTimer) {
135
+ clearTimeout(idleConnection.idleTimeoutTimer)
136
+ }
137
+ this.emit('idleConnectionActivated')
138
+ return idleConnection
139
+ }
140
+ const id = (0, uuid_1.v4)()
141
+ if (this.connections.length < this.options.poolSize) {
142
+ this.connections.push(id)
143
+ try {
144
+ const connection = await this._createConnection(id)
145
+ return connection
146
+ } catch (ex) {
147
+ // Remove the connection id since we failed to connect
148
+ const connectionIndex = this.connections.indexOf(id)
149
+ if (connectionIndex > -1) {
150
+ this.connections.splice(connectionIndex, 1)
151
+ }
152
+ throw ex
153
+ }
154
+ }
155
+ this.emit('connectionRequestQueued')
156
+ this.connectionQueue.push(id)
157
+ let connectionTimeoutTimer = null
158
+ return await Promise.race([
159
+ new Promise((resolve) => {
160
+ this.connectionQueueEventEmitter.on(`connection_${id}`, (client) => {
161
+ if (connectionTimeoutTimer) {
162
+ clearTimeout(connectionTimeoutTimer)
163
+ }
164
+ this.connectionQueueEventEmitter.removeAllListeners(`connection_${id}`)
165
+ this.emit('connectionRequestDequeued')
166
+ resolve(client)
167
+ })
168
+ }),
169
+ (async () => {
170
+ connectionTimeoutTimer = await (0, promises_1.setTimeout)(this.options.waitForAvailableConnectionTimeoutMillis)
171
+ this.connectionQueueEventEmitter.removeAllListeners(`connection_${id}`)
172
+ // Remove this connection attempt from the connection queue
173
+ const index = this.connectionQueue.indexOf(id)
174
+ if (index > -1) {
175
+ this.connectionQueue.splice(index, 1)
176
+ }
177
+ throw new ErrorWithCode_1.ErrorWithCode('Timed out while waiting for available connection in pool', 'ERR_PG_CONNECT_POOL_CONNECTION_TIMEOUT')
178
+ })(),
179
+ ])
180
+ }
181
+ /**
182
+ * Gets a connection to the database and executes the specified query. This method will release the connection back to the pool when the query has finished.
183
+ * @param {string} text
184
+ * @param {object | object[]} values - If an object, keys represent named parameters in the query
185
+ */
186
+ query(text, values) {
187
+ /* eslint-enable @typescript-eslint/no-explicit-any */
188
+ if (Array.isArray(values)) {
189
+ return this._query(text, values)
190
+ }
191
+ if (!values || !Object.keys(values).length) {
192
+ return this._query(text)
193
+ }
194
+ // eslint-disable-next-line @typescript-eslint/prefer-regexp-exec
195
+ const tokenMatches = text.match(this.options.namedParameterFindRegExp)
196
+ if (!tokenMatches) {
197
+ throw new ErrorWithCode_1.ErrorWithCode('Did not find named parameters in in the query. Expected named parameter form is @foo', 'ERR_PG_QUERY_NO_NAMED_PARAMETERS')
198
+ }
199
+ // Get unique token names
200
+ // https://stackoverflow.com/a/45886147/3085
201
+ const tokens = Array.from(new Set(tokenMatches.map(this.options.getNamedParameterName)))
202
+ const missingParameters = []
203
+ for (const token of tokens) {
204
+ if (!(token in values)) {
205
+ missingParameters.push(token)
206
+ }
207
+ }
208
+ if (missingParameters.length) {
209
+ throw new ErrorWithCode_1.ErrorWithCode(`Missing query parameter(s): ${missingParameters.join(', ')}`, 'ERR_PG_QUERY_MISSING_QUERY_PARAMETER')
210
+ }
211
+ let sql = text.slice()
212
+ const params = []
213
+ let tokenIndex = 1
214
+ for (const token of tokens) {
215
+ sql = sql.replace(this.options.getNamedParameterReplaceRegExp(token), `$${tokenIndex}`)
216
+ params.push(values[token])
217
+ tokenIndex += 1
218
+ }
219
+ return this._query(sql, params)
220
+ }
221
+ /**
222
+ * Drains the pool of all active client connections and prevents additional connections
223
+ */
224
+ end() {
225
+ this.isEnding = true
226
+ return this.drainIdleConnections()
227
+ }
228
+ /**
229
+ * Drains the pool of all idle client connections.
230
+ */
231
+ async drainIdleConnections() {
232
+ await Promise.all([...this.idleConnections].map((idleConnection) => this._removeConnection(idleConnection)))
233
+ }
234
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
235
+ async _query(text, values, reconnectQueryStartTime) {
236
+ const connection = await this.connect()
237
+ let removeConnection = false
238
+ let timeoutError
239
+ let connectionError
240
+ try {
241
+ const results = await connection.query(text, values)
242
+ return results
243
+ } catch (ex) {
244
+ const { message } = ex
245
+ if (this.options.reconnectOnReadOnlyTransactionError && /cannot execute [\s\w]+ in a read-only transaction/giu.test(message)) {
246
+ timeoutError = ex
247
+ removeConnection = true
248
+ } else if (this.options.reconnectOnConnectionError && /Client has encountered a connection error and is not queryable/giu.test(message)) {
249
+ connectionError = ex
250
+ removeConnection = true
251
+ } else {
252
+ throw ex
253
+ }
254
+ } finally {
255
+ await connection.release(removeConnection)
256
+ }
257
+ // If we get here, that means that the query was attempted with a read-only connection.
258
+ // This can happen when the cluster fails over to a read-replica
259
+ if (timeoutError) {
260
+ this.emit('queryDeniedForReadOnlyTransaction')
261
+ } else if (connectionError) {
262
+ // This can happen when a cluster fails over
263
+ this.emit('queryDeniedForConnectionError')
264
+ }
265
+ // Clear all idle connections and try the query again with a fresh connection
266
+ await this.drainIdleConnections()
267
+ if (!reconnectQueryStartTime) {
268
+ // eslint-disable-next-line no-param-reassign
269
+ reconnectQueryStartTime = process.hrtime()
270
+ }
271
+ if (timeoutError && this.options.waitForReconnectReadOnlyTransactionMillis > 0) {
272
+ await (0, promises_1.setTimeout)(this.options.waitForReconnectReadOnlyTransactionMillis)
273
+ }
274
+ if (connectionError && this.options.waitForReconnectConnectionMillis > 0) {
275
+ await (0, promises_1.setTimeout)(this.options.waitForReconnectConnectionMillis)
276
+ }
277
+ const diff = process.hrtime(reconnectQueryStartTime)
278
+ const timeSinceLastRun = Number((diff[0] * 1e3 + diff[1] * 1e-6).toFixed(3))
279
+ if (timeoutError && timeSinceLastRun > this.options.readOnlyTransactionReconnectTimeoutMillis) {
280
+ throw timeoutError
281
+ }
282
+ if (connectionError && timeSinceLastRun > this.options.connectionReconnectTimeoutMillis) {
283
+ throw connectionError
284
+ }
285
+ const results = await this._query(text, values, reconnectQueryStartTime)
286
+ return results
287
+ }
288
+ /**
289
+ * Creates a new client connection to add to the pool
290
+ * @param {string} connectionId
291
+ * @param {number} [retryAttempt=0]
292
+ * @param {bigint} [createConnectionStartTime] - High-resolution time (in nanoseconds) for when the connection was created
293
+ * @param {[number,number]} [databaseStartupStartTime] - hrtime when the db was first listed as starting up
294
+ */
295
+ async _createConnection(connectionId, retryAttempt = 0, createConnectionStartTime = process.hrtime.bigint(), databaseStartupStartTime) {
296
+ const client = new pg_1.Client(this.options)
297
+ client.uniqueId = connectionId
298
+ /**
299
+ * Releases the client connection back to the pool, to be used by another query.
300
+ *
301
+ * @param {boolean} [removeConnection=false]
302
+ */
303
+ client.release = async (removeConnection = false) => {
304
+ if (this.isEnding || removeConnection) {
305
+ await this._removeConnection(client)
306
+ return
307
+ }
308
+ const id = this.connectionQueue.shift()
309
+ // Return the connection to be used by a queued request
310
+ if (id) {
311
+ this.connectionQueueEventEmitter.emit(`connection_${id}`, client)
312
+ } else if (this.options.idleTimeoutMillis > 0) {
313
+ client.idleTimeoutTimer = setTimeout(() => {
314
+ // eslint-disable-next-line no-void
315
+ void this._removeConnection(client)
316
+ }, this.options.idleTimeoutMillis)
317
+ this.idleConnections.push(client)
318
+ this.emit('connectionIdle')
319
+ } else {
320
+ await this._removeConnection(client)
321
+ }
322
+ }
323
+ client.errorHandler = (err) => {
324
+ // fire and forget, we will always emit the error.
325
+ // eslint-disable-next-line no-void
326
+ void this._removeConnection(client).finally(() => this.emit('error', err, client))
327
+ }
328
+ client.on('error', client.errorHandler)
329
+ let connectionTimeoutTimer = null
330
+ const { connectionTimeoutMillis } = this.options
331
+ try {
332
+ await Promise.race([
333
+ (async function connectClient() {
334
+ try {
335
+ await client.connect()
336
+ } finally {
337
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
338
+ if (connectionTimeoutTimer) {
339
+ clearTimeout(connectionTimeoutTimer)
340
+ }
341
+ }
342
+ })(),
343
+ (async function connectTimeout() {
344
+ connectionTimeoutTimer = await (0, promises_1.setTimeout)(connectionTimeoutMillis)
345
+ throw new ErrorWithCode_1.ErrorWithCode('Timed out trying to connect to postgres', 'ERR_PG_CONNECT_TIMEOUT')
346
+ })(),
347
+ ])
348
+ this.emit('connectionAddedToPool', {
349
+ connectionId,
350
+ retryAttempt,
351
+ startTime: createConnectionStartTime,
352
+ })
353
+ } catch (ex) {
354
+ const { connection } = client
355
+ if (connection) {
356
+ // Force a disconnect of the socket, if it exists.
357
+ connection.stream.destroy()
358
+ }
359
+ await client.end()
360
+ const { message, code } = ex
361
+ let retryConnection = false
362
+ if (this.options.retryConnectionMaxRetries) {
363
+ if (code) {
364
+ retryConnection = this.options.retryConnectionErrorCodes.includes(code)
365
+ } else {
366
+ for (const errorCode of this.options.retryConnectionErrorCodes) {
367
+ if (message.includes(errorCode)) {
368
+ retryConnection = true
369
+ break
370
+ }
371
+ }
372
+ }
373
+ }
374
+ if (retryConnection && retryAttempt < this.options.retryConnectionMaxRetries) {
375
+ this.emit('retryConnectionOnError')
376
+ if (this.options.retryConnectionWaitMillis > 0) {
377
+ await (0, promises_1.setTimeout)(this.options.retryConnectionWaitMillis)
378
+ }
379
+ const connectionAfterRetry = await this._createConnection(connectionId, retryAttempt + 1, createConnectionStartTime, databaseStartupStartTime)
380
+ return connectionAfterRetry
381
+ }
382
+ if (this.options.reconnectOnDatabaseIsStartingError && /the database system is starting up/giu.test(message)) {
383
+ this.emit('waitingForDatabaseToStart')
384
+ if (!databaseStartupStartTime) {
385
+ // eslint-disable-next-line no-param-reassign
386
+ databaseStartupStartTime = process.hrtime()
387
+ }
388
+ if (this.options.waitForDatabaseStartupMillis > 0) {
389
+ await (0, promises_1.setTimeout)(this.options.waitForDatabaseStartupMillis)
390
+ }
391
+ const diff = process.hrtime(databaseStartupStartTime)
392
+ const timeSinceFirstConnectAttempt = Number((diff[0] * 1e3 + diff[1] * 1e-6).toFixed(3))
393
+ if (timeSinceFirstConnectAttempt > this.options.databaseStartupTimeoutMillis) {
394
+ throw ex
395
+ }
396
+ const connectionAfterRetry = await this._createConnection(connectionId, 0, createConnectionStartTime, databaseStartupStartTime)
397
+ return connectionAfterRetry
398
+ }
399
+ throw ex
400
+ }
401
+ return client
402
+ }
403
+ /**
404
+ * Removes the client connection from the pool and tries to gracefully shut it down
405
+ * @param {PoolClient} client
406
+ */
407
+ async _removeConnection(client) {
408
+ client.removeListener('error', client.errorHandler)
409
+ // Ignore any errors when ending the connection
410
+ client.on('error', () => {
411
+ // NOOP
412
+ })
413
+ if (client.idleTimeoutTimer) {
414
+ clearTimeout(client.idleTimeoutTimer)
415
+ }
416
+ const idleConnectionIndex = this.idleConnections.findIndex((connection) => connection.uniqueId === client.uniqueId)
417
+ if (idleConnectionIndex > -1) {
418
+ this.idleConnections.splice(idleConnectionIndex, 1)
419
+ this.emit('connectionRemovedFromIdlePool')
420
+ }
421
+ const connectionIndex = this.connections.indexOf(client.uniqueId)
422
+ if (connectionIndex > -1) {
423
+ this.connections.splice(connectionIndex, 1)
424
+ }
425
+ try {
426
+ await client.end()
427
+ } catch (ex) {
428
+ const { message } = ex
429
+ if (!/This socket has been ended by the other party/giu.test(message)) {
430
+ this.emit('error', ex)
431
+ }
432
+ }
433
+ this.emit('connectionRemovedFromPool')
434
+ }
435
+ }
436
+ exports.Pool = Pool
437
+ //# sourceMappingURL=index.js.map