Files
OmniRoute/open-sse/utils/directResponseStartTimeout.ts
2026-08-17 08:26:57 -07:00

78 lines
2.5 KiB
TypeScript

type DirectFetchOptions = RequestInit & { dispatcher?: unknown };
type DirectFetch = (
input: RequestInfo | URL,
options: DirectFetchOptions
) => Promise<Response>;
const DEFAULT_DIRECT_HEADERS_TIMEOUT_MS = 30_000;
const DIRECT_RESPONSE_START_TIMEOUT_CODE = "DIRECT_RESPONSE_START_TIMEOUT";
export function resolveDirectHeadersTimeoutMs(
env: Record<string, string | undefined> = process.env
): number {
const raw = env.OMNIROUTE_DIRECT_HEADERS_TIMEOUT_MS;
if (raw == null || raw.trim() === "") return DEFAULT_DIRECT_HEADERS_TIMEOUT_MS;
const parsed = Number(raw);
return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : 0;
}
function createDirectResponseStartTimeout(timeoutMs: number): Error & { code: string } {
const err = new Error(
`Direct response did not start within ${timeoutMs}ms — retrying on a fresh socket`
) as Error & { code: string };
err.name = "TimeoutError";
err.code = DIRECT_RESPONSE_START_TIMEOUT_CODE;
return err;
}
export function isDirectResponseStartTimeout(err: unknown): boolean {
return (
!!err &&
typeof err === "object" &&
"code" in err &&
err.code === DIRECT_RESPONSE_START_TIMEOUT_CODE
);
}
function mergeAbortSignals(
primary: AbortSignal | null | undefined,
secondary: AbortSignal
): AbortSignal {
if (!primary) return secondary;
if (primary.aborted) return primary;
const controller = new AbortController();
const onPrimaryAbort = () => controller.abort(primary.reason);
const onSecondaryAbort = () => controller.abort(secondary.reason);
const cleanup = () => {
primary.removeEventListener("abort", onPrimaryAbort);
secondary.removeEventListener("abort", onSecondaryAbort);
};
primary.addEventListener("abort", onPrimaryAbort, { once: true });
secondary.addEventListener("abort", onSecondaryAbort, { once: true });
controller.signal.addEventListener("abort", cleanup, { once: true });
return controller.signal;
}
export async function directFetchWithBoundedResponseStart(
input: RequestInfo | URL,
options: DirectFetchOptions,
fetchImpl: DirectFetch,
timeoutMs: number
): Promise<Response> {
if (!timeoutMs || timeoutMs <= 0) return fetchImpl(input, options);
const attemptController = new AbortController();
const timer = setTimeout(
() => attemptController.abort(createDirectResponseStartTimeout(timeoutMs)),
timeoutMs
);
timer.unref?.();
try {
return await fetchImpl(input, {
...options,
signal: mergeAbortSignals(options.signal, attemptController.signal),
});
} finally {
clearTimeout(timer);
}
}