prisma-pglite-bridge 0.5.1 → 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -15,18 +15,6 @@ pnpm add -D prisma-pglite-bridge @electric-sql/pglite @prisma/adapter-pg pg
15
15
  The last three are peer dependencies you may already have.
16
16
  TypeScript users also need `@types/pg`.
17
17
 
18
- ### Bridge fs-sync policy
19
-
20
- The adapter defaults `syncToFs` to `'auto'`:
21
-
22
- - in-memory PGlite (`new PGlite()` or `memory://...`) resolves to `false`
23
- - persistent `dataDir` usage resolves to `true`
24
-
25
- That keeps bridge-heavy test workloads on the lower-memory fast path
26
- without changing durability defaults for persistent databases.
27
- If you use a custom `fs`, set `syncToFs` explicitly because the
28
- adapter cannot infer whether that storage is durable.
29
-
30
18
  ## Quickstart
31
19
 
32
20
  ```typescript
@@ -77,6 +65,18 @@ it from environment variables, network input, or any value that
77
65
  crosses a trust boundary, and keep the migrations directory
78
66
  writable only by trusted processes.
79
67
 
68
+ ## Bridge fs-sync policy
69
+
70
+ The adapter defaults `syncToFs` to `'auto'`:
71
+
72
+ - in-memory PGlite (`new PGlite()` or `memory://...`) resolves to `false`
73
+ - persistent `dataDir` usage resolves to `true`
74
+
75
+ That keeps bridge-heavy test workloads on the lower-memory fast path
76
+ without changing durability defaults for persistent databases.
77
+ If you use a custom `fs`, set `syncToFs` explicitly because the
78
+ adapter cannot infer whether that storage is durable.
79
+
80
80
  ## API
81
81
 
82
82
  ### `createPgliteAdapter(options)`
@@ -107,15 +107,20 @@ Returns:
107
107
  Note: this clears all data including seed data — re-seed after
108
108
  reset if needed.
109
109
  - `close()` — shuts down the pool. The caller-supplied PGlite
110
- instance is not closed — you own its lifecycle. Not needed in
111
- tests (process exit handles it); use in long-running scripts
112
- or dev servers.
110
+ instance is not closed — you own its lifecycle. Recommended in
111
+ explicit test teardown, long-running scripts, and dev servers so
112
+ the pool is released promptly and leak warnings do not fire.
113
113
  - `stats()` — returns telemetry when `statsLevel` is `'basic'` or
114
114
  `'full'`, else `undefined`. See [Stats collection](#stats-collection).
115
115
  - `adapterId` — a unique `symbol` identifying this adapter. Use it
116
116
  to filter events from the public
117
117
  [diagnostics channels](#diagnostics-channels) when multiple
118
118
  adapters share a process.
119
+ - `snapshotDb()` — captures the current DB contents into an internal
120
+ snapshot so later `resetDb()` calls restore to that state instead of
121
+ truncating to empty.
122
+ - `resetSnapshot()` — discards the current snapshot so later
123
+ `resetDb()` calls truncate back to empty again.
119
124
 
120
125
  ### `createPool(options)`
121
126
 
@@ -137,7 +142,7 @@ Returns `pool` (pg.Pool), `adapterId` (a unique `symbol` for
137
142
  [diagnostics channel](#diagnostics-channels) filtering), and
138
143
  `close()` (which shuts down the pool only — the caller-supplied
139
144
  PGlite instance is not closed). Accepts `pglite` (required),
140
- `max`, and `adapterId`.
145
+ `max`, `adapterId`, and `syncToFs`.
141
146
 
142
147
  ### `PGliteBridge`
143
148
 
@@ -186,6 +191,7 @@ the in-memory PGlite version:
186
191
  import { PGlite } from '@electric-sql/pglite';
187
192
  import { createPgliteAdapter } from 'prisma-pglite-bridge';
188
193
  import { PrismaClient } from '@prisma/client';
194
+ import { beforeEach, vi } from 'vitest';
189
195
 
190
196
  const pglite = new PGlite();
191
197
  const { adapter, resetDb } = await createPgliteAdapter({
package/dist/index.cjs CHANGED
@@ -585,7 +585,10 @@ var PGliteBridge = class extends node_stream.Duplex {
585
585
  const message = this.input.consume(len);
586
586
  await this.acquireSession();
587
587
  await this.pglite.runExclusive(async () => {
588
- await this.execAndPush(message, false);
588
+ await this.streamProtocol(message, {
589
+ detectErrors: false,
590
+ suppressIntermediateRfq: false
591
+ });
589
592
  });
590
593
  this.phase = "ready";
591
594
  }
@@ -623,7 +626,10 @@ var PGliteBridge = class extends node_stream.Duplex {
623
626
  await this.flushPipeline();
624
627
  continue;
625
628
  }
626
- await this.runWithTiming((detectErrors) => this.execAndPush(message, detectErrors));
629
+ await this.runWithTiming((detectErrors) => this.streamProtocol(message, {
630
+ detectErrors,
631
+ suppressIntermediateRfq: false
632
+ }));
627
633
  }
628
634
  }
629
635
  /**
@@ -640,7 +646,10 @@ var PGliteBridge = class extends node_stream.Duplex {
640
646
  const messages = this.pipeline;
641
647
  this.pipeline = [];
642
648
  const batch = concat(messages);
643
- await this.runWithTiming((detectErrors) => this.runPipelineBatch(batch, detectErrors));
649
+ await this.runWithTiming((detectErrors) => this.streamProtocol(batch, {
650
+ detectErrors,
651
+ suppressIntermediateRfq: true
652
+ }));
644
653
  }
645
654
  /**
646
655
  * Acquires the session, runs the op under `pglite.runExclusive`, and
@@ -694,42 +703,22 @@ var PGliteBridge = class extends node_stream.Duplex {
694
703
  });
695
704
  }
696
705
  }
697
- async runPipelineBatch(batch, detectErrors) {
698
- let errSeen = false;
699
- const framer = new BackendMessageFramer({
700
- suppressIntermediateReadyForQuery: true,
701
- onChunk: (chunk) => {
702
- /* c8 ignore next — race-only: tornDown becomes true mid-stream */
703
- if (!this.tornDown && chunk.length > 0) this.push(chunk);
704
- },
705
- onErrorResponse: () => {
706
- if (detectErrors) errSeen = true;
707
- },
708
- onReadyForQuery: (status) => {
709
- if (this.sessionLock) this.sessionLock.updateStatus(this.bridgeId, status);
710
- }
711
- });
712
- await this.pglite.execProtocolRawStream(batch, {
713
- syncToFs: this.syncToFs,
714
- onRawData: (chunk) => {
715
- /* c8 ignore next — race-only: tornDown becomes true mid-stream */
716
- if (!this.tornDown) framer.write(chunk);
717
- }
718
- });
719
- framer.flush({ dropHeldReadyForQuery: this.tornDown });
720
- return !errSeen;
721
- }
722
706
  /**
723
- * Sends a message to PGlite and pushes response chunks directly to the
724
- * stream as they arrive. Avoids collecting and concatenating for large
725
- * multi-row responses (e.g., findMany 500 rows = ~503 onRawData chunks).
707
+ * Sends a message (or pipelined batch) to PGlite and pushes response
708
+ * chunks directly to the stream as they arrive. Avoids collecting and
709
+ * concatenating for large multi-row responses (e.g., findMany 500 rows
710
+ * = ~503 onRawData chunks).
711
+ *
712
+ * For pipelined Extended Query batches, pass `suppressIntermediateRfq`
713
+ * so only the final ReadyForQuery reaches the client.
726
714
  *
727
715
  * Must be called inside runExclusive.
728
716
  */
729
- async execAndPush(message, detectErrors) {
717
+ async streamProtocol(message, options) {
718
+ const { detectErrors, suppressIntermediateRfq } = options;
730
719
  let errSeen = false;
731
720
  const framer = new BackendMessageFramer({
732
- suppressIntermediateReadyForQuery: false,
721
+ suppressIntermediateReadyForQuery: suppressIntermediateRfq,
733
722
  onChunk: (chunk) => {
734
723
  /* c8 ignore next — race-only: tornDown becomes true mid-stream */
735
724
  if (!this.tornDown && chunk.length > 0) this.push(chunk);
@@ -756,6 +745,49 @@ var PGliteBridge = class extends node_stream.Duplex {
756
745
  }
757
746
  };
758
747
  //#endregion
748
+ //#region src/bridge-client.ts
749
+ const bridgeClientOptionsKey = Symbol("bridgeClientOptions");
750
+ var BridgeClient = class extends pg.default.Client {
751
+ querySubmissionChain = Promise.resolve();
752
+ constructor(config) {
753
+ const { [bridgeClientOptionsKey]: bridge, ...clientConfig } = config ?? {};
754
+ if (!bridge) throw new Error("BridgeClient requires bridge options");
755
+ super({
756
+ ...clientConfig,
757
+ user: "postgres",
758
+ database: "postgres",
759
+ stream: () => new PGliteBridge(bridge.pglite, bridge.sessionLock, bridge.adapterId, bridge.telemetry, bridge.syncToFs)
760
+ });
761
+ }
762
+ query(...args) {
763
+ const first = args[0];
764
+ const callSuper = () => super.query.apply(this, args);
765
+ if (first === null || first === void 0) return callSuper();
766
+ if (typeof first.submit === "function") return callSuper();
767
+ const prior = this.querySubmissionChain;
768
+ let signalDone;
769
+ this.querySubmissionChain = new Promise((resolve) => {
770
+ signalDone = resolve;
771
+ });
772
+ const cbIndex = args.findIndex((arg) => typeof arg === "function");
773
+ if (cbIndex !== -1) {
774
+ const origCb = args[cbIndex];
775
+ args[cbIndex] = (err, res) => {
776
+ signalDone();
777
+ origCb(err, res);
778
+ };
779
+ prior.then(callSuper).catch((err) => {
780
+ signalDone();
781
+ origCb(err, void 0);
782
+ });
783
+ return;
784
+ }
785
+ const p = prior.then(callSuper);
786
+ p.then(signalDone, signalDone);
787
+ return p;
788
+ }
789
+ };
790
+ //#endregion
759
791
  //#region src/utils/session-lock.ts
760
792
  /**
761
793
  * Session-level lock for PGlite's single-session model.
@@ -904,20 +936,18 @@ const createPool$1 = async (options) => {
904
936
  const syncToFs = resolveSyncToFs(pglite, options.syncToFs);
905
937
  await pglite.waitReady;
906
938
  const sessionLock = new SessionLock();
907
- const Client = class extends pg.default.Client {
908
- constructor(config = {}) {
909
- super({
910
- ...config,
911
- user: "postgres",
912
- database: "postgres",
913
- stream: () => new PGliteBridge(pglite, sessionLock, adapterId, telemetry, syncToFs)
914
- });
939
+ const poolConfig = {
940
+ Client: BridgeClient,
941
+ max,
942
+ [bridgeClientOptionsKey]: {
943
+ pglite,
944
+ sessionLock,
945
+ adapterId,
946
+ telemetry,
947
+ syncToFs
915
948
  }
916
949
  };
917
- const pool = new pg.default.Pool({
918
- Client,
919
- max
920
- });
950
+ const pool = new pg.default.Pool(poolConfig);
921
951
  const close = () => pool.end();
922
952
  return {
923
953
  pool,