@m2k-5f/pgtx 2.6.1 → 2.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +237 -487
- 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 +184 -273
- 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 +17 -17
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +22 -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
|
-
void this._restoreSubscriptions();
|
|
112
|
-
this._socket.write(this._registerBatch().end());
|
|
113
|
-
this._batchQueue.push(this._registerBatch());
|
|
114
|
-
})
|
|
115
|
-
.then(() => {
|
|
116
|
-
this._isReconnecting = false;
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
75
|
this._socket.write(batch.end());
|
|
120
76
|
this._activeBatch = null;
|
|
121
77
|
this._batchQueue.push(batch);
|
|
122
78
|
}
|
|
123
79
|
/**
|
|
124
|
-
*
|
|
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
|
-
*
|
|
117
|
+
* Like {@link query}, but for statements that don't return rows (INSERT/UPDATE/DDL/etc).
|
|
176
118
|
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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, meta.columns, this.config.queryTimeout);
|
|
137
|
+
this._registerBatch().registerQuery(query);
|
|
138
|
+
return query.future;
|
|
139
|
+
}
|
|
140
|
+
if (this._parsing.has(text)) {
|
|
141
|
+
const statement = this._parsing.get(text);
|
|
142
|
+
const query = new ExecuteQuery(statement, text, args, null, this.config.queryTimeout);
|
|
143
|
+
this._registerBatch().registerQuery(query);
|
|
144
|
+
return query.future;
|
|
145
|
+
}
|
|
146
|
+
const query = new ExecuteQuery(this._nextStatement(), text, args, null, this.config.queryTimeout);
|
|
147
|
+
this._parsing.set(text, query.statement);
|
|
148
|
+
this._registerBatch().registerParse(query);
|
|
149
|
+
this._registerBatch().registerQuery(query);
|
|
150
|
+
return query.future;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Runs `txCallback` inside `BEGIN`/`COMMIT`, rolling back on error.
|
|
185
154
|
*
|
|
186
155
|
* @example
|
|
187
|
-
*
|
|
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,75 +203,36 @@ export class Connection {
|
|
|
274
203
|
callbackSet.delete(callback);
|
|
275
204
|
if (callbackSet.size === 0) {
|
|
276
205
|
this._listeningCallbacks.delete(channelName);
|
|
277
|
-
return this.
|
|
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
|
-
.catch(err => controller.error(err));
|
|
232
|
+
this._performStream(templates, params, controller);
|
|
339
233
|
return stream;
|
|
340
234
|
}
|
|
341
|
-
|
|
342
|
-
if (!this._isOpened)
|
|
343
|
-
throw ErrConnectionClosed;
|
|
344
|
-
if (this._isReconnecting)
|
|
345
|
-
throw ErrConnectionReconnecting;
|
|
235
|
+
_performStream(templates, params, controller) {
|
|
346
236
|
const { text, args } = compileSqlTemplate(templates, params);
|
|
347
237
|
if (this.config.logLevel === 'query') {
|
|
348
238
|
console.log(`\nQUERY: ${text}\n${args.length !== 0 ? `ARGUMENTS: [${args}]\n` : ""}`);
|
|
@@ -351,54 +241,61 @@ export class Connection {
|
|
|
351
241
|
const meta = this._parsed.get(text);
|
|
352
242
|
const query = new StreamQuery(meta.statement, text, args, controller, meta.columns, this.config.queryTimeout);
|
|
353
243
|
this._registerBatch().registerQuery(query);
|
|
244
|
+
return;
|
|
354
245
|
}
|
|
355
|
-
if (
|
|
356
|
-
const
|
|
357
|
-
|
|
358
|
-
this._registerBatch().registerQuery(parseQuery);
|
|
359
|
-
}
|
|
360
|
-
const future = this._parsing.get(text);
|
|
361
|
-
future
|
|
362
|
-
.tap(meta => {
|
|
363
|
-
const query = new StreamQuery(meta.statement, text, args, controller, meta.columns, this.config.queryTimeout);
|
|
246
|
+
if (this._parsing.has(text)) {
|
|
247
|
+
const statement = this._parsing.get(text);
|
|
248
|
+
const query = new StreamQuery(statement, text, args, controller, null, this.config.queryTimeout);
|
|
364
249
|
this._registerBatch().registerQuery(query);
|
|
365
|
-
|
|
366
|
-
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
const query = new StreamQuery(this._nextStatement(), text, args, controller, null, this.config.queryTimeout);
|
|
253
|
+
this._parsing.set(text, query.statement);
|
|
254
|
+
this._registerBatch().registerParse(query);
|
|
255
|
+
this._registerBatch().registerQuery(query);
|
|
367
256
|
}
|
|
368
|
-
|
|
369
|
-
if (this.
|
|
257
|
+
_reconnect() {
|
|
258
|
+
if (this.isClosed)
|
|
259
|
+
return;
|
|
260
|
+
if (this._reconnecting)
|
|
370
261
|
return;
|
|
371
|
-
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();
|
|
372
270
|
}
|
|
373
|
-
|
|
271
|
+
_performReconnect() {
|
|
272
|
+
this._socket.destroy();
|
|
374
273
|
this._parsed.clear();
|
|
375
274
|
this._parsing.clear();
|
|
275
|
+
this._activeBatch?.reject(ErrConnectionReconnecting);
|
|
376
276
|
this._activeBatch = null;
|
|
377
|
-
this.
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
277
|
+
while (this._batchQueue.hasMore) {
|
|
278
|
+
this._batchQueue.shift.reject(ErrConnectionReconnecting);
|
|
279
|
+
}
|
|
280
|
+
return createAuthorizedSocket(ConnectionRequestWriter.new(), this.config)
|
|
281
|
+
.andThen(socket => {
|
|
282
|
+
const connector = new SocketConnector(socket, (type, length, reader) => this._handlePacket(type, reader, length), () => this._reconnect());
|
|
283
|
+
this._socket = connector;
|
|
284
|
+
return Ok();
|
|
285
|
+
});
|
|
384
286
|
}
|
|
385
287
|
_restoreSubscriptions() {
|
|
386
288
|
if (this._listeningCallbacks.size === 0)
|
|
387
|
-
return
|
|
388
|
-
const
|
|
389
|
-
return this.
|
|
289
|
+
return Future.resolve();
|
|
290
|
+
const futures = Array.from(this._listeningCallbacks.keys()).map(channel => {
|
|
291
|
+
return this.execute `LISTEN ${sql.ident(channel)};`;
|
|
390
292
|
});
|
|
391
|
-
return
|
|
293
|
+
return Future.all(futures).map(() => { });
|
|
392
294
|
}
|
|
393
295
|
_getCurrentQuery() {
|
|
394
296
|
return this._batchQueue.current.current;
|
|
395
297
|
}
|
|
396
|
-
|
|
397
|
-
while (this._batchQueue.hasMore) {
|
|
398
|
-
this._batchQueue.shift.reject(cause);
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
_handlePacket(type, reader) {
|
|
298
|
+
_handlePacket(type, reader, length) {
|
|
402
299
|
switch (type) {
|
|
403
300
|
case ResponseTypes.ParseComplete:
|
|
404
301
|
{
|
|
@@ -408,6 +305,10 @@ export class Connection {
|
|
|
408
305
|
case ResponseTypes.BindComplete:
|
|
409
306
|
{
|
|
410
307
|
reader.readBindComplete();
|
|
308
|
+
const query = this._getCurrentQuery();
|
|
309
|
+
if (!query.columns) {
|
|
310
|
+
query.columns = this._parsed.get(query.text).columns;
|
|
311
|
+
}
|
|
411
312
|
}
|
|
412
313
|
break;
|
|
413
314
|
case ResponseTypes.CloseComplete:
|
|
@@ -426,20 +327,18 @@ export class Connection {
|
|
|
426
327
|
const query = this._getCurrentQuery();
|
|
427
328
|
const meta = { statement: query.statement, columns: [] };
|
|
428
329
|
this._parsing.delete(query.text);
|
|
429
|
-
query.resolve(meta);
|
|
430
330
|
this._parsed.set(query.text, meta);
|
|
431
|
-
this._batchQueue.current.next();
|
|
432
331
|
}
|
|
433
332
|
break;
|
|
434
333
|
case ResponseTypes.RowDescription:
|
|
435
334
|
{
|
|
436
335
|
const columns = reader.readRowDescription();
|
|
437
336
|
const query = this._getCurrentQuery();
|
|
438
|
-
const meta = {
|
|
337
|
+
const meta = {
|
|
338
|
+
statement: query.statement, columns
|
|
339
|
+
};
|
|
439
340
|
this._parsing.delete(query.text);
|
|
440
|
-
query.resolve(meta);
|
|
441
341
|
this._parsed.set(query.text, meta);
|
|
442
|
-
this._batchQueue.current.next();
|
|
443
342
|
}
|
|
444
343
|
break;
|
|
445
344
|
case ResponseTypes.DataRow:
|
|
@@ -462,8 +361,7 @@ export class Connection {
|
|
|
462
361
|
if (this.config.logLevel === 'error' || this.config.logLevel === 'notice' || this.config.logLevel === 'query') {
|
|
463
362
|
console.log(`\nError: ${error}\n`);
|
|
464
363
|
}
|
|
465
|
-
|
|
466
|
-
this._parsing.delete(query.text);
|
|
364
|
+
this._parsing.clear();
|
|
467
365
|
this._batchQueue.current.reject(error);
|
|
468
366
|
}
|
|
469
367
|
break;
|
|
@@ -471,6 +369,7 @@ export class Connection {
|
|
|
471
369
|
{
|
|
472
370
|
reader.readReadyForQuery();
|
|
473
371
|
this._batchQueue.next();
|
|
372
|
+
this._closing && !this._hasQueries && this._closing.resolve();
|
|
474
373
|
}
|
|
475
374
|
break;
|
|
476
375
|
case ResponseTypes.Notice:
|
|
@@ -493,26 +392,38 @@ export class Connection {
|
|
|
493
392
|
default: console.log('Undeclared response type: ', type);
|
|
494
393
|
}
|
|
495
394
|
}
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
395
|
+
get _hasQueries() {
|
|
396
|
+
return this._batchQueue.hasMore || this._activeBatch;
|
|
397
|
+
}
|
|
398
|
+
/** Whether the connection is alive and usable. */
|
|
500
399
|
get isOpened() {
|
|
501
|
-
return this.
|
|
400
|
+
return !this._closing && !this._closed;
|
|
401
|
+
}
|
|
402
|
+
/** Whether the connection is closed or closing. */
|
|
403
|
+
get isClosed() {
|
|
404
|
+
return this._closed || !!this._closing;
|
|
502
405
|
}
|
|
503
406
|
/**
|
|
504
|
-
* Closes the connection
|
|
505
|
-
* All pending queries will be rejected with an error.
|
|
506
|
-
* The connection cannot be used after this call.
|
|
507
|
-
*
|
|
508
|
-
* @example
|
|
509
|
-
* ```ts
|
|
510
|
-
* conn.close()
|
|
511
|
-
* ```
|
|
407
|
+
* Closes the connection, awaiting for all pending queries. Not usable afterward.
|
|
512
408
|
*/
|
|
513
409
|
close() {
|
|
514
|
-
this.
|
|
515
|
-
|
|
516
|
-
this.
|
|
410
|
+
if (this._closed)
|
|
411
|
+
return Future.resolve();
|
|
412
|
+
if (this._closing) {
|
|
413
|
+
return this._closing.future;
|
|
414
|
+
}
|
|
415
|
+
if (!this._hasQueries) {
|
|
416
|
+
this._closed = true;
|
|
417
|
+
this._socket.destroy();
|
|
418
|
+
return Future.resolve();
|
|
419
|
+
}
|
|
420
|
+
const closing = Future.withResolvers();
|
|
421
|
+
this._closing = closing;
|
|
422
|
+
closing.future.tap(() => {
|
|
423
|
+
this._closing = null;
|
|
424
|
+
this._closed = true;
|
|
425
|
+
this._socket.destroy();
|
|
426
|
+
});
|
|
427
|
+
return closing.future;
|
|
517
428
|
}
|
|
518
429
|
}
|