postgrejs 3.13.0 โ 3.14.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 +51 -2
- package/connection/intl-connection.d.ts +18 -0
- package/connection/intl-connection.js +79 -4
- package/constants.d.ts +1 -1
- package/constants.js +1 -1
- package/data-types/bytea-type.d.ts +8 -1
- package/data-types/bytea-type.js +30 -4
- package/interfaces/query-options.d.ts +8 -3
- package/interfaces/script-execute-options.d.ts +8 -3
- package/package.json +3 -3
- package/protocol/pg-socket.d.ts +27 -3
- package/protocol/pg-socket.js +91 -12
- package/protocol/smart-buffer.d.ts +5 -4
- package/protocol/smart-buffer.js +21 -1
- package/protocol/workerd-socket.d.ts +87 -0
- package/protocol/workerd-socket.js +245 -0
- package/util/runtime.d.ts +6 -0
- package/util/runtime.js +19 -0
package/README.md
CHANGED
|
@@ -98,6 +98,32 @@ Each adapter hands the compiled SQL straight to this client, with no `pg` left a
|
|
|
98
98
|
compared query by query against the driver it replaces - and, where the project ships a suite of its own, run
|
|
99
99
|
against that too. So adopting one is a change to how the connection is created and nothing else.
|
|
100
100
|
|
|
101
|
+
### ๐ Runs Where You Run
|
|
102
|
+
|
|
103
|
+
<!-- Same card shape as the adapters above, for the same reason: GitHub strips CSS, and a table
|
|
104
|
+
is the one layout that survives everywhere the README is read. -->
|
|
105
|
+
<table>
|
|
106
|
+
<tr>
|
|
107
|
+
<td align="center" width="230" valign="top">
|
|
108
|
+
<a href="https://www.postgrejs.com/docs/getting-started/benchmarks"><img src="https://www.postgrejs.com/img/nodejs-logo.svg" alt="Node.js" height="46"></a><br><br>
|
|
109
|
+
<b><a href="https://www.postgrejs.com/docs/getting-started/benchmarks">Node.js</a></b><br>
|
|
110
|
+
</td>
|
|
111
|
+
<td align="center" width="230" valign="top">
|
|
112
|
+
<a href="https://www.postgrejs.com/docs/getting-started/benchmarks-bun"><img src="https://www.postgrejs.com/img/bun-logo.svg" alt="Bun" height="46"></a><br><br>
|
|
113
|
+
<b><a href="https://www.postgrejs.com/docs/getting-started/benchmarks-bun">Bun</a></b><br>
|
|
114
|
+
</td>
|
|
115
|
+
<td align="center" width="230" valign="top">
|
|
116
|
+
<a href="https://www.postgrejs.com/docs/guides/cloudflare-workers"><img src="https://www.postgrejs.com/img/cloudflare-logo.svg" alt="Cloudflare Workers" height="46"></a><br><br>
|
|
117
|
+
<b><a href="https://www.postgrejs.com/docs/guides/cloudflare-workers">Cloudflare Workers</a></b><br>
|
|
118
|
+
</td>
|
|
119
|
+
</tr>
|
|
120
|
+
</table>
|
|
121
|
+
|
|
122
|
+
Node and Bun run **the same test suite** in CI on every push - Node across 20, 22, 24 and 26 against PostgreSQL 12,
|
|
123
|
+
16 and 18, Bun against 18 - so neither is a target that merely ought to work. Workers is not in that matrix yet: it
|
|
124
|
+
was measured by hand against PostgreSQL 18, a plain connection and a TLS one, and what the runtime does and does not
|
|
125
|
+
allow is written down in [doc/CLOUDFLARE-WORKERS.md](doc/CLOUDFLARE-WORKERS.md).
|
|
126
|
+
|
|
101
127
|
## Installation
|
|
102
128
|
|
|
103
129
|
```bash
|
|
@@ -133,6 +159,7 @@ in ยท ๐ก partial or needs a separate package ยท โ not supported.
|
|
|
133
159
|
| Packages to install | 1 | 4 <sup>1</sup> | 1 |
|
|
134
160
|
| Module system | ESM | ESM/CJS | ESM/CJS |
|
|
135
161
|
| Language | TS | JS <sup>2</sup> | JS <sup>3</sup> |
|
|
162
|
+
| Cloudflare Workers | โ
<sup>28</sup> | ๐ก <sup>29</sup> | โ
<sup>30</sup> |
|
|
136
163
|
| ***Wire protocol*** | | | |
|
|
137
164
|
| Protocol version | 3.2 | 3.0 | 3.0 |
|
|
138
165
|
| Simple Query protocol | โ
| โ
| โ
|
|
|
@@ -271,6 +298,25 @@ in ยท ๐ก partial or needs a separate package ยท โ not supported.
|
|
|
271
298
|
input parser is what sorts and deduplicates a vector and what defines a query's grammar, so encoding either here
|
|
272
299
|
would make the same text mean one thing written as a literal and another bound as a parameter. They are sent as
|
|
273
300
|
text instead, which is exact.
|
|
301
|
+
- <sup>28</sup> PostgreJS runs on workerd under `nodejs_compat` with nothing installed and no build of its own.
|
|
302
|
+
Measured end to end: a plain connection against PostgreSQL 18, and a TLS one against a managed PostgreSQL 18 whose
|
|
303
|
+
certificate comes from Let's Encrypt - the same server refusing the connection outright (`28000 connection is
|
|
304
|
+
insecure`) when `sslmode` is dropped, which is what makes the encrypted one mean something. TLS opens a socket of
|
|
305
|
+
the runtime's own and upgrades it with `startTls()`, since `tls.connect({ socket })` is not something workerd can
|
|
306
|
+
carry out. Three limits are the runtime's, and all three libraries have them: `ca`, `cert` and `rejectUnauthorized`
|
|
307
|
+
have nowhere to go (workerd's TLS options are `{ expectedServerHostname?: string }`, so a self-signed certificate
|
|
308
|
+
cannot be trusted and a local server cannot be reached over TLS at all); `sslnegotiation=direct` cannot announce the
|
|
309
|
+
`postgresql` ALPN protocol, and is refused by name here; and SCRAM channel binding has no server certificate to bind
|
|
310
|
+
to, so `channelBinding: 'require'` is refused and the default `prefer` authenticates without it. Hyperdrive is the
|
|
311
|
+
other way in, terminating TLS in Cloudflare's own network - checked here against its binding. See
|
|
312
|
+
[doc/CLOUDFLARE-WORKERS.md](doc/CLOUDFLARE-WORKERS.md).
|
|
313
|
+
- <sup>29</sup> pg reaches workerd through `pg-cloudflare`, a separate package it carries as an optional dependency
|
|
314
|
+
and selects at runtime (`lib/stream.js`, on `navigator.userAgent`). That package wraps `cloudflare:sockets` and
|
|
315
|
+
upgrades with its own `startTls()`, so TLS is available where PostgreJS's is not - at the cost of a second package
|
|
316
|
+
and of nothing happening at all if it was not installed.
|
|
317
|
+
- <sup>30</sup> postgres.js ships a build for the runtime: a `workerd` condition in `exports` pointing at `cf/`, whose
|
|
318
|
+
polyfills shim `net`/`tls` onto `cloudflare:sockets` including `startTls({ servername })`. One package, the right
|
|
319
|
+
code chosen by the resolver, TLS included.
|
|
274
320
|
|
|
275
321
|
## Benchmarks
|
|
276
322
|
|
|
@@ -278,10 +324,10 @@ PostgreJS implements the full PostgreSQL wire protocol from scratch, with no dep
|
|
|
278
324
|
[`doc/BENCHMARKS.md`](doc/BENCHMARKS.md) compares it against `pg` (node-postgres) and `postgres` (postgres.js)
|
|
279
325
|
across connection setup, simple/prepared queries, mixed-type decoding, bulk fetches, cursor streaming and pool
|
|
280
326
|
concurrency, each library run through its own idiomatic fast path. The numbers there are reproducible on your own
|
|
281
|
-
machine via `
|
|
327
|
+
machine via `pnpm run bench` against the repo's own
|
|
282
328
|
`docker/docker-compose.yml` Postgres instance; see [`benchmark/README.md`](./benchmark/README.md) for details.
|
|
283
329
|
|
|
284
|
-
The same suite runs under Bun via `
|
|
330
|
+
The same suite runs under Bun via `pnpm run bench:bun`, where it also measures Bun's own built-in `Bun.sql` client as
|
|
285
331
|
a fourth library. Those results live in [`doc/BENCHMARKS-bun.md`](doc/BENCHMARKS-bun.md) and are deliberately kept
|
|
286
332
|
in a separate report rather than merged into the Node one: GC instrumentation, cursor streaming and binary-format
|
|
287
333
|
control all differ enough between the two runtimes that a single table would conflate a library difference with a
|
|
@@ -302,6 +348,9 @@ you open an issue please provide version of NodeJS and PostgreSQL server.
|
|
|
302
348
|
- Bun - the same test suite runs under Bun in CI against PostgreSQL 18 on every push, so Bun is a supported target
|
|
303
349
|
rather than an untested coincidence. (Coverage is collected on the Node matrix only: `c8` instruments V8's
|
|
304
350
|
coverage APIs, which JavaScriptCore doesn't have.)
|
|
351
|
+
- Cloudflare Workers - with `nodejs_compat`, and without TLS: the runtime has no way to trust a server
|
|
352
|
+
certificate, so a database behind TLS is reached through Hyperdrive instead. See
|
|
353
|
+
[doc/CLOUDFLARE-WORKERS.md](doc/CLOUDFLARE-WORKERS.md).
|
|
305
354
|
|
|
306
355
|
## License
|
|
307
356
|
|
|
@@ -64,6 +64,12 @@ export declare class IntlConnection extends SafeEventEmitter {
|
|
|
64
64
|
* never close again.
|
|
65
65
|
*/
|
|
66
66
|
private _preparing;
|
|
67
|
+
/**
|
|
68
|
+
* Resolves when the statement submitted before this one has written its
|
|
69
|
+
* own bytes - see _holdWireOrder(). Undefined whenever nothing is on its
|
|
70
|
+
* way to the wire, which is every statement that writes without waiting.
|
|
71
|
+
*/
|
|
72
|
+
private _wireOrder?;
|
|
67
73
|
/** SQL seen once but not yet prepared - see PREPARE_AFTER_USES. */
|
|
68
74
|
private _preparedCandidates;
|
|
69
75
|
private _preparedCounter;
|
|
@@ -313,6 +319,18 @@ export declare class IntlConnection extends SafeEventEmitter {
|
|
|
313
319
|
protected _queryCached(sql: string, paramTypes: Maybe<Maybe<OID>[]>, params: Maybe<Maybe<any>[]>, options: QueryOptions, savepoint: Maybe<string>,
|
|
314
320
|
/** Whether the CALL asked for text columns, not the connection. */
|
|
315
321
|
callWantsFields: boolean): Promise<QueryResult>;
|
|
322
|
+
/**
|
|
323
|
+
* Keeps this statement's place in the wire order while it waits for
|
|
324
|
+
* something - today, the Parse round trip that names it.
|
|
325
|
+
*
|
|
326
|
+
* @returns `after`, the place held by the statement submitted before this
|
|
327
|
+
* one, to wait on before writing; and `wrote`, to call the moment this
|
|
328
|
+
* statement's own bytes are on the wire.
|
|
329
|
+
*/
|
|
330
|
+
protected _holdWireOrder(): {
|
|
331
|
+
after?: Promise<void>;
|
|
332
|
+
wrote: () => void;
|
|
333
|
+
};
|
|
316
334
|
/**
|
|
317
335
|
* Gives this SQL a name and caches it, once, however many callers are
|
|
318
336
|
* waiting. A Parse that fails caches nothing and reports itself; the
|
|
@@ -195,6 +195,12 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
195
195
|
* never close again.
|
|
196
196
|
*/
|
|
197
197
|
_preparing = new Map();
|
|
198
|
+
/**
|
|
199
|
+
* Resolves when the statement submitted before this one has written its
|
|
200
|
+
* own bytes - see _holdWireOrder(). Undefined whenever nothing is on its
|
|
201
|
+
* way to the wire, which is every statement that writes without waiting.
|
|
202
|
+
*/
|
|
203
|
+
_wireOrder;
|
|
198
204
|
/** SQL seen once but not yet prepared - see PREPARE_AFTER_USES. */
|
|
199
205
|
_preparedCandidates = new Map();
|
|
200
206
|
_preparedCounter = 0;
|
|
@@ -975,8 +981,11 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
975
981
|
/** Whether the CALL asked for text columns, not the connection. */
|
|
976
982
|
callWantsFields) {
|
|
977
983
|
const enabled = options.prepare ?? this.config.prepare ?? true;
|
|
978
|
-
if (!enabled)
|
|
984
|
+
if (!enabled) {
|
|
985
|
+
if (this._wireOrder)
|
|
986
|
+
await this._wireOrder;
|
|
979
987
|
return this.queryOnce(sql, paramTypes, params, options, savepoint);
|
|
988
|
+
}
|
|
980
989
|
const key = preparedCacheKey(sql, paramTypes);
|
|
981
990
|
const cached = this._preparedCache.get(key);
|
|
982
991
|
if (cached) {
|
|
@@ -984,6 +993,8 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
984
993
|
this._preparedCache.delete(key);
|
|
985
994
|
this._preparedCache.set(key, cached);
|
|
986
995
|
try {
|
|
996
|
+
if (this._wireOrder)
|
|
997
|
+
await this._wireOrder;
|
|
987
998
|
return await this.executeReused(cached.name, cached.fields, resolvedBindTypes(paramTypes, params, cached.resolvedParamTypes), params, options, savepoint);
|
|
988
999
|
}
|
|
989
1000
|
catch (e) {
|
|
@@ -1024,8 +1035,11 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
1024
1035
|
// on a statement's first run instead, and per-column codes from its
|
|
1025
1036
|
// second, which is where the name arrives anyway.
|
|
1026
1037
|
const needsFields = callWantsFields && options.columnFormat !== DataFormat.text;
|
|
1027
|
-
if (!needsFields && !this._earnsAName(key))
|
|
1038
|
+
if (!needsFields && !this._earnsAName(key)) {
|
|
1039
|
+
if (this._wireOrder)
|
|
1040
|
+
await this._wireOrder;
|
|
1028
1041
|
return this.queryOnce(sql, paramTypes, params, options, savepoint);
|
|
1042
|
+
}
|
|
1029
1043
|
let preparing = this._preparing.get(key);
|
|
1030
1044
|
if (!preparing) {
|
|
1031
1045
|
// Registered before the first await inside, so everything that
|
|
@@ -1038,10 +1052,71 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
1038
1052
|
// Someone else is already naming it. Waiting would cost a round
|
|
1039
1053
|
// trip the one-shot path does not - the name is for the calls
|
|
1040
1054
|
// after this burst, not for this one.
|
|
1055
|
+
if (this._wireOrder)
|
|
1056
|
+
await this._wireOrder;
|
|
1041
1057
|
return this.queryOnce(sql, paramTypes, params, options, savepoint);
|
|
1042
1058
|
}
|
|
1043
|
-
const
|
|
1044
|
-
|
|
1059
|
+
const place = this._holdWireOrder();
|
|
1060
|
+
let running;
|
|
1061
|
+
try {
|
|
1062
|
+
const entry = await preparing;
|
|
1063
|
+
if (place.after)
|
|
1064
|
+
await place.after;
|
|
1065
|
+
/* Called, not awaited: executeReused() runs as far as the socket
|
|
1066
|
+
write before its first await, so by the time it hands back a
|
|
1067
|
+
promise the bytes are away and the next statement may follow
|
|
1068
|
+
them. Awaiting it here instead would hold the line until this
|
|
1069
|
+
statement had its answer, which is pipelining switched off. */
|
|
1070
|
+
running = this.executeReused(entry.name, entry.fields, paramTypes, params, options, savepoint);
|
|
1071
|
+
}
|
|
1072
|
+
finally {
|
|
1073
|
+
// Also on the way out of a Parse that failed: this statement will
|
|
1074
|
+
// never write, and whatever queued behind it must not wait forever.
|
|
1075
|
+
place.wrote();
|
|
1076
|
+
}
|
|
1077
|
+
return running;
|
|
1078
|
+
}
|
|
1079
|
+
/**
|
|
1080
|
+
* Keeps this statement's place in the wire order while it waits for
|
|
1081
|
+
* something - today, the Parse round trip that names it.
|
|
1082
|
+
*
|
|
1083
|
+
* @returns `after`, the place held by the statement submitted before this
|
|
1084
|
+
* one, to wait on before writing; and `wrote`, to call the moment this
|
|
1085
|
+
* statement's own bytes are on the wire.
|
|
1086
|
+
*/
|
|
1087
|
+
/* PostgreSQL executes what reaches it in the order it reaches it, and
|
|
1088
|
+
cannot know what order the query() calls were made in. A statement
|
|
1089
|
+
that has to await a Parse before it can Bind therefore lets every
|
|
1090
|
+
later statement overtake it - invisibly, because the overtaking only
|
|
1091
|
+
happens when one statement is cached and the other is not, which is a
|
|
1092
|
+
property of the connection's history rather than of the code. Two
|
|
1093
|
+
updates to one row then land in the opposite order on a connection
|
|
1094
|
+
that had been running them correctly all along.
|
|
1095
|
+
|
|
1096
|
+
Holding a place rather than the wire: the Parse still overlaps with
|
|
1097
|
+
everything else, which is the point of pipelining. Only the Bind is
|
|
1098
|
+
made to queue, and only behind calls that really were submitted
|
|
1099
|
+
first.
|
|
1100
|
+
|
|
1101
|
+
Nothing pending leaves `_wireOrder` undefined, so a statement that
|
|
1102
|
+
writes without waiting - a cache hit, the common case - reads one
|
|
1103
|
+
field and writes, paying no promise and no microtask for the
|
|
1104
|
+
ordering. */
|
|
1105
|
+
_holdWireOrder() {
|
|
1106
|
+
const after = this._wireOrder;
|
|
1107
|
+
let resolve;
|
|
1108
|
+
const mine = new Promise(r => (resolve = r));
|
|
1109
|
+
this._wireOrder = mine;
|
|
1110
|
+
return {
|
|
1111
|
+
after,
|
|
1112
|
+
wrote: () => {
|
|
1113
|
+
// Only if nobody queued behind this one in the meantime - their
|
|
1114
|
+
// place is the one that must be waited on now.
|
|
1115
|
+
if (this._wireOrder === mine)
|
|
1116
|
+
this._wireOrder = undefined;
|
|
1117
|
+
resolve();
|
|
1118
|
+
},
|
|
1119
|
+
};
|
|
1045
1120
|
}
|
|
1046
1121
|
/**
|
|
1047
1122
|
* Gives this SQL a name and caches it, once, however many callers are
|
package/constants.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Protocol } from './protocol/protocol.js';
|
|
2
2
|
import DataFormat = Protocol.DataFormat;
|
|
3
|
-
export declare const version = "3.
|
|
3
|
+
export declare const version = "3.14.1";
|
|
4
4
|
export { DataFormat };
|
|
5
5
|
/**
|
|
6
6
|
* What columns come back in unless something asks otherwise.
|
package/constants.js
CHANGED
|
@@ -2,7 +2,7 @@ import { Protocol } from './protocol/protocol.js';
|
|
|
2
2
|
var DataFormat = Protocol.DataFormat;
|
|
3
3
|
// @ts-ignore
|
|
4
4
|
Symbol.asyncDispose ??= Symbol('Symbol.asyncDispose');
|
|
5
|
-
export const version = '3.
|
|
5
|
+
export const version = '3.14.1';
|
|
6
6
|
export { DataFormat };
|
|
7
7
|
/**
|
|
8
8
|
* What columns come back in unless something asks otherwise.
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import type { DataType } from '../interfaces/data-type.js';
|
|
2
|
-
/**
|
|
2
|
+
/**
|
|
3
|
+
* `bytea`. A value read back is a `Buffer`; a value sent may be any
|
|
4
|
+
* ArrayBuffer view - a `Buffer`, a `Uint8Array` off `fetch` or `subarray`,
|
|
5
|
+
* a `DataView` - and only the bytes that view covers are sent.
|
|
6
|
+
*
|
|
7
|
+
* The binary format is the bytes themselves; the text one is `\\x`-prefixed
|
|
8
|
+
* hex, at two characters a byte.
|
|
9
|
+
*/
|
|
3
10
|
export declare const ByteaType: DataType;
|
|
4
11
|
/** The `_bytea` array of {@link ByteaType}. */
|
|
5
12
|
export declare const ArrayByteaType: DataType;
|
package/data-types/bytea-type.js
CHANGED
|
@@ -1,16 +1,42 @@
|
|
|
1
1
|
import { DataTypeOIDs } from '../constants.js';
|
|
2
2
|
import { parseBytea, parseByteaBuffer } from '../util/parse-bytea.js';
|
|
3
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
* Reads any ArrayBuffer view as a `Buffer` over exactly the bytes it covers.
|
|
5
|
+
*
|
|
6
|
+
* @param v The view to read.
|
|
7
|
+
* @returns It unchanged when it is already a `Buffer`, or a window onto the
|
|
8
|
+
* same memory - never a copy.
|
|
9
|
+
*/
|
|
10
|
+
/* `byteOffset`/`byteLength` are the whole point: a `subarray()` of a larger
|
|
11
|
+
buffer describes a window, and `Buffer.from(view.buffer)` would send
|
|
12
|
+
everything on both sides of it. */
|
|
13
|
+
function asBuffer(v) {
|
|
14
|
+
return Buffer.isBuffer(v)
|
|
15
|
+
? v
|
|
16
|
+
: Buffer.from(v.buffer, v.byteOffset, v.byteLength);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* `bytea`. A value read back is a `Buffer`; a value sent may be any
|
|
20
|
+
* ArrayBuffer view - a `Buffer`, a `Uint8Array` off `fetch` or `subarray`,
|
|
21
|
+
* a `DataView` - and only the bytes that view covers are sent.
|
|
22
|
+
*
|
|
23
|
+
* The binary format is the bytes themselves; the text one is `\\x`-prefixed
|
|
24
|
+
* hex, at two characters a byte.
|
|
25
|
+
*/
|
|
4
26
|
export const ByteaType = {
|
|
5
27
|
name: 'bytea',
|
|
6
28
|
oid: DataTypeOIDs.bytea,
|
|
7
29
|
jsType: 'Buffer',
|
|
8
30
|
// The hex form PostgreSQL has emitted by default since 9.0.
|
|
9
31
|
encodeText(v) {
|
|
10
|
-
return '\\x' + v.toString('hex');
|
|
32
|
+
return '\\x' + asBuffer(v).toString('hex');
|
|
11
33
|
},
|
|
34
|
+
/* Through `asBuffer` rather than straight to `writeBytes`, which reaches
|
|
35
|
+
for `.copy()` - a `Buffer` method that a plain `Uint8Array` does not
|
|
36
|
+
have, and the `buffer.copy is not a function` an explicit
|
|
37
|
+
`BindParam(17, bytes)` used to fail with. */
|
|
12
38
|
encodeBinary(buf, v) {
|
|
13
|
-
buf.writeBytes(v);
|
|
39
|
+
buf.writeBytes(asBuffer(v));
|
|
14
40
|
},
|
|
15
41
|
decodeBinary(v, offset = 0, len) {
|
|
16
42
|
// A view, not a copy - the value handed back is the caller's own
|
|
@@ -20,7 +46,7 @@ export const ByteaType = {
|
|
|
20
46
|
decodeText: parseBytea,
|
|
21
47
|
decodeTextBuffer: parseByteaBuffer,
|
|
22
48
|
isType(v) {
|
|
23
|
-
return v
|
|
49
|
+
return ArrayBuffer.isView(v);
|
|
24
50
|
},
|
|
25
51
|
};
|
|
26
52
|
/** The `_bytea` array of {@link ByteaType}. */
|
|
@@ -37,9 +37,14 @@ export interface QueryOptions extends DataMappingOptions {
|
|
|
37
37
|
*
|
|
38
38
|
* `false` asks for the wire to itself: the statement waits until
|
|
39
39
|
* nothing else is running on the connection, and nothing else starts
|
|
40
|
-
* until it finishes, whichever mode those asked for.
|
|
41
|
-
*
|
|
42
|
-
*
|
|
40
|
+
* until it finishes, whichever mode those asked for. It is not how a
|
|
41
|
+
* statement says it depends on an earlier one: pipelined statements on
|
|
42
|
+
* one connection execute in the order they were submitted. What this
|
|
43
|
+
* is for is a statement that needs the connection to itself for longer
|
|
44
|
+
* than the wire - session state another caller would see, or a result
|
|
45
|
+
* the caller has to read before it can decide what to send next.
|
|
46
|
+
* Statements on different pooled connections have no order between
|
|
47
|
+
* them at all.
|
|
43
48
|
*
|
|
44
49
|
* Some statements never share a *pooled* connection however this is
|
|
45
50
|
* set, because they need one to themselves for longer than the call:
|
|
@@ -32,9 +32,14 @@ export interface ScriptExecuteOptions extends DataMappingOptions {
|
|
|
32
32
|
*
|
|
33
33
|
* `false` asks for the wire to itself: the statement waits until
|
|
34
34
|
* nothing else is running on the connection, and nothing else starts
|
|
35
|
-
* until it finishes, whichever mode those asked for.
|
|
36
|
-
*
|
|
37
|
-
*
|
|
35
|
+
* until it finishes, whichever mode those asked for. It is not how a
|
|
36
|
+
* statement says it depends on an earlier one: pipelined statements on
|
|
37
|
+
* one connection execute in the order they were submitted. What this
|
|
38
|
+
* is for is a statement that needs the connection to itself for longer
|
|
39
|
+
* than the wire - session state another caller would see, or a result
|
|
40
|
+
* the caller has to read before it can decide what to send next.
|
|
41
|
+
* Statements on different pooled connections have no order between
|
|
42
|
+
* them at all.
|
|
38
43
|
*
|
|
39
44
|
* Some statements never share a *pooled* connection however this is
|
|
40
45
|
* set, because they need one to themselves for longer than the call:
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "postgrejs",
|
|
3
3
|
"description": "Professional PostgreSQL client NodeJS",
|
|
4
|
-
"version": "3.
|
|
4
|
+
"version": "3.14.1",
|
|
5
5
|
"author": "Panates",
|
|
6
6
|
"license": "BSD-3-Clause",
|
|
7
7
|
"dependencies": {
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
],
|
|
37
37
|
"repository": {
|
|
38
38
|
"type": "git",
|
|
39
|
-
"url": "https://github.com/panates/postgrejs.git"
|
|
39
|
+
"url": "git+https://github.com/panates/postgrejs.git"
|
|
40
40
|
},
|
|
41
41
|
"keywords": [
|
|
42
42
|
"postgresql",
|
|
@@ -65,4 +65,4 @@
|
|
|
65
65
|
"publishConfig": {
|
|
66
66
|
"access": "public"
|
|
67
67
|
}
|
|
68
|
-
}
|
|
68
|
+
}
|
package/protocol/pg-socket.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import net from 'node:net';
|
|
2
1
|
import tls from 'node:tls';
|
|
3
2
|
import { ConnectionState } from '../constants.js';
|
|
4
3
|
import type { ConnectionConfiguration } from '../interfaces/database-connection-params.js';
|
|
@@ -20,6 +19,29 @@ export interface SocketError extends Error {
|
|
|
20
19
|
* is asked for, and a queue that pairs each response with whoever sent
|
|
21
20
|
* the statement that caused it.
|
|
22
21
|
*/
|
|
22
|
+
/**
|
|
23
|
+
* What this client needs a socket to be, whoever provides one.
|
|
24
|
+
*
|
|
25
|
+
* `net.Socket`, its TLS upgrade, and `WorkerdSocket` all satisfy it.
|
|
26
|
+
*/
|
|
27
|
+
interface ClientSocket {
|
|
28
|
+
readonly destroyed: boolean;
|
|
29
|
+
readonly writable: boolean;
|
|
30
|
+
write(data: any, cb?: (err?: Error | null) => void): boolean;
|
|
31
|
+
destroy(err?: Error): any;
|
|
32
|
+
end(...args: any[]): any;
|
|
33
|
+
pause(): any;
|
|
34
|
+
resume(): any;
|
|
35
|
+
cork(): void;
|
|
36
|
+
uncork(): void;
|
|
37
|
+
setNoDelay(enable?: boolean): any;
|
|
38
|
+
setKeepAlive(enable?: boolean): any;
|
|
39
|
+
setTimeout(ms: number, cb?: () => void): any;
|
|
40
|
+
connect(...args: any[]): any;
|
|
41
|
+
on(event: string, listener: (...args: any[]) => void): any;
|
|
42
|
+
once(event: string, listener: (...args: any[]) => void): any;
|
|
43
|
+
removeAllListeners(event?: string): any;
|
|
44
|
+
}
|
|
23
45
|
export declare class PgSocket extends SafeEventEmitter {
|
|
24
46
|
options: ConnectionConfiguration;
|
|
25
47
|
private _state;
|
|
@@ -29,6 +51,7 @@ export declare class PgSocket extends SafeEventEmitter {
|
|
|
29
51
|
* bare. Cleared by _reset().
|
|
30
52
|
*/
|
|
31
53
|
private _closeReason?;
|
|
54
|
+
/** Whichever kind of socket this runtime connects with. */
|
|
32
55
|
private _socket?;
|
|
33
56
|
private _backend;
|
|
34
57
|
private _frontend;
|
|
@@ -65,10 +88,10 @@ export declare class PgSocket extends SafeEventEmitter {
|
|
|
65
88
|
* afterwards. Resolves `onReady` with whichever socket ends up ready to
|
|
66
89
|
* speak the PostgreSQL protocol: the plain one, or its TLS upgrade.
|
|
67
90
|
*/
|
|
68
|
-
protected _negotiateTls(socket:
|
|
91
|
+
protected _negotiateTls(socket: ClientSocket, target: {
|
|
69
92
|
host: string;
|
|
70
93
|
port?: number;
|
|
71
|
-
}, onReady: (socket:
|
|
94
|
+
}, onReady: (socket: ClientSocket) => void, onError: (err: Error) => void): void;
|
|
72
95
|
/**
|
|
73
96
|
* Asks the server to cancel whatever this session is currently running.
|
|
74
97
|
*
|
|
@@ -337,3 +360,4 @@ export declare class PgSocket extends SafeEventEmitter {
|
|
|
337
360
|
*/
|
|
338
361
|
protected _sendCopy(data: Buffer | Buffer[], cb?: Callback): boolean;
|
|
339
362
|
}
|
|
363
|
+
export {};
|
package/protocol/pg-socket.js
CHANGED
|
@@ -6,6 +6,7 @@ import DoublyLinked from 'doublylinked';
|
|
|
6
6
|
import promisify from 'putil-promisify';
|
|
7
7
|
import { ConnectionState } from '../constants.js';
|
|
8
8
|
import { SafeEventEmitter } from '../safe-event-emitter.js';
|
|
9
|
+
import { isWorkerd } from '../util/runtime.js';
|
|
9
10
|
import { Backend } from './backend.js';
|
|
10
11
|
import { signatureHashOfCertificate } from './cert-signature.js';
|
|
11
12
|
import { ConnectionLostError } from './connection-lost-error.js';
|
|
@@ -13,13 +14,20 @@ import { DatabaseError } from './database-error.js';
|
|
|
13
14
|
import { Frontend } from './frontend.js';
|
|
14
15
|
import { Protocol } from './protocol.js';
|
|
15
16
|
import { SASL } from './sasl.js';
|
|
17
|
+
import { WorkerdSocket } from './workerd-socket.js';
|
|
16
18
|
const DEFAULT_PORT_NUMBER = 5432;
|
|
17
19
|
const COMMAND_RESULT_PATTERN = /^([^\d]+)(?: (\d+)(?: (\d+))?)?$/;
|
|
18
20
|
/**
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
21
|
+
* Whether a connection is going to ask for TLS at all.
|
|
22
|
+
*
|
|
23
|
+
* @param options The connection's own options.
|
|
24
|
+
* @returns True when any of the three ways of asking is set.
|
|
22
25
|
*/
|
|
26
|
+
/* Read in two places that have to agree: which socket to open, and
|
|
27
|
+
whether to negotiate once it is open. */
|
|
28
|
+
function wantsTls(options) {
|
|
29
|
+
return (!!options.ssl || !!options.requireSSL || options.sslNegotiation === 'direct');
|
|
30
|
+
}
|
|
23
31
|
export class PgSocket extends SafeEventEmitter {
|
|
24
32
|
options;
|
|
25
33
|
_state = ConnectionState.CLOSED;
|
|
@@ -29,6 +37,7 @@ export class PgSocket extends SafeEventEmitter {
|
|
|
29
37
|
* bare. Cleared by _reset().
|
|
30
38
|
*/
|
|
31
39
|
_closeReason;
|
|
40
|
+
/** Whichever kind of socket this runtime connects with. */
|
|
32
41
|
_socket;
|
|
33
42
|
_backend = new Backend();
|
|
34
43
|
_frontend;
|
|
@@ -86,7 +95,17 @@ export class PgSocket extends SafeEventEmitter {
|
|
|
86
95
|
this._sessionAttrsChecked = false;
|
|
87
96
|
this._state = ConnectionState.CONNECTING;
|
|
88
97
|
const target = this._hosts[this._hostIndex];
|
|
89
|
-
|
|
98
|
+
/* Workerd cannot upgrade a `net.Socket`, and the socket that can be
|
|
99
|
+
upgraded has to be opened as one from the start - `secureTransport:
|
|
100
|
+
'starttls'` is a connect-time choice, not a later one. So the kind
|
|
101
|
+
of socket is decided here, by whether TLS is going to be asked for
|
|
102
|
+
at all. The plain path stays on `node:net`, which works there. */
|
|
103
|
+
const socket = (this._socket =
|
|
104
|
+
isWorkerd() &&
|
|
105
|
+
wantsTls(this.options) &&
|
|
106
|
+
this.options.sslNegotiation !== 'direct'
|
|
107
|
+
? new WorkerdSocket(true)
|
|
108
|
+
: new net.Socket());
|
|
90
109
|
const errorHandler = (err) => {
|
|
91
110
|
this._state = ConnectionState.CLOSED;
|
|
92
111
|
this._removeListeners();
|
|
@@ -133,7 +152,14 @@ export class PgSocket extends SafeEventEmitter {
|
|
|
133
152
|
_negotiateTls(socket, target, onReady, onError) {
|
|
134
153
|
const options = this.options;
|
|
135
154
|
const startTls = () => {
|
|
136
|
-
|
|
155
|
+
/* Only ever reached off workerd - the branch below hands that
|
|
156
|
+
runtime to its own upgrade - so the socket here is the
|
|
157
|
+
`net.Socket` `tls.connect()` wants, which the structural type
|
|
158
|
+
cannot say on its own. */
|
|
159
|
+
const tlsOptions = {
|
|
160
|
+
...options.ssl,
|
|
161
|
+
socket: socket,
|
|
162
|
+
};
|
|
137
163
|
if (target.host && net.isIP(target.host) === 0)
|
|
138
164
|
tlsOptions.servername = target.host;
|
|
139
165
|
if (options.sslNegotiation === 'direct')
|
|
@@ -142,11 +168,51 @@ export class PgSocket extends SafeEventEmitter {
|
|
|
142
168
|
tlsSocket.once('error', onError);
|
|
143
169
|
tlsSocket.once('secureConnect', () => onReady(tlsSocket));
|
|
144
170
|
};
|
|
145
|
-
|
|
146
|
-
!!options.requireSSL ||
|
|
147
|
-
options.sslNegotiation === 'direct';
|
|
148
|
-
if (!wantsSSL)
|
|
171
|
+
if (!wantsTls(options))
|
|
149
172
|
return onReady(socket);
|
|
173
|
+
/* Workerd upgrades through the socket it already has rather than by
|
|
174
|
+
wrapping it: `tls.connect({ socket })` does nothing it can carry
|
|
175
|
+
out, and measured, attempting it answers "Network connection
|
|
176
|
+
lost." after the server has already agreed to TLS. The exchange
|
|
177
|
+
either side of the upgrade is the same one as below - this branch
|
|
178
|
+
exists because the call in the middle is a different call.
|
|
179
|
+
|
|
180
|
+
One limit is the runtime's and no client escapes it: workerd's TLS
|
|
181
|
+
options are `{ expectedServerHostname?: string }` and nothing
|
|
182
|
+
else, so nothing here can say which certificate to trust, and a
|
|
183
|
+
server whose certificate does not chain to a public CA cannot be
|
|
184
|
+
reached over TLS at all - which is every local PostgreSQL with a
|
|
185
|
+
self-signed certificate. */
|
|
186
|
+
/* Direct negotiation is TLS from the first byte, and PostgreSQL 17
|
|
187
|
+
only accepts it when the client announces the `postgresql`
|
|
188
|
+
protocol over ALPN. Workerd's socket options are
|
|
189
|
+
`{ secureTransport, allowHalfOpen }` - there is nowhere to say it,
|
|
190
|
+
and the server answers by closing, which surfaces as an internal
|
|
191
|
+
error with a reference number. Refused by name instead. */
|
|
192
|
+
if (isWorkerd() && options.sslNegotiation === 'direct')
|
|
193
|
+
return onError(new Error('sslNegotiation "direct" is not available on Cloudflare Workers: it needs the ' +
|
|
194
|
+
'"postgresql" ALPN protocol, which the runtime offers no way to announce. ' +
|
|
195
|
+
'Leave sslNegotiation unset to negotiate with SSLRequest instead.'));
|
|
196
|
+
if (socket instanceof WorkerdSocket) {
|
|
197
|
+
const upgrade = () => {
|
|
198
|
+
socket.once('error', onError);
|
|
199
|
+
socket.once('secureConnect', () => onReady(socket));
|
|
200
|
+
socket.startTls(target.host && net.isIP(target.host) === 0 ? target.host : undefined);
|
|
201
|
+
};
|
|
202
|
+
socket.write(this._frontend.getSSLRequestMessage());
|
|
203
|
+
socket.readOnce((x) => {
|
|
204
|
+
const command = x?.toString();
|
|
205
|
+
if (command === 'S')
|
|
206
|
+
return upgrade();
|
|
207
|
+
if (command === 'N') {
|
|
208
|
+
if (options.requireSSL)
|
|
209
|
+
return onError(new Error('Server does not support SSL connections'));
|
|
210
|
+
return onReady(socket);
|
|
211
|
+
}
|
|
212
|
+
onError(new Error('Unexpected response from server: ' + command));
|
|
213
|
+
});
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
150
216
|
if (options.sslNegotiation === 'direct') {
|
|
151
217
|
// Straight into the handshake: no SSLRequest, nothing in the clear.
|
|
152
218
|
return startTls();
|
|
@@ -825,15 +891,28 @@ export class PgSocket extends SafeEventEmitter {
|
|
|
825
891
|
break;
|
|
826
892
|
case Protocol.AuthenticationMessageKind.SASL: {
|
|
827
893
|
const mode = this.options.channelBinding || 'prefer';
|
|
894
|
+
/* Encrypted and "can be bound to" are two questions on workerd,
|
|
895
|
+
where they are one on Node. Binding mixes a hash of the
|
|
896
|
+
server's certificate into the SCRAM proof, and that runtime
|
|
897
|
+
hands out no certificate - so a connection can be perfectly
|
|
898
|
+
secure there and still have nothing to bind to. Saying "not
|
|
899
|
+
using TLS" for it, which is what the one test used to answer,
|
|
900
|
+
sends the reader looking in the wrong place. */
|
|
901
|
+
const secure = this._socket instanceof tls.TLSSocket ||
|
|
902
|
+
(this._socket instanceof WorkerdSocket && this._socket.secure);
|
|
828
903
|
const tlsSocket = mode !== 'disable' && this._socket instanceof tls.TLSSocket
|
|
829
904
|
? this._socket
|
|
830
905
|
: undefined;
|
|
831
906
|
const offersBinding = !!msg.mechanisms?.includes('SCRAM-SHA-256-PLUS');
|
|
832
907
|
const useBinding = !!tlsSocket && offersBinding;
|
|
833
908
|
if (mode === 'require' && !useBinding) {
|
|
834
|
-
throw new Error(
|
|
835
|
-
? 'SASL: channelBinding is "require" but the
|
|
836
|
-
:
|
|
909
|
+
throw new Error(!secure
|
|
910
|
+
? 'SASL: channelBinding is "require" but the connection is not using TLS'
|
|
911
|
+
: !tlsSocket
|
|
912
|
+
? 'SASL: channelBinding is "require", but this runtime does not expose the ' +
|
|
913
|
+
'server certificate, so there is nothing to bind to. Cloudflare Workers is ' +
|
|
914
|
+
'one; use "prefer", which is the default.'
|
|
915
|
+
: 'SASL: channelBinding is "require" but the server does not offer SCRAM-SHA-256-PLUS');
|
|
837
916
|
}
|
|
838
917
|
if (!useBinding && !msg.mechanisms?.includes('SCRAM-SHA-256')) {
|
|
839
918
|
throw new Error('SASL: Only mechanisms SCRAM-SHA-256 and SCRAM-SHA-256-PLUS are supported');
|
|
@@ -9,11 +9,12 @@ export interface SmartBufferConfig {
|
|
|
9
9
|
houseKeepMs?: number;
|
|
10
10
|
}
|
|
11
11
|
/**
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
12
|
+
* The most a buffer may ever hold, given how much memory the machine has.
|
|
13
|
+
*
|
|
14
|
+
* @param totalMemory Bytes of memory, or 0 when that cannot be known.
|
|
15
|
+
* @returns Half of it, capped - or the cap alone when it is unknown.
|
|
16
16
|
*/
|
|
17
|
+
export declare function maxSizeFor(totalMemory: number): number;
|
|
17
18
|
export declare class SmartBuffer extends FlexyBuffer {
|
|
18
19
|
static DEFAULT_PAGE_SIZE: number;
|
|
19
20
|
static DEFAULT_MAX_SIZE: number;
|
package/protocol/smart-buffer.js
CHANGED
|
@@ -6,9 +6,29 @@ import * as os from 'os';
|
|
|
6
6
|
* length-prefixed string where a `null`/`undefined` value is written as a
|
|
7
7
|
* length of -1 with no bytes following.
|
|
8
8
|
*/
|
|
9
|
+
/** The ceiling on `maxSizeFor`, and its answer when there is no machine to measure. */
|
|
10
|
+
const HARD_MAX = 1024 * 1024 * 1024 * 2; // 2 GB
|
|
11
|
+
/**
|
|
12
|
+
* The most a buffer may ever hold, given how much memory the machine has.
|
|
13
|
+
*
|
|
14
|
+
* @param totalMemory Bytes of memory, or 0 when that cannot be known.
|
|
15
|
+
* @returns Half of it, capped - or the cap alone when it is unknown.
|
|
16
|
+
*/
|
|
17
|
+
/* `os.totalmem()` answers 0 where there is no machine to ask about:
|
|
18
|
+
measured under Cloudflare's workerd, where `node:os` resolves and
|
|
19
|
+
returns zeros rather than throwing. Taking the minimum of that was a
|
|
20
|
+
maximum size of zero, which worked only because flexy-buffer reads a
|
|
21
|
+
falsy `maxLength` as "use the default" - an accident, and one that
|
|
22
|
+
would have become a buffer refusing every write the day that default
|
|
23
|
+
changed. */
|
|
24
|
+
export function maxSizeFor(totalMemory) {
|
|
25
|
+
return totalMemory > 0
|
|
26
|
+
? Math.min(Math.floor(totalMemory / 2), HARD_MAX)
|
|
27
|
+
: HARD_MAX;
|
|
28
|
+
}
|
|
9
29
|
export class SmartBuffer extends FlexyBuffer {
|
|
10
30
|
static DEFAULT_PAGE_SIZE = 512;
|
|
11
|
-
static DEFAULT_MAX_SIZE =
|
|
31
|
+
static DEFAULT_MAX_SIZE = maxSizeFor(typeof os.totalmem === 'function' ? os.totalmem() : 0);
|
|
12
32
|
constructor(cfg) {
|
|
13
33
|
super({
|
|
14
34
|
pageSize: cfg?.pageSize || SmartBuffer.DEFAULT_PAGE_SIZE,
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { SafeEventEmitter } from '../safe-event-emitter.js';
|
|
2
|
+
/** What `cloudflare:sockets` hands back, as much of it as is used here. */
|
|
3
|
+
interface CfSocket {
|
|
4
|
+
readable: ReadableStream<Uint8Array>;
|
|
5
|
+
writable: WritableStream<Uint8Array>;
|
|
6
|
+
closed: Promise<void>;
|
|
7
|
+
close(): Promise<void>;
|
|
8
|
+
startTls(options: {
|
|
9
|
+
expectedServerHostname?: string;
|
|
10
|
+
}): CfSocket;
|
|
11
|
+
}
|
|
12
|
+
/** The one function `cloudflare:sockets` is reached for. */
|
|
13
|
+
export type CfConnect = (address: string, options?: {
|
|
14
|
+
secureTransport?: 'off' | 'on' | 'starttls';
|
|
15
|
+
}) => CfSocket;
|
|
16
|
+
/**
|
|
17
|
+
* A `net.Socket`-shaped view of a Cloudflare socket, for the part of it
|
|
18
|
+
* this client uses: `connect`, `write`, `destroy`, and `data`/`error`/
|
|
19
|
+
* `close` events, plus `startTls()`, which is the one thing Node's
|
|
20
|
+
* `tls.connect({ socket })` cannot do on that runtime.
|
|
21
|
+
*
|
|
22
|
+
* Reads are a loop over the stream rather than a `'data'` event the
|
|
23
|
+
* runtime emits, so the loop is what turns one into the other.
|
|
24
|
+
*/
|
|
25
|
+
export declare class WorkerdSocket extends SafeEventEmitter {
|
|
26
|
+
private readonly _wantsTls;
|
|
27
|
+
private readonly _connect;
|
|
28
|
+
private _cf?;
|
|
29
|
+
private _writer?;
|
|
30
|
+
private _reader?;
|
|
31
|
+
private _upgrading;
|
|
32
|
+
private _destroyed;
|
|
33
|
+
private _paused?;
|
|
34
|
+
private _resume?;
|
|
35
|
+
constructor(_wantsTls: boolean, _connect?: () => Promise<CfConnect>);
|
|
36
|
+
/** Whether a TLS upgrade has been asked for and completed. */
|
|
37
|
+
get secure(): boolean;
|
|
38
|
+
private _secure;
|
|
39
|
+
/** Whether `destroy()` has been called. */
|
|
40
|
+
get destroyed(): boolean;
|
|
41
|
+
/** Whether writes are still accepted. */
|
|
42
|
+
get writable(): boolean;
|
|
43
|
+
cork(): void;
|
|
44
|
+
uncork(): void;
|
|
45
|
+
setNoDelay(): this;
|
|
46
|
+
setKeepAlive(): this;
|
|
47
|
+
setTimeout(): this;
|
|
48
|
+
/** Stops reading, so the sender is made to wait. */
|
|
49
|
+
pause(): this;
|
|
50
|
+
/** Starts reading again. */
|
|
51
|
+
resume(): this;
|
|
52
|
+
/** Closes the connection once anything already written has gone. */
|
|
53
|
+
end(): this;
|
|
54
|
+
/**
|
|
55
|
+
* Opens the connection and emits `connect` once it is writable.
|
|
56
|
+
*
|
|
57
|
+
* @param port The port to reach.
|
|
58
|
+
* @param host The host to reach.
|
|
59
|
+
*/
|
|
60
|
+
connect(port: number, host: string): void;
|
|
61
|
+
/**
|
|
62
|
+
* Reads one chunk and hands it over, without starting the loop.
|
|
63
|
+
*
|
|
64
|
+
* @param cb Called with the bytes, or with nothing if the connection
|
|
65
|
+
* ended first.
|
|
66
|
+
*/
|
|
67
|
+
readOnce(cb: (data?: Buffer) => void): void;
|
|
68
|
+
/**
|
|
69
|
+
* Upgrades the connection in place.
|
|
70
|
+
*
|
|
71
|
+
* @param servername The name to check the certificate against.
|
|
72
|
+
*/
|
|
73
|
+
startTls(servername?: string): void;
|
|
74
|
+
/**
|
|
75
|
+
* Writes to the connection.
|
|
76
|
+
*
|
|
77
|
+
* @param data The bytes to send.
|
|
78
|
+
* @param cb Called once they are written, or with the error.
|
|
79
|
+
* @returns True - the runtime offers no "buffer is full" answer.
|
|
80
|
+
*/
|
|
81
|
+
write(data: Uint8Array, cb?: (err?: Error | null) => void): boolean;
|
|
82
|
+
/** Closes the connection; `close` follows once the runtime agrees. */
|
|
83
|
+
destroy(): this;
|
|
84
|
+
protected _attach(cf: CfSocket, readLoop: boolean): void;
|
|
85
|
+
protected _read(reader: ReadableStreamDefaultReader<Uint8Array>): Promise<void>;
|
|
86
|
+
}
|
|
87
|
+
export {};
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { SafeEventEmitter } from '../safe-event-emitter.js';
|
|
2
|
+
/* Imported where it is called rather than at the top of the file: the
|
|
3
|
+
specifier does not resolve off workerd, and this module is reachable
|
|
4
|
+
from a Node build through `pg-socket.ts`. Nothing awaits it unless a
|
|
5
|
+
connection is actually being made on that runtime. */
|
|
6
|
+
const importCfConnect = async () => (await import(/* webpackIgnore: true */ 'cloudflare:sockets'))
|
|
7
|
+
.connect;
|
|
8
|
+
/**
|
|
9
|
+
* A `net.Socket`-shaped view of a Cloudflare socket, for the part of it
|
|
10
|
+
* this client uses: `connect`, `write`, `destroy`, and `data`/`error`/
|
|
11
|
+
* `close` events, plus `startTls()`, which is the one thing Node's
|
|
12
|
+
* `tls.connect({ socket })` cannot do on that runtime.
|
|
13
|
+
*
|
|
14
|
+
* Reads are a loop over the stream rather than a `'data'` event the
|
|
15
|
+
* runtime emits, so the loop is what turns one into the other.
|
|
16
|
+
*/
|
|
17
|
+
/* Written here rather than taken from `pg-cloudflare`, which does the
|
|
18
|
+
same job for `pg`: the adapter is a hundred lines whose lifecycle this
|
|
19
|
+
client would otherwise not control, and only the upgrade is actually
|
|
20
|
+
needed - the plain path on workerd already works through `node:net`.
|
|
21
|
+
|
|
22
|
+
The setNoDelay/setKeepAlive/setTimeout trio answers nothing because
|
|
23
|
+
the runtime exposes no equivalent; they are here so the connect path
|
|
24
|
+
above can treat both socket kinds the same way. */
|
|
25
|
+
export class WorkerdSocket extends SafeEventEmitter {
|
|
26
|
+
_wantsTls;
|
|
27
|
+
_connect;
|
|
28
|
+
_cf;
|
|
29
|
+
_writer;
|
|
30
|
+
_reader;
|
|
31
|
+
/* True between startTls() and the old socket's `closed` settling: that
|
|
32
|
+
promise resolves because the socket was replaced, not because the
|
|
33
|
+
connection went away, and reporting it as a close would end the
|
|
34
|
+
session in the middle of its handshake. */
|
|
35
|
+
_upgrading = false;
|
|
36
|
+
_destroyed = false;
|
|
37
|
+
_paused;
|
|
38
|
+
_resume;
|
|
39
|
+
constructor(_wantsTls, _connect = importCfConnect) {
|
|
40
|
+
super();
|
|
41
|
+
this._wantsTls = _wantsTls;
|
|
42
|
+
this._connect = _connect;
|
|
43
|
+
}
|
|
44
|
+
/** Whether a TLS upgrade has been asked for and completed. */
|
|
45
|
+
get secure() {
|
|
46
|
+
return this._secure;
|
|
47
|
+
}
|
|
48
|
+
_secure = false;
|
|
49
|
+
/** Whether `destroy()` has been called. */
|
|
50
|
+
get destroyed() {
|
|
51
|
+
return this._destroyed;
|
|
52
|
+
}
|
|
53
|
+
/** Whether writes are still accepted. */
|
|
54
|
+
get writable() {
|
|
55
|
+
return !this._destroyed && !!this._writer;
|
|
56
|
+
}
|
|
57
|
+
/* `cork`/`uncork` gather several small writes into one syscall on
|
|
58
|
+
Node. There is no syscall to gather here - a write is a promise
|
|
59
|
+
against a stream - so the pair is accepted and does nothing, which
|
|
60
|
+
is what it means on this runtime. */
|
|
61
|
+
cork() {
|
|
62
|
+
/* nothing to gather */
|
|
63
|
+
}
|
|
64
|
+
uncork() {
|
|
65
|
+
/* nothing to gather */
|
|
66
|
+
}
|
|
67
|
+
setNoDelay() {
|
|
68
|
+
return this;
|
|
69
|
+
}
|
|
70
|
+
setKeepAlive() {
|
|
71
|
+
return this;
|
|
72
|
+
}
|
|
73
|
+
setTimeout() {
|
|
74
|
+
return this;
|
|
75
|
+
}
|
|
76
|
+
/** Stops reading, so the sender is made to wait. */
|
|
77
|
+
/* Real backpressure rather than a no-op: the read loop is what pulls
|
|
78
|
+
from the stream, so not pulling is the whole of it. `pause()` during
|
|
79
|
+
a COPY TO is the only caller, and a stream left unread is how the
|
|
80
|
+
runtime learns to stop. */
|
|
81
|
+
pause() {
|
|
82
|
+
if (!this._paused)
|
|
83
|
+
this._paused = new Promise(r => (this._resume = r));
|
|
84
|
+
return this;
|
|
85
|
+
}
|
|
86
|
+
/** Starts reading again. */
|
|
87
|
+
resume() {
|
|
88
|
+
const go = this._resume;
|
|
89
|
+
this._paused = undefined;
|
|
90
|
+
this._resume = undefined;
|
|
91
|
+
go?.();
|
|
92
|
+
return this;
|
|
93
|
+
}
|
|
94
|
+
/** Closes the connection once anything already written has gone. */
|
|
95
|
+
end() {
|
|
96
|
+
return this.destroy();
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Opens the connection and emits `connect` once it is writable.
|
|
100
|
+
*
|
|
101
|
+
* @param port The port to reach.
|
|
102
|
+
* @param host The host to reach.
|
|
103
|
+
*/
|
|
104
|
+
connect(port, host) {
|
|
105
|
+
void (async () => {
|
|
106
|
+
try {
|
|
107
|
+
const connect = await this._connect();
|
|
108
|
+
this._cf = connect(`${host}:${port}`, {
|
|
109
|
+
// `starttls` leaves the connection in the clear and allows a
|
|
110
|
+
// later upgrade, which is what the SSLRequest exchange needs.
|
|
111
|
+
secureTransport: this._wantsTls ? 'starttls' : 'off',
|
|
112
|
+
});
|
|
113
|
+
/* No read loop yet when a TLS upgrade is coming: `releaseLock()`
|
|
114
|
+
throws on a reader with an outstanding read, and a loop parked
|
|
115
|
+
in `reader.read()` is exactly that. The negotiation asks for
|
|
116
|
+
the one byte it expects through `readOnce()`, and the loop
|
|
117
|
+
starts on the upgraded socket. */
|
|
118
|
+
this._attach(this._cf, !this._wantsTls);
|
|
119
|
+
await this._writer.ready;
|
|
120
|
+
this.emit('connect');
|
|
121
|
+
}
|
|
122
|
+
catch (e) {
|
|
123
|
+
this.emit('error', e);
|
|
124
|
+
}
|
|
125
|
+
})();
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Reads one chunk and hands it over, without starting the loop.
|
|
129
|
+
*
|
|
130
|
+
* @param cb Called with the bytes, or with nothing if the connection
|
|
131
|
+
* ended first.
|
|
132
|
+
*/
|
|
133
|
+
/* What the SSLRequest exchange needs: a single answer, read while no
|
|
134
|
+
loop holds the reader, so the lock can be released for the upgrade
|
|
135
|
+
that follows. */
|
|
136
|
+
readOnce(cb) {
|
|
137
|
+
const reader = this._reader;
|
|
138
|
+
if (!reader)
|
|
139
|
+
return cb();
|
|
140
|
+
reader.read().then(({ done, value }) => cb(done || !value ? undefined : Buffer.from(value)), (e) => {
|
|
141
|
+
this.emit('error', e);
|
|
142
|
+
cb();
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Upgrades the connection in place.
|
|
147
|
+
*
|
|
148
|
+
* @param servername The name to check the certificate against.
|
|
149
|
+
*/
|
|
150
|
+
/* The locks have to go back before the socket is replaced, and the new
|
|
151
|
+
one needs its own reader and writer and its own read loop. */
|
|
152
|
+
startTls(servername) {
|
|
153
|
+
const cf = this._cf;
|
|
154
|
+
if (!cf) {
|
|
155
|
+
this.emit('error', new Error('Cannot upgrade a socket that is not connected'));
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
try {
|
|
159
|
+
this._writer.releaseLock();
|
|
160
|
+
this._reader.releaseLock();
|
|
161
|
+
this._upgrading = true;
|
|
162
|
+
this._cf = cf.startTls(servername ? { expectedServerHostname: servername } : {});
|
|
163
|
+
this._attach(this._cf, true);
|
|
164
|
+
void this._writer.ready.then(() => {
|
|
165
|
+
this._secure = true;
|
|
166
|
+
this.emit('secureConnect');
|
|
167
|
+
}, (e) => this.emit('error', e));
|
|
168
|
+
}
|
|
169
|
+
catch (e) {
|
|
170
|
+
this.emit('error', e);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Writes to the connection.
|
|
175
|
+
*
|
|
176
|
+
* @param data The bytes to send.
|
|
177
|
+
* @param cb Called once they are written, or with the error.
|
|
178
|
+
* @returns True - the runtime offers no "buffer is full" answer.
|
|
179
|
+
*/
|
|
180
|
+
write(data, cb) {
|
|
181
|
+
const writer = this._writer;
|
|
182
|
+
if (!writer) {
|
|
183
|
+
cb?.(new Error('Socket is not connected'));
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
writer.write(data).then(() => cb?.(), (e) => {
|
|
187
|
+
cb?.(e);
|
|
188
|
+
this.emit('error', e);
|
|
189
|
+
});
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
/** Closes the connection; `close` follows once the runtime agrees. */
|
|
193
|
+
destroy() {
|
|
194
|
+
if (this._destroyed)
|
|
195
|
+
return this;
|
|
196
|
+
this._destroyed = true;
|
|
197
|
+
const cf = this._cf;
|
|
198
|
+
this._cf = undefined;
|
|
199
|
+
cf?.close().catch(() => undefined);
|
|
200
|
+
return this;
|
|
201
|
+
}
|
|
202
|
+
_attach(cf, readLoop) {
|
|
203
|
+
this._writer = cf.writable.getWriter();
|
|
204
|
+
this._reader = cf.readable.getReader();
|
|
205
|
+
if (readLoop)
|
|
206
|
+
void this._read(this._reader);
|
|
207
|
+
cf.closed.then(() => {
|
|
208
|
+
if (this._upgrading) {
|
|
209
|
+
this._upgrading = false;
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
this.emit('close');
|
|
213
|
+
}, (e) => {
|
|
214
|
+
if (this._upgrading) {
|
|
215
|
+
this._upgrading = false;
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
this.emit('error', e);
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
async _read(reader) {
|
|
222
|
+
try {
|
|
223
|
+
for (;;) {
|
|
224
|
+
if (this._paused)
|
|
225
|
+
await this._paused;
|
|
226
|
+
const { done, value } = await reader.read();
|
|
227
|
+
if (done)
|
|
228
|
+
return;
|
|
229
|
+
/* Asked again on the way out: a pause() during a read already in
|
|
230
|
+
flight cannot un-await it, and delivering that chunk anyway
|
|
231
|
+
would be data arriving after the consumer said stop. */
|
|
232
|
+
if (this._paused)
|
|
233
|
+
await this._paused;
|
|
234
|
+
if (value?.length)
|
|
235
|
+
this.emit('data', Buffer.from(value));
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
catch (e) {
|
|
239
|
+
// A read that ends because the socket was replaced is the upgrade
|
|
240
|
+
// happening, not a failure to report.
|
|
241
|
+
if (!this._upgrading && !this._destroyed)
|
|
242
|
+
this.emit('error', e);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
package/util/runtime.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether this client is running inside a Cloudflare Worker.
|
|
3
|
+
*
|
|
4
|
+
* @returns True on workerd, false on Node.js, Bun and anything else.
|
|
5
|
+
*/
|
|
6
|
+
/* The documented tell, and the one `pg` uses: since the
|
|
7
|
+
`global_navigator` compatibility flag went on, workerd defines
|
|
8
|
+
`navigator.userAgent` and sets it to exactly this. Node and Bun define
|
|
9
|
+
`navigator` too, with a userAgent of their own, so the test is for the
|
|
10
|
+
whole string rather than a substring of it.
|
|
11
|
+
|
|
12
|
+
Asked on each call rather than read once at load: it is reached once
|
|
13
|
+
per connection, where the cost is nothing, and a value frozen at
|
|
14
|
+
module load cannot be tested from both sides. */
|
|
15
|
+
export function isWorkerd() {
|
|
16
|
+
return (typeof navigator === 'object' &&
|
|
17
|
+
navigator !== null &&
|
|
18
|
+
navigator.userAgent === 'Cloudflare-Workers');
|
|
19
|
+
}
|