mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
fix: address code review — complete fetch coverage, refresh timing, dedupe chunk detection
This commit is contained in:
@@ -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) {
|
||||||
|
|||||||
@@ -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
@@ -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,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 (
|
||||||
|
|||||||
Reference in New Issue
Block a user