kern-db 0.3.0 → 0.5.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.
@@ -1,90 +1,8 @@
1
1
  import { i as decodeBoolean } from "./bytea-DNZ_ftio.mjs";
2
2
  import { S as createStatement, _ as sql, f as compileFragment, n as startSqlQuery, p as compileScript, x as quoteIdentifier } from "./query-FKBN7BMY.mjs";
3
- import { i as setBrand, n as QueryError, r as isInstance, t as ConnectionError } from "./driver-error-26zcdHeU.mjs";
3
+ import { i as setBrand, n as QueryError, r as isInstance, t as ConnectionError } from "./driver-error-D8D4UQ4F.mjs";
4
4
  import { n as formatArray } from "./array-BLt8ZNR5.mjs";
5
5
  import { AsyncLocalStorage } from "node:async_hooks";
6
- // src/transaction/release.ts
7
- /**
8
- * Returns a connection to the driver and passes a failed release to `onReleaseError`, or to `console.error` without one.
9
- *
10
- * The result or error of the transaction is decided before its release, so a failed release must not change it: a committed transaction that rejected would be retried.
11
- * Reporting the failure cannot change it either.
12
- * The error of a handler that throws or rejects goes to `console.error` with the failure of the release, and a `console.error` that throws is ignored.
13
- */
14
- async function releaseConnection(connection, onReleaseError) {
15
- try {
16
- await connection.release();
17
- } catch (error) {
18
- reportReleaseError(error, onReleaseError);
19
- }
20
- }
21
- /** Passes a failed release to `onReleaseError`, or to `console.error` without one, without throwing. */
22
- function reportReleaseError(error, onReleaseError) {
23
- if (!onReleaseError) {
24
- logReleaseError(error);
25
- return;
26
- }
27
- try {
28
- Promise.resolve(onReleaseError(error)).catch((handlerError) => logReleaseError(error, { error: handlerError }));
29
- } catch (handlerError) {
30
- logReleaseError(error, { error: handlerError });
31
- }
32
- }
33
- /**
34
- * Writes a failed release to `console.error`, preceded by the error of `onReleaseError` when the handler failed.
35
- *
36
- * A console that throws, such as one redirected to a failing logger, leaves nowhere to report to, so its error is ignored.
37
- */
38
- function logReleaseError(error, handlerFailure) {
39
- try {
40
- if (handlerFailure) console.error("kern-db: onReleaseError failed", handlerFailure.error);
41
- console.error("kern-db: releasing a connection failed", error);
42
- } catch {}
43
- }
44
- // src/transaction/abort.ts
45
- /**
46
- * Runs `start` and settles as its promise, or rejects with the reason of `signal` when it aborts first and `abort`, called on the abort, returns `true`.
47
- *
48
- * It listens from before `start`, which may abort the signal before it returns, and stops once the promise of `start` settles, such as for the signal that `node:test` aborts after every test.
49
- * A later rejection of that promise stays handled, and a value it resolves with after the rejection goes to `discard`, which handles its own failures.
50
- */
51
- function settleUnlessAborted(start, signal, abort, discard) {
52
- const settled = Promise.withResolvers();
53
- let abandoned = false;
54
- const onAbort = () => {
55
- if (abort()) {
56
- abandoned = true;
57
- settled.reject(signal.reason);
58
- }
59
- };
60
- signal.addEventListener("abort", onAbort, { once: true });
61
- let running;
62
- try {
63
- running = start();
64
- } catch (error) {
65
- signal.removeEventListener("abort", onAbort);
66
- settled.reject(error);
67
- return settled.promise;
68
- }
69
- running.then((value) => {
70
- signal.removeEventListener("abort", onAbort);
71
- if (abandoned) discard?.(value);
72
- else settled.resolve(value);
73
- }, (error) => {
74
- signal.removeEventListener("abort", onAbort);
75
- settled.reject(error);
76
- });
77
- return settled.promise;
78
- }
79
- /**
80
- * Acquires a connection for a test transaction, rejecting with the reason of `signal` when it aborts first.
81
- *
82
- * A connection acquired after the abort returns to the pool as best-effort cleanup: the test transaction has already rejected, so nothing awaits the release, and its failure goes to `onReleaseError`.
83
- */
84
- function acquireUnlessAborted(driver, signal, onReleaseError) {
85
- if (signal.aborted) return Promise.reject(signal.reason);
86
- return settleUnlessAborted(() => driver.acquire(), signal, () => true, (connection) => void releaseConnection(connection, onReleaseError));
87
- }
88
6
  // src/transaction/advisory-lock.ts
89
7
  /** Pause of `pollLock` in milliseconds after its first attempt, which doubles after each attempt up to `maxPollDelay`. */
90
8
  const firstPollDelay = 50;
@@ -217,14 +135,13 @@ var TransactionConnectionError = class TransactionConnectionError extends Connec
217
135
  *
218
136
  * - `TransactionConnectionError` of phase `commit` gives `true`, since `COMMIT` may have succeeded.
219
137
  * - `TransactionConnectionError` of another phase gives `false`, since the transaction was not committed.
138
+ * - A `ConnectionError` whose `unsent` is `true`, such as after a failure to get a connection or of a closed driver, gives `false`, since the statement was not sent.
220
139
  * - Another `ConnectionError` gives `true`, since a statement outside a transaction may have run before its connection failed.
221
140
  * - Any other error gives `false`, such as a `QueryError`, or a `ResultDecodeError` of a statement that has run: `false` does not mean that the operation applied nothing.
222
- *
223
- * Classification depends only on the error class, so failures before the statement was sent, such as a failure to acquire a connection, also give `true`.
224
141
  */
225
142
  function isOutcomeUnknown(error) {
226
143
  if (error instanceof TransactionConnectionError) return error.phase === "commit";
227
- return error instanceof ConnectionError;
144
+ return error instanceof ConnectionError && error.unsent !== true;
228
145
  }
229
146
  /**
230
147
  * Reserved connection of a transaction of `transaction` that records the first `ConnectionError` of its statements and scripts, including those of its nested levels.
@@ -301,6 +218,88 @@ function createSettingStatement(settings, local) {
301
218
  }
302
219
  return createStatement(text, parameters);
303
220
  }
221
+ // src/transaction/release.ts
222
+ /**
223
+ * Returns a connection to the driver and passes a failed release to `onReleaseError`, or to `console.error` without one.
224
+ *
225
+ * The result or error of the transaction is decided before its release, so a failed release must not change it: a committed transaction that rejected would be retried.
226
+ * Reporting the failure cannot change it either.
227
+ * The error of a handler that throws or rejects goes to `console.error` with the failure of the release, and a `console.error` that throws is ignored.
228
+ */
229
+ async function releaseConnection(connection, onReleaseError) {
230
+ try {
231
+ await connection.release();
232
+ } catch (error) {
233
+ reportReleaseError(error, onReleaseError);
234
+ }
235
+ }
236
+ /** Passes a failed release to `onReleaseError`, or to `console.error` without one, without throwing. */
237
+ function reportReleaseError(error, onReleaseError) {
238
+ if (!onReleaseError) {
239
+ logReleaseError(error);
240
+ return;
241
+ }
242
+ try {
243
+ Promise.resolve(onReleaseError(error)).catch((handlerError) => logReleaseError(error, { error: handlerError }));
244
+ } catch (handlerError) {
245
+ logReleaseError(error, { error: handlerError });
246
+ }
247
+ }
248
+ /**
249
+ * Writes a failed release to `console.error`, preceded by the error of `onReleaseError` when the handler failed.
250
+ *
251
+ * A console that throws, such as one redirected to a failing logger, leaves nowhere to report to, so its error is ignored.
252
+ */
253
+ function logReleaseError(error, handlerFailure) {
254
+ try {
255
+ if (handlerFailure) console.error("kern-db: onReleaseError failed", handlerFailure.error);
256
+ console.error("kern-db: releasing a connection failed", error);
257
+ } catch {}
258
+ }
259
+ // src/transaction/test-levels/abort.ts
260
+ /**
261
+ * Runs `start` and settles as its promise, or rejects with the reason of `signal` when it aborts first and `abort`, called on the abort, returns `true`.
262
+ *
263
+ * It listens from before `start`, which may abort the signal before it returns, and stops once the promise of `start` settles, such as for the signal that `node:test` aborts after every test.
264
+ * A later rejection of that promise stays handled, and a value it resolves with after the rejection goes to `discard`, which handles its own failures.
265
+ */
266
+ function settleUnlessAborted(start, signal, abort, discard) {
267
+ const settled = Promise.withResolvers();
268
+ let abandoned = false;
269
+ const onAbort = () => {
270
+ if (abort()) {
271
+ abandoned = true;
272
+ settled.reject(signal.reason);
273
+ }
274
+ };
275
+ signal.addEventListener("abort", onAbort, { once: true });
276
+ let running;
277
+ try {
278
+ running = start();
279
+ } catch (error) {
280
+ signal.removeEventListener("abort", onAbort);
281
+ settled.reject(error);
282
+ return settled.promise;
283
+ }
284
+ running.then((value) => {
285
+ signal.removeEventListener("abort", onAbort);
286
+ if (abandoned) discard?.(value);
287
+ else settled.resolve(value);
288
+ }, (error) => {
289
+ signal.removeEventListener("abort", onAbort);
290
+ settled.reject(error);
291
+ });
292
+ return settled.promise;
293
+ }
294
+ /**
295
+ * Acquires a connection for a test transaction, rejecting with the reason of `signal` when it aborts first.
296
+ *
297
+ * A connection acquired after the abort returns to the pool as best-effort cleanup: the test transaction has already rejected, so nothing awaits the release, and its failure goes to `onReleaseError`.
298
+ */
299
+ function acquireUnlessAborted(driver, signal, onReleaseError) {
300
+ if (signal.aborted) return Promise.reject(signal.reason);
301
+ return settleUnlessAborted(() => driver.acquire(), signal, () => true, (connection) => void releaseConnection(connection, onReleaseError));
302
+ }
304
303
  // src/transaction/level.ts
305
304
  /** Options of a level started without options, shared so that such a start allocates none. */
306
305
  const noLevelOptions = {};
@@ -347,7 +346,8 @@ function createLevel(connection, parent, transaction, isolation) {
347
346
  endedBy: void 0,
348
347
  restore: void 0,
349
348
  isolation: enclosing ? enclosing.isolation : isolation,
350
- locks: void 0
349
+ locks: void 0,
350
+ turns: void 0
351
351
  };
352
352
  }
353
353
  /**
@@ -434,6 +434,11 @@ async function runLevel(storage, level, callback, commits, signal) {
434
434
  rejection = { error };
435
435
  }
436
436
  }
437
+ if (!rejection && level.turns && !level.cleanup && !level.turns.tryTake()) try {
438
+ await level.turns.take(signal);
439
+ } catch {
440
+ endLevel(level, false, "abort");
441
+ }
437
442
  if (level.cleanup) return getEndedOutcome(level.cleanup, level.endedBy === "abort" ? signal?.reason : createClosedError(level, rejection));
438
443
  if (!rejection && level.child) rejection = { error: /* @__PURE__ */ new Error(level.session ? "Session ended with an error: a transaction of it was still running when its callback finished" : "Transaction was rolled back: a nested transaction was still running when its callback finished") };
439
444
  if (!rejection && level.rollbackOnly) rejection = { error: new Error("Transaction was rolled back: a nested transaction failed to end", { cause: level.rollbackOnly.error }) };
@@ -443,6 +448,12 @@ async function runLevel(storage, level, callback, commits, signal) {
443
448
  rejection: combineRejections(rejection, cleanup)
444
449
  };
445
450
  }
451
+ /** Runs `callback` in a started `level` in `storage` with `runLevel`, returning its result or throwing its rejection. */
452
+ async function runStarted(storage, level, callback, commits, signal) {
453
+ const { result, rejection } = await runLevel(storage, level, callback, commits, signal);
454
+ if (rejection) throw rejection.error;
455
+ return result;
456
+ }
446
457
  /**
447
458
  * Ends `level` once, whoever ends it: later calls return the cleanup already started.
448
459
  *
@@ -457,6 +468,7 @@ function endLevel(level, commit, endedBy) {
457
468
  return level.cleanup;
458
469
  }
459
470
  level.state = "closing";
471
+ level.turns?.close();
460
472
  level.cleanup = cleanUpLevel(level, commit, level.child && endLevel(level.child, false, "enclosing level"));
461
473
  return level.cleanup;
462
474
  }
@@ -557,223 +569,6 @@ function combineRejections(earlier, cleanup) {
557
569
  function suppress(error, earlier, message) {
558
570
  return { error: earlier ? new SuppressedError(error, earlier.error, message) : error };
559
571
  }
560
- // src/transaction/fixture-stack.ts
561
- /** Open test fixtures of a transaction coordinator and the queue of the changes of their stack. */
562
- var TestFixtureStack = class {
563
- #driver;
564
- #onReleaseError;
565
- /** Async context of the levels of the coordinator, which tells whether a call runs inside a level. */
566
- #storage;
567
- /** Open test fixtures, the innermost last. */
568
- #fixtures = [];
569
- /** Changes of the fixture stack, run one after another so that each one sees the stack the previous one left. */
570
- #transitions = Promise.resolve();
571
- /**
572
- * The `close` functions of the test fixtures that are still opening, from the call that starts the opening until the fixture enters the stack or the opening ends otherwise.
573
- *
574
- * An opening holds the queue of the fixture stack while its seed runs, so a closing of the stack queued behind it would wait for a seed that may never settle.
575
- * Closing the stack therefore calls these first: each one tells its opening to stop waiting for the seed.
576
- */
577
- #openings = /* @__PURE__ */ new Set();
578
- /**
579
- * Outermost fixture in the stack whose closing is queued but has not finished.
580
- *
581
- * Openings that start after that closing is requested nest outside of the fixture once it closes, so a repeated closing of it, or a closing of a fixture nested in it, must not close them.
582
- */
583
- #closing;
584
- constructor(driver, storage, onReleaseError) {
585
- this.#driver = driver;
586
- this.#storage = storage;
587
- this.#onReleaseError = onReleaseError;
588
- }
589
- /**
590
- * Starts opening a test fixture with `options`, see `startTestFixture` of the coordinator, or, given `refusal`, returns a fixture that rejects with it.
591
- *
592
- * The `close` of a refused fixture resolves once the earlier changes of the stack end.
593
- */
594
- start(seed, refusal, options) {
595
- let level;
596
- let closeRequested = false;
597
- const closing = Promise.withResolvers();
598
- let closedRejection;
599
- const close = () => {
600
- if (this.#storage.getStore()) return Promise.reject(/* @__PURE__ */ new Error("Cannot close a test fixture inside a transaction"));
601
- closeRequested = true;
602
- closing.resolve();
603
- if (level && this.#fixtures.includes(level)) {
604
- const opened = level;
605
- if (this.#closing && this.#closing.depth <= opened.depth) return this.#enqueue(() => this.#closeFixture(opened));
606
- this.#closing = opened;
607
- return this.#closeAfterOpenings(() => this.#enqueue(async () => {
608
- try {
609
- await this.#closeFixture(opened);
610
- } finally {
611
- if (this.#closing === opened) this.#closing = void 0;
612
- }
613
- }));
614
- }
615
- return this.#enqueue(() => {
616
- const rejection = closedRejection;
617
- closedRejection = void 0;
618
- if (rejection) throw rejection.error;
619
- });
620
- };
621
- if (refusal) return {
622
- fixture: Promise.reject(refusal),
623
- close
624
- };
625
- /**
626
- * Runs stages 2 and 3 of the fixture and puts it in the stack, returning `undefined` when closing was requested and the fixture rolled back cleanly.
627
- *
628
- * It rejects with a failure to start the fixture, with the rejection of `seed`, or with a failure to roll back, which suppresses the rejection of `seed`.
629
- */
630
- const open = async () => {
631
- if (closeRequested) return;
632
- const parent = this.#fixtures.at(-1);
633
- const started = parent ? await this.#startInFixture(parent, options) : await this.#beginFixture(options?.isolation);
634
- started.state = "open";
635
- const outcome = closeRequested ? void 0 : await Promise.race([this.#seedFixture(started, seed), closing.promise]);
636
- if (outcome && !outcome.rejection && !closeRequested) {
637
- this.#fixtures.push(started);
638
- level = started;
639
- this.#openings.delete(close);
640
- const { result } = outcome;
641
- return {
642
- get data() {
643
- if (started.state !== "open") throw new Error("Test fixture is closed");
644
- return result;
645
- },
646
- close,
647
- [Symbol.asyncDispose]: close
648
- };
649
- }
650
- const rejection = combineRejections(outcome?.rejection, { rejection: await this.#endFixture(started) });
651
- if (rejection) throw rejection.error;
652
- };
653
- this.#openings.add(close);
654
- return {
655
- fixture: this.#enqueue(async () => {
656
- let opened;
657
- try {
658
- opened = await open();
659
- } catch (error) {
660
- if (!closeRequested) throw error;
661
- closedRejection = { error };
662
- } finally {
663
- this.#openings.delete(close);
664
- }
665
- if (!opened) throw new Error("Test fixture was closed before it opened");
666
- return opened;
667
- }),
668
- close
669
- };
670
- }
671
- /**
672
- * Starts the level of a test transaction in the innermost test fixture as a change of the fixture stack, resolving to `undefined` when no fixture is open.
673
- *
674
- * Aborted while it waits for its turn, it rejects at once and its change starts nothing, so that a test that has already ended never preempts the active level of the fixture.
675
- * From its turn on, it settles with its change.
676
- */
677
- startTestLevel(options) {
678
- const signal = options?.signal;
679
- let waiting = true;
680
- const start = () => this.#enqueue(() => {
681
- if (signal?.aborted) return;
682
- waiting = false;
683
- const fixture = this.#fixtures.at(-1);
684
- return fixture && this.#startInFixture(fixture, options);
685
- });
686
- return signal ? settleUnlessAborted(start, signal, () => waiting) : start();
687
- }
688
- /**
689
- * Closes every fixture, including those still opening, rejecting with the failures of all of them.
690
- *
691
- * Unlike closing a fixture, it closes every opening, including one started after a closing of the stack was requested, since nothing may stay open.
692
- */
693
- close() {
694
- return this.#closeAfterOpenings(() => this.#enqueue(() => {
695
- const root = this.#fixtures[0];
696
- return root && this.#closeFixture(root);
697
- }));
698
- }
699
- /**
700
- * Closes the fixtures still opening and runs `closeStack`, rejecting with the failures of all of them.
701
- *
702
- * A seed that never settles holds the queue of the fixture stack, so closing is requested for every opening before `closeStack` queues its change, and the rollback does not wait for the seeds.
703
- */
704
- async #closeAfterOpenings(closeStack) {
705
- const closings = [];
706
- for (const closeOpening of this.#openings) closings.push(closeOpening());
707
- closings.push(closeStack());
708
- let rejection;
709
- for (const closing of await Promise.allSettled(closings)) if (closing.status === "rejected") rejection = suppress(closing.reason, rejection, "Closing a test fixture failed");
710
- if (rejection) throw rejection.error;
711
- }
712
- /**
713
- * Runs `seed` in a savepoint of a fixture that is released when it resolves, returning its result or rejection.
714
- *
715
- * The savepoint has its own context, so late code of `seed` cannot reach the fixture after it ends.
716
- */
717
- async #seedFixture(fixture, seed) {
718
- try {
719
- return await runLevel(this.#storage, await startNestedLevel(fixture, false), seed, true);
720
- } catch (error) {
721
- return { rejection: { error } };
722
- }
723
- }
724
- /** Closes an opened fixture as a change of the fixture stack. */
725
- async #closeFixture(fixture) {
726
- const index = this.#fixtures.indexOf(fixture);
727
- if (index < 0) return;
728
- let rejection;
729
- try {
730
- rejection = await this.#endFixture(fixture);
731
- } finally {
732
- this.#fixtures.length = index;
733
- }
734
- if (rejection) throw rejection.error;
735
- }
736
- /** Rolls back a fixture and releases the connection of the first one, returning the error to reject with. */
737
- async #endFixture(fixture) {
738
- const { rejection } = await endLevel(fixture, false);
739
- if (!fixture.parent) await releaseConnection(fixture.connection, this.#onReleaseError);
740
- return rejection;
741
- }
742
- /** Runs a change of the fixture stack after the earlier ones. */
743
- #enqueue(transition) {
744
- const result = this.#transitions.then(transition);
745
- this.#transitions = result.catch(() => {});
746
- return result;
747
- }
748
- /** Starts the first test fixture: a transaction at `isolation` on a reserved connection. */
749
- async #beginFixture(isolation) {
750
- const connection = await this.#driver.acquire();
751
- try {
752
- await connection.execute(getBeginStatement(isolation));
753
- } catch (error) {
754
- await releaseConnection(connection, this.#onReleaseError);
755
- throw error;
756
- }
757
- return createLevel(connection, void 0, false, isolation);
758
- }
759
- /**
760
- * Starts a nested level of a test fixture with `options`, preempting its active one.
761
- *
762
- * A fixture runs one nested level at a time, so a new one ends the previous one, such as a test that timed out.
763
- * A level whose `signal` aborts while it preempts rejects with its reason instead of starting, and a failure to start after the abort suppresses the reason.
764
- */
765
- async #startInFixture(fixture, options) {
766
- const signal = options?.signal;
767
- if (fixture.child) {
768
- const cleanup = await endLevel(fixture.child, false, "preemption");
769
- if (cleanup.rejection) throw suppressAbortReason(new Error("Cannot start a level of the test fixture: ending its active level failed", { cause: cleanup.rejection.error }), signal);
770
- }
771
- const unavailable = createUnavailableError(fixture, "start a level of the test fixture");
772
- if (unavailable) throw suppressAbortReason(unavailable, signal);
773
- signal?.throwIfAborted();
774
- return startNestedLevel(fixture, false, options);
775
- }
776
- };
777
572
  // src/error-condition-codes.ts
778
573
  /**
779
574
  * Condition names of the SQLSTATE codes of PostgreSQL errors, by code: the codes of type `E` with a condition name, which PL/pgSQL takes in `EXCEPTION WHEN`, under the headings of their classes.
@@ -1069,7 +864,8 @@ function createSessionLevel(connection) {
1069
864
  endedBy: void 0,
1070
865
  restore: void 0,
1071
866
  isolation: void 0,
1072
- locks: void 0
867
+ locks: void 0,
868
+ turns: void 0
1073
869
  };
1074
870
  }
1075
871
  /**
@@ -1121,6 +917,436 @@ async function leaveInTransaction(connection) {
1121
917
  if (!(error instanceof QueryError) || isQueryError(error, "in_failed_sql_transaction")) return;
1122
918
  }
1123
919
  }
920
+ // src/transaction/test-levels/turns.ts
921
+ /**
922
+ * Queue of the units of work that share the connection of a level of the test helpers, which run one at a time in the order they ask for a turn.
923
+ *
924
+ * The holder of the turn passes it to the next waiter with `release`.
925
+ * Once closed, it grants every turn at once, so that its waiters see that the level is closed and reject.
926
+ */
927
+ var TurnQueue = class {
928
+ #held = false;
929
+ #closed = false;
930
+ /** Waiters in the order they asked for a turn; each one, called, takes the turn. */
931
+ #waiters = [];
932
+ /**
933
+ * Resolves once the caller holds the turn, or at once after `close`.
934
+ *
935
+ * Aborted by `signal` while it waits, it leaves the queue and rejects with the reason of the signal; given an aborted signal, it rejects without taking the turn.
936
+ */
937
+ take(signal) {
938
+ if (signal?.aborted) return Promise.reject(signal.reason);
939
+ if (this.#closed) return Promise.resolve();
940
+ if (!this.#held) {
941
+ this.#held = true;
942
+ return Promise.resolve();
943
+ }
944
+ const { promise, resolve, reject } = Promise.withResolvers();
945
+ const onAbort = () => {
946
+ this.#waiters.splice(this.#waiters.indexOf(grant), 1);
947
+ reject(signal.reason);
948
+ };
949
+ const grant = () => {
950
+ signal?.removeEventListener("abort", onAbort);
951
+ resolve();
952
+ };
953
+ signal?.addEventListener("abort", onAbort, { once: true });
954
+ this.#waiters.push(grant);
955
+ return promise;
956
+ }
957
+ /** Takes the turn at once when it is free or the queue is closed, and tells whether it did. */
958
+ tryTake() {
959
+ if (this.#closed) return true;
960
+ if (this.#held) return false;
961
+ this.#held = true;
962
+ return true;
963
+ }
964
+ /** Passes the turn of the caller to the next waiter, or frees it. */
965
+ release() {
966
+ if (this.#closed) return;
967
+ const next = this.#waiters.shift();
968
+ if (next) next();
969
+ else this.#held = false;
970
+ }
971
+ /** Grants the turn to every waiter and to every later caller. */
972
+ close() {
973
+ this.#closed = true;
974
+ for (const grant of this.#waiters.splice(0)) grant();
975
+ }
976
+ };
977
+ /**
978
+ * Runs `start` on its turn on the connection of a level of the test helpers `level`, and passes the turn on once it settles, after the level it started has ended.
979
+ *
980
+ * The code of the level runs statements and transactions at once on its connection, such as the requests of an open test transaction, where PostgreSQL would run a statement inside a nested transaction of other code, so they take turns rather than reject as in a transaction of `transaction`.
981
+ * It rejects without calling `start` when the level accepts no statements on its turn, and with the reason of `signal` when it aborts first.
982
+ */
983
+ async function runInTurn(level, operation, start, signal) {
984
+ const turns = level.turns;
985
+ if (!turns.tryTake()) await turns.take(signal);
986
+ try {
987
+ signal?.throwIfAborted();
988
+ const unavailable = createUnavailableError(level, operation);
989
+ if (unavailable) throw unavailable;
990
+ return await start();
991
+ } finally {
992
+ turns.release();
993
+ }
994
+ }
995
+ /**
996
+ * Runs a statement of `run` on its turn in a savepoint of its own in a level of the test helpers `level`, released when it succeeds and rolled back when it fails.
997
+ *
998
+ * A statement outside a transaction commits or fails alone in production, so its failure must not abort the transaction of the test.
999
+ * A statement sent before the level ends settles with its own result, as on the connection of a transaction: the rollback of the level undoes it, and reports the failures of that rollback.
1000
+ */
1001
+ function runStatementInTurn(level, operation, run) {
1002
+ return runInTurn(level, operation, async () => {
1003
+ const unit = createLevel(level.connection, level, false);
1004
+ level.child = unit;
1005
+ const { connection } = unit;
1006
+ const savepoint = connection.execute(createStatement(`SAVEPOINT ${getSavepointName(unit)}`));
1007
+ unit.state = "open";
1008
+ const running = run(connection);
1009
+ try {
1010
+ await savepoint;
1011
+ } catch (error) {
1012
+ await running.catch(() => {});
1013
+ if (unit.cleanup) throw combineRejections({ error }, await unit.cleanup).error;
1014
+ level.rollbackOnly ??= { error };
1015
+ closeStarting(unit);
1016
+ throw error;
1017
+ }
1018
+ let result;
1019
+ try {
1020
+ result = await running;
1021
+ } catch (error) {
1022
+ const cleanup = await endLevel(unit, false);
1023
+ throw unit.endedBy ? error : combineRejections({ error }, cleanup).error;
1024
+ }
1025
+ const cleanup = await endLevel(unit, true);
1026
+ if (cleanup.rejection && !unit.endedBy) throw cleanup.rejection.error;
1027
+ return result;
1028
+ });
1029
+ }
1030
+ // src/transaction/test-levels/fixture-stack.ts
1031
+ /** Open test fixtures of a transaction coordinator and the queue of the changes of their stack. */
1032
+ var TestFixtureStack = class {
1033
+ #driver;
1034
+ #onReleaseError;
1035
+ /** Async context of the levels of the coordinator, which tells whether a call runs inside a level. */
1036
+ #storage;
1037
+ /** Open test fixtures, the innermost last. */
1038
+ #fixtures = [];
1039
+ /** Changes of the fixture stack, run one after another so that each one sees the stack the previous one left. */
1040
+ #transitions = Promise.resolve();
1041
+ /**
1042
+ * The `close` functions of the test fixtures that are still opening, from the call that starts the opening until the fixture enters the stack or the opening ends otherwise.
1043
+ *
1044
+ * An opening holds the queue of the fixture stack while its seed runs, so a closing of the stack queued behind it would wait for a seed that may never settle.
1045
+ * Closing the stack therefore calls these first: each one tells its opening to stop waiting for the seed.
1046
+ */
1047
+ #openings = /* @__PURE__ */ new Set();
1048
+ /**
1049
+ * Outermost fixture in the stack whose closing is queued but has not finished.
1050
+ *
1051
+ * Openings that start after that closing is requested nest outside of the fixture once it closes, so a repeated closing of it, or a closing of a fixture nested in it, must not close them.
1052
+ */
1053
+ #closing;
1054
+ constructor(driver, storage, onReleaseError) {
1055
+ this.#driver = driver;
1056
+ this.#storage = storage;
1057
+ this.#onReleaseError = onReleaseError;
1058
+ }
1059
+ /**
1060
+ * Starts opening a test fixture with `options`, see `startTestFixture` of the coordinator, or, given `refusal`, returns a fixture that rejects with it.
1061
+ *
1062
+ * The `close` of a refused fixture resolves once the earlier changes of the stack end.
1063
+ */
1064
+ start(seed, refusal, options) {
1065
+ let level;
1066
+ let closeRequested = false;
1067
+ const closing = Promise.withResolvers();
1068
+ let closedRejection;
1069
+ const close = () => {
1070
+ if (this.#storage.getStore()) return Promise.reject(/* @__PURE__ */ new Error("Cannot close a test fixture inside a transaction"));
1071
+ closeRequested = true;
1072
+ closing.resolve();
1073
+ if (level && this.#fixtures.includes(level)) {
1074
+ const opened = level;
1075
+ if (this.#closing && this.#closing.depth <= opened.depth) return this.#enqueue(() => this.#closeFixture(opened));
1076
+ this.#closing = opened;
1077
+ return this.#closeAfterOpenings(() => this.#enqueue(async () => {
1078
+ try {
1079
+ await this.#closeFixture(opened);
1080
+ } finally {
1081
+ if (this.#closing === opened) this.#closing = void 0;
1082
+ }
1083
+ }));
1084
+ }
1085
+ return this.#enqueue(() => {
1086
+ const rejection = closedRejection;
1087
+ closedRejection = void 0;
1088
+ if (rejection) throw rejection.error;
1089
+ });
1090
+ };
1091
+ if (refusal) return {
1092
+ fixture: Promise.reject(refusal),
1093
+ close
1094
+ };
1095
+ /**
1096
+ * Runs stages 2 and 3 of the fixture and puts it in the stack, returning `undefined` when closing was requested and the fixture rolled back cleanly.
1097
+ *
1098
+ * It rejects with a failure to start the fixture, with the rejection of `seed`, or with a failure to roll back, which suppresses the rejection of `seed`.
1099
+ */
1100
+ const open = async () => {
1101
+ if (closeRequested) return;
1102
+ const parent = this.#fixtures.at(-1);
1103
+ const started = parent ? await this.#startInFixture(parent, options) : await this.#beginFixture(options?.isolation);
1104
+ started.state = "open";
1105
+ const outcome = closeRequested ? void 0 : await Promise.race([this.#seedFixture(started, seed), closing.promise]);
1106
+ if (outcome && !outcome.rejection && !closeRequested) {
1107
+ this.#fixtures.push(started);
1108
+ level = started;
1109
+ this.#openings.delete(close);
1110
+ const { result } = outcome;
1111
+ return {
1112
+ get data() {
1113
+ if (started.state !== "open") throw new Error("Test fixture is closed");
1114
+ return result;
1115
+ },
1116
+ close,
1117
+ [Symbol.asyncDispose]: close
1118
+ };
1119
+ }
1120
+ const rejection = combineRejections(outcome?.rejection, { rejection: await this.#endFixture(started) });
1121
+ if (rejection) throw rejection.error;
1122
+ };
1123
+ this.#openings.add(close);
1124
+ return {
1125
+ fixture: this.#enqueue(async () => {
1126
+ let opened;
1127
+ try {
1128
+ opened = await open();
1129
+ } catch (error) {
1130
+ if (!closeRequested) throw error;
1131
+ closedRejection = { error };
1132
+ } finally {
1133
+ this.#openings.delete(close);
1134
+ }
1135
+ if (!opened) throw new Error("Test fixture was closed before it opened");
1136
+ return opened;
1137
+ }),
1138
+ close
1139
+ };
1140
+ }
1141
+ /**
1142
+ * Starts the level of a test transaction in the innermost test fixture as a change of the fixture stack, resolving to `undefined` when no fixture is open.
1143
+ *
1144
+ * Aborted while it waits for its turn, it rejects at once and its change starts nothing, so that a test that has already ended never preempts the active level of the fixture.
1145
+ * From its turn on, it settles with its change.
1146
+ */
1147
+ startTestLevel(options) {
1148
+ const signal = options?.signal;
1149
+ let waiting = true;
1150
+ const start = () => this.#enqueue(() => {
1151
+ if (signal?.aborted) return;
1152
+ waiting = false;
1153
+ const fixture = this.#fixtures.at(-1);
1154
+ return fixture && this.#startInFixture(fixture, options);
1155
+ });
1156
+ return signal ? settleUnlessAborted(start, signal, () => waiting) : start();
1157
+ }
1158
+ /**
1159
+ * Closes every fixture, including those still opening, rejecting with the failures of all of them.
1160
+ *
1161
+ * Unlike closing a fixture, it closes every opening, including one started after a closing of the stack was requested, since nothing may stay open.
1162
+ */
1163
+ close() {
1164
+ return this.#closeAfterOpenings(() => this.#enqueue(() => {
1165
+ const root = this.#fixtures[0];
1166
+ return root && this.#closeFixture(root);
1167
+ }));
1168
+ }
1169
+ /**
1170
+ * Closes the fixtures still opening and runs `closeStack`, rejecting with the failures of all of them.
1171
+ *
1172
+ * A seed that never settles holds the queue of the fixture stack, so closing is requested for every opening before `closeStack` queues its change, and the rollback does not wait for the seeds.
1173
+ */
1174
+ async #closeAfterOpenings(closeStack) {
1175
+ const closings = [];
1176
+ for (const closeOpening of this.#openings) closings.push(closeOpening());
1177
+ closings.push(closeStack());
1178
+ let rejection;
1179
+ for (const closing of await Promise.allSettled(closings)) if (closing.status === "rejected") rejection = suppress(closing.reason, rejection, "Closing a test fixture failed");
1180
+ if (rejection) throw rejection.error;
1181
+ }
1182
+ /**
1183
+ * Runs `seed` in a savepoint of a fixture that is released when it resolves, returning its result or rejection.
1184
+ *
1185
+ * The savepoint has its own context, so late code of `seed` cannot reach the fixture after it ends.
1186
+ * Its statements and transactions take turns, as in a test transaction.
1187
+ */
1188
+ async #seedFixture(fixture, seed) {
1189
+ try {
1190
+ const level = await startNestedLevel(fixture, false);
1191
+ level.turns = new TurnQueue();
1192
+ return await runLevel(this.#storage, level, seed, true);
1193
+ } catch (error) {
1194
+ return { rejection: { error } };
1195
+ }
1196
+ }
1197
+ /** Closes an opened fixture as a change of the fixture stack. */
1198
+ async #closeFixture(fixture) {
1199
+ const index = this.#fixtures.indexOf(fixture);
1200
+ if (index < 0) return;
1201
+ let rejection;
1202
+ try {
1203
+ rejection = await this.#endFixture(fixture);
1204
+ } finally {
1205
+ this.#fixtures.length = index;
1206
+ }
1207
+ if (rejection) throw rejection.error;
1208
+ }
1209
+ /** Rolls back a fixture and releases the connection of the first one, returning the error to reject with. */
1210
+ async #endFixture(fixture) {
1211
+ const { rejection } = await endLevel(fixture, false);
1212
+ if (!fixture.parent) await releaseConnection(fixture.connection, this.#onReleaseError);
1213
+ return rejection;
1214
+ }
1215
+ /** Runs a change of the fixture stack after the earlier ones. */
1216
+ #enqueue(transition) {
1217
+ const result = this.#transitions.then(transition);
1218
+ this.#transitions = result.catch(() => {});
1219
+ return result;
1220
+ }
1221
+ /** Starts the first test fixture: a transaction at `isolation` on a reserved connection. */
1222
+ async #beginFixture(isolation) {
1223
+ const connection = await this.#driver.acquire();
1224
+ try {
1225
+ await connection.execute(getBeginStatement(isolation));
1226
+ } catch (error) {
1227
+ await releaseConnection(connection, this.#onReleaseError);
1228
+ throw error;
1229
+ }
1230
+ return createLevel(connection, void 0, false, isolation);
1231
+ }
1232
+ /**
1233
+ * Starts a nested level of a test fixture with `options`, preempting its active one.
1234
+ *
1235
+ * A fixture runs one nested level at a time, so a new one ends the previous one, such as a test that timed out.
1236
+ * A level whose `signal` aborts while it preempts rejects with its reason instead of starting, and a failure to start after the abort suppresses the reason.
1237
+ */
1238
+ async #startInFixture(fixture, options) {
1239
+ const signal = options?.signal;
1240
+ if (fixture.child) {
1241
+ const cleanup = await endLevel(fixture.child, false, "preemption");
1242
+ if (cleanup.rejection) throw suppressAbortReason(new Error("Cannot start a level of the test fixture: ending its active level failed", { cause: cleanup.rejection.error }), signal);
1243
+ }
1244
+ const unavailable = createUnavailableError(fixture, "start a level of the test fixture");
1245
+ if (unavailable) throw suppressAbortReason(unavailable, signal);
1246
+ signal?.throwIfAborted();
1247
+ return startNestedLevel(fixture, false, options);
1248
+ }
1249
+ };
1250
+ // src/transaction/test-levels/open-test-transaction.ts
1251
+ /** Opens the test transactions of a transaction coordinator, and closes those on connections of their own when the database closes. */
1252
+ var OpenTestTransactions = class {
1253
+ #driver;
1254
+ #storage;
1255
+ #fixtures;
1256
+ #onReleaseError;
1257
+ /** The `close` functions of the open test transactions on connections of their own; those in a fixture close with it. */
1258
+ #roots = /* @__PURE__ */ new Set();
1259
+ constructor(driver, storage, fixtures, onReleaseError) {
1260
+ this.#driver = driver;
1261
+ this.#storage = storage;
1262
+ this.#fixtures = fixtures;
1263
+ this.#onReleaseError = onReleaseError;
1264
+ }
1265
+ /**
1266
+ * Opens a test transaction: a savepoint in the innermost open fixture, which preempts its active level, or a transaction on a connection of its own.
1267
+ *
1268
+ * Its statements outside a transaction and its nested levels take turns on its connection, see `runInTurn`.
1269
+ * `getClosedError` returns the error of an opening once closing the database is requested; an opening on a connection of its own that the closing overtakes rolls back and rejects with it, since the closing no longer sees it.
1270
+ */
1271
+ async open(options, getClosedError) {
1272
+ const nested = await this.#fixtures.startTestLevel(options);
1273
+ const level = nested ?? await this.#begin(options?.isolation, getClosedError);
1274
+ if (nested?.cleanup) {
1275
+ const { rejection } = await getEndedOutcome(nested.cleanup, /* @__PURE__ */ new Error("Cannot open a test transaction: its test fixture ended it before it opened"));
1276
+ throw rejection.error;
1277
+ }
1278
+ level.state = "open";
1279
+ level.turns = new TurnQueue();
1280
+ let closing;
1281
+ const close = () => {
1282
+ closing ??= (async () => {
1283
+ const { rejection } = await endLevel(level, false);
1284
+ if (!nested) {
1285
+ this.#roots.delete(close);
1286
+ await releaseConnection(level.connection, this.#onReleaseError);
1287
+ }
1288
+ if (rejection) throw rejection.error;
1289
+ })();
1290
+ return closing;
1291
+ };
1292
+ if (!nested) {
1293
+ const closedError = getClosedError();
1294
+ if (closedError) {
1295
+ let rejection = { error: closedError };
1296
+ try {
1297
+ await close();
1298
+ } catch (error) {
1299
+ rejection = suppress(error, rejection, "Transaction rollback failed");
1300
+ }
1301
+ throw rejection.error;
1302
+ }
1303
+ this.#roots.add(close);
1304
+ }
1305
+ return {
1306
+ run: async (callback) => {
1307
+ if (level.state !== "open") throw new Error("Cannot run a callback in the test transaction: it is already closed");
1308
+ const current = this.#storage.getStore();
1309
+ if (current === level) return callback();
1310
+ if (current) throw new Error("Cannot run a callback in an open test transaction inside a transaction");
1311
+ return this.#storage.run(level, callback);
1312
+ },
1313
+ close,
1314
+ [Symbol.asyncDispose]: close
1315
+ };
1316
+ }
1317
+ /** Rolls back the open test transactions on connections of their own, rejecting with the failures of all of them. */
1318
+ async close() {
1319
+ const closings = [];
1320
+ for (const close of this.#roots) closings.push(close());
1321
+ let rejection;
1322
+ for (const closing of await Promise.allSettled(closings)) if (closing.status === "rejected") rejection = suppress(closing.reason, rejection, "Closing a test transaction failed");
1323
+ if (rejection) throw rejection.error;
1324
+ }
1325
+ /**
1326
+ * Begins an open test transaction outside a fixture at `isolation` on a connection of its own.
1327
+ *
1328
+ * Once closing the database is requested, it releases the connection instead of sending `BEGIN`, and rejects with the error of `getClosedError`.
1329
+ */
1330
+ async #begin(isolation, getClosedError) {
1331
+ const closedError = getClosedError();
1332
+ if (closedError) throw closedError;
1333
+ const connection = await this.#driver.acquire();
1334
+ const lateError = getClosedError();
1335
+ if (lateError) {
1336
+ await releaseConnection(connection, this.#onReleaseError);
1337
+ throw lateError;
1338
+ }
1339
+ const level = createLevel(connection, void 0, false, isolation);
1340
+ try {
1341
+ await connection.execute(getBeginStatement(isolation));
1342
+ } catch (error) {
1343
+ closeStarting(level);
1344
+ await releaseConnection(connection, this.#onReleaseError);
1345
+ throw error;
1346
+ }
1347
+ return level;
1348
+ }
1349
+ };
1124
1350
  // src/transaction/coordinator.ts
1125
1351
  /**
1126
1352
  * Routes statements and scripts to the ambient transaction or session of the current async context or to the pool.
@@ -1134,6 +1360,7 @@ var TransactionCoordinator = class {
1134
1360
  #onReleaseError;
1135
1361
  /** Closing of the database, set by the first `close`; from then on, operations outside a transaction reject. */
1136
1362
  #closed;
1363
+ #openTestTransactions;
1137
1364
  /** Rejects statements and transactions outside a test transaction or the seed of a test fixture instead of running them on the pool. */
1138
1365
  test;
1139
1366
  constructor(driver, { test = false, onReleaseError } = {}) {
@@ -1141,19 +1368,23 @@ var TransactionCoordinator = class {
1141
1368
  this.test = test;
1142
1369
  this.#onReleaseError = onReleaseError;
1143
1370
  this.#fixtures = new TestFixtureStack(driver, this.#storage, onReleaseError);
1371
+ this.#openTestTransactions = new OpenTestTransactions(driver, this.#storage, this.#fixtures, onReleaseError);
1144
1372
  }
1145
1373
  execute(statement) {
1146
- const target = this.#route("execute a query");
1374
+ const level = this.#storage.getStore();
1375
+ if (level?.turns) return runStatementInTurn(level, "execute a query", (connection) => connection.execute(statement));
1376
+ const target = this.#route(level, "execute a query");
1147
1377
  return target instanceof Error ? Promise.reject(target) : target.execute(statement);
1148
1378
  }
1149
1379
  /** Executes a script of statements without parameters where `execute` executes a statement. */
1150
1380
  executeScript(text) {
1151
- const target = this.#route("execute a script");
1381
+ const level = this.#storage.getStore();
1382
+ if (level?.turns) return runStatementInTurn(level, "execute a script", (connection) => connection.executeScript(text));
1383
+ const target = this.#route(level, "execute a script");
1152
1384
  return target instanceof Error ? Promise.reject(target) : target.executeScript(text);
1153
1385
  }
1154
- /** Returns the connection of the ambient transaction or session, or the driver of the pool, that runs `operation`, or the error that rejects it. */
1155
- #route(operation) {
1156
- const level = this.#storage.getStore();
1386
+ /** Returns the connection of the ambient transaction or session `level`, or the driver of the pool, that runs `operation`, or the error that rejects it. */
1387
+ #route(level, operation) {
1157
1388
  if (!level) {
1158
1389
  if (this.#closed) return createDatabaseClosedError(operation);
1159
1390
  return this.test ? new Error(describeOutsideTest(operation)) : this.driver;
@@ -1262,6 +1493,7 @@ var TransactionCoordinator = class {
1262
1493
  * Runs `callback` in a transaction that always rolls back, on a coordinator created for tests.
1263
1494
  *
1264
1495
  * Inside a transaction, it runs `callback` in a savepoint nested in it that always rolls back.
1496
+ * Outside a transaction of `transaction`, the statements and nested levels of `callback` take turns on its connection, each statement in a savepoint of its own, see `runInTurn`; when `callback` resolves, the level ends after the statements and nested levels it started.
1265
1497
  * When `signal` aborts, the level stops accepting statements at once and ends without waiting for `callback`.
1266
1498
  * Once the level ends, the transaction rejects with the reason of the signal.
1267
1499
  * A failure to start or end the level after the abort suppresses the reason in a `SuppressedError`.
@@ -1274,7 +1506,24 @@ var TransactionCoordinator = class {
1274
1506
  const parent = this.#storage.getStore();
1275
1507
  if (parent) return this.#runNested(parent, callback, false, options);
1276
1508
  if (this.#closed) return Promise.reject(createDatabaseClosedError("start a test transaction"));
1277
- return this.#fixtures.startTestLevel(options).then((level) => level ? this.#runStarted(level, callback, false, signal) : this.#runRoot(callback, false, options));
1509
+ return this.#fixtures.startTestLevel(options).then((level) => {
1510
+ if (!level) return this.#runRoot(callback, false, options);
1511
+ level.turns = new TurnQueue();
1512
+ return runStarted(this.#storage, level, callback, false, signal);
1513
+ });
1514
+ }
1515
+ /**
1516
+ * Opens a test transaction that the callbacks of `run` share until it closes, see `OpenTestTransactions`.
1517
+ *
1518
+ * It rejects inside a transaction, and once closing the database is requested.
1519
+ */
1520
+ async openTestTransaction(options) {
1521
+ if (!this.test) throw new Error("openTestTransaction requires a database created with test: true");
1522
+ if (this.#storage.getStore()) throw new Error("Cannot open a test transaction inside a transaction");
1523
+ const getClosedError = () => this.#closed ? createDatabaseClosedError("open a test transaction") : void 0;
1524
+ const closedError = getClosedError();
1525
+ if (closedError) throw closedError;
1526
+ return this.#openTestTransactions.open(options, getClosedError);
1278
1527
  }
1279
1528
  /**
1280
1529
  * Opens a test fixture: a transaction on a reserved connection, or a savepoint in the innermost open fixture, and runs `seed` in it.
@@ -1296,15 +1545,15 @@ var TransactionCoordinator = class {
1296
1545
  }
1297
1546
  /** Returns the error that rejects a test fixture created now, or `undefined` when it can open. */
1298
1547
  #getFixtureRefusal() {
1299
- if (!this.test) return /* @__PURE__ */ new Error("createTestFixture requires a database created with test: true");
1548
+ if (!this.test) return /* @__PURE__ */ new Error("openTestFixture requires a database created with test: true");
1300
1549
  if (this.#storage.getStore()) return /* @__PURE__ */ new Error("Cannot create a test fixture inside a transaction");
1301
1550
  if (this.#closed) return createDatabaseClosedError("create a test fixture");
1302
1551
  }
1303
1552
  /**
1304
- * Closes the test fixtures, including those still opening, and then the driver, which waits for the running transactions and sessions to end.
1553
+ * Closes the test fixtures, including those still opening, and the open test transactions on connections of their own, and then the driver, which waits for the running transactions and sessions to end.
1305
1554
  *
1306
- * A driver waits for every reserved connection, so closing it under an open fixture would wait forever.
1307
- * A failed closing of the driver suppresses the failures of closing the fixtures.
1555
+ * A driver waits for every reserved connection, so closing it under an open fixture or test transaction would wait forever.
1556
+ * A failed closing of the driver suppresses the failures of closing the fixtures and test transactions.
1308
1557
  * From the first call, statements, transactions, sessions, and fixtures started outside a transaction and a session reject, while the levels already running can still end; a repeated call returns the same closing.
1309
1558
  */
1310
1559
  close() {
@@ -1313,14 +1562,10 @@ var TransactionCoordinator = class {
1313
1562
  this.#closed ??= this.#close();
1314
1563
  return this.#closed;
1315
1564
  }
1316
- /** Closes the test fixtures and then the driver, see `close`. */
1565
+ /** Closes the test fixtures and the open test transactions on connections of their own, and then the driver, see `close`. */
1317
1566
  async #close() {
1318
1567
  let rejection;
1319
- try {
1320
- await this.#fixtures.close();
1321
- } catch (error) {
1322
- rejection = { error };
1323
- }
1568
+ for (const closing of await Promise.allSettled([this.#fixtures.close(), this.#openTestTransactions.close()])) if (closing.status === "rejected") rejection = suppress(closing.reason, rejection, "Closing a test fixture or transaction failed");
1324
1569
  try {
1325
1570
  await this.driver.close();
1326
1571
  } catch (error) {
@@ -1346,6 +1591,7 @@ var TransactionCoordinator = class {
1346
1591
  throw signal.reason;
1347
1592
  }
1348
1593
  const level = createLevel(connection, void 0, commits, options.isolation);
1594
+ if (!commits) level.turns = new TurnQueue();
1349
1595
  const outcome = await this.#runTransaction(level, callback, commits, options);
1350
1596
  await releaseConnection(connection, this.#onReleaseError);
1351
1597
  return settleTransaction(level, outcome);
@@ -1403,15 +1649,15 @@ var TransactionCoordinator = class {
1403
1649
  return outcome.result;
1404
1650
  }
1405
1651
  async #runNested(parent, callback, commits, options) {
1652
+ const start = async () => {
1653
+ const level = await startNestedLevel(parent, commits, options);
1654
+ if (!level.inTransaction) level.turns = new TurnQueue();
1655
+ return runStarted(this.#storage, level, callback, commits, options?.signal);
1656
+ };
1657
+ if (parent.turns) return runInTurn(parent, "start a nested transaction", start, options?.signal);
1406
1658
  const unavailable = createUnavailableError(parent, "start a nested transaction");
1407
1659
  if (unavailable) throw unavailable;
1408
- return this.#runStarted(await startNestedLevel(parent, commits, options), callback, commits, options?.signal);
1409
- }
1410
- /** Runs `callback` in a started level, returning its result or throwing its rejection. */
1411
- async #runStarted(level, callback, commits, signal) {
1412
- const { result, rejection } = await runLevel(this.#storage, level, callback, commits, signal);
1413
- if (rejection) throw rejection.error;
1414
- return result;
1660
+ return start();
1415
1661
  }
1416
1662
  };
1417
1663
  /**