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 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
const timeoutMs = Math.max(
15 * 60_000,
@@ -202,6 +208,26 @@ async function sendAndWaitForUpload(
}
}, 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
const handleUpdate = (update: any) => {
// 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
if (update?._ === "updateMessageSendSucceeded") {
const msg = update.message;
const oldMsgId = update.old_message_id;
if (tempMsgId !== null && oldMsgId === tempMsgId) {
if (!settled) {
settled = true;
cleanup();
const finalId = BigInt(msg.id);
log.info(
{ fileName, tempMsgId, finalMsgId: Number(finalId) },
"Upload confirmed by Telegram"
);
resolve(finalId);
}
const oldMsgId: number = update.old_message_id;
if (tempMsgId === null) {
// Race: event arrived before our .then() assigned tempMsgId.
// Buffer it and process once tempMsgId is known.
pendingSuccess = { oldMsgId, finalId: msg.id };
return;
}
if (oldMsgId === tempMsgId) {
completeWithSuccess(msg.id);
}
}
// Upload failed
if (update?._ === "updateMessageSendFailed") {
const oldMsgId = update.old_message_id;
if (tempMsgId !== null && oldMsgId === tempMsgId) {
if (!settled) {
settled = true;
cleanup();
const errorMsg = update.error?.message ?? "Unknown upload error";
const error = new Error(`Upload failed for ${fileName}: ${errorMsg}`);
(error as Error & { code?: number }).code = update.error?.code;
reject(error);
}
const oldMsgId: number = update.old_message_id;
const errorMsg: string = update.error?.message ?? "Unknown upload error";
const code: number | undefined = update.error?.code;
if (tempMsgId === null) {
pendingFailure = { oldMsgId, errorMsg, code };
return;
}
if (oldMsgId === tempMsgId) {
completeWithFailure(errorMsg, code);
}
}
};
@@ -302,6 +324,13 @@ async function sendAndWaitForUpload(
{ fileName, tempMsgId },
"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) => {
if (!settled) {