/** * Use Electron's network stack when available so requests honor * session.defaultSession.setProxy(...). Fall back to the Node global fetch * for non-Electron test environments. */ import { net } from 'electron'; const SAFE_FALLBACK_METHODS = new Set(['GET', 'HEAD', 'OPTIONS']); export class RequestDeadlineExceededError extends Error { readonly timeoutMs: number; constructor(timeoutMs: number) { super(`Request did not complete within ${timeoutMs}ms`); this.name = 'RequestDeadlineExceededError'; this.timeoutMs = timeoutMs; } } function abortReason(signal: AbortSignal): unknown { if (signal.reason !== undefined) return signal.reason; const error = new Error('The operation was aborted'); error.name = 'AbortError'; return error; } export async function runWithDeadline( operation: (signal: AbortSignal) => Promise, timeoutMs: number, sourceSignal?: AbortSignal | null, ): Promise { if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) { throw new RangeError('Request timeout must be a positive finite number'); } if (sourceSignal?.aborted) throw abortReason(sourceSignal); const controller = new AbortController(); let rejectInterruption!: (reason: unknown) => void; const interruption = new Promise((_resolve, reject) => { rejectInterruption = reject; }); const abortFromSource = () => { if (!sourceSignal) return; const reason = abortReason(sourceSignal); controller.abort(reason); rejectInterruption(reason); }; sourceSignal?.addEventListener('abort', abortFromSource, { once: true }); const timeout = setTimeout(() => { const error = new RequestDeadlineExceededError(timeoutMs); controller.abort(error); rejectInterruption(error); }, timeoutMs); timeout.unref?.(); try { return await Promise.race([operation(controller.signal), interruption]); } finally { clearTimeout(timeout); sourceSignal?.removeEventListener('abort', abortFromSource); } } export function fetchWithDeadline( fetchImpl: typeof fetch, input: Parameters[0], init: RequestInit | undefined, timeoutMs: number, ): Promise { return runWithDeadline( (signal) => fetchImpl(input, { ...init, signal }), timeoutMs, init?.signal, ); } export async function proxyAwareFetch( input: string | URL, init?: RequestInit ): Promise { if (process.versions.electron) { try { return await net.fetch(input, init); } catch (error) { const method = (init?.method ?? 'GET').toUpperCase(); if (!SAFE_FALLBACK_METHODS.has(method) || init?.signal?.aborted) { throw error; } // Safe reads retain the Node fallback for proxy compatibility. } } return await fetch(input, init); }