fix: address code review — complete fetch coverage, refresh timing, dedupe chunk detection

This commit is contained in:
ashim-hq
2026-04-20 22:18:27 +08:00
parent 5e1159fa75
commit da52088e39
4 changed files with 63 additions and 32 deletions
+2 -7
View File
@@ -5,7 +5,7 @@ import { ConnectionBanner } from "./components/common/connection-banner";
import { KeyboardShortcutProvider } from "./components/common/keyboard-shortcut-provider"; import { KeyboardShortcutProvider } from "./components/common/keyboard-shortcut-provider";
import { useAuth } from "./hooks/use-auth"; import { useAuth } from "./hooks/use-auth";
import { useConnectionMonitor } from "./hooks/use-connection-monitor"; 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 // Lazy-load all pages with automatic retry so chunk failures from
// deployments are recovered transparently instead of white-screening. // deployments are recovered transparently instead of white-screening.
@@ -44,12 +44,7 @@ class ErrorBoundary extends Component<
} }
static getDerivedStateFromError(error: Error) { static getDerivedStateFromError(error: Error) {
const msg = error.message.toLowerCase(); return { hasError: true, error, isChunkError: isChunkError(error) };
const isChunkError =
msg.includes("dynamically imported module") ||
msg.includes("loading chunk") ||
msg.includes("failed to fetch");
return { hasError: true, error, isChunkError };
} }
componentDidCatch(error: Error, info: ErrorInfo) { componentDidCatch(error: Error, info: ErrorInfo) {
+10 -6
View File
@@ -25,12 +25,16 @@ export function useConnectionMonitor() {
if (state.status === "reconnected") { if (state.status === "reconnected") {
store.getState().stopPolling(); store.getState().stopPolling();
store.getState().refreshStaleData(); store
setTimeout(() => { .getState()
if (store.getState().status === "reconnected") { .refreshStaleData()
store.setState({ status: "connected" }); .finally(() => {
} setTimeout(() => {
}, 2500); if (store.getState().status === "reconnected") {
store.setState({ status: "connected" });
}
}, 2500);
});
} }
if (state.status === "offline") { if (state.status === "offline") {
+50 -18
View File
@@ -162,11 +162,19 @@ export async function apiUpload(files: File[]): Promise<{
}> { }> {
const formData = new FormData(); const formData = new FormData();
for (const f of files) formData.append("files", f); for (const f of files) formData.append("files", f);
const res = await fetch("/api/v1/upload", { let res: Response;
method: "POST", try {
headers: formatHeaders(), res = await fetch("/api/v1/upload", {
body: formData, 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}`); if (!res.ok) throw new Error(`Upload failed: ${res.status}`);
return res.json(); return res.json();
} }
@@ -224,21 +232,37 @@ export async function apiUploadUserFiles(
): Promise<{ files: Array<{ id: string; originalName: string; size: number; version: number }> }> { ): Promise<{ files: Array<{ id: string; originalName: string; size: number; version: number }> }> {
const formData = new FormData(); const formData = new FormData();
for (const f of files) formData.append("files", f); for (const f of files) formData.append("files", f);
const res = await fetch("/api/v1/files/upload", { let res: Response;
method: "POST", try {
headers: formatHeaders(), res = await fetch("/api/v1/files/upload", {
body: formData, 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}`); if (!res.ok) throw new Error(`Upload failed: ${res.status}`);
return res.json(); return res.json();
} }
export async function apiDeleteUserFiles(ids: string[]): Promise<{ deleted: number }> { export async function apiDeleteUserFiles(ids: string[]): Promise<{ deleted: number }> {
const res = await fetch("/api/v1/files", { let res: Response;
method: "DELETE", try {
headers: formatHeaders({ "Content-Type": "application/json" }), res = await fetch("/api/v1/files", {
body: JSON.stringify({ ids }), 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}`); if (!res.ok) throw new Error(`Delete failed: ${res.status}`);
return res.json(); return res.json();
} }
@@ -252,9 +276,17 @@ export function getFileDownloadUrl(id: string): string {
} }
export async function apiDownloadBlob(jobId: string, filename: string): Promise<Blob> { export async function apiDownloadBlob(jobId: string, filename: string): Promise<Blob> {
const res = await fetch(getDownloadUrl(jobId, filename), { let res: Response;
headers: formatHeaders(), 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}`); if (!res.ok) throw new Error(`Download failed: ${res.status}`);
return res.blob(); return res.blob();
} }
+1 -1
View File
@@ -1,6 +1,6 @@
import { type ComponentType, lazy } from "react"; import { type ComponentType, lazy } from "react";
function isChunkError(error: unknown): boolean { export function isChunkError(error: unknown): boolean {
if (!(error instanceof Error)) return false; if (!(error instanceof Error)) return false;
const msg = error.message.toLowerCase(); const msg = error.message.toLowerCase();
return ( return (