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