cloudflare-next-intl 0.8.17 → 0.8.18
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 +13 -0
- package/dist/src/db/connection.js +52 -25
- package/dist/src/db/context.js +18 -14
- package/package.json +1 -1
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
import type { Client } from 'pg';
|
|
2
2
|
import type { LocalePrefixMode, Locales, RoutingConfig } from '../types/types';
|
|
3
3
|
export type DbConfig = RoutingConfig<Locales, LocalePrefixMode>;
|
|
4
|
+
/**
|
|
5
|
+
* Runs `fn` exclusively against the shared client: no other
|
|
6
|
+
* `withSessionLock` caller's queries can interleave with `fn`'s until it
|
|
7
|
+
* settles. `serializeQueries` alone only orders individual `.query()` calls —
|
|
8
|
+
* it does nothing to stop a *different* concurrent request's queries from
|
|
9
|
+
* landing between, say, a transaction's `BEGIN`/`SET LOCAL ROLE` and its
|
|
10
|
+
* `COMMIT` on the one `pg.Client` every request in the isolate shares. That
|
|
11
|
+
* gap let one request's role/RLS identity leak into another's queries
|
|
12
|
+
* whenever two requests overlapped in the same Worker isolate. Every caller
|
|
13
|
+
* that opens a transaction, or otherwise depends on session-scoped state
|
|
14
|
+
* (`SET LOCAL`, `set_config(..., true)`), MUST run inside this lock.
|
|
15
|
+
*/
|
|
16
|
+
export declare function withSessionLock<T>(fn: () => Promise<T>): Promise<T>;
|
|
4
17
|
/**
|
|
5
18
|
* Forgets the cached client and connection string so the next
|
|
6
19
|
* `connectToPostgres` call builds both from scratch. Intended for tests and
|
|
@@ -5,9 +5,33 @@ const DEFAULT_DISCONNECT_TIMEOUT_MS = 2000;
|
|
|
5
5
|
let connectionString = null;
|
|
6
6
|
let client = null;
|
|
7
7
|
let connectionPromise = null;
|
|
8
|
-
let connectingPromise = null;
|
|
9
8
|
let disconnectionPromise = null;
|
|
10
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, a transaction's `BEGIN`/`SET LOCAL ROLE` and its
|
|
17
|
+
* `COMMIT` on the one `pg.Client` every request in the isolate shares. That
|
|
18
|
+
* gap let one request's role/RLS identity leak into another's queries
|
|
19
|
+
* whenever two requests overlapped in the same Worker isolate. Every caller
|
|
20
|
+
* that opens a transaction, or otherwise depends on session-scoped state
|
|
21
|
+
* (`SET LOCAL`, `set_config(..., true)`), MUST run inside this lock.
|
|
22
|
+
*/
|
|
23
|
+
export async function withSessionLock(fn) {
|
|
24
|
+
const previous = sessionLock;
|
|
25
|
+
let release;
|
|
26
|
+
sessionLock = new Promise((resolve) => { release = resolve; });
|
|
27
|
+
await previous;
|
|
28
|
+
try {
|
|
29
|
+
return await fn();
|
|
30
|
+
}
|
|
31
|
+
finally {
|
|
32
|
+
release();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
11
35
|
/**
|
|
12
36
|
* Forgets the cached client and connection string so the next
|
|
13
37
|
* `connectToPostgres` call builds both from scratch. Intended for tests and
|
|
@@ -21,9 +45,9 @@ export function resetConnectionState() {
|
|
|
21
45
|
connectionString = null;
|
|
22
46
|
client = null;
|
|
23
47
|
connectionPromise = null;
|
|
24
|
-
connectingPromise = null;
|
|
25
48
|
disconnectionPromise = null;
|
|
26
49
|
activeUsers = 0;
|
|
50
|
+
sessionLock = Promise.resolve();
|
|
27
51
|
}
|
|
28
52
|
/**
|
|
29
53
|
* Resolves the connection string from `db.connectionString`, awaiting it when
|
|
@@ -76,31 +100,35 @@ export default async function connectToPostgres(config, resolved) {
|
|
|
76
100
|
const db = config.db;
|
|
77
101
|
requireDbConfig(db);
|
|
78
102
|
await disconnectionPromise;
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
// next call retries instead of replaying the same rejection for the
|
|
93
|
-
// life of the Worker isolate.
|
|
94
|
-
connectingPromise = null;
|
|
95
|
-
connectionString = null;
|
|
96
|
-
client = null;
|
|
97
|
-
connectionPromise = null;
|
|
98
|
-
throw error;
|
|
99
|
-
}
|
|
103
|
+
// Guards the race between concurrent callers that both see `client ===
|
|
104
|
+
// null` before either has awaited anything: `connectionPromise` is set
|
|
105
|
+
// synchronously (before the `await`s below) so every caller in the same
|
|
106
|
+
// microtask tick shares the one client being created instead of each
|
|
107
|
+
// starting its own.
|
|
108
|
+
if (connectionPromise === null) {
|
|
109
|
+
connectionPromise = (async () => {
|
|
110
|
+
connectionString = resolved ?? await resolveConnectionString(db);
|
|
111
|
+
const { Client } = await import('pg');
|
|
112
|
+
const created = serializeQueries(new Client({ connectionString }));
|
|
113
|
+
client = created;
|
|
114
|
+
await created.connect();
|
|
115
|
+
return created;
|
|
100
116
|
})();
|
|
101
117
|
}
|
|
102
118
|
activeUsers++;
|
|
103
|
-
|
|
119
|
+
try {
|
|
120
|
+
return await connectionPromise;
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
// A failed connect must not be cached forever — clear state so the
|
|
124
|
+
// next call retries instead of replaying the same rejection for the
|
|
125
|
+
// life of the Worker isolate.
|
|
126
|
+
activeUsers = Math.max(0, activeUsers - 1);
|
|
127
|
+
connectionString = null;
|
|
128
|
+
client = null;
|
|
129
|
+
connectionPromise = null;
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
104
132
|
}
|
|
105
133
|
/**
|
|
106
134
|
* Releases one caller's hold on the shared client, closing it once the last
|
|
@@ -127,7 +155,6 @@ export function disconnectPostgres(config) {
|
|
|
127
155
|
// instead of reusing one that is about to close.
|
|
128
156
|
client = null;
|
|
129
157
|
connectionPromise = null;
|
|
130
|
-
connectingPromise = null;
|
|
131
158
|
const endPromise = closing.end();
|
|
132
159
|
disconnectionPromise = endPromise;
|
|
133
160
|
const timeoutMs = db.disconnectTimeoutMs ?? DEFAULT_DISCONNECT_TIMEOUT_MS;
|
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 connectToPostgres, { disconnectPostgres } from './connection';
|
|
3
|
+
import connectToPostgres, { disconnectPostgres, withSessionLock } from './connection';
|
|
4
4
|
import resolveDbMode from './resolve_mode';
|
|
5
5
|
import resolveSupabaseEndpoint from './supabase_config';
|
|
6
6
|
import createSupabaseTransport from './supabase_transport';
|
|
@@ -131,10 +131,12 @@ export async function withPublicDb(fn) {
|
|
|
131
131
|
}
|
|
132
132
|
const client = await connectToPostgres(config, resolved.connectionString);
|
|
133
133
|
try {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
134
|
+
return await withSessionLock(async () => {
|
|
135
|
+
const { drizzle } = await import('drizzle-orm/node-postgres');
|
|
136
|
+
const drizzleHandle = drizzle(client);
|
|
137
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
138
|
+
return await fn(await postgresDb(drizzleHandle, client));
|
|
139
|
+
});
|
|
138
140
|
}
|
|
139
141
|
finally {
|
|
140
142
|
disconnectPostgres(config);
|
|
@@ -186,15 +188,17 @@ export async function withUserDb(fn, uid) {
|
|
|
186
188
|
const client = await connectToPostgres(config, resolved.connectionString);
|
|
187
189
|
const role = db.authenticatedRole ?? DEFAULT_ROLE;
|
|
188
190
|
try {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
await transaction
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
191
|
+
return await withSessionLock(async () => {
|
|
192
|
+
const { drizzle } = await import('drizzle-orm/node-postgres');
|
|
193
|
+
const { sql } = await import('drizzle-orm');
|
|
194
|
+
return await drizzle(client).transaction(async (transaction) => {
|
|
195
|
+
await transaction.execute(sql `select set_config('request.jwt.claims', ${JSON.stringify({ sub: userId })}, true)`);
|
|
196
|
+
await transaction.execute(sql `set local role ${sql.raw(role)}`);
|
|
197
|
+
// The transaction handle's session.client is the live pg socket — use it directly.
|
|
198
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
199
|
+
const txClient = transaction.session?.client ?? client;
|
|
200
|
+
return fn(await postgresDb(transaction, txClient));
|
|
201
|
+
});
|
|
198
202
|
});
|
|
199
203
|
}
|
|
200
204
|
finally {
|