@m2k-5f/pgtx 2.6.11 → 2.7.0

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
- this._isReconnecting = false;
112
- })
113
- .then(() => {
114
- void this._restoreSubscriptions();
115
- })
116
- .catch(() => this.close());
117
- return;
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.
176
- *
177
- * Automatically handles:
178
- * - `BEGIN` before the callback
179
- * - `COMMIT` on successful completion
180
- * - `ROLLBACK` if an error is thrown
117
+ * Like {@link query}, but for statements that don't return rows (INSERT/UPDATE/DDL/etc).
181
118
  *
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, 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, this.config.queryTimeout);
143
+ this._registerBatch().registerQuery(query);
144
+ return query.future;
145
+ }
146
+ const query = new ExecuteQuery(this._nextStatement(), text, args, 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,76 +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
- .tapErr(err => controller.error(err))
339
- .recover();
232
+ this._performStream(templates, params, controller);
340
233
  return stream;
341
234
  }
342
- _streamWithController(templates, params, controller) {
343
- if (!this._isOpened)
344
- throw ErrConnectionClosed;
345
- if (this._isReconnecting)
346
- throw ErrConnectionReconnecting;
235
+ _performStream(templates, params, controller) {
347
236
  const { text, args } = compileSqlTemplate(templates, params);
348
237
  if (this.config.logLevel === 'query') {
349
238
  console.log(`\nQUERY: ${text}\n${args.length !== 0 ? `ARGUMENTS: [${args}]\n` : ""}`);
@@ -354,56 +243,59 @@ export class Connection {
354
243
  this._registerBatch().registerQuery(query);
355
244
  return;
356
245
  }
357
- if (!this._parsing.has(text)) {
358
- const parseQuery = new ParseQuery(this._nextStatement(), text, this.config.queryTimeout);
359
- this._parsing.set(text, parseQuery.future);
360
- this._registerBatch().registerQuery(parseQuery);
361
- }
362
- const future = this._parsing.get(text);
363
- future
364
- .tap(meta => {
365
- 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);
366
249
  this._registerBatch().registerQuery(query);
367
- })
368
- .tapErr(err => {
369
- controller.error(err);
370
- })
371
- .recover();
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);
372
256
  }
373
- _registerReconnect() {
374
- if (this._isReconnecting)
257
+ _reconnect() {
258
+ if (this.isClosed)
259
+ return;
260
+ if (this._reconnecting)
375
261
  return;
376
- 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();
377
270
  }
378
- _resetConnectionState(cause) {
271
+ _performReconnect() {
272
+ this._socket.destroy();
379
273
  this._parsed.clear();
380
274
  this._parsing.clear();
275
+ this._activeBatch?.reject(ErrConnectionReconnecting);
381
276
  this._activeBatch = null;
382
- this._rejectAllBatches(cause);
383
- }
384
- async _reconnect() {
385
- const socket = await createAuthorizedSocket(ConnectionRequestWriter.new(), this.config);
386
- const connector = new SocketConnector(socket, (type, _, reader) => this._handlePacket(type, reader), (err) => this._registerReconnect());
387
- this._socket = connector;
388
- 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
+ });
389
286
  }
390
287
  _restoreSubscriptions() {
391
288
  if (this._listeningCallbacks.size === 0)
392
- return Promise.resolve();
393
- const promises = Array.from(this._listeningCallbacks.keys()).map(channel => {
394
- 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)};`;
395
292
  });
396
- return Promise.all(promises);
293
+ return Future.all(futures).map(() => { });
397
294
  }
398
295
  _getCurrentQuery() {
399
296
  return this._batchQueue.current.current;
400
297
  }
401
- _rejectAllBatches(cause) {
402
- while (this._batchQueue.hasMore) {
403
- this._batchQueue.shift.reject(cause);
404
- }
405
- }
406
- _handlePacket(type, reader) {
298
+ _handlePacket(type, reader, length) {
407
299
  switch (type) {
408
300
  case ResponseTypes.ParseComplete:
409
301
  {
@@ -413,6 +305,13 @@ export class Connection {
413
305
  case ResponseTypes.BindComplete:
414
306
  {
415
307
  reader.readBindComplete();
308
+ const query = this._getCurrentQuery();
309
+ if (query instanceof ExecuteQuery) {
310
+ break;
311
+ }
312
+ if (!query.columns) {
313
+ query.columns = this._parsed.get(query.text).columns;
314
+ }
416
315
  }
417
316
  break;
418
317
  case ResponseTypes.CloseComplete:
@@ -431,25 +330,27 @@ export class Connection {
431
330
  const query = this._getCurrentQuery();
432
331
  const meta = { statement: query.statement, columns: [] };
433
332
  this._parsing.delete(query.text);
434
- query.resolve(meta);
435
333
  this._parsed.set(query.text, meta);
436
- this._batchQueue.current.next();
437
334
  }
438
335
  break;
439
336
  case ResponseTypes.RowDescription:
440
337
  {
441
338
  const columns = reader.readRowDescription();
442
339
  const query = this._getCurrentQuery();
443
- const meta = { statement: query.statement, columns };
340
+ const meta = {
341
+ statement: query.statement, columns
342
+ };
444
343
  this._parsing.delete(query.text);
445
- query.resolve(meta);
446
344
  this._parsed.set(query.text, meta);
447
- this._batchQueue.current.next();
448
345
  }
449
346
  break;
450
347
  case ResponseTypes.DataRow:
451
348
  {
452
349
  let query = this._getCurrentQuery();
350
+ if (query instanceof ExecuteQuery) {
351
+ reader.skip(length - 4);
352
+ break;
353
+ }
453
354
  query.push(reader.readDataRow(query.columns, this.config.int8toBigint));
454
355
  }
455
356
  break;
@@ -467,10 +368,7 @@ export class Connection {
467
368
  if (this.config.logLevel === 'error' || this.config.logLevel === 'notice' || this.config.logLevel === 'query') {
468
369
  console.log(`\nError: ${error}\n`);
469
370
  }
470
- const query = this._getCurrentQuery();
471
- if (query instanceof ParseQuery) {
472
- this._parsing.delete(query.text);
473
- }
371
+ this._parsing.clear();
474
372
  this._batchQueue.current.reject(error);
475
373
  }
476
374
  break;
@@ -478,6 +376,7 @@ export class Connection {
478
376
  {
479
377
  reader.readReadyForQuery();
480
378
  this._batchQueue.next();
379
+ this._closing && !this._hasQueries && this._closing.resolve();
481
380
  }
482
381
  break;
483
382
  case ResponseTypes.Notice:
@@ -500,26 +399,38 @@ export class Connection {
500
399
  default: console.log('Undeclared response type: ', type);
501
400
  }
502
401
  }
503
- /**
504
- * Checks if the connection is still alive and usable.
505
- * Returns `false` if the socket is destroyed or connection is dead.
506
- */
402
+ get _hasQueries() {
403
+ return this._batchQueue.hasMore || this._activeBatch;
404
+ }
405
+ /** Whether the connection is alive and usable. */
507
406
  get isOpened() {
508
- return this._isOpened;
407
+ return !this._closing && !this._closed;
408
+ }
409
+ /** Whether the connection is closed or closing. */
410
+ get isClosed() {
411
+ return this._closed || !!this._closing;
509
412
  }
510
413
  /**
511
- * Closes the connection immediately.
512
- * All pending queries will be rejected with an error.
513
- * The connection cannot be used after this call.
514
- *
515
- * @example
516
- * ```ts
517
- * conn.close()
518
- * ```
414
+ * Closes the connection, awaiting for all pending queries. Not usable afterward.
519
415
  */
520
416
  close() {
521
- this._isOpened = false;
522
- this._socket.destroy();
523
- this._rejectAllBatches(ErrConnectionClosed);
417
+ if (this._closed)
418
+ return Future.resolve();
419
+ if (this._closing) {
420
+ return this._closing.future;
421
+ }
422
+ if (!this._hasQueries) {
423
+ this._closed = true;
424
+ this._socket.destroy();
425
+ return Future.resolve();
426
+ }
427
+ const closing = Future.withResolvers();
428
+ this._closing = closing;
429
+ closing.future.tap(() => {
430
+ this._closing = null;
431
+ this._closed = true;
432
+ this._socket.destroy();
433
+ });
434
+ return closing.future;
524
435
  }
525
436
  }