retry.js 2.26 KB
/**
 * 重试纯函数 —— 瞬态错误判定 + 指数退避计算。
 *
 * 零副作用、零依赖:被本包 `withRetry` 与 `@drama/llm` 的网关编排层调用,可独立单测。
 * 不在此处 sleep、不读时钟、不发请求;只做判定与算术。
 */

/** @typedef {'auth'|'rateLimit'|'transient'|'badRequest'|'timeout'|'aborted'|'unknown'} CallErrorReason */

/**
 * 默认重试策略。调用方可在包装时覆盖。
 * @typedef {object} RetryPolicy
 * @property {number} maxRetries   最大重试次数(不含首次尝试)。
 * @property {number} baseDelayMs  首次退避基数。
 * @property {number} maxDelayMs   退避上限。
 * @property {number} factor       指数因子。
 */

/** @type {RetryPolicy} */
export const DEFAULT_RETRY_POLICY = {
  maxRetries: 2,
  baseDelayMs: 500,
  maxDelayMs: 8000,
  factor: 2,
}

/**
 * 该 reason 是否属于「可重试的瞬态类」。
 * rateLimit / transient / timeout = true;auth / badRequest / aborted / unknown = false。
 * @param {CallErrorReason} reason
 * @returns {boolean}
 */
export function isRetryableReason(reason) {
  return reason === 'rateLimit' || reason === 'transient' || reason === 'timeout'
}

/**
 * 在给定策略下,第 `attempt` 次失败后是否还应再试。
 * @param {number} attempt   已失败的次数(首次失败时为 1)。
 * @param {boolean} retryable 该错误是否可重试。
 * @param {RetryPolicy} [policy]
 * @returns {boolean}
 */
export function shouldRetry(attempt, retryable, policy = DEFAULT_RETRY_POLICY) {
  if (!retryable) return false
  return attempt <= policy.maxRetries
}

/**
 * 第 `attempt` 次重试前的退避毫秒数(指数退避 + 抖动可选)。
 * attempt 从 1 起算(第一次重试)。`retryAfterMs` 若由 429 头给出则优先采用。
 * @param {number} attempt
 * @param {RetryPolicy} [policy]
 * @param {number} [retryAfterMs] 厂商显式给出的等待时间(如 Retry-After)。
 * @returns {number}
 */
export function backoffDelayMs(attempt, policy = DEFAULT_RETRY_POLICY, retryAfterMs) {
  if (typeof retryAfterMs === 'number' && retryAfterMs >= 0) {
    return Math.min(retryAfterMs, policy.maxDelayMs)
  }
  const raw = policy.baseDelayMs * Math.pow(policy.factor, attempt - 1)
  return Math.min(raw, policy.maxDelayMs)
}