@m2k-5f/pgtx 2.6.1 → 2.6.2

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.
@@ -5,41 +5,26 @@ import { compileSqlTemplate } from "./utils/template-compiler";
5
5
  import { Transaction } from "./transaction";
6
6
  import { SocketConnector } from "./protocol/socket-connector";
7
7
  import { Queue } from "./queue";
8
- import { ParseQuery, SimpleQuery, StreamQuery } from "./query";
8
+ import { CollectQuery, StreamQuery, ExecuteQuery } from "./query";
9
9
  import { sql } from ".";
10
10
  import { Begin, Future, Ok } from 'fluent-future';
11
11
  import { ErrConnectionClosed, ErrConnectionReconnecting } from "./error";
12
12
  import { Batch } from "./batch";
13
13
  import { nextTick } from "process";
14
+ const shedule = {
15
+ Immediate: setImmediate,
16
+ afterMicrotask: setTimeout,
17
+ beforeMicrotask: nextTick
18
+ };
14
19
  /**
15
- * Represents a single dedicated connection to the PostgreSQL database.
16
- *
17
- * Supports:
18
- * - Tagged template queries with automatic parameter binding
19
- * - Prepared statements with caching
20
- * - Transaction management with savepoints
21
- * - Pipeline execution for concurrent queries
20
+ * A dedicated connection to PostgreSQL: tagged-template queries, prepared
21
+ * statement caching, transactions with savepoints, and pipelined execution.
22
22
  *
23
23
  * @example
24
- * ```ts
25
- * const conn = await Connection.new({
26
- * host: 'localhost',
27
- * user: 'postgres',
28
- * password: 'postgres',
29
- * database: 'test'
30
- * })
31
- *
32
- * // Simple query
24
+ * const conn = await Connection.new({ host: 'localhost', user: 'postgres', password: 'postgres', database: 'test' })
33
25
  * const users = await conn.query`SELECT * FROM users WHERE id = ${1}`
34
- *
35
- * // Transaction
36
- * await conn.begin(async tx => {
37
- * await tx.query`INSERT INTO users ...`
38
- * })
39
- *
40
- * // Close connection
26
+ * await conn.begin(async tx => tx.query`INSERT INTO users ...`)
41
27
  * conn.close()
42
- * ```
43
28
  */
44
29
  export class Connection {
45
30
  _nextStatement() {
@@ -47,8 +32,9 @@ export class Connection {
47
32
  }
48
33
  constructor(config, socket) {
49
34
  this._activeBatch = null;
50
- this._isOpened = true;
51
- this._isReconnecting = false;
35
+ this._closing = null;
36
+ this._closed = false;
37
+ this._reconnecting = null;
52
38
  this._cachedBuffer = ConnectionRequestWriter.new();
53
39
  this._batchQueue = new Queue();
54
40
  this._parsed = new Map();
@@ -56,25 +42,11 @@ export class Connection {
56
42
  this._listeningCallbacks = new Map();
57
43
  this._stmtCounter = 0;
58
44
  this.config = config;
59
- this._socket = new SocketConnector(socket, (type, _, reader) => this._handlePacket(type, reader), () => this._registerReconnect());
45
+ this._socket = new SocketConnector(socket, (type, length, reader) => this._handlePacket(type, reader, length), () => this._reconnect());
60
46
  }
61
47
  /**
62
- * Creates a new database connection.
63
- *
64
- * @param params - Connection parameters
65
- * @returns A new Connection instance
66
- * @throws {PostgresError} If authentication fails or connection cannot be established
67
- *
68
- * @example
69
- * ```ts
70
- * const conn = await Connection.new({
71
- * host: 'localhost',
72
- * port: 5432,
73
- * user: 'postgres',
74
- * password: 'secret',
75
- * database: 'myapp'
76
- * })
77
- * ```
48
+ * Opens a new connection and authenticates.
49
+ * @throws {PostgresError} if authentication fails or the connection can't be established
78
50
  */
79
51
  static new(config) {
80
52
  const conf = {
@@ -82,7 +54,7 @@ export class Connection {
82
54
  logLevel: config.logLevel || 'error',
83
55
  int8toBigint: config.int8toBigint || false,
84
56
  queryTimeout: config.queryTimeout || 30000,
85
- syncShedule: config.syncShedule || 'Immediate'
57
+ syncShedule: config.syncShedule || 'afterMicrotask'
86
58
  };
87
59
  const writer = ConnectionRequestWriter.new();
88
60
  return createAuthorizedSocket(writer, conf)
@@ -92,181 +64,138 @@ export class Connection {
92
64
  if (!this._activeBatch) {
93
65
  const batch = new Batch(this._cachedBuffer.clear());
94
66
  this._activeBatch = batch;
95
- (this.config.syncShedule === 'Immediate' ? setImmediate : nextTick)(() => {
96
- this._sync(batch);
97
- });
67
+ shedule[this.config.syncShedule](() => this._sync(batch));
98
68
  return batch;
99
69
  }
100
70
  return this._activeBatch;
101
71
  }
102
72
  _sync(batch) {
103
- if (!this._isOpened) {
104
- batch.reject(ErrConnectionClosed);
73
+ if (this._reconnecting)
105
74
  return;
106
- }
107
- if (this._isReconnecting) {
108
- this._reconnect()
109
- .then(() => {
110
- this._activeBatch = null;
111
- void this._restoreSubscriptions();
112
- this._socket.write(this._registerBatch().end());
113
- this._batchQueue.push(this._registerBatch());
114
- })
115
- .then(() => {
116
- this._isReconnecting = false;
117
- });
118
- }
119
75
  this._socket.write(batch.end());
120
76
  this._activeBatch = null;
121
77
  this._batchQueue.push(batch);
122
78
  }
123
79
  /**
124
- * Executes a query using tagged template literals.
125
- *
126
- * Parameters are automatically bound to `$1, $2, ...` placeholders.
127
- * Queries with parameters use prepared statements for performance.
128
- *
129
- * @param templates - Tagged template string with SQL
130
- * @param args - Query parameters
131
- * @returns Array of rows with proper typing
132
- * @throws {Error} If connection is dead or query fails
80
+ * Runs a query, binding template values as `$1, $2, ...`.
81
+ * Parameterized queries are cached as prepared statements.
133
82
  *
134
83
  * @example
135
- * ```ts
136
- * // Simple query
137
- * const users = await conn.query`SELECT * FROM users`
138
- *
139
- * // With parameters
140
- * const user = await conn.query`SELECT * FROM users WHERE id = ${1}`
141
- *
142
- * // With typed result
143
- * type User = { id: number, name: string }
144
- * const users = await conn.query<User>`SELECT * FROM users`
145
- * ```
84
+ * const users = await conn.query<User>`SELECT * FROM users WHERE id = ${1}`
146
85
  */
147
86
  query(templates, ...params) {
148
- if (!this._isOpened)
87
+ if (this.isClosed)
149
88
  return Future.reject(ErrConnectionClosed);
150
- if (this._isReconnecting)
151
- return Future.reject(ErrConnectionReconnecting);
89
+ if (this._reconnecting)
90
+ return this._reconnecting.andThen(() => this._performQuery(templates, ...params));
91
+ return this._performQuery(templates, ...params);
92
+ }
93
+ _performQuery(templates, ...params) {
152
94
  const { text, args } = compileSqlTemplate(templates, params);
153
95
  if (this.config.logLevel === 'query') {
154
96
  console.log(`\nQUERY: ${text}\n${args.length !== 0 ? `ARGUMENTS: [${args}]\n` : ""}`);
155
97
  }
156
98
  if (this._parsed.has(text)) {
157
99
  const meta = this._parsed.get(text);
158
- const query = new SimpleQuery(meta.statement, text, args, meta.columns, this.config.queryTimeout);
100
+ const query = new CollectQuery(meta.statement, text, args, meta.columns, this.config.queryTimeout);
159
101
  this._registerBatch().registerQuery(query);
160
102
  return query.future;
161
103
  }
162
- if (!this._parsing.has(text)) {
163
- const parseQuery = new ParseQuery(this._nextStatement(), text, this.config.queryTimeout);
164
- this._parsing.set(text, parseQuery.future);
165
- this._registerBatch().registerQuery(parseQuery);
166
- }
167
- const future = this._parsing.get(text);
168
- return future.andThen(meta => {
169
- const query = new SimpleQuery(meta.statement, text, args, meta.columns, this.config.queryTimeout);
104
+ if (this._parsing.has(text)) {
105
+ const statement = this._parsing.get(text);
106
+ const query = new CollectQuery(statement, text, args, null, this.config.queryTimeout);
170
107
  this._registerBatch().registerQuery(query);
171
108
  return query.future;
172
- });
109
+ }
110
+ const query = new CollectQuery(this._nextStatement(), text, args, null, this.config.queryTimeout);
111
+ this._parsing.set(text, query.statement);
112
+ this._registerBatch().registerParse(query);
113
+ this._registerBatch().registerQuery(query);
114
+ return query.future;
173
115
  }
174
116
  /**
175
- * Starts a managed transaction on this connection.
117
+ * Like {@link query}, but for statements that don't return rows (INSERT/UPDATE/DDL/etc).
176
118
  *
177
- * Automatically handles:
178
- * - `BEGIN` before the callback
179
- * - `COMMIT` on successful completion
180
- * - `ROLLBACK` if an error is thrown
181
- *
182
- * @param txCallback - Async function that receives a `Transaction` instance
183
- * @returns The value returned from the callback
184
- * @throws {Error} If connection is dead or transaction fails
119
+ * @example
120
+ * await conn.execute`UPDATE users SET name = ${name} WHERE id = ${id}`
121
+ */
122
+ execute(templates, ...params) {
123
+ if (this.isClosed)
124
+ return Future.reject(ErrConnectionClosed);
125
+ if (this._reconnecting)
126
+ return this._reconnecting.andThen(() => this._performExecute(templates, ...params));
127
+ return this._performExecute(templates, ...params);
128
+ }
129
+ _performExecute(templates, ...params) {
130
+ const { text, args } = compileSqlTemplate(templates, params);
131
+ if (this.config.logLevel === 'query') {
132
+ console.log(`\nQUERY: ${text}\n${args.length !== 0 ? `ARGUMENTS: [${args}]\n` : ""}`);
133
+ }
134
+ if (this._parsed.has(text)) {
135
+ const meta = this._parsed.get(text);
136
+ const query = new ExecuteQuery(meta.statement, text, args, meta.columns, this.config.queryTimeout);
137
+ this._registerBatch().registerQuery(query);
138
+ return query.future;
139
+ }
140
+ if (this._parsing.has(text)) {
141
+ const statement = this._parsing.get(text);
142
+ const query = new ExecuteQuery(statement, text, args, null, this.config.queryTimeout);
143
+ this._registerBatch().registerQuery(query);
144
+ return query.future;
145
+ }
146
+ const query = new ExecuteQuery(this._nextStatement(), text, args, null, this.config.queryTimeout);
147
+ this._parsing.set(text, query.statement);
148
+ this._registerBatch().registerParse(query);
149
+ this._registerBatch().registerQuery(query);
150
+ return query.future;
151
+ }
152
+ /**
153
+ * Runs `txCallback` inside `BEGIN`/`COMMIT`, rolling back on error.
185
154
  *
186
155
  * @example
187
- * ```ts
188
- * const result = await conn.begin(async tx => {
189
- * await tx.query`INSERT INTO accounts (id, balance) VALUES (1, 100)`
156
+ * await conn.begin(async tx => {
190
157
  * await tx.query`UPDATE accounts SET balance = balance - 10 WHERE id = 1`
191
- * return { success: true }
192
158
  * })
193
- * ```
194
159
  */
195
160
  begin(txCallback) {
196
- if (!this._isOpened)
161
+ if (this.isClosed)
197
162
  return Future.reject(ErrConnectionClosed);
198
- if (this._isReconnecting)
199
- return Future.reject(ErrConnectionReconnecting);
200
- const tx = new Transaction(this);
201
163
  return Begin()
202
- .andThen(() => tx.query `BEGIN`)
203
- .andThen(() => Future.of(txCallback(tx))
204
- .tap(() => {
205
- if (tx.isActive)
206
- return tx.commit();
207
- })
208
- .tapErr(() => {
209
- if (tx.isActive)
210
- return tx.rollback();
211
- }));
164
+ .andThen(() => this.execute `begin`)
165
+ .andThen(() => {
166
+ const tx = new Transaction(this);
167
+ return Future.of(txCallback(tx))
168
+ .tap(() => {
169
+ if (tx.isActive)
170
+ return tx.commit();
171
+ })
172
+ .tapErr(() => {
173
+ if (tx.isActive)
174
+ return tx.rollback();
175
+ });
176
+ });
212
177
  }
213
- /**
214
- * Sends an asynchronous notification to a channel via `pg_notify`.
215
- *
216
- * @param channelName - The channel identifier
217
- * @param payload - Optional string data (max 8000 bytes)
218
- *
219
- * @example
220
- * ```ts
221
- * await conn.notify('events', 'hello')
222
- * ```
223
- */
178
+ /** Sends a `pg_notify` message on `channelName` (payload ≤ 8000 bytes). */
224
179
  notify(channelName, payload = "") {
225
- if (!this._isOpened)
180
+ if (this.isClosed)
226
181
  return Future.reject(ErrConnectionClosed);
227
- if (this._isReconnecting)
228
- return Future.reject(ErrConnectionReconnecting);
229
- return this.query `select pg_notify(${channelName}, ${payload})`.map(() => { });
182
+ return this.execute `select pg_notify(${channelName}, ${payload})`.map(() => { });
230
183
  }
231
- /**
232
- * Subscribes a callback to a channel. Sends `LISTEN` on the first subscription.
233
- *
234
- * @param channelName - The channel identifier
235
- * @param callback - Function invoked when a notification arrives
236
- *
237
- * @example
238
- * ```ts
239
- * await conn.listen('events', data => console.log(data))
240
- * ```
241
- */
184
+ /** Subscribes `callback` to `channelName`, issuing `LISTEN` on first subscription. */
242
185
  listen(channelName, callback) {
243
- if (!this._isOpened)
186
+ if (this.isClosed)
244
187
  return Future.reject(ErrConnectionClosed);
245
- if (this._isReconnecting)
246
- return Future.reject(ErrConnectionReconnecting);
247
188
  if (!this._listeningCallbacks.has(channelName)) {
248
189
  this._listeningCallbacks.set(channelName, new Set());
249
190
  }
250
191
  const callbackSet = this._listeningCallbacks.get(channelName);
251
192
  callbackSet.add(callback);
252
- return this.query `listen ${sql.ident(channelName)};`.map(() => { });
193
+ return this.execute `listen ${sql.ident(channelName)};`;
253
194
  }
254
- /**
255
- * Unsubscribes a callback. Sends `UNLISTEN` if no callbacks remain for the channel.
256
- *
257
- * @param channelName - The channel identifier
258
- * @param callback - The registered callback to remove
259
- *
260
- * @example
261
- * ```ts
262
- * await conn.unlisten('events', callback)
263
- * ```
264
- */
195
+ /** Unsubscribes `callback`, issuing `UNLISTEN` once no callbacks remain. */
265
196
  unlisten(channelName, callback) {
266
- if (!this._isOpened)
197
+ if (this.isClosed)
267
198
  return Future.reject(ErrConnectionClosed);
268
- if (this._isReconnecting)
269
- return Future.reject(ErrConnectionReconnecting);
270
199
  if (!this._listeningCallbacks.has(channelName)) {
271
200
  return Ok();
272
201
  }
@@ -274,75 +203,36 @@ export class Connection {
274
203
  callbackSet.delete(callback);
275
204
  if (callbackSet.size === 0) {
276
205
  this._listeningCallbacks.delete(channelName);
277
- return this.query `unlisten ${sql.ident(channelName)};`.map(() => { });
206
+ return this.execute `unlisten ${sql.ident(channelName)};`.map(() => { });
278
207
  }
279
208
  return Ok();
280
209
  }
281
210
  /**
282
- * Executes an SQL query in streaming mode.
283
- *
284
- * Data is streamed directly from the PostgreSQL binary network buffer into the Web Streams API
285
- * (`ReadableStream`), bypassing any intermediate array allocation or row accumulation in the JS heap.
286
- * This pattern provides a true Zero-Memory Footprint and is ideal for exporting massive tables
287
- * or piping database payloads directly into HTTP responses (e.g., via `Bun.serve` or fetch `Response`).
288
- *
289
- * @template T The expected shape of a single row interface.
290
- * @param {TemplateStringsArray} templates The SQL string parts from the tagged template literal.
291
- * @param {...any} args The parameterized query arguments.
292
- * @returns {ReadableStream<T>} Synchronously returns a native Web ReadableStream instance.
293
- *
294
- * @example
295
- * // Streaming a giant table directly to an HTTP response (Bun.serve)
296
- * const userStream = conn.stream<User>`SELECT id, name FROM users`;
297
- * return new Response(userStream, { headers: { 'Content-Type': 'application/json' } });
211
+ * Streams query results as a `ReadableStream`, without buffering rows in memory.
212
+ * Ideal for large result sets or piping straight into an HTTP response.
298
213
  *
299
214
  * @example
300
- * // Asynchronously iterating over rows as they arrive from the wire socket
301
- * const stream = conn.stream<User>`SELECT * FROM orders WHERE status = ${'processed'}`;
302
- * for await (const row of stream) {
303
- * console.log(row.id, row.amount); // Row object is eligible for GC immediately after iteration
304
- * }
215
+ * for await (const row of conn.stream<User>`SELECT * FROM orders`) { ... }
305
216
  */
306
217
  stream(templates, ...params) {
307
- if (!this._isOpened)
218
+ if (this.isClosed)
308
219
  throw ErrConnectionClosed;
309
- if (this._isReconnecting)
310
- throw ErrConnectionReconnecting;
311
- const { text, args } = compileSqlTemplate(templates, params);
312
- if (this.config.logLevel === 'query') {
313
- console.log(`\nQUERY: ${text}\n${args.length !== 0 ? `ARGUMENTS: [${args}]\n` : ""}`);
314
- }
315
220
  let controller;
316
221
  const stream = new ReadableStream({
317
222
  start: c => {
318
223
  controller = c;
319
224
  }
320
225
  });
321
- if (this._parsed.has(text)) {
322
- const meta = this._parsed.get(text);
323
- const query = new StreamQuery(meta.statement, text, args, controller, meta.columns, this.config.queryTimeout);
324
- this._registerBatch().registerQuery(query);
226
+ if (this._reconnecting) {
227
+ this._reconnecting
228
+ .tap(() => this._performStream(templates, params, controller))
229
+ .tapErr(err => controller.error(err));
325
230
  return stream;
326
231
  }
327
- if (!this._parsing.has(text)) {
328
- const parseQuery = new ParseQuery(this._nextStatement(), text, this.config.queryTimeout);
329
- this._parsing.set(text, parseQuery.future);
330
- this._registerBatch().registerQuery(parseQuery);
331
- }
332
- const future = this._parsing.get(text);
333
- future
334
- .tap(meta => {
335
- const query = new StreamQuery(meta.statement, text, args, controller, meta.columns, this.config.queryTimeout);
336
- this._registerBatch().registerQuery(query);
337
- })
338
- .catch(err => controller.error(err));
232
+ this._performStream(templates, params, controller);
339
233
  return stream;
340
234
  }
341
- _streamWithController(templates, params, controller) {
342
- if (!this._isOpened)
343
- throw ErrConnectionClosed;
344
- if (this._isReconnecting)
345
- throw ErrConnectionReconnecting;
235
+ _performStream(templates, params, controller) {
346
236
  const { text, args } = compileSqlTemplate(templates, params);
347
237
  if (this.config.logLevel === 'query') {
348
238
  console.log(`\nQUERY: ${text}\n${args.length !== 0 ? `ARGUMENTS: [${args}]\n` : ""}`);
@@ -351,54 +241,61 @@ export class Connection {
351
241
  const meta = this._parsed.get(text);
352
242
  const query = new StreamQuery(meta.statement, text, args, controller, meta.columns, this.config.queryTimeout);
353
243
  this._registerBatch().registerQuery(query);
244
+ return;
354
245
  }
355
- if (!this._parsing.has(text)) {
356
- const parseQuery = new ParseQuery(this._nextStatement(), text, this.config.queryTimeout);
357
- this._parsing.set(text, parseQuery.future);
358
- this._registerBatch().registerQuery(parseQuery);
359
- }
360
- const future = this._parsing.get(text);
361
- future
362
- .tap(meta => {
363
- const query = new StreamQuery(meta.statement, text, args, controller, meta.columns, this.config.queryTimeout);
246
+ if (this._parsing.has(text)) {
247
+ const statement = this._parsing.get(text);
248
+ const query = new StreamQuery(statement, text, args, controller, null, this.config.queryTimeout);
364
249
  this._registerBatch().registerQuery(query);
365
- })
366
- .catch(err => controller.error(err));
250
+ return;
251
+ }
252
+ const query = new StreamQuery(this._nextStatement(), text, args, controller, null, this.config.queryTimeout);
253
+ this._parsing.set(text, query.statement);
254
+ this._registerBatch().registerParse(query);
255
+ this._registerBatch().registerQuery(query);
367
256
  }
368
- _registerReconnect() {
369
- if (this._isReconnecting)
257
+ _reconnect() {
258
+ if (this.isClosed)
259
+ return;
260
+ if (this._reconnecting)
370
261
  return;
371
- this._isReconnecting = true;
262
+ this._reconnecting = this._performReconnect()
263
+ .tap(() => this._reconnecting = null)
264
+ .andThen(() => this._restoreSubscriptions())
265
+ .tapErr(() => {
266
+ this._reconnecting = null;
267
+ this._reconnect();
268
+ })
269
+ .recover();
372
270
  }
373
- _resetConnectionState(cause) {
271
+ _performReconnect() {
272
+ this._socket.destroy();
374
273
  this._parsed.clear();
375
274
  this._parsing.clear();
275
+ this._activeBatch?.reject(ErrConnectionReconnecting);
376
276
  this._activeBatch = null;
377
- this._rejectAllBatches(cause);
378
- }
379
- async _reconnect() {
380
- const socket = await createAuthorizedSocket(ConnectionRequestWriter.new(), this.config);
381
- const connector = new SocketConnector(socket, (type, _, reader) => this._handlePacket(type, reader), (err) => this._registerReconnect());
382
- this._socket = connector;
383
- this._resetConnectionState(ErrConnectionReconnecting);
277
+ while (this._batchQueue.hasMore) {
278
+ this._batchQueue.shift.reject(ErrConnectionReconnecting);
279
+ }
280
+ return createAuthorizedSocket(ConnectionRequestWriter.new(), this.config)
281
+ .andThen(socket => {
282
+ const connector = new SocketConnector(socket, (type, length, reader) => this._handlePacket(type, reader, length), () => this._reconnect());
283
+ this._socket = connector;
284
+ return Ok();
285
+ });
384
286
  }
385
287
  _restoreSubscriptions() {
386
288
  if (this._listeningCallbacks.size === 0)
387
- return Promise.resolve();
388
- const promises = Array.from(this._listeningCallbacks.keys()).map(channel => {
389
- return this.query `LISTEN ${sql.ident(channel)};`;
289
+ return Future.resolve();
290
+ const futures = Array.from(this._listeningCallbacks.keys()).map(channel => {
291
+ return this.execute `LISTEN ${sql.ident(channel)};`;
390
292
  });
391
- return Promise.all(promises);
293
+ return Future.all(futures).map(() => { });
392
294
  }
393
295
  _getCurrentQuery() {
394
296
  return this._batchQueue.current.current;
395
297
  }
396
- _rejectAllBatches(cause) {
397
- while (this._batchQueue.hasMore) {
398
- this._batchQueue.shift.reject(cause);
399
- }
400
- }
401
- _handlePacket(type, reader) {
298
+ _handlePacket(type, reader, length) {
402
299
  switch (type) {
403
300
  case ResponseTypes.ParseComplete:
404
301
  {
@@ -408,6 +305,10 @@ export class Connection {
408
305
  case ResponseTypes.BindComplete:
409
306
  {
410
307
  reader.readBindComplete();
308
+ const query = this._getCurrentQuery();
309
+ if (!query.columns) {
310
+ query.columns = this._parsed.get(query.text).columns;
311
+ }
411
312
  }
412
313
  break;
413
314
  case ResponseTypes.CloseComplete:
@@ -426,20 +327,18 @@ export class Connection {
426
327
  const query = this._getCurrentQuery();
427
328
  const meta = { statement: query.statement, columns: [] };
428
329
  this._parsing.delete(query.text);
429
- query.resolve(meta);
430
330
  this._parsed.set(query.text, meta);
431
- this._batchQueue.current.next();
432
331
  }
433
332
  break;
434
333
  case ResponseTypes.RowDescription:
435
334
  {
436
335
  const columns = reader.readRowDescription();
437
336
  const query = this._getCurrentQuery();
438
- const meta = { statement: query.statement, columns };
337
+ const meta = {
338
+ statement: query.statement, columns
339
+ };
439
340
  this._parsing.delete(query.text);
440
- query.resolve(meta);
441
341
  this._parsed.set(query.text, meta);
442
- this._batchQueue.current.next();
443
342
  }
444
343
  break;
445
344
  case ResponseTypes.DataRow:
@@ -462,8 +361,7 @@ export class Connection {
462
361
  if (this.config.logLevel === 'error' || this.config.logLevel === 'notice' || this.config.logLevel === 'query') {
463
362
  console.log(`\nError: ${error}\n`);
464
363
  }
465
- const query = this._getCurrentQuery();
466
- this._parsing.delete(query.text);
364
+ this._parsing.clear();
467
365
  this._batchQueue.current.reject(error);
468
366
  }
469
367
  break;
@@ -471,6 +369,7 @@ export class Connection {
471
369
  {
472
370
  reader.readReadyForQuery();
473
371
  this._batchQueue.next();
372
+ this._closing && !this._hasQueries && this._closing.resolve();
474
373
  }
475
374
  break;
476
375
  case ResponseTypes.Notice:
@@ -493,26 +392,38 @@ export class Connection {
493
392
  default: console.log('Undeclared response type: ', type);
494
393
  }
495
394
  }
496
- /**
497
- * Checks if the connection is still alive and usable.
498
- * Returns `false` if the socket is destroyed or connection is dead.
499
- */
395
+ get _hasQueries() {
396
+ return this._batchQueue.hasMore || this._activeBatch;
397
+ }
398
+ /** Whether the connection is alive and usable. */
500
399
  get isOpened() {
501
- return this._isOpened;
400
+ return !this._closing && !this._closed;
401
+ }
402
+ /** Whether the connection is closed or closing. */
403
+ get isClosed() {
404
+ return this._closed || !!this._closing;
502
405
  }
503
406
  /**
504
- * Closes the connection immediately.
505
- * All pending queries will be rejected with an error.
506
- * The connection cannot be used after this call.
507
- *
508
- * @example
509
- * ```ts
510
- * conn.close()
511
- * ```
407
+ * Closes the connection, awaiting for all pending queries. Not usable afterward.
512
408
  */
513
409
  close() {
514
- this._isOpened = false;
515
- this._socket.destroy();
516
- this._rejectAllBatches(ErrConnectionClosed);
410
+ if (this._closed)
411
+ return Future.resolve();
412
+ if (this._closing) {
413
+ return this._closing.future;
414
+ }
415
+ if (!this._hasQueries) {
416
+ this._closed = true;
417
+ this._socket.destroy();
418
+ return Future.resolve();
419
+ }
420
+ const closing = Future.withResolvers();
421
+ this._closing = closing;
422
+ closing.future.tap(() => {
423
+ this._closing = null;
424
+ this._closed = true;
425
+ this._socket.destroy();
426
+ });
427
+ return closing.future;
517
428
  }
518
429
  }