/**
 * 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<void> {
  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 });
  });
}
