From 7ed0f082693df7d1c8d5e7bc463dc1cb57f77f86 Mon Sep 17 00:00:00 2001 From: 134 Date: Wed, 8 Jul 2026 20:29:46 +0800 Subject: [PATCH] polish canvas toolbar and upload stall handling --- components/canvas/nodes/ImageNode.tsx | 2 +- lib/client/upload-with-retry.ts | 33 +++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/components/canvas/nodes/ImageNode.tsx b/components/canvas/nodes/ImageNode.tsx index 109e435b1..f1340ec0b 100644 --- a/components/canvas/nodes/ImageNode.tsx +++ b/components/canvas/nodes/ImageNode.tsx @@ -10193,7 +10193,7 @@ export function ImageNode({ createPortal(
e.stopPropagation()} onMouseDown={(e) => e.stopPropagation()} onPointerDown={(e) => e.stopPropagation()} diff --git a/lib/client/upload-with-retry.ts b/lib/client/upload-with-retry.ts index a6339a316..ba19a1b61 100644 --- a/lib/client/upload-with-retry.ts +++ b/lib/client/upload-with-retry.ts @@ -7,6 +7,8 @@ const SERVER_UPLOAD_MAX_BYTES = 50 * 1024 * 1024; const DIRECT_UPLOAD_PART_CONCURRENCY = 4; const DIRECT_UPLOAD_SUPPRESS_MS = 2 * 60 * 1000; const CLIENT_UPLOAD_ID_FIELD = "clientUploadId"; +const XHR_UPLOAD_TOTAL_TIMEOUT_MS = 6 * 60 * 1000; +const XHR_UPLOAD_STALL_TIMEOUT_MS = 30 * 1000; const RETRYABLE_UPLOAD_STATUSES = new Set([ 408, @@ -388,6 +390,8 @@ function xhrUpload( options?: { signal?: AbortSignal; onProgress?: (loaded: number, total: number) => void; + timeoutMs?: number; + stallTimeoutMs?: number; } ) { return new Promise((resolve, reject) => { @@ -398,10 +402,18 @@ function xhrUpload( const xhr = new XMLHttpRequest(); let settled = false; + let stallTimer: ReturnType | null = null; + + const clearStallTimer = () => { + if (!stallTimer) return; + clearTimeout(stallTimer); + stallTimer = null; + }; const settle = (callback: () => void) => { if (settled) return; settled = true; + clearStallTimer(); options?.signal?.removeEventListener("abort", onAbort); callback(); }; @@ -412,12 +424,32 @@ function xhrUpload( }; xhr.open(init.method, url, true); + xhr.timeout = options?.timeoutMs ?? XHR_UPLOAD_TOTAL_TIMEOUT_MS; for (const [key, value] of normalizeXhrHeaders(init.headers)) { xhr.setRequestHeader(key, value); } + const resetStallTimer = () => { + clearStallTimer(); + if (settled) return; + + const stallTimeoutMs = + options?.stallTimeoutMs ?? XHR_UPLOAD_STALL_TIMEOUT_MS; + if (stallTimeoutMs <= 0) return; + + stallTimer = setTimeout(() => { + const error = new Error( + `Upload stalled for ${Math.round(stallTimeoutMs / 1000)}s` + ); + error.name = "TimeoutError"; + settle(() => reject(error)); + xhr.abort(); + }, stallTimeoutMs); + }; + xhr.upload.onprogress = (event) => { + resetStallTimer(); const total = event.lengthComputable ? event.total : 0; options?.onProgress?.(event.loaded, total); }; @@ -435,6 +467,7 @@ function xhrUpload( }; options?.signal?.addEventListener("abort", onAbort, { once: true }); + resetStallTimer(); xhr.send(init.body); }); }