/** * 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} fn - Async function to execute * @param {number} timeoutMs - Timeout in milliseconds * @param {string} [label='Operation'] - Label for error messages * @returns {Promise} * @throws {Error} With name 'TimeoutError' if operation times out */ export async function withTimeout( fn: () => Promise, timeoutMs: number, label = "Operation" ): Promise { return new Promise((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 = { 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; }