File size: 2,777 Bytes
dfe11f8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
/**
 * Cliente HTTP reutilizable con retry y timeout.
 *
 * Responsabilidades:
 *   - Realizar peticiones GET y POST usando fetch nativo de Node.js.
 *   - Aplicar timeout via AbortController.
 *   - Reintentar automaticamente ante errores de red o HTTP 429/5xx.
 *   - Backoff exponencial con tope de 30s.
 *   - Inyectar User-Agent: PolySignal/1.0 en todas las peticiones.
 *
 * Uso:
 *   const data = await httpGet(url, { headers, timeout, retries });
 *   const data = await httpPost(url, body, { headers, timeout, retries });
 */

import { logger } from './logger.js';

const DEFAULT_TIMEOUT_MS = 10_000;
const DEFAULT_RETRIES = 3;
const RETRYABLE_STATUSES = new Set([429, 500, 502, 503, 504]);

function backoff(attempt) {
  return Math.min(1_000 * 2 ** attempt, 30_000);
}

async function request(url, init = {}, { timeout = DEFAULT_TIMEOUT_MS, retries = DEFAULT_RETRIES } = {}) {
  for (let attempt = 0; attempt <= retries; attempt++) {
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), timeout);

    try {
      const res = await fetch(url, {
        ...init,
        headers: { 'User-Agent': 'PolySignal/1.0', ...init.headers },
        signal: controller.signal,
      });

      clearTimeout(timeoutId);

      if (RETRYABLE_STATUSES.has(res.status) && attempt < retries) {
        const wait = backoff(attempt);
        logger.warn({ url, status: res.status, attempt, wait }, 'retrying request');
        await new Promise((r) => setTimeout(r, wait));
        continue;
      }

      const text = await res.text();
      const data = text ? JSON.parse(text) : null;

      if (!res.ok) {
        const err = Object.assign(new Error(`HTTP ${res.status}${url}`), {
          status: res.status,
          body: data,
        });
        throw err;
      }

      return data;
    } catch (err) {
      clearTimeout(timeoutId);

      if (err.name === 'AbortError') {
        throw Object.assign(new Error(`Timeout after ${timeout}ms — ${url}`), { code: 'TIMEOUT' });
      }

      // network error (no HTTP status) → retry
      if (!err.status && attempt < retries) {
        const wait = backoff(attempt);
        logger.warn({ url, err: err.message, attempt, wait }, 'network error, retrying');
        await new Promise((r) => setTimeout(r, wait));
        continue;
      }

      logger.error({ url, err: err.message }, 'request failed');
      throw err;
    }
  }
}

export const httpGet = (url, { headers, ...opts } = {}) =>
  request(url, { method: 'GET', headers }, opts);

export const httpPost = (url, body, { headers, ...opts } = {}) =>
  request(
    url,
    { method: 'POST', headers: { 'Content-Type': 'application/json', ...headers }, body: JSON.stringify(body) },
    opts,
  );