Files
inkreach-agent/packages/plugin-sdk/src/async.ts
T

69 lines
2.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 插件异步工具:超时与重试(零依赖,主进程 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
}