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,
models::app_data::UserData,
services::{
friend_active_doll_sprite, presence_modules::models::ModuleMetadata, sprite,
friends, presence_modules::models::ModuleMetadata, sprite,
},
state::{init_app_data_scoped, AppDataRefreshScope, FDOLL},
};
@@ -38,7 +38,7 @@ pub fn get_active_doll_sprite_base64() -> Result<Option<String>, String> {
#[tauri::command]
#[specta::specta]
pub fn get_friend_active_doll_sprites_base64(
) -> Result<friend_active_doll_sprite::FriendActiveDollSpritesDto, String> {
friend_active_doll_sprite::sync_from_app_data();
Ok(friend_active_doll_sprite::get_snapshot())
) -> Result<friends::FriendActiveDollSpritesDto, String> {
friends::sync_active_doll_sprites_from_app_data();
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
/// 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;
open_health_manager_window(error_message);
}

View File

@@ -1,6 +1,6 @@
use crate::{
init::{
lifecycle::{construct_user_session, handle_disasterous_failure, validate_server_health},
lifecycle::{construct_user_session, handle_disastrous_failure, validate_server_health},
tracing::init_logging,
},
services::{
@@ -28,7 +28,7 @@ pub async fn launch_app() {
init_modules();
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;
}

View File

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

View File

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

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::{
init::lifecycle::{handle_disasterous_failure, validate_server_health},
init::lifecycle::{handle_disastrous_failure, validate_server_health},
lock_w,
services::ws::client::establish_websocket_connection,
state::FDOLL,
@@ -44,7 +44,7 @@ pub async fn start_health_monitor() {
if consecutive_failures >= 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: {}",
e
)))

View File

@@ -1,59 +1,12 @@
use serde_json::json;
use tracing::error;
use tracing::warn;
use crate::{
lock_r, models::interaction::SendInteractionDto, services::ws::WS_EVENT, state::FDOLL,
};
use crate::{models::interaction::SendInteractionDto, services::ws::{ws_emit_soft, WS_EVENT}};
pub async fn send_interaction(dto: SendInteractionDto) -> Result<(), String> {
// Check if WS is initialized
let client = {
let guard = lock_r!(FDOLL);
if let Some(clients) = &guard.network.clients {
if clients.is_ws_initialized {
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)
ws_emit_soft(WS_EVENT::CLIENT_SEND_INTERACTION, dto)
.await
.map_err(|err| {
warn!("Failed to send interaction: {}", err);
err
})
.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 crate::get_app_handle;
use tracing::warn;
pub mod app_events;
pub mod app_menu;
@@ -8,8 +9,7 @@ pub mod auth;
pub mod client_config_manager;
pub mod cursor;
pub mod doll_editor;
pub mod friend_active_doll_sprite;
pub mod friend_cursor;
pub mod friends;
pub mod health_manager;
pub mod health_monitor;
pub mod interaction;
@@ -25,6 +25,8 @@ pub fn close_all_windows() {
let app_handle = get_app_handle();
let webview_windows = app_handle.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::services::ws::user_status::report_user_status;
use crate::services::ws::{ws_emit_soft, WS_EVENT};
use super::models::PresenceStatus;
@@ -48,7 +47,9 @@ async fn update_status(status: PresenceStatus) {
presence_status: status,
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) {
@@ -56,7 +57,7 @@ async fn update_status_async(status: PresenceStatus) {
presence_status: status,
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);
}
}

View File

@@ -35,6 +35,7 @@ fn scene_interactive_state() -> Arc<AtomicBool> {
pub fn update_scene_interactive(interactive: bool, should_click: bool) {
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
// 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
} else {
// 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())
).await;
return;
@@ -34,7 +34,7 @@ pub async fn establish_websocket_connection() {
}
// 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())
).await;
}

View File

@@ -5,7 +5,7 @@ use tauri_specta::Event;
use tracing::{error, warn};
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
@@ -87,7 +87,7 @@ pub async fn ws_emit<T: Serialize + Send + 'static>(
Ok(_) => Ok(()),
Err(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)
}
}

View File

@@ -12,7 +12,7 @@ use crate::services::app_events::{
};
use crate::services::{
cursor::{normalized_to_absolute, CursorPositions},
friend_active_doll_sprite, friend_cursor,
friends,
};
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 raw_pos = normalized_to_absolute(mapped_pos);
friend_cursor::update_position(
friends::update_cursor_position(
friend_data.user_id,
CursorPositions {
raw: raw_pos,
@@ -77,8 +77,7 @@ pub fn on_friend_disconnected(payload: Payload, _socket: RawClient) {
if let Ok(data) =
utils::extract_and_parse::<FriendDisconnectedPayload>(payload, "friend-disconnected")
{
friend_active_doll_sprite::remove_friend(&data.user_id);
friend_cursor::remove_friend(&data.user_id);
friends::remove_friend(&data.user_id);
emitter::emit_to_frontend_typed(&FriendDisconnected(data));
}
}
@@ -112,8 +111,7 @@ pub fn on_friend_active_doll_changed(payload: Payload, _socket: RawClient) {
payload,
"friend-active-doll-changed",
) {
friend_active_doll_sprite::set_active_doll(&data.friend_id, data.doll.as_ref());
friend_cursor::set_active_doll(&data.friend_id, data.doll.is_some());
friends::set_active_doll(&data.friend_id, data.doll.as_ref());
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));
/// 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;
// Cancel previous pending report
@@ -25,7 +25,7 @@ pub async fn report_user_status(status: UserStatusPayload) {
}
if !status.has_presence_content() {
return;
return Ok(());
}
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);
Ok(())
}

View File

@@ -3,7 +3,7 @@ use crate::{
remotes::{dolls::DollsRemote, friends::FriendRemote, user::UserRemote},
services::{
app_events::{ActiveDollSpriteChanged, AppDataRefreshed},
friend_active_doll_sprite, friend_cursor, sprite,
friends, sprite,
},
state::FDOLL,
};
@@ -165,8 +165,7 @@ pub async fn init_app_data_scoped(scope: AppDataRefreshScope) {
let mut guard = lock_w!(crate::state::FDOLL);
guard.user_data.friends = Some(friends);
drop(guard);
friend_active_doll_sprite::sync_from_app_data();
friend_cursor::sync_from_app_data();
friends::sync_from_app_data();
}
Err(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.friends = None;
drop(guard);
friend_active_doll_sprite::clear();
friend_cursor::clear();
friends::clear();
}