export const CONTROL_REQUEST_TIMEOUT_MS = 8_000; export const EVENT_REQUEST_TIMEOUT_MS = 10_000; const MEDIA_REQUEST_MIN_TIMEOUT_MS = 15_000; const MEDIA_REQUEST_MAX_TIMEOUT_MS = 90_000; const MEDIA_REQUEST_HEADROOM_MS = 10_000; const MEDIA_MIN_BYTES_PER_SECOND = 64 * 1024; /** Internal error used to distinguish a bounded request from a generic network failure. */ export class RequestDeadlineError extends Error { readonly code = "request-timeout" as const; constructor( readonly operation: string, readonly timeoutMs: number, ) { super(`${operation} timed out after ${timeoutMs}ms`); this.name = "RequestDeadlineError"; } } /** * Give one asynchronous request an absolute deadline. The race is deliberate: * it still settles when a browser, proxy, or test double ignores AbortSignal. */ export async function withRequestDeadline( operation: string, timeoutMs: number, run: (signal: AbortSignal) => Promise, parentSignal?: AbortSignal, ): Promise { if (parentSignal?.aborted) throw abortError(); const controller = new AbortController(); let timedOut = false; let timer: ReturnType | undefined; let onParentAbort: (() => void) | undefined; const cancelled = new Promise((_resolve, reject) => { onParentAbort = () => { controller.abort(); reject(abortError()); }; parentSignal?.addEventListener("abort", onParentAbort, { once: true }); timer = setTimeout( () => { timedOut = true; controller.abort(); reject(new RequestDeadlineError(operation, timeoutMs)); }, Math.max(0, timeoutMs), ); }); try { return await Promise.race([Promise.resolve().then(() => run(controller.signal)), cancelled]); } catch (error) { if (timedOut) throw new RequestDeadlineError(operation, timeoutMs); throw error; } finally { if (timer !== undefined) clearTimeout(timer); if (onParentAbort) parentSignal?.removeEventListener("abort", onParentAbort); } } /** Size-aware media deadline with a hard upper bound. */ export function mediaRequestTimeoutMs(byteSize: number): number { const transferMs = Math.ceil((Math.max(0, byteSize) / MEDIA_MIN_BYTES_PER_SECOND) * 1_000); return Math.min( MEDIA_REQUEST_MAX_TIMEOUT_MS, Math.max(MEDIA_REQUEST_MIN_TIMEOUT_MS, transferMs + MEDIA_REQUEST_HEADROOM_MS), ); } function abortError(): Error { const error = new Error("The request was aborted"); error.name = "AbortError"; return error; }