69 lines
2.7 KiB
TypeScript
69 lines
2.7 KiB
TypeScript
/**
|
||
* 插件异步工具:超时与重试(零依赖,主进程 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<T>(promise: Promise<T>, ms: number, label?: string): Promise<T> {
|
||
let timer: ReturnType<typeof setTimeout> | undefined
|
||
const timeout = new Promise<never>((_, reject) => {
|
||
timer = setTimeout(() => {
|
||
reject(new TimeoutError(`${label ?? '操作'} 超时(${ms}ms)`))
|
||
}, ms)
|
||
})
|
||
return Promise.race([promise, timeout]).finally(() => clearTimeout(timer))
|
||
}
|
||
|
||
export function sleep(ms: number): Promise<void> {
|
||
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<T>(fn: (attempt: number) => Promise<T>, options: RetryOptions = {}): Promise<T> {
|
||
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
|
||
}
|