turbine-orm 0.79.1 → 0.80.0-next.cbb21b1

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.
Files changed (71) hide show
  1. package/README.md +4 -4
  2. package/dist/checkout.d.ts +53 -0
  3. package/dist/checkout.js +78 -0
  4. package/dist/cjs/checkout.d.ts +53 -0
  5. package/dist/cjs/checkout.js +82 -0
  6. package/dist/cjs/cli/index.js +4 -0
  7. package/dist/cjs/cli/mcp.js +4 -0
  8. package/dist/cjs/cli/migrate.js +6 -0
  9. package/dist/cjs/cli/observe.js +8 -0
  10. package/dist/cjs/cli/studio.js +13 -1
  11. package/dist/cjs/client.d.ts +12 -2
  12. package/dist/cjs/client.js +61 -75
  13. package/dist/cjs/connection-guard.d.ts +120 -0
  14. package/dist/cjs/connection-guard.js +191 -0
  15. package/dist/cjs/errors.d.ts +26 -0
  16. package/dist/cjs/errors.js +85 -1
  17. package/dist/cjs/index.d.ts +1 -1
  18. package/dist/cjs/nested-write.d.ts +12 -2
  19. package/dist/cjs/nested-write.js +4 -10
  20. package/dist/cjs/pipeline.js +12 -7
  21. package/dist/cjs/plan-flip-probe.js +4 -0
  22. package/dist/cjs/powdb-shared.d.ts +22 -2
  23. package/dist/cjs/powdb-shared.js +27 -2
  24. package/dist/cjs/powdb.js +36 -37
  25. package/dist/cjs/powql.d.ts +51 -6
  26. package/dist/cjs/powql.js +199 -45
  27. package/dist/cjs/prisma-compat.js +28 -4
  28. package/dist/cjs/query/builder.d.ts +44 -24
  29. package/dist/cjs/query/builder.js +125 -66
  30. package/dist/cjs/query/deferred.d.ts +9 -0
  31. package/dist/cjs/query/option-surface.js +12 -0
  32. package/dist/cjs/query/types.d.ts +68 -4
  33. package/dist/cjs/query/writes.d.ts +39 -9
  34. package/dist/cjs/query/writes.js +72 -34
  35. package/dist/cjs/realtime.d.ts +46 -2
  36. package/dist/cjs/realtime.js +125 -20
  37. package/dist/cjs/schema-sql.js +6 -0
  38. package/dist/cli/index.js +4 -0
  39. package/dist/cli/mcp.js +4 -0
  40. package/dist/cli/migrate.js +6 -0
  41. package/dist/cli/observe.js +8 -0
  42. package/dist/cli/studio.js +13 -1
  43. package/dist/client.d.ts +12 -2
  44. package/dist/client.js +62 -76
  45. package/dist/connection-guard.d.ts +120 -0
  46. package/dist/connection-guard.js +183 -0
  47. package/dist/errors.d.ts +26 -0
  48. package/dist/errors.js +83 -1
  49. package/dist/index.d.ts +1 -1
  50. package/dist/index.js +1 -1
  51. package/dist/nested-write.d.ts +12 -2
  52. package/dist/nested-write.js +4 -10
  53. package/dist/pipeline.js +13 -8
  54. package/dist/plan-flip-probe.js +4 -0
  55. package/dist/powdb-shared.d.ts +22 -2
  56. package/dist/powdb-shared.js +25 -2
  57. package/dist/powdb.js +23 -24
  58. package/dist/powql.d.ts +51 -6
  59. package/dist/powql.js +200 -46
  60. package/dist/prisma-compat.js +28 -4
  61. package/dist/query/builder.d.ts +44 -24
  62. package/dist/query/builder.js +126 -67
  63. package/dist/query/deferred.d.ts +9 -0
  64. package/dist/query/option-surface.js +12 -0
  65. package/dist/query/types.d.ts +68 -4
  66. package/dist/query/writes.d.ts +39 -9
  67. package/dist/query/writes.js +71 -34
  68. package/dist/realtime.d.ts +46 -2
  69. package/dist/realtime.js +125 -20
  70. package/dist/schema-sql.js +6 -0
  71. package/package.json +5 -3
package/README.md CHANGED
@@ -18,7 +18,7 @@ Turbine is **pre-1.0** (`0.x`). Which surfaces hold steady across minors, which
18
18
 
19
19
  Six reasons, each with the mechanism that makes it true:
20
20
 
21
- 1. **One dependency.** `dependencies` is `{ "pg": "^8.13.1" }`. No engine binary, no WASM compiler, no adapter packages in lockstep. The optional engines (SQLite, MySQL, SQL Server, PowDB, all tiered Experimental in [STABILITY.md](STABILITY.md)) are peer dependencies or Node builtins you install only if you use them.
21
+ 1. **One dependency.** `dependencies` is `{ "pg": "^8.15.0" }`. No engine binary, no WASM compiler, no adapter packages in lockstep. The optional engines (SQLite, MySQL, SQL Server, PowDB, all tiered Experimental in [STABILITY.md](STABILITY.md)) are peer dependencies or Node builtins you install only if you use them.
22
22
  2. **Written from scratch.** Turbine is not a layer over Knex or a query-builder library. Query compilation is plain string building with an FNV-1a shape fingerprint into a bounded LRU of SQL templates, so there is no plan cache to size and no compiler running on your event loop.
23
23
  3. **Nested relations in one statement.** A `with` clause compiles to correlated `json_agg` subqueries, so users with posts with comments is one round trip, typed end to end: `users[0].posts[0].comments[0].author.name` autocompletes with no annotation.
24
24
  4. **Close to raw SQL.** In the last published run, Turbine's overhead over a hand-written `pg` control was 1.08x by geometric mean. The table is below; the losses are stated with the wins.
@@ -272,7 +272,7 @@ Going deep on one database means the parts other ORMs push to raw SQL are typed
272
272
 
273
273
  ## Serverless and edge
274
274
 
275
- The core is driver-agnostic: hand any pg-compatible pool to `turbineHttp()` and Turbine runs on Vercel Edge, Cloudflare Workers, Deno Deploy, or anywhere else without TCP. The main entry's import graph is held under **91 kB brotli** (edge entry under **72 kB**) with `pg` external, enforced by `size-limit` in CI at those exact numbers; run `npm run size` for the current figure.
275
+ The core is driver-agnostic: hand any pg-compatible pool to `turbineHttp()` and Turbine runs on Vercel Edge, Cloudflare Workers, Deno Deploy, or anywhere else without TCP. The main entry's import graph is held under **93 kB brotli** (edge entry under **74 kB**) with `pg` external, enforced by `size-limit` in CI at those exact numbers; run `npm run size` for the current figure.
276
276
 
277
277
  ```typescript
278
278
  import { Pool } from '@neondatabase/serverless';
@@ -359,11 +359,11 @@ It is also built to be extended rather than wrapped. All SQL generation routes t
359
359
  |---|---|---|---|---|
360
360
  | **Engine / runtime** | No engine binary (`pg` only) | Client + TS/WASM query compiler | No engine | No engine |
361
361
  | **Runtime deps** | 1 (`pg`) | `@prisma/client` + required driver adapter | 0 | 0 |
362
- | **Main bundle (brotli)** | under 91 kB import graph (CI-enforced), `pg` external | ~1.6 MB client (TS/WASM compiler) | ~7 KB core | small |
362
+ | **Main bundle (brotli)** | under 93 kB import graph (CI-enforced), `pg` external | ~1.6 MB client (TS/WASM compiler) | ~7 KB core | small |
363
363
  | **Studio** | Read-only by default | Full CRUD, cloud-hosted | Full CRUD; [Gateway](https://gateway.drizzle.team/) self-hosted, free | None |
364
364
  | **Error PII safety** | Keys only by default | Values in messages | Raw pg errors | Raw pg errors |
365
365
  | **Migrations** | SQL-first, SHA-256 checksums | DSL-generated, shadow DB | SQL or Drizzle Kit | None |
366
- | **Edge runtime** | One import swap, under 72 kB brotli (CI-enforced) | Driver adapter + WASM compiler | Native | Native |
366
+ | **Edge runtime** | One import swap, under 74 kB brotli (CI-enforced) | Driver adapter + WASM compiler | Native | Native |
367
367
  | **Pipeline batching** | Parse/Bind/Execute protocol | Sequential in txn | Sequential | Manual |
368
368
  | **Typed errors** | `isRetryable` discriminant | Error codes only | None | None |
369
369
  | **Nested relations** | 1 query, deep type inference | 1 query per relation by default; single-query `relationJoins` is Preview | 1 query, `relations()` re-declaration | Manual (`jsonArrayFrom`) |
@@ -0,0 +1,53 @@
1
+ /**
2
+ * turbine-orm, checking a connection out of a pool for a unit of work
3
+ *
4
+ * Every path that holds a pooled connection across awaits (`$transaction`,
5
+ * `transaction()`, nested writes, cursor streams, `connect()`) starts the same
6
+ * way: `pool.connect()`, a typed error if that fails, and a guard for the
7
+ * checkout window (connection-guard.ts). They share this module so they cannot
8
+ * drift apart on any of the three.
9
+ *
10
+ * {@link openCheckout} adds the one recovery that is always safe. A unit of
11
+ * work opens with a statement that has no effect of its own (`BEGIN`, or the
12
+ * `SELECT 1` of a connectivity check). If THAT statement fails because the
13
+ * connection had already been closed by the server, nothing has run, so the
14
+ * connection is destroyed and the statement is sent once more on a fresh one.
15
+ * This is the write-path counterpart of the query builder's read retry: a
16
+ * write cannot be resent after a lost connection, because nobody can tell
17
+ * whether it committed first, but a transaction that has not begun holds no
18
+ * write to lose.
19
+ */
20
+ import { type CheckoutGuard } from './connection-guard.js';
21
+ import type { PgCompatPool, PgCompatPoolClient } from './pg-types.js';
22
+ export interface Checkout {
23
+ client: PgCompatPoolClient;
24
+ /** Release through this, never `client.release()`, so the guard comes off. */
25
+ checkout: CheckoutGuard;
26
+ }
27
+ /**
28
+ * Check a connection out, as a typed Turbine error when that fails.
29
+ *
30
+ * `pool.connect()` is where the first-run failures actually land: wrong
31
+ * password (SQLSTATE 28P01), no such database (3D000), nothing listening
32
+ * (ECONNREFUSED), an unverifiable TLS certificate. Unwrapped, every one of
33
+ * those left `$transaction`, `transaction()` and `connect()` as a raw pg
34
+ * `DatabaseError` carrying a SQLSTATE in `.code`, the same property Turbine
35
+ * puts `TURBINE_E0NN` in.
36
+ *
37
+ * Query paths need no equivalent: `pool.query()` opens the connection itself
38
+ * and rejects with the connect error, which the query boundary already wraps.
39
+ */
40
+ export declare function acquireConnection(pool: PgCompatPool): Promise<Checkout>;
41
+ /**
42
+ * Check a connection out and run `opening` on it, retrying ONCE on a fresh
43
+ * connection when `opening` fails with {@link isStaleConnectionError}.
44
+ *
45
+ * `opening` must be a statement with no effect (`BEGIN`, `SELECT 1`): the
46
+ * retry is safe only because a failure there leaves nothing behind. Before the
47
+ * retry the event loop runs one poll phase, so any other connection the server
48
+ * closed alongside this one has been evicted from the idle list rather than
49
+ * lent out again. On success the caller owns the checkout; on failure it has
50
+ * already been released and the error (the retry's, if there was one) is
51
+ * rethrown as {@link explainConnectionLoss} reports it.
52
+ */
53
+ export declare function openCheckout(pool: PgCompatPool, opening: (client: PgCompatPoolClient) => Promise<unknown>): Promise<Checkout>;
@@ -0,0 +1,78 @@
1
+ /**
2
+ * turbine-orm, checking a connection out of a pool for a unit of work
3
+ *
4
+ * Every path that holds a pooled connection across awaits (`$transaction`,
5
+ * `transaction()`, nested writes, cursor streams, `connect()`) starts the same
6
+ * way: `pool.connect()`, a typed error if that fails, and a guard for the
7
+ * checkout window (connection-guard.ts). They share this module so they cannot
8
+ * drift apart on any of the three.
9
+ *
10
+ * {@link openCheckout} adds the one recovery that is always safe. A unit of
11
+ * work opens with a statement that has no effect of its own (`BEGIN`, or the
12
+ * `SELECT 1` of a connectivity check). If THAT statement fails because the
13
+ * connection had already been closed by the server, nothing has run, so the
14
+ * connection is destroyed and the statement is sent once more on a fresh one.
15
+ * This is the write-path counterpart of the query builder's read retry: a
16
+ * write cannot be resent after a lost connection, because nobody can tell
17
+ * whether it committed first, but a transaction that has not begun holds no
18
+ * write to lose.
19
+ */
20
+ import { guardCheckout, settleEventLoop } from './connection-guard.js';
21
+ import { explainConnectionLoss, isStaleConnectionError, wrapPgError } from './errors.js';
22
+ /**
23
+ * Check a connection out, as a typed Turbine error when that fails.
24
+ *
25
+ * `pool.connect()` is where the first-run failures actually land: wrong
26
+ * password (SQLSTATE 28P01), no such database (3D000), nothing listening
27
+ * (ECONNREFUSED), an unverifiable TLS certificate. Unwrapped, every one of
28
+ * those left `$transaction`, `transaction()` and `connect()` as a raw pg
29
+ * `DatabaseError` carrying a SQLSTATE in `.code`, the same property Turbine
30
+ * puts `TURBINE_E0NN` in.
31
+ *
32
+ * Query paths need no equivalent: `pool.query()` opens the connection itself
33
+ * and rejects with the connect error, which the query boundary already wraps.
34
+ */
35
+ export async function acquireConnection(pool) {
36
+ let client;
37
+ try {
38
+ client = await pool.connect();
39
+ }
40
+ catch (err) {
41
+ throw wrapPgError(err);
42
+ }
43
+ return { client, checkout: guardCheckout(client) };
44
+ }
45
+ /**
46
+ * Check a connection out and run `opening` on it, retrying ONCE on a fresh
47
+ * connection when `opening` fails with {@link isStaleConnectionError}.
48
+ *
49
+ * `opening` must be a statement with no effect (`BEGIN`, `SELECT 1`): the
50
+ * retry is safe only because a failure there leaves nothing behind. Before the
51
+ * retry the event loop runs one poll phase, so any other connection the server
52
+ * closed alongside this one has been evicted from the idle list rather than
53
+ * lent out again. On success the caller owns the checkout; on failure it has
54
+ * already been released and the error (the retry's, if there was one) is
55
+ * rethrown as {@link explainConnectionLoss} reports it.
56
+ */
57
+ export async function openCheckout(pool, opening) {
58
+ for (let attempt = 0;; attempt++) {
59
+ const held = await acquireConnection(pool);
60
+ try {
61
+ await opening(held.client);
62
+ return held;
63
+ }
64
+ catch (err) {
65
+ const lostWith = held.checkout.lostWith;
66
+ if (attempt === 0 && isStaleConnectionError(err)) {
67
+ // Released WITH the error so pg-pool destroys it: the close that made
68
+ // it fail may not have been read yet, and a connection the pool still
69
+ // believes is queryable goes back to the idle list.
70
+ held.checkout.release(err instanceof Error ? err : true);
71
+ await settleEventLoop();
72
+ continue;
73
+ }
74
+ held.checkout.release();
75
+ throw explainConnectionLoss(err, lostWith);
76
+ }
77
+ }
78
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * turbine-orm, checking a connection out of a pool for a unit of work
3
+ *
4
+ * Every path that holds a pooled connection across awaits (`$transaction`,
5
+ * `transaction()`, nested writes, cursor streams, `connect()`) starts the same
6
+ * way: `pool.connect()`, a typed error if that fails, and a guard for the
7
+ * checkout window (connection-guard.ts). They share this module so they cannot
8
+ * drift apart on any of the three.
9
+ *
10
+ * {@link openCheckout} adds the one recovery that is always safe. A unit of
11
+ * work opens with a statement that has no effect of its own (`BEGIN`, or the
12
+ * `SELECT 1` of a connectivity check). If THAT statement fails because the
13
+ * connection had already been closed by the server, nothing has run, so the
14
+ * connection is destroyed and the statement is sent once more on a fresh one.
15
+ * This is the write-path counterpart of the query builder's read retry: a
16
+ * write cannot be resent after a lost connection, because nobody can tell
17
+ * whether it committed first, but a transaction that has not begun holds no
18
+ * write to lose.
19
+ */
20
+ import { type CheckoutGuard } from './connection-guard.js';
21
+ import type { PgCompatPool, PgCompatPoolClient } from './pg-types.js';
22
+ export interface Checkout {
23
+ client: PgCompatPoolClient;
24
+ /** Release through this, never `client.release()`, so the guard comes off. */
25
+ checkout: CheckoutGuard;
26
+ }
27
+ /**
28
+ * Check a connection out, as a typed Turbine error when that fails.
29
+ *
30
+ * `pool.connect()` is where the first-run failures actually land: wrong
31
+ * password (SQLSTATE 28P01), no such database (3D000), nothing listening
32
+ * (ECONNREFUSED), an unverifiable TLS certificate. Unwrapped, every one of
33
+ * those left `$transaction`, `transaction()` and `connect()` as a raw pg
34
+ * `DatabaseError` carrying a SQLSTATE in `.code`, the same property Turbine
35
+ * puts `TURBINE_E0NN` in.
36
+ *
37
+ * Query paths need no equivalent: `pool.query()` opens the connection itself
38
+ * and rejects with the connect error, which the query boundary already wraps.
39
+ */
40
+ export declare function acquireConnection(pool: PgCompatPool): Promise<Checkout>;
41
+ /**
42
+ * Check a connection out and run `opening` on it, retrying ONCE on a fresh
43
+ * connection when `opening` fails with {@link isStaleConnectionError}.
44
+ *
45
+ * `opening` must be a statement with no effect (`BEGIN`, `SELECT 1`): the
46
+ * retry is safe only because a failure there leaves nothing behind. Before the
47
+ * retry the event loop runs one poll phase, so any other connection the server
48
+ * closed alongside this one has been evicted from the idle list rather than
49
+ * lent out again. On success the caller owns the checkout; on failure it has
50
+ * already been released and the error (the retry's, if there was one) is
51
+ * rethrown as {@link explainConnectionLoss} reports it.
52
+ */
53
+ export declare function openCheckout(pool: PgCompatPool, opening: (client: PgCompatPoolClient) => Promise<unknown>): Promise<Checkout>;
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+ /**
3
+ * turbine-orm, checking a connection out of a pool for a unit of work
4
+ *
5
+ * Every path that holds a pooled connection across awaits (`$transaction`,
6
+ * `transaction()`, nested writes, cursor streams, `connect()`) starts the same
7
+ * way: `pool.connect()`, a typed error if that fails, and a guard for the
8
+ * checkout window (connection-guard.ts). They share this module so they cannot
9
+ * drift apart on any of the three.
10
+ *
11
+ * {@link openCheckout} adds the one recovery that is always safe. A unit of
12
+ * work opens with a statement that has no effect of its own (`BEGIN`, or the
13
+ * `SELECT 1` of a connectivity check). If THAT statement fails because the
14
+ * connection had already been closed by the server, nothing has run, so the
15
+ * connection is destroyed and the statement is sent once more on a fresh one.
16
+ * This is the write-path counterpart of the query builder's read retry: a
17
+ * write cannot be resent after a lost connection, because nobody can tell
18
+ * whether it committed first, but a transaction that has not begun holds no
19
+ * write to lose.
20
+ */
21
+ Object.defineProperty(exports, "__esModule", { value: true });
22
+ exports.acquireConnection = acquireConnection;
23
+ exports.openCheckout = openCheckout;
24
+ const connection_guard_js_1 = require("./connection-guard.js");
25
+ const errors_js_1 = require("./errors.js");
26
+ /**
27
+ * Check a connection out, as a typed Turbine error when that fails.
28
+ *
29
+ * `pool.connect()` is where the first-run failures actually land: wrong
30
+ * password (SQLSTATE 28P01), no such database (3D000), nothing listening
31
+ * (ECONNREFUSED), an unverifiable TLS certificate. Unwrapped, every one of
32
+ * those left `$transaction`, `transaction()` and `connect()` as a raw pg
33
+ * `DatabaseError` carrying a SQLSTATE in `.code`, the same property Turbine
34
+ * puts `TURBINE_E0NN` in.
35
+ *
36
+ * Query paths need no equivalent: `pool.query()` opens the connection itself
37
+ * and rejects with the connect error, which the query boundary already wraps.
38
+ */
39
+ async function acquireConnection(pool) {
40
+ let client;
41
+ try {
42
+ client = await pool.connect();
43
+ }
44
+ catch (err) {
45
+ throw (0, errors_js_1.wrapPgError)(err);
46
+ }
47
+ return { client, checkout: (0, connection_guard_js_1.guardCheckout)(client) };
48
+ }
49
+ /**
50
+ * Check a connection out and run `opening` on it, retrying ONCE on a fresh
51
+ * connection when `opening` fails with {@link isStaleConnectionError}.
52
+ *
53
+ * `opening` must be a statement with no effect (`BEGIN`, `SELECT 1`): the
54
+ * retry is safe only because a failure there leaves nothing behind. Before the
55
+ * retry the event loop runs one poll phase, so any other connection the server
56
+ * closed alongside this one has been evicted from the idle list rather than
57
+ * lent out again. On success the caller owns the checkout; on failure it has
58
+ * already been released and the error (the retry's, if there was one) is
59
+ * rethrown as {@link explainConnectionLoss} reports it.
60
+ */
61
+ async function openCheckout(pool, opening) {
62
+ for (let attempt = 0;; attempt++) {
63
+ const held = await acquireConnection(pool);
64
+ try {
65
+ await opening(held.client);
66
+ return held;
67
+ }
68
+ catch (err) {
69
+ const lostWith = held.checkout.lostWith;
70
+ if (attempt === 0 && (0, errors_js_1.isStaleConnectionError)(err)) {
71
+ // Released WITH the error so pg-pool destroys it: the close that made
72
+ // it fail may not have been read yet, and a connection the pool still
73
+ // believes is queryable goes back to the idle list.
74
+ held.checkout.release(err instanceof Error ? err : true);
75
+ await (0, connection_guard_js_1.settleEventLoop)();
76
+ continue;
77
+ }
78
+ held.checkout.release();
79
+ throw (0, errors_js_1.explainConnectionLoss)(err, lostWith);
80
+ }
81
+ }
82
+ }
@@ -92,6 +92,7 @@ const node_fs_1 = require("node:fs");
92
92
  const node_os_1 = require("node:os");
93
93
  const node_path_1 = require("node:path");
94
94
  const node_url_1 = require("node:url");
95
+ const connection_guard_js_1 = require("../connection-guard.js");
95
96
  const connection_url_js_1 = require("../connection-url.js");
96
97
  const errors_js_1 = require("../errors.js");
97
98
  const generate_js_1 = require("../generate.js");
@@ -1018,6 +1019,7 @@ async function probeDatabase(url, schema) {
1018
1019
  try {
1019
1020
  const { default: pg } = await Promise.resolve().then(() => __importStar(require('pg')));
1020
1021
  const client = new pg.Client({ connectionString: url });
1022
+ (0, connection_guard_js_1.guardConnection)(client);
1021
1023
  await client.connect();
1022
1024
  let tableCount = 0;
1023
1025
  try {
@@ -3041,6 +3043,7 @@ async function seedConnectionString(config) {
3041
3043
  (0, migrate_js_1.assertPinnableSchema)(schema);
3042
3044
  const { default: pg } = await Promise.resolve().then(() => __importStar(require('pg')));
3043
3045
  const probe = new pg.Client({ connectionString: config.url });
3046
+ (0, connection_guard_js_1.guardConnection)(probe);
3044
3047
  await probe.connect();
3045
3048
  let inherited;
3046
3049
  try {
@@ -3112,6 +3115,7 @@ async function runSeedPlan(plan, config) {
3112
3115
  const url = seedUrl ?? requireUrl(config);
3113
3116
  const { default: pg } = await Promise.resolve().then(() => __importStar(require('pg')));
3114
3117
  const client = new pg.Client({ connectionString: url });
3118
+ (0, connection_guard_js_1.guardConnection)(client);
3115
3119
  await client.connect();
3116
3120
  try {
3117
3121
  await client.query((0, node_fs_1.readFileSync)(plan.file, 'utf-8'));
@@ -12,6 +12,7 @@ const node_crypto_1 = require("node:crypto");
12
12
  const node_fs_1 = require("node:fs");
13
13
  const node_path_1 = require("node:path");
14
14
  const pg_1 = __importDefault(require("pg"));
15
+ const connection_guard_js_1 = require("../connection-guard.js");
15
16
  const index_advisor_js_1 = require("../index-advisor.js");
16
17
  const index_stats_js_1 = require("../index-stats.js");
17
18
  const introspect_js_1 = require("../introspect.js");
@@ -299,6 +300,9 @@ function startMcpServer(options, transport = {}) {
299
300
  ctx.pool.on?.('error', (err) => {
300
301
  process.stderr.write(`[turbine] mcp pool error: ${(0, ui_js_1.redactUrl)(err.message)}\n`);
301
302
  });
303
+ // The same event on a CHECKED-OUT client (a tool call mid-query when the
304
+ // server restarts) has no pool listener to fall back on; see connection-guard.ts.
305
+ (0, connection_guard_js_1.absorbCheckedOutErrors)(ctx.pool);
302
306
  announcePiiTags(options);
303
307
  let buffer = '';
304
308
  let disposed = false;
@@ -63,6 +63,7 @@ const node_fs_1 = require("node:fs");
63
63
  const node_path_1 = require("node:path");
64
64
  const pg_1 = __importDefault(require("pg"));
65
65
  const index_js_1 = require("../adapters/index.js");
66
+ const connection_guard_js_1 = require("../connection-guard.js");
66
67
  const connection_url_js_1 = require("../connection-url.js");
67
68
  const dialect_js_1 = require("../dialect.js");
68
69
  const errors_js_1 = require("../errors.js");
@@ -882,6 +883,9 @@ async function connectMigrationClient(connectionString, schema) {
882
883
  const inherited = schema === undefined || schema === '' ? [] : await probeConnectionSchemas(connectionString, schema);
883
884
  const pinned = connectionStringForSchema(connectionString, schema, inherited);
884
885
  const client = new pg_1.default.Client({ connectionString: pinned });
886
+ // Held for the whole run: a server restart must fail the migration with a
887
+ // message, not exit through an unheard 'error' event (connection-guard.ts).
888
+ (0, connection_guard_js_1.guardConnection)(client);
885
889
  await client.connect();
886
890
  return { client, connectionString: pinned };
887
891
  }
@@ -894,6 +898,7 @@ async function probeConnectionSchemas(connectionString, schema) {
894
898
  // so in those terms, see assertPinnableSchema.
895
899
  assertPinnableSchema(schema);
896
900
  const probe = new pg_1.default.Client({ connectionString });
901
+ (0, connection_guard_js_1.guardConnection)(probe);
897
902
  await probe.connect();
898
903
  try {
899
904
  await assertSchemaExists(probe, schema);
@@ -907,6 +912,7 @@ async function probeConnectionSchemas(connectionString, schema) {
907
912
  /** Open the second, lock-only connection. Separated so tests can fake it. */
908
913
  async function openLockConnection(connectionString) {
909
914
  const client = new pg_1.default.Client({ connectionString });
915
+ (0, connection_guard_js_1.guardConnection)(client);
910
916
  await client.connect();
911
917
  return client;
912
918
  }
@@ -15,8 +15,10 @@ exports.handleRequest = handleRequest;
15
15
  const node_crypto_1 = require("node:crypto");
16
16
  const node_http_1 = require("node:http");
17
17
  const pg_1 = __importDefault(require("pg"));
18
+ const connection_guard_js_1 = require("../connection-guard.js");
18
19
  const observe_ui_js_1 = require("./observe-ui.js");
19
20
  const rate_limit_js_1 = require("./rate-limit.js");
21
+ const ui_js_1 = require("./ui.js");
20
22
  // ---------------------------------------------------------------------------
21
23
  // Main entry point
22
24
  // ---------------------------------------------------------------------------
@@ -26,6 +28,12 @@ async function startObserve(options) {
26
28
  max: 2,
27
29
  idleTimeoutMillis: 10_000,
28
30
  });
31
+ // Idle-client errors surface on the pool, checked-out ones on the client;
32
+ // with no listener either one exits the dashboard (see connection-guard.ts).
33
+ pool.on('error', (err) => {
34
+ console.error(`[turbine] observe pool error: ${(0, ui_js_1.redactUrl)(err.message)}`);
35
+ });
36
+ (0, connection_guard_js_1.absorbCheckedOutErrors)(pool);
29
37
  const probe = await pool.connect();
30
38
  try {
31
39
  await probe.query('SELECT 1');
@@ -62,6 +62,7 @@ const node_http_1 = require("node:http");
62
62
  const node_os_1 = require("node:os");
63
63
  const node_path_1 = require("node:path");
64
64
  const pg_1 = __importDefault(require("pg"));
65
+ const connection_guard_js_1 = require("../connection-guard.js");
65
66
  const errors_js_1 = require("../errors.js");
66
67
  const introspect_js_1 = require("../introspect.js");
67
68
  const index_js_1 = require("../query/index.js");
@@ -77,6 +78,7 @@ const pii_tags_js_1 = require("./pii-tags.js");
77
78
  const rate_limit_js_1 = require("./rate-limit.js");
78
79
  const studio_demo_js_1 = require("./studio-demo.js");
79
80
  const studio_ui_generated_js_1 = require("./studio-ui.generated.js");
81
+ const ui_js_1 = require("./ui.js");
80
82
  // ---------------------------------------------------------------------------
81
83
  // Main entry point
82
84
  // ---------------------------------------------------------------------------
@@ -121,11 +123,21 @@ async function startStudio(options) {
121
123
  (0, utils_js_1.registerUtcTemporalParsers)();
122
124
  // pg.Pool satisfies the PgCompatPool contract (same as the external-pool
123
125
  // seam in client.ts); the cast keeps one typed pool field for both modes.
124
- pool = new pg_1.default.Pool({
126
+ const pgPool = new pg_1.default.Pool({
125
127
  connectionString: options.url,
126
128
  max: 4, // small pool, single-user tool
127
129
  idleTimeoutMillis: 10_000,
128
130
  });
131
+ // A connection that dies while idle emits 'error' on the POOL, and one that
132
+ // dies mid-request emits it on the checked-out client. Neither had a
133
+ // listener, so a database restart exited Studio. Now the pending request
134
+ // fails and the server keeps running. The message is redacted because pg
135
+ // can echo the connection string into connection failures.
136
+ pgPool.on('error', (err) => {
137
+ console.error(`[turbine] studio pool error: ${(0, ui_js_1.redactUrl)(err.message)}`);
138
+ });
139
+ (0, connection_guard_js_1.absorbCheckedOutErrors)(pgPool);
140
+ pool = pgPool;
129
141
  // Verify connectivity before starting the server, fail fast.
130
142
  const probe = await pool.connect();
131
143
  try {
@@ -27,7 +27,7 @@ import { type ObserveConfig, type ObserveHandle } from './observe.js';
27
27
  import type { PgCompatPool, PgCompatPoolClient } from './pg-types.js';
28
28
  import { type PipelineOptions, type PipelineResults } from './pipeline.js';
29
29
  import { type DeferredQuery, type GlobalFilters, type JsonEncoding, type QueryEventListener, QueryInterface, type QueryInterfaceOptions, type RelationLoadStrategy, type TemporalInfinityReading } from './query/index.js';
30
- import { type NotificationHandler, type Subscription } from './realtime.js';
30
+ import { type ListenOptions, type NotificationHandler, type Subscription } from './realtime.js';
31
31
  import type { SchemaMetadata } from './schema.js';
32
32
  import { TypedSqlQuery } from './typed-sql.js';
33
33
  export interface RetryOptions {
@@ -1206,17 +1206,27 @@ export declare class TurbineClient {
1206
1206
  * cannot do this, `$listen` throws a `ConnectionError` rather than hang.
1207
1207
  * `$notify` works on every driver.
1208
1208
  *
1209
+ * **Connection loss:** if the subscription's connection dies (a restart,
1210
+ * failover, compute suspend, `pg_terminate_backend`), it reconnects with
1211
+ * exponential backoff and re-issues `LISTEN`; `options.reconnect: false`
1212
+ * ends it instead. Postgres does not hold notifications for a disconnected
1213
+ * listener, so anything sent during the gap is lost: use
1214
+ * `options.onReconnect` to resynchronise. `options.onError` receives the loss
1215
+ * and each failed attempt (default: one `console.error` line each).
1216
+ *
1209
1217
  * @example
1210
1218
  * ```ts
1211
1219
  * const sub = await db.$listen('order_created', (payload) => {
1212
1220
  * const order = JSON.parse(payload);
1213
1221
  * console.log('new order', order.id);
1222
+ * }, {
1223
+ * onReconnect: () => refreshOrdersFromDatabase(),
1214
1224
  * });
1215
1225
  * // ...later
1216
1226
  * await sub.unsubscribe();
1217
1227
  * ```
1218
1228
  */
1219
- $listen(channel: string, handler: NotificationHandler): Promise<Subscription>;
1229
+ $listen(channel: string, handler: NotificationHandler, options?: ListenOptions): Promise<Subscription>;
1220
1230
  /**
1221
1231
  * Send a Postgres NOTIFY on `channel` with an optional payload string.
1222
1232
  *