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.
Files changed (65) hide show
  1. package/dist/admin/http-proxy.d.ts.map +1 -1
  2. package/dist/admin/http-proxy.js.map +1 -1
  3. package/dist/admin/log-store.d.ts.map +1 -1
  4. package/dist/admin/log-store.js.map +1 -1
  5. package/dist/admin/server.d.ts +2 -2
  6. package/dist/admin/server.d.ts.map +1 -1
  7. package/dist/admin/server.js.map +1 -1
  8. package/dist/admin/ui.d.ts.map +1 -1
  9. package/dist/admin/ui.js +2 -2
  10. package/dist/admin/ui.js.map +1 -1
  11. package/dist/cli.d.ts.map +1 -1
  12. package/dist/cli.js +6 -112
  13. package/dist/cli.js.map +1 -1
  14. package/dist/config.d.ts +0 -5
  15. package/dist/config.d.ts.map +1 -1
  16. package/dist/config.js +0 -5
  17. package/dist/config.js.map +1 -1
  18. package/dist/index.d.ts +0 -9
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +91 -249
  21. package/dist/index.js.map +1 -1
  22. package/dist/log.d.ts +0 -9
  23. package/dist/log.d.ts.map +1 -1
  24. package/dist/log.js +1 -24
  25. package/dist/log.js.map +1 -1
  26. package/dist/mutex.d.ts.map +1 -1
  27. package/dist/mutex.js +2 -13
  28. package/dist/mutex.js.map +1 -1
  29. package/dist/pg-proxy.d.ts +2 -3
  30. package/dist/pg-proxy.d.ts.map +1 -1
  31. package/dist/pg-proxy.js +167 -377
  32. package/dist/pg-proxy.js.map +1 -1
  33. package/dist/pglite-manager.d.ts +0 -1
  34. package/dist/pglite-manager.d.ts.map +1 -1
  35. package/dist/pglite-manager.js +1 -1
  36. package/dist/pglite-manager.js.map +1 -1
  37. package/dist/replication/change-tracker.d.ts +0 -6
  38. package/dist/replication/change-tracker.d.ts.map +1 -1
  39. package/dist/replication/change-tracker.js +0 -74
  40. package/dist/replication/change-tracker.js.map +1 -1
  41. package/dist/replication/handler.d.ts.map +1 -1
  42. package/dist/replication/handler.js +5 -47
  43. package/dist/replication/handler.js.map +1 -1
  44. package/dist/vite-plugin.d.ts +0 -3
  45. package/dist/vite-plugin.d.ts.map +1 -1
  46. package/dist/vite-plugin.js +0 -24
  47. package/dist/vite-plugin.js.map +1 -1
  48. package/package.json +5 -4
  49. package/src/admin/http-proxy.ts +5 -1
  50. package/src/admin/log-store.ts +4 -1
  51. package/src/admin/server.ts +7 -3
  52. package/src/admin/ui.ts +682 -680
  53. package/src/cli.ts +6 -111
  54. package/src/config.ts +0 -10
  55. package/src/index.ts +92 -262
  56. package/src/integration/integration.test.ts +264 -133
  57. package/src/log.ts +1 -25
  58. package/src/mutex.ts +2 -12
  59. package/src/pg-proxy.ts +187 -449
  60. package/src/pglite-manager.ts +1 -1
  61. package/src/replication/change-tracker.ts +0 -92
  62. package/src/replication/handler.ts +4 -50
  63. package/src/shim/hooks.mjs +34 -1
  64. package/src/vite-plugin.ts +0 -28
  65. 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,8CAA8C;AAC9C,sEAAsE;AACtE,0EAA0E;AAC1E,0EAA0E;AAC1E,mEAAmE;AACnE,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,wEAAwE;YACxE,4CAA4C;YAC5C,IAAI,OAAO,YAAY,KAAK,WAAW,EAAE,CAAC;gBACxC,YAAY,CAAC,IAAI,CAAC,CAAA;YACpB,CAAC;iBAAM,CAAC;gBACN,UAAU,CAAC,IAAI,EAAE,CAAC,CAAC,CAAA;YACrB,CAAC;QACH,CAAC;aAAM,CAAC;YACN,IAAI,CAAC,MAAM,GAAG,KAAK,CAAA;QACrB,CAAC;IACH,CAAC;CACF"}
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"}
@@ -1,9 +1,8 @@
1
1
  /**
2
2
  * tcp proxy that makes pglite speak postgresql wire protocol.
3
3
  *
4
- * handles the postgresql wire protocol directly using raw tcp sockets,
5
- * avoiding pg-gateway's Duplex.toWeb() which deadlocks under concurrent
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
@@ -1 +1 @@
1
- {"version":3,"file":"pg-proxy.d.ts","sourceRoot":"","sources":["../src/pg-proxy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAgB,KAAK,MAAM,EAAe,MAAM,UAAU,CAAA;AAMjE,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;AAwnBlD,wBAAsB,YAAY,CAChC,OAAO,EAAE,MAAM,GAAG,eAAe,EACjC,MAAM,EAAE,cAAc,GACrB,OAAO,CAAC,MAAM,CAAC,CA0GjB"}
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
- * handles the postgresql wire protocol directly using raw tcp sockets,
5
- * avoiding pg-gateway's Duplex.toWeb() which deadlocks under concurrent
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
- // queries to intercept and return no-op success (synthetic SET response)
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
- function buildAuthOk() {
89
- const buf = new Uint8Array(9);
90
- buf[0] = 0x52; // 'R' AuthenticationOk
91
- new DataView(buf.buffer).setInt32(1, 8);
92
- new DataView(buf.buffer).setInt32(5, 0); // auth ok
93
- return buf;
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
- const connId = ++connCounter;
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
- // perform startup handshake
586
- const { params } = await performHandshake(socket, config);
587
- dbName = params.database || 'postgres';
588
- isReplicationConnection = params.replication === 'database';
589
- // track active connections per database
590
- activeConns[dbName] = (activeConns[dbName] || 0) + 1;
591
- console.info(`[orez-proxy#${connId}] connect db=${dbName} repl=${params.replication || 'none'}`);
592
- const { db } = getDbContext(dbName);
593
- await db.waitReady;
594
- // clean up pglite session state when client disconnects.
595
- // pglite is single-session — all connections share one session.
596
- // only ROLLBACK + reset when this is the LAST connection for this db,
597
- // to avoid killing another connection's active transaction.
598
- socket.on('close', async () => {
599
- activeConns[dbName] = Math.max(0, (activeConns[dbName] || 1) - 1);
600
- const remaining = activeConns[dbName];
601
- const shouldRollback = remaining === 0;
602
- console.info(`[orez-proxy#${connId}] close [${dbName}] (remaining=${remaining}, shouldRollback=${shouldRollback})`);
603
- if (!shouldRollback)
604
- return;
605
- const { db: closeDb, mutex: closeMutex } = getDbContext(dbName);
606
- await closeMutex.acquire();
607
- try {
608
- await closeDb.exec('ROLLBACK');
609
- }
610
- catch {
611
- // no transaction to rollback
612
- }
613
- try {
614
- await closeDb.exec(`SET search_path TO public`);
615
- await closeDb.exec(`RESET statement_timeout`);
616
- await closeDb.exec(`RESET lock_timeout`);
617
- await closeDb.exec(`RESET idle_in_transaction_session_timeout`);
618
- }
619
- catch {
620
- // best-effort reset
621
- }
622
- finally {
623
- closeMutex.release();
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