refactor: dedupe channel and gateway surfaces
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user