orez 0.0.47 → 0.0.49
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/dist/admin/http-proxy.d.ts.map +1 -1
- package/dist/admin/http-proxy.js.map +1 -1
- package/dist/admin/log-store.d.ts.map +1 -1
- package/dist/admin/log-store.js.map +1 -1
- package/dist/admin/server.d.ts +2 -2
- package/dist/admin/server.d.ts.map +1 -1
- package/dist/admin/server.js.map +1 -1
- package/dist/admin/ui.d.ts.map +1 -1
- package/dist/admin/ui.js +2 -2
- package/dist/admin/ui.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +6 -112
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +0 -5
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +0 -5
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +0 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +91 -249
- package/dist/index.js.map +1 -1
- package/dist/log.d.ts +0 -9
- package/dist/log.d.ts.map +1 -1
- package/dist/log.js +1 -24
- package/dist/log.js.map +1 -1
- package/dist/mutex.d.ts.map +1 -1
- package/dist/mutex.js +2 -13
- package/dist/mutex.js.map +1 -1
- package/dist/pg-proxy.d.ts +2 -3
- package/dist/pg-proxy.d.ts.map +1 -1
- package/dist/pg-proxy.js +167 -377
- package/dist/pg-proxy.js.map +1 -1
- package/dist/pglite-manager.d.ts +0 -1
- package/dist/pglite-manager.d.ts.map +1 -1
- package/dist/pglite-manager.js +1 -1
- package/dist/pglite-manager.js.map +1 -1
- package/dist/replication/change-tracker.d.ts +0 -6
- package/dist/replication/change-tracker.d.ts.map +1 -1
- package/dist/replication/change-tracker.js +0 -74
- package/dist/replication/change-tracker.js.map +1 -1
- package/dist/replication/handler.d.ts.map +1 -1
- package/dist/replication/handler.js +5 -47
- package/dist/replication/handler.js.map +1 -1
- package/dist/vite-plugin.d.ts +0 -3
- package/dist/vite-plugin.d.ts.map +1 -1
- package/dist/vite-plugin.js +0 -24
- package/dist/vite-plugin.js.map +1 -1
- package/package.json +5 -4
- package/src/admin/http-proxy.ts +5 -1
- package/src/admin/log-store.ts +4 -1
- package/src/admin/server.ts +7 -3
- package/src/admin/ui.ts +682 -680
- package/src/cli.ts +6 -111
- package/src/config.ts +0 -10
- package/src/index.ts +92 -262
- package/src/integration/integration.test.ts +264 -133
- package/src/log.ts +1 -25
- package/src/mutex.ts +2 -12
- package/src/pg-proxy.ts +187 -449
- package/src/pglite-manager.ts +1 -1
- package/src/replication/change-tracker.ts +0 -92
- package/src/replication/handler.ts +4 -50
- package/src/shim/hooks.mjs +34 -1
- package/src/vite-plugin.ts +0 -28
- package/src/wasm-sqlite.test.ts +1 -2
package/dist/mutex.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mutex.js","sourceRoot":"","sources":["../src/mutex.ts"],"names":[],"mappings":"AAAA,
|
|
1
|
+
{"version":3,"file":"mutex.js","sourceRoot":"","sources":["../src/mutex.ts"],"names":[],"mappings":"AAAA,6CAA6C;AAC7C,MAAM,OAAO,KAAK;IACR,MAAM,GAAG,KAAK,CAAA;IACd,KAAK,GAAsB,EAAE,CAAA;IAErC,KAAK,CAAC,OAAO;QACX,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;YACjB,IAAI,CAAC,MAAM,GAAG,IAAI,CAAA;YAClB,OAAM;QACR,CAAC;QACD,OAAO,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE;YACnC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;QAC1B,CAAC,CAAC,CAAA;IACJ,CAAC;IAED,OAAO;QACL,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAA;QAC/B,IAAI,IAAI,EAAE,CAAC;YACT,IAAI,EAAE,CAAA;QACR,CAAC;aAAM,CAAC;YACN,IAAI,CAAC,MAAM,GAAG,KAAK,CAAA;QACrB,CAAC;IACH,CAAC;CACF"}
|
package/dist/pg-proxy.d.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* tcp proxy that makes pglite speak postgresql wire protocol.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* connections with large responses.
|
|
4
|
+
* uses pg-gateway to handle protocol lifecycle for regular connections,
|
|
5
|
+
* and directly handles the raw socket for replication connections.
|
|
7
6
|
*
|
|
8
7
|
* regular connections: forwarded to pglite via execProtocolRaw()
|
|
9
8
|
* replication connections: intercepted, replication protocol faked
|
package/dist/pg-proxy.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pg-proxy.d.ts","sourceRoot":"","sources":["../src/pg-proxy.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"pg-proxy.d.ts","sourceRoot":"","sources":["../src/pg-proxy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAgB,KAAK,MAAM,EAAe,MAAM,UAAU,CAAA;AAQjE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AACjD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAA;AAC1D,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAA;AAuQlD,wBAAsB,YAAY,CAChC,OAAO,EAAE,MAAM,GAAG,eAAe,EACjC,MAAM,EAAE,cAAc,GACrB,OAAO,CAAC,MAAM,CAAC,CA2JjB"}
|
package/dist/pg-proxy.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* tcp proxy that makes pglite speak postgresql wire protocol.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* connections with large responses.
|
|
4
|
+
* uses pg-gateway to handle protocol lifecycle for regular connections,
|
|
5
|
+
* and directly handles the raw socket for replication connections.
|
|
7
6
|
*
|
|
8
7
|
* regular connections: forwarded to pglite via execProtocolRaw()
|
|
9
8
|
* replication connections: intercepted, replication protocol faked
|
|
@@ -13,6 +12,7 @@
|
|
|
13
12
|
* query interleaving that causes CVR concurrent modification errors.
|
|
14
13
|
*/
|
|
15
14
|
import { createServer } from 'node:net';
|
|
15
|
+
import { fromNodeSocket } from 'pg-gateway/node';
|
|
16
16
|
import { log } from './log.js';
|
|
17
17
|
import { Mutex } from './mutex.js';
|
|
18
18
|
import { handleReplicationQuery, handleStartReplication } from './replication/handler.js';
|
|
@@ -55,7 +55,6 @@ const QUERY_REWRITES = [
|
|
|
55
55
|
// parameter status messages sent during connection handshake
|
|
56
56
|
// pg_restore and other tools read these to determine server capabilities
|
|
57
57
|
const SERVER_PARAMS = [
|
|
58
|
-
['server_version', '16.4'],
|
|
59
58
|
['server_encoding', 'UTF8'],
|
|
60
59
|
['client_encoding', 'UTF8'],
|
|
61
60
|
['DateStyle', 'ISO, MDY'],
|
|
@@ -64,10 +63,7 @@ const SERVER_PARAMS = [
|
|
|
64
63
|
['TimeZone', 'UTC'],
|
|
65
64
|
['IntervalStyle', 'postgres'],
|
|
66
65
|
];
|
|
67
|
-
//
|
|
68
|
-
// pglite rejects SET TRANSACTION if any query (e.g. SET search_path) ran first
|
|
69
|
-
const NOOP_QUERY_PATTERNS = [/^\s*SET\s+TRANSACTION\b/i, /^\s*SET\s+SESSION\b/i];
|
|
70
|
-
// ── wire protocol helpers ──
|
|
66
|
+
// build a ParameterStatus wire protocol message (type 'S', 0x53)
|
|
71
67
|
function buildParameterStatus(name, value) {
|
|
72
68
|
const encoder = new TextEncoder();
|
|
73
69
|
const nameBytes = encoder.encode(name);
|
|
@@ -85,58 +81,12 @@ function buildParameterStatus(name, value) {
|
|
|
85
81
|
buf[pos] = 0;
|
|
86
82
|
return buf;
|
|
87
83
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
95
|
-
function buildAuthCleartextPassword() {
|
|
96
|
-
const buf = new Uint8Array(9);
|
|
97
|
-
buf[0] = 0x52; // 'R'
|
|
98
|
-
new DataView(buf.buffer).setInt32(1, 8);
|
|
99
|
-
new DataView(buf.buffer).setInt32(5, 3); // cleartext password
|
|
100
|
-
return buf;
|
|
101
|
-
}
|
|
102
|
-
function buildBackendKeyData() {
|
|
103
|
-
const buf = new Uint8Array(13);
|
|
104
|
-
buf[0] = 0x4b; // 'K'
|
|
105
|
-
new DataView(buf.buffer).setInt32(1, 12);
|
|
106
|
-
new DataView(buf.buffer).setInt32(5, process.pid);
|
|
107
|
-
new DataView(buf.buffer).setInt32(9, 0);
|
|
108
|
-
return buf;
|
|
109
|
-
}
|
|
110
|
-
function buildReadyForQuery(status = 0x49) {
|
|
111
|
-
const buf = new Uint8Array(6);
|
|
112
|
-
buf[0] = 0x5a; // 'Z'
|
|
113
|
-
new DataView(buf.buffer).setInt32(1, 5);
|
|
114
|
-
buf[5] = status; // 'I' = idle
|
|
115
|
-
return buf;
|
|
116
|
-
}
|
|
117
|
-
function buildErrorResponse(message) {
|
|
118
|
-
const encoder = new TextEncoder();
|
|
119
|
-
const msgBytes = encoder.encode(message);
|
|
120
|
-
// S(ERROR) + C(code) + M(message) + terminator
|
|
121
|
-
const sField = new Uint8Array([0x53, ...encoder.encode('ERROR'), 0]);
|
|
122
|
-
const cField = new Uint8Array([0x43, ...encoder.encode('08006'), 0]);
|
|
123
|
-
const mField = new Uint8Array([0x4d, ...msgBytes, 0]);
|
|
124
|
-
const terminator = new Uint8Array([0]);
|
|
125
|
-
const bodyLen = 4 + sField.length + cField.length + mField.length + terminator.length;
|
|
126
|
-
const buf = new Uint8Array(1 + bodyLen);
|
|
127
|
-
buf[0] = 0x45; // 'E'
|
|
128
|
-
new DataView(buf.buffer).setInt32(1, bodyLen);
|
|
129
|
-
let pos = 5;
|
|
130
|
-
buf.set(sField, pos);
|
|
131
|
-
pos += sField.length;
|
|
132
|
-
buf.set(cField, pos);
|
|
133
|
-
pos += cField.length;
|
|
134
|
-
buf.set(mField, pos);
|
|
135
|
-
pos += mField.length;
|
|
136
|
-
buf.set(terminator, pos);
|
|
137
|
-
return buf;
|
|
138
|
-
}
|
|
139
|
-
// ── query helpers ──
|
|
84
|
+
// queries to intercept and return no-op success (synthetic SET response)
|
|
85
|
+
// pglite rejects SET TRANSACTION if any query (e.g. SET search_path) ran first
|
|
86
|
+
const NOOP_QUERY_PATTERNS = [/^\s*SET\s+TRANSACTION\b/i, /^\s*SET\s+SESSION\b/i];
|
|
87
|
+
/**
|
|
88
|
+
* extract query text from a Parse message (0x50).
|
|
89
|
+
*/
|
|
140
90
|
function extractParseQuery(data) {
|
|
141
91
|
if (data[0] !== 0x50)
|
|
142
92
|
return null;
|
|
@@ -149,6 +99,9 @@ function extractParseQuery(data) {
|
|
|
149
99
|
offset++;
|
|
150
100
|
return new TextDecoder().decode(data.subarray(queryStart, offset));
|
|
151
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* rebuild a Parse message with a modified query string.
|
|
104
|
+
*/
|
|
152
105
|
function rebuildParseMessage(data, newQuery) {
|
|
153
106
|
let offset = 5;
|
|
154
107
|
while (offset < data.length && data[offset] !== 0)
|
|
@@ -176,6 +129,9 @@ function rebuildParseMessage(data, newQuery) {
|
|
|
176
129
|
result.set(suffix, pos);
|
|
177
130
|
return result;
|
|
178
131
|
}
|
|
132
|
+
/**
|
|
133
|
+
* rebuild a Simple Query message with a modified query string.
|
|
134
|
+
*/
|
|
179
135
|
function rebuildSimpleQuery(newQuery) {
|
|
180
136
|
const encoder = new TextEncoder();
|
|
181
137
|
const queryBytes = encoder.encode(newQuery + '\0');
|
|
@@ -185,6 +141,9 @@ function rebuildSimpleQuery(newQuery) {
|
|
|
185
141
|
buf.set(queryBytes, 5);
|
|
186
142
|
return buf;
|
|
187
143
|
}
|
|
144
|
+
/**
|
|
145
|
+
* intercept and rewrite query messages to make pglite look like real postgres.
|
|
146
|
+
*/
|
|
188
147
|
function interceptQuery(data) {
|
|
189
148
|
const msgType = data[0];
|
|
190
149
|
if (msgType === 0x51) {
|
|
@@ -224,6 +183,9 @@ function interceptQuery(data) {
|
|
|
224
183
|
}
|
|
225
184
|
return data;
|
|
226
185
|
}
|
|
186
|
+
/**
|
|
187
|
+
* check if a query should be intercepted as a no-op.
|
|
188
|
+
*/
|
|
227
189
|
function isNoopQuery(data) {
|
|
228
190
|
let query = null;
|
|
229
191
|
if (data[0] === 0x51) {
|
|
@@ -238,6 +200,9 @@ function isNoopQuery(data) {
|
|
|
238
200
|
return false;
|
|
239
201
|
return NOOP_QUERY_PATTERNS.some((p) => p.test(query));
|
|
240
202
|
}
|
|
203
|
+
/**
|
|
204
|
+
* build a synthetic "SET" command complete response.
|
|
205
|
+
*/
|
|
241
206
|
function buildSetCompleteResponse() {
|
|
242
207
|
const encoder = new TextEncoder();
|
|
243
208
|
const tag = encoder.encode('SET\0');
|
|
@@ -254,12 +219,18 @@ function buildSetCompleteResponse() {
|
|
|
254
219
|
result.set(rfq, cc.length);
|
|
255
220
|
return result;
|
|
256
221
|
}
|
|
222
|
+
/**
|
|
223
|
+
* build a synthetic ParseComplete response for extended protocol no-ops.
|
|
224
|
+
*/
|
|
257
225
|
function buildParseCompleteResponse() {
|
|
258
226
|
const pc = new Uint8Array(5);
|
|
259
227
|
pc[0] = 0x31; // ParseComplete
|
|
260
228
|
new DataView(pc.buffer).setInt32(1, 4);
|
|
261
229
|
return pc;
|
|
262
230
|
}
|
|
231
|
+
/**
|
|
232
|
+
* strip ReadyForQuery messages from a response buffer.
|
|
233
|
+
*/
|
|
263
234
|
function stripReadyForQuery(data) {
|
|
264
235
|
if (data.length === 0)
|
|
265
236
|
return data;
|
|
@@ -289,273 +260,6 @@ function stripReadyForQuery(data) {
|
|
|
289
260
|
}
|
|
290
261
|
return result;
|
|
291
262
|
}
|
|
292
|
-
// ── socket write with backpressure ──
|
|
293
|
-
function socketWrite(socket, data) {
|
|
294
|
-
if (data.length === 0 || socket.destroyed)
|
|
295
|
-
return Promise.resolve();
|
|
296
|
-
return new Promise((resolve, reject) => {
|
|
297
|
-
const ok = socket.write(data, (err) => (err ? reject(err) : resolve()));
|
|
298
|
-
// if buffer is full, the callback still fires when flushed
|
|
299
|
-
if (!ok)
|
|
300
|
-
void 0;
|
|
301
|
-
});
|
|
302
|
-
}
|
|
303
|
-
// ── startup handshake ──
|
|
304
|
-
// parse startup message from raw bytes.
|
|
305
|
-
// handles SSLRequest (8 bytes, code 80877103) and StartupMessage.
|
|
306
|
-
function parseStartupMessage(buf) {
|
|
307
|
-
const dv = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
308
|
-
const len = dv.getInt32(0);
|
|
309
|
-
const code = dv.getInt32(4);
|
|
310
|
-
// SSL request: length=8, code=80877103
|
|
311
|
-
if (len === 8 && code === 80877103) {
|
|
312
|
-
return { isSSL: true, params: {} };
|
|
313
|
-
}
|
|
314
|
-
// startup message: length, protocol(196608=3.0), then key=value pairs
|
|
315
|
-
const params = {};
|
|
316
|
-
let offset = 8;
|
|
317
|
-
while (offset < len) {
|
|
318
|
-
const keyStart = offset;
|
|
319
|
-
while (offset < buf.length && buf[offset] !== 0)
|
|
320
|
-
offset++;
|
|
321
|
-
const key = buf.subarray(keyStart, offset).toString();
|
|
322
|
-
offset++;
|
|
323
|
-
if (!key)
|
|
324
|
-
break; // double-null = end of params
|
|
325
|
-
const valStart = offset;
|
|
326
|
-
while (offset < buf.length && buf[offset] !== 0)
|
|
327
|
-
offset++;
|
|
328
|
-
params[key] = buf.subarray(valStart, offset).toString();
|
|
329
|
-
offset++;
|
|
330
|
-
}
|
|
331
|
-
return { isSSL: false, params };
|
|
332
|
-
}
|
|
333
|
-
// read exactly `n` bytes from socket
|
|
334
|
-
function readBytes(socket, n) {
|
|
335
|
-
return new Promise((resolve, reject) => {
|
|
336
|
-
let collected = Buffer.alloc(0);
|
|
337
|
-
const onData = (chunk) => {
|
|
338
|
-
collected = Buffer.concat([collected, chunk]);
|
|
339
|
-
if (collected.length >= n) {
|
|
340
|
-
socket.removeListener('data', onData);
|
|
341
|
-
socket.removeListener('error', onError);
|
|
342
|
-
socket.removeListener('close', onClose);
|
|
343
|
-
socket.pause();
|
|
344
|
-
resolve(collected);
|
|
345
|
-
}
|
|
346
|
-
};
|
|
347
|
-
const onError = (err) => {
|
|
348
|
-
socket.removeListener('data', onData);
|
|
349
|
-
socket.removeListener('close', onClose);
|
|
350
|
-
reject(err);
|
|
351
|
-
};
|
|
352
|
-
const onClose = () => {
|
|
353
|
-
socket.removeListener('data', onData);
|
|
354
|
-
socket.removeListener('error', onError);
|
|
355
|
-
reject(new Error('socket closed'));
|
|
356
|
-
};
|
|
357
|
-
socket.on('data', onData);
|
|
358
|
-
socket.on('error', onError);
|
|
359
|
-
socket.on('close', onClose);
|
|
360
|
-
socket.resume();
|
|
361
|
-
});
|
|
362
|
-
}
|
|
363
|
-
// perform the startup handshake (SSL negotiation, auth, parameter status)
|
|
364
|
-
async function performHandshake(socket, config) {
|
|
365
|
-
// read initial message length (first 4 bytes)
|
|
366
|
-
let buf = await readBytes(socket, 8);
|
|
367
|
-
// check for SSL request
|
|
368
|
-
const startup = parseStartupMessage(buf);
|
|
369
|
-
if (startup.isSSL) {
|
|
370
|
-
// reject SSL, client will reconnect without it
|
|
371
|
-
socket.write(Buffer.from('N'));
|
|
372
|
-
buf = await readBytes(socket, 8);
|
|
373
|
-
}
|
|
374
|
-
// now we have startup message header - read the rest if needed
|
|
375
|
-
const dv = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
376
|
-
const msgLen = dv.getInt32(0);
|
|
377
|
-
if (buf.length < msgLen) {
|
|
378
|
-
const rest = await readBytes(socket, msgLen - buf.length);
|
|
379
|
-
buf = Buffer.concat([buf, rest]);
|
|
380
|
-
}
|
|
381
|
-
const { params } = parseStartupMessage(buf);
|
|
382
|
-
// request cleartext password
|
|
383
|
-
socket.write(buildAuthCleartextPassword());
|
|
384
|
-
// read password message: type(1) + len(4) + password + null
|
|
385
|
-
const pwBuf = await readBytes(socket, 5);
|
|
386
|
-
const pwDv = new DataView(pwBuf.buffer, pwBuf.byteOffset, pwBuf.byteLength);
|
|
387
|
-
const pwLen = pwDv.getInt32(1);
|
|
388
|
-
let fullPwBuf = pwBuf;
|
|
389
|
-
if (fullPwBuf.length < 1 + pwLen) {
|
|
390
|
-
const rest = await readBytes(socket, 1 + pwLen - fullPwBuf.length);
|
|
391
|
-
fullPwBuf = Buffer.concat([fullPwBuf, rest]);
|
|
392
|
-
}
|
|
393
|
-
const password = fullPwBuf.subarray(5, 1 + pwLen - 1).toString();
|
|
394
|
-
// validate credentials
|
|
395
|
-
if (params.user !== config.pgUser || password !== config.pgPassword) {
|
|
396
|
-
socket.write(buildErrorResponse('authentication failed'));
|
|
397
|
-
socket.write(buildReadyForQuery());
|
|
398
|
-
socket.destroy();
|
|
399
|
-
throw new Error('auth failed');
|
|
400
|
-
}
|
|
401
|
-
// auth ok
|
|
402
|
-
socket.write(buildAuthOk());
|
|
403
|
-
// send parameter status messages
|
|
404
|
-
for (const [name, value] of SERVER_PARAMS) {
|
|
405
|
-
socket.write(buildParameterStatus(name, value));
|
|
406
|
-
}
|
|
407
|
-
// backend key data
|
|
408
|
-
socket.write(buildBackendKeyData());
|
|
409
|
-
// ready for query
|
|
410
|
-
socket.write(buildReadyForQuery());
|
|
411
|
-
return { params };
|
|
412
|
-
}
|
|
413
|
-
// ── connection tracking ──
|
|
414
|
-
// per-database active connection count. pglite is single-session so all
|
|
415
|
-
// connections share one transaction context. we skip ROLLBACK on close when
|
|
416
|
-
// other connections are still active to avoid killing their transactions.
|
|
417
|
-
const activeConns = {};
|
|
418
|
-
let connCounter = 0;
|
|
419
|
-
// ── message loop ──
|
|
420
|
-
// process messages from a connected, authenticated client.
|
|
421
|
-
// uses callback-based 'data' events instead of async iterators
|
|
422
|
-
// for reliable behavior across runtimes (node.js, bun).
|
|
423
|
-
function messageLoop(socket, db, mutex, isReplicationConnection, replicationDb, replicationMutex) {
|
|
424
|
-
return new Promise((resolve, reject) => {
|
|
425
|
-
let buffer = Buffer.alloc(0);
|
|
426
|
-
let processing = false;
|
|
427
|
-
async function processBuffer() {
|
|
428
|
-
if (processing)
|
|
429
|
-
return;
|
|
430
|
-
processing = true;
|
|
431
|
-
socket.pause();
|
|
432
|
-
try {
|
|
433
|
-
while (buffer.length >= 5) {
|
|
434
|
-
const msgType = buffer[0];
|
|
435
|
-
const dv = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength);
|
|
436
|
-
const msgLen = dv.getInt32(1);
|
|
437
|
-
const totalLen = 1 + msgLen;
|
|
438
|
-
if (buffer.length < totalLen)
|
|
439
|
-
break; // need more data
|
|
440
|
-
// copy message out before modifying buffer
|
|
441
|
-
const message = new Uint8Array(buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + totalLen));
|
|
442
|
-
buffer = buffer.subarray(totalLen);
|
|
443
|
-
// handle Terminate message
|
|
444
|
-
if (msgType === 0x58) {
|
|
445
|
-
resolve();
|
|
446
|
-
return;
|
|
447
|
-
}
|
|
448
|
-
// handle replication connections
|
|
449
|
-
if (isReplicationConnection) {
|
|
450
|
-
await handleReplicationMsg(message, socket, replicationDb, replicationMutex);
|
|
451
|
-
continue;
|
|
452
|
-
}
|
|
453
|
-
// handle regular messages
|
|
454
|
-
await handleRegularMessage(message, socket, db, mutex);
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
catch (err) {
|
|
458
|
-
reject(err);
|
|
459
|
-
return;
|
|
460
|
-
}
|
|
461
|
-
processing = false;
|
|
462
|
-
socket.resume();
|
|
463
|
-
}
|
|
464
|
-
socket.on('data', (chunk) => {
|
|
465
|
-
buffer = buffer.length > 0 ? Buffer.concat([buffer, chunk]) : chunk;
|
|
466
|
-
processBuffer();
|
|
467
|
-
});
|
|
468
|
-
socket.on('end', () => resolve());
|
|
469
|
-
socket.on('error', (err) => reject(err));
|
|
470
|
-
socket.on('close', () => resolve());
|
|
471
|
-
socket.resume();
|
|
472
|
-
});
|
|
473
|
-
}
|
|
474
|
-
async function handleRegularMessage(data, socket, db, mutex) {
|
|
475
|
-
// check for no-op queries
|
|
476
|
-
if (isNoopQuery(data)) {
|
|
477
|
-
if (data[0] === 0x51) {
|
|
478
|
-
await socketWrite(socket, buildSetCompleteResponse());
|
|
479
|
-
return;
|
|
480
|
-
}
|
|
481
|
-
else if (data[0] === 0x50) {
|
|
482
|
-
await socketWrite(socket, buildParseCompleteResponse());
|
|
483
|
-
return;
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
// intercept and rewrite queries
|
|
487
|
-
data = interceptQuery(data);
|
|
488
|
-
// serialize pglite access
|
|
489
|
-
await mutex.acquire();
|
|
490
|
-
let result;
|
|
491
|
-
try {
|
|
492
|
-
result = await db.execProtocolRaw(data, { throwOnError: false });
|
|
493
|
-
}
|
|
494
|
-
catch (err) {
|
|
495
|
-
mutex.release();
|
|
496
|
-
// send error response instead of killing the connection — PGlite internal
|
|
497
|
-
// errors shouldn't terminate the client's tcp session
|
|
498
|
-
log.debug.proxy(`execProtocolRaw error: ${err?.message || err}`);
|
|
499
|
-
const errMsg = err?.message || 'internal error';
|
|
500
|
-
const errResp = buildErrorResponse(errMsg);
|
|
501
|
-
const rfq = buildReadyForQuery(0x45); // 'E' = failed transaction
|
|
502
|
-
const combined = new Uint8Array(errResp.length + rfq.length);
|
|
503
|
-
combined.set(errResp, 0);
|
|
504
|
-
combined.set(rfq, errResp.length);
|
|
505
|
-
await socketWrite(socket, combined);
|
|
506
|
-
return;
|
|
507
|
-
}
|
|
508
|
-
// strip ReadyForQuery from non-Sync/non-SimpleQuery responses
|
|
509
|
-
if (data[0] !== 0x53 && data[0] !== 0x51) {
|
|
510
|
-
result = stripReadyForQuery(result);
|
|
511
|
-
}
|
|
512
|
-
mutex.release();
|
|
513
|
-
// write response directly to socket
|
|
514
|
-
await socketWrite(socket, result);
|
|
515
|
-
}
|
|
516
|
-
async function handleReplicationMsg(data, socket, db, mutex) {
|
|
517
|
-
if (data[0] !== 0x51)
|
|
518
|
-
return;
|
|
519
|
-
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
520
|
-
const len = view.getInt32(1);
|
|
521
|
-
const query = new TextDecoder().decode(data.subarray(5, 1 + len - 1)).replace(/\0$/, '');
|
|
522
|
-
const upper = query.trim().toUpperCase();
|
|
523
|
-
log.debug.proxy(`repl query: ${query.slice(0, 200)}`);
|
|
524
|
-
if (upper.startsWith('START_REPLICATION')) {
|
|
525
|
-
const writer = {
|
|
526
|
-
write(chunk) {
|
|
527
|
-
if (!socket.destroyed) {
|
|
528
|
-
socket.write(chunk);
|
|
529
|
-
}
|
|
530
|
-
},
|
|
531
|
-
};
|
|
532
|
-
// drain incoming standby status updates
|
|
533
|
-
socket.on('data', (_chunk) => { });
|
|
534
|
-
socket.on('close', () => socket.destroy());
|
|
535
|
-
// this runs indefinitely until the socket closes
|
|
536
|
-
await handleStartReplication(query, writer, db, mutex).catch((err) => {
|
|
537
|
-
log.debug.proxy(`replication stream ended: ${err}`);
|
|
538
|
-
});
|
|
539
|
-
return;
|
|
540
|
-
}
|
|
541
|
-
// handle replication queries + fallthrough to pglite
|
|
542
|
-
await mutex.acquire();
|
|
543
|
-
try {
|
|
544
|
-
const response = await handleReplicationQuery(query, db);
|
|
545
|
-
if (response) {
|
|
546
|
-
await socketWrite(socket, response);
|
|
547
|
-
return;
|
|
548
|
-
}
|
|
549
|
-
// apply query rewrites before forwarding
|
|
550
|
-
data = interceptQuery(data);
|
|
551
|
-
const result = await db.execProtocolRaw(data, { throwOnError: false });
|
|
552
|
-
await socketWrite(socket, result);
|
|
553
|
-
}
|
|
554
|
-
finally {
|
|
555
|
-
mutex.release();
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
// ── main entry point ──
|
|
559
263
|
export async function startPgProxy(dbInput, config) {
|
|
560
264
|
// normalize input: single PGlite instance = use it for all databases (backwards compat for tests)
|
|
561
265
|
const instances = 'postgres' in dbInput
|
|
@@ -567,6 +271,7 @@ export async function startPgProxy(dbInput, config) {
|
|
|
567
271
|
cvr: new Mutex(),
|
|
568
272
|
cdb: new Mutex(),
|
|
569
273
|
};
|
|
274
|
+
// helper to get instance + mutex for a database name
|
|
570
275
|
function getDbContext(dbName) {
|
|
571
276
|
if (dbName === 'zero_cvr')
|
|
572
277
|
return { db: instances.cvr, mutex: mutexes.cvr };
|
|
@@ -575,64 +280,105 @@ export async function startPgProxy(dbInput, config) {
|
|
|
575
280
|
return { db: instances.postgres, mutex: mutexes.postgres };
|
|
576
281
|
}
|
|
577
282
|
const server = createServer(async (socket) => {
|
|
283
|
+
// prevent idle timeouts from killing connections
|
|
578
284
|
socket.setKeepAlive(true, 30000);
|
|
579
285
|
socket.setTimeout(0);
|
|
580
|
-
socket.setNoDelay(true);
|
|
581
286
|
let dbName = 'postgres';
|
|
582
287
|
let isReplicationConnection = false;
|
|
583
|
-
|
|
288
|
+
// clean up pglite transaction state when a client disconnects
|
|
289
|
+
socket.on('close', async () => {
|
|
290
|
+
const { db, mutex } = getDbContext(dbName);
|
|
291
|
+
await mutex.acquire();
|
|
292
|
+
try {
|
|
293
|
+
await db.exec('ROLLBACK');
|
|
294
|
+
}
|
|
295
|
+
catch {
|
|
296
|
+
// no transaction to rollback
|
|
297
|
+
}
|
|
298
|
+
finally {
|
|
299
|
+
mutex.release();
|
|
300
|
+
}
|
|
301
|
+
});
|
|
584
302
|
try {
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
303
|
+
const connection = await fromNodeSocket(socket, {
|
|
304
|
+
serverVersion: '16.4',
|
|
305
|
+
auth: {
|
|
306
|
+
method: 'password',
|
|
307
|
+
getClearTextPassword() {
|
|
308
|
+
return config.pgPassword;
|
|
309
|
+
},
|
|
310
|
+
validateCredentials(credentials) {
|
|
311
|
+
return (credentials.password === credentials.clearTextPassword &&
|
|
312
|
+
credentials.username === config.pgUser);
|
|
313
|
+
},
|
|
314
|
+
},
|
|
315
|
+
// send ParameterStatus messages that standard postgres tools expect
|
|
316
|
+
// pg-gateway sends server_version via the serverVersion option above,
|
|
317
|
+
// but tools like pg_restore also need encoding, datestyle, etc.
|
|
318
|
+
onAuthenticated() {
|
|
319
|
+
for (const [name, value] of SERVER_PARAMS) {
|
|
320
|
+
socket.write(buildParameterStatus(name, value));
|
|
321
|
+
}
|
|
322
|
+
},
|
|
323
|
+
async onStartup(state) {
|
|
324
|
+
const params = state.clientParams;
|
|
325
|
+
if (params?.replication === 'database') {
|
|
326
|
+
isReplicationConnection = true;
|
|
327
|
+
}
|
|
328
|
+
dbName = params?.database || 'postgres';
|
|
329
|
+
log.debug.proxy(`connection: db=${dbName} user=${params?.user} replication=${params?.replication || 'none'}`);
|
|
330
|
+
const { db } = getDbContext(dbName);
|
|
331
|
+
await db.waitReady;
|
|
332
|
+
},
|
|
333
|
+
async onMessage(data, state) {
|
|
334
|
+
if (!state.isAuthenticated)
|
|
335
|
+
return;
|
|
336
|
+
// handle replication connections (always go to postgres instance)
|
|
337
|
+
if (isReplicationConnection) {
|
|
338
|
+
if (data[0] === 0x51) {
|
|
339
|
+
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
340
|
+
const len = view.getInt32(1);
|
|
341
|
+
const query = new TextDecoder()
|
|
342
|
+
.decode(data.subarray(5, 1 + len - 1))
|
|
343
|
+
.replace(/\0$/, '');
|
|
344
|
+
log.debug.proxy(`repl query: ${query.slice(0, 200)}`);
|
|
345
|
+
}
|
|
346
|
+
return handleReplicationMessage(data, socket, instances.postgres, mutexes.postgres, connection);
|
|
347
|
+
}
|
|
348
|
+
// check for no-op queries
|
|
349
|
+
if (isNoopQuery(data)) {
|
|
350
|
+
if (data[0] === 0x51) {
|
|
351
|
+
return buildSetCompleteResponse();
|
|
352
|
+
}
|
|
353
|
+
else if (data[0] === 0x50) {
|
|
354
|
+
return buildParseCompleteResponse();
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
// intercept and rewrite queries
|
|
358
|
+
data = interceptQuery(data);
|
|
359
|
+
// message-level locking on the connection's pglite instance
|
|
360
|
+
const { db, mutex } = getDbContext(dbName);
|
|
361
|
+
await mutex.acquire();
|
|
362
|
+
let result;
|
|
363
|
+
try {
|
|
364
|
+
result = await db.execProtocolRaw(data, {
|
|
365
|
+
throwOnError: false,
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
catch (err) {
|
|
369
|
+
mutex.release();
|
|
370
|
+
throw err;
|
|
371
|
+
}
|
|
372
|
+
// strip ReadyForQuery from non-Sync/non-SimpleQuery responses
|
|
373
|
+
if (data[0] !== 0x53 && data[0] !== 0x51) {
|
|
374
|
+
result = stripReadyForQuery(result);
|
|
375
|
+
}
|
|
376
|
+
mutex.release();
|
|
377
|
+
return result;
|
|
378
|
+
},
|
|
625
379
|
});
|
|
626
|
-
// enter message processing loop
|
|
627
|
-
const { db: msgDb, mutex: msgMutex } = getDbContext(dbName);
|
|
628
|
-
await messageLoop(socket, msgDb, msgMutex, isReplicationConnection, instances.postgres, mutexes.postgres);
|
|
629
380
|
}
|
|
630
381
|
catch (err) {
|
|
631
|
-
const msg = err?.message || err;
|
|
632
|
-
// suppress expected errors (client disconnected, auth failures)
|
|
633
|
-
if (msg !== 'auth failed' && msg !== 'socket closed') {
|
|
634
|
-
log.debug.proxy(`connection error: ${msg}`);
|
|
635
|
-
}
|
|
636
382
|
if (!socket.destroyed) {
|
|
637
383
|
socket.destroy();
|
|
638
384
|
}
|
|
@@ -646,4 +392,48 @@ export async function startPgProxy(dbInput, config) {
|
|
|
646
392
|
server.on('error', reject);
|
|
647
393
|
});
|
|
648
394
|
}
|
|
395
|
+
async function handleReplicationMessage(data, socket, db, mutex, connection) {
|
|
396
|
+
if (data[0] !== 0x51)
|
|
397
|
+
return undefined;
|
|
398
|
+
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
399
|
+
const len = view.getInt32(1);
|
|
400
|
+
const query = new TextDecoder().decode(data.subarray(5, 1 + len - 1)).replace(/\0$/, '');
|
|
401
|
+
const upper = query.trim().toUpperCase();
|
|
402
|
+
// check if this is a START_REPLICATION command
|
|
403
|
+
if (upper.startsWith('START_REPLICATION')) {
|
|
404
|
+
await connection.detach();
|
|
405
|
+
const writer = {
|
|
406
|
+
write(chunk) {
|
|
407
|
+
if (!socket.destroyed) {
|
|
408
|
+
socket.write(chunk);
|
|
409
|
+
}
|
|
410
|
+
},
|
|
411
|
+
};
|
|
412
|
+
// drain incoming standby status updates
|
|
413
|
+
socket.on('data', (_chunk) => { });
|
|
414
|
+
socket.on('close', () => {
|
|
415
|
+
socket.destroy();
|
|
416
|
+
});
|
|
417
|
+
handleStartReplication(query, writer, db, mutex).catch((err) => {
|
|
418
|
+
log.debug.proxy(`replication stream ended: ${err}`);
|
|
419
|
+
});
|
|
420
|
+
return undefined;
|
|
421
|
+
}
|
|
422
|
+
// handle replication queries + fallthrough to pglite, all under mutex
|
|
423
|
+
await mutex.acquire();
|
|
424
|
+
try {
|
|
425
|
+
const response = await handleReplicationQuery(query, db);
|
|
426
|
+
if (response)
|
|
427
|
+
return response;
|
|
428
|
+
// apply query rewrites before forwarding
|
|
429
|
+
data = interceptQuery(data);
|
|
430
|
+
// fall through to pglite for unrecognized queries
|
|
431
|
+
return await db.execProtocolRaw(data, {
|
|
432
|
+
throwOnError: false,
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
finally {
|
|
436
|
+
mutex.release();
|
|
437
|
+
}
|
|
438
|
+
}
|
|
649
439
|
//# sourceMappingURL=pg-proxy.js.map
|