@happyvertical/sql 0.80.3 → 0.80.4

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.js CHANGED
@@ -1963,6 +1963,9 @@ async function createDatabase(options, cacheKey) {
1963
1963
  database,
1964
1964
  max: poolMax
1965
1965
  });
1966
+ pool.on("error", (error) => {
1967
+ console.warn("Warning: PostgreSQL pool client error (connection discarded):", error instanceof Error ? error.message : String(error));
1968
+ });
1966
1969
  const originalEnd = pool.end.bind(pool);
1967
1970
  const evictFromCache = () => {
1968
1971
  for (const [key, cached] of connectionCache.entries()) if (cached.client === pool) connectionCache.delete(key);
@@ -2076,18 +2079,83 @@ async function createDatabase(options, cacheKey) {
2076
2079
  affected: (await executor.query(insertSql, values)).rowCount ?? 0
2077
2080
  };
2078
2081
  };
2079
- const executeNullAwarePostgresUpsertInTransaction = async (table, conflictColumns, serializedData) => {
2082
+ /**
2083
+ * Absorbs 'error' events for the clients currently checked out for a
2084
+ * transaction, keyed by client so the listener can be removed on release.
2085
+ */
2086
+ const txErrorAbsorbers = /* @__PURE__ */ new WeakMap();
2087
+ const releasedTxClients = /* @__PURE__ */ new WeakSet();
2088
+ /**
2089
+ * Checks out a pooled client for a transaction.
2090
+ *
2091
+ * pg-pool drops its own idle 'error' listener while a client is checked out,
2092
+ * so a backend that dies mid-transaction would emit an unhandled 'error' and
2093
+ * terminate the process. Absorb it here — the failure still reaches the
2094
+ * caller as a rejected query — and drop the listener again on release so
2095
+ * repeated checkouts of the same client cannot accumulate them.
2096
+ */
2097
+ const acquireTxClient = async () => {
2080
2098
  const txClient = await client.connect();
2099
+ releasedTxClients.delete(txClient);
2100
+ const absorb = () => {};
2101
+ txClient.on("error", absorb);
2102
+ txErrorAbsorbers.set(txClient, absorb);
2103
+ return txClient;
2104
+ };
2105
+ /**
2106
+ * Returns a transaction client to the pool exactly once.
2107
+ *
2108
+ * Every teardown path must run this, including the ones reached by a throwing
2109
+ * COMMIT or ROLLBACK — a client that is never released stays checked out for
2110
+ * the life of the process, and enough of them exhaust the pool. Releasing
2111
+ * twice throws, so the released clients are tracked rather than assumed.
2112
+ *
2113
+ * @param txClient - The pooled client backing a transaction
2114
+ * @param destroy - Destroy the connection instead of reusing it, for when the
2115
+ * transaction state could not be cleaned up
2116
+ */
2117
+ const releaseTxClient = (txClient, destroy = false) => {
2118
+ if (releasedTxClients.has(txClient)) return;
2119
+ releasedTxClients.add(txClient);
2120
+ const absorb = txErrorAbsorbers.get(txClient);
2121
+ if (absorb) {
2122
+ txClient.off("error", absorb);
2123
+ txErrorAbsorbers.delete(txClient);
2124
+ }
2125
+ txClient.release(destroy || void 0);
2126
+ };
2127
+ /**
2128
+ * Rolls back a failed transaction and returns its client to the pool.
2129
+ *
2130
+ * The connection may still be inside a transaction, so ROLLBACK is attempted
2131
+ * to normalize it. If that works the connection is clean and worth reusing;
2132
+ * if it does not, the state is unknown and the connection is destroyed rather
2133
+ * than handed to unrelated code later.
2134
+ *
2135
+ * @returns The rollback failure, if the connection could not be normalized
2136
+ */
2137
+ const discardTxClient = async (txClient) => {
2138
+ try {
2139
+ await txClient.query("ROLLBACK");
2140
+ releaseTxClient(txClient);
2141
+ return;
2142
+ } catch (rollbackError) {
2143
+ releaseTxClient(txClient, true);
2144
+ return rollbackError;
2145
+ }
2146
+ };
2147
+ const executeNullAwarePostgresUpsertInTransaction = async (table, conflictColumns, serializedData) => {
2148
+ const txClient = await acquireTxClient();
2081
2149
  try {
2082
2150
  await txClient.query("BEGIN");
2083
2151
  const result = await executeNullAwarePostgresUpsert(txClient, table, conflictColumns, serializedData);
2084
2152
  await txClient.query("COMMIT");
2153
+ releaseTxClient(txClient);
2085
2154
  return result;
2086
2155
  } catch (error) {
2087
- await txClient.query("ROLLBACK");
2156
+ const rollbackError = await discardTxClient(txClient);
2157
+ if (rollbackError !== void 0 && error instanceof Error) error.cause ??= rollbackError;
2088
2158
  throw error;
2089
- } finally {
2090
- txClient.release();
2091
2159
  }
2092
2160
  };
2093
2161
  const executePostgresUpsert = async (executor, table, conflictColumns, data, options, acquireTransaction) => {
@@ -2583,7 +2651,7 @@ async function createDatabase(options, cacheKey) {
2583
2651
  * @returns Promise resolving to callback result
2584
2652
  */
2585
2653
  const transaction = async (callback) => {
2586
- const txClient = await client.connect();
2654
+ const txClient = await acquireTxClient();
2587
2655
  try {
2588
2656
  await txClient.query("BEGIN");
2589
2657
  const txDb = {
@@ -2719,12 +2787,12 @@ async function createDatabase(options, cacheKey) {
2719
2787
  };
2720
2788
  const result = await callback(txDb);
2721
2789
  await txClient.query("COMMIT");
2790
+ releaseTxClient(txClient);
2722
2791
  return result;
2723
2792
  } catch (error) {
2724
- await txClient.query("ROLLBACK");
2793
+ const rollbackError = await discardTxClient(txClient);
2794
+ if (rollbackError !== void 0 && error instanceof Error) error.cause ??= rollbackError;
2725
2795
  throw error;
2726
- } finally {
2727
- txClient.release();
2728
2796
  }
2729
2797
  };
2730
2798
  /**
@@ -2736,21 +2804,29 @@ async function createDatabase(options, cacheKey) {
2736
2804
  * @returns Promise resolving to a TransactionHandle
2737
2805
  */
2738
2806
  const beginTransaction = async () => {
2739
- const txClient = await client.connect();
2740
- await txClient.query("BEGIN");
2807
+ const txClient = await acquireTxClient();
2808
+ try {
2809
+ await txClient.query("BEGIN");
2810
+ } catch (error) {
2811
+ releaseTxClient(txClient, true);
2812
+ throw error;
2813
+ }
2741
2814
  let active = true;
2742
- const commit = async () => {
2815
+ const end = async (command) => {
2743
2816
  if (!active) throw new DatabaseError("Transaction already ended", {});
2744
- await txClient.query("COMMIT");
2745
- active = false;
2746
- txClient.release();
2747
- };
2748
- const rollback = async () => {
2749
- if (!active) throw new DatabaseError("Transaction already ended", {});
2750
- await txClient.query("ROLLBACK");
2751
- active = false;
2752
- txClient.release();
2817
+ try {
2818
+ await txClient.query(command);
2819
+ active = false;
2820
+ releaseTxClient(txClient);
2821
+ } catch (error) {
2822
+ active = false;
2823
+ const rollbackError = await discardTxClient(txClient);
2824
+ if (rollbackError !== void 0 && error instanceof Error) error.cause ??= rollbackError;
2825
+ throw error;
2826
+ }
2753
2827
  };
2828
+ const commit = () => end("COMMIT");
2829
+ const rollback = () => end("ROLLBACK");
2754
2830
  const isActive = () => active;
2755
2831
  const txHandle = {
2756
2832
  url,