UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

205 lines (204 loc) 8.8 kB
import { D as resolveExpiresAtMsFromDurationMs, E as resolveDateTimestampMs, M as resolveNonNegativeIntegerOption } from "./number-coercion-CLj0HTDM.js"; import { i as extractErrorCode, n as collectErrorGraphCandidates } from "./error-coercion-D_-xJ90S.js"; import { c as isRecord } from "./record-coerce-DItp3I4t.js"; import { r as computeBackoffSchedule } from "./src-BQ327IOM.js"; import { t as sleep } from "./sleep-D7nua6TP.js"; import "./errors-Db3Ymjlb.js"; import { n as getRetryAttemptErrors } from "./retry-DIUON3ys.js"; import { c as isPlatformMessageNotDispatchedError, l as isPlatformMessageRejectedError, s as isOutboundDeliveryError } from "./deliver-types-Diy-VQKA.js"; //#region src/infra/delivery-recovery.shared.ts const RECOVERY_BACKOFF_MS = [ 5e3, 25e3, 12e4, 6e5 ]; const RECOVERY_REPLAY_SPACING_MS = 250; const PRE_CONNECT_ERROR_CODES = /* @__PURE__ */ new Set([ "ECONNREFUSED", "ENOTFOUND", "EAI_AGAIN", "ENETDOWN", "ENETUNREACH", "EHOSTUNREACH" ]); const TRANSPORT_ERROR_CODE_RE = /^(?:E(?:AI_|CONN|NET|HOST|ADDR|PIPE|TIMEDOUT|SOCKET)|UND_ERR_|ERR_(?:NETWORK|HTTP2|QUIC|TLS|SSL))/; const UNPROVEN_ERROR_BRANCH = "unproven delivery error branch"; function createEmptyDeliveryRecoverySummary() { return { recovered: 0, failed: 0, skippedMaxRetries: 0, deferredBackoff: 0 }; } function resolveDeliveryRecoveryDeadlineMs(maxRecoveryMs) { const durationMs = resolveNonNegativeIntegerOption(maxRecoveryMs, 6e4); if (durationMs <= 0) return resolveDateTimestampMs(Date.now()); return resolveExpiresAtMsFromDurationMs(durationMs) ?? resolveDateTimestampMs(Date.now()); } function isDeliveryRecoveryRetryEligible(entry, now) { if (entry.availableAt && now < entry.availableAt) return { eligible: false, remainingBackoffMs: entry.availableAt - now }; const backoff = computeBackoffMs(entry.retryCount); if (backoff <= 0 || entry.retryCount === 0 && entry.lastAttemptAt === void 0) return { eligible: true }; const lastAttemptAt = entry.lastAttemptAt; const remainingBackoffMs = (typeof lastAttemptAt === "number" && Number.isFinite(lastAttemptAt) && lastAttemptAt > 0 ? lastAttemptAt : entry.enqueuedAt) + backoff - now; return remainingBackoffMs > 0 ? { eligible: false, remainingBackoffMs } : { eligible: true }; } function preserveProofBranches(branches) { return branches?.map((branch) => branch ?? UNPROVEN_ERROR_BRANCH) ?? []; } function isProvenPreConnectCandidate(candidate) { const code = extractErrorCode(candidate)?.trim().toUpperCase(); if (code === "UND_ERR_CONNECT_TIMEOUT" || code === "UND_ERR_DNS_RESOLVE_FAILED") return true; if (!code || !PRE_CONNECT_ERROR_CODES.has(code) || !candidate || typeof candidate !== "object") return false; const syscall = candidate.syscall; return syscall === "connect" || syscall === "getaddrinfo"; } function nestedErrorCandidates(current) { const retryBranches = preserveProofBranches(getRetryAttemptErrors(current)); if (isPlatformMessageNotDispatchedError(current) || isProvenPreConnectCandidate(current)) return retryBranches; const nestedObjects = [ current.cause, current.original, current.error, current.reason ].filter((candidate) => candidate !== null && typeof candidate === "object"); const aggregateBranches = Array.isArray(current.errors) ? preserveProofBranches(current.errors) : []; return [ ...retryBranches, ...aggregateBranches, ...nestedObjects ]; } function isProvenDeliveryNotSentError(err) { let foundNotSentProof = false; for (const candidate of collectErrorGraphCandidates(err, nestedErrorCandidates)) { if (isOutboundDeliveryError(candidate) && candidate.sentBeforeError) return false; const code = extractErrorCode(candidate)?.trim().toUpperCase(); if (isPlatformMessageNotDispatchedError(candidate) || isProvenPreConnectCandidate(candidate)) { foundNotSentProof = true; continue; } const nested = candidate && typeof candidate === "object" ? nestedErrorCandidates(candidate) : []; const isPreConnectAggregateSummary = candidate !== null && typeof candidate === "object" && Array.isArray(candidate.errors) && code !== void 0 && PRE_CONNECT_ERROR_CODES.has(code); if (nested.length === 0 || code && !isPreConnectAggregateSummary && (PRE_CONNECT_ERROR_CODES.has(code) || TRANSPORT_ERROR_CODE_RE.test(code))) return false; } return foundNotSentProof; } /** Finds a provider's permanent pre-dispatch rejection through delivery wrappers. */ function findPlatformMessageRejectedError(err) { for (const candidate of collectErrorGraphCandidates(err, nestedErrorCandidates)) if (isPlatformMessageRejectedError(candidate)) return candidate; } /** * Returns the typed no-send marker's retry decision after proving the full error graph. * Untyped pre-connect proof stays undefined so caller-specific text policy keeps precedence. */ function resolveDeliveryNotSentRetryability(err) { const candidates = collectErrorGraphCandidates(err, nestedErrorCandidates); if (!candidates.some(isPlatformMessageNotDispatchedError) || !isProvenDeliveryNotSentError(err) || hasDeliverySendEvidence(candidates)) return; return findPlatformMessageRejectedError(err) === void 0; } function hasDeliverySendEvidence(candidates) { return candidates.some((candidate) => isRecord(candidate) && (candidate.sentBeforeError === true || candidate.visibleReplySent === true || isRecord(candidate.deliveryResult) && candidate.deliveryResult.visibleReplySent === true)); } /** True only when the complete error graph proves a retryable recipient no-send. */ function isRetryableDeliveryNotSentError(err) { return resolveDeliveryNotSentRetryability(err) ?? (isProvenDeliveryNotSentError(err) && !hasDeliverySendEvidence(collectErrorGraphCandidates(err, nestedErrorCandidates))); } function computeBackoffMs(retryCount) { return computeBackoffSchedule(RECOVERY_BACKOFF_MS, retryCount); } function getErrnoCode(err) { return err && typeof err === "object" && "code" in err ? String(err.code) : null; } function createRecoveryReplayPacer() { let lastReplayStartedAt = 0; let waitQueue = Promise.resolve(); return { async wait(deadlineMs) { let releaseWaiter = () => {}; const previousWaiter = waitQueue; waitQueue = new Promise((resolve) => { releaseWaiter = resolve; }); await previousWaiter; try { const now = Date.now(); if (deadlineMs !== void 0 && now >= deadlineMs) return "deadline-exceeded"; const elapsedMs = now - lastReplayStartedAt; const waitMs = elapsedMs < 0 ? 0 : Math.max(0, RECOVERY_REPLAY_SPACING_MS - elapsedMs); if (waitMs > 0) { const remainingBudgetMs = deadlineMs === void 0 ? waitMs : Math.max(0, deadlineMs - now); await sleep(Math.min(waitMs, remainingBudgetMs)); } if (deadlineMs !== void 0 && Date.now() >= deadlineMs) return "deadline-exceeded"; lastReplayStartedAt = Date.now(); return "ready"; } finally { releaseWaiter(); } } }; } /** Own one queue namespace's live claims, drain exclusion, and replay pacing. */ function createDeliveryRecoveryCoordinator() { const activeDrains = /* @__PURE__ */ new Set(); const activeEntries = /* @__PURE__ */ new Set(); const replayPacer = createRecoveryReplayPacer(); async function withClaim(entryId, run) { if (activeEntries.has(entryId)) return { status: "claimed-by-other-owner" }; activeEntries.add(entryId); try { return { status: "claimed", value: await run() }; } finally { activeEntries.delete(entryId); } } return { withClaim, async withDrain(drainKey, run) { if (activeDrains.has(drainKey)) return false; activeDrains.add(drainKey); try { await run(); return true; } finally { activeDrains.delete(drainKey); } }, async scan(options) { for (const entry of options.entries.toSorted((a, b) => a.enqueuedAt - b.enqueuedAt)) { if (options.deadlineMs !== void 0 && Date.now() >= options.deadlineMs) { options.onDeadlineExceeded?.(); break; } const claim = await withClaim(entry.id, async () => { const current = await options.loadEntry(entry.id); if (!current) { options.onMissingEntry?.(entry); return "continue"; } return await options.onEntry(current) ?? "continue"; }); if (claim.status === "claimed-by-other-owner") { options.onClaimConflict?.(entry); continue; } if (claim.value === "stop") break; } }, waitForReplay(deadlineMs) { return replayPacer.wait(deadlineMs); } }; } //#endregion export { getErrnoCode as a, isRetryableDeliveryNotSentError as c, findPlatformMessageRejectedError as i, resolveDeliveryNotSentRetryability as l, createDeliveryRecoveryCoordinator as n, isDeliveryRecoveryRetryEligible as o, createEmptyDeliveryRecoverySummary as r, isProvenDeliveryNotSentError as s, computeBackoffMs as t, resolveDeliveryRecoveryDeadlineMs as u };