/** * 插件异步工具:超时与重试(零依赖,主进程 main.mjs 与 renderer 通用)。 * * 性能铁律(docs/plugins/08-performance.md):插件内一切可能挂起/失败的异步操作 * (网络请求、子进程、文件轮询…)都应经 withTimeout / withRetry 包裹。宿主对插件 * 激活(10s)与 renderer 装载(8s)有兜底超时——超时即判失败,不会无限等; * 插件自己的异步调用应先于宿主超时并自行恢复,而不是被宿主判死。 */ /** 超时错误(err.name === 'TimeoutError',可据此与真实业务失败区分) */ export class TimeoutError extends Error { constructor(message: string) { super(message) this.name = 'TimeoutError' } } /** * 给 Promise 加超时;超时后以 TimeoutError 拒绝。 * 注意:底层操作不会被取消(Promise 无 cancel 语义),有 AbortController 的调用方 * 应自行把 signal 接到底层,再用本函数兜底计时。 */ export function withTimeout(promise: Promise, ms: number, label?: string): Promise { let timer: ReturnType | undefined const timeout = new Promise((_, reject) => { timer = setTimeout(() => { reject(new TimeoutError(`${label ?? '操作'} 超时(${ms}ms)`)) }, ms) }) return Promise.race([promise, timeout]).finally(() => clearTimeout(timer)) } export function sleep(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)) } export interface RetryOptions { /** 失败后的额外重试次数(总尝试 = retries + 1),默认 2 */ retries?: number /** 首次重试前的等待,默认 300ms;之后指数退避(×2) */ delayMs?: number /** 单次尝试的超时上限(ms),经 withTimeout 计时 */ timeoutMs?: number /** 每次重试前的回调(记日志/上报用) */ onRetry?: (err: unknown, attempt: number, nextDelayMs: number) => void } /** * 带超时与指数退避的重试:fn 返回 reject 或超时都算失败,重试耗尽后抛最后一次的错误。 */ export async function withRetry(fn: (attempt: number) => Promise, options: RetryOptions = {}): Promise { const retries = options.retries ?? 2 const delayMs = options.delayMs ?? 300 let lastErr: unknown for (let attempt = 0; attempt <= retries; attempt++) { try { const run = fn(attempt) return options.timeoutMs !== undefined ? await withTimeout(run, options.timeoutMs, `第 ${attempt + 1} 次尝试`) : await run } catch (err) { lastErr = err if (attempt === retries) break const nextDelay = delayMs * 2 ** attempt options.onRetry?.(err, attempt + 1, nextDelay) await sleep(nextDelay) } } throw lastErr }