fix: prevent silent config fallback and data loss on startup

This commit introduces fail-fast error handling for config loading failures,
replacing the previous silent fallback to default config which could cause
data loss (e.g., all user providers disappearing).

Key changes:

Backend (Rust):
- Replace AppState::new() with AppState::try_new() to explicitly propagate errors
- Remove Default trait to prevent accidental empty state creation
- Add init_status module as global error cache (OnceLock + RwLock)
- Implement dual-channel error notification:
  1. Event emission (low-latency, may race with frontend subscription)
  2. Command-based polling (reliable, guaranteed delivery)
- Remove unconditional save on startup to prevent overwriting corrupted config

Frontend (TypeScript):
- Add event listener for "configLoadError" (fast path)
- Add bootstrap-time polling via get_init_error command (reliable path)
- Display detailed error dialog with recovery instructions
- Prompt user to exit for manual repair

Impact:
- First-time users: No change (load() returns Ok(default) when file missing)
- Corrupted config: Application shows error and exits gracefully
- Prevents accidental config overwrite during initialization

Fixes the only critical issue identified in previous code review (silent
fallback causing data loss).
This commit is contained in:
Jason
2025-11-03 22:33:10 +08:00
parent 36fd61b2a2
commit 4afa68eac6
5 changed files with 154 additions and 29 deletions

View File

@@ -9,6 +9,10 @@ import { QueryClientProvider } from "@tanstack/react-query";
import { ThemeProvider } from "@/components/theme-provider";
import { queryClient } from "@/lib/query";
import { Toaster } from "@/components/ui/sonner";
import { listen } from "@tauri-apps/api/event";
import { invoke } from "@tauri-apps/api/core";
import { confirm, message } from "@tauri-apps/plugin-dialog";
import { exit } from "@tauri-apps/plugin-process";
// 根据平台添加 body class便于平台特定样式
try {
@@ -22,15 +26,73 @@ try {
// 忽略平台检测失败
}
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<ThemeProvider defaultTheme="system" storageKey="cc-switch-theme">
<UpdateProvider>
<App />
<Toaster />
</UpdateProvider>
</ThemeProvider>
</QueryClientProvider>
</React.StrictMode>,
);
// 监听后端的配置加载错误事件:仅提醒用户并在确认后退出,不修改任何配置文件
try {
void listen("configLoadError", async (evt) => {
const payload = evt.payload as { path?: string; error?: string } | null;
const path = payload?.path ?? "~/.cc-switch/config.json";
const detail = payload?.error ?? "Unknown error";
await message(
`无法读取配置文件:\n${path}\n\n错误详情\n${detail}\n\n请手动检查 JSON 是否有效,或从同目录的备份文件(如 config.json.bak恢复。`,
{ title: "配置加载失败", kind: "error" },
);
const shouldExit = await confirm("现在退出应用以进行修复?", {
title: "退出确认",
okLabel: "退出应用",
cancelLabel: "取消",
});
if (shouldExit) {
await exit(1);
}
});
} catch (e) {
// 忽略事件订阅异常(例如在非 Tauri 环境下)
console.error("订阅 configLoadError 事件失败", e);
}
async function bootstrap() {
// 启动早期主动查询后端初始化错误,避免事件竞态
try {
const initError = (await invoke("get_init_error")) as
| { path?: string; error?: string }
| null;
if (initError && (initError.path || initError.error)) {
const path = initError.path ?? "~/.cc-switch/config.json";
const detail = initError.error ?? "Unknown error";
await message(
`无法读取配置文件:\n${path}\n\n错误详情\n${detail}\n\n请手动检查 JSON 是否有效,或从同目录的备份文件(如 config.json.bak恢复。`,
{ title: "配置加载失败", kind: "error" },
);
const shouldExit = await confirm("现在退出应用以进行修复?", {
title: "退出确认",
okLabel: "退出应用",
cancelLabel: "取消",
});
if (shouldExit) {
await exit(1);
return; // 退出流程
}
}
} catch (e) {
// 忽略拉取错误,继续渲染
console.error("拉取初始化错误失败", e);
}
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<ThemeProvider defaultTheme="system" storageKey="cc-switch-theme">
<UpdateProvider>
<App />
<Toaster />
</UpdateProvider>
</ThemeProvider>
</QueryClientProvider>
</React.StrictMode>,
);
}
void bootstrap();