prisma-pglite-bridge 1.0.0 → 1.1.0

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/index.cjs CHANGED
@@ -109,6 +109,38 @@ const EQP_MESSAGES = new Set([
109
109
  const MAX_BACKEND_MESSAGE_LENGTH = 1073741824;
110
110
  //#endregion
111
111
  //#region src/duplex/row-description.ts
112
+ const readUInt32BE = (buf, offset) => {
113
+ /* c8 ignore start — callers guard fixed-width reads */
114
+ const b1 = buf[offset] ?? 0;
115
+ const b2 = buf[offset + 1] ?? 0;
116
+ const b3 = buf[offset + 2] ?? 0;
117
+ const b4 = buf[offset + 3] ?? 0;
118
+ /* c8 ignore stop */
119
+ return (b1 << 24 | b2 << 16 | b3 << 8 | b4) >>> 0;
120
+ };
121
+ const readUInt16BE = (buf, offset) => {
122
+ /* c8 ignore start — callers guard fixed-width reads */
123
+ const b1 = buf[offset] ?? 0;
124
+ const b2 = buf[offset + 1] ?? 0;
125
+ /* c8 ignore stop */
126
+ return b1 << 8 | b2;
127
+ };
128
+ const rowDescriptionNeedsRewrite = (buf, start = 0, end = buf.length) => {
129
+ if (end - start < 7) return false;
130
+ const fieldCount = readUInt16BE(buf, start + 5);
131
+ let p = start + 7;
132
+ for (let i = 0; i < fieldCount; i++) {
133
+ while (p < end && buf[p] !== 0) p++;
134
+ p++;
135
+ /* c8 ignore next — defense-in-depth: framer caller passes a complete frame */
136
+ if (p + 18 > end) return false;
137
+ const tableOID = readUInt32BE(buf, p);
138
+ p += 6;
139
+ if (readUInt32BE(buf, p) === 18 && tableOID !== 0 && tableOID < 16384) return true;
140
+ p += 12;
141
+ }
142
+ return false;
143
+ };
112
144
  /**
113
145
  * Widens any field whose dataTypeOID is 18 ("char") to oid 25 (text) — but
114
146
  * only when the field originates from a pg_catalog relation (tableOID is
@@ -121,6 +153,7 @@ const MAX_BACKEND_MESSAGE_LENGTH = 1073741824;
121
153
  * fixed-size in place.
122
154
  */
123
155
  const rewriteRowDescriptionInPlace = (buf) => {
156
+ if (!rowDescriptionNeedsRewrite(buf)) return;
124
157
  const fieldCount = buf.readInt16BE(5);
125
158
  let p = 7;
126
159
  for (let i = 0; i < fieldCount; i++) {
@@ -213,7 +246,7 @@ var BackendMessageFramer = class {
213
246
  this.emitReadyForQuery();
214
247
  this.rfqBytesRead = 0;
215
248
  }
216
- } else if (msgType === 84) {
249
+ } else if (msgType === 84) if (rowDescriptionNeedsRewrite(chunk, offset, offset + totalLen)) {
217
250
  flushPassthrough(offset);
218
251
  if (this.suppressIntermediateReadyForQuery && this.rfqBytesRead === 6) this.dropHeldReadyForQuery();
219
252
  this.emitRewrittenRowDescription(Buffer.from(chunk.subarray(offset, offset + totalLen)));
@@ -221,6 +254,10 @@ var BackendMessageFramer = class {
221
254
  if (this.suppressIntermediateReadyForQuery && this.rfqBytesRead === 6) this.dropHeldReadyForQuery();
222
255
  if (passthroughStart < 0) passthroughStart = offset;
223
256
  }
257
+ else {
258
+ if (this.suppressIntermediateReadyForQuery && this.rfqBytesRead === 6) this.dropHeldReadyForQuery();
259
+ if (passthroughStart < 0) passthroughStart = offset;
260
+ }
224
261
  offset += totalLen;
225
262
  continue;
226
263
  }
@@ -502,6 +539,15 @@ var FrontendMessageBuffer = class {
502
539
  * after each sub-message (PGlite's single-user mode). These are stripped,
503
540
  * keeping only the final ReadyForQuery after Sync.
504
541
  */
542
+ const TERMINATE_MESSAGE = new Uint8Array([
543
+ 88,
544
+ 0,
545
+ 0,
546
+ 0,
547
+ 4
548
+ ]);
549
+ const PROTOCOL_CLEANUP_RAW_BYTES = 8 * 1024 * 1024;
550
+ const PROTOCOL_CLEANUP_CALLS = 32;
505
551
  /**
506
552
  * Duplex stream that bridges `pg.Client` to an in-process PGlite instance.
507
553
  *
@@ -553,6 +599,9 @@ var PGliteDuplex = class extends node_stream.Duplex {
553
599
  /** Memoized rollback so concurrent teardown paths (e.g., `_final` then
554
600
  * `_destroy`) don't issue duplicate `ROLLBACK` statements. */
555
601
  rollbackPromise;
602
+ pendingProtocolCleanupBytes = 0;
603
+ pendingProtocolCleanupCalls = 0;
604
+ protocolCleanupUnsupported = false;
556
605
  /** Resolves once the stream has fully torn down (post-`_final` rollback,
557
606
  * post-`_destroy`). Single-shot, mirroring the `'close'` event. */
558
607
  onClose;
@@ -766,20 +815,38 @@ var PGliteDuplex = class extends node_stream.Duplex {
766
815
  let batch;
767
816
  /* c8 ignore next 3 — flushPipeline only runs after Sync is appended */
768
817
  if (messages.length === 1) batch = messages[0] ?? new Uint8Array(0);
769
- else {
770
- const total = messages.reduce((sum, p) => sum + p.length, 0);
771
- batch = new Uint8Array(total);
772
- let offset = 0;
773
- for (const part of messages) {
774
- batch.set(part, offset);
775
- offset += part.length;
776
- }
777
- }
818
+ else batch = this.tryContiguousPipelineBatch(messages) ?? this.concatPipeline(messages);
778
819
  await this.runWithTiming((detectErrors) => this.streamProtocol(batch, {
779
820
  detectErrors,
780
821
  suppressIntermediateRfq: true
781
822
  }));
782
823
  }
824
+ tryContiguousPipelineBatch(messages) {
825
+ const first = messages[0];
826
+ /* c8 ignore next — caller only passes non-empty pipelines */
827
+ if (first === void 0) return void 0;
828
+ const buffer = first.buffer;
829
+ const start = first.byteOffset;
830
+ let end = start + first.byteLength;
831
+ for (let i = 1; i < messages.length; i++) {
832
+ const part = messages[i];
833
+ /* c8 ignore next — pipeline array has no holes */
834
+ if (part === void 0) return void 0;
835
+ if (part.buffer !== buffer || part.byteOffset !== end) return;
836
+ end += part.byteLength;
837
+ }
838
+ return new Uint8Array(buffer, start, end - start);
839
+ }
840
+ concatPipeline(messages) {
841
+ const total = messages.reduce((sum, p) => sum + p.length, 0);
842
+ const batch = new Uint8Array(total);
843
+ let offset = 0;
844
+ for (const part of messages) {
845
+ batch.set(part, offset);
846
+ offset += part.length;
847
+ }
848
+ return batch;
849
+ }
783
850
  /**
784
851
  * Acquires the session, runs the op under `pglite.runExclusive`, and
785
852
  * updates internal stats and/or publishes diagnostics events when enabled.
@@ -835,10 +902,8 @@ var PGliteDuplex = class extends node_stream.Duplex {
835
902
  }
836
903
  }
837
904
  /**
838
- * Sends a message (or pipelined batch) to PGlite and pushes response
839
- * chunks directly to the stream as they arrive. Avoids collecting and
840
- * concatenating for large multi-row responses (e.g., findMany 500 rows
841
- * = ~503 onRawData chunks).
905
+ * Sends a message (or pipelined batch) to PGlite and pushes the raw protocol
906
+ * response to the stream.
842
907
  *
843
908
  * For pipelined Extended Query batches, pass `suppressIntermediateRfq`
844
909
  * so only the final ReadyForQuery reaches the client.
@@ -863,15 +928,47 @@ var PGliteDuplex = class extends node_stream.Duplex {
863
928
  }
864
929
  });
865
930
  await waitPGliteReady(this.pglite, this.timeout);
866
- await this.pglite.execProtocolRawStream(message, {
867
- syncToFs: this.syncToFs,
868
- onRawData: (chunk) => {
869
- framer.write(chunk);
931
+ let rawBytes = 0;
932
+ let streamFailed = false;
933
+ try {
934
+ await this.pglite.execProtocolRawStream(message, {
935
+ syncToFs: this.syncToFs,
936
+ onRawData: (chunk) => {
937
+ rawBytes += chunk.byteLength;
938
+ framer.write(chunk);
939
+ }
940
+ });
941
+ } catch (err) {
942
+ streamFailed = true;
943
+ throw err;
944
+ } finally {
945
+ this.pendingProtocolCleanupBytes += rawBytes;
946
+ this.pendingProtocolCleanupCalls++;
947
+ if (!this.protocolCleanupUnsupported && (streamFailed || this.pendingProtocolCleanupBytes >= PROTOCOL_CLEANUP_RAW_BYTES || this.pendingProtocolCleanupCalls >= PROTOCOL_CLEANUP_CALLS)) {
948
+ await this.clearPGliteProtocolMessages();
949
+ this.pendingProtocolCleanupBytes = 0;
950
+ this.pendingProtocolCleanupCalls = 0;
870
951
  }
871
- });
952
+ }
872
953
  framer.flush({ dropHeldReadyForQuery: this.tornDown });
873
954
  return !errSeen;
874
955
  }
956
+ async clearPGliteProtocolMessages() {
957
+ if (this.protocolCleanupUnsupported) return;
958
+ const { execProtocolStream } = this.pglite;
959
+ if (typeof execProtocolStream !== "function") {
960
+ this.protocolCleanupUnsupported = true;
961
+ return;
962
+ }
963
+ try {
964
+ await execProtocolStream.call(this.pglite, TERMINATE_MESSAGE, {
965
+ syncToFs: false,
966
+ throwOnError: false
967
+ });
968
+ } catch {
969
+ this.protocolCleanupUnsupported = true;
970
+ }
971
+ }
875
972
  acquireSession() {
876
973
  return this.sessionLock?.acquire(this.duplexId);
877
974
  }
@@ -1109,7 +1206,7 @@ const wrapTypesWithFastArrayParsers = (types) => {
1109
1206
  //#endregion
1110
1207
  //#region src/pool/pg-bridge-client.ts
1111
1208
  var PgBridgeClient = class PgBridgeClient extends pg.default.Client {
1112
- querySubmissionChain = Promise.resolve();
1209
+ querySubmissionChain;
1113
1210
  static OptionsKey = Symbol("PgBridgeClientOptions");
1114
1211
  constructor(config) {
1115
1212
  const { [PgBridgeClient.OptionsKey]: bridge, ...clientConfig } = config ?? {};
@@ -1123,33 +1220,41 @@ var PgBridgeClient = class PgBridgeClient extends pg.default.Client {
1123
1220
  }
1124
1221
  query(...args) {
1125
1222
  const first = args[0];
1126
- const callSuper = () => super.query.apply(this, args);
1127
- if (first === null || first === void 0) return callSuper();
1128
- if (typeof first.submit === "function") return callSuper();
1129
- if (isObject(first) && isTypesLike(first.types)) args[0] = {
1130
- ...first,
1131
- types: wrapTypesWithFastArrayParsers(first.types)
1223
+ const submit = () => {
1224
+ return super.query.apply(this, args);
1132
1225
  };
1133
- const prior = this.querySubmissionChain;
1134
- let signalDone;
1135
- this.querySubmissionChain = new Promise((resolve) => {
1136
- signalDone = resolve;
1137
- });
1226
+ if (first === null || first === void 0) return submit();
1227
+ if (typeof first.submit === "function") return submit();
1138
1228
  const cbIndex = args.findIndex((arg) => typeof arg === "function");
1139
1229
  if (cbIndex !== -1) {
1140
1230
  const origCb = args[cbIndex];
1141
- args[cbIndex] = (err, res) => {
1142
- signalDone();
1143
- origCb(err, res);
1144
- };
1145
- prior.then(callSuper).catch((err) => {
1146
- signalDone();
1231
+ const promiseArgs = args.slice();
1232
+ promiseArgs.splice(cbIndex, 1);
1233
+ try {
1234
+ this.query(...promiseArgs).then((res) => origCb(null, res), (err) => origCb(err, void 0));
1235
+ } catch (err) {
1147
1236
  origCb(err, void 0);
1148
- });
1237
+ }
1149
1238
  return;
1150
1239
  }
1151
- const p = prior.then(callSuper);
1152
- p.then(signalDone, signalDone);
1240
+ if (isObject(first) && isTypesLike(first.types)) args[0] = {
1241
+ ...first,
1242
+ types: wrapTypesWithFastArrayParsers(first.types)
1243
+ };
1244
+ const prior = this.querySubmissionChain;
1245
+ let p;
1246
+ if (prior === void 0) try {
1247
+ p = submit();
1248
+ } catch (err) {
1249
+ return Promise.reject(err);
1250
+ }
1251
+ else p = prior.then(submit);
1252
+ let done;
1253
+ const clearChain = () => {
1254
+ if (this.querySubmissionChain === done) this.querySubmissionChain = void 0;
1255
+ };
1256
+ done = p.then(clearChain, clearChain);
1257
+ this.querySubmissionChain = done;
1153
1258
  return p;
1154
1259
  }
1155
1260
  };