From 26e2cba69df6d9a4994c223305f5089a6d1b2c29 Mon Sep 17 00:00:00 2001 From: xCyanGrizzly Date: Fri, 22 May 2026 22:48:38 +0200 Subject: [PATCH] fix: buffer upload confirmation events to close tempMsgId race MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- worker/src/upload/channel.ts | 73 +++++++++++++++++++++++++----------- 1 file changed, 51 insertions(+), 22 deletions(-) diff --git a/worker/src/upload/channel.ts b/worker/src/upload/channel.ts index 301c3e0..f684cde 100644 --- a/worker/src/upload/channel.ts +++ b/worker/src/upload/channel.ts @@ -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) {