File size: 2,832 Bytes
9e4583c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
/**
 * Request Timeout Utility — FASE-04 Observability
 *
 * Wraps fetch/async calls with configurable timeouts and
 * abort controller support.
 *
 * @module shared/utils/requestTimeout
 */

interface TimeoutOptions {
  timeoutMs?: number;
  label?: string;
  signal?: AbortSignal;
}

export async function fetchWithTimeout(url: string, options: RequestInit & TimeoutOptions = {}) {
  const { timeoutMs = 30000, label = "Request", signal: externalSignal, ...fetchOptions } = options;

  const controller = new AbortController();

  // Merge with external signal if provided
  if (externalSignal) {
    externalSignal.addEventListener("abort", () => controller.abort(externalSignal.reason));
  }

  const timeoutId = setTimeout(() => {
    controller.abort(new Error(`${label} timed out after ${timeoutMs}ms`));
  }, timeoutMs);

  try {
    const response = await fetch(url, {
      ...fetchOptions,
      signal: controller.signal,
    });
    return response;
  } catch (error: any) {
    if (error.name === "AbortError" || controller.signal.aborted) {
      const timeoutError: any = new Error(`${label} timed out after ${timeoutMs}ms`);
      timeoutError.name = "TimeoutError";
      timeoutError.originalUrl = url;
      timeoutError.timeoutMs = timeoutMs;
      throw timeoutError;
    }
    throw error;
  } finally {
    clearTimeout(timeoutId);
  }
}

/**
 * Execute any async function with a timeout.
 *
 * @template T
 * @param {() => Promise<T>} fn - Async function to execute
 * @param {number} timeoutMs - Timeout in milliseconds
 * @param {string} [label='Operation'] - Label for error messages
 * @returns {Promise<T>}
 * @throws {Error} With name 'TimeoutError' if operation times out
 */
export async function withTimeout<T>(
  fn: () => Promise<T>,
  timeoutMs: number,
  label = "Operation"
): Promise<T> {
  return new Promise<T>((resolve, reject) => {
    const timeoutId = setTimeout(() => {
      const error: any = new Error(`${label} timed out after ${timeoutMs}ms`);
      error.name = "TimeoutError";
      error.timeoutMs = timeoutMs;
      reject(error);
    }, timeoutMs);

    fn()
      .then((result) => {
        clearTimeout(timeoutId);
        resolve(result);
      })
      .catch((error) => {
        clearTimeout(timeoutId);
        reject(error);
      });
  });
}

/**
 * Default provider timeouts (ms).
 */
export const PROVIDER_TIMEOUTS: Record<string, number> = {
  openai: 60000,
  claude: 90000, // Claude can be slower for long outputs
  gemini: 60000,
  codex: 120000, // Coding tasks often take longer
  qwen: 45000,
  deepseek: 60000,
  cohere: 45000,
  groq: 30000, // Groq is fast
  mistral: 45000,
  openrouter: 60000,
  default: 60000,
};

export function getProviderTimeout(provider: string): number {
  return PROVIDER_TIMEOUTS[provider] || PROVIDER_TIMEOUTS.default;
}