combinators.js
5.42 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
/**
* 高阶组合器 —— 把「一次外部服务调用」包成带重试/降级的同形函数。
*
* 本包不认识 key、凭据、厂商、审计:候选就是 `async fn`,key 属于调用方闭包。
* 唯一判定契约是结构化错误上的 `retryable === true`(duck-type,LLMError/ImageError/
* TtsError/VideoError 天然满足);判定可经 `isRetryable` 覆盖,缺省不读 message、不判状态码。
* 嵌套顺序即编排语义:`withFallback(fns.map(withRetry))` = 每候选重试耗尽再降级。
*/
import { DEFAULT_RETRY_POLICY, shouldRetry, backoffDelayMs } from './retry.js'
/**
* 缺省重试/降级判定:结构化错误的 retryable 布尔。
* 普通 Error / null / 缺字段一律不可重试——未归一的错误宁可上抛也不重试。
* @param {unknown} err
* @returns {boolean}
*/
export function isRetryableError(err) {
return /** @type {{ retryable?: unknown } | null | undefined} */ (err)?.retryable === true
}
/**
* withRetry —— 单函数重试(指数退避;错误带数值 `retryAfterMs` 时优先采用并封顶)。
* 返回与 `fn` 同形的函数,参数原样透传。
* @template {(...args: any[]) => Promise<any>} F
* @param {F} fn
* @param {object} [cfg]
* @param {import('./retry.js').RetryPolicy} [cfg.policy]
* @param {(err: unknown) => boolean} [cfg.isRetryable]
* @param {AbortSignal} [cfg.signal] 中止后不再发起新 attempt,退避等待中被中止立即抛。
* @param {(event: { attempt: number, error: unknown }) => void} [cfg.observe] 每次失败回调;attempt 从 1 起算。
* @param {(ms: number, signal?: AbortSignal) => Promise<void>} [cfg.sleep] 可注入(测试免等真实退避)。
* @returns {F}
*/
export function withRetry(fn, {
policy = DEFAULT_RETRY_POLICY,
isRetryable = isRetryableError,
signal,
observe,
sleep = abortableSleep,
} = {}) {
if (typeof fn !== 'function') throw new TypeError('withRetry: fn must be a function')
return /** @type {F} */ (async (...args) => {
let attempt = 0
for (;;) {
if (signal?.aborted) throw abortReason(signal)
try {
return await fn(...args)
} catch (err) {
attempt += 1
observe?.({ attempt, error: err })
if (!shouldRetry(attempt, isRetryable(err), policy)) throw err
await sleep(backoffDelayMs(attempt, policy, retryAfterMsOf(err)), signal)
}
}
})
}
/**
* withFallback —— 候选按序降级:可重试错误滑到下一候选,不可重试立即上抛,
* 链尾无论何因原样上抛(不吞、不替换)。返回与候选同形的函数,参数原样透传给每个被尝试的候选。
* @template {(...args: any[]) => Promise<any>} F
* @param {F[]} fns 非空候选数组,索引 0 为主、末位为最后兜底。
* @param {object} [cfg]
* @param {(err: unknown) => boolean} [cfg.isRetryable]
* @param {(event: { candidateIndex: number, error: unknown }) => void} [cfg.observe] 每个候选失败时回调(含链尾)。
* @returns {F}
*/
export function withFallback(fns, { isRetryable = isRetryableError, observe } = {}) {
if (!Array.isArray(fns) || fns.length < 1) {
throw new TypeError('withFallback: fns must be a non-empty array')
}
for (const fn of fns) {
if (typeof fn !== 'function') throw new TypeError('withFallback: every candidate must be a function')
}
return /** @type {F} */ (async (...args) => {
for (let i = 0; ; i++) {
try {
return await fns[i](...args)
} catch (err) {
observe?.({ candidateIndex: i, error: err })
if (i === fns.length - 1 || !isRetryable(err)) throw err
}
}
})
}
/**
* bindEach —— 一个函数 × N 份绑定(key、endpoint、账号……)展开成候选数组,
* 绑定作为最后一个参数传入。配合 withFallback 表达「一个函数几把 key」。
* @template {(...args: any[]) => Promise<any>} F
* @template B
* @param {F} fn
* @param {B[]} bindings 非空。
* @returns {Array<(...args: any[]) => Promise<any>>}
*/
export function bindEach(fn, bindings) {
if (typeof fn !== 'function') throw new TypeError('bindEach: fn must be a function')
if (!Array.isArray(bindings) || bindings.length < 1) {
throw new TypeError('bindEach: bindings must be a non-empty array')
}
return bindings.map((binding) => (...args) => fn(...args, binding))
}
/**
* 错误上的显式等待提示(如 429 的 Retry-After、keyrouter 冷却窗口)。
* @param {unknown} err
* @returns {number | undefined}
*/
function retryAfterMsOf(err) {
const v = /** @type {{ retryAfterMs?: unknown } | null | undefined} */ (err)?.retryAfterMs
return typeof v === 'number' ? v : undefined
}
/**
* 可被 abort 的 sleep。
* @param {number} ms
* @param {AbortSignal} [signal]
* @returns {Promise<void>}
*/
function abortableSleep(ms, signal) {
return new Promise((resolve, reject) => {
if (signal?.aborted) {
reject(abortReason(signal))
return
}
const t = setTimeout(() => {
signal?.removeEventListener?.('abort', onAbort)
resolve()
}, ms)
function onAbort() {
clearTimeout(t)
reject(abortReason(signal))
}
signal?.addEventListener?.('abort', onAbort, { once: true })
})
}
/**
* 中止原因:优先透传 signal.reason,缺省造 AbortError。
* @param {AbortSignal} [signal]
* @returns {unknown}
*/
function abortReason(signal) {
return signal?.reason ?? Object.assign(new Error('aborted'), { name: 'AbortError' })
}