From 42f798c8b70cd80a8fb4e8078bebe553487bf2c9 Mon Sep 17 00:00:00 2001 From: Wind-Explorer Date: Wed, 24 Dec 2025 16:25:48 +0800 Subject: [PATCH] trivial enhancements to state management solution --- src-tauri/src/lib.rs | 97 +++++--- src-tauri/src/models/app_data.rs | 3 +- src-tauri/src/remotes/dolls.rs | 72 ++++-- src-tauri/src/remotes/mod.rs | 2 +- src-tauri/src/services/auth.rs | 9 + src-tauri/src/services/cursor.rs | 7 +- src-tauri/src/services/doll_editor.rs | 2 +- src-tauri/src/services/ws.rs | 84 ++++++- src-tauri/src/state.rs | 220 ++++++++++++++---- .../app-menu/tabs/your-dolls/index.svelte | 79 ++----- src/types/bindings/AppData.ts | 3 +- 11 files changed, 400 insertions(+), 178 deletions(-) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 3dc96af..0740fe9 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -6,12 +6,11 @@ use crate::{ UserBasicDto, }, remotes::user::UserRemote, - services::cursor::start_cursor_tracking, - services::doll_editor::open_doll_editor_window, - state::{init_app_data, FDOLL}, + services::{cursor::start_cursor_tracking, doll_editor::open_doll_editor_window}, + state::{init_app_data, init_app_data_scoped, AppDataRefreshScope, FDOLL}, }; use tauri::async_runtime; -use tauri::{Emitter, Manager}; +use tauri::Manager; use tracing_subscriber::{self, util::SubscriberInitExt}; static APP_HANDLE: std::sync::OnceLock> = std::sync::OnceLock::new(); @@ -204,12 +203,11 @@ async fn create_doll(dto: CreateDollDto) -> Result { .await .map_err(|e| e.to_string())?; - // Emit event locally so the app knows about the update immediately - // The websocket event will also come in, but this makes the UI snappy - if let Err(e) = get_app_handle().emit("doll_created", &result) { - tracing::error!("Failed to emit local doll.created event: {}", e); - } - + // Refresh dolls list in background (deduped inside init_app_data_scoped) + async_runtime::spawn(async { + init_app_data_scoped(AppDataRefreshScope::Dolls).await; + }); + Ok(result) } @@ -220,10 +218,26 @@ async fn update_doll(id: String, dto: UpdateDollDto) -> Result .await .map_err(|e| e.to_string())?; - // Emit event locally - if let Err(e) = get_app_handle().emit("doll_updated", &result) { - tracing::error!("Failed to emit local doll.updated event: {}", e); - } + // Check if this was the active doll (after update completes to avoid stale reads) + let is_active_doll = { + let guard = lock_r!(FDOLL); + guard + .app_data + .user + .as_ref() + .and_then(|u| u.active_doll_id.as_ref()) + .map(|active_id| active_id == &id) + .unwrap_or(false) + }; + + // Refresh dolls list + User/Friends if this was the active doll + async_runtime::spawn(async move { + init_app_data_scoped(AppDataRefreshScope::Dolls).await; + if is_active_doll { + init_app_data_scoped(AppDataRefreshScope::User).await; + init_app_data_scoped(AppDataRefreshScope::Friends).await; + } + }); Ok(result) } @@ -235,20 +249,26 @@ async fn delete_doll(id: String) -> Result<(), String> { .await .map_err(|e| e.to_string())?; - // Emit event locally - // We need to send something that matches the structure expected by the frontend - // The WS event sends the full object, but for deletion usually ID is enough or the object itself - // Let's send a dummy object with just the ID if we can, or just the ID if the frontend handles it. - // Looking at WS code: on_doll_deleted emits the payload it gets. - // Ideally we'd return the deleted doll from the backend but delete usually returns empty. - // Let's just emit the ID wrapped in an object or similar if needed. - // Actually, let's check what the frontend expects. - // src/routes/app-menu/tabs/your-dolls/index.svelte just listens and calls refreshDolls(), - // it doesn't use the payload. So any payload is fine. - - if let Err(e) = get_app_handle().emit("doll_deleted", ()) { - tracing::error!("Failed to emit local doll.deleted event: {}", e); - } + // Check if this was the active doll (after delete completes to avoid stale reads) + let is_active_doll = { + let guard = lock_r!(FDOLL); + guard + .app_data + .user + .as_ref() + .and_then(|u| u.active_doll_id.as_ref()) + .map(|active_id| active_id == &id) + .unwrap_or(false) + }; + + // Refresh dolls list + User/Friends if the deleted doll was active + async_runtime::spawn(async move { + init_app_data_scoped(AppDataRefreshScope::Dolls).await; + if is_active_doll { + init_app_data_scoped(AppDataRefreshScope::User).await; + init_app_data_scoped(AppDataRefreshScope::Friends).await; + } + }); Ok(()) } @@ -258,7 +278,16 @@ async fn set_active_doll(doll_id: String) -> Result<(), String> { UserRemote::new() .set_active_doll(&doll_id) .await - .map_err(|e| e.to_string()) + .map_err(|e| e.to_string())?; + + // Refresh User (for active_doll_id) + Friends (so friends see your active doll) + // We don't need to refresh Dolls since the doll itself hasn't changed + async_runtime::spawn(async { + init_app_data_scoped(AppDataRefreshScope::User).await; + init_app_data_scoped(AppDataRefreshScope::Friends).await; + }); + + Ok(()) } #[tauri::command] @@ -266,7 +295,15 @@ async fn remove_active_doll() -> Result<(), String> { UserRemote::new() .remove_active_doll() .await - .map_err(|e| e.to_string()) + .map_err(|e| e.to_string())?; + + // Refresh User (for active_doll_id) + Friends (so friends see your doll is gone) + async_runtime::spawn(async { + init_app_data_scoped(AppDataRefreshScope::User).await; + init_app_data_scoped(AppDataRefreshScope::Friends).await; + }); + + Ok(()) } #[tauri::command] diff --git a/src-tauri/src/models/app_data.rs b/src-tauri/src/models/app_data.rs index ff749b5..671a892 100644 --- a/src-tauri/src/models/app_data.rs +++ b/src-tauri/src/models/app_data.rs @@ -1,7 +1,7 @@ use serde::{Deserialize, Serialize}; use ts_rs::TS; -use crate::remotes::{friends::FriendshipResponseDto, user::UserProfile}; +use crate::remotes::{dolls::DollDto, friends::FriendshipResponseDto, user::UserProfile}; #[derive(Serialize, Deserialize, Clone, Debug, TS)] #[ts(export)] @@ -42,5 +42,6 @@ impl Default for SceneData { pub struct AppData { pub user: Option, pub friends: Option>, + pub dolls: Option>, pub scene: SceneData, } diff --git a/src-tauri/src/remotes/dolls.rs b/src-tauri/src/remotes/dolls.rs index 23bf501..f34d196 100644 --- a/src-tauri/src/remotes/dolls.rs +++ b/src-tauri/src/remotes/dolls.rs @@ -83,32 +83,44 @@ impl DollsRemote { pub async fn get_dolls(&self) -> Result, RemoteError> { let url = format!("{}/dolls/me", self.base_url); - tracing::info!("DollsRemote::get_dolls - Sending GET request to URL: {}", url); + tracing::info!( + "DollsRemote::get_dolls - Sending GET request to URL: {}", + url + ); let resp = with_auth(self.client.get(url)).await.send().await?; - + let resp = resp.error_for_status().map_err(|e| { tracing::error!("DollsRemote::get_dolls - HTTP error: {}", e); e })?; let text = resp.text().await.map_err(|e| { - tracing::error!("DollsRemote::get_dolls - Failed to read response text: {}", e); + tracing::error!( + "DollsRemote::get_dolls - Failed to read response text: {}", + e + ); e })?; - + let dolls: Vec = serde_json::from_str(&text).map_err(|e| { tracing::error!("DollsRemote::get_dolls - Failed to parse JSON: {}", e); e })?; - - tracing::info!("DollsRemote::get_dolls - Successfully parsed {} dolls", dolls.len()); + + tracing::info!( + "DollsRemote::get_dolls - Successfully parsed {} dolls", + dolls.len() + ); Ok(dolls) } pub async fn get_doll(&self, id: &str) -> Result { let url = format!("{}/dolls/{}", self.base_url, id); - tracing::info!("DollsRemote::get_doll - Sending GET request to URL: {}", url); - + tracing::info!( + "DollsRemote::get_doll - Sending GET request to URL: {}", + url + ); + let resp = with_auth(self.client.get(url)).await.send().await?; let resp = resp.error_for_status().map_err(|e| { @@ -117,7 +129,10 @@ impl DollsRemote { })?; let text = resp.text().await.map_err(|e| { - tracing::error!("DollsRemote::get_doll - Failed to read response text: {}", e); + tracing::error!( + "DollsRemote::get_doll - Failed to read response text: {}", + e + ); e })?; @@ -125,14 +140,17 @@ impl DollsRemote { tracing::error!("DollsRemote::get_doll - Failed to parse JSON: {}", e); e })?; - + Ok(doll) } pub async fn create_doll(&self, dto: CreateDollDto) -> Result { let url = format!("{}/dolls", self.base_url); - tracing::info!("DollsRemote::create_doll - Sending POST request to URL: {}", url); - + tracing::info!( + "DollsRemote::create_doll - Sending POST request to URL: {}", + url + ); + let resp = with_auth(self.client.post(url)) .await .json(&dto) @@ -145,7 +163,10 @@ impl DollsRemote { })?; let text = resp.text().await.map_err(|e| { - tracing::error!("DollsRemote::create_doll - Failed to read response text: {}", e); + tracing::error!( + "DollsRemote::create_doll - Failed to read response text: {}", + e + ); e })?; @@ -153,14 +174,17 @@ impl DollsRemote { tracing::error!("DollsRemote::create_doll - Failed to parse JSON: {}", e); e })?; - + Ok(doll) } pub async fn update_doll(&self, id: &str, dto: UpdateDollDto) -> Result { let url = format!("{}/dolls/{}", self.base_url, id); - tracing::info!("DollsRemote::update_doll - Sending PATCH request to URL: {}", url); - + tracing::info!( + "DollsRemote::update_doll - Sending PATCH request to URL: {}", + url + ); + let resp = with_auth(self.client.patch(url)) .await .json(&dto) @@ -173,7 +197,10 @@ impl DollsRemote { })?; let text = resp.text().await.map_err(|e| { - tracing::error!("DollsRemote::update_doll - Failed to read response text: {}", e); + tracing::error!( + "DollsRemote::update_doll - Failed to read response text: {}", + e + ); e })?; @@ -181,16 +208,19 @@ impl DollsRemote { tracing::error!("DollsRemote::update_doll - Failed to parse JSON: {}", e); e })?; - + Ok(doll) } pub async fn delete_doll(&self, id: &str) -> Result<(), RemoteError> { let url = format!("{}/dolls/{}", self.base_url, id); - tracing::info!("DollsRemote::delete_doll - Sending DELETE request to URL: {}", url); - + tracing::info!( + "DollsRemote::delete_doll - Sending DELETE request to URL: {}", + url + ); + let resp = with_auth(self.client.delete(url)).await.send().await?; - + resp.error_for_status().map_err(|e| { tracing::error!("DollsRemote::delete_doll - HTTP error: {}", e); e diff --git a/src-tauri/src/remotes/mod.rs b/src-tauri/src/remotes/mod.rs index 88ed21a..16eb049 100644 --- a/src-tauri/src/remotes/mod.rs +++ b/src-tauri/src/remotes/mod.rs @@ -1,3 +1,3 @@ +pub mod dolls; pub mod friends; pub mod user; -pub mod dolls; diff --git a/src-tauri/src/services/auth.rs b/src-tauri/src/services/auth.rs index c7c3b17..0493425 100644 --- a/src-tauri/src/services/auth.rs +++ b/src-tauri/src/services/auth.rs @@ -633,6 +633,15 @@ where if let Err(e) = save_auth_pass(&auth_pass) { error!("Failed to save auth pass: {}", e); } + + // Immediately refresh app data now that auth is available + tauri::async_runtime::spawn(async { + crate::state::init_app_data_scoped( + crate::state::AppDataRefreshScope::All, + ) + .await; + }); + on_success(); } Err(e) => error!("Token exchange failed: {}", e), diff --git a/src-tauri/src/services/cursor.rs b/src-tauri/src/services/cursor.rs index 1167be7..310d45d 100644 --- a/src-tauri/src/services/cursor.rs +++ b/src-tauri/src/services/cursor.rs @@ -135,11 +135,8 @@ async fn init_cursor_tracking() -> Result<(), String> { }; let mapped = absolute_to_normalized(&raw); - - let positions = CursorPositions { - raw, - mapped, - }; + + let positions = CursorPositions { raw, mapped }; // Send to consumer channel (non-blocking) if let Err(e) = tx.try_send(positions) { diff --git a/src-tauri/src/services/doll_editor.rs b/src-tauri/src/services/doll_editor.rs index 854ec23..19aedb5 100644 --- a/src-tauri/src/services/doll_editor.rs +++ b/src-tauri/src/services/doll_editor.rs @@ -122,7 +122,7 @@ pub async fn open_doll_editor_window(doll_id: Option) { // macOS Specific: Focus Trap Listener ID // We need to capture this to unlisten later. - + let mut parent_focus_listener_id: Option = None; if let Some(parent) = app_handle.get_webview_window(APP_MENU_WINDOW_LABEL) { diff --git a/src-tauri/src/services/ws.rs b/src-tauri/src/services/ws.rs index cda0df2..b84a150 100644 --- a/src-tauri/src/services/ws.rs +++ b/src-tauri/src/services/ws.rs @@ -7,7 +7,7 @@ use crate::{ get_app_handle, lock_r, lock_w, models::app_config::AppConfig, services::cursor::{normalized_to_absolute, CursorPosition, CursorPositions}, - state::FDOLL, + state::{init_app_data_scoped, AppDataRefreshScope, FDOLL}, }; use serde::{Deserialize, Serialize}; @@ -59,7 +59,7 @@ fn on_initialized(payload: Payload, _socket: RawClient) { Payload::Text(values) => { if let Some(first_value) = values.first() { info!("Received initialized event: {:?}", first_value); - + // Mark WebSocket as initialized let mut guard = lock_w!(FDOLL); if let Some(clients) = guard.clients.as_mut() { @@ -93,6 +93,11 @@ fn on_friend_request_accepted(payload: Payload, _socket: RawClient) { if let Err(e) = get_app_handle().emit(WS_EVENT::FRIEND_REQUEST_ACCEPTED, str) { error!("Failed to emit friend request accepted event: {:?}", e); } + + // Refresh friends list only (optimized - no need to fetch user profile) + tauri::async_runtime::spawn(async { + init_app_data_scoped(AppDataRefreshScope::Friends).await; + }); } _ => error!("Received unexpected payload format for friend request accepted"), } @@ -117,6 +122,11 @@ fn on_unfriended(payload: Payload, _socket: RawClient) { if let Err(e) = get_app_handle().emit(WS_EVENT::UNFRIENDED, str) { error!("Failed to emit unfriended event: {:?}", e); } + + // Refresh friends list only (optimized - no need to fetch user profile) + tauri::async_runtime::spawn(async { + init_app_data_scoped(AppDataRefreshScope::Friends).await; + }); } _ => error!("Received unexpected payload format for unfriended"), } @@ -232,6 +242,12 @@ fn on_friend_active_doll_changed(payload: Payload, _socket: RawClient) { } else { info!("Emitted friend-active-doll-changed to frontend"); } + + // Refresh friends list only (optimized - friend's active doll is part of friends data) + // Deduplicate burst events inside init_app_data_scoped. + tauri::async_runtime::spawn(async { + init_app_data_scoped(AppDataRefreshScope::Friends).await; + }); } else { info!("Received friend-active-doll-changed event with empty payload"); } @@ -245,9 +261,11 @@ fn on_doll_created(payload: Payload, _socket: RawClient) { Payload::Text(values) => { if let Some(first_value) = values.first() { info!("Received doll.created event: {:?}", first_value); - if let Err(e) = get_app_handle().emit(WS_EVENT::DOLL_CREATED, first_value) { - error!("Failed to emit doll.created event: {:?}", e); - } + + // Refresh dolls list + tauri::async_runtime::spawn(async { + init_app_data_scoped(AppDataRefreshScope::Dolls).await; + }); } else { info!("Received doll.created event with empty payload"); } @@ -261,9 +279,31 @@ fn on_doll_updated(payload: Payload, _socket: RawClient) { Payload::Text(values) => { if let Some(first_value) = values.first() { info!("Received doll.updated event: {:?}", first_value); - if let Err(e) = get_app_handle().emit(WS_EVENT::DOLL_UPDATED, first_value) { - error!("Failed to emit doll.updated event: {:?}", e); - } + + // Try to extract doll ID to check if it's the active doll + let doll_id = first_value.get("id").and_then(|v| v.as_str()); + + let is_active_doll = if let Some(id) = doll_id { + let guard = lock_r!(FDOLL); + guard + .app_data + .user + .as_ref() + .and_then(|u| u.active_doll_id.as_ref()) + .map(|active_id| active_id == id) + .unwrap_or(false) + } else { + false + }; + + // Refresh dolls + potentially User/Friends if active doll + tauri::async_runtime::spawn(async move { + init_app_data_scoped(AppDataRefreshScope::Dolls).await; + if is_active_doll { + init_app_data_scoped(AppDataRefreshScope::User).await; + init_app_data_scoped(AppDataRefreshScope::Friends).await; + } + }); } else { info!("Received doll.updated event with empty payload"); } @@ -277,9 +317,31 @@ fn on_doll_deleted(payload: Payload, _socket: RawClient) { Payload::Text(values) => { if let Some(first_value) = values.first() { info!("Received doll.deleted event: {:?}", first_value); - if let Err(e) = get_app_handle().emit(WS_EVENT::DOLL_DELETED, first_value) { - error!("Failed to emit doll.deleted event: {:?}", e); - } + + // Try to extract doll ID to check if it was the active doll + let doll_id = first_value.get("id").and_then(|v| v.as_str()); + + let is_active_doll = if let Some(id) = doll_id { + let guard = lock_r!(FDOLL); + guard + .app_data + .user + .as_ref() + .and_then(|u| u.active_doll_id.as_ref()) + .map(|active_id| active_id == id) + .unwrap_or(false) + } else { + false + }; + + // Refresh dolls + User/Friends if the deleted doll was active + tauri::async_runtime::spawn(async move { + init_app_data_scoped(AppDataRefreshScope::Dolls).await; + if is_active_doll { + init_app_data_scoped(AppDataRefreshScope::User).await; + init_app_data_scoped(AppDataRefreshScope::Friends).await; + } + }); } else { info!("Received doll.deleted event with empty payload"); } diff --git a/src-tauri/src/state.rs b/src-tauri/src/state.rs index 82a595d..e701cff 100644 --- a/src-tauri/src/state.rs +++ b/src-tauri/src/state.rs @@ -5,15 +5,16 @@ use crate::{ app_config::{AppConfig, AuthConfig}, app_data::AppData, }, - remotes::{friends::FriendRemote, user::UserRemote}, + remotes::{dolls::DollsRemote, friends::FriendRemote, user::UserRemote}, services::auth::{load_auth_pass, AuthPass}, }; -use serde_json::json; use std::{ + collections::HashSet, env, sync::{Arc, LazyLock, RwLock}, }; use tauri::Emitter; +use tokio::sync::Mutex; use tracing::{info, warn}; #[derive(Default, Clone)] @@ -154,57 +155,188 @@ pub fn init_fdoll_state(tracing_guard: Option { - let mut guard = lock_w!(FDOLL); - guard.app_data.user = Some(user); - } - Err(e) => { - warn!("Failed to fetch user profile: {}", e); - use tauri_plugin_dialog::MessageDialogBuilder; - use tauri_plugin_dialog::{DialogExt, MessageDialogKind}; +/// Populate specific parts of app data from the server based on the scope. +/// +/// # Arguments +/// * `scope` - Determines which data to refresh (All, User, or Friends) +/// +/// # Examples +/// ``` +/// // Refresh only friends list when a friend request is accepted +/// init_app_data_scoped(AppDataRefreshScope::Friends).await; +/// +/// // Refresh only user profile when updating user settings +/// init_app_data_scoped(AppDataRefreshScope::User).await; +/// ``` +static REFRESH_IN_FLIGHT: LazyLock>> = + LazyLock::new(|| Mutex::new(HashSet::new())); +static REFRESH_PENDING: LazyLock>> = + LazyLock::new(|| Mutex::new(HashSet::new())); - let handle = get_app_handle(); - MessageDialogBuilder::new( - handle.dialog().clone(), - "Network Error", - "Failed to fetch user profile. You may be offline.", - ) - .kind(MessageDialogKind::Error) - .show(|_| {}); - // We continue execution to see if other parts (like friends) can be loaded or if we just stay in a partial state. - // Alternatively, return early here. +pub async fn init_app_data_scoped(scope: AppDataRefreshScope) { + loop { + // Deduplicate concurrent refreshes for the same scope + { + let mut in_flight = REFRESH_IN_FLIGHT.lock().await; + if in_flight.contains(&scope) { + let mut pending = REFRESH_PENDING.lock().await; + pending.insert(scope); + return; + } + in_flight.insert(scope); } - } - // Fetch friends list - match friend_remote.get_friends().await { - Ok(friends) => { - let mut guard = lock_w!(FDOLL); - guard.app_data.friends = Some(friends); - } - Err(e) => { - warn!("Failed to fetch friends list: {}", e); - // Optionally show another dialog or just log it. - // Showing too many dialogs on startup is annoying, so we might skip this one if user profile failed too. - } - } + let result: Result<(), ()> = async { + let user_remote = UserRemote::new(); + let friend_remote = FriendRemote::new(); + let dolls_remote = DollsRemote::new(); - // Emit event regardless of partial success, frontend should handle nulls/empty states - { - let guard = lock_r!(FDOLL); // Use read lock to get data - let app_data_clone = guard.app_data.clone(); - drop(guard); // Drop lock before emitting to prevent potential deadlocks + // Fetch user profile if needed + if matches!(scope, AppDataRefreshScope::All | AppDataRefreshScope::User) { + match user_remote.get_user(None).await { + Ok(user) => { + let mut guard = lock_w!(FDOLL); + guard.app_data.user = Some(user); + } + Err(e) => { + warn!("Failed to fetch user profile: {}", e); + use tauri_plugin_dialog::MessageDialogBuilder; + use tauri_plugin_dialog::{DialogExt, MessageDialogKind}; - if let Err(e) = get_app_handle().emit("app-data-refreshed", json!(app_data_clone)) { - warn!("Failed to emit app-data-refreshed event: {}", e); + let handle = get_app_handle(); + MessageDialogBuilder::new( + handle.dialog().clone(), + "Network Error", + "Failed to fetch user profile. You may be offline.", + ) + .kind(MessageDialogKind::Error) + .show(|_| {}); + return Err(()); + } + } + } + + // Fetch friends list if needed + if matches!( + scope, + AppDataRefreshScope::All | AppDataRefreshScope::Friends + ) { + match friend_remote.get_friends().await { + Ok(friends) => { + let mut guard = lock_w!(FDOLL); + guard.app_data.friends = Some(friends); + } + Err(e) => { + warn!("Failed to fetch friends list: {}", e); + use tauri_plugin_dialog::MessageDialogBuilder; + use tauri_plugin_dialog::{DialogExt, MessageDialogKind}; + + let handle = get_app_handle(); + MessageDialogBuilder::new( + handle.dialog().clone(), + "Network Error", + "Failed to fetch friends list. You may be offline.", + ) + .kind(MessageDialogKind::Error) + .show(|_| {}); + return Err(()); + } + } + } + + // Fetch dolls list if needed + if matches!(scope, AppDataRefreshScope::All | AppDataRefreshScope::Dolls) { + match dolls_remote.get_dolls().await { + Ok(dolls) => { + let mut guard = lock_w!(FDOLL); + guard.app_data.dolls = Some(dolls); + } + Err(e) => { + warn!("Failed to fetch dolls list: {}", e); + use tauri_plugin_dialog::MessageDialogBuilder; + use tauri_plugin_dialog::{DialogExt, MessageDialogKind}; + + let handle = get_app_handle(); + MessageDialogBuilder::new( + handle.dialog().clone(), + "Network Error", + "Failed to fetch dolls list. You may be offline.", + ) + .kind(MessageDialogKind::Error) + .show(|_| {}); + return Err(()); + } + } + } + + // Emit event regardless of partial success, frontend should handle nulls/empty states + { + let guard = lock_r!(FDOLL); // Use read lock to get data + let app_data_clone = guard.app_data.clone(); + drop(guard); // Drop lock before emitting to prevent potential deadlocks + + if let Err(e) = get_app_handle().emit("app-data-refreshed", &app_data_clone) { + warn!("Failed to emit app-data-refreshed event: {}", e); + use tauri_plugin_dialog::MessageDialogBuilder; + use tauri_plugin_dialog::{DialogExt, MessageDialogKind}; + + let handle = get_app_handle(); + MessageDialogBuilder::new( + handle.dialog().clone(), + "Sync Error", + "Could not broadcast refreshed data to the UI. Some data may be stale.", + ) + .kind(MessageDialogKind::Error) + .show(|_| {}); + } + } + + Ok(()) } + .await; + + // Clear in-flight marker even on early exit + { + let mut in_flight = REFRESH_IN_FLIGHT.lock().await; + in_flight.remove(&scope); + } + + // If a refresh was queued while this one was running, run again + let rerun = { + let mut pending = REFRESH_PENDING.lock().await; + pending.remove(&scope) + }; + + if rerun { + continue; + } + + if result.is_err() { + return; + } + + break; } } diff --git a/src/routes/app-menu/tabs/your-dolls/index.svelte b/src/routes/app-menu/tabs/your-dolls/index.svelte index a499742..3750b27 100644 --- a/src/routes/app-menu/tabs/your-dolls/index.svelte +++ b/src/routes/app-menu/tabs/your-dolls/index.svelte @@ -1,72 +1,19 @@ @@ -106,7 +59,7 @@ | null, scene: SceneData, }; +export type AppData = { user: UserProfile | null, friends: Array | null, dolls: Array | null, scene: SceneData, };