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

@@ -0,0 +1,42 @@
use serde::Serialize;
use std::sync::{OnceLock, RwLock};
#[derive(Debug, Clone, Serialize)]
pub struct InitErrorPayload {
pub path: String,
pub error: String,
}
static INIT_ERROR: OnceLock<RwLock<Option<InitErrorPayload>>> = OnceLock::new();
fn cell() -> &'static RwLock<Option<InitErrorPayload>> {
INIT_ERROR.get_or_init(|| RwLock::new(None))
}
pub fn set_init_error(payload: InitErrorPayload) {
if let Ok(mut guard) = cell().write() {
*guard = Some(payload);
}
}
pub fn get_init_error() -> Option<InitErrorPayload> {
cell().read().ok()?.clone()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn init_error_roundtrip() {
let payload = InitErrorPayload {
path: "/tmp/config.json".into(),
error: "broken json".into(),
};
set_init_error(payload.clone());
let got = get_init_error().expect("should get payload back");
assert_eq!(got.path, payload.path);
assert_eq!(got.error, payload.error);
}
}