feat(electron): error handling foundation
Adds the infrastructure for a coherent client-side error story: - `lib/errors.ts`: canonical ApiError + QuotaExceededError, `toUserMessage` (friendly strings for ApiError/ZodError/network errors, strips Electron IPC message prefixes), `logError` (expected), `reportError` (unexpected). - `lib/query-client.ts`: QueryClient factory with sane retry defaults (no retry on 4xx except 408/429, 2 retries otherwise; 0 mutation retries), `QueryCache` onError logs + opts in via `meta.toastOnError`, and `MutationCache` onError toasts `toUserMessage(err)` by default with `meta.suppressToast` as the opt-out. - `components/app-error-boundary.tsx` + `error-fallback.tsx`: two boundaries (top-level outside the router, route-level inside) with a Card-based fallback offering 'Go home' + 'Try again'. Route boundary resets on pathname change and clears React Query error cache on retry. - `main/ipc-utils.ts` + main.ts migration: `safeHandle` wraps ipcMain.handle so main-process failures log with full stack and surface a sanitized message to the renderer. `link:fetch-metadata` keeps its null contract but now logs. - `useCreateParticle` opts out of the global toast (compose-overlay renders its own quota UX) so nothing double-toasts. Render crashes now have a recovery UI, every mutation gets a free error toast, and silent-catch cleanup + Sentry land in follow-up PRs.
This commit is contained in:
+28
-22
@@ -3,10 +3,7 @@ import { HashRouter, Routes, Route, useNavigate } from "react-router-dom";
|
||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||
import { useAuthStore } from "@/stores/auth-store";
|
||||
import { LoginPage } from "@/features/auth/login-page";
|
||||
import {
|
||||
QueryClient,
|
||||
QueryClientProvider,
|
||||
} from '@tanstack/react-query'
|
||||
import { QueryClientProvider } from '@tanstack/react-query';
|
||||
import SettingsPage from "@/features/settings-page";
|
||||
import AudioVideoSettingsPage from "@/features/settings/audio-video-settings-page";
|
||||
import NetworkSelector from "@/features/network-selector";
|
||||
@@ -16,8 +13,13 @@ import Layout from "@/features/layout";
|
||||
import NetworkSettingsPage from "@/features/network-settings";
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
import { PusherProvider } from "@/lib/pusher-provider";
|
||||
import { createQueryClient } from "@/lib/query-client";
|
||||
import {
|
||||
RouteErrorBoundary,
|
||||
TopLevelErrorBoundary,
|
||||
} from "@/components/app-error-boundary";
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
const queryClient = createQueryClient();
|
||||
|
||||
const App = () => {
|
||||
const status = useAuthStore((s) => s.status);
|
||||
@@ -62,30 +64,34 @@ function AuthenticatedApp() {
|
||||
return (
|
||||
<HashRouter>
|
||||
<AutoplayNavigationListener />
|
||||
<Routes>
|
||||
<Route path="settings" element={<SettingsPage />} />
|
||||
<Route path="settings/audio-video" element={<AudioVideoSettingsPage />} />
|
||||
<RouteErrorBoundary>
|
||||
<Routes>
|
||||
<Route path="settings" element={<SettingsPage />} />
|
||||
<Route path="settings/audio-video" element={<AudioVideoSettingsPage />} />
|
||||
|
||||
<Route path="/">
|
||||
<Route index element={<Layout><NetworkSelector /></Layout>} />
|
||||
<Route path=":networkId">
|
||||
<Route index element={<Layout><NetworkRoot /></Layout>} />
|
||||
<Route path="settings" element={<NetworkSettingsPage />} />
|
||||
<Route path="*" element={<ParticleViewResolver />} />
|
||||
<Route path="/">
|
||||
<Route index element={<Layout><NetworkSelector /></Layout>} />
|
||||
<Route path=":networkId">
|
||||
<Route index element={<Layout><NetworkRoot /></Layout>} />
|
||||
<Route path="settings" element={<NetworkSettingsPage />} />
|
||||
<Route path="*" element={<ParticleViewResolver />} />
|
||||
</Route>
|
||||
</Route>
|
||||
</Route>
|
||||
</Routes>
|
||||
</Routes>
|
||||
</RouteErrorBoundary>
|
||||
</HashRouter>
|
||||
);
|
||||
}
|
||||
|
||||
const AppWithProviders = () => (
|
||||
<TooltipProvider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<App />
|
||||
<Toaster />
|
||||
</QueryClientProvider>
|
||||
</TooltipProvider>
|
||||
<TopLevelErrorBoundary>
|
||||
<TooltipProvider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<App />
|
||||
<Toaster />
|
||||
</QueryClientProvider>
|
||||
</TooltipProvider>
|
||||
</TopLevelErrorBoundary>
|
||||
);
|
||||
|
||||
export default AppWithProviders;
|
||||
|
||||
Reference in New Issue
Block a user