File size: 3,497 Bytes
7f88bdf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
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<Response> {
  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;
}