@porulle/adapter-neon 0.35.1 → 0.36.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/dist/index.d.ts +30 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +71 -4
- package/package.json +2 -2
- package/src/index.ts +79 -4
package/dist/index.d.ts
CHANGED
|
@@ -19,6 +19,36 @@ export interface NeonAdapterOptions {
|
|
|
19
19
|
} | undefined;
|
|
20
20
|
}
|
|
21
21
|
export type NeonDatabaseAdapter = DatabaseAdapter<HttpClient, unknown>;
|
|
22
|
+
/**
|
|
23
|
+
* Run `fn` with ONE Hyperdrive client shared by every transaction it takes, closed when `fn`
|
|
24
|
+
* settles either way.
|
|
25
|
+
*
|
|
26
|
+
* Why it exists. `runInHyperdriveClient` below used to open and end a Postgres.js client per
|
|
27
|
+
* `transaction()` call. Measured on a deployed Worker on 2026-09-15: one import of 100 products
|
|
28
|
+
* opened **63,355** of them, and a probe from inside that same Worker priced a client at 4 ms on
|
|
29
|
+
* the medians and 7.7 ms on the means against a reused one, 88 ms on the first — on the order of
|
|
30
|
+
* 250 to 500 seconds inside a 985-second import, paid for nothing.
|
|
31
|
+
*
|
|
32
|
+
* Why it is a scope and not a module-level client. A Worker may not reuse a socket across
|
|
33
|
+
* invocations; an I/O object created in one request context throws when touched from another. So
|
|
34
|
+
* the reuse is bounded by whatever the caller declares an invocation to be — a fetch, a queue
|
|
35
|
+
* batch, a Workflow step — and the caller opens the scope. Nothing here is assumed to survive
|
|
36
|
+
* past it.
|
|
37
|
+
*
|
|
38
|
+
* Why plain queries are NOT routed through it. The same probe measured Neon HTTP at 6.76 ms per
|
|
39
|
+
* query against this client's 8.02 ms, with identical 7 ms medians. Moving them here would be
|
|
40
|
+
* slower, and an HTTP query costs no connection at all.
|
|
41
|
+
*
|
|
42
|
+
* Hyperdrive's limits bound the scope: a query may run for at most 60 s and an idle connection is
|
|
43
|
+
* dropped after 10 minutes. The client below raises Postgres.js's own `idle_timeout` from 5 s to
|
|
44
|
+
* 30 s so a gap between two transactions in an import loop — about 2.6 s at the measured rate —
|
|
45
|
+
* does not silently close and reopen the connection this function exists to hold, while staying
|
|
46
|
+
* far inside Hyperdrive's own window.
|
|
47
|
+
*
|
|
48
|
+
* Nested calls join the enclosing scope rather than opening a second client, so a caller that
|
|
49
|
+
* wraps both its handler and an inner unit of work gets one connection, not two.
|
|
50
|
+
*/
|
|
51
|
+
export declare function withPooledTransactions<T>(fn: () => Promise<T>): Promise<T>;
|
|
22
52
|
/**
|
|
23
53
|
* Normalizes `.execute()` to the postgres-js shape (array of rows). Core and
|
|
24
54
|
* custom routes iterate `.execute()` results directly; the raw neon drivers
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAqBA,OAAO,EAA0B,KAAK,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AACtF,OAAO,EAAwB,KAAK,YAAY,EAAE,MAAM,6BAA6B,CAAC;AACtF,OAAO,EAAwB,KAAK,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAExF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAMrD,KAAK,UAAU,GAAG,gBAAgB,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;AAC5D,KAAK,QAAQ,GAAG,YAAY,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;AACtD,KAAK,QAAQ,GAAG,kBAAkB,CAAC,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC;AAC1D,KAAK,KAAK,GAAG,UAAU,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAE9C,MAAM,WAAW,kBAAkB;IACjC,qEAAqE;IACrE,gBAAgB,EAAE,MAAM,CAAC;IACzB;;;;OAIG;IACH,UAAU,CAAC,EAAE;QAAE,gBAAgB,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS,CAAC;CACvD;AAED,MAAM,MAAM,mBAAmB,GAAG,eAAe,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;AAUvE;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAsB,sBAAsB,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAahF;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,CAAC,SAAS,KAAK,EAAE,EAAE,EAAE,CAAC,GAAG,CAAC,CAsB/D;AAED,wBAAgB,WAAW,CAAC,OAAO,EAAE,kBAAkB,GAAG,mBAAmB,CAgF5E"}
|
package/dist/index.js
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
* WebSocket client cannot speak to Hyperdrive's TCP endpoint. The HTTP driver
|
|
18
18
|
* always speaks directly to Neon, so a direct `connectionString` is required.
|
|
19
19
|
*/
|
|
20
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
20
21
|
import { Pool, neon, neonConfig } from "@neondatabase/serverless";
|
|
21
22
|
import { drizzle as drizzleHttp } from "drizzle-orm/neon-http";
|
|
22
23
|
import { drizzle as drizzleWs } from "drizzle-orm/neon-serverless";
|
|
@@ -25,6 +26,52 @@ import postgres from "postgres";
|
|
|
25
26
|
if (typeof WebSocket !== "undefined") {
|
|
26
27
|
neonConfig.webSocketConstructor = WebSocket;
|
|
27
28
|
}
|
|
29
|
+
const pooledScope = new AsyncLocalStorage();
|
|
30
|
+
/**
|
|
31
|
+
* Run `fn` with ONE Hyperdrive client shared by every transaction it takes, closed when `fn`
|
|
32
|
+
* settles either way.
|
|
33
|
+
*
|
|
34
|
+
* Why it exists. `runInHyperdriveClient` below used to open and end a Postgres.js client per
|
|
35
|
+
* `transaction()` call. Measured on a deployed Worker on 2026-09-15: one import of 100 products
|
|
36
|
+
* opened **63,355** of them, and a probe from inside that same Worker priced a client at 4 ms on
|
|
37
|
+
* the medians and 7.7 ms on the means against a reused one, 88 ms on the first — on the order of
|
|
38
|
+
* 250 to 500 seconds inside a 985-second import, paid for nothing.
|
|
39
|
+
*
|
|
40
|
+
* Why it is a scope and not a module-level client. A Worker may not reuse a socket across
|
|
41
|
+
* invocations; an I/O object created in one request context throws when touched from another. So
|
|
42
|
+
* the reuse is bounded by whatever the caller declares an invocation to be — a fetch, a queue
|
|
43
|
+
* batch, a Workflow step — and the caller opens the scope. Nothing here is assumed to survive
|
|
44
|
+
* past it.
|
|
45
|
+
*
|
|
46
|
+
* Why plain queries are NOT routed through it. The same probe measured Neon HTTP at 6.76 ms per
|
|
47
|
+
* query against this client's 8.02 ms, with identical 7 ms medians. Moving them here would be
|
|
48
|
+
* slower, and an HTTP query costs no connection at all.
|
|
49
|
+
*
|
|
50
|
+
* Hyperdrive's limits bound the scope: a query may run for at most 60 s and an idle connection is
|
|
51
|
+
* dropped after 10 minutes. The client below raises Postgres.js's own `idle_timeout` from 5 s to
|
|
52
|
+
* 30 s so a gap between two transactions in an import loop — about 2.6 s at the measured rate —
|
|
53
|
+
* does not silently close and reopen the connection this function exists to hold, while staying
|
|
54
|
+
* far inside Hyperdrive's own window.
|
|
55
|
+
*
|
|
56
|
+
* Nested calls join the enclosing scope rather than opening a second client, so a caller that
|
|
57
|
+
* wraps both its handler and an inner unit of work gets one connection, not two.
|
|
58
|
+
*/
|
|
59
|
+
export async function withPooledTransactions(fn) {
|
|
60
|
+
if (pooledScope.getStore())
|
|
61
|
+
return fn();
|
|
62
|
+
const scope = { client: undefined };
|
|
63
|
+
try {
|
|
64
|
+
return await pooledScope.run(scope, fn);
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
const client = scope.client;
|
|
68
|
+
scope.client = undefined;
|
|
69
|
+
// Both paths, deliberately: a failed invocation that leaks its connection is worse than one
|
|
70
|
+
// that never pooled, because the leak is invisible until Hyperdrive runs out of them.
|
|
71
|
+
if (client)
|
|
72
|
+
await client.end({ timeout: 1 }).catch(() => { });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
28
75
|
/**
|
|
29
76
|
* Normalizes `.execute()` to the postgres-js shape (array of rows). Core and
|
|
30
77
|
* custom routes iterate `.execute()` results directly; the raw neon drivers
|
|
@@ -67,7 +114,28 @@ export function neonAdapter(options) {
|
|
|
67
114
|
await pool.end().catch(() => { });
|
|
68
115
|
}
|
|
69
116
|
};
|
|
70
|
-
const
|
|
117
|
+
const runInHyperdriveClient = async (fn) => {
|
|
118
|
+
const runOn = async (client) => {
|
|
119
|
+
const pgDb = normalizeExecuteShape(drizzlePg(client));
|
|
120
|
+
return await pgDb.transaction(async (tx) => fn(normalizeExecuteShape(tx)));
|
|
121
|
+
};
|
|
122
|
+
// Inside a `withPooledTransactions` scope the client is created once and owned by the scope,
|
|
123
|
+
// which closes it on both the success and the failure path. Closing it here instead would
|
|
124
|
+
// defeat the reuse and pull the connection out from under the transactions still to come.
|
|
125
|
+
const scope = pooledScope.getStore();
|
|
126
|
+
if (scope) {
|
|
127
|
+
scope.client ??= postgres(options.hyperdrive.connectionString, {
|
|
128
|
+
max: 1,
|
|
129
|
+
prepare: false,
|
|
130
|
+
connect_timeout: 10,
|
|
131
|
+
// 30 s rather than the 5 s below: the gap between two transactions in an import loop is
|
|
132
|
+
// about 2.6 s at the measured rate, close enough to 5 s that the connection this scope
|
|
133
|
+
// exists to hold would close and reopen anyway. Far inside Hyperdrive's own 10-minute
|
|
134
|
+
// idle timeout.
|
|
135
|
+
idle_timeout: 30,
|
|
136
|
+
});
|
|
137
|
+
return await runOn(scope.client);
|
|
138
|
+
}
|
|
71
139
|
const client = postgres(options.hyperdrive.connectionString, {
|
|
72
140
|
max: 1,
|
|
73
141
|
prepare: false,
|
|
@@ -75,15 +143,14 @@ export function neonAdapter(options) {
|
|
|
75
143
|
idle_timeout: 5,
|
|
76
144
|
});
|
|
77
145
|
try {
|
|
78
|
-
|
|
79
|
-
return await pgDb.transaction(async (tx) => fn(normalizeExecuteShape(tx)));
|
|
146
|
+
return await runOn(client);
|
|
80
147
|
}
|
|
81
148
|
finally {
|
|
82
149
|
await client.end({ timeout: 1 }).catch(() => { });
|
|
83
150
|
}
|
|
84
151
|
};
|
|
85
152
|
const runTransaction = options.hyperdrive
|
|
86
|
-
?
|
|
153
|
+
? runInHyperdriveClient
|
|
87
154
|
: runInFreshNeonPool;
|
|
88
155
|
// Some core paths call `kernel.database.db.transaction(...)` directly —
|
|
89
156
|
// splice the pool-backed transaction onto the HTTP client so both entry
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@porulle/adapter-neon",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.36.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"@neondatabase/serverless": "^1.0.1",
|
|
15
15
|
"drizzle-orm": "^0.45.1",
|
|
16
16
|
"postgres": "^3.4.7",
|
|
17
|
-
"@porulle/core": "0.
|
|
17
|
+
"@porulle/core": "0.36.0"
|
|
18
18
|
},
|
|
19
19
|
"devDependencies": {
|
|
20
20
|
"@types/node": "^24.5.2",
|
package/src/index.ts
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
* WebSocket client cannot speak to Hyperdrive's TCP endpoint. The HTTP driver
|
|
18
18
|
* always speaks directly to Neon, so a direct `connectionString` is required.
|
|
19
19
|
*/
|
|
20
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
20
21
|
import { Pool, neon, neonConfig } from "@neondatabase/serverless";
|
|
21
22
|
import { drizzle as drizzleHttp, type NeonHttpDatabase } from "drizzle-orm/neon-http";
|
|
22
23
|
import { drizzle as drizzleWs, type NeonDatabase } from "drizzle-orm/neon-serverless";
|
|
@@ -46,6 +47,58 @@ export interface NeonAdapterOptions {
|
|
|
46
47
|
|
|
47
48
|
export type NeonDatabaseAdapter = DatabaseAdapter<HttpClient, unknown>;
|
|
48
49
|
|
|
50
|
+
type PostgresClient = ReturnType<typeof postgres>;
|
|
51
|
+
|
|
52
|
+
interface PooledScope {
|
|
53
|
+
client: PostgresClient | undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const pooledScope = new AsyncLocalStorage<PooledScope>();
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Run `fn` with ONE Hyperdrive client shared by every transaction it takes, closed when `fn`
|
|
60
|
+
* settles either way.
|
|
61
|
+
*
|
|
62
|
+
* Why it exists. `runInHyperdriveClient` below used to open and end a Postgres.js client per
|
|
63
|
+
* `transaction()` call. Measured on a deployed Worker on 2026-09-15: one import of 100 products
|
|
64
|
+
* opened **63,355** of them, and a probe from inside that same Worker priced a client at 4 ms on
|
|
65
|
+
* the medians and 7.7 ms on the means against a reused one, 88 ms on the first — on the order of
|
|
66
|
+
* 250 to 500 seconds inside a 985-second import, paid for nothing.
|
|
67
|
+
*
|
|
68
|
+
* Why it is a scope and not a module-level client. A Worker may not reuse a socket across
|
|
69
|
+
* invocations; an I/O object created in one request context throws when touched from another. So
|
|
70
|
+
* the reuse is bounded by whatever the caller declares an invocation to be — a fetch, a queue
|
|
71
|
+
* batch, a Workflow step — and the caller opens the scope. Nothing here is assumed to survive
|
|
72
|
+
* past it.
|
|
73
|
+
*
|
|
74
|
+
* Why plain queries are NOT routed through it. The same probe measured Neon HTTP at 6.76 ms per
|
|
75
|
+
* query against this client's 8.02 ms, with identical 7 ms medians. Moving them here would be
|
|
76
|
+
* slower, and an HTTP query costs no connection at all.
|
|
77
|
+
*
|
|
78
|
+
* Hyperdrive's limits bound the scope: a query may run for at most 60 s and an idle connection is
|
|
79
|
+
* dropped after 10 minutes. The client below raises Postgres.js's own `idle_timeout` from 5 s to
|
|
80
|
+
* 30 s so a gap between two transactions in an import loop — about 2.6 s at the measured rate —
|
|
81
|
+
* does not silently close and reopen the connection this function exists to hold, while staying
|
|
82
|
+
* far inside Hyperdrive's own window.
|
|
83
|
+
*
|
|
84
|
+
* Nested calls join the enclosing scope rather than opening a second client, so a caller that
|
|
85
|
+
* wraps both its handler and an inner unit of work gets one connection, not two.
|
|
86
|
+
*/
|
|
87
|
+
export async function withPooledTransactions<T>(fn: () => Promise<T>): Promise<T> {
|
|
88
|
+
if (pooledScope.getStore()) return fn();
|
|
89
|
+
|
|
90
|
+
const scope: PooledScope = { client: undefined };
|
|
91
|
+
try {
|
|
92
|
+
return await pooledScope.run(scope, fn);
|
|
93
|
+
} finally {
|
|
94
|
+
const client = scope.client;
|
|
95
|
+
scope.client = undefined;
|
|
96
|
+
// Both paths, deliberately: a failed invocation that leaks its connection is worse than one
|
|
97
|
+
// that never pooled, because the leak is invisible until Hyperdrive runs out of them.
|
|
98
|
+
if (client) await client.end({ timeout: 1 }).catch(() => {});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
49
102
|
/**
|
|
50
103
|
* Normalizes `.execute()` to the postgres-js shape (array of rows). Core and
|
|
51
104
|
* custom routes iterate `.execute()` results directly; the raw neon drivers
|
|
@@ -93,9 +146,32 @@ export function neonAdapter(options: NeonAdapterOptions): NeonDatabaseAdapter {
|
|
|
93
146
|
}
|
|
94
147
|
};
|
|
95
148
|
|
|
96
|
-
const
|
|
149
|
+
const runInHyperdriveClient = async (
|
|
97
150
|
fn: (tx: unknown) => Promise<unknown>,
|
|
98
151
|
): Promise<unknown> => {
|
|
152
|
+
const runOn = async (client: PostgresClient) => {
|
|
153
|
+
const pgDb = normalizeExecuteShape(drizzlePg(client) as PgClient);
|
|
154
|
+
return await pgDb.transaction(async (tx) => fn(normalizeExecuteShape(tx as PgClient)));
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
// Inside a `withPooledTransactions` scope the client is created once and owned by the scope,
|
|
158
|
+
// which closes it on both the success and the failure path. Closing it here instead would
|
|
159
|
+
// defeat the reuse and pull the connection out from under the transactions still to come.
|
|
160
|
+
const scope = pooledScope.getStore();
|
|
161
|
+
if (scope) {
|
|
162
|
+
scope.client ??= postgres(options.hyperdrive!.connectionString, {
|
|
163
|
+
max: 1,
|
|
164
|
+
prepare: false,
|
|
165
|
+
connect_timeout: 10,
|
|
166
|
+
// 30 s rather than the 5 s below: the gap between two transactions in an import loop is
|
|
167
|
+
// about 2.6 s at the measured rate, close enough to 5 s that the connection this scope
|
|
168
|
+
// exists to hold would close and reopen anyway. Far inside Hyperdrive's own 10-minute
|
|
169
|
+
// idle timeout.
|
|
170
|
+
idle_timeout: 30,
|
|
171
|
+
});
|
|
172
|
+
return await runOn(scope.client);
|
|
173
|
+
}
|
|
174
|
+
|
|
99
175
|
const client = postgres(options.hyperdrive!.connectionString, {
|
|
100
176
|
max: 1,
|
|
101
177
|
prepare: false,
|
|
@@ -103,15 +179,14 @@ export function neonAdapter(options: NeonAdapterOptions): NeonDatabaseAdapter {
|
|
|
103
179
|
idle_timeout: 5,
|
|
104
180
|
});
|
|
105
181
|
try {
|
|
106
|
-
|
|
107
|
-
return await pgDb.transaction(async (tx) => fn(normalizeExecuteShape(tx as PgClient)));
|
|
182
|
+
return await runOn(client);
|
|
108
183
|
} finally {
|
|
109
184
|
await client.end({ timeout: 1 }).catch(() => {});
|
|
110
185
|
}
|
|
111
186
|
};
|
|
112
187
|
|
|
113
188
|
const runTransaction = options.hyperdrive
|
|
114
|
-
?
|
|
189
|
+
? runInHyperdriveClient
|
|
115
190
|
: runInFreshNeonPool;
|
|
116
191
|
|
|
117
192
|
// Some core paths call `kernel.database.db.transaction(...)` directly —
|