UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

193 lines (192 loc) 8.1 kB
//#region packages/retry/src/index.ts const MAX_TIMER_TIMEOUT_MS = 2147e6; function computeBackoff(policy, attempt) { const base = Math.min(policy.maxMs, policy.initialMs * policy.factor ** Math.max(attempt - 1, 0)); const jitter = base * policy.jitter * Math.random(); return Math.min(policy.maxMs, Math.round(base + jitter)); } function computeBackoffSchedule(scheduleMs, attempt) { const index = Math.min(attempt - 1, scheduleMs.length - 1); return attempt <= 0 ? 0 : scheduleMs[index] ?? 0; } async function sleepWithAbort(ms, abortSignal, options = {}) { if (!Number.isFinite(ms) || ms <= 0) return; const delayMs = Math.min(Math.max(Math.floor(ms), 1), MAX_TIMER_TIMEOUT_MS); await new Promise((resolve, reject) => { let settled = false; let timer = null; const cleanup = () => abortSignal?.removeEventListener("abort", onAbort); const onAbort = () => { if (settled) return; settled = true; if (timer) clearTimeout(timer); timer = null; cleanup(); const error = new Error("aborted", { cause: abortSignal?.reason ?? /* @__PURE__ */ new Error("aborted") }); error.name = "AbortError"; reject(error); }; abortSignal?.addEventListener("abort", onAbort, { once: true }); if (abortSignal?.aborted) { onAbort(); return; } timer = setTimeout(() => { settled = true; cleanup(); timer = null; resolve(); }, delayMs); if (options.ref === false) timer.unref?.(); if (abortSignal?.aborted) onAbort(); }); } var RetrySupervisor = class { constructor(policy, maxAttempts = Number.POSITIVE_INFINITY) { this.policy = policy; this.maxAttempts = maxAttempts; this.attempts = 0; this.initialMs = policy.initialMs; } reset(initialMs = this.policy.initialMs) { this.cancel(); this.attempts = 0; this.initialMs = initialMs; this.nextDelayOverrideMs = void 0; } cancel(reason = /* @__PURE__ */ new Error("retry cancelled")) { this.pendingAbort?.abort(reason); this.pendingAbort = void 0; } next(abortSignal) { const override = this.nextDelayOverrideMs; this.nextDelayOverrideMs = void 0; if (override === void 0 && ++this.attempts > Math.ceil(this.maxAttempts)) return; const attempt = Math.max(this.attempts, 1); const delayMs = override ?? computeBackoff({ ...this.policy, initialMs: this.initialMs }, attempt); this.cancel(); const pendingAbort = new AbortController(); this.pendingAbort = pendingAbort; return { attempt, delayMs, signal: abortSignal ? AbortSignal.any([pendingAbort.signal, abortSignal]) : pendingAbort.signal }; } }; const DEFAULT_RETRY_CONFIG = { attempts: 3, minDelayMs: 300, maxDelayMs: 3e4, jitter: 0 }; const defaultSleep = (ms) => new Promise((resolve) => { setTimeout(resolve, ms); }); function clampNumber(value, fallback, min, max) { const next = Number.isFinite(value) ? value : void 0; if (next === void 0) return fallback; return Math.min(Math.max(next, min ?? Number.NEGATIVE_INFINITY), max ?? Number.POSITIVE_INFINITY); } function resolveAttemptCount(value, fallback) { return Math.max(1, Math.round(Number.isFinite(value) ? value : fallback)); } function resolveRetryDelayMs(value) { const finite = value === Number.POSITIVE_INFINITY ? MAX_TIMER_TIMEOUT_MS : Number.isFinite(value) ? value : 0; return Math.min(Math.max(Math.round(finite), 0), MAX_TIMER_TIMEOUT_MS); } function resolveJitterConfig(value, fallback) { if (value === "full") return "full"; const fraction = Number.isFinite(value) ? value : void 0; return fraction === void 0 ? fallback : Math.min(Math.max(fraction, 0), 1); } function resolveRetryConfig(defaults = DEFAULT_RETRY_CONFIG, overrides) { const attempts = resolveAttemptCount(overrides?.attempts, defaults.attempts); const minDelayMs = resolveRetryDelayMs(clampNumber(overrides?.minDelayMs, defaults.minDelayMs, 0)); return { attempts, minDelayMs, maxDelayMs: Math.max(minDelayMs, resolveRetryDelayMs(clampNumber(overrides?.maxDelayMs, defaults.maxDelayMs, 0))), jitter: resolveJitterConfig(overrides?.jitter, defaults.jitter) }; } function applyJitter(delayMs, jitter, mode, random) { if (jitter === "full") { if (mode === "symmetric") return Math.max(0, Math.round(delayMs * (.5 + random() * .5))); return Math.max(0, Math.ceil(delayMs * (1 + random()))); } if (jitter <= 0) return mode === "positive" ? Math.ceil(delayMs) : delayMs; const fraction = random(); const raw = delayMs * (1 + (mode === "positive" ? fraction * jitter : (fraction * 2 - 1) * jitter)); return Math.max(0, mode === "positive" ? Math.ceil(raw) : Math.round(raw)); } function toRetryError(value, fallbackMessage = "Non-Error thrown") { if (value instanceof Error) return value; if (typeof value === "string") return new Error(value); const error = new Error(fallbackMessage, { cause: value }); if (typeof value === "object" && value !== null || typeof value === "function") Object.assign(error, value); return error; } function createRetryRunner(runtime = {}) { const runtimeSleep = runtime.sleep ?? defaultSleep; const runtimeRandom = runtime.random ?? Math.random; const createFailure = runtime.createFailure ?? ((errors) => toRetryError(errors.at(-1) ?? /* @__PURE__ */ new Error("Retry failed"))); return async function retryAsync(fn, attemptsOrOptions = 3, initialDelayMs = 300) { const attemptErrors = []; if (typeof attemptsOrOptions === "number") { const attempts = resolveAttemptCount(attemptsOrOptions, DEFAULT_RETRY_CONFIG.attempts); for (let index = 0; index < attempts; index += 1) try { return await fn(); } catch (err) { attemptErrors.push(err); if (index === attempts - 1) break; await runtimeSleep(resolveRetryDelayMs(initialDelayMs * 2 ** index)); } throw createFailure(attemptErrors); } const options = attemptsOrOptions; const resolved = resolveRetryConfig(DEFAULT_RETRY_CONFIG, options); const maxAttempts = resolved.attempts; const minDelayMs = resolved.minDelayMs; const maxDelayMs = resolved.maxDelayMs > 0 ? resolved.maxDelayMs : Number.POSITIVE_INFINITY; const retryAfterMaxDelayMs = options.retryAfterMaxDelayMs === void 0 ? maxDelayMs : Math.max(minDelayMs, resolveRetryDelayMs(clampNumber(options.retryAfterMaxDelayMs, maxDelayMs, 0))); const random = options.random ?? runtimeRandom; const sleep = options.sleep ?? runtimeSleep; const shouldRetry = options.shouldRetry ?? (() => true); for (let attempt = 1; attempt <= maxAttempts; attempt += 1) try { return await fn(); } catch (err) { attemptErrors.push(err); if (attempt >= maxAttempts || !shouldRetry(err, attempt)) break; const context = { attempt, maxAttempts, err, label: options.label }; const retryAfterMs = options.retryAfterMs?.(err); const hasRetryAfter = typeof retryAfterMs === "number" && Number.isFinite(retryAfterMs); const configuredDelay = typeof options.delayMs === "function" ? options.delayMs(context) : options.delayMs; const resolvedConfiguredDelay = configuredDelay === void 0 ? void 0 : resolveRetryDelayMs(configuredDelay); const baseDelay = hasRetryAfter ? Math.max(retryAfterMs, minDelayMs) : resolvedConfiguredDelay === void 0 ? minDelayMs * 2 ** (attempt - 1) : Math.max(resolvedConfiguredDelay, minDelayMs); const delayCap = hasRetryAfter ? retryAfterMaxDelayMs : maxDelayMs; let delay = Math.min(baseDelay, delayCap); const canHonorRetryAfter = hasRetryAfter && (retryAfterMs ?? 0) <= delayCap; const wantsPositiveDraw = resolved.jitter === "full" ? !hasRetryAfter || canHonorRetryAfter : canHonorRetryAfter; delay = applyJitter(delay, resolved.jitter, wantsPositiveDraw ? "positive" : "symmetric", random); delay = Math.min(Math.max(delay, minDelayMs), delayCap); await options.onRetry?.({ ...context, delayMs: delay }); if (delay > 0) await sleep(delay); } throw createFailure(attemptErrors); }; } const retryAsync = createRetryRunner(); //#endregion export { resolveRetryConfig as a, toRetryError as c, createRetryRunner as i, computeBackoff as n, retryAsync as o, computeBackoffSchedule as r, sleepWithAbort as s, RetrySupervisor as t };