fix: buffer upload confirmation events to close tempMsgId race

sendMessage resolves with the temporary message ID inside a .then()
microtask. If TDLib emits updateMessageSendSucceeded synchronously
(cached file, already-known media), the event handler fires while
tempMsgId is still null — the success is dropped and the promise hangs
until the 15-min upload timeout fires.

Buffer success/failure events that arrive before tempMsgId is known,
then replay them in the .then() callback once tempMsgId is set.
Extract completeWithSuccess / completeWithFailure helpers so the
resolution path is shared between live events and replayed events.

This race matters more now that stalls fail fast — without the buffer,
a fast-completing upload could still hang for 15 min before recovery.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-22 22:48:38 +02:00
parent 84cc8d995b
commit 26e2cba69d

View File

@@ -166,6 +166,12 @@ async function sendAndWaitForUpload(
let lastProgressBytes = 0; let lastProgressBytes = 0;
let lastProgressTime = Date.now(); let lastProgressTime = Date.now();
// Events for our message can arrive before `sendMessage` resolves
// (TDLib emits them while our .then() is still in the microtask queue).
// Buffer them and replay once tempMsgId is known.
let pendingSuccess: { oldMsgId: number; finalId: number } | null = null;
let pendingFailure: { oldMsgId: number; errorMsg: string; code?: number } | null = null;
// Timeout: 20 minutes per GB, minimum 15 minutes // Timeout: 20 minutes per GB, minimum 15 minutes
const timeoutMs = Math.max( const timeoutMs = Math.max(
15 * 60_000, 15 * 60_000,
@@ -202,6 +208,26 @@ async function sendAndWaitForUpload(
} }
}, 30_000); }, 30_000);
const completeWithSuccess = (finalId: number) => {
if (settled) return;
settled = true;
cleanup();
log.info(
{ fileName, tempMsgId, finalMsgId: finalId },
"Upload confirmed by Telegram"
);
resolve(BigInt(finalId));
};
const completeWithFailure = (errorMsg: string, code?: number) => {
if (settled) return;
settled = true;
cleanup();
const error = new Error(`Upload failed for ${fileName}: ${errorMsg}`);
(error as Error & { code?: number }).code = code;
reject(error);
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const handleUpdate = (update: any) => { const handleUpdate = (update: any) => {
// Track upload progress via updateFile events // Track upload progress via updateFile events
@@ -232,33 +258,29 @@ async function sendAndWaitForUpload(
// The money event: upload succeeded, we get the final server message ID // The money event: upload succeeded, we get the final server message ID
if (update?._ === "updateMessageSendSucceeded") { if (update?._ === "updateMessageSendSucceeded") {
const msg = update.message; const msg = update.message;
const oldMsgId = update.old_message_id; const oldMsgId: number = update.old_message_id;
if (tempMsgId !== null && oldMsgId === tempMsgId) { if (tempMsgId === null) {
if (!settled) { // Race: event arrived before our .then() assigned tempMsgId.
settled = true; // Buffer it and process once tempMsgId is known.
cleanup(); pendingSuccess = { oldMsgId, finalId: msg.id };
const finalId = BigInt(msg.id); return;
log.info(
{ fileName, tempMsgId, finalMsgId: Number(finalId) },
"Upload confirmed by Telegram"
);
resolve(finalId);
} }
if (oldMsgId === tempMsgId) {
completeWithSuccess(msg.id);
} }
} }
// Upload failed // Upload failed
if (update?._ === "updateMessageSendFailed") { if (update?._ === "updateMessageSendFailed") {
const oldMsgId = update.old_message_id; const oldMsgId: number = update.old_message_id;
if (tempMsgId !== null && oldMsgId === tempMsgId) { const errorMsg: string = update.error?.message ?? "Unknown upload error";
if (!settled) { const code: number | undefined = update.error?.code;
settled = true; if (tempMsgId === null) {
cleanup(); pendingFailure = { oldMsgId, errorMsg, code };
const errorMsg = update.error?.message ?? "Unknown upload error"; return;
const error = new Error(`Upload failed for ${fileName}: ${errorMsg}`);
(error as Error & { code?: number }).code = update.error?.code;
reject(error);
} }
if (oldMsgId === tempMsgId) {
completeWithFailure(errorMsg, code);
} }
} }
}; };
@@ -302,6 +324,13 @@ async function sendAndWaitForUpload(
{ fileName, tempMsgId }, { fileName, tempMsgId },
"Message queued, waiting for upload confirmation" "Message queued, waiting for upload confirmation"
); );
// Replay any event that arrived before we knew tempMsgId
if (pendingSuccess && pendingSuccess.oldMsgId === tempMsgId) {
completeWithSuccess(pendingSuccess.finalId);
} else if (pendingFailure && pendingFailure.oldMsgId === tempMsgId) {
completeWithFailure(pendingFailure.errorMsg, pendingFailure.code);
}
}) })
.catch((err) => { .catch((err) => {
if (!settled) { if (!settled) {