feat: keep error status until polling recovers, classify unconfirmed deliveries, document the UI
Validate / validate (push) Successful in 9m52s
Validate / validate (push) Successful in 9m52s
- the poll loop now clears its error status after the next successful getUpdates, not before the retry, so a slow retry never shows as healthy - recovery is reported only when getUpdates itself failed - a send that may have reached Telegram is reported as unconfirmed instead of failed - README and CHANGELOG describe the states, toasts, queue preview and spinner text
This commit is contained in:
1 parent
057281a3bb
commit
6f7e266a6e
8 files changed
+144
-19
No files matched your search
+12
-8
@@ -36,22 +36,26 @@ export interface TelegramNoticeRuntime<TContext> {
|
||||
}
|
||||
|
||||
function describeTelegramDeliveryFailure(error: unknown): {
|
||||
reason: string;
|
||||
message: string;
|
||||
level: TelegramNoticeLevel;
|
||||
} {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
const summary = summarizeTelegramStatusError(message);
|
||||
if (summary?.deliveryUnknown) {
|
||||
return {
|
||||
message: "Telegram: reply delivery unconfirmed (check the chat)",
|
||||
level: "warning",
|
||||
};
|
||||
}
|
||||
if (summary) {
|
||||
return {
|
||||
reason: summary.label,
|
||||
message: `Telegram: reply not delivered (${summary.label})`,
|
||||
level: summary.kind === "fatal" ? "error" : "warning",
|
||||
};
|
||||
}
|
||||
const raw = sanitizeTelegramStatusText(message);
|
||||
return {
|
||||
reason: raw.length > 80 ? `${raw.slice(0, 79)}…` : raw,
|
||||
level: "error",
|
||||
};
|
||||
const reason = raw.length > 80 ? `${raw.slice(0, 79)}…` : raw;
|
||||
return { message: `Telegram: reply not delivered (${reason})`, level: "error" };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -93,8 +97,8 @@ export function createTelegramNoticeRuntime<TContext>(
|
||||
return;
|
||||
}
|
||||
lastDeliveryNoticeAtMs = nowMs;
|
||||
const { reason, level } = describeTelegramDeliveryFailure(error);
|
||||
show(ctx, `Telegram: reply not delivered (${reason})`, level);
|
||||
const failure = describeTelegramDeliveryFailure(error);
|
||||
show(ctx, failure.message, failure.level);
|
||||
},
|
||||
};
|
||||
}
|
||||
+22
-7
@@ -1055,16 +1055,32 @@ export async function runTelegramPollLoop<
|
||||
const admittedUpdates = new Set<number>();
|
||||
let handledUpdateFailureRethrown = false;
|
||||
let consecutiveGetUpdatesConflicts = 0;
|
||||
let failing = false;
|
||||
// An error status is on screen; it stays until polling works again, so a
|
||||
// slow retry (for example a long rate-limit wait) never shows as healthy.
|
||||
let errorShown = false;
|
||||
// The last getUpdates failed, so the next successful one is a recovery.
|
||||
// Failures of an update handler do not count: the transport kept working.
|
||||
let transportDown = false;
|
||||
const showError = (message: string): void => {
|
||||
errorShown = true;
|
||||
deps.onErrorStatus(message);
|
||||
};
|
||||
while (!deps.signal.aborted) {
|
||||
let inGetUpdates = false;
|
||||
try {
|
||||
inGetUpdates = true;
|
||||
const updates = await deps.getUpdates(
|
||||
buildTelegramLongPollRequest(deps.config.lastUpdateId),
|
||||
deps.signal,
|
||||
);
|
||||
inGetUpdates = false;
|
||||
consecutiveGetUpdatesConflicts = 0;
|
||||
if (failing) {
|
||||
failing = false;
|
||||
if (errorShown) {
|
||||
errorShown = false;
|
||||
deps.onStatusReset();
|
||||
}
|
||||
if (transportDown) {
|
||||
transportDown = false;
|
||||
deps.onRecovered?.();
|
||||
}
|
||||
for (const update of updates) {
|
||||
@@ -1095,7 +1111,7 @@ export async function runTelegramPollLoop<
|
||||
throw error;
|
||||
}
|
||||
const message = getTelegramPollingErrorMessage(error);
|
||||
deps.onErrorStatus(
|
||||
showError(
|
||||
`skipping Telegram update ${update.update_id} after ${failureCount} failures: ${message}`,
|
||||
);
|
||||
admittedUpdates.add(update.update_id);
|
||||
@@ -1124,11 +1140,10 @@ export async function runTelegramPollLoop<
|
||||
continue;
|
||||
}
|
||||
consecutiveGetUpdatesConflicts = 0;
|
||||
failing = true;
|
||||
deps.onErrorStatus(getTelegramPollingErrorMessage(error));
|
||||
if (inGetUpdates) transportDown = true;
|
||||
showError(getTelegramPollingErrorMessage(error));
|
||||
await deps.sleep(TELEGRAM_POLLING_RETRY_MS, deps.signal);
|
||||
if (deps.signal.aborted) return;
|
||||
deps.onStatusReset();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -908,6 +908,11 @@ export interface TelegramStatusErrorSummary {
|
||||
hint?: string;
|
||||
/** fatal needs the operator, attention needs a look, retrying heals itself. */
|
||||
kind: "fatal" | "attention" | "retrying";
|
||||
/**
|
||||
* The failed call may still have reached Telegram: a non-idempotent send hit
|
||||
* a transport failure after the request left, so the outcome is unknown.
|
||||
*/
|
||||
deliveryUnknown?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -917,6 +922,13 @@ export interface TelegramStatusErrorSummary {
|
||||
export function summarizeTelegramStatusError(
|
||||
message: string,
|
||||
): TelegramStatusErrorSummary | undefined {
|
||||
if (/may have committed before transport failed/i.test(message)) {
|
||||
return {
|
||||
kind: "attention",
|
||||
label: "delivery unconfirmed",
|
||||
deliveryUnknown: true,
|
||||
};
|
||||
}
|
||||
if (/HTTP 401\b|\bUnauthorized\b/i.test(message)) {
|
||||
return { kind: "fatal", label: "invalid token", hint: "/telegram-setup" };
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user