@m2k-5f/pgtx 2.4.1 → 2.5.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 +157 -36
- package/dist/connection.d.ts +33 -21
- package/dist/connection.d.ts.map +1 -1
- package/dist/connection.js +95 -14
- package/dist/pool.d.ts +73 -3
- package/dist/pool.d.ts.map +1 -1
- package/dist/pool.js +119 -8
- package/dist/protocol/connection-response-reader.d.ts +5 -2
- package/dist/protocol/connection-response-reader.d.ts.map +1 -1
- package/dist/protocol/connection-response-reader.js +6 -23
- package/dist/protocol/socket-authorization.d.ts +1 -0
- package/dist/protocol/socket-authorization.d.ts.map +1 -1
- package/dist/protocol/socket-authorization.js +80 -83
- package/dist/protocol/socket-connector.d.ts +1 -1
- package/dist/protocol/socket-connector.d.ts.map +1 -1
- package/dist/protocol/socket-connector.js +2 -1
- package/dist/query.d.ts +21 -2
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +42 -4
- package/dist/transaction.d.ts +2 -2
- package/dist/transaction.d.ts.map +1 -1
- package/dist/transaction.js +4 -2
- package/package.json +54 -55
package/README.md
CHANGED
|
@@ -55,6 +55,7 @@
|
|
|
55
55
|
|
|
56
56
|
- **Pipeline queries** — Automatic query multiplexing over PostgreSQL pipeline protocol
|
|
57
57
|
- **Tagged templates** — Natural SQL with type safety
|
|
58
|
+
- **Native Web Streams API** — Memory-efficient data streaming via `pool.stream()`
|
|
58
59
|
- **Transactions & Savepoints** — Nested transactions with rollback
|
|
59
60
|
- **Bulk inserts** — Auto-extract columns from objects
|
|
60
61
|
- **Dynamic updates** — Generate SET clauses from objects
|
|
@@ -63,42 +64,64 @@
|
|
|
63
64
|
- **Connection pool** — Auto-management connections with support for pipeline queries via the pool itself.
|
|
64
65
|
- **Zero dependencies** — Lightweight and blazing
|
|
65
66
|
|
|
66
|
-
|
|
67
|
+
---
|
|
67
68
|
|
|
68
|
-
|
|
69
|
+
## ⚡ Performance
|
|
69
70
|
|
|
70
|
-
|
|
71
|
-
*Measured using `mitata` inside GitHub Actions cloud runners (Ubuntu, 2 vCPUs, 10 DB Connections).*
|
|
71
|
+
All benchmarks are executed on **GitHub Actions** (Ubuntu, 2 vCPUs) and are fully reproducible. Benchmark sources are included in this repository.
|
|
72
72
|
|
|
73
|
-
|
|
74
|
-
| :--- | :---: | :---: | :---: |
|
|
75
|
-
| **Pgtx (Pipeline)** | **19.21 ms** | **Baseline (3.6×)** | **874.71 KB** |
|
|
76
|
-
| Postgres.js (Pipeline) | 70.50 ms | 3.6× Slower | 1.29 MB |
|
|
77
|
-
| node-postgres (no pipeline) | 327.07 ms | 17.0× Slower | 4.05 MB |
|
|
73
|
+
### 1. PostgreSQL Pipeline Stress Test
|
|
78
74
|
|
|
79
|
-
|
|
75
|
+
**3000 concurrent parameterized `SELECT` queries**
|
|
80
76
|
|
|
81
|
-
|
|
82
|
-
|
|
77
|
+
* Connection pool: **10 connections**
|
|
78
|
+
* Measured with **mitata**
|
|
83
79
|
|
|
84
|
-
|
|
85
|
-
|
|
80
|
+
| Driver | Avg Time | Relative Speed | Memory (p75) |
|
|
81
|
+
| :--------------------- | -----------: | -------------------: | -----------: |
|
|
82
|
+
| **Pgtx (Pipeline)** | **24.18 ms** | **Baseline (1.00×)** | **≈2.5 MB** |
|
|
83
|
+
| Postgres.js (Pipeline) | 83.36 ms | **3.45× slower** | ≈7.6 MB |
|
|
84
|
+
| node-postgres (`pg`) | 377.95 ms | **15.63× slower** | ≈11.4 MB |
|
|
86
85
|
|
|
87
|
-
|
|
86
|
+
### 2. Real-World HTTP Throughput
|
|
88
87
|
|
|
89
|
-
|
|
88
|
+
Simple `node:http` server serving a PostgreSQL-backed endpoint.
|
|
90
89
|
|
|
91
|
-
|
|
92
|
-
* Synchronous protocol encoding
|
|
93
|
-
* Batched socket writes
|
|
94
|
-
* Automatic prepared statement caching
|
|
95
|
-
* Row description caching
|
|
96
|
-
* Zero-dependency implementation
|
|
97
|
-
* Binary protocol support
|
|
90
|
+
Measured with:
|
|
98
91
|
|
|
99
|
-
|
|
92
|
+
```bash
|
|
93
|
+
wrk -t2 -c<N> -d10s http://localhost:3000/users
|
|
94
|
+
```
|
|
100
95
|
|
|
101
|
-
|
|
96
|
+
| Concurrent Connections | Pgtx | Postgres.js | Speedup |
|
|
97
|
+
| ---------------------: | ---------------: | -----------: | --------: |
|
|
98
|
+
| 50 | 5,272 req/s | 5,691 req/s | 0.93× |
|
|
99
|
+
| 200 | **12,918 req/s** | 6,724 req/s | **1.92×** |
|
|
100
|
+
| 1000 | **21,429 req/s** | 8,423 req/s | **2.54×** |
|
|
101
|
+
| 10000 | **22,486 req/s** | 12,764 req/s | **1.76×** |
|
|
102
|
+
|
|
103
|
+
### Why is Pgtx fast?
|
|
104
|
+
|
|
105
|
+
Pgtx is engineered for **throughput**, not for minimizing the latency of individual queries.
|
|
106
|
+
|
|
107
|
+
Instead of optimizing a single request in isolation, Pgtx minimizes per-query overhead under sustained concurrent load by combining:
|
|
108
|
+
|
|
109
|
+
* Pipeline query multiplexing
|
|
110
|
+
* Synchronous PostgreSQL wire protocol encoding
|
|
111
|
+
* Batched socket writes
|
|
112
|
+
* Automatic prepared statement caching
|
|
113
|
+
* Prepared statement deduplication
|
|
114
|
+
* Row description caching
|
|
115
|
+
* Binary protocol support
|
|
116
|
+
* Zero-dependency implementation
|
|
117
|
+
|
|
118
|
+
As concurrency increases, these optimizations significantly reduce protocol overhead, allowing Pgtx to scale more efficiently than traditional PostgreSQL drivers.
|
|
119
|
+
|
|
120
|
+
In the `mitata` benchmark, Pgtx also demonstrated approximately **3× lower memory usage** than Postgres.js while processing the same workload, reducing allocation pressure and improving sustained throughput under heavy load.
|
|
121
|
+
|
|
122
|
+
> **Blazing** isn't just a tagline — it's backed by reproducible benchmarks.
|
|
123
|
+
|
|
124
|
+
---
|
|
102
125
|
|
|
103
126
|
## 🔥 Why Pgtx?
|
|
104
127
|
|
|
@@ -156,6 +179,64 @@
|
|
|
156
179
|
> 🚀 Pgtx automatically groups concurrent queries into pipeline batches, reducing network overhead by up to 5x compared to sequential queries.
|
|
157
180
|
|
|
158
181
|
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
### High-Performance Data Streaming (`pool.stream`)
|
|
186
|
+
|
|
187
|
+
For heavy database lookups (exporting millions of rows, bulk reports, or large analytical dumps), memory accumulation is the ultimate killer of backend stability. Storing rows in a standard JavaScript array causes massive heap pollution and triggers blocking Garbage Collection spikes.
|
|
188
|
+
|
|
189
|
+
Pgtx solves this at the protocol level by introducing `pool.stream()`, which bypasses row aggregation entirely and pipes rows transitively directly into a native Web **`ReadableStream`**.
|
|
190
|
+
|
|
191
|
+
#### 1. Ultra-Low Memory Row Iteration
|
|
192
|
+
You can consume database rows sequentially using standard `for await...of` syntax. Rows are processed and evicted from memory the moment they arrive from the network socket buffer.
|
|
193
|
+
|
|
194
|
+
```typescript
|
|
195
|
+
interface HeavyLog { id: number; data: string; timestamp: Date; }
|
|
196
|
+
|
|
197
|
+
const logStream = pool.stream<HeavyLog>`
|
|
198
|
+
SELECT id, data, timestamp FROM application_logs WHERE level = ${'error'}
|
|
199
|
+
`;
|
|
200
|
+
|
|
201
|
+
for await (const log of logStream) {
|
|
202
|
+
// Each log object is parsed on-the-fly and processed instantly.
|
|
203
|
+
// Zero rows are accumulated in the internal driver state!
|
|
204
|
+
console.log(`[${log.timestamp.toISOString()}] ${log.data}`);
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
#### 2. Streaming Directly to HTTP Responses (`Bun.serve`)
|
|
209
|
+
Since Pgtx implements the standardized Web Streams API, you can bridge your database query directly into an HTTP response body with absolutely zero intermediate buffers.
|
|
210
|
+
|
|
211
|
+
```typescript
|
|
212
|
+
import { Pool } from "@m2k-5f/pgtx";
|
|
213
|
+
|
|
214
|
+
const pool = new Pool({ /* ... config ... */ });
|
|
215
|
+
|
|
216
|
+
export default {
|
|
217
|
+
port: 3000,
|
|
218
|
+
async fetch(request) {
|
|
219
|
+
const url = new URL(request.url);
|
|
220
|
+
|
|
221
|
+
if (url.pathname === "/export/users") {
|
|
222
|
+
// Synchronously returns a stream handle even if pool sockets are currently busy
|
|
223
|
+
const userStream = pool.stream`SELECT id, email, profile_metadata FROM giant_user_table`;
|
|
224
|
+
|
|
225
|
+
return new Response(userStream, {
|
|
226
|
+
headers: {
|
|
227
|
+
"Content-Type": "application/json",
|
|
228
|
+
"Transfer-Encoding": "chunked",
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return new Response("Not Found", { status: 404 });
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
159
240
|
### Transactions & Savepoints
|
|
160
241
|
|
|
161
242
|
```typescript
|
|
@@ -170,16 +251,40 @@
|
|
|
170
251
|
})
|
|
171
252
|
```
|
|
172
253
|
|
|
254
|
+
---
|
|
173
255
|
|
|
174
256
|
### Async Notifications (LISTEN / NOTIFY)
|
|
175
257
|
|
|
176
|
-
Pgtx natively handles PostgreSQL `LISTEN/NOTIFY` protocol messages asynchronously without interrupting multiplexed query pipeline.
|
|
258
|
+
Pgtx natively handles PostgreSQL `LISTEN/NOTIFY` protocol messages asynchronously without interrupting the multiplexed query pipeline. It offers two distinct ways to subscribe: high-level pool-driven subscriptions and low-level connection pinning.
|
|
177
259
|
|
|
260
|
+
#### 1. Sending a Notification
|
|
261
|
+
Notifications are atomic and can be triggered directly from the `Pool` utilizing any available socket:
|
|
178
262
|
```typescript
|
|
179
|
-
// 1. Sending a notification
|
|
180
263
|
await pool.notify('user_events', JSON.stringify({ id: 42, action: 'signup' }))
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
#### 2. High-Level Pool Subscription (Recommended)
|
|
267
|
+
You can subscribe directly via the `Pool` instance. Pgtx will automatically borrow a dedicated connection from the pool, issue the `LISTEN` command, and seamlessly manage its lifecycle.
|
|
268
|
+
|
|
269
|
+
The method returns a lazy, async **unsubscribe function** that cleanly handles `UNLISTEN` and returns the connection to the pool when invoked.
|
|
270
|
+
|
|
271
|
+
```typescript
|
|
272
|
+
const onEvent = (payload: string) => {
|
|
273
|
+
console.log(`Received payload: ${payload}`)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Automatically borrows a connection and sets up the listener
|
|
277
|
+
const unsubscribe = await pool.listen('user_events', onEvent)
|
|
278
|
+
|
|
279
|
+
// When the subscription is no longer needed (e.g., server shutdown):
|
|
280
|
+
// It automatically sends UNLISTEN and releases the connection back to the pool!
|
|
281
|
+
await unsubscribe()
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
#### 3. Low-Level Connection Subscription (Stateful)
|
|
285
|
+
If you need complete control over a specific PostgreSQL backend process, you can acquire an explicit `Connection` instance. This allows you to multiplex multiple callbacks onto a single channel seamlessly.
|
|
181
286
|
|
|
182
|
-
|
|
287
|
+
```typescript
|
|
183
288
|
const conn = await pool.acquire()
|
|
184
289
|
|
|
185
290
|
const onEvent = (payload: string) => {
|
|
@@ -190,15 +295,17 @@
|
|
|
190
295
|
await conn.listen('user_events', onEvent)
|
|
191
296
|
await conn.listen('user_events', (data) => logToFile(data))
|
|
192
297
|
|
|
193
|
-
//
|
|
298
|
+
// Cleans up callbacks (Sends UNLISTEN only when the channel has zero callbacks left)
|
|
194
299
|
await conn.unlisten('user_events', onEvent)
|
|
195
300
|
|
|
196
|
-
//
|
|
197
|
-
// Do NOT release it back to the pool
|
|
301
|
+
// ⚠️ Manual lifecycle management is strictly required for this pattern!
|
|
302
|
+
// Do NOT release it back to the pool until you are completely done listening.
|
|
303
|
+
this.release(conn)
|
|
198
304
|
```
|
|
199
305
|
|
|
200
|
-
> ⚠️ **Architecture Note:** While `notify` is
|
|
306
|
+
> ⚠️ **Architecture Note:** While `pool.notify` is a fire-and-forget atomic command, subscription states (`LISTEN`/`UNLISTEN`) are strictly tied to specific PostgreSQL backend processes. Using the high-level `pool.listen()` is strongly recommended for application code, as it encapsulates socket management into an elegant, leak-proof callback boundary.
|
|
201
307
|
|
|
308
|
+
---
|
|
202
309
|
|
|
203
310
|
### Bulk Inserts
|
|
204
311
|
|
|
@@ -214,6 +321,8 @@
|
|
|
214
321
|
// INSERT INTO users (name, email) VALUES ($1, $2), ($3, $4)
|
|
215
322
|
```
|
|
216
323
|
|
|
324
|
+
---
|
|
325
|
+
|
|
217
326
|
### Dynamic Updates
|
|
218
327
|
|
|
219
328
|
```typescript
|
|
@@ -225,6 +334,8 @@
|
|
|
225
334
|
// UPDATE users SET status = $1, last_login = $2 WHERE id = $3
|
|
226
335
|
```
|
|
227
336
|
|
|
337
|
+
---
|
|
338
|
+
|
|
228
339
|
### Recursive Fragments
|
|
229
340
|
|
|
230
341
|
```typescript
|
|
@@ -237,6 +348,8 @@
|
|
|
237
348
|
`
|
|
238
349
|
```
|
|
239
350
|
|
|
351
|
+
---
|
|
352
|
+
|
|
240
353
|
### Smart Lists
|
|
241
354
|
|
|
242
355
|
```typescript
|
|
@@ -255,6 +368,8 @@
|
|
|
255
368
|
`
|
|
256
369
|
```
|
|
257
370
|
|
|
371
|
+
---
|
|
372
|
+
|
|
258
373
|
### Clean WHERE Clauses
|
|
259
374
|
|
|
260
375
|
```typescript
|
|
@@ -265,6 +380,8 @@
|
|
|
265
380
|
// SELECT * FROM users WHERE role = $1 AND active = $2
|
|
266
381
|
```
|
|
267
382
|
|
|
383
|
+
---
|
|
384
|
+
|
|
268
385
|
### Conditional Logic
|
|
269
386
|
|
|
270
387
|
```typescript
|
|
@@ -342,7 +459,7 @@
|
|
|
342
459
|
notify(channelName: string, payload?: string): Future<[], PostgresError>
|
|
343
460
|
listen(channelName: string, callback: (payload: string) => void): Future<[], PostgresError>
|
|
344
461
|
unlisten(channelName: string, callback: (payload: string) => void): Future<[], PostgresError>
|
|
345
|
-
|
|
462
|
+
stream<T extends Record<string, any>>(templates: TemplateStringsArray, ...params: any[]): ReadableStream<T>
|
|
346
463
|
get isAlive(): boolean
|
|
347
464
|
close(): void
|
|
348
465
|
}
|
|
@@ -353,7 +470,8 @@
|
|
|
353
470
|
host: string
|
|
354
471
|
port: number
|
|
355
472
|
database: string
|
|
356
|
-
|
|
473
|
+
queryTimeout?: number // default: 30 srconds
|
|
474
|
+
logLevel?: 'none' | 'error' | 'notice' | 'query' // default: "error"
|
|
357
475
|
}
|
|
358
476
|
```
|
|
359
477
|
|
|
@@ -363,10 +481,13 @@
|
|
|
363
481
|
class Pool {
|
|
364
482
|
constructor(config: PoolConfig)
|
|
365
483
|
|
|
366
|
-
query<T>(strings: TemplateStringsArray, ...values: any[]): Future<T[],
|
|
484
|
+
query<T>(strings: TemplateStringsArray, ...values: any[]): Future<T[], PostgresError>
|
|
367
485
|
begin<T>(callback: (tx: Transaction) => Promise<T>): Future<T, Error>
|
|
368
486
|
notify(channelName: string, payload?: string): Future<[], PostgresError>
|
|
369
|
-
|
|
487
|
+
listen(channel: string, callback: (payload: string) => void): Future<() => Promise<void>, PostgresError>
|
|
488
|
+
stream<T extends Record<string, any>>(templates: TemplateStringsArray, ...args: any[]): ReadableStream<T>
|
|
489
|
+
withAcquire<T>(fn: (conn: Connection) => Promise<T>): Future<T, Error>
|
|
490
|
+
acquire(): Future<Connection, PostgresError>
|
|
370
491
|
release(conn: Connection): void
|
|
371
492
|
close(): void
|
|
372
493
|
|
package/dist/connection.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Branded
|
|
1
|
+
import { Branded } from "./types";
|
|
2
2
|
import { Transaction } from "./transaction";
|
|
3
3
|
import { Future } from 'fluent-future';
|
|
4
4
|
import { PostgresError } from "./error";
|
|
@@ -11,23 +11,7 @@ export type ConnectionParams = {
|
|
|
11
11
|
database: string;
|
|
12
12
|
logLevel?: LogLevel;
|
|
13
13
|
int8toBigint?: boolean;
|
|
14
|
-
|
|
15
|
-
export type ExecuteQueueUnit = {
|
|
16
|
-
rows: (string | null)[][];
|
|
17
|
-
resolve: (value: any) => void;
|
|
18
|
-
reject: (err: Error) => void;
|
|
19
|
-
statementName: StatementName;
|
|
20
|
-
};
|
|
21
|
-
export type ParsingQueueUnit = {
|
|
22
|
-
resolve: (statementName: StatementName) => void;
|
|
23
|
-
reject: (error: Error) => void;
|
|
24
|
-
text: QueryText;
|
|
25
|
-
statementName: StatementName;
|
|
26
|
-
};
|
|
27
|
-
export type DescribeQueueUnit = {
|
|
28
|
-
resolve: (value: ColumnDescription[]) => void;
|
|
29
|
-
reject: (error: Error) => void;
|
|
30
|
-
statementName: StatementName;
|
|
14
|
+
queryTimeout?: number;
|
|
31
15
|
};
|
|
32
16
|
export type StatementName = Branded<string, 'StatementName'>;
|
|
33
17
|
export type QueryText = Branded<string, 'QueryText'>;
|
|
@@ -145,7 +129,7 @@ export declare class Connection {
|
|
|
145
129
|
* })
|
|
146
130
|
* ```
|
|
147
131
|
*/
|
|
148
|
-
begin<T>(txCallback: (transaction: Transaction) => Promise<T>): Future<T,
|
|
132
|
+
begin<T>(txCallback: (transaction: Transaction) => Promise<T>): Future<T, unknown>;
|
|
149
133
|
/**
|
|
150
134
|
* Sends an asynchronous notification to a channel via `pg_notify`.
|
|
151
135
|
*
|
|
@@ -169,7 +153,7 @@ export declare class Connection {
|
|
|
169
153
|
* await conn.listen('events', data => console.log(data))
|
|
170
154
|
* ```
|
|
171
155
|
*/
|
|
172
|
-
listen(channelName: string, callback: (payload: string) => void): Future<
|
|
156
|
+
listen(channelName: string, callback: (payload: string) => void): Future<void, PostgresError>;
|
|
173
157
|
/**
|
|
174
158
|
* Unsubscribes a callback. Sends `UNLISTEN` if no callbacks remain for the channel.
|
|
175
159
|
*
|
|
@@ -181,8 +165,36 @@ export declare class Connection {
|
|
|
181
165
|
* await conn.unlisten('events', callback)
|
|
182
166
|
* ```
|
|
183
167
|
*/
|
|
184
|
-
unlisten(channelName: string, callback: (payload: string) => void): Future<
|
|
168
|
+
unlisten(channelName: string, callback: (payload: string) => void): Future<void, PostgresError>;
|
|
169
|
+
/**
|
|
170
|
+
* Executes an SQL query in streaming mode.
|
|
171
|
+
*
|
|
172
|
+
* Data is streamed directly from the PostgreSQL binary network buffer into the Web Streams API
|
|
173
|
+
* (`ReadableStream`), bypassing any intermediate array allocation or row accumulation in the JS heap.
|
|
174
|
+
* This pattern provides a true Zero-Memory Footprint and is ideal for exporting massive tables
|
|
175
|
+
* or piping database payloads directly into HTTP responses (e.g., via `Bun.serve` or fetch `Response`).
|
|
176
|
+
*
|
|
177
|
+
* @template T The expected shape of a single row interface.
|
|
178
|
+
* @param {TemplateStringsArray} templates The SQL string parts from the tagged template literal.
|
|
179
|
+
* @param {...any} args The parameterized query arguments.
|
|
180
|
+
* @returns {ReadableStream<T>} Synchronously returns a native Web ReadableStream instance.
|
|
181
|
+
*
|
|
182
|
+
* @example
|
|
183
|
+
* // Streaming a giant table directly to an HTTP response (Bun.serve)
|
|
184
|
+
* const userStream = conn.stream<User>`SELECT id, name FROM users`;
|
|
185
|
+
* return new Response(userStream, { headers: { 'Content-Type': 'application/json' } });
|
|
186
|
+
*
|
|
187
|
+
* @example
|
|
188
|
+
* // Asynchronously iterating over rows as they arrive from the wire socket
|
|
189
|
+
* const stream = conn.stream<User>`SELECT * FROM orders WHERE status = ${'processed'}`;
|
|
190
|
+
* for await (const row of stream) {
|
|
191
|
+
* console.log(row.id, row.amount); // Row object is eligible for GC immediately after iteration
|
|
192
|
+
* }
|
|
193
|
+
*/
|
|
194
|
+
stream<T extends Record<string, any>>(templates: TemplateStringsArray, ...params: any[]): ReadableStream<T>;
|
|
195
|
+
private _streamWithController;
|
|
185
196
|
private _createQuery;
|
|
197
|
+
private _createStream;
|
|
186
198
|
private _writeQuery;
|
|
187
199
|
private _registerFlush;
|
|
188
200
|
private _flush;
|
package/dist/connection.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"connection.d.ts","sourceRoot":"","sources":["../src/connection.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,OAAO,
|
|
1
|
+
{"version":3,"file":"connection.d.ts","sourceRoot":"","sources":["../src/connection.ts"],"names":[],"mappings":"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"}
|
package/dist/connection.js
CHANGED
|
@@ -5,9 +5,9 @@ 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 { Query, QueryState } from "./query";
|
|
8
|
+
import { Query, QueryState, StreamQuery } from "./query";
|
|
9
9
|
import { sql } from ".";
|
|
10
|
-
import { Begin, Future,
|
|
10
|
+
import { Begin, Future, Ok } from 'fluent-future';
|
|
11
11
|
import { PostgresError } from "./error";
|
|
12
12
|
const ErrConnectionClosed = new PostgresError("Connection is closed", 'connection_closed', "", "ERROR");
|
|
13
13
|
const ErrConnectionReconnecring = new PostgresError("Connection are reconnecting", "connection_reconnecting", "", "ERROR");
|
|
@@ -62,7 +62,7 @@ export class Connection {
|
|
|
62
62
|
this._stmtCounter = 0;
|
|
63
63
|
this.params = params;
|
|
64
64
|
this._logLevel = logLevel;
|
|
65
|
-
this._socket = new SocketConnector(socket, (type, reader) => this._handlePacket(type, reader), (err) => this._registerReconnect());
|
|
65
|
+
this._socket = new SocketConnector(socket, (type, _, reader) => this._handlePacket(type, reader), (err) => this._registerReconnect());
|
|
66
66
|
this._writer = writer;
|
|
67
67
|
}
|
|
68
68
|
/**
|
|
@@ -86,7 +86,7 @@ export class Connection {
|
|
|
86
86
|
static new(params) {
|
|
87
87
|
const writer = ConnectionRequestWriter.new();
|
|
88
88
|
return createAuthorizedSocket(writer, params)
|
|
89
|
-
.andThen(socket =>
|
|
89
|
+
.andThen(socket => Ok(new Connection(socket, writer, params.logLevel || 'error', params)));
|
|
90
90
|
}
|
|
91
91
|
/**
|
|
92
92
|
* Executes a query using tagged template literals.
|
|
@@ -121,11 +121,8 @@ export class Connection {
|
|
|
121
121
|
}
|
|
122
122
|
const query = this._createQuery(text, args);
|
|
123
123
|
this._writeQuery(query);
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
return error;
|
|
127
|
-
return new PostgresError(error.message);
|
|
128
|
-
});
|
|
124
|
+
query.startTimeout(this.params.queryTimeout || 30000);
|
|
125
|
+
return query.future;
|
|
129
126
|
}
|
|
130
127
|
/**
|
|
131
128
|
* Starts a managed transaction on this connection.
|
|
@@ -196,7 +193,7 @@ export class Connection {
|
|
|
196
193
|
}
|
|
197
194
|
const callbackSet = this._listeningCallbacks.get(channelName);
|
|
198
195
|
callbackSet.add(callback);
|
|
199
|
-
return this.query `listen ${sql.ident(channelName)}
|
|
196
|
+
return this.query `listen ${sql.ident(channelName)};`.map(() => { });
|
|
200
197
|
}
|
|
201
198
|
/**
|
|
202
199
|
* Unsubscribes a callback. Sends `UNLISTEN` if no callbacks remain for the channel.
|
|
@@ -212,15 +209,70 @@ export class Connection {
|
|
|
212
209
|
unlisten(channelName, callback) {
|
|
213
210
|
this._checkOpened();
|
|
214
211
|
if (!this._listeningCallbacks.has(channelName)) {
|
|
215
|
-
return
|
|
212
|
+
return Ok();
|
|
216
213
|
}
|
|
217
214
|
const callbackSet = this._listeningCallbacks.get(channelName);
|
|
218
215
|
callbackSet.delete(callback);
|
|
219
216
|
if (callbackSet.size === 0) {
|
|
220
217
|
this._listeningCallbacks.delete(channelName);
|
|
221
|
-
return this.query `unlisten ${sql.ident(channelName)}
|
|
218
|
+
return this.query `unlisten ${sql.ident(channelName)};`.map(() => { });
|
|
219
|
+
}
|
|
220
|
+
return Ok();
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Executes an SQL query in streaming mode.
|
|
224
|
+
*
|
|
225
|
+
* Data is streamed directly from the PostgreSQL binary network buffer into the Web Streams API
|
|
226
|
+
* (`ReadableStream`), bypassing any intermediate array allocation or row accumulation in the JS heap.
|
|
227
|
+
* This pattern provides a true Zero-Memory Footprint and is ideal for exporting massive tables
|
|
228
|
+
* or piping database payloads directly into HTTP responses (e.g., via `Bun.serve` or fetch `Response`).
|
|
229
|
+
*
|
|
230
|
+
* @template T The expected shape of a single row interface.
|
|
231
|
+
* @param {TemplateStringsArray} templates The SQL string parts from the tagged template literal.
|
|
232
|
+
* @param {...any} args The parameterized query arguments.
|
|
233
|
+
* @returns {ReadableStream<T>} Synchronously returns a native Web ReadableStream instance.
|
|
234
|
+
*
|
|
235
|
+
* @example
|
|
236
|
+
* // Streaming a giant table directly to an HTTP response (Bun.serve)
|
|
237
|
+
* const userStream = conn.stream<User>`SELECT id, name FROM users`;
|
|
238
|
+
* return new Response(userStream, { headers: { 'Content-Type': 'application/json' } });
|
|
239
|
+
*
|
|
240
|
+
* @example
|
|
241
|
+
* // Asynchronously iterating over rows as they arrive from the wire socket
|
|
242
|
+
* const stream = conn.stream<User>`SELECT * FROM orders WHERE status = ${'processed'}`;
|
|
243
|
+
* for await (const row of stream) {
|
|
244
|
+
* console.log(row.id, row.amount); // Row object is eligible for GC immediately after iteration
|
|
245
|
+
* }
|
|
246
|
+
*/
|
|
247
|
+
stream(templates, ...params) {
|
|
248
|
+
this._checkOpened();
|
|
249
|
+
this._registerFlush();
|
|
250
|
+
const { text, args } = compileSqlTemplate({ templates, args: params });
|
|
251
|
+
if (this._logLevel === 'query') {
|
|
252
|
+
console.log(`\nQUERY: ${text}\n${args.length !== 0 ? `ARGUMENTS: [${args}]\n` : ""}`);
|
|
253
|
+
}
|
|
254
|
+
let controller;
|
|
255
|
+
const stream = new ReadableStream({
|
|
256
|
+
start: c => {
|
|
257
|
+
controller = c;
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
const query = this._createStream(text, args, controller);
|
|
261
|
+
this._writeQuery(query);
|
|
262
|
+
query.startTimeout(this.params.queryTimeout || 30000);
|
|
263
|
+
return stream;
|
|
264
|
+
}
|
|
265
|
+
_streamWithController(templates, params, controller) {
|
|
266
|
+
this._checkOpened();
|
|
267
|
+
this._registerFlush();
|
|
268
|
+
const { text, args } = compileSqlTemplate({ templates, args: params });
|
|
269
|
+
if (this._logLevel === 'query') {
|
|
270
|
+
console.log(`\nQUERY: ${text}\n${args.length !== 0 ? `ARGUMENTS: [${args}]\n` : ""}`);
|
|
222
271
|
}
|
|
223
|
-
|
|
272
|
+
const query = this._createStream(text, args, controller);
|
|
273
|
+
this._writeQuery(query);
|
|
274
|
+
query.startTimeout(this.params.queryTimeout || 30000);
|
|
275
|
+
return query;
|
|
224
276
|
}
|
|
225
277
|
_createQuery(text, args) {
|
|
226
278
|
if (this._parsed.has(text)) {
|
|
@@ -251,6 +303,35 @@ export class Connection {
|
|
|
251
303
|
}
|
|
252
304
|
}
|
|
253
305
|
}
|
|
306
|
+
_createStream(text, args, controller) {
|
|
307
|
+
if (this._parsed.has(text)) {
|
|
308
|
+
const statementName = this._parsed.get(text);
|
|
309
|
+
if (this._described.has(statementName)) {
|
|
310
|
+
const columns = this._described.get(statementName);
|
|
311
|
+
return new StreamQuery(text, args, QueryState.Executing, statementName, controller, columns);
|
|
312
|
+
}
|
|
313
|
+
else {
|
|
314
|
+
if (this._describingPending.has(statementName)) {
|
|
315
|
+
return new StreamQuery(text, args, QueryState.Executing, statementName, controller);
|
|
316
|
+
}
|
|
317
|
+
else {
|
|
318
|
+
this._describingPending.add(statementName);
|
|
319
|
+
return new StreamQuery(text, args, QueryState.Describing, statementName, controller);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
else {
|
|
324
|
+
if (this._parsingPending.has(text)) {
|
|
325
|
+
const statementName = this._parsingPending.get(text);
|
|
326
|
+
return new StreamQuery(text, args, QueryState.Describing, statementName, controller);
|
|
327
|
+
}
|
|
328
|
+
else {
|
|
329
|
+
const statementName = this._nextStatement();
|
|
330
|
+
this._parsingPending.set(text, statementName);
|
|
331
|
+
return new StreamQuery(text, args, QueryState.Parsing, statementName, controller);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
254
335
|
_writeQuery(query) {
|
|
255
336
|
if (query.state === QueryState.Parsing) {
|
|
256
337
|
this._writer
|
|
@@ -314,7 +395,7 @@ export class Connection {
|
|
|
314
395
|
async _reconnect() {
|
|
315
396
|
this._resetConnectionState(ErrConnectionReconnecring);
|
|
316
397
|
const socket = await createAuthorizedSocket(ConnectionRequestWriter.new(), this.params);
|
|
317
|
-
const connector = new SocketConnector(socket, (type, reader) => this._handlePacket(type, reader), (err) => this._registerReconnect());
|
|
398
|
+
const connector = new SocketConnector(socket, (type, _, reader) => this._handlePacket(type, reader), (err) => this._registerReconnect());
|
|
318
399
|
this._socket = connector;
|
|
319
400
|
void this._restoreSubscriptions();
|
|
320
401
|
this._isReconnecting = false;
|