cloudflare-next-intl 0.8.23 → 0.8.24

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 CHANGED
@@ -461,23 +461,21 @@ export default setIntlConfig({
461
461
  returning one (resolved on each connect). The function form is how you reach
462
462
  a value that isn't available at module scope — a Cloudflare Hyperdrive
463
463
  binding, or a secret store — as in the example above.
464
- - `disconnectAfterRequest` — whether the pooled client is closed once the
465
- last in-flight `withPublicDb`/`withUserDb` call of the request
466
- finishes. Defaults to `true` (one connection per request, released to
467
- Hyperdrive immediately). Set `false` to keep the connection open for the
468
- lifetime of the isolate faster for a long-lived server, but it holds a
469
- Hyperdrive connection slot between requests.
470
- - `authenticatedRole` Postgres role `withUserDb` switches the shared
471
- session to for the duration of your callback (`set role`, reset once it
472
- settles — no transaction involved). Defaults to `'authenticated'` (the
473
- Supabase RLS convention).
464
+ - `disconnectAfterRequest` — **deprecated, ignored since 0.8.23.** Every
465
+ `withPublicDb`/`withUserDb` call now opens and closes its own client, so
466
+ there is no surviving connection to keep open. Hyperdrive pools the
467
+ server-side connection.
468
+ - `authenticatedRole`Postgres role `withUserDb` switches its
469
+ call-scoped session to for the duration of your callback (`set role`, no
470
+ transaction involved; the session is closed when the call ends). Defaults
471
+ to `'authenticated'` (the Supabase RLS convention).
474
472
  - `getUserId` — resolves the user id injected as
475
473
  `request.jwt.claims->>'sub'` inside `withUserDb`. Omit when
476
474
  `firebaseAuth` is configured — the uid then comes from this package's own
477
475
  `getAuthUser()` automatically. Provide it to use a different auth source
478
476
  (or when `firebaseAuth` is absent).
479
- - `disconnectTimeoutMs` — milliseconds `disconnectPostgres` waits for
480
- `client.end()` before giving up. Defaults to `2000`.
477
+ - `disconnectTimeoutMs` — **deprecated, ignored since 0.8.23.** Client
478
+ teardown is awaited or deferred to `ctx.waitUntil` without a timeout.
481
479
 
482
480
  #### Choosing a transport
483
481
 
@@ -1,13 +1,39 @@
1
- import { Client } from 'pg';
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
4
  /**
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.
5
+ * Runs `queryFn` on a Postgres client scoped to this single call: one
6
+ * `connect()`, your callback, then a guaranteed `end()`. Each call gets its own
7
+ * client, so concurrent renders in the same isolate can never share session
8
+ * state (role, `request.jwt.claims`, an open transaction) with each other.
9
+ * Hyperdrive pools the server-side connection behind this.
8
10
  */
9
11
  export declare function withDbClient<T>(config: DbConfig, queryFn: (client: Client) => Promise<T>): Promise<T>;
12
+ /**
13
+ * No-op kept for backward compatibility. There is no cached connection state
14
+ * to reset now that every {@link withDbClient} call owns its client.
15
+ *
16
+ * @deprecated Connection state is per-call; this does nothing.
17
+ */
10
18
  export declare function resetConnectionState(): void;
19
+ /**
20
+ * Runs `fn` directly. Kept for backward compatibility: session state can no
21
+ * longer leak between callers, so there is nothing left to serialize.
22
+ *
23
+ * @deprecated Clients are per-call now; no lock is needed.
24
+ */
11
25
  export declare function withSessionLock<T>(fn: () => Promise<T>): Promise<T>;
26
+ /**
27
+ * Opens a Postgres client the caller owns and must close with
28
+ * {@link disconnectPostgres}. Prefer {@link withDbClient}, which closes the
29
+ * client for you even when the callback throws.
30
+ *
31
+ * @deprecated Use {@link withDbClient} instead.
32
+ */
12
33
  export declare function connectToPostgres(config: DbConfig): Promise<Client>;
13
- export declare function disconnectPostgres(config: DbConfig): void;
34
+ /**
35
+ * Closes a client from {@link connectToPostgres}.
36
+ *
37
+ * @deprecated Use {@link withDbClient} instead.
38
+ */
39
+ export declare function disconnectPostgres(client?: Client): Promise<void>;
@@ -1,9 +1,16 @@
1
- import { Client } from 'pg';
2
1
  import reportError from '../error_handling/report_error';
3
2
  import requireDbConfig from './require_config';
4
3
  import resolveConfigValue from './resolve_config_value';
5
- // Removed Global Singletons (no `client`, `connectionString`, `activeUsers`, `connectionPromise`)
6
- // Removed Custom Locks (no `serializeQueries`, no `sessionLock`)
4
+ let pgModule;
5
+ /**
6
+ * Loads `pg` lazily, so an app that never touches the Postgres transport never
7
+ * bundles it, and caches the module promise so concurrent callers share one
8
+ * resolution instead of racing separate `import()` calls.
9
+ */
10
+ function loadPg() {
11
+ pgModule ?? (pgModule = import('pg'));
12
+ return pgModule;
13
+ }
7
14
  async function resolveConnectionString(db) {
8
15
  const configured = await resolveConfigValue(db.connectionString);
9
16
  if (configured)
@@ -13,70 +20,99 @@ async function resolveConnectionString(db) {
13
20
  'Hyperdrive binding off `getCloudflareContext().env`).');
14
21
  }
15
22
  /**
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.
23
+ * Runs `queryFn` on a Postgres client scoped to this single call: one
24
+ * `connect()`, your callback, then a guaranteed `end()`. Each call gets its own
25
+ * client, so concurrent renders in the same isolate can never share session
26
+ * state (role, `request.jwt.claims`, an open transaction) with each other.
27
+ * Hyperdrive pools the server-side connection behind this.
19
28
  */
20
29
  export async function withDbClient(config, queryFn) {
21
30
  const db = config.db;
22
31
  requireDbConfig(db);
23
32
  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 });
33
+ const { Client: PgClient } = await loadPg();
34
+ const client = new PgClient({ connectionString });
26
35
  let result;
36
+ let connected = false;
27
37
  try {
28
- await client.connect();
29
- // Pass this client independently straight into your callback to be executed
30
- result = await queryFn(client);
31
- }
32
- catch (error) {
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' });
38
+ try {
39
+ await client.connect();
40
+ connected = true;
37
41
  }
38
- throw error;
42
+ catch (error) {
43
+ const message = error instanceof Error ? error.message : String(error ?? '');
44
+ if (!/(connection terminated|connection closed|socket closed|unexpected eof)/i.test(message)) {
45
+ void reportError({ errorHandling: config.errorHandling, generate: config.generate }, { error, classOrMethodName: 'db.withDbClient.connectError' });
46
+ }
47
+ throw error;
48
+ }
49
+ result = await queryFn(client);
39
50
  }
40
51
  finally {
41
- // GUARANTEED TEAR-DOWN
42
- // Triggers `.end()` right when your data has finished being processed.
43
- const endPromise = client.end();
52
+ const endPromise = connected ? client.end() : Promise.resolve();
44
53
  const getContext = config.generate?.getCloudflareContext;
45
54
  if (!getContext || db.disconnectAfterRequest === false) {
46
- await endPromise.catch(() => { });
55
+ await endPromise.catch(() => undefined);
47
56
  }
48
57
  else {
49
58
  try {
50
59
  const context = await getContext({ async: true });
51
60
  if (typeof context?.ctx?.waitUntil === 'function') {
52
- context.ctx.waitUntil(endPromise.catch(() => { }));
61
+ context.ctx.waitUntil(endPromise.catch(() => undefined));
53
62
  }
54
63
  else {
55
- await endPromise.catch(() => { });
64
+ await endPromise.catch(() => undefined);
56
65
  }
57
66
  }
58
67
  catch {
59
- await endPromise.catch(() => { });
68
+ await endPromise.catch(() => undefined);
60
69
  }
61
70
  }
62
71
  }
63
72
  return result;
64
73
  }
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
- // -------------------------------------------------------------
74
+ /**
75
+ * No-op kept for backward compatibility. There is no cached connection state
76
+ * to reset now that every {@link withDbClient} call owns its client.
77
+ *
78
+ * @deprecated Connection state is per-call; this does nothing.
79
+ */
70
80
  export function resetConnectionState() {
71
- // Obsolete - ignored
81
+ // no cached state to reset
72
82
  }
83
+ /**
84
+ * Runs `fn` directly. Kept for backward compatibility: session state can no
85
+ * longer leak between callers, so there is nothing left to serialize.
86
+ *
87
+ * @deprecated Clients are per-call now; no lock is needed.
88
+ */
73
89
  export async function withSessionLock(fn) {
74
- // Locks are no longer needed as clients are entirely isolated
75
90
  return await fn();
76
91
  }
92
+ /**
93
+ * Opens a Postgres client the caller owns and must close with
94
+ * {@link disconnectPostgres}. Prefer {@link withDbClient}, which closes the
95
+ * client for you even when the callback throws.
96
+ *
97
+ * @deprecated Use {@link withDbClient} instead.
98
+ */
77
99
  export async function connectToPostgres(config) {
78
- throw new Error('CRITICAL REFACTOR: Replace `connectToPostgres` with `withDbClient(config, async (client) => { ... })` for Cloudflare Hyperdrive.');
100
+ const db = config.db;
101
+ requireDbConfig(db);
102
+ const connectionString = await resolveConnectionString(db);
103
+ const { Client: PgClient } = await loadPg();
104
+ const client = new PgClient({ connectionString });
105
+ client.on('error', (error) => {
106
+ void reportError({ errorHandling: config.errorHandling, generate: config.generate }, { error, classOrMethodName: 'db.connectToPostgres.clientError' });
107
+ });
108
+ await client.connect();
109
+ return client;
79
110
  }
80
- export function disconnectPostgres(config) {
81
- // Obsolete - disconnected natively in withDbClient now.
111
+ /**
112
+ * Closes a client from {@link connectToPostgres}.
113
+ *
114
+ * @deprecated Use {@link withDbClient} instead.
115
+ */
116
+ export async function disconnectPostgres(client) {
117
+ await client?.end().catch(() => undefined);
82
118
  }
@@ -15,15 +15,11 @@ export type DrizzleDb = NodePgDatabase<Record<string, never>>;
15
15
  */
16
16
  export declare function withPublicDb<T>(fn: (db: DrizzleDb) => Promise<T>): Promise<T>;
17
17
  /**
18
- * Runs a query as the **signed-in user**.
18
+ * Runs a query as the **signed-in user**, with `request.jwt.claims` and the
19
+ * authenticated role set on the session so RLS policies apply to their id.
19
20
  *
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.
23
- *
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.
21
+ * The session lives on a client scoped to this call and closed when it ends,
22
+ * so the role and claims can never be observed by another caller.
27
23
  *
28
24
  * @param fn Receives the Drizzle handle
29
25
  * @param uid Overrides the user ID for authenticated calls
@@ -1,6 +1,6 @@
1
1
  import config from '../config/intl_config';
2
2
  import requireDbConfig from './require_config';
3
- import { withDbClient } from './connection'; // ✨ REPLACED connection hooks
3
+ import { withDbClient } from './connection';
4
4
  import resolveDbMode from './resolve_mode';
5
5
  import resolveSupabaseEndpoint from './supabase_config';
6
6
  import createSupabaseTransport from './supabase_transport';
@@ -73,9 +73,8 @@ 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
- * 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.
76
+ * The client is scoped to one `withDbClient` call, so no other caller can
77
+ * interleave statements into this transaction.
79
78
  */
80
79
  async function runPostgresTransaction(rawClient, build) {
81
80
  const queries = await build(buildOnlyDb());
@@ -91,7 +90,7 @@ async function runPostgresTransaction(rawClient, build) {
91
90
  return results;
92
91
  }
93
92
  catch (error) {
94
- await rawClient.query('rollback').catch(() => { });
93
+ await rawClient.query('rollback').catch(() => undefined);
95
94
  throw error;
96
95
  }
97
96
  }
@@ -128,26 +127,19 @@ export async function withPublicDb(fn) {
128
127
  const { anonKey } = await resolveSupabaseEndpoint(resolved.supabase);
129
128
  return fn(await supabaseDb(resolved.supabase, anonKey));
130
129
  }
131
- // ✨ Uses isolated clients. Clean setup internally handles
132
- // tear-down (`disconnectPostgres`) magically behind the scenes right on completion
133
130
  return await withDbClient(config, async (client) => {
134
131
  const { drizzle } = await import('drizzle-orm/node-postgres');
135
132
  const drizzleHandle = drizzle(client);
136
- // Execute Drizzle proxy payload natively
137
133
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
138
134
  return await fn(await postgresDb(drizzleHandle, client));
139
135
  });
140
136
  }
141
137
  /**
142
- * Runs a query as the **signed-in user**.
138
+ * Runs a query as the **signed-in user**, with `request.jwt.claims` and the
139
+ * authenticated role set on the session so RLS policies apply to their id.
143
140
  *
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.
147
- *
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.
141
+ * The session lives on a client scoped to this call and closed when it ends,
142
+ * so the role and claims can never be observed by another caller.
151
143
  *
152
144
  * @param fn Receives the Drizzle handle
153
145
  * @param uid Overrides the user ID for authenticated calls
@@ -162,23 +154,13 @@ export async function withUserDb(fn, uid) {
162
154
  }
163
155
  const userId = await resolveUserId(uid);
164
156
  const role = db.authenticatedRole ?? DEFAULT_ROLE;
165
- // ✨ Completely isolated setup per-render utilizing `.withDbClient()` Native block
166
- // Eliminates requiring our application `withSessionLock` since NextJS workers do not leak!
167
157
  return await withDbClient(config, async (client) => {
168
158
  const rawClient = client;
169
159
  await rawClient.query(`select set_config('request.jwt.claims', $1, false)`, [JSON.stringify({ sub: userId })]);
170
160
  await rawClient.query(`set role "${role}"`);
171
161
  const { drizzle } = await import('drizzle-orm/node-postgres');
172
162
  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
- }
163
+ return await fn(await postgresDb(drizzleHandle, rawClient));
182
164
  });
183
165
  }
184
166
  /**
@@ -38,5 +38,5 @@
38
38
  */
39
39
  export { withPublicDb, withUserDb } from './context';
40
40
  export type { DrizzleDb, TransactionResult } from './context';
41
- export { connectToPostgres, disconnectPostgres, resetConnectionState } from './connection';
41
+ export { withDbClient, connectToPostgres, disconnectPostgres, resetConnectionState } from './connection';
42
42
  export type { DbRoutingConfig } from '../types/types';
@@ -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 { connectToPostgres, disconnectPostgres, resetConnectionState } from './connection';
40
+ export { withDbClient, connectToPostgres, disconnectPostgres, resetConnectionState } from './connection';
@@ -884,10 +884,12 @@ export interface DbRoutingConfig {
884
884
  /**
885
885
  * Whether the pooled client is closed once the last in-flight
886
886
  * `withPublicDb`/`withUserDb` call of the request finishes.
887
- * Defaults to `true` (one connection per request, released to Hyperdrive
888
- * immediately). Set `false` to keep the connection open for the lifetime
889
- * of the isolate faster for a long-lived server, but it holds a
890
- * Hyperdrive connection slot between requests.
887
+ *
888
+ * @deprecated Ignored since 0.8.23. Every `withPublicDb`/`withUserDb` call
889
+ * now opens and closes its own client, so no connection survives a call to
890
+ * be kept open. `true` and `false` behave identically; the only difference
891
+ * is that `false` awaits the close instead of deferring it to
892
+ * `ctx.waitUntil`. Hyperdrive pools the server-side connection.
891
893
  */
892
894
  disconnectAfterRequest?: boolean;
893
895
  /**
@@ -902,7 +904,12 @@ export interface DbRoutingConfig {
902
904
  * to use a different auth source (or when `firebaseAuth` is absent).
903
905
  */
904
906
  getUserId?: () => Promise<string | null> | string | null;
905
- /** Milliseconds `disconnectPostgres` waits for `client.end()` before giving up. Defaults to `2000`. */
907
+ /**
908
+ * Milliseconds `disconnectPostgres` waits for `client.end()` before giving up.
909
+ *
910
+ * @deprecated Ignored since 0.8.23. Client teardown is awaited or deferred
911
+ * to `ctx.waitUntil` without a timeout.
912
+ */
906
913
  disconnectTimeoutMs?: number;
907
914
  /**
908
915
  * Reaches Postgres through the Supabase Data API instead of a direct
package/llms.txt CHANGED
@@ -53,11 +53,11 @@ Two transports, picked by which `db` fields are set — `pg`/`drizzle-orm`/`@sup
53
53
 
54
54
  - Direct Postgres (wins if configured): `db.connectionString` — Postgres connection string, or a sync/async function returning one (resolved on each connect). The function form is the way to read a value unavailable at module scope, e.g. a Cloudflare Hyperdrive binding: `connectionString: async () => (await getCloudflareContext({ async: true })).env.HYPERDRIVE.connectionString`. There is no separate `hyperdriveBinding` option.
55
55
  - Supabase Data API (used only when neither of the above is set): `db.supabase` — `{ url?, anonKey?, execFunction?, rawSql? }` where `url`/`anonKey` each accept a string or a sync/async function returning one, defaulting `url`/`anonKey` to `NEXT_PUBLIC_SUPABASE_URL`/`NEXT_PUBLIC_SUPABASE_ANON_KEY`. Statements are translated to PostgREST REST calls first; unsupported statements fall back to `supabase/cfni_exec.sql` (a `security invoker` SQL-exec function) in your database — `cfni-db-codegen`/`cfni-db-install-exec` can install it for you (see below). No multi-statement transactions — each statement in a `withUserDb` callback is its own round-trip; `.transaction()` throws instead of running non-atomically. `rawSql: false` disables `cfni_exec` fallback, throwing an informative error when a query cannot be served over REST.
56
- - `db.disconnectAfterRequest` — direct-Postgres mode only: closes the pooled client once the last in-flight `withPublicDb`/`withUserDb` call of the request finishes. Defaults to `true`; set `false` to keep the connection open for the isolate's lifetime (holds a Hyperdrive slot between requests).
56
+ - `db.disconnectAfterRequest` — deprecated, ignored since 0.8.23. Each `withPublicDb`/`withUserDb` call opens and closes its own client; Hyperdrive pools the server-side connection.
57
57
  - `db.authenticatedRole` — direct-Postgres mode only: Postgres role assumed inside `withUserDb`'s transaction. Defaults to `'authenticated'` (Supabase RLS convention).
58
58
  - `db.getUserId` — direct-Postgres mode only: resolves the user id injected as `request.jwt.claims->>'sub'` in `withUserDb`. Omit when `firebaseAuth` is configured — the uid is then taken automatically from the signed-in Firebase user via this package's own `getAuthUser()`.
59
59
  - `db.getAccessToken` — Supabase mode only: resolves the JWT sent as `Authorization: Bearer` in `withUserDb`, which is what makes PostgREST resolve `authenticated` and apply RLS. Omit when `firebaseAuth` is configured — the signed-in user's Firebase ID token is used automatically.
60
- - `db.disconnectTimeoutMs` — direct-Postgres mode only: ms `disconnectPostgres` waits for `client.end()` before giving up. Defaults to `2000`.
60
+ - `db.disconnectTimeoutMs` — deprecated, ignored since 0.8.23. Teardown is awaited or deferred to `ctx.waitUntil` without a timeout.
61
61
  - `withPublicDb(fn)` — anonymous role. Direct-Postgres mode: the request's pooled connection, no transaction, no role switch. Supabase mode: the anon key as the PostgREST bearer token. Either way, no user id is attached — RLS keyed on `auth.jwt()` denies access.
62
62
  - `withUserDb(fn, uid?)` — signed-in-user role. Direct-Postgres mode: a transaction with `set_config('request.jwt.claims', ...)` + `set local role`, `uid` resolution order explicit arg → `db.getUserId()` → Firebase auth uid → throws. Supabase mode: identity rides on the JWT from `db.getAccessToken`/Firebase instead (`uid` param is ignored), no transaction wraps the call.
63
63
  - `./dbHelpers` functions are plain Drizzle `sql`-building utilities with no config dependency — usable standalone.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cloudflare-next-intl",
3
- "version": "0.8.23",
3
+ "version": "0.8.24",
4
4
  "description": "Optimized Next Intl Package Special for App Router and Cloudflare",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",