@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.
- package/README.md +231 -472
- package/dist/batch.d.ts +2 -1
- package/dist/batch.d.ts.map +1 -1
- package/dist/batch.js +8 -7
- package/dist/connection.d.ts +39 -150
- package/dist/connection.d.ts.map +1 -1
- package/dist/connection.js +190 -279
- package/dist/pool.d.ts +29 -181
- package/dist/pool.d.ts.map +1 -1
- package/dist/pool.js +49 -180
- package/dist/protocol/connection-response-reader.d.ts +1 -0
- package/dist/protocol/connection-response-reader.d.ts.map +1 -1
- package/dist/protocol/connection-response-reader.js +3 -0
- package/dist/protocol/socket-connector.d.ts +5 -5
- package/dist/protocol/socket-connector.d.ts.map +1 -1
- package/dist/protocol/socket-connector.js +24 -15
- package/dist/query.d.ts +14 -17
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +20 -17
- package/dist/transaction.d.ts +9 -1
- package/dist/transaction.d.ts.map +1 -1
- package/dist/transaction.js +11 -0
- package/dist/types.d.ts +9 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/connection.js
CHANGED
|
@@ -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 {
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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.
|
|
51
|
-
this.
|
|
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,
|
|
45
|
+
this._socket = new SocketConnector(socket, (type, length, reader) => this._handlePacket(type, reader, length), () => this._reconnect());
|
|
60
46
|
}
|
|
61
47
|
/**
|
|
62
|
-
*
|
|
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 || '
|
|
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
|
-
|
|
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 (
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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 (
|
|
87
|
+
if (this.isClosed)
|
|
149
88
|
return Future.reject(ErrConnectionClosed);
|
|
150
|
-
if (this.
|
|
151
|
-
return
|
|
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
|
|
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 (
|
|
163
|
-
const
|
|
164
|
-
|
|
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
|
-
*
|
|
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
|
-
* @
|
|
183
|
-
*
|
|
184
|
-
|
|
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
|
-
*
|
|
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 (
|
|
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(() =>
|
|
203
|
-
.andThen(() =>
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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 (
|
|
180
|
+
if (this.isClosed)
|
|
226
181
|
return Future.reject(ErrConnectionClosed);
|
|
227
|
-
|
|
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 (
|
|
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.
|
|
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 (
|
|
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.
|
|
206
|
+
return this.execute `unlisten ${sql.ident(channelName)};`.map(() => { });
|
|
278
207
|
}
|
|
279
208
|
return Ok();
|
|
280
209
|
}
|
|
281
210
|
/**
|
|
282
|
-
*
|
|
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
|
-
*
|
|
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 (
|
|
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.
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
358
|
-
const
|
|
359
|
-
|
|
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
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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
|
-
|
|
374
|
-
if (this.
|
|
257
|
+
_reconnect() {
|
|
258
|
+
if (this.isClosed)
|
|
259
|
+
return;
|
|
260
|
+
if (this._reconnecting)
|
|
375
261
|
return;
|
|
376
|
-
this.
|
|
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
|
-
|
|
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.
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
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
|
|
393
|
-
const
|
|
394
|
-
return this.
|
|
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
|
|
293
|
+
return Future.all(futures).map(() => { });
|
|
397
294
|
}
|
|
398
295
|
_getCurrentQuery() {
|
|
399
296
|
return this._batchQueue.current.current;
|
|
400
297
|
}
|
|
401
|
-
|
|
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 = {
|
|
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
|
-
|
|
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
|
-
|
|
505
|
-
|
|
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.
|
|
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
|
|
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.
|
|
522
|
-
|
|
523
|
-
this.
|
|
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
|
}
|