orez 0.5.9 → 0.5.12

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 (84) hide show
  1. package/dist/browser.js +1 -1
  2. package/dist/browser.js.map +1 -1
  3. package/dist/cf-do/tx-journal.d.ts +6 -5
  4. package/dist/cf-do/tx-journal.d.ts.map +1 -1
  5. package/dist/cf-do/tx-journal.js +116 -27
  6. package/dist/cf-do/tx-journal.js.map +1 -1
  7. package/dist/pg-proxy-browser.d.ts +3 -1
  8. package/dist/pg-proxy-browser.d.ts.map +1 -1
  9. package/dist/pg-proxy-browser.js +240 -210
  10. package/dist/pg-proxy-browser.js.map +1 -1
  11. package/dist/pg-proxy-do-backend.d.ts +8 -0
  12. package/dist/pg-proxy-do-backend.d.ts.map +1 -1
  13. package/dist/pg-proxy-do-backend.js +27 -9
  14. package/dist/pg-proxy-do-backend.js.map +1 -1
  15. package/dist/pg-sqlite-compiler/compiler.d.ts +17 -0
  16. package/dist/pg-sqlite-compiler/compiler.d.ts.map +1 -0
  17. package/dist/pg-sqlite-compiler/compiler.js +80 -0
  18. package/dist/pg-sqlite-compiler/compiler.js.map +1 -0
  19. package/dist/pg-sqlite-compiler/index.d.ts +1 -16
  20. package/dist/pg-sqlite-compiler/index.d.ts.map +1 -1
  21. package/dist/pg-sqlite-compiler/index.js +2 -79
  22. package/dist/pg-sqlite-compiler/index.js.map +1 -1
  23. package/dist/replication/handler.d.ts +8 -7
  24. package/dist/replication/handler.d.ts.map +1 -1
  25. package/dist/replication/handler.js +147 -116
  26. package/dist/replication/handler.js.map +1 -1
  27. package/dist/worker/browser-build-config.d.ts.map +1 -1
  28. package/dist/worker/browser-build-config.js +1 -0
  29. package/dist/worker/browser-build-config.js.map +1 -1
  30. package/dist/worker/browser-embed.d.ts +3 -2
  31. package/dist/worker/browser-embed.d.ts.map +1 -1
  32. package/dist/worker/browser-embed.js +187 -150
  33. package/dist/worker/browser-embed.js.map +1 -1
  34. package/dist/worker/cf-instance-runtime.d.ts +62 -0
  35. package/dist/worker/cf-instance-runtime.d.ts.map +1 -0
  36. package/dist/worker/cf-instance-runtime.js +260 -0
  37. package/dist/worker/cf-instance-runtime.js.map +1 -0
  38. package/dist/worker/cf-patches.d.ts +9 -6
  39. package/dist/worker/cf-patches.d.ts.map +1 -1
  40. package/dist/worker/cf-patches.js +117 -60
  41. package/dist/worker/cf-patches.js.map +1 -1
  42. package/dist/worker/durable-object-websocket-handoff.d.ts +2 -0
  43. package/dist/worker/durable-object-websocket-handoff.d.ts.map +1 -1
  44. package/dist/worker/durable-object-websocket-handoff.js +22 -41
  45. package/dist/worker/durable-object-websocket-handoff.js.map +1 -1
  46. package/dist/worker/embed-generation.d.ts +9 -6
  47. package/dist/worker/embed-generation.d.ts.map +1 -1
  48. package/dist/worker/embed-generation.js +27 -6
  49. package/dist/worker/embed-generation.js.map +1 -1
  50. package/dist/worker/shims/fastify-websocket.d.ts +3 -0
  51. package/dist/worker/shims/fastify-websocket.d.ts.map +1 -0
  52. package/dist/worker/shims/fastify-websocket.js +3 -0
  53. package/dist/worker/shims/fastify-websocket.js.map +1 -0
  54. package/dist/worker/shims/fastify.d.ts +2 -10
  55. package/dist/worker/shims/fastify.d.ts.map +1 -1
  56. package/dist/worker/shims/fastify.js +14 -21
  57. package/dist/worker/shims/fastify.js.map +1 -1
  58. package/dist/worker/shims/postgres-browser.d.ts +1 -1
  59. package/dist/worker/shims/postgres-browser.d.ts.map +1 -1
  60. package/dist/worker/shims/postgres-browser.js +29 -20
  61. package/dist/worker/shims/postgres-browser.js.map +1 -1
  62. package/dist/worker/shims/sqlite-browser.d.ts +2 -3
  63. package/dist/worker/shims/sqlite-browser.d.ts.map +1 -1
  64. package/dist/worker/shims/sqlite-browser.js +2 -3
  65. package/dist/worker/shims/sqlite-browser.js.map +1 -1
  66. package/dist/worker/shims/sqlite.d.ts.map +1 -1
  67. package/dist/worker/shims/sqlite.js +56 -35
  68. package/dist/worker/shims/sqlite.js.map +1 -1
  69. package/dist/worker/shims/ws.d.ts.map +1 -1
  70. package/dist/worker/shims/ws.js +54 -39
  71. package/dist/worker/shims/ws.js.map +1 -1
  72. package/dist/worker/shims/zero-process-env.d.ts +1 -1
  73. package/dist/worker/shims/zero-process-env.d.ts.map +1 -1
  74. package/dist/worker/shims/zero-process-env.js +79 -8
  75. package/dist/worker/shims/zero-process-env.js.map +1 -1
  76. package/dist/worker/zero-cache-embed-cf.d.ts +5 -2
  77. package/dist/worker/zero-cache-embed-cf.d.ts.map +1 -1
  78. package/dist/worker/zero-cache-embed-cf.js +339 -256
  79. package/dist/worker/zero-cache-embed-cf.js.map +1 -1
  80. package/dist/worker/zero-cache-run-worker.d.ts +2 -0
  81. package/dist/worker/zero-cache-run-worker.d.ts.map +1 -0
  82. package/dist/worker/zero-cache-run-worker.js +3 -0
  83. package/dist/worker/zero-cache-run-worker.js.map +1 -0
  84. package/package.json +6 -2
@@ -14,29 +14,10 @@
14
14
  import { PostgresConnection } from 'pg-gateway';
15
15
  import { log } from './log.js';
16
16
  import { Mutex } from './mutex.js';
17
- import { createReplicationFeedbackParser, handleReplicationQuery, handleStartReplication, signalReplicationChange, } from './replication/handler.js';
17
+ import { createReplicationFeedbackParser, handleReplicationQuery, handleStartReplication, noteConfirmedFlushLsn, signalReplicationChange, } from './replication/handler.js';
18
18
  // shared encoder/decoder instances
19
19
  const textEncoder = new TextEncoder();
20
20
  const textDecoder = new TextDecoder();
21
- // verbose wire-protocol logging. off by default — every postgres message hits
22
- // the hot path, so unconditional console.debug calls are surprisingly expensive
23
- // even when devtools is set to hide them (the interpolated strings still get
24
- // built, and devtools retains them).
25
- //
26
- // re-evaluated on every call so you can flip it at runtime from any iframe,
27
- // worker, or console without restarting. toggle via either:
28
- // - env: OREZ_DEBUG_WIRE=1 (picked up at process start)
29
- // - global: globalThis.__OREZ_DEBUG_WIRE__ = true (works anywhere, anytime)
30
- function isDebugWire() {
31
- try {
32
- if (globalThis.__OREZ_DEBUG_WIRE__ === true)
33
- return true;
34
- if (typeof process !== 'undefined' && process.env?.OREZ_DEBUG_WIRE)
35
- return true;
36
- }
37
- catch { }
38
- return false;
39
- }
40
21
  // postgres frontend/backend message type bytes — only used for debug-wire logging
41
22
  const PG_MSG_TYPE_NAMES = {
42
23
  0x50: 'Parse',
@@ -109,8 +90,6 @@ function extractQueryText(data) {
109
90
  }
110
91
  return null;
111
92
  }
112
- // abort previous replication handler when a new one starts
113
- let abortPreviousReplication = null;
114
93
  // clean version string: strip emscripten compiler info that breaks pg_restore/pg_dump
115
94
  const PG_VERSION_STRING = "'PostgreSQL 17.4 on x86_64-pc-linux-gnu, compiled by gcc (GCC) 12.2.0, 64-bit'";
116
95
  // query rewrites: make pglite look like real postgres with logical replication
@@ -552,7 +531,6 @@ function stripResponseMessages(data, stripRfq) {
552
531
  * readable receives Uint8Array messages from the port.
553
532
  * writable sends Uint8Array messages via the port.
554
533
  */
555
- let _globalWriteCount = 0;
556
534
  function messagePortToDuplexWithInject(port) {
557
535
  let readController;
558
536
  let msgCount = 0;
@@ -593,52 +571,6 @@ function messagePortToDuplexWithInject(port) {
593
571
  };
594
572
  return { duplex: { readable, writable }, rawWrite, injectMessage };
595
573
  }
596
- function messagePortToDuplex(port) {
597
- let msgCount = 0;
598
- const readable = new ReadableStream({
599
- start(controller) {
600
- port.onmessage = (ev) => {
601
- msgCount++;
602
- if (isDebugWire() && msgCount <= 3) {
603
- console.debug(`[pg-proxy-duplex] msg#${msgCount} type=${typeof ev.data} isAB=${ev.data instanceof ArrayBuffer} isU8=${ev.data instanceof Uint8Array} len=${ev.data?.byteLength ?? ev.data?.length ?? '?'}`);
604
- }
605
- if (ev.data instanceof ArrayBuffer) {
606
- controller.enqueue(new Uint8Array(ev.data));
607
- }
608
- else if (ev.data instanceof Uint8Array) {
609
- controller.enqueue(ev.data);
610
- }
611
- else {
612
- console.warn(`[pg-proxy-duplex] unexpected data type:`, typeof ev.data, ev.data);
613
- }
614
- };
615
- },
616
- cancel() {
617
- port.close();
618
- },
619
- });
620
- const writable = new WritableStream({
621
- write(chunk) {
622
- _globalWriteCount++;
623
- if (isDebugWire() && _globalWriteCount <= 200) {
624
- console.debug(`[pg-proxy-ws-write] #${_globalWriteCount} len=${chunk.byteLength}`);
625
- }
626
- // CF workerd MessagePort does not support transfer lists.
627
- const buf = chunk.buffer.slice(chunk.byteOffset, chunk.byteOffset + chunk.byteLength);
628
- port.postMessage(buf);
629
- },
630
- close() {
631
- port.close();
632
- },
633
- });
634
- // raw write function for injecting data outside of pg-gateway's stream
635
- // (e.g. parameter status messages during onAuthenticated)
636
- const rawWrite = (data) => {
637
- const buf = data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
638
- port.postMessage(buf);
639
- };
640
- return { duplex: { readable, writable }, rawWrite };
641
- }
642
574
  function hasProtocolSessionFactory(db) {
643
575
  return (typeof db.createProtocolSession === 'function');
644
576
  }
@@ -660,6 +592,8 @@ function createConnectionContext(ctx) {
660
592
  };
661
593
  }
662
594
  export async function createBrowserProxy(dbInput, config) {
595
+ const debugWire = config.debugWire === true;
596
+ const isDebugWire = () => debugWire;
663
597
  // normalize input: single PGlite instance = use it for all databases (backwards compat for tests)
664
598
  const instances = 'postgres' in dbInput
665
599
  ? dbInput
@@ -767,16 +701,39 @@ export async function createBrowserProxy(dbInput, config) {
767
701
  signalPending = true;
768
702
  queueMicrotask(() => {
769
703
  signalPending = false;
770
- signalReplicationChange();
704
+ signalReplicationChange(config.instanceId);
771
705
  });
772
706
  }
773
707
  let closed = false;
708
+ let closePromise = null;
774
709
  const activeConnectionClosers = new Set();
710
+ const unreportedConnectionCleanupErrors = new Set();
711
+ let abortPreviousReplication = null;
712
+ const activeReplicationTasks = new Set();
713
+ const replicationRuntime = {
714
+ replace(abort) {
715
+ abortPreviousReplication?.();
716
+ abortPreviousReplication = abort;
717
+ },
718
+ track(promise, abort, onError) {
719
+ let tracked;
720
+ tracked = promise.catch(onError).finally(() => {
721
+ activeReplicationTasks.delete(tracked);
722
+ if (abortPreviousReplication === abort)
723
+ abortPreviousReplication = null;
724
+ });
725
+ activeReplicationTasks.add(tracked);
726
+ },
727
+ };
775
728
  function handleConnection(port) {
776
729
  if (closed) {
777
730
  port.close();
778
731
  return;
779
732
  }
733
+ const closePendingConnection = async () => {
734
+ port.close();
735
+ };
736
+ activeConnectionClosers.add(closePendingConnection);
780
737
  port.start();
781
738
  // peek at the first message to detect replication connections.
782
739
  // replication connections bypass pg-gateway entirely and are handled
@@ -786,6 +743,10 @@ export async function createBrowserProxy(dbInput, config) {
786
743
  const buffered = [];
787
744
  let handlerInstalled = false;
788
745
  port.onmessage = (ev) => {
746
+ if (closed) {
747
+ port.close();
748
+ return;
749
+ }
789
750
  if (handlerInstalled)
790
751
  return; // shouldn't happen — handler replaced port.onmessage
791
752
  buffered.push(ev);
@@ -795,6 +756,7 @@ export async function createBrowserProxy(dbInput, config) {
795
756
  if (!(data instanceof Uint8Array) || data.length < 8) {
796
757
  handlerInstalled = true;
797
758
  handleRegularConnection(port, ev);
759
+ activeConnectionClosers.delete(closePendingConnection);
798
760
  // flush buffered messages to the new handler
799
761
  for (let i = 1; i < buffered.length; i++) {
800
762
  port.onmessage?.(buffered[i]);
@@ -813,6 +775,7 @@ export async function createBrowserProxy(dbInput, config) {
813
775
  // in browser Web Workers (same root cause as patches #9, #18, #20).
814
776
  handleRawConnection(port, data, params, getDbContext(dbName), isRepl);
815
777
  handlerInstalled = true;
778
+ activeConnectionClosers.delete(closePendingConnection);
816
779
  // flush any messages that arrived while we were processing the startup
817
780
  for (let i = 1; i < buffered.length; i++) {
818
781
  if (port.onmessage) {
@@ -855,43 +818,53 @@ export async function createBrowserProxy(dbInput, config) {
855
818
  const connId = {};
856
819
  const dbName = params.database || 'postgres';
857
820
  let connClosed = false;
858
- let sessionClosed = false;
859
- const closeSession = async () => {
860
- if (sessionClosed)
861
- return;
862
- sessionClosed = true;
863
- activeConnectionClosers.delete(closeSession);
864
- // a connection torn down mid-pipeline still OWNS the db mutex. a dead
865
- // pipeline can never send its Sync, so without this release the mutex
866
- // is leaked forever and every later connection on this db (including
867
- // the next embed generation after an idle-hibernation teardown) blocks
868
- // in mutex.acquire() before its first statement. when a statement is
869
- // still executing, the pipeline continuation performs this cleanup
870
- // after it settles instead (pipelineExecInFlight).
871
- if (pipelineMutexHeld && !pipelineExecInFlight) {
872
- pipelineMutexHeld = false;
873
- try {
874
- await rollbackOwnTransaction();
821
+ let closeSessionPromise = null;
822
+ const closeSession = () => {
823
+ if (closeSessionPromise)
824
+ return closeSessionPromise;
825
+ connClosed = true;
826
+ closeSessionPromise = (async () => {
827
+ port.close();
828
+ // a connection torn down mid-pipeline still OWNS the db mutex. a dead
829
+ // pipeline can never send its Sync, so without this release the mutex
830
+ // is leaked forever and every later connection on this db (including
831
+ // the next embed generation after an idle-hibernation teardown) blocks
832
+ // in mutex.acquire() before its first statement. when a statement is
833
+ // still executing, the pipeline continuation performs this cleanup
834
+ // after it settles instead (pipelineExecInFlight).
835
+ if (pipelineExecInFlight)
836
+ await pipelineIdlePromise;
837
+ if (pipelineMutexHeld) {
838
+ pipelineMutexHeld = false;
839
+ try {
840
+ await rollbackOwnTransaction();
841
+ }
842
+ finally {
843
+ mutex.release();
844
+ }
875
845
  }
876
- finally {
877
- mutex.release();
846
+ else {
847
+ await mutex.acquire();
848
+ try {
849
+ await rollbackOwnTransaction();
850
+ }
851
+ finally {
852
+ mutex.release();
853
+ }
878
854
  }
879
- }
880
- try {
881
855
  await scopedContext.close();
882
- }
883
- catch { }
856
+ })();
857
+ void closeSessionPromise.then(() => activeConnectionClosers.delete(closeSession), (error) => {
858
+ activeConnectionClosers.delete(closeSession);
859
+ unreportedConnectionCleanupErrors.add(error);
860
+ });
861
+ return closeSessionPromise;
884
862
  };
885
863
  activeConnectionClosers.add(closeSession);
886
864
  // roll back this connection's open transaction. caller must hold the mutex.
887
865
  const rollbackOwnTransaction = async () => {
888
866
  if (txState.owner === connId && txState.status !== 0x49) {
889
- try {
890
- await db.exec('ROLLBACK');
891
- }
892
- catch {
893
- // db is closed or rollback failed — ignore
894
- }
867
+ await db.exec('ROLLBACK');
895
868
  txState.status = 0x49;
896
869
  txState.owner = null;
897
870
  }
@@ -959,6 +932,18 @@ export async function createBrowserProxy(dbInput, config) {
959
932
  // statement currently executing inside the held pipeline — closeSession
960
933
  // must not release the mutex under it (see closeSession above)
961
934
  let pipelineExecInFlight = false;
935
+ let resolvePipelineIdle = () => { };
936
+ let pipelineIdlePromise = Promise.resolve();
937
+ const beginPipelineExec = () => {
938
+ pipelineExecInFlight = true;
939
+ pipelineIdlePromise = new Promise((resolve) => {
940
+ resolvePipelineIdle = resolve;
941
+ });
942
+ };
943
+ const endPipelineExec = () => {
944
+ pipelineExecInFlight = false;
945
+ resolvePipelineIdle();
946
+ };
962
947
  let extWritePending = false;
963
948
  let pipelineBuffer = [];
964
949
  // remember the most recent Parse / SimpleQuery for diagnostic logging when
@@ -1029,9 +1014,15 @@ export async function createBrowserProxy(dbInput, config) {
1029
1014
  .replace(/\0$/, '');
1030
1015
  const upper = query.trim().toUpperCase();
1031
1016
  if (upper.startsWith('START_REPLICATION')) {
1032
- if (abortPreviousReplication)
1033
- abortPreviousReplication();
1034
1017
  let aborted = false;
1018
+ const abort = () => {
1019
+ if (aborted)
1020
+ return;
1021
+ aborted = true;
1022
+ connClosed = true;
1023
+ port.close();
1024
+ void closeSession();
1025
+ };
1035
1026
  const writer = {
1036
1027
  write(chunk) {
1037
1028
  if (!connClosed && !aborted) {
@@ -1047,20 +1038,15 @@ export async function createBrowserProxy(dbInput, config) {
1047
1038
  return connClosed || aborted;
1048
1039
  },
1049
1040
  close() {
1050
- abortPreviousReplication?.();
1041
+ abort();
1051
1042
  },
1052
1043
  };
1053
- abortPreviousReplication = () => {
1054
- aborted = true;
1055
- connClosed = true;
1056
- port.close();
1057
- void closeSession();
1058
- };
1044
+ replicationRuntime.replace(abort);
1059
1045
  // client→server traffic on a replication connection carries
1060
1046
  // standby status updates: the consumer's confirmed-flush LSN
1061
1047
  // drives purging of _zero_changes (rows are retained until the
1062
1048
  // consumer durably commits them, like real pg WAL retention).
1063
- const feedbackParser = createReplicationFeedbackParser();
1049
+ const feedbackParser = createReplicationFeedbackParser((lsn) => noteConfirmedFlushLsn(lsn, config.instanceId));
1064
1050
  port.onmessage = (ev) => {
1065
1051
  const incoming = ev.data instanceof ArrayBuffer
1066
1052
  ? new Uint8Array(ev.data)
@@ -1068,7 +1054,7 @@ export async function createBrowserProxy(dbInput, config) {
1068
1054
  if (incoming instanceof Uint8Array)
1069
1055
  feedbackParser(incoming);
1070
1056
  };
1071
- handleStartReplication(query, writer, db, transactionAwareMutex(db, mutex, txState, null)).catch(() => { });
1057
+ replicationRuntime.track(handleStartReplication(query, writer, db, transactionAwareMutex(db, mutex, txState, null), config.instanceId), abort, () => { });
1072
1058
  return;
1073
1059
  }
1074
1060
  // replication queries (IDENTIFY_SYSTEM, SET ..., CREATE/DROP SLOT).
@@ -1086,7 +1072,7 @@ export async function createBrowserProxy(dbInput, config) {
1086
1072
  // open tx and commits with it.
1087
1073
  await mutex.acquire();
1088
1074
  try {
1089
- const response = await handleReplicationQuery(query, db);
1075
+ const response = await handleReplicationQuery(query, db, config.instanceId);
1090
1076
  if (response) {
1091
1077
  write(response);
1092
1078
  return;
@@ -1163,7 +1149,7 @@ export async function createBrowserProxy(dbInput, config) {
1163
1149
  const combined = concatUint8Arrays(pipelineBuffer);
1164
1150
  pipelineBuffer = [];
1165
1151
  let flushResult;
1166
- pipelineExecInFlight = true;
1152
+ beginPipelineExec();
1167
1153
  try {
1168
1154
  flushResult = await db.execProtocolRaw(combined, { syncToFs: false });
1169
1155
  }
@@ -1174,19 +1160,7 @@ export async function createBrowserProxy(dbInput, config) {
1174
1160
  return;
1175
1161
  }
1176
1162
  finally {
1177
- pipelineExecInFlight = false;
1178
- }
1179
- if (sessionClosed && pipelineMutexHeld) {
1180
- // session torn down while this statement was executing —
1181
- // closeSession skipped its cleanup, finish it here
1182
- pipelineMutexHeld = false;
1183
- try {
1184
- await rollbackOwnTransaction();
1185
- }
1186
- finally {
1187
- mutex.release();
1188
- }
1189
- return;
1163
+ endPipelineExec();
1190
1164
  }
1191
1165
  flushResult = stripResponseMessages(flushResult, true);
1192
1166
  write(flushResult);
@@ -1199,7 +1173,7 @@ export async function createBrowserProxy(dbInput, config) {
1199
1173
  pipelineBuffer = [];
1200
1174
  let result;
1201
1175
  const t0 = performance.now();
1202
- pipelineExecInFlight = true;
1176
+ beginPipelineExec();
1203
1177
  try {
1204
1178
  result = await db.execProtocolRaw(combined, { syncToFs: false });
1205
1179
  }
@@ -1210,7 +1184,7 @@ export async function createBrowserProxy(dbInput, config) {
1210
1184
  return;
1211
1185
  }
1212
1186
  finally {
1213
- pipelineExecInFlight = false;
1187
+ endPipelineExec();
1214
1188
  }
1215
1189
  const dt = performance.now() - t0;
1216
1190
  if (isDebugWire() && dt > 100)
@@ -1290,8 +1264,9 @@ export async function createBrowserProxy(dbInput, config) {
1290
1264
  // signal writes
1291
1265
  if (dbName === 'postgres' && msgType === 0x51) {
1292
1266
  const qn = extractQueryText(data)?.trimStart().toLowerCase();
1293
- if (qn && isWriteNormalized(qn))
1294
- signalReplicationChange();
1267
+ if (qn && isWriteNormalized(qn)) {
1268
+ signalReplicationChange(config.instanceId);
1269
+ }
1295
1270
  }
1296
1271
  write(result);
1297
1272
  }
@@ -1327,17 +1302,25 @@ export async function createBrowserProxy(dbInput, config) {
1327
1302
  // not release the mutex under it; the pipeline continuation finishes the
1328
1303
  // cleanup after the statement settles.
1329
1304
  let pipelineExecInFlight = false;
1305
+ let resolvePipelineIdle = () => { };
1306
+ let pipelineIdlePromise = Promise.resolve();
1307
+ const beginPipelineExec = () => {
1308
+ pipelineExecInFlight = true;
1309
+ pipelineIdlePromise = new Promise((resolve) => {
1310
+ resolvePipelineIdle = resolve;
1311
+ });
1312
+ };
1313
+ const endPipelineExec = () => {
1314
+ pipelineExecInFlight = false;
1315
+ resolvePipelineIdle();
1316
+ };
1330
1317
  // connection closed flag
1331
1318
  let connClosed = false;
1319
+ let cleanupPromise = null;
1332
1320
  // roll back this connection's open transaction. caller must hold the mutex.
1333
1321
  const rollbackOwnTransaction = async (db, txState) => {
1334
1322
  if (txState.owner === connId && txState.status !== 0x49) {
1335
- try {
1336
- await db.exec('ROLLBACK');
1337
- }
1338
- catch {
1339
- // db is closed or rollback failed — ignore
1340
- }
1323
+ await db.exec('ROLLBACK');
1341
1324
  txState.status = 0x49;
1342
1325
  txState.owner = null;
1343
1326
  }
@@ -1350,32 +1333,30 @@ export async function createBrowserProxy(dbInput, config) {
1350
1333
  // pollution that 2eb1873 fixed on the node side. browser cleanup was
1351
1334
  // missed and showed up as "current transaction is aborted, commands
1352
1335
  // ignored" when a foreign connection closed mid-someone-else's-txn.
1353
- const cleanup = async () => {
1354
- if (connClosed)
1355
- return;
1336
+ const cleanup = () => {
1337
+ if (cleanupPromise)
1338
+ return cleanupPromise;
1356
1339
  connClosed = true;
1357
- activeConnectionClosers.delete(cleanup);
1358
- // replication connections don't own a transaction — skip ROLLBACK
1359
- if (isReplicationConnection)
1360
- return;
1361
- try {
1340
+ cleanupPromise = (async () => {
1341
+ port.close();
1342
+ // replication connections don't own a transaction — skip ROLLBACK
1343
+ if (isReplicationConnection)
1344
+ return;
1362
1345
  const { db, mutex, txState } = getDbContext(dbName);
1346
+ if (pipelineExecInFlight)
1347
+ await pipelineIdlePromise;
1363
1348
  if (pipelineMutexHeld) {
1364
1349
  // this connection died mid-pipeline and still OWNS the mutex. a
1365
1350
  // dead pipeline can never send its Sync, so without this release
1366
1351
  // the mutex is leaked forever and every later connection on this
1367
1352
  // db (including the next embed generation after idle-hibernation
1368
1353
  // teardown) blocks in mutex.acquire() before its first statement.
1369
- // when a statement is in flight, the pipeline continuation does
1370
- // this exact cleanup after it settles.
1371
- if (!pipelineExecInFlight) {
1372
- pipelineMutexHeld = false;
1373
- try {
1374
- await rollbackOwnTransaction(db, txState);
1375
- }
1376
- finally {
1377
- mutex.release();
1378
- }
1354
+ pipelineMutexHeld = false;
1355
+ try {
1356
+ await rollbackOwnTransaction(db, txState);
1357
+ }
1358
+ finally {
1359
+ mutex.release();
1379
1360
  }
1380
1361
  return;
1381
1362
  }
@@ -1386,10 +1367,12 @@ export async function createBrowserProxy(dbInput, config) {
1386
1367
  finally {
1387
1368
  mutex.release();
1388
1369
  }
1389
- }
1390
- catch {
1391
- // instance may have been replaced during reset, ignore
1392
- }
1370
+ })();
1371
+ void cleanupPromise.then(() => activeConnectionClosers.delete(cleanup), (error) => {
1372
+ activeConnectionClosers.delete(cleanup);
1373
+ unreportedConnectionCleanupErrors.add(error);
1374
+ });
1375
+ return cleanupPromise;
1393
1376
  };
1394
1377
  activeConnectionClosers.add(cleanup);
1395
1378
  try {
@@ -1452,7 +1435,7 @@ export async function createBrowserProxy(dbInput, config) {
1452
1435
  return handleReplicationMessageBrowser(data, rawWrite, () => connClosed, () => {
1453
1436
  connClosed = true;
1454
1437
  port.close();
1455
- }, instances.postgres, mutexes.postgres, connection);
1438
+ }, instances.postgres, mutexes.postgres, connection, config.instanceId, replicationRuntime, debugWire);
1456
1439
  }
1457
1440
  const msgType = data[0];
1458
1441
  const { db, mutex, txState } = getDbContext(dbName);
@@ -1486,7 +1469,7 @@ export async function createBrowserProxy(dbInput, config) {
1486
1469
  data = interceptQuery(data);
1487
1470
  const t1 = performance.now();
1488
1471
  let result;
1489
- pipelineExecInFlight = true;
1472
+ beginPipelineExec();
1490
1473
  try {
1491
1474
  result = await db.execProtocolRaw(data, { syncToFs: false });
1492
1475
  }
@@ -1496,7 +1479,7 @@ export async function createBrowserProxy(dbInput, config) {
1496
1479
  throw err;
1497
1480
  }
1498
1481
  finally {
1499
- pipelineExecInFlight = false;
1482
+ endPipelineExec();
1500
1483
  }
1501
1484
  const t2 = performance.now();
1502
1485
  proxyStats.totalExecMs += t2 - t1;
@@ -1518,18 +1501,6 @@ export async function createBrowserProxy(dbInput, config) {
1518
1501
  signalWrite();
1519
1502
  }
1520
1503
  }
1521
- else if (connClosed) {
1522
- // the connection died while this statement was executing, so
1523
- // cleanup skipped its release (pipelineExecInFlight). no Sync
1524
- // can ever arrive — roll back and release here instead.
1525
- pipelineMutexHeld = false;
1526
- try {
1527
- await rollbackOwnTransaction(db, txState);
1528
- }
1529
- finally {
1530
- mutex.release();
1531
- }
1532
- }
1533
1504
  else {
1534
1505
  // strip ReadyForQuery from non-Sync pipeline messages
1535
1506
  // result = stripResponseMessages(result, true) // disabled for debugging
@@ -1627,7 +1598,7 @@ export async function createBrowserProxy(dbInput, config) {
1627
1598
  result = stripResponseMessages(result, stripRfq);
1628
1599
  // signal replication handler on postgres writes for instant sync
1629
1600
  if (dbName === 'postgres' && queryNorm && isWriteNormalized(queryNorm)) {
1630
- signalReplicationChange();
1601
+ signalReplicationChange(config.instanceId);
1631
1602
  }
1632
1603
  return result;
1633
1604
  },
@@ -1707,11 +1678,59 @@ export async function createBrowserProxy(dbInput, config) {
1707
1678
  txState.owner = owner;
1708
1679
  }
1709
1680
  const defaultExternalOwner = {};
1681
+ const closedExternalOwners = new WeakSet();
1682
+ function createExternalOwnerCloser(owner, dbNames) {
1683
+ let closing = null;
1684
+ const closeOwner = () => {
1685
+ if (closing)
1686
+ return closing;
1687
+ closedExternalOwners.add(owner);
1688
+ closing = (async () => {
1689
+ const contexts = new Map();
1690
+ for (const dbName of dbNames) {
1691
+ const context = getDbContext(dbName);
1692
+ contexts.set(context.txState, context);
1693
+ }
1694
+ const results = await Promise.allSettled([...contexts.values()].map(async ({ db, mutex, txState }) => {
1695
+ await mutex.acquire();
1696
+ try {
1697
+ if (txState.owner === owner && txState.status !== 0x49) {
1698
+ await db.exec('ROLLBACK');
1699
+ txState.status = 0x49;
1700
+ txState.owner = null;
1701
+ }
1702
+ }
1703
+ finally {
1704
+ mutex.release();
1705
+ }
1706
+ }));
1707
+ const errors = results.flatMap((result) => result.status === 'rejected' ? [result.reason] : []);
1708
+ if (errors.length === 1)
1709
+ throw errors[0];
1710
+ if (errors.length > 1) {
1711
+ throw new AggregateError(errors, 'browser proxy external cleanup failed');
1712
+ }
1713
+ })();
1714
+ void closing.then(() => activeConnectionClosers.delete(closeOwner), (error) => {
1715
+ activeConnectionClosers.delete(closeOwner);
1716
+ unreportedConnectionCleanupErrors.add(error);
1717
+ });
1718
+ return closing;
1719
+ };
1720
+ activeConnectionClosers.add(closeOwner);
1721
+ return closeOwner;
1722
+ }
1723
+ createExternalOwnerCloser(defaultExternalOwner, ['postgres', 'zero_cvr', 'zero_cdb']);
1710
1724
  async function externalQuery(dbName, sql, params, owner = defaultExternalOwner) {
1711
- if (closed)
1725
+ if (closed || closedExternalOwners.has(owner)) {
1712
1726
  throw new Error('pg-proxy is closed');
1727
+ }
1713
1728
  const { db, mutex, txState } = getDbContext(dbName);
1714
1729
  await acquireForOwner(db, mutex, txState, owner);
1730
+ if (closed || closedExternalOwners.has(owner)) {
1731
+ mutex.release();
1732
+ throw new Error('pg-proxy is closed');
1733
+ }
1715
1734
  try {
1716
1735
  const result = await db.query(sql, params);
1717
1736
  updateExternalTxStateAfterSuccess(txState, owner, sql);
@@ -1730,10 +1749,15 @@ export async function createBrowserProxy(dbInput, config) {
1730
1749
  }
1731
1750
  }
1732
1751
  async function externalExec(dbName, sql, owner = defaultExternalOwner) {
1733
- if (closed)
1752
+ if (closed || closedExternalOwners.has(owner)) {
1734
1753
  throw new Error('pg-proxy is closed');
1754
+ }
1735
1755
  const { db, mutex, txState } = getDbContext(dbName);
1736
1756
  await acquireForOwner(db, mutex, txState, owner);
1757
+ if (closed || closedExternalOwners.has(owner)) {
1758
+ mutex.release();
1759
+ throw new Error('pg-proxy is closed');
1760
+ }
1737
1761
  try {
1738
1762
  const result = await db.exec(sql);
1739
1763
  updateExternalTxStateAfterSuccess(txState, owner, sql);
@@ -1752,59 +1776,68 @@ export async function createBrowserProxy(dbInput, config) {
1752
1776
  }
1753
1777
  function createExternalSession(dbName = 'postgres') {
1754
1778
  const owner = {};
1779
+ let sessionClosed = false;
1780
+ const closeOwner = createExternalOwnerCloser(owner, [dbName]);
1755
1781
  return {
1756
1782
  query(sql, params) {
1783
+ if (sessionClosed)
1784
+ return Promise.reject(new Error('pg-proxy session is closed'));
1757
1785
  return externalQuery(dbName, sql, params, owner);
1758
1786
  },
1759
1787
  exec(sql) {
1788
+ if (sessionClosed)
1789
+ return Promise.reject(new Error('pg-proxy session is closed'));
1760
1790
  return externalExec(dbName, sql, owner);
1761
1791
  },
1762
- async close() {
1763
- const { db, mutex, txState } = getDbContext(dbName);
1764
- await mutex.acquire();
1765
- try {
1766
- if (txState.owner === owner && txState.status !== 0x49) {
1767
- try {
1768
- await db.exec('ROLLBACK');
1769
- }
1770
- catch { }
1771
- txState.status = 0x49;
1772
- txState.owner = null;
1773
- }
1774
- }
1775
- finally {
1776
- mutex.release();
1777
- }
1792
+ close() {
1793
+ sessionClosed = true;
1794
+ return closeOwner();
1778
1795
  },
1779
1796
  };
1780
1797
  }
1781
1798
  return {
1782
1799
  handleConnection,
1783
1800
  close() {
1801
+ if (closePromise)
1802
+ return closePromise;
1784
1803
  closed = true;
1785
1804
  signalPending = false;
1786
- for (const closeConnection of activeConnectionClosers) {
1787
- void closeConnection();
1788
- }
1805
+ closePromise = (async () => {
1806
+ abortPreviousReplication?.();
1807
+ abortPreviousReplication = null;
1808
+ signalReplicationChange(config.instanceId);
1809
+ const results = await Promise.allSettled([...activeConnectionClosers].map((closeConnection) => Promise.resolve().then(() => closeConnection())));
1810
+ const errors = new Set(unreportedConnectionCleanupErrors);
1811
+ for (const result of results) {
1812
+ if (result.status === 'rejected')
1813
+ errors.add(result.reason);
1814
+ }
1815
+ await Promise.all([...activeReplicationTasks]);
1816
+ unreportedConnectionCleanupErrors.clear();
1817
+ if (errors.size > 0) {
1818
+ throw new AggregateError([...errors], 'browser proxy connection cleanup failed');
1819
+ }
1820
+ })();
1821
+ return closePromise;
1789
1822
  },
1790
1823
  createExternalSession,
1791
1824
  query: externalQuery,
1792
1825
  exec: externalExec,
1793
1826
  };
1794
1827
  }
1795
- async function handleReplicationMessageBrowser(data, rawWrite, isClosed, closeConn, db, mutex, connection) {
1796
- if (isDebugWire()) {
1828
+ async function handleReplicationMessageBrowser(data, rawWrite, isClosed, closeConn, db, mutex, connection, instanceId, replicationRuntime, debugWire) {
1829
+ if (debugWire) {
1797
1830
  console.debug(`[pg-proxy-repl] ENTRY type=0x${data[0].toString(16)} len=${data.length}`);
1798
1831
  }
1799
1832
  // for non-SimpleQuery messages (extended protocol), execute against PGlite directly.
1800
1833
  if (data[0] !== 0x51) {
1801
- if (isDebugWire()) {
1834
+ if (debugWire) {
1802
1835
  console.debug(`[pg-proxy-repl] ext protocol msg type=0x${data[0].toString(16)} len=${data.length}`);
1803
1836
  }
1804
1837
  await mutex.acquire();
1805
1838
  try {
1806
1839
  const result = await db.execProtocolRaw(data, { syncToFs: false });
1807
- if (isDebugWire()) {
1840
+ if (debugWire) {
1808
1841
  console.debug(`[pg-proxy-repl] ext protocol result len=${result.length}`);
1809
1842
  }
1810
1843
  return result;
@@ -1820,11 +1853,6 @@ async function handleReplicationMessageBrowser(data, rawWrite, isClosed, closeCo
1820
1853
  // check if this is a START_REPLICATION command
1821
1854
  if (upper.startsWith('START_REPLICATION')) {
1822
1855
  await connection.detach();
1823
- // abort any previous replication handler to prevent zombies
1824
- if (abortPreviousReplication) {
1825
- log.proxy('aborting previous replication handler');
1826
- abortPreviousReplication();
1827
- }
1828
1856
  let aborted = false;
1829
1857
  const writer = {
1830
1858
  write(chunk) {
@@ -1846,30 +1874,32 @@ async function handleReplicationMessageBrowser(data, rawWrite, isClosed, closeCo
1846
1874
  },
1847
1875
  };
1848
1876
  const abort = () => {
1877
+ if (aborted)
1878
+ return;
1849
1879
  aborted = true;
1850
1880
  closeConn();
1851
1881
  };
1852
- abortPreviousReplication = abort;
1853
- handleStartReplication(query, writer, db, mutex).catch((err) => {
1882
+ replicationRuntime.replace(abort);
1883
+ replicationRuntime.track(handleStartReplication(query, writer, db, mutex, instanceId), abort, (err) => {
1854
1884
  log.proxy(`replication stream ended: ${err}`);
1855
1885
  });
1856
1886
  return undefined;
1857
1887
  }
1858
1888
  // handle replication queries + fallthrough to pglite, all under mutex
1859
- if (isDebugWire()) {
1889
+ if (debugWire) {
1860
1890
  console.debug(`[pg-proxy-repl] query: ${query.slice(0, 100)}`);
1861
1891
  console.debug(`[pg-proxy-repl] acquiring mutex...`);
1862
1892
  }
1863
1893
  await mutex.acquire();
1864
- if (isDebugWire())
1894
+ if (debugWire)
1865
1895
  console.debug(`[pg-proxy-repl] mutex acquired, testing db access...`);
1866
1896
  try {
1867
1897
  const testResult = await db.query('SELECT 1 as test');
1868
- if (isDebugWire()) {
1898
+ if (debugWire) {
1869
1899
  console.debug(`[pg-proxy-repl] db.query works: ${JSON.stringify(testResult.rows)}`);
1870
1900
  }
1871
- const response = await handleReplicationQuery(query, db);
1872
- if (isDebugWire()) {
1901
+ const response = await handleReplicationQuery(query, db, instanceId);
1902
+ if (debugWire) {
1873
1903
  console.debug(`[pg-proxy-repl] handleReplicationQuery result: ${response ? 'bytes(' + response.length + ')' : 'null'}`);
1874
1904
  }
1875
1905
  if (response)