retryWithBackoff<T>() with capped jitter

TypeScript
export interface RetryOptions {
  attempts?: number;
  baseDelayMs?: number;
  maxDelayMs?: number;
  jitterRatio?: number;
  signal?: AbortSignal;
}

const sleep = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms));

/** Run an async task, retrying with capped exponential backoff plus jitter. */
export async function retryWithBackoff<T>(
  task: (attempt: number) => Promise<T>,
  options: RetryOptions = {},
): Promise<T> {
  const { attempts = 4, baseDelayMs = 100, maxDelayMs = 10_000, jitterRatio = 0.2, signal } = options;
  let lastError: unknown;
  for (let attempt = 1; attempt <= attempts; attempt += 1) {
    if (signal?.aborted) throw new Error('retry aborted');
    try {
      return await task(attempt);
    } catch (error) {
      lastError = error;
      if (attempt === attempts) break;
      const capped = Math.min(maxDelayMs, baseDelayMs * 2 ** (attempt - 1));
      const jitter = capped * jitterRatio * (Math.random() * 2 - 1);
      await sleep(Math.max(0, Math.round(capped + jitter)));
    }
  }
  throw lastError ?? new Error('retry failed without an attempt');
}