feat: 插件性能护栏 + 公共组件库——激活/装载限时兜底、renderer 装载三态呈现、@aso/plugin-sdk/ui、withTimeout/withRetry
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* 插件异步工具:超时与重试(零依赖,主进程 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
|
||||
}
|
||||
Reference in New Issue
Block a user