Files
makelore/electron/utils/proxy-fetch.ts

98 lines
2.7 KiB
TypeScript

/**
* 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<T>(
operation: (signal: AbortSignal) => Promise<T>,
timeoutMs: number,
sourceSignal?: AbortSignal | null,
): Promise<T> {
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<never>((_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<typeof fetch>[0],
init: RequestInit | undefined,
timeoutMs: number,
): Promise<Response> {
return runWithDeadline(
(signal) => fetchImpl(input, { ...init, signal }),
timeoutMs,
init?.signal,
);
}
export async function proxyAwareFetch(
input: string | URL,
init?: RequestInit
): Promise<Response> {
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);
}