← 中文资源

简体中文版本

Eventually 轮询断言辅助函数

反复执行异步断言直到成功或超时,支持 AbortSignal 取消并保留最后一次错误作为原因。

阅读英文原文

功能说明

eventually 用于等待异步状态最终满足条件,例如元素出现、任务完成或数据库写入可见。谓词返回真值时立即返回该值,返回假值或抛错时继续轮询,直到超时或收到取消信号。它适合 Playwright 之外的轻量集成测试、CLI 等待服务和本地脚本,也可在运行时等待依赖就绪。

关键代码

函数先校验超时与间隔,计算绝对截止时间。每轮先检查 AbortSignal,再执行谓词并等待结果;真值直接返回,异常被暂存但不立即失败。随后判断是否超时,超时错误会把最后一次异常作为 cause,便于定位根因。等待时间取配置间隔与剩余时间的较小值,避免最后一次睡眠超过截止时间。

使用注意

谓词应可重复执行且无副作用,否则轮询会重复创建资源。默认两秒超时适合本地测试,生产环境应根据依赖延迟调整。超时后不会自动取消正在执行的谓词,谓词内部应支持 AbortSignal。不要用轮询替代事件通知,高频轮询会增加数据库或网络压力。返回值使用真值判断,因此 false、0、空字符串都视为未完成。

验证方式

测试首次即成功、第三次才成功、一直为假直到超时,以及谓词抛错后恢复成功。断言超时错误包含指定毫秒数与 cause。测试 AbortSignal 在开始前与轮询中触发,错误名称为 AbortError。验证零超时只执行一次、非法间隔抛错,并用假定时器确认等待时长符合截止时间上限。

代码示例

export interface EventuallyOptions {
  timeoutMs?: number;
  intervalMs?: number;
  signal?: AbortSignal;
}

export async function eventually<T>(
  predicate: () => T | Promise<T>,
  options: EventuallyOptions = {},
): Promise<T> {
  const { timeoutMs = 2000, intervalMs = 50, signal } = options;
  if (!Number.isFinite(timeoutMs) || timeoutMs < 0) {
    throw new RangeError("timeoutMs must be a non-negative finite number");
  }
  if (!Number.isFinite(intervalMs) || intervalMs <= 0) {
    throw new RangeError("intervalMs must be a positive finite number");
  }
  const deadline = Date.now() + timeoutMs;
  let lastError: unknown;
  while (true) {
    if (signal?.aborted) {
      const aborted = new Error("eventually aborted");
      aborted.name = "AbortError";
      throw aborted;
    }
    try {
      const value = await predicate();
      if (value) return value;
    } catch (error) {
      lastError = error;
    }
    if (Date.now() >= deadline) {
      const message = "eventually timed out after " + timeoutMs + "ms";
      throw lastError ? new Error(message, { cause: lastError }) : new Error(message);
    }
    const wait = Math.min(intervalMs, Math.max(0, deadline - Date.now()));
    await new Promise<void>((resolve) => setTimeout(resolve, wait));
  }
}