@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.
- package/README.md +244 -494
- 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 +199 -284
- package/dist/error.d.ts +4 -0
- package/dist/error.d.ts.map +1 -1
- package/dist/error.js +4 -0
- 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-request-writer.d.ts +1 -0
- package/dist/protocol/connection-request-writer.d.ts.map +1 -1
- package/dist/protocol/connection-request-writer.js +6 -0
- 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/constants.d.ts +2 -0
- package/dist/protocol/constants.d.ts.map +1 -1
- package/dist/protocol/constants.js +3 -1
- package/dist/protocol/socket-authorization.d.ts +5 -4
- package/dist/protocol/socket-authorization.d.ts.map +1 -1
- package/dist/protocol/socket-authorization.js +167 -74
- 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 +14 -9
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/connection.js
CHANGED
|
@@ -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 {
|
|
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
|
-
*
|
|
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() {
|
|
46
31
|
return `s-${this._stmtCounter++}`;
|
|
47
32
|
}
|
|
48
|
-
constructor(
|
|
33
|
+
constructor(socket, config) {
|
|
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,
|
|
45
|
+
this._socket = new SocketConnector(socket, this._handlePacket.bind(this), () => 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,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 || '
|
|
57
|
+
syncShedule: config.syncShedule || 'afterMicrotask',
|
|
58
|
+
ssl: config.caPath ? 'require' : (config.ssl || 'prefer')
|
|
86
59
|
};
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
.andThen(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
|
-
|
|
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 (
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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 (
|
|
89
|
+
if (this.isClosed)
|
|
149
90
|
return Future.reject(ErrConnectionClosed);
|
|
150
|
-
if (this.
|
|
151
|
-
return
|
|
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
|
|
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 (
|
|
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);
|
|
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
|
-
*
|
|
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
|
-
* @
|
|
183
|
-
*
|
|
184
|
-
|
|
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
|
-
*
|
|
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 (
|
|
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(() =>
|
|
203
|
-
.andThen(() =>
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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 (
|
|
182
|
+
if (this.isClosed)
|
|
226
183
|
return Future.reject(ErrConnectionClosed);
|
|
227
|
-
|
|
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 (
|
|
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.
|
|
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 (
|
|
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.
|
|
208
|
+
return this.execute `unlisten ${sql.ident(channelName)};`.map(() => { });
|
|
278
209
|
}
|
|
279
210
|
return Ok();
|
|
280
211
|
}
|
|
281
212
|
/**
|
|
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' } });
|
|
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
|
-
*
|
|
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 (
|
|
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.
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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);
|
|
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
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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
|
-
|
|
374
|
-
if (this.
|
|
259
|
+
_reconnect() {
|
|
260
|
+
if (this.isClosed)
|
|
261
|
+
return;
|
|
262
|
+
if (this._reconnecting)
|
|
375
263
|
return;
|
|
376
|
-
this.
|
|
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
|
-
|
|
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.
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
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
|
|
393
|
-
const
|
|
394
|
-
return this.
|
|
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
|
|
297
|
+
return Future.all(futures).map(() => { });
|
|
397
298
|
}
|
|
398
299
|
_getCurrentQuery() {
|
|
399
300
|
return this._batchQueue.current.current;
|
|
400
301
|
}
|
|
401
|
-
|
|
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 = {
|
|
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
|
-
|
|
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
|
-
|
|
505
|
-
|
|
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.
|
|
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
|
|
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.
|
|
522
|
-
|
|
523
|
-
this.
|
|
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
|
}
|