fixed connection reconnection logic

This commit is contained in:
2026-02-08 00:54:31 +08:00
parent 4ad6fb709c
commit f0cb2fd1d0
7 changed files with 141 additions and 28 deletions

View File

@@ -9,6 +9,7 @@ use crate::{
services::{
close_all_windows,
health_manager::open_health_manager_window,
health_monitor::{start_health_monitor, stop_health_monitor},
scene::open_scene_window,
ws::client::{clear_ws_client, establish_websocket_connection},
},
@@ -39,13 +40,15 @@ async fn connect_user_profile() {
init_app_data_scoped(AppDataRefreshScope::All).await;
establish_websocket_connection().await;
start_background_token_refresh().await;
start_health_monitor().await;
}
/// Clears the user profile and WebSocket connection.
async fn disconnect_user_profile() {
stop_health_monitor();
stop_background_token_refresh();
clear_app_data();
clear_ws_client().await;
stop_background_token_refresh();
}
/// Destructs the user session and show health manager window

View File

@@ -0,0 +1,72 @@
use crate::{
init::lifecycle::{handle_disasterous_failure, validate_server_health},
lock_w,
services::ws::client::establish_websocket_connection,
state::FDOLL,
};
use tokio::time::{self, Duration};
use tokio_util::sync::CancellationToken;
use tracing::{info, warn};
/// Starts a periodic health monitor that validates server connectivity
/// and attempts to recover WebSocket connection if health checks fail.
pub async fn start_health_monitor() {
stop_health_monitor();
let cancel_token = CancellationToken::new();
{
let mut guard = lock_w!(FDOLL);
guard.network.health_monitor_token = Some(cancel_token.clone());
}
tokio::spawn(async move {
let mut interval = time::interval(Duration::from_secs(30)); // Check every 30 seconds
let mut consecutive_failures = 0u8;
const MAX_FAILURES: u8 = 3;
loop {
tokio::select! {
_ = cancel_token.cancelled() => {
info!("Health monitor stopped");
break;
}
_ = interval.tick() => {
match validate_server_health().await {
Ok(_) => {
consecutive_failures = 0;
}
Err(e) => {
consecutive_failures = consecutive_failures.saturating_add(1);
warn!(
"Health check failed ({}/{}): {}",
consecutive_failures, MAX_FAILURES, e
);
if consecutive_failures >= MAX_FAILURES {
info!("Server appears unreachable after {} attempts, triggering recovery", MAX_FAILURES);
handle_disasterous_failure(Some(format!(
"Lost connection to server: {}",
e
)))
.await;
break;
} else {
// Try to re-establish WebSocket connection
info!("Attempting to re-establish WebSocket connection");
establish_websocket_connection().await;
}
}
}
}
}
}
});
}
/// Stops the health monitor loop.
pub fn stop_health_monitor() {
let mut guard = lock_w!(FDOLL);
if let Some(token) = guard.network.health_monitor_token.take() {
token.cancel();
}
}

View File

@@ -9,6 +9,7 @@ pub mod client_config_manager;
pub mod cursor;
pub mod doll_editor;
pub mod health_manager;
pub mod health_monitor;
pub mod interaction;
pub mod scene;
pub mod sprite_recolor;

View File

@@ -19,15 +19,27 @@ pub async fn establish_websocket_connection() {
for _attempt in 1..=MAX_ATTEMPTS {
if get_access_token().await.is_some() {
init_ws_client().await;
if init_ws_client().await {
return; // Success
} else {
// Connection failed, trigger disaster recovery
crate::init::lifecycle::handle_disasterous_failure(
Some("WebSocket connection failed. Please check your network and try again.".to_string())
).await;
return;
}
}
sleep(BACKOFF).await;
}
// If we exhausted retries without valid token
crate::init::lifecycle::handle_disasterous_failure(
Some("Failed to authenticate. Please restart and sign in again.".to_string())
).await;
}
pub async fn init_ws_client() {
pub async fn init_ws_client() -> bool {
let app_config = {
let guard = lock_r!(FDOLL);
guard.app_config.clone()
@@ -40,10 +52,12 @@ pub async fn init_ws_client() {
clients.ws_client = Some(ws_client);
clients.is_ws_initialized = false; // wait for initialized event
}
true
}
Err(e) => {
error!("Failed to initialize WebSocket client: {}", e);
clear_ws_client().await;
false
}
}
}
@@ -53,6 +67,7 @@ pub async fn clear_ws_client() {
if let Some(clients) = guard.network.clients.as_mut() {
clients.ws_client = None;
clients.is_ws_initialized = false;
clients.ws_emit_failures = 0;
}
}

View File

@@ -2,10 +2,7 @@ use rust_socketio::{Payload, RawClient};
use tracing::info;
use crate::{
lock_w,
services::health_manager::close_health_manager_window,
services::scene::open_scene_window,
state::{init_app_data_scoped, AppDataRefreshScope, FDOLL},
init::lifecycle::construct_user_session, lock_w, services::health_manager::close_health_manager_window, state::FDOLL
};
use super::{types::WS_EVENT, utils};
@@ -26,33 +23,30 @@ pub fn on_connected(_payload: Payload, socket: RawClient) {
/// Handler for initialized event
pub fn on_initialized(payload: Payload, _socket: RawClient) {
if utils::extract_text_value(payload, "initialized").is_ok() {
let needs_data_refresh = check_and_mark_initialized();
restore_connection_ui();
let is_reconnection = mark_ws_initialized();
if needs_data_refresh {
info!("Reconnection detected: refreshing app data");
if is_reconnection {
info!("Reconnection detected: reconstructing user session");
tauri::async_runtime::spawn(async {
init_app_data_scoped(AppDataRefreshScope::All).await;
construct_user_session().await;
});
} else {
// First-time initialization, just close health manager if open
close_health_manager_window();
}
}
}
/// Mark WebSocket as initialized and check if app data needs refreshing
/// Mark WebSocket as initialized and check if this is a reconnection.
///
/// Returns true if user data is missing (indicating a reconnection
/// after session teardown where app data was cleared).
fn check_and_mark_initialized() -> bool {
fn mark_ws_initialized() -> bool {
let mut guard = lock_w!(FDOLL);
if let Some(clients) = guard.network.clients.as_mut() {
clients.is_ws_initialized = true;
clients.ws_emit_failures = 0;
}
// If user data is gone, we need to re-fetch everything
// If user data is gone, we need full session reconstruction
guard.user_data.user.is_none()
}
/// Restore UI after successful connection
fn restore_connection_ui() {
close_health_manager_window();
open_scene_window();
}

View File

@@ -3,7 +3,9 @@ use serde::Serialize;
use tauri::{async_runtime, Emitter};
use tracing::{error, warn};
use crate::{get_app_handle, init::lifecycle::handle_disasterous_failure, lock_r, state::FDOLL};
use crate::{
get_app_handle, init::lifecycle::handle_disasterous_failure, lock_r, lock_w, state::FDOLL,
};
/// Acquire WebSocket client and initialization state from app state
fn get_ws_state() -> (Option<rust_socketio::client::Client>, bool) {
@@ -47,6 +49,27 @@ async fn do_emit<T: Serialize + Send + 'static>(
}
}
async fn handle_soft_emit_failure(err_msg: &str) {
const MAX_FAILURES: u8 = 10;
let should_reinit = {
let mut guard = lock_w!(FDOLL);
if let Some(clients) = guard.network.clients.as_mut() {
clients.ws_emit_failures = clients.ws_emit_failures.saturating_add(1);
clients.ws_emit_failures >= MAX_FAILURES
} else {
false
}
};
if should_reinit {
warn!("WebSocket emit failed {} times, reinitializing connection", MAX_FAILURES);
let _ = crate::services::ws::client::clear_ws_client().await;
crate::services::ws::client::establish_websocket_connection().await;
} else {
warn!("[non-critical] {}", err_msg);
}
}
/// Emit critical data to WebSocket server
///
/// On failure, triggers disaster recovery (session teardown + health manager).
@@ -78,7 +101,7 @@ pub async fn ws_emit_soft<T: Serialize + Send + 'static>(
match do_emit(event, payload).await {
Ok(_) => Ok(()),
Err(err_msg) => {
warn!("[non-critical] {}", err_msg);
handle_soft_emit_failure(&err_msg).await;
Err(err_msg)
}
}

View File

@@ -1,19 +1,22 @@
#[derive(Clone)]
pub struct Clients {
pub http_client: reqwest::Client,
pub ws_client: Option<rust_socketio::client::Client>,
pub is_ws_initialized: bool,
pub ws_emit_failures: u8,
}
pub struct NetworkState {
pub clients: Option<Clients>,
pub health_monitor_token: Option<tokio_util::sync::CancellationToken>,
}
impl Default for NetworkState {
fn default() -> Self {
Self { clients: None }
Self {
clients: None,
health_monitor_token: None,
}
}
}
@@ -30,6 +33,8 @@ pub fn init_network_state() -> NetworkState {
http_client,
ws_client: None,
is_ws_initialized: false,
ws_emit_failures: 0,
}),
health_monitor_token: None,
}
}