turbine-orm 0.79.1 → 0.80.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.
- package/README.md +4 -4
- package/dist/checkout.d.ts +53 -0
- package/dist/checkout.js +78 -0
- package/dist/cjs/checkout.d.ts +53 -0
- package/dist/cjs/checkout.js +82 -0
- package/dist/cjs/cli/index.js +4 -0
- package/dist/cjs/cli/mcp.js +4 -0
- package/dist/cjs/cli/migrate.js +6 -0
- package/dist/cjs/cli/observe.js +8 -0
- package/dist/cjs/cli/studio.js +13 -1
- package/dist/cjs/client.d.ts +12 -2
- package/dist/cjs/client.js +61 -75
- package/dist/cjs/connection-guard.d.ts +120 -0
- package/dist/cjs/connection-guard.js +191 -0
- package/dist/cjs/errors.d.ts +26 -0
- package/dist/cjs/errors.js +85 -1
- package/dist/cjs/index.d.ts +1 -1
- package/dist/cjs/nested-write.d.ts +12 -2
- package/dist/cjs/nested-write.js +4 -10
- package/dist/cjs/pipeline.js +12 -7
- package/dist/cjs/plan-flip-probe.js +4 -0
- package/dist/cjs/powdb-shared.d.ts +22 -2
- package/dist/cjs/powdb-shared.js +27 -2
- package/dist/cjs/powdb.js +36 -37
- package/dist/cjs/powql.d.ts +51 -6
- package/dist/cjs/powql.js +199 -45
- package/dist/cjs/prisma-compat.js +28 -4
- package/dist/cjs/query/builder.d.ts +44 -24
- package/dist/cjs/query/builder.js +125 -66
- package/dist/cjs/query/deferred.d.ts +9 -0
- package/dist/cjs/query/option-surface.js +12 -0
- package/dist/cjs/query/types.d.ts +68 -4
- package/dist/cjs/query/writes.d.ts +39 -9
- package/dist/cjs/query/writes.js +72 -34
- package/dist/cjs/realtime.d.ts +46 -2
- package/dist/cjs/realtime.js +125 -20
- package/dist/cjs/schema-sql.js +6 -0
- package/dist/cli/index.js +4 -0
- package/dist/cli/mcp.js +4 -0
- package/dist/cli/migrate.js +6 -0
- package/dist/cli/observe.js +8 -0
- package/dist/cli/studio.js +13 -1
- package/dist/client.d.ts +12 -2
- package/dist/client.js +62 -76
- package/dist/connection-guard.d.ts +120 -0
- package/dist/connection-guard.js +183 -0
- package/dist/errors.d.ts +26 -0
- package/dist/errors.js +83 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/nested-write.d.ts +12 -2
- package/dist/nested-write.js +4 -10
- package/dist/pipeline.js +13 -8
- package/dist/plan-flip-probe.js +4 -0
- package/dist/powdb-shared.d.ts +22 -2
- package/dist/powdb-shared.js +25 -2
- package/dist/powdb.js +23 -24
- package/dist/powql.d.ts +51 -6
- package/dist/powql.js +200 -46
- package/dist/prisma-compat.js +28 -4
- package/dist/query/builder.d.ts +44 -24
- package/dist/query/builder.js +126 -67
- package/dist/query/deferred.d.ts +9 -0
- package/dist/query/option-surface.js +12 -0
- package/dist/query/types.d.ts +68 -4
- package/dist/query/writes.d.ts +39 -9
- package/dist/query/writes.js +71 -34
- package/dist/realtime.d.ts +46 -2
- package/dist/realtime.js +125 -20
- package/dist/schema-sql.js +6 -0
- 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.
|
|
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 **
|
|
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
|
|
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
|
|
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>;
|
package/dist/checkout.js
ADDED
|
@@ -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
|
+
}
|
package/dist/cjs/cli/index.js
CHANGED
|
@@ -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'));
|
package/dist/cjs/cli/mcp.js
CHANGED
|
@@ -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;
|
package/dist/cjs/cli/migrate.js
CHANGED
|
@@ -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
|
}
|
package/dist/cjs/cli/observe.js
CHANGED
|
@@ -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');
|
package/dist/cjs/cli/studio.js
CHANGED
|
@@ -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
|
-
|
|
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 {
|
package/dist/cjs/client.d.ts
CHANGED
|
@@ -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
|
*
|