import { createClient, type SupabaseClient } from "@supabase/supabase-js"; let cachedClient: SupabaseClient | null = null; function parsePositiveInt(value: string | undefined, fallback: number) { const parsed = Number.parseInt(value ?? "", 10); return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback; } function parseRetryAttempts(value: string | undefined, fallback: number) { const parsed = Number.parseInt(value ?? "", 10); if (!Number.isFinite(parsed)) { return fallback; } return Math.max(0, parsed); } function shouldRetryStatus(status: number) { return status === 408 || status === 425 || status === 429 || status >= 500; } function isRetryableNetworkError(error: unknown) { if (!error || typeof error !== "object") { return false; } const name = "name" in error ? String((error as { name?: unknown }).name ?? "") : ""; const message = "message" in error ? String((error as { message?: unknown }).message ?? "") : ""; if (name === "AbortError") { return true; } return /fetch failed|network|socket|timed out|timeout|econnreset|enotfound|eai_again/i.test( message ); } function isIdempotentMethod(method: string) { return method === "GET" || method === "HEAD" || method === "OPTIONS"; } function sleep(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)); } async function fetchWithRetry(input: RequestInfo | URL, init?: RequestInit): Promise { const method = (init?.method ?? "GET").toUpperCase(); const retryAttempts = parseRetryAttempts(process.env.SUPABASE_HTTP_RETRY_ATTEMPTS, 2); const timeoutMs = parsePositiveInt(process.env.SUPABASE_HTTP_TIMEOUT_MS, 15_000); const backoffMs = parsePositiveInt(process.env.SUPABASE_HTTP_RETRY_BACKOFF_MS, 350); const allowWriteRetries = process.env.SUPABASE_HTTP_RETRY_WRITES === "1"; const canRetry = allowWriteRetries || isIdempotentMethod(method); const totalAttempts = canRetry ? retryAttempts + 1 : 1; let attempt = 0; let lastError: unknown = null; while (attempt < totalAttempts) { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), timeoutMs); try { const response = await fetch(input, { ...init, signal: controller.signal }); clearTimeout(timeout); const isFinalAttempt = attempt === totalAttempts - 1; if (!shouldRetryStatus(response.status) || isFinalAttempt) { return response; } } catch (error) { clearTimeout(timeout); lastError = error; const isFinalAttempt = attempt === totalAttempts - 1; if (!canRetry || !isRetryableNetworkError(error) || isFinalAttempt) { throw error; } } attempt += 1; const waitMs = backoffMs * Math.max(1, attempt); await sleep(waitMs); } if (lastError) { throw lastError; } throw new Error("Supabase fetch retry exhausted without response."); } export function getSupabaseAdmin() { if (cachedClient) { return cachedClient; } const url = process.env.NEXT_PUBLIC_SUPABASE_URL; const key = process.env.SUPABASE_SERVICE_ROLE_KEY; if (!url || !key) { throw new Error( "Missing Supabase configuration. Set NEXT_PUBLIC_SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY." ); } cachedClient = createClient(url, key, { auth: { persistSession: false, autoRefreshToken: false }, global: { fetch: fetchWithRetry } }); return cachedClient; }