69 lines
2.7 KiB
TypeScript
69 lines
2.7 KiB
TypeScript
import { createClient, SupabaseClient } from '@supabase/supabase-js';
|
|
import { Agent } from 'undici';
|
|
|
|
let _admin: SupabaseClient | null = null;
|
|
|
|
/**
|
|
* Dedicated undici dispatcher for all admin-client traffic (GoTrue + PostgREST).
|
|
*
|
|
* ROOT CAUSE this addresses: GoTrue/undici intermittently returns an empty or
|
|
* truncated HTTP body when a pooled keep-alive connection is REUSED right after
|
|
* a write (bulk ban/role/delete) and then immediately read back (the auto list
|
|
* refresh). supabase-js then throws `Unexpected end of JSON input` and the
|
|
* route 500s. Node 24's bundled undici reuses connections more aggressively,
|
|
* amplifying the race.
|
|
*
|
|
* `pipelining: 0` disables request pipelining on a connection, and the bounded
|
|
* keep-alive timeouts keep idle sockets from lingering long enough to be reused
|
|
* in a half-closed state. When a request DOES still hit a poisoned socket,
|
|
* undici destroys that socket on error, so the `withAdminRetry` wrapper at the
|
|
* call sites lands on a FRESH connection rather than the same dead one. The two
|
|
* layers together eliminate the empty-body 500s.
|
|
*
|
|
* NOTE: `setGlobalDispatcher` from the npm `undici` package does NOT affect
|
|
* Node's built-in global `fetch` (it bundles its own undici), so we attach this
|
|
* dispatcher explicitly via the `dispatcher` init option, which built-in fetch
|
|
* does honour.
|
|
*/
|
|
const adminDispatcher = new Agent({
|
|
pipelining: 0,
|
|
keepAliveTimeout: 10_000,
|
|
keepAliveMaxTimeout: 10_000,
|
|
});
|
|
|
|
export function getSupabaseAdmin(): SupabaseClient {
|
|
if (_admin) return _admin;
|
|
const url = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
|
const key = process.env.SUPABASE_SERVICE_ROLE_KEY;
|
|
if (!url || !key) {
|
|
throw new Error('Supabase admin env not configured');
|
|
}
|
|
_admin = createClient(url, key, {
|
|
auth: { autoRefreshToken: false, persistSession: false },
|
|
global: {
|
|
// Force every request the admin client makes (GoTrue listUsers and
|
|
// PostgREST reads alike) to bypass Next's fetch Data Cache, so the admin
|
|
// surface always reflects current state regardless of page config, and
|
|
// route it through the dedicated keep-alive-tamed dispatcher above.
|
|
fetch: (input: RequestInfo | URL, init?: RequestInit) =>
|
|
fetch(input, {
|
|
...init,
|
|
cache: 'no-store',
|
|
dispatcher: adminDispatcher,
|
|
} as RequestInit),
|
|
},
|
|
});
|
|
return _admin;
|
|
}
|
|
|
|
export function getSupabaseAnon(): SupabaseClient {
|
|
const url = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
|
const key = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
|
|
if (!url || !key) {
|
|
throw new Error('Supabase anon env not configured');
|
|
}
|
|
return createClient(url, key, {
|
|
auth: { autoRefreshToken: false, persistSession: false },
|
|
});
|
|
}
|