@m2k-5f/pgtx 2.6.11 → 2.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/README.md +244 -494
  2. package/dist/batch.d.ts +2 -1
  3. package/dist/batch.d.ts.map +1 -1
  4. package/dist/batch.js +8 -7
  5. package/dist/connection.d.ts +39 -150
  6. package/dist/connection.d.ts.map +1 -1
  7. package/dist/connection.js +199 -284
  8. package/dist/error.d.ts +4 -0
  9. package/dist/error.d.ts.map +1 -1
  10. package/dist/error.js +4 -0
  11. package/dist/pool.d.ts +29 -181
  12. package/dist/pool.d.ts.map +1 -1
  13. package/dist/pool.js +49 -180
  14. package/dist/protocol/connection-request-writer.d.ts +1 -0
  15. package/dist/protocol/connection-request-writer.d.ts.map +1 -1
  16. package/dist/protocol/connection-request-writer.js +6 -0
  17. package/dist/protocol/connection-response-reader.d.ts +1 -0
  18. package/dist/protocol/connection-response-reader.d.ts.map +1 -1
  19. package/dist/protocol/connection-response-reader.js +3 -0
  20. package/dist/protocol/constants.d.ts +2 -0
  21. package/dist/protocol/constants.d.ts.map +1 -1
  22. package/dist/protocol/constants.js +3 -1
  23. package/dist/protocol/socket-authorization.d.ts +5 -4
  24. package/dist/protocol/socket-authorization.d.ts.map +1 -1
  25. package/dist/protocol/socket-authorization.js +167 -74
  26. package/dist/protocol/socket-connector.d.ts +5 -5
  27. package/dist/protocol/socket-connector.d.ts.map +1 -1
  28. package/dist/protocol/socket-connector.js +24 -15
  29. package/dist/query.d.ts +14 -17
  30. package/dist/query.d.ts.map +1 -1
  31. package/dist/query.js +20 -17
  32. package/dist/transaction.d.ts +9 -1
  33. package/dist/transaction.d.ts.map +1 -1
  34. package/dist/transaction.js +11 -0
  35. package/dist/types.d.ts +14 -9
  36. package/dist/types.d.ts.map +1 -1
  37. package/package.json +1 -1
@@ -1,54 +1,40 @@
1
1
  import { ConnectionRequestWriter } from "./protocol/connection-request-writer";
2
- import { createAuthorizedSocket } from "./protocol/socket-authorization";
3
2
  import { ResponseTypes } from "./protocol/constants";
4
3
  import { compileSqlTemplate } from "./utils/template-compiler";
5
4
  import { Transaction } from "./transaction";
6
5
  import { SocketConnector } from "./protocol/socket-connector";
7
6
  import { Queue } from "./queue";
8
- import { ParseQuery, SimpleQuery, StreamQuery } from "./query";
7
+ import { CollectQuery, StreamQuery, ExecuteQuery } from "./query";
9
8
  import { sql } from ".";
10
9
  import { Begin, Future, Ok } from 'fluent-future';
11
10
  import { ErrConnectionClosed, ErrConnectionReconnecting } from "./error";
12
11
  import { Batch } from "./batch";
13
12
  import { nextTick } from "process";
13
+ import { authorizeSocket, createSocket, upgradeSocket } from "./protocol/socket-authorization";
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() {
46
31
  return `s-${this._stmtCounter++}`;
47
32
  }
48
- constructor(config, socket) {
33
+ constructor(socket, config) {
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, this._handlePacket.bind(this), () => 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,191 +54,150 @@ 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',
58
+ ssl: config.caPath ? 'require' : (config.ssl || 'prefer')
86
59
  };
87
- const writer = ConnectionRequestWriter.new();
88
- return createAuthorizedSocket(writer, conf)
89
- .andThen(socket => Ok(new Connection(conf, socket)));
60
+ return createSocket(conf)
61
+ .andThen(socket => upgradeSocket(socket, conf))
62
+ .andThen(socket => authorizeSocket(socket, conf))
63
+ .andThen(socket => Ok(new Connection(socket, conf)));
90
64
  }
91
65
  _registerBatch() {
92
66
  if (!this._activeBatch) {
93
67
  const batch = new Batch(this._cachedBuffer.clear());
94
68
  this._activeBatch = batch;
95
- (this.config.syncShedule === 'Immediate' ? setImmediate : nextTick)(() => {
96
- this._sync(batch);
97
- });
69
+ shedule[this.config.syncShedule](() => this._sync(batch));
98
70
  return batch;
99
71
  }
100
72
  return this._activeBatch;
101
73
  }
102
74
  _sync(batch) {
103
- if (!this._isOpened) {
104
- batch.reject(ErrConnectionClosed);
75
+ if (this._reconnecting)
105
76
  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
77
  this._socket.write(batch.end());
120
78
  this._activeBatch = null;
121
79
  this._batchQueue.push(batch);
122
80
  }
123
81
  /**
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
82
+ * Runs a query, binding template values as `$1, $2, ...`.
83
+ * Parameterized queries are cached as prepared statements.
133
84
  *
134
85
  * @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
- * ```
86
+ * const users = await conn.query<User>`SELECT * FROM users WHERE id = ${1}`
146
87
  */
147
88
  query(templates, ...params) {
148
- if (!this._isOpened)
89
+ if (this.isClosed)
149
90
  return Future.reject(ErrConnectionClosed);
150
- if (this._isReconnecting)
151
- return Future.reject(ErrConnectionReconnecting);
91
+ if (this._reconnecting)
92
+ return this._reconnecting.andThen(() => this._performQuery(templates, ...params));
93
+ return this._performQuery(templates, ...params);
94
+ }
95
+ _performQuery(templates, ...params) {
152
96
  const { text, args } = compileSqlTemplate(templates, params);
153
97
  if (this.config.logLevel === 'query') {
154
98
  console.log(`\nQUERY: ${text}\n${args.length !== 0 ? `ARGUMENTS: [${args}]\n` : ""}`);
155
99
  }
156
100
  if (this._parsed.has(text)) {
157
101
  const meta = this._parsed.get(text);
158
- const query = new SimpleQuery(meta.statement, text, args, meta.columns, this.config.queryTimeout);
102
+ const query = new CollectQuery(meta.statement, text, args, meta.columns, this.config.queryTimeout);
159
103
  this._registerBatch().registerQuery(query);
160
104
  return query.future;
161
105
  }
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);
106
+ if (this._parsing.has(text)) {
107
+ const statement = this._parsing.get(text);
108
+ const query = new CollectQuery(statement, text, args, null, this.config.queryTimeout);
170
109
  this._registerBatch().registerQuery(query);
171
110
  return query.future;
172
- });
111
+ }
112
+ const query = new CollectQuery(this._nextStatement(), text, args, null, this.config.queryTimeout);
113
+ this._parsing.set(text, query.statement);
114
+ this._registerBatch().registerParse(query);
115
+ this._registerBatch().registerQuery(query);
116
+ return query.future;
173
117
  }
174
118
  /**
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
119
+ * Like {@link query}, but for statements that don't return rows (INSERT/UPDATE/DDL/etc).
181
120
  *
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
121
+ * @example
122
+ * await conn.execute`UPDATE users SET name = ${name} WHERE id = ${id}`
123
+ */
124
+ execute(templates, ...params) {
125
+ if (this.isClosed)
126
+ return Future.reject(ErrConnectionClosed);
127
+ if (this._reconnecting)
128
+ return this._reconnecting.andThen(() => this._performExecute(templates, ...params));
129
+ return this._performExecute(templates, ...params);
130
+ }
131
+ _performExecute(templates, ...params) {
132
+ const { text, args } = compileSqlTemplate(templates, params);
133
+ if (this.config.logLevel === 'query') {
134
+ console.log(`\nQUERY: ${text}\n${args.length !== 0 ? `ARGUMENTS: [${args}]\n` : ""}`);
135
+ }
136
+ if (this._parsed.has(text)) {
137
+ const meta = this._parsed.get(text);
138
+ const query = new ExecuteQuery(meta.statement, text, args, this.config.queryTimeout);
139
+ this._registerBatch().registerQuery(query);
140
+ return query.future;
141
+ }
142
+ if (this._parsing.has(text)) {
143
+ const statement = this._parsing.get(text);
144
+ const query = new ExecuteQuery(statement, text, args, this.config.queryTimeout);
145
+ this._registerBatch().registerQuery(query);
146
+ return query.future;
147
+ }
148
+ const query = new ExecuteQuery(this._nextStatement(), text, args, this.config.queryTimeout);
149
+ this._parsing.set(text, query.statement);
150
+ this._registerBatch().registerParse(query);
151
+ this._registerBatch().registerQuery(query);
152
+ return query.future;
153
+ }
154
+ /**
155
+ * Runs `txCallback` inside `BEGIN`/`COMMIT`, rolling back on error.
185
156
  *
186
157
  * @example
187
- * ```ts
188
- * const result = await conn.begin(async tx => {
189
- * await tx.query`INSERT INTO accounts (id, balance) VALUES (1, 100)`
158
+ * await conn.begin(async tx => {
190
159
  * await tx.query`UPDATE accounts SET balance = balance - 10 WHERE id = 1`
191
- * return { success: true }
192
160
  * })
193
- * ```
194
161
  */
195
162
  begin(txCallback) {
196
- if (!this._isOpened)
163
+ if (this.isClosed)
197
164
  return Future.reject(ErrConnectionClosed);
198
- if (this._isReconnecting)
199
- return Future.reject(ErrConnectionReconnecting);
200
- const tx = new Transaction(this);
201
165
  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
- }));
166
+ .andThen(() => this.execute `begin`)
167
+ .andThen(() => {
168
+ const tx = new Transaction(this);
169
+ return Future.of(txCallback(tx))
170
+ .tap(() => {
171
+ if (tx.isActive)
172
+ return tx.commit();
173
+ })
174
+ .tapErr(() => {
175
+ if (tx.isActive)
176
+ return tx.rollback();
177
+ });
178
+ });
212
179
  }
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
- */
180
+ /** Sends a `pg_notify` message on `channelName` (payload ≤ 8000 bytes). */
224
181
  notify(channelName, payload = "") {
225
- if (!this._isOpened)
182
+ if (this.isClosed)
226
183
  return Future.reject(ErrConnectionClosed);
227
- if (this._isReconnecting)
228
- return Future.reject(ErrConnectionReconnecting);
229
- return this.query `select pg_notify(${channelName}, ${payload})`.map(() => { });
184
+ return this.execute `select pg_notify(${channelName}, ${payload})`.map(() => { });
230
185
  }
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
- */
186
+ /** Subscribes `callback` to `channelName`, issuing `LISTEN` on first subscription. */
242
187
  listen(channelName, callback) {
243
- if (!this._isOpened)
188
+ if (this.isClosed)
244
189
  return Future.reject(ErrConnectionClosed);
245
- if (this._isReconnecting)
246
- return Future.reject(ErrConnectionReconnecting);
247
190
  if (!this._listeningCallbacks.has(channelName)) {
248
191
  this._listeningCallbacks.set(channelName, new Set());
249
192
  }
250
193
  const callbackSet = this._listeningCallbacks.get(channelName);
251
194
  callbackSet.add(callback);
252
- return this.query `listen ${sql.ident(channelName)};`.map(() => { });
195
+ return this.execute `listen ${sql.ident(channelName)};`;
253
196
  }
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
- */
197
+ /** Unsubscribes `callback`, issuing `UNLISTEN` once no callbacks remain. */
265
198
  unlisten(channelName, callback) {
266
- if (!this._isOpened)
199
+ if (this.isClosed)
267
200
  return Future.reject(ErrConnectionClosed);
268
- if (this._isReconnecting)
269
- return Future.reject(ErrConnectionReconnecting);
270
201
  if (!this._listeningCallbacks.has(channelName)) {
271
202
  return Ok();
272
203
  }
@@ -274,76 +205,36 @@ export class Connection {
274
205
  callbackSet.delete(callback);
275
206
  if (callbackSet.size === 0) {
276
207
  this._listeningCallbacks.delete(channelName);
277
- return this.query `unlisten ${sql.ident(channelName)};`.map(() => { });
208
+ return this.execute `unlisten ${sql.ident(channelName)};`.map(() => { });
278
209
  }
279
210
  return Ok();
280
211
  }
281
212
  /**
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' } });
213
+ * Streams query results as a `ReadableStream`, without buffering rows in memory.
214
+ * Ideal for large result sets or piping straight into an HTTP response.
298
215
  *
299
216
  * @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
- * }
217
+ * for await (const row of conn.stream<User>`SELECT * FROM orders`) { ... }
305
218
  */
306
219
  stream(templates, ...params) {
307
- if (!this._isOpened)
220
+ if (this.isClosed)
308
221
  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
222
  let controller;
316
223
  const stream = new ReadableStream({
317
224
  start: c => {
318
225
  controller = c;
319
226
  }
320
227
  });
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);
228
+ if (this._reconnecting) {
229
+ this._reconnecting
230
+ .tap(() => this._performStream(templates, params, controller))
231
+ .tapErr(err => controller.error(err));
325
232
  return stream;
326
233
  }
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();
234
+ this._performStream(templates, params, controller);
340
235
  return stream;
341
236
  }
342
- _streamWithController(templates, params, controller) {
343
- if (!this._isOpened)
344
- throw ErrConnectionClosed;
345
- if (this._isReconnecting)
346
- throw ErrConnectionReconnecting;
237
+ _performStream(templates, params, controller) {
347
238
  const { text, args } = compileSqlTemplate(templates, params);
348
239
  if (this.config.logLevel === 'query') {
349
240
  console.log(`\nQUERY: ${text}\n${args.length !== 0 ? `ARGUMENTS: [${args}]\n` : ""}`);
@@ -354,56 +245,61 @@ export class Connection {
354
245
  this._registerBatch().registerQuery(query);
355
246
  return;
356
247
  }
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);
248
+ if (this._parsing.has(text)) {
249
+ const statement = this._parsing.get(text);
250
+ const query = new StreamQuery(statement, text, args, controller, null, this.config.queryTimeout);
366
251
  this._registerBatch().registerQuery(query);
367
- })
368
- .tapErr(err => {
369
- controller.error(err);
370
- })
371
- .recover();
252
+ return;
253
+ }
254
+ const query = new StreamQuery(this._nextStatement(), text, args, controller, null, this.config.queryTimeout);
255
+ this._parsing.set(text, query.statement);
256
+ this._registerBatch().registerParse(query);
257
+ this._registerBatch().registerQuery(query);
372
258
  }
373
- _registerReconnect() {
374
- if (this._isReconnecting)
259
+ _reconnect() {
260
+ if (this.isClosed)
261
+ return;
262
+ if (this._reconnecting)
375
263
  return;
376
- this._isReconnecting = true;
264
+ this._reconnecting = this._performReconnect()
265
+ .tap(() => this._reconnecting = null)
266
+ .andThen(() => this._restoreSubscriptions())
267
+ .tapErr(() => {
268
+ this._reconnecting = null;
269
+ this._reconnect();
270
+ })
271
+ .recover();
377
272
  }
378
- _resetConnectionState(cause) {
273
+ _performReconnect() {
274
+ this._socket.destroy();
379
275
  this._parsed.clear();
380
276
  this._parsing.clear();
277
+ this._activeBatch?.reject(ErrConnectionReconnecting);
381
278
  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);
279
+ while (this._batchQueue.hasMore) {
280
+ this._batchQueue.shift.reject(ErrConnectionReconnecting);
281
+ }
282
+ return createSocket(this.config)
283
+ .andThen(socket => upgradeSocket(socket, this.config))
284
+ .andThen(socket => authorizeSocket(socket, this.config))
285
+ .andThen(socket => {
286
+ const connector = new SocketConnector(socket, this._handlePacket.bind(this), () => this._reconnect());
287
+ this._socket = connector;
288
+ return Ok();
289
+ });
389
290
  }
390
291
  _restoreSubscriptions() {
391
292
  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)};`;
293
+ return Future.resolve();
294
+ const futures = Array.from(this._listeningCallbacks.keys()).map(channel => {
295
+ return this.execute `LISTEN ${sql.ident(channel)};`;
395
296
  });
396
- return Promise.all(promises);
297
+ return Future.all(futures).map(() => { });
397
298
  }
398
299
  _getCurrentQuery() {
399
300
  return this._batchQueue.current.current;
400
301
  }
401
- _rejectAllBatches(cause) {
402
- while (this._batchQueue.hasMore) {
403
- this._batchQueue.shift.reject(cause);
404
- }
405
- }
406
- _handlePacket(type, reader) {
302
+ _handlePacket(type, length, reader) {
407
303
  switch (type) {
408
304
  case ResponseTypes.ParseComplete:
409
305
  {
@@ -413,6 +309,13 @@ export class Connection {
413
309
  case ResponseTypes.BindComplete:
414
310
  {
415
311
  reader.readBindComplete();
312
+ const query = this._getCurrentQuery();
313
+ if (query instanceof ExecuteQuery) {
314
+ break;
315
+ }
316
+ if (!query.columns) {
317
+ query.columns = this._parsed.get(query.text).columns;
318
+ }
416
319
  }
417
320
  break;
418
321
  case ResponseTypes.CloseComplete:
@@ -431,25 +334,27 @@ export class Connection {
431
334
  const query = this._getCurrentQuery();
432
335
  const meta = { statement: query.statement, columns: [] };
433
336
  this._parsing.delete(query.text);
434
- query.resolve(meta);
435
337
  this._parsed.set(query.text, meta);
436
- this._batchQueue.current.next();
437
338
  }
438
339
  break;
439
340
  case ResponseTypes.RowDescription:
440
341
  {
441
342
  const columns = reader.readRowDescription();
442
343
  const query = this._getCurrentQuery();
443
- const meta = { statement: query.statement, columns };
344
+ const meta = {
345
+ statement: query.statement, columns
346
+ };
444
347
  this._parsing.delete(query.text);
445
- query.resolve(meta);
446
348
  this._parsed.set(query.text, meta);
447
- this._batchQueue.current.next();
448
349
  }
449
350
  break;
450
351
  case ResponseTypes.DataRow:
451
352
  {
452
353
  let query = this._getCurrentQuery();
354
+ if (query instanceof ExecuteQuery) {
355
+ reader.skip(length - 4);
356
+ break;
357
+ }
453
358
  query.push(reader.readDataRow(query.columns, this.config.int8toBigint));
454
359
  }
455
360
  break;
@@ -467,10 +372,7 @@ export class Connection {
467
372
  if (this.config.logLevel === 'error' || this.config.logLevel === 'notice' || this.config.logLevel === 'query') {
468
373
  console.log(`\nError: ${error}\n`);
469
374
  }
470
- const query = this._getCurrentQuery();
471
- if (query instanceof ParseQuery) {
472
- this._parsing.delete(query.text);
473
- }
375
+ this._parsing.clear();
474
376
  this._batchQueue.current.reject(error);
475
377
  }
476
378
  break;
@@ -478,6 +380,7 @@ export class Connection {
478
380
  {
479
381
  reader.readReadyForQuery();
480
382
  this._batchQueue.next();
383
+ this._closing && !this._hasQueries && this._closing.resolve();
481
384
  }
482
385
  break;
483
386
  case ResponseTypes.Notice:
@@ -500,26 +403,38 @@ export class Connection {
500
403
  default: console.log('Undeclared response type: ', type);
501
404
  }
502
405
  }
503
- /**
504
- * Checks if the connection is still alive and usable.
505
- * Returns `false` if the socket is destroyed or connection is dead.
506
- */
406
+ get _hasQueries() {
407
+ return this._batchQueue.hasMore || this._activeBatch;
408
+ }
409
+ /** Whether the connection is alive and usable. */
507
410
  get isOpened() {
508
- return this._isOpened;
411
+ return !this._closing && !this._closed;
412
+ }
413
+ /** Whether the connection is closed or closing. */
414
+ get isClosed() {
415
+ return this._closed || !!this._closing;
509
416
  }
510
417
  /**
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
- * ```
418
+ * Closes the connection, awaiting for all pending queries. Not usable afterward.
519
419
  */
520
420
  close() {
521
- this._isOpened = false;
522
- this._socket.destroy();
523
- this._rejectAllBatches(ErrConnectionClosed);
421
+ if (this._closed)
422
+ return Future.resolve();
423
+ if (this._closing) {
424
+ return this._closing.future;
425
+ }
426
+ if (!this._hasQueries) {
427
+ this._closed = true;
428
+ this._socket.destroy();
429
+ return Future.resolve();
430
+ }
431
+ const closing = Future.withResolvers();
432
+ this._closing = closing;
433
+ closing.future.tap(() => {
434
+ this._closing = null;
435
+ this._closed = true;
436
+ this._socket.destroy();
437
+ });
438
+ return closing.future;
524
439
  }
525
440
  }