File size: 2,809 Bytes
ccb6b75
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import logger from './logger.js';

export async function withRetry(fn, options = {}) {
  const {
    maxRetries = 3,
    baseDelay = 1000,
    maxDelay = 30000,
    jitter = true,
    retryableErrors = ['ECONNRESET', 'ETIMEDOUT', 'ECONNREFUSED', 'ENOTFOUND'],
    onRetry = null,
  } = options;

  let lastError;

  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      return await fn();
    } catch (error) {
      lastError = error;

      const isRetryable = _isRetryableError(error, retryableErrors);

      if (!isRetryable || attempt === maxRetries) {
        throw error;
      }

      const delay = _calculateDelay(attempt, baseDelay, maxDelay, jitter);

      logger.warn(
        `Retry ${attempt + 1}/${maxRetries} after ${delay}ms: ${error.message}`
      );

      if (onRetry) {
        await onRetry(error, attempt, delay);
      }

      await _sleep(delay);
    }
  }

  throw lastError;
}

export async function withTimeout(promise, ms, error = null) {
  let timeoutId;

  const timeout = new Promise((_, reject) => {
    timeoutId = setTimeout(
      () => reject(error || new Error(`Operation timed out after ${ms}ms`)),
      ms
    );
  });

  try {
    return await Promise.race([promise, timeout]);
  } finally {
    clearTimeout(timeoutId);
  }
}

export async function withRateLimit(fn, options = {}) {
  const {
    maxRetries = 5,
    baseDelay = 2000,
    maxDelay = 120000,
  } = options;

  return withRetry(fn, {
    maxRetries,
    baseDelay,
    maxDelay,
    retryableErrors: ['RATE_LIMITED', '403', '429'],
    onRetry: (error, attempt, delay) => {
      if (error.status === 403 || error.status === 429) {
        const resetTime = error.response?.headers?.['x-ratelimit-reset'];
        if (resetTime) {
          const waitTime = (parseInt(resetTime) * 1000) - Date.now();
          if (waitTime > 0 && waitTime < maxDelay) {
            logger.info(`Rate limit reset in ${Math.round(waitTime / 1000)}s`);
          }
        }
      }
    },
  });
}

function _isRetryableError(error, retryableErrors) {
  if (!error) return false;

  if (error.code && retryableErrors.includes(error.code)) return true;
  if (error.status && (error.status >= 500 || error.status === 429)) return true;
  if (error.message && retryableErrors.some(r => error.message.includes(r))) return true;

  return false;
}

function _calculateDelay(attempt, baseDelay, maxDelay, jitter) {
  const exponential = baseDelay * Math.pow(2, attempt);
  const delay = Math.min(exponential, maxDelay);

  if (jitter) {
    const jitterAmount = delay * 0.1;
    return delay + (Math.random() - 0.5) * 2 * jitterAmount;
  }

  return delay;
}

function _sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

export default { withRetry, withTimeout, withRateLimit };