File size: 3,701 Bytes
c09f67c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
119
120
import type { Database, DatabaseWithPrimary } from "@midday/db/client";

/**
 * Retry helper for database queries that may fail due to replication lag
 * or transient connection issues. Tries replica first, falls back to primary on failure.
 *
 * This preserves the benefit of fast replicas while handling replication lag gracefully.
 *
 * @param db - The database instance (may be a replica)
 * @param fn - The database query function to execute
 * @param options - Configuration options
 * @param options.maxRetries - Maximum number of retry attempts on primary (default: 1)
 * @param options.baseDelay - Base delay in milliseconds for exponential backoff (default: 100)
 * @param options.retryOnNull - If true, retry on primary when result is null/undefined (default: false)
 * @returns The result of the query function
 */
export async function withRetryOnPrimary<T>(
  db: Database,
  fn: (db: Database) => Promise<T>,
  options?: {
    maxRetries?: number;
    baseDelay?: number;
    retryOnNull?: boolean;
  },
): Promise<T> {
  const {
    maxRetries = 1,
    baseDelay = 100,
    retryOnNull = false,
  } = options || {};
  const dbWithPrimary = db as DatabaseWithPrimary;
  let lastError: unknown;

  // First attempt: try with replica (default behavior)
  try {
    const result = await fn(db);

    // If retryOnNull is enabled and result is null/undefined, retry on primary
    if (retryOnNull && (result === null || result === undefined)) {
      // Check if we can use primary
      if (!dbWithPrimary.usePrimaryOnly) {
        return result;
      }

      // Retry on primary
      for (let attempt = 0; attempt <= maxRetries; attempt++) {
        try {
          const primaryDb = dbWithPrimary.usePrimaryOnly();
          const primaryResult = await fn(primaryDb);
          // Return primary result even if it's still null (user genuinely doesn't exist)
          return primaryResult;
        } catch (error) {
          lastError = error;

          // Don't retry on the last attempt
          if (attempt === maxRetries) {
            break;
          }

          // Exponential backoff with jitter
          const delay = baseDelay * 2 ** attempt + Math.random() * 50;
          await new Promise((resolve) => setTimeout(resolve, delay));
        }
      }

      // If all retries failed, throw the last error
      if (lastError) {
        throw lastError;
      }

      // If no error but still null, return null
      return result;
    }

    return result;
  } catch (error) {
    lastError = error;

    // Check if this is a retryable error
    const errorMessage = error instanceof Error ? error.message : String(error);
    const isRetryableError =
      errorMessage.includes("timeout") ||
      errorMessage.includes("connection") ||
      errorMessage.includes("Failed query") ||
      errorMessage.includes("canceling statement") ||
      errorMessage.includes("cancelled");

    // If not retryable, throw immediately
    if (!isRetryableError) {
      throw error;
    }

    // If we can't use primary, throw the original error
    if (!dbWithPrimary.usePrimaryOnly) {
      throw error;
    }
  }

  // Retry attempts: use primary database
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      const primaryDb = dbWithPrimary.usePrimaryOnly();
      return await fn(primaryDb);
    } catch (error) {
      lastError = error;

      // Don't retry on the last attempt
      if (attempt === maxRetries) {
        break;
      }

      // Exponential backoff with jitter
      const delay = baseDelay * 2 ** attempt + Math.random() * 50;
      await new Promise((resolve) => setTimeout(resolve, delay));
    }
  }

  throw lastError;
}