/** * Sleep for `ms`, but resolve early if the AbortSignal fires. * * Используем в poll-циклах (analyze / fetch-status) чтобы цикл пробуждался * сразу при abort (unmount, смена cad/horizon, cancel), а не ждал полные `ms` * до следующей проверки `signal.aborted`. Без этого осиротевший цикл живёт до * hard-cap (60 × 2s = 2 мин), сыпя GET /fetch-status + POST /analyze — зомби- * поллинг (#1248, sibling #1242). * * Резолвится (не реджектится) — вызывающий сам решает бросать ли AbortError, * проверив `signal.aborted` после await. */ export function abortableSleep( ms: number, signal?: AbortSignal, ): Promise { return new Promise((resolve) => { if (signal?.aborted) { resolve(); return; } const onAbort = () => { clearTimeout(timer); resolve(); }; const timer = setTimeout(() => { signal?.removeEventListener("abort", onAbort); resolve(); }, ms); signal?.addEventListener("abort", onAbort, { once: true }); }); }