feat: 插件性能护栏 + 公共组件库——激活/装载限时兜底、renderer 装载三态呈现、@aso/plugin-sdk/ui、withTimeout/withRetry

This commit is contained in:
Rick Ye
2026-09-11 09:56:26 +08:00
parent 4ed31a34fc
commit f77942ee2b
7 changed files with 473 additions and 12 deletions
+68
View File
@@ -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
}