const MAX_DISCORD_TIMEOUT_MS = 2_147_483_647; export const DISCORD_DEFAULT_LISTENER_TIMEOUT_MS = 120_000; export const DISCORD_DEFAULT_INBOUND_WORKER_TIMEOUT_MS = 30 * 60_000; function clampDiscordTimeoutMs(timeoutMs: number, minimumMs: number): number { return Math.max(minimumMs, Math.min(Math.floor(timeoutMs), MAX_DISCORD_TIMEOUT_MS)); } export function normalizeDiscordListenerTimeoutMs(raw: number | undefined): number { if (!Number.isFinite(raw) || (raw ?? 0) <= 0) { return DISCORD_DEFAULT_LISTENER_TIMEOUT_MS; } return clampDiscordTimeoutMs(raw!, 1_000); } export function normalizeDiscordInboundWorkerTimeoutMs( raw: number | undefined, ): number | undefined { if (raw === 0) { return undefined; } if (typeof raw !== "number" || !Number.isFinite(raw) || raw < 0) { return DISCORD_DEFAULT_INBOUND_WORKER_TIMEOUT_MS; } return clampDiscordTimeoutMs(raw, 1); } export function isAbortError(error: unknown): boolean { if (typeof error !== "object" || error === null) { return false; } return "name" in error && String((error as { name?: unknown }).name) === "AbortError"; } export function mergeAbortSignals( signals: Array, ): AbortSignal | undefined { const activeSignals = signals.filter((signal): signal is AbortSignal => Boolean(signal)); if (activeSignals.length === 0) { return undefined; } if (activeSignals.length === 1) { return activeSignals[0]; } if (typeof AbortSignal.any === "function") { return AbortSignal.any(activeSignals); } const fallbackController = new AbortController(); for (const signal of activeSignals) { if (signal.aborted) { fallbackController.abort(); return fallbackController.signal; } } const abortFallback = () => { fallbackController.abort(); for (const signal of activeSignals) { signal.removeEventListener("abort", abortFallback); } }; for (const signal of activeSignals) { signal.addEventListener("abort", abortFallback, { once: true }); } return fallbackController.signal; } export async function runDiscordTaskWithTimeout(params: { run: (abortSignal: AbortSignal | undefined) => Promise; timeoutMs?: number; abortSignals?: Array; onTimeout: (timeoutMs: number) => void; onAbortAfterTimeout?: () => void; onErrorAfterTimeout?: (error: unknown) => void; }): Promise { const timeoutAbortController = params.timeoutMs ? new AbortController() : undefined; const mergedAbortSignal = mergeAbortSignals([ ...(params.abortSignals ?? []), timeoutAbortController?.signal, ]); let timedOut = false; let timeoutHandle: ReturnType | null = null; const runPromise = params.run(mergedAbortSignal).catch((error) => { if (!timedOut) { throw error; } if (timeoutAbortController?.signal.aborted && isAbortError(error)) { params.onAbortAfterTimeout?.(); return; } params.onErrorAfterTimeout?.(error); }); try { if (!params.timeoutMs) { await runPromise; return false; } const timeoutPromise = new Promise<"timeout">((resolve) => { timeoutHandle = setTimeout(() => resolve("timeout"), params.timeoutMs); timeoutHandle.unref?.(); }); const result = await Promise.race([ runPromise.then(() => "completed" as const), timeoutPromise, ]); if (result === "timeout") { timedOut = true; timeoutAbortController?.abort(); params.onTimeout(params.timeoutMs); return true; } return false; } finally { if (timeoutHandle) { clearTimeout(timeoutHandle); } } }