功能说明
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));
}
}