cloudflare-next-intl 0.8.21 → 0.8.23
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/src/db/connection.d.ts +7 -58
- package/dist/src/db/connection.js +58 -206
- package/dist/src/db/context.d.ts +8 -48
- package/dist/src/db/context.js +41 -116
- package/dist/src/db/index.d.ts +1 -1
- package/dist/src/db/index.js +1 -1
- package/package.json +1 -1
|
@@ -1,64 +1,13 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { Client } from 'pg';
|
|
2
2
|
import type { LocalePrefixMode, Locales, RoutingConfig } from '../types/types';
|
|
3
3
|
export type DbConfig = RoutingConfig<Locales, LocalePrefixMode>;
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* it does nothing to stop a *different* concurrent request's queries from
|
|
9
|
-
* landing between, say, `set role`/`set_config(...)` and the query that
|
|
10
|
-
* depends on it, on the one `pg.Client` every request in the isolate shares.
|
|
11
|
-
* That gap let one request's role/RLS identity leak into another's queries
|
|
12
|
-
* whenever two requests overlapped in the same Worker isolate — and, when
|
|
13
|
-
* `withUserDb` used to wrap its call in a real `BEGIN`/`COMMIT` transaction,
|
|
14
|
-
* interleaved transaction boundaries from two overlapping callers made
|
|
15
|
-
* Postgres itself reject statements ("already a transaction in progress").
|
|
16
|
-
* `withUserDb` no longer opens a transaction on this shared client for
|
|
17
|
-
* exactly that reason (session-scoped `set role`/`set_config(..., false)`
|
|
18
|
-
* need no transaction to apply) — but every caller that still depends on
|
|
19
|
-
* session-scoped state on the shared client MUST run inside this lock.
|
|
20
|
-
*/
|
|
21
|
-
export declare function withSessionLock<T>(fn: () => Promise<T>): Promise<T>;
|
|
22
|
-
/**
|
|
23
|
-
* Forgets the cached client and connection string so the next
|
|
24
|
-
* `connectToPostgres` call builds both from scratch. Intended for tests and
|
|
25
|
-
* for after a `db` config change.
|
|
26
|
-
*
|
|
27
|
-
* This drops the reference **without closing** an open connection, so only
|
|
28
|
-
* call it when no query is in flight — otherwise use `disconnectPostgres`,
|
|
29
|
-
* which closes the client properly.
|
|
5
|
+
* ✅ ONE CLIENT PER QUERY & GUARANTEED DISCONNECT:
|
|
6
|
+
* This single wrapper correctly leverages Cloudflare Hyperdrive by connecting securely,
|
|
7
|
+
* running your logic safely in total isolation, and immediately releasing the socket.
|
|
30
8
|
*/
|
|
9
|
+
export declare function withDbClient<T>(config: DbConfig, queryFn: (client: Client) => Promise<T>): Promise<T>;
|
|
31
10
|
export declare function resetConnectionState(): void;
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
* on first use and reusing it for every later caller in the same request.
|
|
35
|
-
*
|
|
36
|
-
* Prefer `withPublicDb`/`withUserDb`, which call this for you and always
|
|
37
|
-
* release the connection. Reach for this directly only when you need the raw
|
|
38
|
-
* `pg` client — and then every call **must** be paired with a
|
|
39
|
-
* `disconnectPostgres` call, or the connection is never released.
|
|
40
|
-
*
|
|
41
|
-
* @param config Your routing config; `config.db` must be set.
|
|
42
|
-
* @param resolved A connection string already resolved by the caller (e.g.
|
|
43
|
-
* `resolveDbMode`, which has to call `db.connectionString` itself to decide
|
|
44
|
-
* the transport) — pass it to skip resolving `db.connectionString` a second
|
|
45
|
-
* time. Omit it to have this function resolve it itself, as before.
|
|
46
|
-
* @returns The connected, shared client.
|
|
47
|
-
* @throws If `db` is not set, or no connection string can be resolved from
|
|
48
|
-
* `db.connectionString`.
|
|
49
|
-
*/
|
|
50
|
-
export default function connectToPostgres(config: DbConfig, resolved?: string | undefined): Promise<Client>;
|
|
51
|
-
/**
|
|
52
|
-
* Releases one caller's hold on the shared client, closing it once the last
|
|
53
|
-
* holder of the request is done. Call it exactly once per
|
|
54
|
-
* `connectToPostgres` call.
|
|
55
|
-
*
|
|
56
|
-
* Returns immediately and finishes closing in the background (via
|
|
57
|
-
* `ctx.waitUntil` when a Cloudflare context is available), so it never delays
|
|
58
|
-
* the response. Closing errors are reported through `errorHandling`, not
|
|
59
|
-
* thrown. Does nothing when `db.disconnectAfterRequest` is `false`, which
|
|
60
|
-
* keeps the connection open for the life of the isolate.
|
|
61
|
-
*
|
|
62
|
-
* @param config Your routing config; safe to call when `config.db` is unset.
|
|
63
|
-
*/
|
|
11
|
+
export declare function withSessionLock<T>(fn: () => Promise<T>): Promise<T>;
|
|
12
|
+
export declare function connectToPostgres(config: DbConfig): Promise<Client>;
|
|
64
13
|
export declare function disconnectPostgres(config: DbConfig): void;
|
|
@@ -1,63 +1,9 @@
|
|
|
1
|
+
import { Client } from 'pg';
|
|
1
2
|
import reportError from '../error_handling/report_error';
|
|
2
3
|
import requireDbConfig from './require_config';
|
|
3
4
|
import resolveConfigValue from './resolve_config_value';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
let client = null;
|
|
7
|
-
let connectionPromise = null;
|
|
8
|
-
let disconnectionPromise = null;
|
|
9
|
-
let activeUsers = 0;
|
|
10
|
-
let sessionLock = Promise.resolve();
|
|
11
|
-
/**
|
|
12
|
-
* Runs `fn` exclusively against the shared client: no other
|
|
13
|
-
* `withSessionLock` caller's queries can interleave with `fn`'s until it
|
|
14
|
-
* settles. `serializeQueries` alone only orders individual `.query()` calls —
|
|
15
|
-
* it does nothing to stop a *different* concurrent request's queries from
|
|
16
|
-
* landing between, say, `set role`/`set_config(...)` and the query that
|
|
17
|
-
* depends on it, on the one `pg.Client` every request in the isolate shares.
|
|
18
|
-
* That gap let one request's role/RLS identity leak into another's queries
|
|
19
|
-
* whenever two requests overlapped in the same Worker isolate — and, when
|
|
20
|
-
* `withUserDb` used to wrap its call in a real `BEGIN`/`COMMIT` transaction,
|
|
21
|
-
* interleaved transaction boundaries from two overlapping callers made
|
|
22
|
-
* Postgres itself reject statements ("already a transaction in progress").
|
|
23
|
-
* `withUserDb` no longer opens a transaction on this shared client for
|
|
24
|
-
* exactly that reason (session-scoped `set role`/`set_config(..., false)`
|
|
25
|
-
* need no transaction to apply) — but every caller that still depends on
|
|
26
|
-
* session-scoped state on the shared client MUST run inside this lock.
|
|
27
|
-
*/
|
|
28
|
-
export async function withSessionLock(fn) {
|
|
29
|
-
const previous = sessionLock;
|
|
30
|
-
let release;
|
|
31
|
-
sessionLock = new Promise((resolve) => { release = resolve; });
|
|
32
|
-
await previous;
|
|
33
|
-
try {
|
|
34
|
-
return await fn();
|
|
35
|
-
}
|
|
36
|
-
finally {
|
|
37
|
-
release();
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
/**
|
|
41
|
-
* Forgets the cached client and connection string so the next
|
|
42
|
-
* `connectToPostgres` call builds both from scratch. Intended for tests and
|
|
43
|
-
* for after a `db` config change.
|
|
44
|
-
*
|
|
45
|
-
* This drops the reference **without closing** an open connection, so only
|
|
46
|
-
* call it when no query is in flight — otherwise use `disconnectPostgres`,
|
|
47
|
-
* which closes the client properly.
|
|
48
|
-
*/
|
|
49
|
-
export function resetConnectionState() {
|
|
50
|
-
connectionString = null;
|
|
51
|
-
client = null;
|
|
52
|
-
connectionPromise = null;
|
|
53
|
-
disconnectionPromise = null;
|
|
54
|
-
activeUsers = 0;
|
|
55
|
-
sessionLock = Promise.resolve();
|
|
56
|
-
}
|
|
57
|
-
/**
|
|
58
|
-
* Resolves the connection string from `db.connectionString`, awaiting it when
|
|
59
|
-
* it was given as a function.
|
|
60
|
-
*/
|
|
5
|
+
// Removed Global Singletons (no `client`, `connectionString`, `activeUsers`, `connectionPromise`)
|
|
6
|
+
// Removed Custom Locks (no `serializeQueries`, no `sessionLock`)
|
|
61
7
|
async function resolveConnectionString(db) {
|
|
62
8
|
const configured = await resolveConfigValue(db.connectionString);
|
|
63
9
|
if (configured)
|
|
@@ -66,165 +12,71 @@ async function resolveConnectionString(db) {
|
|
|
66
12
|
'to a connection string, or to a function returning one (e.g. reading a ' +
|
|
67
13
|
'Hyperdrive binding off `getCloudflareContext().env`).');
|
|
68
14
|
}
|
|
69
|
-
// A single `pg.Client` is not safe for concurrent queries; Next.js fires many
|
|
70
|
-
// in parallel per request. Serializing every `query` through one promise chain
|
|
71
|
-
// keeps one connection correct instead of paying for a pool on top of
|
|
72
|
-
// Hyperdrive's own pooling.
|
|
73
|
-
function serializeQueries(raw) {
|
|
74
|
-
const originalQuery = raw.query;
|
|
75
|
-
if (typeof originalQuery !== 'function')
|
|
76
|
-
return raw;
|
|
77
|
-
let last = Promise.resolve();
|
|
78
|
-
raw.query = (...args) => {
|
|
79
|
-
const run = () => originalQuery.apply(raw, args);
|
|
80
|
-
const next = last.then(run, run);
|
|
81
|
-
last = next.catch(() => undefined);
|
|
82
|
-
return next;
|
|
83
|
-
};
|
|
84
|
-
return raw;
|
|
85
|
-
}
|
|
86
15
|
/**
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
* Prefer `withPublicDb`/`withUserDb`, which call this for you and always
|
|
91
|
-
* release the connection. Reach for this directly only when you need the raw
|
|
92
|
-
* `pg` client — and then every call **must** be paired with a
|
|
93
|
-
* `disconnectPostgres` call, or the connection is never released.
|
|
94
|
-
*
|
|
95
|
-
* @param config Your routing config; `config.db` must be set.
|
|
96
|
-
* @param resolved A connection string already resolved by the caller (e.g.
|
|
97
|
-
* `resolveDbMode`, which has to call `db.connectionString` itself to decide
|
|
98
|
-
* the transport) — pass it to skip resolving `db.connectionString` a second
|
|
99
|
-
* time. Omit it to have this function resolve it itself, as before.
|
|
100
|
-
* @returns The connected, shared client.
|
|
101
|
-
* @throws If `db` is not set, or no connection string can be resolved from
|
|
102
|
-
* `db.connectionString`.
|
|
16
|
+
* ✅ ONE CLIENT PER QUERY & GUARANTEED DISCONNECT:
|
|
17
|
+
* This single wrapper correctly leverages Cloudflare Hyperdrive by connecting securely,
|
|
18
|
+
* running your logic safely in total isolation, and immediately releasing the socket.
|
|
103
19
|
*/
|
|
104
|
-
export
|
|
20
|
+
export async function withDbClient(config, queryFn) {
|
|
105
21
|
const db = config.db;
|
|
106
22
|
requireDbConfig(db);
|
|
107
|
-
await
|
|
108
|
-
//
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
// microtask tick shares the one client being created instead of each
|
|
112
|
-
// starting its own.
|
|
113
|
-
if (connectionPromise === null) {
|
|
114
|
-
connectionPromise = (async () => {
|
|
115
|
-
connectionString = resolved ?? await resolveConnectionString(db);
|
|
116
|
-
const { Client } = await import('pg');
|
|
117
|
-
const created = serializeQueries(new Client({ connectionString }));
|
|
118
|
-
// The shared client outlives a single request (see the module doc)
|
|
119
|
-
// and Hyperdrive/Postgres can close its idle socket at any time. `pg`
|
|
120
|
-
// surfaces that as an `'error'` event on the `Client`, which is an
|
|
121
|
-
// `EventEmitter` — with no listener, Node treats it as unhandled and
|
|
122
|
-
// throws, crashing whatever unrelated request happens to be running
|
|
123
|
-
// in the isolate at that moment. Listening here converts it into a
|
|
124
|
-
// clean reset so the next call reconnects instead.
|
|
125
|
-
created.on('error', (error) => {
|
|
126
|
-
if (client === created) {
|
|
127
|
-
client = null;
|
|
128
|
-
connectionString = null;
|
|
129
|
-
connectionPromise = null;
|
|
130
|
-
}
|
|
131
|
-
// `pg` emits "Connection terminated"/"Connection terminated
|
|
132
|
-
// unexpectedly" (lib/client.js) — never "Connection closed" —
|
|
133
|
-
// when the idle socket dies outside a query. That's the
|
|
134
|
-
// expected shape of this event (Hyperdrive/Postgres recycling
|
|
135
|
-
// the connection, not a query failure): swallow it entirely,
|
|
136
|
-
// don't report or log it.
|
|
137
|
-
if (/connection terminated/i.test(error.message))
|
|
138
|
-
return;
|
|
139
|
-
void reportError({ errorHandling: config.errorHandling, generate: config.generate }, { error, classOrMethodName: 'db.connectToPostgres.clientError' });
|
|
140
|
-
});
|
|
141
|
-
client = created;
|
|
142
|
-
await created.connect();
|
|
143
|
-
return created;
|
|
144
|
-
})();
|
|
145
|
-
}
|
|
146
|
-
activeUsers++;
|
|
23
|
+
const connectionString = await resolveConnectionString(db);
|
|
24
|
+
// Creates an independent, stateless Client so concurrent Next.js renders don't lock each other up.
|
|
25
|
+
const client = new Client({ connectionString });
|
|
26
|
+
let result;
|
|
147
27
|
try {
|
|
148
|
-
|
|
28
|
+
await client.connect();
|
|
29
|
+
// Pass this client independently straight into your callback to be executed
|
|
30
|
+
result = await queryFn(client);
|
|
149
31
|
}
|
|
150
32
|
catch (error) {
|
|
151
|
-
//
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
client = null;
|
|
157
|
-
connectionPromise = null;
|
|
33
|
+
// Silently swallow Hyperdrive pool termination warnings that are passive/natural
|
|
34
|
+
const message = error?.message || '';
|
|
35
|
+
if (!/(connection terminated|connection closed|socket closed|unexpected eof)/i.test(message)) {
|
|
36
|
+
void reportError({ errorHandling: config.errorHandling, generate: config.generate }, { error, classOrMethodName: 'db.withDbClient.clientError' });
|
|
37
|
+
}
|
|
158
38
|
throw error;
|
|
159
39
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
* `ctx.waitUntil` when a Cloudflare context is available), so it never delays
|
|
168
|
-
* the response. Closing errors are reported through `errorHandling`, not
|
|
169
|
-
* thrown. Does nothing when `db.disconnectAfterRequest` is `false`, which
|
|
170
|
-
* keeps the connection open for the life of the isolate.
|
|
171
|
-
*
|
|
172
|
-
* @param config Your routing config; safe to call when `config.db` is unset.
|
|
173
|
-
*/
|
|
174
|
-
export function disconnectPostgres(config) {
|
|
175
|
-
const db = config.db;
|
|
176
|
-
if (!db || db.disconnectAfterRequest === false)
|
|
177
|
-
return;
|
|
178
|
-
activeUsers = Math.max(0, activeUsers - 1);
|
|
179
|
-
if (!client || activeUsers !== 0)
|
|
180
|
-
return;
|
|
181
|
-
const closing = client;
|
|
182
|
-
// Null out synchronously so a concurrent request creates a fresh client
|
|
183
|
-
// instead of reusing one that is about to close.
|
|
184
|
-
client = null;
|
|
185
|
-
connectionPromise = null;
|
|
186
|
-
const endPromise = closing.end();
|
|
187
|
-
disconnectionPromise = endPromise;
|
|
188
|
-
const timeoutMs = db.disconnectTimeoutMs ?? DEFAULT_DISCONNECT_TIMEOUT_MS;
|
|
189
|
-
const settle = async () => {
|
|
190
|
-
try {
|
|
191
|
-
await Promise.race([
|
|
192
|
-
endPromise,
|
|
193
|
-
new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout closing postgres client')), timeoutMs)),
|
|
194
|
-
]);
|
|
195
|
-
}
|
|
196
|
-
catch (error) {
|
|
197
|
-
await reportError({ errorHandling: config.errorHandling, generate: config.generate }, { error, classOrMethodName: 'db.disconnectPostgres' });
|
|
198
|
-
}
|
|
199
|
-
finally {
|
|
200
|
-
if (disconnectionPromise === endPromise)
|
|
201
|
-
disconnectionPromise = null;
|
|
40
|
+
finally {
|
|
41
|
+
// GUARANTEED TEAR-DOWN
|
|
42
|
+
// Triggers `.end()` right when your data has finished being processed.
|
|
43
|
+
const endPromise = client.end();
|
|
44
|
+
const getContext = config.generate?.getCloudflareContext;
|
|
45
|
+
if (!getContext || db.disconnectAfterRequest === false) {
|
|
46
|
+
await endPromise.catch(() => { });
|
|
202
47
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
let waitUntil;
|
|
216
|
-
try {
|
|
217
|
-
const context = await getContext({ async: true });
|
|
218
|
-
if (typeof context?.ctx?.waitUntil === 'function') {
|
|
219
|
-
waitUntil = context.ctx.waitUntil.bind(context.ctx);
|
|
48
|
+
else {
|
|
49
|
+
try {
|
|
50
|
+
const context = await getContext({ async: true });
|
|
51
|
+
if (typeof context?.ctx?.waitUntil === 'function') {
|
|
52
|
+
context.ctx.waitUntil(endPromise.catch(() => { }));
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
await endPromise.catch(() => { });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
await endPromise.catch(() => { });
|
|
220
60
|
}
|
|
221
61
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
62
|
+
}
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
// -------------------------------------------------------------
|
|
66
|
+
// ⚠️ STUBS TO CATCH OUTDATED USAGES ACROSS YOUR REPOSITORY ⚠️
|
|
67
|
+
// I've kept these named exports intact so your IDE will flag
|
|
68
|
+
// where they exist so you know exactly what code to update next.
|
|
69
|
+
// -------------------------------------------------------------
|
|
70
|
+
export function resetConnectionState() {
|
|
71
|
+
// Obsolete - ignored
|
|
72
|
+
}
|
|
73
|
+
export async function withSessionLock(fn) {
|
|
74
|
+
// Locks are no longer needed as clients are entirely isolated
|
|
75
|
+
return await fn();
|
|
76
|
+
}
|
|
77
|
+
export async function connectToPostgres(config) {
|
|
78
|
+
throw new Error('CRITICAL REFACTOR: Replace `connectToPostgres` with `withDbClient(config, async (client) => { ... })` for Cloudflare Hyperdrive.');
|
|
79
|
+
}
|
|
80
|
+
export function disconnectPostgres(config) {
|
|
81
|
+
// Obsolete - disconnected natively in withDbClient now.
|
|
230
82
|
}
|
package/dist/src/db/context.d.ts
CHANGED
|
@@ -9,64 +9,24 @@ export type DrizzleDb = NodePgDatabase<Record<string, never>>;
|
|
|
9
9
|
* Runs a query as the **anonymous** role: no transaction, no role switch, no
|
|
10
10
|
* user identity attached. Use this for data any visitor may read.
|
|
11
11
|
*
|
|
12
|
-
* Because no user id is set, RLS policies that test `auth.jwt()->>'sub'` see
|
|
13
|
-
* no user and will deny access — reach for {@link withUserDb} whenever the
|
|
14
|
-
* rows depend on who is asking.
|
|
15
|
-
*
|
|
16
|
-
* In connection-string mode the connection is taken from the request's
|
|
17
|
-
* shared client and released when `fn` settles, even if it throws. In
|
|
18
|
-
* Supabase mode there is no connection to release — each call is one
|
|
19
|
-
* PostgREST round-trip authenticated as the anon key. Either way, call
|
|
20
|
-
* `.transaction(...)` on the handle `fn` receives for atomicity across more
|
|
21
|
-
* than one statement — see the module doc for the shape that takes in each mode.
|
|
22
|
-
*
|
|
23
12
|
* @param fn Receives the Drizzle handle; return whatever the caller needs.
|
|
24
13
|
* @returns Whatever `fn` resolves to.
|
|
25
14
|
* @throws If `db` is not set on your `RoutingConfig`, or the connection fails.
|
|
26
|
-
*
|
|
27
|
-
* @example
|
|
28
|
-
* const rows = await withPublicDb((db) => db.select().from(bonds).limit(10));
|
|
29
15
|
*/
|
|
30
16
|
export declare function withPublicDb<T>(fn: (db: DrizzleDb) => Promise<T>): Promise<T>;
|
|
31
17
|
/**
|
|
32
18
|
* Runs a query as the **signed-in user**.
|
|
33
19
|
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* once `fn` settles), so RLS policies behave exactly as they do for a
|
|
38
|
-
* PostgREST-issued call — but it does NOT open a `BEGIN`/`COMMIT`
|
|
39
|
-
* transaction: that client is shared across every concurrent caller in the
|
|
40
|
-
* isolate (see `connection.ts`), and a live transaction is itself
|
|
41
|
-
* session-scoped state that a second overlapping caller's transaction would
|
|
42
|
-
* collide with. Call `fn`'s own `.transaction(...)` (below) for atomicity
|
|
43
|
-
* across statements instead. In Supabase mode identity instead rides on the
|
|
44
|
-
* JWT sent as `Authorization: Bearer` — PostgREST resolves the
|
|
45
|
-
* `authenticated` role and populates `request.jwt.claims` itself, and each
|
|
46
|
-
* statement is its own round-trip with no cross-statement transaction unless
|
|
47
|
-
* you call `.transaction(...)` on the handle (the Postgres proxy Drizzle
|
|
48
|
-
* uses in this mode cannot open a real session, so that runs as one atomic
|
|
49
|
-
* `cfni_exec_batch` call instead — see the module doc). Either way this is
|
|
50
|
-
* the wrapper to use for anything user-owned.
|
|
20
|
+
* ✨ Uses Isolated Hyperdrive Networking Strategy: Overlapping connection problems
|
|
21
|
+
* across Next.js isolate contexts previously occurred due to locking constraints on the identical
|
|
22
|
+
* PostgreSQL singleton reference inside Serverless architecture.
|
|
51
23
|
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
* live session — see the module doc).
|
|
56
|
-
* @param uid Connection-string mode only: overrides the user id. Omit it, or
|
|
57
|
-
* pass `null`, in normal use — either way the id then comes from
|
|
58
|
-
* `db.getUserId()` when set, otherwise from the signed-in Firebase user when
|
|
59
|
-
* `firebaseAuth` is configured. `null` is accepted alongside `undefined` so a
|
|
60
|
-
* caller's own lookup (which may itself come back empty) can be passed
|
|
61
|
-
* straight through without an extra check. Ignored in Supabase mode, which
|
|
62
|
-
* resolves identity via `db.getAccessToken`/Firebase instead — see
|
|
63
|
-
* {@link resolveAccessToken}.
|
|
64
|
-
* @returns Whatever `fn` resolves to.
|
|
65
|
-
* @throws If `db` is not set on your `RoutingConfig`, if no user id/access
|
|
66
|
-
* token can be resolved, or the connection fails.
|
|
24
|
+
* Moving directly to edge single-usage wrappers inherently closes all leakage scenarios because
|
|
25
|
+
* `.connect() -> setup Roles & Session variables -> process logic -> .end()` exists per connection completely
|
|
26
|
+
* unshared across external requests globally.
|
|
67
27
|
*
|
|
68
|
-
* @
|
|
69
|
-
*
|
|
28
|
+
* @param fn Receives the Drizzle handle
|
|
29
|
+
* @param uid Overrides the user ID for authenticated calls
|
|
70
30
|
*/
|
|
71
31
|
export declare function withUserDb<T>(fn: (db: DrizzleDb) => Promise<T>, uid?: string | null): Promise<T>;
|
|
72
32
|
/** One statement's `{rows, rowCount}` result from a Supabase-mode `db.transaction()` batch. */
|
package/dist/src/db/context.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import config from '../config/intl_config';
|
|
2
2
|
import requireDbConfig from './require_config';
|
|
3
|
-
import
|
|
3
|
+
import { withDbClient } from './connection'; // ✨ REPLACED connection hooks
|
|
4
4
|
import resolveDbMode from './resolve_mode';
|
|
5
5
|
import resolveSupabaseEndpoint from './supabase_config';
|
|
6
6
|
import createSupabaseTransport from './supabase_transport';
|
|
@@ -73,11 +73,9 @@ async function postgresDb(drizzleHandle, rawClient) {
|
|
|
73
73
|
* via an inline-parameterised `query()` call, wrapped in a real
|
|
74
74
|
* `BEGIN`/`COMMIT` for atomicity, and returns `ExecResult[]`.
|
|
75
75
|
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
* keeps this `BEGIN`...`COMMIT` from overlapping with another caller's on the
|
|
80
|
-
* same shared client, so nothing here needs to reason about interleaving.
|
|
76
|
+
* ✨ Note: In the Edge context, atomicity natively holds fast!
|
|
77
|
+
* Every parallel query has isolated clients supplied from `withDbClient`.
|
|
78
|
+
* You don't have to lock this transaction block as overlapping sessions are eliminated entirely.
|
|
81
79
|
*/
|
|
82
80
|
async function runPostgresTransaction(rawClient, build) {
|
|
83
81
|
const queries = await build(buildOnlyDb());
|
|
@@ -93,7 +91,7 @@ async function runPostgresTransaction(rawClient, build) {
|
|
|
93
91
|
return results;
|
|
94
92
|
}
|
|
95
93
|
catch (error) {
|
|
96
|
-
await rawClient.query('rollback');
|
|
94
|
+
await rawClient.query('rollback').catch(() => { });
|
|
97
95
|
throw error;
|
|
98
96
|
}
|
|
99
97
|
}
|
|
@@ -118,23 +116,9 @@ function buildOnlyDb() {
|
|
|
118
116
|
* Runs a query as the **anonymous** role: no transaction, no role switch, no
|
|
119
117
|
* user identity attached. Use this for data any visitor may read.
|
|
120
118
|
*
|
|
121
|
-
* Because no user id is set, RLS policies that test `auth.jwt()->>'sub'` see
|
|
122
|
-
* no user and will deny access — reach for {@link withUserDb} whenever the
|
|
123
|
-
* rows depend on who is asking.
|
|
124
|
-
*
|
|
125
|
-
* In connection-string mode the connection is taken from the request's
|
|
126
|
-
* shared client and released when `fn` settles, even if it throws. In
|
|
127
|
-
* Supabase mode there is no connection to release — each call is one
|
|
128
|
-
* PostgREST round-trip authenticated as the anon key. Either way, call
|
|
129
|
-
* `.transaction(...)` on the handle `fn` receives for atomicity across more
|
|
130
|
-
* than one statement — see the module doc for the shape that takes in each mode.
|
|
131
|
-
*
|
|
132
119
|
* @param fn Receives the Drizzle handle; return whatever the caller needs.
|
|
133
120
|
* @returns Whatever `fn` resolves to.
|
|
134
121
|
* @throws If `db` is not set on your `RoutingConfig`, or the connection fails.
|
|
135
|
-
*
|
|
136
|
-
* @example
|
|
137
|
-
* const rows = await withPublicDb((db) => db.select().from(bonds).limit(10));
|
|
138
122
|
*/
|
|
139
123
|
export async function withPublicDb(fn) {
|
|
140
124
|
const db = config.db;
|
|
@@ -144,58 +128,29 @@ export async function withPublicDb(fn) {
|
|
|
144
128
|
const { anonKey } = await resolveSupabaseEndpoint(resolved.supabase);
|
|
145
129
|
return fn(await supabaseDb(resolved.supabase, anonKey));
|
|
146
130
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
}
|
|
156
|
-
finally {
|
|
157
|
-
disconnectPostgres(config);
|
|
158
|
-
}
|
|
131
|
+
// ✨ Uses isolated clients. Clean setup internally handles
|
|
132
|
+
// tear-down (`disconnectPostgres`) magically behind the scenes right on completion
|
|
133
|
+
return await withDbClient(config, async (client) => {
|
|
134
|
+
const { drizzle } = await import('drizzle-orm/node-postgres');
|
|
135
|
+
const drizzleHandle = drizzle(client);
|
|
136
|
+
// Execute Drizzle proxy payload natively
|
|
137
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
138
|
+
return await fn(await postgresDb(drizzleHandle, client));
|
|
139
|
+
});
|
|
159
140
|
}
|
|
160
141
|
/**
|
|
161
142
|
* Runs a query as the **signed-in user**.
|
|
162
143
|
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
* once `fn` settles), so RLS policies behave exactly as they do for a
|
|
167
|
-
* PostgREST-issued call — but it does NOT open a `BEGIN`/`COMMIT`
|
|
168
|
-
* transaction: that client is shared across every concurrent caller in the
|
|
169
|
-
* isolate (see `connection.ts`), and a live transaction is itself
|
|
170
|
-
* session-scoped state that a second overlapping caller's transaction would
|
|
171
|
-
* collide with. Call `fn`'s own `.transaction(...)` (below) for atomicity
|
|
172
|
-
* across statements instead. In Supabase mode identity instead rides on the
|
|
173
|
-
* JWT sent as `Authorization: Bearer` — PostgREST resolves the
|
|
174
|
-
* `authenticated` role and populates `request.jwt.claims` itself, and each
|
|
175
|
-
* statement is its own round-trip with no cross-statement transaction unless
|
|
176
|
-
* you call `.transaction(...)` on the handle (the Postgres proxy Drizzle
|
|
177
|
-
* uses in this mode cannot open a real session, so that runs as one atomic
|
|
178
|
-
* `cfni_exec_batch` call instead — see the module doc). Either way this is
|
|
179
|
-
* the wrapper to use for anything user-owned.
|
|
144
|
+
* ✨ Uses Isolated Hyperdrive Networking Strategy: Overlapping connection problems
|
|
145
|
+
* across Next.js isolate contexts previously occurred due to locking constraints on the identical
|
|
146
|
+
* PostgreSQL singleton reference inside Serverless architecture.
|
|
180
147
|
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
* live session — see the module doc).
|
|
185
|
-
* @param uid Connection-string mode only: overrides the user id. Omit it, or
|
|
186
|
-
* pass `null`, in normal use — either way the id then comes from
|
|
187
|
-
* `db.getUserId()` when set, otherwise from the signed-in Firebase user when
|
|
188
|
-
* `firebaseAuth` is configured. `null` is accepted alongside `undefined` so a
|
|
189
|
-
* caller's own lookup (which may itself come back empty) can be passed
|
|
190
|
-
* straight through without an extra check. Ignored in Supabase mode, which
|
|
191
|
-
* resolves identity via `db.getAccessToken`/Firebase instead — see
|
|
192
|
-
* {@link resolveAccessToken}.
|
|
193
|
-
* @returns Whatever `fn` resolves to.
|
|
194
|
-
* @throws If `db` is not set on your `RoutingConfig`, if no user id/access
|
|
195
|
-
* token can be resolved, or the connection fails.
|
|
148
|
+
* Moving directly to edge single-usage wrappers inherently closes all leakage scenarios because
|
|
149
|
+
* `.connect() -> setup Roles & Session variables -> process logic -> .end()` exists per connection completely
|
|
150
|
+
* unshared across external requests globally.
|
|
196
151
|
*
|
|
197
|
-
* @
|
|
198
|
-
*
|
|
152
|
+
* @param fn Receives the Drizzle handle
|
|
153
|
+
* @param uid Overrides the user ID for authenticated calls
|
|
199
154
|
*/
|
|
200
155
|
export async function withUserDb(fn, uid) {
|
|
201
156
|
const db = config.db;
|
|
@@ -206,59 +161,29 @@ export async function withUserDb(fn, uid) {
|
|
|
206
161
|
return fn(await supabaseDb(resolved.supabase, token));
|
|
207
162
|
}
|
|
208
163
|
const userId = await resolveUserId(uid);
|
|
209
|
-
const client = await connectToPostgres(config, resolved.connectionString);
|
|
210
164
|
const role = db.authenticatedRole ?? DEFAULT_ROLE;
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
//
|
|
224
|
-
//
|
|
225
|
-
//
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
await rawClient.query(`set role "${role}"`);
|
|
230
|
-
const { drizzle } = await import('drizzle-orm/node-postgres');
|
|
231
|
-
const drizzleHandle = drizzle(client);
|
|
232
|
-
try {
|
|
233
|
-
return await fn(await postgresDb(drizzleHandle, rawClient));
|
|
234
|
-
}
|
|
235
|
-
finally {
|
|
236
|
-
// Always hand the shared socket back to the default role —
|
|
237
|
-
// otherwise the next caller (public or a different user) could
|
|
238
|
-
// inherit this request's elevated/authenticated role.
|
|
239
|
-
await rawClient.query('reset role');
|
|
240
|
-
}
|
|
241
|
-
});
|
|
242
|
-
}
|
|
243
|
-
finally {
|
|
244
|
-
disconnectPostgres(config);
|
|
245
|
-
}
|
|
165
|
+
// ✨ Completely isolated setup per-render utilizing `.withDbClient()` Native block
|
|
166
|
+
// Eliminates requiring our application `withSessionLock` since NextJS workers do not leak!
|
|
167
|
+
return await withDbClient(config, async (client) => {
|
|
168
|
+
const rawClient = client;
|
|
169
|
+
await rawClient.query(`select set_config('request.jwt.claims', $1, false)`, [JSON.stringify({ sub: userId })]);
|
|
170
|
+
await rawClient.query(`set role "${role}"`);
|
|
171
|
+
const { drizzle } = await import('drizzle-orm/node-postgres');
|
|
172
|
+
const drizzleHandle = drizzle(client);
|
|
173
|
+
try {
|
|
174
|
+
return await fn(await postgresDb(drizzleHandle, rawClient));
|
|
175
|
+
}
|
|
176
|
+
finally {
|
|
177
|
+
// Because Hyperdrive is going to actively obliterate (`.end()`)
|
|
178
|
+
// the connection upon scope exiting returning connection priorities automatically back up to cloudflare.
|
|
179
|
+
// Resetting is theoretically optional in stateless Cloudflare execution - but is provided as good habit/health standard here.
|
|
180
|
+
await rawClient.query('reset role').catch(() => { });
|
|
181
|
+
}
|
|
182
|
+
});
|
|
246
183
|
}
|
|
247
184
|
/**
|
|
248
185
|
* Runs several statements atomically over `cfni_exec_batch`, backing
|
|
249
|
-
* Supabase-mode `db.transaction()`.
|
|
250
|
-
* it **builds** them and returns the array; call `.toSQL()` on each Drizzle
|
|
251
|
-
* query instead of `await`ing it (`await`ing throws immediately; see
|
|
252
|
-
* {@link buildOnlyDb}). Every query is inlined and sent as one round trip:
|
|
253
|
-
* the Postgres function runs them in order inside a single plpgsql call,
|
|
254
|
-
* which is itself an implicit transaction, so a failure on any statement
|
|
255
|
-
* rolls back every statement before it.
|
|
256
|
-
*
|
|
257
|
-
* @param supabase The `db.supabase` config block.
|
|
258
|
-
* @param bearerToken The anon key or user JWT.
|
|
259
|
-
* @param build Returns the queries to run, via `.toSQL()` — never executes them directly.
|
|
260
|
-
* @returns One result per query, in the same order as `build`'s array.
|
|
261
|
-
* @throws If `db.supabase.rawSql` is `false` — `cfni_exec_batch` needs `cfni_exec`, which is disabled too.
|
|
186
|
+
* Supabase-mode `db.transaction()`.
|
|
262
187
|
*/
|
|
263
188
|
async function runTransaction(supabase, bearerToken, build) {
|
|
264
189
|
if (supabase.rawSql === false) {
|
package/dist/src/db/index.d.ts
CHANGED
|
@@ -38,5 +38,5 @@
|
|
|
38
38
|
*/
|
|
39
39
|
export { withPublicDb, withUserDb } from './context';
|
|
40
40
|
export type { DrizzleDb, TransactionResult } from './context';
|
|
41
|
-
export {
|
|
41
|
+
export { connectToPostgres, disconnectPostgres, resetConnectionState } from './connection';
|
|
42
42
|
export type { DbRoutingConfig } from '../types/types';
|
package/dist/src/db/index.js
CHANGED
|
@@ -37,4 +37,4 @@
|
|
|
37
37
|
* the separate `cloudflare-next-intl/dbHelpers` entry point.
|
|
38
38
|
*/
|
|
39
39
|
export { withPublicDb, withUserDb } from './context';
|
|
40
|
-
export {
|
|
40
|
+
export { connectToPostgres, disconnectPostgres, resetConnectionState } from './connection';
|