/**
 * Bound an async operation with a timeout.
 *
 * Reliability principle: any LLM-*optional* operation (gate prompt-selection,
 * pagination summaries) must be time-bounded so a slow/hung LLM (e.g. a
 * thinking model that streams for minutes) can never block the caller. On
 * timeout the returned promise rejects with a {@link TimeoutError}; callers are
 * expected to fall back deterministically and report the degradation.
 *
 * The passed `AbortSignal` is aborted on timeout so fetch-based providers can
 * cancel the in-flight request. Providers that ignore the signal are still
 * unblocked immediately by the race (their orphaned request settles harmlessly
 * in the background).
 */
export class TimeoutError extends Error {
  constructor(label: string, ms: number) {
    super(`${label} timed out after ${ms}ms`);
    this.name = 'TimeoutError';
  }
}

export async function withTimeout<T>(
  run: (signal: AbortSignal) => Promise<T>,
  ms: number,
  label: string,
): Promise<T> {
  const controller = new AbortController();
  let timer: ReturnType<typeof setTimeout> | undefined;
  const timeout = new Promise<never>((_, reject) => {
    timer = setTimeout(() => {
      controller.abort();
      reject(new TimeoutError(label, ms));
    }, ms);
  });
  try {
    return await Promise.race([run(controller.signal), timeout]);
  } finally {
    if (timer !== undefined) clearTimeout(timer);
  }
}
