Rust service refactor: friends & some other

This commit is contained in:
2026-03-10 13:27:12 +08:00
parent e38697faa9
commit 3437bc5746
17 changed files with 84 additions and 93 deletions

View File

@@ -2,7 +2,7 @@ use crate::{
lock_r, lock_r,
models::app_data::UserData, models::app_data::UserData,
services::{ services::{
friend_active_doll_sprite, presence_modules::models::ModuleMetadata, sprite, friends, presence_modules::models::ModuleMetadata, sprite,
}, },
state::{init_app_data_scoped, AppDataRefreshScope, FDOLL}, state::{init_app_data_scoped, AppDataRefreshScope, FDOLL},
}; };
@@ -38,7 +38,7 @@ pub fn get_active_doll_sprite_base64() -> Result<Option<String>, String> {
#[tauri::command] #[tauri::command]
#[specta::specta] #[specta::specta]
pub fn get_friend_active_doll_sprites_base64( pub fn get_friend_active_doll_sprites_base64(
) -> Result<friend_active_doll_sprite::FriendActiveDollSpritesDto, String> { ) -> Result<friends::FriendActiveDollSpritesDto, String> {
friend_active_doll_sprite::sync_from_app_data(); friends::sync_active_doll_sprites_from_app_data();
Ok(friend_active_doll_sprite::get_snapshot()) Ok(friends::get_active_doll_sprites_snapshot())
} }

View File

@@ -53,7 +53,7 @@ async fn disconnect_user_profile() {
/// Destructs the user session and show health manager window /// Destructs the user session and show health manager window
/// with error message, offering troubleshooting options. /// with error message, offering troubleshooting options.
pub async fn handle_disasterous_failure(error_message: Option<String>) { pub async fn handle_disastrous_failure(error_message: Option<String>) {
destruct_user_session().await; destruct_user_session().await;
open_health_manager_window(error_message); open_health_manager_window(error_message);
} }

View File

@@ -1,6 +1,6 @@
use crate::{ use crate::{
init::{ init::{
lifecycle::{construct_user_session, handle_disasterous_failure, validate_server_health}, lifecycle::{construct_user_session, handle_disastrous_failure, validate_server_health},
tracing::init_logging, tracing::init_logging,
}, },
services::{ services::{
@@ -28,7 +28,7 @@ pub async fn launch_app() {
init_modules(); init_modules();
if let Err(err) = validate_server_health().await { if let Err(err) = validate_server_health().await {
handle_disasterous_failure(Some(err.to_string())).await; handle_disastrous_failure(Some(err.to_string())).await;
return; return;
} }

View File

@@ -13,8 +13,8 @@ use crate::{
interaction::{InteractionDeliveryFailedDto, InteractionPayloadDto}, interaction::{InteractionDeliveryFailedDto, InteractionPayloadDto},
}, },
services::{ services::{
cursor::CursorPositions, friend_active_doll_sprite::FriendActiveDollSpritesDto, cursor::CursorPositions,
friend_cursor::FriendCursorPositionsDto, friends::{FriendActiveDollSpritesDto, FriendCursorPositionsDto},
}, },
}; };

View File

@@ -103,10 +103,8 @@ pub fn set_active_doll(user_id: &str, has_active_doll: bool) {
.active_dolls .active_dolls
.insert(user_id.to_string(), has_active_doll); .insert(user_id.to_string(), has_active_doll);
if !has_active_doll { if !has_active_doll && projection.positions.remove(user_id).is_some() {
if projection.positions.remove(user_id).is_some() { emit_snapshot(&projection.positions);
emit_snapshot(&projection.positions);
}
} }
} }

View File

@@ -0,0 +1,39 @@
mod active_doll_sprites;
mod cursor_positions;
use crate::{models::dolls::DollDto, services::cursor::CursorPositions};
pub use active_doll_sprites::FriendActiveDollSpritesDto;
pub use cursor_positions::FriendCursorPositionsDto;
pub fn sync_from_app_data() {
active_doll_sprites::sync_from_app_data();
cursor_positions::sync_from_app_data();
}
pub fn clear() {
active_doll_sprites::clear();
cursor_positions::clear();
}
pub fn remove_friend(user_id: &str) {
active_doll_sprites::remove_friend(user_id);
cursor_positions::remove_friend(user_id);
}
pub fn set_active_doll(user_id: &str, doll: Option<&DollDto>) {
active_doll_sprites::set_active_doll(user_id, doll);
cursor_positions::set_active_doll(user_id, doll.is_some());
}
pub fn update_cursor_position(user_id: String, position: CursorPositions) {
cursor_positions::update_position(user_id, position);
}
pub fn sync_active_doll_sprites_from_app_data() {
active_doll_sprites::sync_from_app_data();
}
pub fn get_active_doll_sprites_snapshot() -> FriendActiveDollSpritesDto {
active_doll_sprites::get_snapshot()
}

View File

@@ -1,5 +1,5 @@
use crate::{ use crate::{
init::lifecycle::{handle_disasterous_failure, validate_server_health}, init::lifecycle::{handle_disastrous_failure, validate_server_health},
lock_w, lock_w,
services::ws::client::establish_websocket_connection, services::ws::client::establish_websocket_connection,
state::FDOLL, state::FDOLL,
@@ -44,7 +44,7 @@ pub async fn start_health_monitor() {
if consecutive_failures >= MAX_FAILURES { if consecutive_failures >= MAX_FAILURES {
info!("Server appears unreachable after {} attempts, triggering recovery", MAX_FAILURES); info!("Server appears unreachable after {} attempts, triggering recovery", MAX_FAILURES);
handle_disasterous_failure(Some(format!( handle_disastrous_failure(Some(format!(
"Lost connection to server: {}", "Lost connection to server: {}",
e e
))) )))

View File

@@ -1,59 +1,12 @@
use serde_json::json; use tracing::warn;
use tracing::error;
use crate::{ use crate::{models::interaction::SendInteractionDto, services::ws::{ws_emit_soft, WS_EVENT}};
lock_r, models::interaction::SendInteractionDto, services::ws::WS_EVENT, state::FDOLL,
};
pub async fn send_interaction(dto: SendInteractionDto) -> Result<(), String> { pub async fn send_interaction(dto: SendInteractionDto) -> Result<(), String> {
// Check if WS is initialized ws_emit_soft(WS_EVENT::CLIENT_SEND_INTERACTION, dto)
let client = { .await
let guard = lock_r!(FDOLL); .map_err(|err| {
if let Some(clients) = &guard.network.clients { warn!("Failed to send interaction: {}", err);
if clients.is_ws_initialized { err
clients.ws_client.clone()
} else {
return Err("WebSocket not initialized".to_string());
}
} else {
return Err("App not fully initialized".to_string());
}
};
if let Some(socket) = client {
// Prepare payload for client-send-interaction event
// The DTO structure matches what the server expects:
// { recipientUserId, content, type } (handled by serde rename_all="camelCase")
let payload = json!({
"recipientUserId": dto.recipient_user_id,
"content": dto.content,
"type": dto.type_
});
// Blocking emission because rust_socketio::Client::emit is synchronous/blocking
// but we are in an async context. Ideally we spawn_blocking.
let spawn_result = tauri::async_runtime::spawn_blocking(move || {
socket.emit(WS_EVENT::CLIENT_SEND_INTERACTION, payload)
}) })
.await;
match spawn_result {
Ok(emit_result) => match emit_result {
Ok(_) => Ok(()),
Err(e) => {
let err_msg = format!("Failed to emit interaction event: {}", e);
error!("{}", err_msg);
Err(err_msg)
}
},
Err(e) => {
let err_msg = format!("Failed to spawn blocking task for interaction emit: {}", e);
error!("{}", err_msg);
Err(err_msg)
}
}
} else {
Err("WebSocket client not available".to_string())
}
} }

View File

@@ -1,6 +1,7 @@
use tauri::Manager; use tauri::Manager;
use crate::get_app_handle; use crate::get_app_handle;
use tracing::warn;
pub mod app_events; pub mod app_events;
pub mod app_menu; pub mod app_menu;
@@ -8,8 +9,7 @@ pub mod auth;
pub mod client_config_manager; pub mod client_config_manager;
pub mod cursor; pub mod cursor;
pub mod doll_editor; pub mod doll_editor;
pub mod friend_active_doll_sprite; pub mod friends;
pub mod friend_cursor;
pub mod health_manager; pub mod health_manager;
pub mod health_monitor; pub mod health_monitor;
pub mod interaction; pub mod interaction;
@@ -25,6 +25,8 @@ pub fn close_all_windows() {
let app_handle = get_app_handle(); let app_handle = get_app_handle();
let webview_windows = app_handle.webview_windows(); let webview_windows = app_handle.webview_windows();
for window in webview_windows { for window in webview_windows {
window.1.close().unwrap(); if let Err(err) = window.1.close() {
warn!("Failed to close window '{}': {}", window.0, err);
}
} }
} }

View File

@@ -5,7 +5,6 @@ use tracing::{error, info, warn};
use crate::models::event_payloads::{UserStatusPayload, UserStatusState}; use crate::models::event_payloads::{UserStatusPayload, UserStatusState};
use crate::services::ws::user_status::report_user_status; use crate::services::ws::user_status::report_user_status;
use crate::services::ws::{ws_emit_soft, WS_EVENT};
use super::models::PresenceStatus; use super::models::PresenceStatus;
@@ -48,7 +47,9 @@ async fn update_status(status: PresenceStatus) {
presence_status: status, presence_status: status,
state: UserStatusState::Idle, state: UserStatusState::Idle,
}; };
report_user_status(user_status).await; if let Err(e) = report_user_status(user_status).await {
warn!("User status report failed: {}", e);
}
} }
async fn update_status_async(status: PresenceStatus) { async fn update_status_async(status: PresenceStatus) {
@@ -56,7 +57,7 @@ async fn update_status_async(status: PresenceStatus) {
presence_status: status, presence_status: status,
state: UserStatusState::Idle, state: UserStatusState::Idle,
}; };
if let Err(e) = ws_emit_soft(WS_EVENT::CLIENT_REPORT_USER_STATUS, payload).await { if let Err(e) = report_user_status(payload).await {
warn!("User status report failed: {}", e); warn!("User status report failed: {}", e);
} }
} }

View File

@@ -35,6 +35,7 @@ fn scene_interactive_state() -> Arc<AtomicBool> {
pub fn update_scene_interactive(interactive: bool, should_click: bool) { pub fn update_scene_interactive(interactive: bool, should_click: bool) {
let app_handle = get_app_handle(); let app_handle = get_app_handle();
scene_interactive_state().store(interactive, Ordering::SeqCst);
// If we are forcing interactive to false (e.g. background click), clear any open menus // If we are forcing interactive to false (e.g. background click), clear any open menus
// This prevents the loop from immediately re-enabling it if the frontend hasn't updated yet // This prevents the loop from immediately re-enabling it if the frontend hasn't updated yet

View File

@@ -23,7 +23,7 @@ pub async fn establish_websocket_connection() {
return; // Success return; // Success
} else { } else {
// Connection failed, trigger disaster recovery // Connection failed, trigger disaster recovery
crate::init::lifecycle::handle_disasterous_failure( crate::init::lifecycle::handle_disastrous_failure(
Some("WebSocket connection failed. Please check your network and try again.".to_string()) Some("WebSocket connection failed. Please check your network and try again.".to_string())
).await; ).await;
return; return;
@@ -32,9 +32,9 @@ pub async fn establish_websocket_connection() {
sleep(BACKOFF).await; sleep(BACKOFF).await;
} }
// If we exhausted retries without valid token // If we exhausted retries without valid token
crate::init::lifecycle::handle_disasterous_failure( crate::init::lifecycle::handle_disastrous_failure(
Some("Failed to authenticate. Please restart and sign in again.".to_string()) Some("Failed to authenticate. Please restart and sign in again.".to_string())
).await; ).await;
} }

View File

@@ -5,7 +5,7 @@ use tauri_specta::Event;
use tracing::{error, warn}; use tracing::{error, warn};
use crate::{ use crate::{
get_app_handle, init::lifecycle::handle_disasterous_failure, lock_r, lock_w, state::FDOLL, get_app_handle, init::lifecycle::handle_disastrous_failure, lock_r, lock_w, state::FDOLL,
}; };
/// Acquire WebSocket client and initialization state from app state /// Acquire WebSocket client and initialization state from app state
@@ -87,7 +87,7 @@ pub async fn ws_emit<T: Serialize + Send + 'static>(
Ok(_) => Ok(()), Ok(_) => Ok(()),
Err(err_msg) => { Err(err_msg) => {
error!("[critical] {}", err_msg); error!("[critical] {}", err_msg);
handle_disasterous_failure(Some(err_msg.clone())).await; handle_disastrous_failure(Some(err_msg.clone())).await;
Err(err_msg) Err(err_msg)
} }
} }

View File

@@ -12,7 +12,7 @@ use crate::services::app_events::{
}; };
use crate::services::{ use crate::services::{
cursor::{normalized_to_absolute, CursorPositions}, cursor::{normalized_to_absolute, CursorPositions},
friend_active_doll_sprite, friend_cursor, friends,
}; };
use crate::state::AppDataRefreshScope; use crate::state::AppDataRefreshScope;
@@ -62,7 +62,7 @@ pub fn on_friend_cursor_position(payload: Payload, _socket: RawClient) {
let mapped_pos = &friend_data.position; let mapped_pos = &friend_data.position;
let raw_pos = normalized_to_absolute(mapped_pos); let raw_pos = normalized_to_absolute(mapped_pos);
friend_cursor::update_position( friends::update_cursor_position(
friend_data.user_id, friend_data.user_id,
CursorPositions { CursorPositions {
raw: raw_pos, raw: raw_pos,
@@ -77,8 +77,7 @@ pub fn on_friend_disconnected(payload: Payload, _socket: RawClient) {
if let Ok(data) = if let Ok(data) =
utils::extract_and_parse::<FriendDisconnectedPayload>(payload, "friend-disconnected") utils::extract_and_parse::<FriendDisconnectedPayload>(payload, "friend-disconnected")
{ {
friend_active_doll_sprite::remove_friend(&data.user_id); friends::remove_friend(&data.user_id);
friend_cursor::remove_friend(&data.user_id);
emitter::emit_to_frontend_typed(&FriendDisconnected(data)); emitter::emit_to_frontend_typed(&FriendDisconnected(data));
} }
} }
@@ -112,8 +111,7 @@ pub fn on_friend_active_doll_changed(payload: Payload, _socket: RawClient) {
payload, payload,
"friend-active-doll-changed", "friend-active-doll-changed",
) { ) {
friend_active_doll_sprite::set_active_doll(&data.friend_id, data.doll.as_ref()); friends::set_active_doll(&data.friend_id, data.doll.as_ref());
friend_cursor::set_active_doll(&data.friend_id, data.doll.is_some());
emitter::emit_to_frontend_typed(&FriendActiveDollChanged(data)); emitter::emit_to_frontend_typed(&FriendActiveDollChanged(data));
} }
} }

View File

@@ -16,7 +16,7 @@ static USER_STATUS_REPORT_DEBOUNCE: Lazy<Mutex<Option<JoinHandle<()>>>> =
Lazy::new(|| Mutex::new(None)); Lazy::new(|| Mutex::new(None));
/// Report user status to WebSocket server with debouncing /// Report user status to WebSocket server with debouncing
pub async fn report_user_status(status: UserStatusPayload) { pub async fn report_user_status(status: UserStatusPayload) -> Result<(), String> {
let mut debouncer = USER_STATUS_REPORT_DEBOUNCE.lock().await; let mut debouncer = USER_STATUS_REPORT_DEBOUNCE.lock().await;
// Cancel previous pending report // Cancel previous pending report
@@ -25,7 +25,7 @@ pub async fn report_user_status(status: UserStatusPayload) {
} }
if !status.has_presence_content() { if !status.has_presence_content() {
return; return Ok(());
} }
if let Err(e) = UserStatusChanged(status.clone()).emit(crate::get_app_handle()) { if let Err(e) = UserStatusChanged(status.clone()).emit(crate::get_app_handle()) {
@@ -43,4 +43,5 @@ pub async fn report_user_status(status: UserStatusPayload) {
}); });
*debouncer = Some(handle); *debouncer = Some(handle);
Ok(())
} }

View File

@@ -3,7 +3,7 @@ use crate::{
remotes::{dolls::DollsRemote, friends::FriendRemote, user::UserRemote}, remotes::{dolls::DollsRemote, friends::FriendRemote, user::UserRemote},
services::{ services::{
app_events::{ActiveDollSpriteChanged, AppDataRefreshed}, app_events::{ActiveDollSpriteChanged, AppDataRefreshed},
friend_active_doll_sprite, friend_cursor, sprite, friends, sprite,
}, },
state::FDOLL, state::FDOLL,
}; };
@@ -165,8 +165,7 @@ pub async fn init_app_data_scoped(scope: AppDataRefreshScope) {
let mut guard = lock_w!(crate::state::FDOLL); let mut guard = lock_w!(crate::state::FDOLL);
guard.user_data.friends = Some(friends); guard.user_data.friends = Some(friends);
drop(guard); drop(guard);
friend_active_doll_sprite::sync_from_app_data(); friends::sync_from_app_data();
friend_cursor::sync_from_app_data();
} }
Err(e) => { Err(e) => {
warn!("Failed to fetch friends list: {}", e); warn!("Failed to fetch friends list: {}", e);
@@ -287,6 +286,5 @@ pub fn clear_app_data() {
guard.user_data.user = None; guard.user_data.user = None;
guard.user_data.friends = None; guard.user_data.friends = None;
drop(guard); drop(guard);
friend_active_doll_sprite::clear(); friends::clear();
friend_cursor::clear();
} }