SPB Git

Repository Public

retry.ts 2.4 KB 68 lines typescript

1import { ProviderError } from './errors.js';23export interface RetryOptions {4  maxAttempts: number;5  sleep: (ms: number, signal?: AbortSignal) => Promise<void>;6  random: () => number;7  signal?: AbortSignal;8}910export const DEFAULT_MAX_ATTEMPTS = 6;11const BASE_DELAY_MS = 1_000;12const MAX_BACKOFF_MS = 30_000;13/** A server may ask for a long pause; beyond this we give up the wait and let the step fail. */14const MAX_RETRY_AFTER_MS = 120_000;1516/** Exponential backoff with full jitter on the upper half: attempt 1 → 0.5–1 s, 2 → 1–2 s, … capped at 30 s. */17export function backoffMs(attempt: number, random: () => number): number {18  const ceiling = Math.min(MAX_BACKOFF_MS, BASE_DELAY_MS * 2 ** (attempt - 1));19  return Math.round(ceiling * (0.5 + 0.5 * random()));20}2122export function abortableSleep(ms: number, signal?: AbortSignal): Promise<void> {23  return new Promise((resolve, reject) => {24    if (signal?.aborted) {25      reject(signal.reason instanceof Error ? signal.reason : new Error('aborted'));26      return;27    }28    const onAbort = (): void => {29      clearTimeout(timer);30      reject(signal?.reason instanceof Error ? signal.reason : new Error('aborted'));31    };32    const timer = setTimeout(() => {33      signal?.removeEventListener('abort', onAbort);34      resolve();35    }, ms);36    signal?.addEventListener('abort', onAbort, { once: true });37  });38}3940/**41 * Runs `fn` until it succeeds, throws a non-retryable `ProviderError`, or attempts are exhausted.42 * `fn` must throw `ProviderError` only (adapters map SDK errors before rethrowing).43 */44export async function withRetries<T>(45  fn: (attempt: number) => Promise<T>,46  opts: RetryOptions,47  onAbort: (cause: unknown, attempt: number) => ProviderError,48): Promise<{ value: T; attempts: number }> {49  for (let attempt = 1; ; attempt++) {50    if (opts.signal?.aborted) throw onAbort(opts.signal.reason, attempt - 1);51    try {52      return { value: await fn(attempt), attempts: attempt };53    } catch (err) {54      if (!(err instanceof ProviderError)) throw err;55      err.attempts = attempt;56      if (!err.retryable || attempt >= opts.maxAttempts) throw err;57      const wait = err.retryAfterMs !== null58        ? Math.min(err.retryAfterMs, MAX_RETRY_AFTER_MS)59        : backoffMs(attempt, opts.random);60      try {61        await opts.sleep(wait, opts.signal);62      } catch (cause) {63        throw onAbort(cause, attempt);64      }65    }66  }67}68
Files main
Menu

Theme