refactor: dedupe channel and gateway surfaces

This commit is contained in:
Peter Steinberger
2026-03-02 19:48:12 +00:00
parent 9617ac9dd5
commit 9d30159fcd
44 changed files with 1072 additions and 1479 deletions

View File

@@ -1,4 +1,9 @@
import { extractErrorCode, formatErrorMessage } from "../infra/errors.js";
import {
collectErrorGraphCandidates,
extractErrorCode,
formatErrorMessage,
readErrorName,
} from "../infra/errors.js";
const RECOVERABLE_ERROR_CODES = new Set([
"ECONNRESET",
@@ -44,13 +49,6 @@ function normalizeCode(code?: string): string {
return code?.trim().toUpperCase() ?? "";
}
function getErrorName(err: unknown): string {
if (!err || typeof err !== "object") {
return "";
}
return "name" in err ? String(err.name) : "";
}
function getErrorCode(err: unknown): string | undefined {
const direct = extractErrorCode(err);
if (direct) {
@@ -69,50 +67,6 @@ function getErrorCode(err: unknown): string | undefined {
return undefined;
}
function collectErrorCandidates(err: unknown): unknown[] {
const queue = [err];
const seen = new Set<unknown>();
const candidates: unknown[] = [];
while (queue.length > 0) {
const current = queue.shift();
if (current == null || seen.has(current)) {
continue;
}
seen.add(current);
candidates.push(current);
if (typeof current === "object") {
const cause = (current as { cause?: unknown }).cause;
if (cause && !seen.has(cause)) {
queue.push(cause);
}
const reason = (current as { reason?: unknown }).reason;
if (reason && !seen.has(reason)) {
queue.push(reason);
}
const errors = (current as { errors?: unknown }).errors;
if (Array.isArray(errors)) {
for (const nested of errors) {
if (nested && !seen.has(nested)) {
queue.push(nested);
}
}
}
// Grammy's HttpError wraps the underlying error in .error (not .cause)
// Only follow .error for HttpError to avoid widening the search graph
if (getErrorName(current) === "HttpError") {
const wrappedError = (current as { error?: unknown }).error;
if (wrappedError && !seen.has(wrappedError)) {
queue.push(wrappedError);
}
}
}
}
return candidates;
}
export type TelegramNetworkErrorContext = "polling" | "send" | "webhook" | "unknown";
export function isRecoverableTelegramNetworkError(
@@ -127,13 +81,23 @@ export function isRecoverableTelegramNetworkError(
? options.allowMessageMatch
: options.context !== "send";
for (const candidate of collectErrorCandidates(err)) {
for (const candidate of collectErrorGraphCandidates(err, (current) => {
const nested: Array<unknown> = [current.cause, current.reason];
if (Array.isArray(current.errors)) {
nested.push(...current.errors);
}
// Grammy's HttpError wraps the underlying error in .error (not .cause).
if (readErrorName(current) === "HttpError") {
nested.push(current.error);
}
return nested;
})) {
const code = normalizeCode(getErrorCode(candidate));
if (code && RECOVERABLE_ERROR_CODES.has(code)) {
return true;
}
const name = getErrorName(candidate);
const name = readErrorName(candidate);
if (name && RECOVERABLE_ERROR_NAMES.has(name)) {
return true;
}