From da52088e3912158bdf55b299189d49b4bc8e169d Mon Sep 17 00:00:00 2001 From: ashim-hq Date: Mon, 20 Apr 2026 22:18:27 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20address=20code=20review=20=E2=80=94=20co?= =?UTF-8?q?mplete=20fetch=20coverage,=20refresh=20timing,=20dedupe=20chunk?= =?UTF-8?q?=20detection?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/src/App.tsx | 9 +-- apps/web/src/hooks/use-connection-monitor.ts | 16 +++-- apps/web/src/lib/api.ts | 68 ++++++++++++++------ apps/web/src/lib/lazy-with-retry.ts | 2 +- 4 files changed, 63 insertions(+), 32 deletions(-) diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index eb6f8992..20ea42d2 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -5,7 +5,7 @@ import { ConnectionBanner } from "./components/common/connection-banner"; import { KeyboardShortcutProvider } from "./components/common/keyboard-shortcut-provider"; import { useAuth } from "./hooks/use-auth"; import { useConnectionMonitor } from "./hooks/use-connection-monitor"; -import { lazyWithRetry } from "./lib/lazy-with-retry"; +import { isChunkError, lazyWithRetry } from "./lib/lazy-with-retry"; // Lazy-load all pages with automatic retry so chunk failures from // deployments are recovered transparently instead of white-screening. @@ -44,12 +44,7 @@ class ErrorBoundary extends Component< } static getDerivedStateFromError(error: Error) { - const msg = error.message.toLowerCase(); - const isChunkError = - msg.includes("dynamically imported module") || - msg.includes("loading chunk") || - msg.includes("failed to fetch"); - return { hasError: true, error, isChunkError }; + return { hasError: true, error, isChunkError: isChunkError(error) }; } componentDidCatch(error: Error, info: ErrorInfo) { diff --git a/apps/web/src/hooks/use-connection-monitor.ts b/apps/web/src/hooks/use-connection-monitor.ts index 35df2bac..519caa90 100644 --- a/apps/web/src/hooks/use-connection-monitor.ts +++ b/apps/web/src/hooks/use-connection-monitor.ts @@ -25,12 +25,16 @@ export function useConnectionMonitor() { if (state.status === "reconnected") { store.getState().stopPolling(); - store.getState().refreshStaleData(); - setTimeout(() => { - if (store.getState().status === "reconnected") { - store.setState({ status: "connected" }); - } - }, 2500); + store + .getState() + .refreshStaleData() + .finally(() => { + setTimeout(() => { + if (store.getState().status === "reconnected") { + store.setState({ status: "connected" }); + } + }, 2500); + }); } if (state.status === "offline") { diff --git a/apps/web/src/lib/api.ts b/apps/web/src/lib/api.ts index 4a0fa5dd..6d9dd242 100644 --- a/apps/web/src/lib/api.ts +++ b/apps/web/src/lib/api.ts @@ -162,11 +162,19 @@ export async function apiUpload(files: File[]): Promise<{ }> { const formData = new FormData(); for (const f of files) formData.append("files", f); - const res = await fetch("/api/v1/upload", { - method: "POST", - headers: formatHeaders(), - body: formData, - }); + let res: Response; + try { + res = await fetch("/api/v1/upload", { + method: "POST", + headers: formatHeaders(), + body: formData, + }); + } catch (error) { + if (error instanceof TypeError) { + useConnectionStore.getState().setDisconnected(); + } + throw error; + } if (!res.ok) throw new Error(`Upload failed: ${res.status}`); return res.json(); } @@ -224,21 +232,37 @@ export async function apiUploadUserFiles( ): Promise<{ files: Array<{ id: string; originalName: string; size: number; version: number }> }> { const formData = new FormData(); for (const f of files) formData.append("files", f); - const res = await fetch("/api/v1/files/upload", { - method: "POST", - headers: formatHeaders(), - body: formData, - }); + let res: Response; + try { + res = await fetch("/api/v1/files/upload", { + method: "POST", + headers: formatHeaders(), + body: formData, + }); + } catch (error) { + if (error instanceof TypeError) { + useConnectionStore.getState().setDisconnected(); + } + throw error; + } if (!res.ok) throw new Error(`Upload failed: ${res.status}`); return res.json(); } export async function apiDeleteUserFiles(ids: string[]): Promise<{ deleted: number }> { - const res = await fetch("/api/v1/files", { - method: "DELETE", - headers: formatHeaders({ "Content-Type": "application/json" }), - body: JSON.stringify({ ids }), - }); + let res: Response; + try { + res = await fetch("/api/v1/files", { + method: "DELETE", + headers: formatHeaders({ "Content-Type": "application/json" }), + body: JSON.stringify({ ids }), + }); + } catch (error) { + if (error instanceof TypeError) { + useConnectionStore.getState().setDisconnected(); + } + throw error; + } if (!res.ok) throw new Error(`Delete failed: ${res.status}`); return res.json(); } @@ -252,9 +276,17 @@ export function getFileDownloadUrl(id: string): string { } export async function apiDownloadBlob(jobId: string, filename: string): Promise { - const res = await fetch(getDownloadUrl(jobId, filename), { - headers: formatHeaders(), - }); + let res: Response; + try { + res = await fetch(getDownloadUrl(jobId, filename), { + headers: formatHeaders(), + }); + } catch (error) { + if (error instanceof TypeError) { + useConnectionStore.getState().setDisconnected(); + } + throw error; + } if (!res.ok) throw new Error(`Download failed: ${res.status}`); return res.blob(); } diff --git a/apps/web/src/lib/lazy-with-retry.ts b/apps/web/src/lib/lazy-with-retry.ts index 96d95d92..cc37950a 100644 --- a/apps/web/src/lib/lazy-with-retry.ts +++ b/apps/web/src/lib/lazy-with-retry.ts @@ -1,6 +1,6 @@ import { type ComponentType, lazy } from "react"; -function isChunkError(error: unknown): boolean { +export function isChunkError(error: unknown): boolean { if (!(error instanceof Error)) return false; const msg = error.message.toLowerCase(); return (