@coffre/db 0.1.9 → 0.1.11

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/connect.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { t as Database } from "./database-CWpo5aF0.js";
1
+ import { t as Database } from "./database-BhLco7OA.js";
2
2
  import { Engine } from "./dialect.js";
3
3
  import { ClientConfig } from "pg";
4
4
  //#region src/postgres.d.ts
@@ -23,6 +23,16 @@ type PoolLike = pg.Pool | {
23
23
  release: () => void;
24
24
  }>;
25
25
  };
26
+ /**
27
+ * A query made on the database from inside one of its own transactions,
28
+ * rather than through the transaction. With one connection per call, as on
29
+ * Workers, it would wait for the transaction, which waits for it: a call
30
+ * Cloudflare cancels as hung, and nothing says why. So it is refused at
31
+ * once, with this, wherever the database runs.
32
+ */
33
+ declare class QueryOutsideTransaction extends Error {
34
+ constructor();
35
+ }
26
36
  declare function createDatabase(pool: PoolLike): Database;
27
37
  /**
28
38
  * The tables of the database's own dialect: the Postgres ones on Postgres,
@@ -36,4 +46,4 @@ declare function tablesOf(db: Queryable): typeof schema_d_exports;
36
46
  */
37
47
  declare function own<T extends Table>(db: Queryable, table: T): T;
38
48
  //#endregion
39
- export { createDatabase as a, Transaction as i, PoolLike as n, own as o, Queryable as r, tablesOf as s, Database as t };
49
+ export { Transaction as a, tablesOf as c, Queryable as i, PoolLike as n, createDatabase as o, QueryOutsideTransaction as r, own as s, Database as t };
package/dist/dialect.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { r as Queryable } from "./database-CWpo5aF0.js";
1
+ import { i as Queryable } from "./database-BhLco7OA.js";
2
2
  import { SQL } from "drizzle-orm";
3
3
  import { PgTable } from "drizzle-orm/pg-core";
4
4
  //#region src/dialect.d.ts
@@ -1,4 +1,4 @@
1
- import { n as PoolLike } from "./database-CWpo5aF0.js";
1
+ import { n as PoolLike } from "./database-BhLco7OA.js";
2
2
  import pg from "pg";
3
3
  //#region src/hyperdrive.d.ts
4
4
  /** Postgres through a Hyperdrive binding: `postgres(env.HYPERDRIVE)`. */
@@ -13,20 +13,36 @@ export declare function postgres(hyperdrive: {
13
13
  readonly connectionString: string;
14
14
  }): PostgresDatabase;
15
15
  /**
16
- * Request-scoped node-postgres adapter for Hyperdrive.
16
+ * node-postgres for one call on Workers, through Hyperdrive: one client,
17
+ * connected on the first query, which every query and transaction of the
18
+ * call takes its turn on.
17
19
  *
18
- * Hyperdrive owns the durable origin pool. The Worker creates edge clients
19
- * only inside one invocation, which prevents I/O objects from leaking across
20
- * Worker request contexts while keeping the Pool shape Drizzle expects. The
21
- * class name must end in `Pool`: that is how Drizzle knows to check out a
22
- * client for each transaction.
20
+ * Hyperdrive keeps the pool to the database; a Worker's connections belong
21
+ * to the call that opened them, so each call opens its own. One, not one per
22
+ * query: on Cloudflare, a call that opened several Hyperdrive connections at
23
+ * once was now and then cancelled as hung, with nothing left pending, and
24
+ * every connection costs a round trip before the query can be sent.
25
+ *
26
+ * The turns keep a transaction whole: queries go to the client in the order
27
+ * they are asked, a transaction holds it from BEGIN until it is released,
28
+ * and a query asked meanwhile waits until then, so nothing runs inside a
29
+ * transaction it is not part of. So code inside a transaction must query
30
+ * through the transaction: a query on the database there would wait for the
31
+ * transaction to end, which waits for it. Neither the vault nor the server
32
+ * does.
33
+ *
34
+ * The class name must end in `Pool`: that is how Drizzle knows to check out
35
+ * a client for each transaction.
23
36
  */
24
37
  export declare class HyperdrivePool implements Exclude<PoolLike, pg.Pool> {
25
38
  #private;
26
39
  constructor(connectionString: string);
27
40
  readonly query: pg.Pool['query'];
41
+ /** The client, for a transaction: held from now until `release`. */
28
42
  connect(): Promise<Pick<pg.PoolClient, 'query'> & {
29
43
  release: () => void;
30
44
  }>;
45
+ /** Close the client once nothing waits on it: what a Worker hands `waitUntil` when the call is done. */
46
+ end(): Promise<void>;
31
47
  }
32
48
  //#endregion
@@ -10,44 +10,115 @@ function postgres(hyperdrive) {
10
10
  };
11
11
  }
12
12
  /**
13
- * Request-scoped node-postgres adapter for Hyperdrive.
13
+ * node-postgres for one call on Workers, through Hyperdrive: one client,
14
+ * connected on the first query, which every query and transaction of the
15
+ * call takes its turn on.
14
16
  *
15
- * Hyperdrive owns the durable origin pool. The Worker creates edge clients
16
- * only inside one invocation, which prevents I/O objects from leaking across
17
- * Worker request contexts while keeping the Pool shape Drizzle expects. The
18
- * class name must end in `Pool`: that is how Drizzle knows to check out a
19
- * client for each transaction.
17
+ * Hyperdrive keeps the pool to the database; a Worker's connections belong
18
+ * to the call that opened them, so each call opens its own. One, not one per
19
+ * query: on Cloudflare, a call that opened several Hyperdrive connections at
20
+ * once was now and then cancelled as hung, with nothing left pending, and
21
+ * every connection costs a round trip before the query can be sent.
22
+ *
23
+ * The turns keep a transaction whole: queries go to the client in the order
24
+ * they are asked, a transaction holds it from BEGIN until it is released,
25
+ * and a query asked meanwhile waits until then, so nothing runs inside a
26
+ * transaction it is not part of. So code inside a transaction must query
27
+ * through the transaction: a query on the database there would wait for the
28
+ * transaction to end, which waits for it. Neither the vault nor the server
29
+ * does.
30
+ *
31
+ * The class name must end in `Pool`: that is how Drizzle knows to check out
32
+ * a client for each transaction.
20
33
  */
21
34
  var HyperdrivePool = class {
22
35
  #connectionString;
36
+ /** The client, connecting or connected; null before the first query, and once it has failed. */
37
+ #client = null;
38
+ /** Whether a transaction holds the client. */
39
+ #held = false;
40
+ /** What waits for its turn, in the order it asked. */
41
+ #waiting = [];
42
+ /** Queries and transactions asked and not done: waiting for the client, their turn, or an answer. */
43
+ #busy = 0;
44
+ /** What waits for the client to be idle: `end`. */
45
+ #idle = [];
23
46
  constructor(connectionString) {
24
47
  if (connectionString.trim().length === 0) throw new Error("Hyperdrive returned an empty connection string");
25
48
  this.#connectionString = connectionString;
26
49
  }
27
50
  query = (async (...args) => {
28
- const client = await this.#connectClient();
51
+ this.#busy += 1;
29
52
  try {
30
- return await client.query(...args);
53
+ const client = await this.#connected();
54
+ return await new Promise((resolve, reject) => {
55
+ this.#turn(() => client.query(...args).then(resolve, reject));
56
+ });
31
57
  } finally {
32
- await client.end();
58
+ this.#done();
33
59
  }
34
60
  });
61
+ /** The client, for a transaction: held from now until `release`. */
35
62
  async connect() {
36
- const client = await this.#connectClient();
63
+ this.#busy += 1;
64
+ let client;
65
+ try {
66
+ client = await this.#connected();
67
+ } catch (error) {
68
+ this.#done();
69
+ throw error;
70
+ }
71
+ await new Promise((resolve) => this.#turn(() => {
72
+ this.#held = true;
73
+ resolve();
74
+ }));
37
75
  let released = false;
38
76
  return {
39
77
  query: client.query.bind(client),
40
78
  release: () => {
41
79
  if (released) return;
42
80
  released = true;
43
- client.end().catch(() => {});
81
+ this.#held = false;
82
+ this.#next();
83
+ this.#done();
44
84
  }
45
85
  };
46
86
  }
47
- async #connectClient() {
87
+ /** Close the client once nothing waits on it: what a Worker hands `waitUntil` when the call is done. */
88
+ async end() {
89
+ while (this.#busy > 0) await new Promise((resolve) => this.#idle.push(resolve));
90
+ const client = this.#client;
91
+ this.#client = null;
92
+ if (client === null) return;
93
+ await client.then((connected) => connected.end()).catch(() => {});
94
+ }
95
+ /** Run `go` now if nothing holds the client or waits before it, otherwise at its turn. */
96
+ #turn(go) {
97
+ if (!this.#held && this.#waiting.length === 0) go();
98
+ else this.#waiting.push(go);
99
+ }
100
+ /** Give the next turns: queries until one is a transaction, which holds the client. */
101
+ #next() {
102
+ while (!this.#held && this.#waiting.length > 0) this.#waiting.shift()();
103
+ }
104
+ /** One query or transaction done: once none is left, what waits for the client to be idle goes on. */
105
+ #done() {
106
+ this.#busy -= 1;
107
+ if (this.#busy === 0) for (const resolve of this.#idle.splice(0)) resolve();
108
+ }
109
+ /** The call's client, connecting it the first time. One that fails is dropped: the next query connects again. */
110
+ #connected() {
111
+ if (this.#client !== null) return this.#client;
48
112
  const client = new pg.Client(postgresConnection(this.#connectionString));
49
- await client.connect();
50
- return client;
113
+ const connecting = client.connect().then(() => client);
114
+ const drop = () => {
115
+ if (this.#client === connecting) this.#client = null;
116
+ };
117
+ client.on("error", drop);
118
+ client.on("end", drop);
119
+ connecting.catch(drop);
120
+ this.#client = connecting;
121
+ return connecting;
51
122
  }
52
123
  };
53
124
  //#endregion
package/dist/index.d.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  import { l as schema_d_exports } from "./schema-Cd4WOnB5.js";
2
- import { a as createDatabase, i as Transaction, n as PoolLike, o as own, r as Queryable, s as tablesOf, t as Database } from "./database-CWpo5aF0.js";
3
- export { Database, PoolLike, Queryable, Transaction, createDatabase, own, schema_d_exports as schema, tablesOf };
2
+ import { a as Transaction, c as tablesOf, i as Queryable, n as PoolLike, o as createDatabase, r as QueryOutsideTransaction, s as own, t as Database } from "./database-BhLco7OA.js";
3
+ export { Database, PoolLike, QueryOutsideTransaction, Queryable, Transaction, createDatabase, own, schema_d_exports as schema, tablesOf };
package/dist/index.js CHANGED
@@ -1,10 +1,62 @@
1
1
  import { n as trackCommits } from "./commits-BuW-NL3n.js";
2
2
  import { t as schema_exports } from "./schema.js";
3
+ import { AsyncLocalStorage } from "node:async_hooks";
3
4
  import { Table, getTableName, is } from "drizzle-orm";
4
5
  import { drizzle } from "drizzle-orm/node-postgres";
5
6
  //#region src/database.ts
7
+ /**
8
+ * A query made on the database from inside one of its own transactions,
9
+ * rather than through the transaction. With one connection per call, as on
10
+ * Workers, it would wait for the transaction, which waits for it: a call
11
+ * Cloudflare cancels as hung, and nothing says why. So it is refused at
12
+ * once, with this, wherever the database runs.
13
+ */
14
+ var QueryOutsideTransaction = class extends Error {
15
+ constructor() {
16
+ super("query outside its transaction: use tx. A query on the database inside one of its own transactions waits for that transaction, which waits for it");
17
+ this.name = "QueryOutsideTransaction";
18
+ }
19
+ };
20
+ /** The transaction a flow of work runs inside, with the pool it holds, while it is open. */
21
+ const inside = new AsyncLocalStorage();
22
+ /**
23
+ * `pool`, refusing a query or a transaction asked from inside a transaction
24
+ * it is already running. Work from any other flow, a concurrent request's,
25
+ * or what the transaction left running after it ended, goes on as before.
26
+ */
27
+ function guarded(pool) {
28
+ return new Proxy(pool, { get(target, key) {
29
+ const value = Reflect.get(target, key, target);
30
+ if (typeof value !== "function") return value;
31
+ if (key !== "query" && key !== "connect") return value.bind(target);
32
+ return (...args) => {
33
+ const open = inside.getStore();
34
+ if (open?.open === true && open.pool === target) throw new QueryOutsideTransaction();
35
+ return value.apply(target, args);
36
+ };
37
+ } });
38
+ }
39
+ /** The refusal `error` is, or was caused by. */
40
+ function brokenRule(error) {
41
+ for (let cause = error; cause instanceof Error; cause = cause.cause) if (cause instanceof QueryOutsideTransaction) return cause;
42
+ }
6
43
  function createDatabase(pool) {
7
- return trackCommits(drizzle(pool, { schema: schema_exports }));
44
+ const db = drizzle(guarded(pool), { schema: schema_exports });
45
+ const run = db.transaction.bind(db);
46
+ db.transaction = (async (work, config) => {
47
+ const open = {
48
+ pool,
49
+ open: true
50
+ };
51
+ try {
52
+ return await run((tx) => inside.run(open, () => work(tx)), config);
53
+ } catch (error) {
54
+ throw brokenRule(error) ?? error;
55
+ } finally {
56
+ open.open = false;
57
+ }
58
+ });
59
+ return trackCommits(db);
8
60
  }
9
61
  /**
10
62
  * The tables of the database's own dialect: the Postgres ones on Postgres,
@@ -31,4 +83,4 @@ function own(db, table) {
31
83
  return found;
32
84
  }
33
85
  //#endregion
34
- export { createDatabase, own, schema_exports as schema, tablesOf };
86
+ export { QueryOutsideTransaction, createDatabase, own, schema_exports as schema, tablesOf };
package/dist/log.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { i as Transaction } from "./database-CWpo5aF0.js";
1
+ import { a as Transaction } from "./database-BhLco7OA.js";
2
2
  import { LogKey } from "@coffre/core/audit";
3
3
  //#region src/log.d.ts
4
4
  /**
@@ -1,5 +1,5 @@
1
1
  import { l as schema_d_exports } from "./schema-Cd4WOnB5.js";
2
- import { t as Database } from "./database-CWpo5aF0.js";
2
+ import { t as Database } from "./database-BhLco7OA.js";
3
3
  import { t as schema_sqlite_d_exports } from "./schema-sqlite.js";
4
4
  import { LibSQLDatabase } from "drizzle-orm/libsql";
5
5
  //#region src/portable.d.ts
@@ -1,4 +1,4 @@
1
- import { r as Queryable } from "./database-CWpo5aF0.js";
1
+ import { i as Queryable } from "./database-BhLco7OA.js";
2
2
  import { Engine } from "./dialect.js";
3
3
  //#region src/schema-version.d.ts
4
4
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coffre/db",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
4
4
  "description": "coffre's database: the schema, its migrations and the connections, on Postgres or SQLite.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -74,7 +74,7 @@
74
74
  "drizzle-orm": "0.45.2",
75
75
  "pg": "8.16.3",
76
76
  "pg-connection-string": "2.14.0",
77
- "@coffre/core": "0.1.9"
77
+ "@coffre/core": "0.1.11"
78
78
  },
79
79
  "devDependencies": {
80
80
  "@types/node": "26.1.1",
package/src/database.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+
1
3
  import { getTableName, is, Table } from 'drizzle-orm';
2
4
  import { drizzle, type NodePgDatabase } from 'drizzle-orm/node-postgres';
3
5
  import type pg from 'pg';
@@ -27,8 +29,67 @@ export type PoolLike = pg.Pool | {
27
29
  connect: () => Promise<Pick<pg.PoolClient, 'query'> & { release: () => void }>;
28
30
  };
29
31
 
32
+ /**
33
+ * A query made on the database from inside one of its own transactions,
34
+ * rather than through the transaction. With one connection per call, as on
35
+ * Workers, it would wait for the transaction, which waits for it: a call
36
+ * Cloudflare cancels as hung, and nothing says why. So it is refused at
37
+ * once, with this, wherever the database runs.
38
+ */
39
+ export class QueryOutsideTransaction extends Error {
40
+ constructor() {
41
+ super('query outside its transaction: use tx. A query on the database inside one of its own transactions waits for that transaction, which waits for it');
42
+ this.name = 'QueryOutsideTransaction';
43
+ }
44
+ }
45
+
46
+ /** The transaction a flow of work runs inside, with the pool it holds, while it is open. */
47
+ const inside = new AsyncLocalStorage<{ pool: PoolLike; open: boolean }>();
48
+
49
+ /**
50
+ * `pool`, refusing a query or a transaction asked from inside a transaction
51
+ * it is already running. Work from any other flow, a concurrent request's,
52
+ * or what the transaction left running after it ended, goes on as before.
53
+ */
54
+ function guarded(pool: PoolLike): PoolLike {
55
+ return new Proxy(pool, {
56
+ get(target, key) {
57
+ const value: unknown = Reflect.get(target, key, target);
58
+ if (typeof value !== 'function') return value;
59
+ if (key !== 'query' && key !== 'connect') return value.bind(target);
60
+ return (...args: unknown[]) => {
61
+ const open = inside.getStore();
62
+ if (open?.open === true && open.pool === target) throw new QueryOutsideTransaction();
63
+ return (value as (...args: unknown[]) => unknown).apply(target, args);
64
+ };
65
+ },
66
+ });
67
+ }
68
+
69
+ /** The refusal `error` is, or was caused by. */
70
+ function brokenRule(error: unknown): QueryOutsideTransaction | undefined {
71
+ for (let cause = error; cause instanceof Error; cause = cause.cause) {
72
+ if (cause instanceof QueryOutsideTransaction) return cause;
73
+ }
74
+ return undefined;
75
+ }
76
+
30
77
  export function createDatabase(pool: PoolLike): Database {
31
- return trackCommits(drizzle(pool as pg.Pool, { schema }));
78
+ const db = drizzle(guarded(pool) as pg.Pool, { schema });
79
+ // Each transaction's work runs knowing which one it is in, until the transaction ends.
80
+ const run = db.transaction.bind(db);
81
+ db.transaction = (async (work, config) => {
82
+ const open = { pool, open: true };
83
+ try {
84
+ return await run((tx) => inside.run(open, () => work(tx)), config);
85
+ } catch (error) {
86
+ // Drizzle wraps what a query throws: the rule broken is what to say.
87
+ throw brokenRule(error) ?? error;
88
+ } finally {
89
+ open.open = false;
90
+ }
91
+ }) as Database['transaction'];
92
+ return trackCommits(db);
32
93
  }
33
94
 
34
95
  /**
package/src/hyperdrive.ts CHANGED
@@ -15,16 +15,39 @@ export function postgres(hyperdrive: { readonly connectionString: string }): Pos
15
15
  }
16
16
 
17
17
  /**
18
- * Request-scoped node-postgres adapter for Hyperdrive.
18
+ * node-postgres for one call on Workers, through Hyperdrive: one client,
19
+ * connected on the first query, which every query and transaction of the
20
+ * call takes its turn on.
19
21
  *
20
- * Hyperdrive owns the durable origin pool. The Worker creates edge clients
21
- * only inside one invocation, which prevents I/O objects from leaking across
22
- * Worker request contexts while keeping the Pool shape Drizzle expects. The
23
- * class name must end in `Pool`: that is how Drizzle knows to check out a
24
- * client for each transaction.
22
+ * Hyperdrive keeps the pool to the database; a Worker's connections belong
23
+ * to the call that opened them, so each call opens its own. One, not one per
24
+ * query: on Cloudflare, a call that opened several Hyperdrive connections at
25
+ * once was now and then cancelled as hung, with nothing left pending, and
26
+ * every connection costs a round trip before the query can be sent.
27
+ *
28
+ * The turns keep a transaction whole: queries go to the client in the order
29
+ * they are asked, a transaction holds it from BEGIN until it is released,
30
+ * and a query asked meanwhile waits until then, so nothing runs inside a
31
+ * transaction it is not part of. So code inside a transaction must query
32
+ * through the transaction: a query on the database there would wait for the
33
+ * transaction to end, which waits for it. Neither the vault nor the server
34
+ * does.
35
+ *
36
+ * The class name must end in `Pool`: that is how Drizzle knows to check out
37
+ * a client for each transaction.
25
38
  */
26
39
  export class HyperdrivePool implements Exclude<PoolLike, pg.Pool> {
27
40
  readonly #connectionString: string;
41
+ /** The client, connecting or connected; null before the first query, and once it has failed. */
42
+ #client: Promise<pg.Client> | null = null;
43
+ /** Whether a transaction holds the client. */
44
+ #held = false;
45
+ /** What waits for its turn, in the order it asked. */
46
+ readonly #waiting: (() => void)[] = [];
47
+ /** Queries and transactions asked and not done: waiting for the client, their turn, or an answer. */
48
+ #busy = 0;
49
+ /** What waits for the client to be idle: `end`. */
50
+ readonly #idle: (() => void)[] = [];
28
51
 
29
52
  constructor(connectionString: string) {
30
53
  if (connectionString.trim().length === 0) {
@@ -34,30 +57,88 @@ export class HyperdrivePool implements Exclude<PoolLike, pg.Pool> {
34
57
  }
35
58
 
36
59
  readonly query: pg.Pool['query'] = (async (...args: unknown[]) => {
37
- const client = await this.#connectClient();
60
+ this.#busy += 1;
38
61
  try {
39
- return await (client.query as (...queryArgs: unknown[]) => Promise<unknown>)(...args);
62
+ const client = await this.#connected();
63
+ return await new Promise((resolve, reject) => {
64
+ // Sent at its turn, at once: pg answers its queries in the order they are sent.
65
+ this.#turn(() => (client.query as (...queryArgs: unknown[]) => Promise<unknown>)(...args).then(resolve, reject));
66
+ });
40
67
  } finally {
41
- await client.end();
68
+ this.#done();
42
69
  }
43
70
  }) as pg.Pool['query'];
44
71
 
72
+ /** The client, for a transaction: held from now until `release`. */
45
73
  async connect(): Promise<Pick<pg.PoolClient, 'query'> & { release: () => void }> {
46
- const client = await this.#connectClient();
74
+ this.#busy += 1;
75
+ let client: pg.Client;
76
+ try {
77
+ client = await this.#connected();
78
+ } catch (error) {
79
+ this.#done();
80
+ throw error;
81
+ }
82
+ await new Promise<void>((resolve) =>
83
+ this.#turn(() => {
84
+ this.#held = true;
85
+ resolve();
86
+ }),
87
+ );
47
88
  let released = false;
48
89
  return {
49
90
  query: client.query.bind(client) as pg.PoolClient['query'],
50
91
  release: () => {
51
92
  if (released) return;
52
93
  released = true;
53
- client.end().catch(() => {});
94
+ this.#held = false;
95
+ this.#next();
96
+ this.#done();
54
97
  },
55
98
  };
56
99
  }
57
100
 
58
- async #connectClient(): Promise<pg.Client> {
101
+ /** Close the client once nothing waits on it: what a Worker hands `waitUntil` when the call is done. */
102
+ async end(): Promise<void> {
103
+ while (this.#busy > 0) {
104
+ await new Promise<void>((resolve) => this.#idle.push(resolve));
105
+ }
106
+ const client = this.#client;
107
+ this.#client = null;
108
+ if (client === null) return;
109
+ await client.then((connected) => connected.end()).catch(() => {});
110
+ }
111
+
112
+ /** Run `go` now if nothing holds the client or waits before it, otherwise at its turn. */
113
+ #turn(go: () => void): void {
114
+ if (!this.#held && this.#waiting.length === 0) go();
115
+ else this.#waiting.push(go);
116
+ }
117
+
118
+ /** Give the next turns: queries until one is a transaction, which holds the client. */
119
+ #next(): void {
120
+ while (!this.#held && this.#waiting.length > 0) this.#waiting.shift()!();
121
+ }
122
+
123
+ /** One query or transaction done: once none is left, what waits for the client to be idle goes on. */
124
+ #done(): void {
125
+ this.#busy -= 1;
126
+ if (this.#busy === 0) for (const resolve of this.#idle.splice(0)) resolve();
127
+ }
128
+
129
+ /** The call's client, connecting it the first time. One that fails is dropped: the next query connects again. */
130
+ #connected(): Promise<pg.Client> {
131
+ if (this.#client !== null) return this.#client;
59
132
  const client = new pg.Client(postgresConnection(this.#connectionString));
60
- await client.connect();
61
- return client;
133
+ const connecting: Promise<pg.Client> = client.connect().then(() => client);
134
+ const drop = () => {
135
+ if (this.#client === connecting) this.#client = null;
136
+ };
137
+ // A connection lost between queries: pg fails what was waiting on it, and the next query connects again.
138
+ client.on('error', drop);
139
+ client.on('end', drop);
140
+ connecting.catch(drop);
141
+ this.#client = connecting;
142
+ return connecting;
62
143
  }
63
144
  }