trivial enhancements to state management solution
This commit is contained in:
@@ -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<tracing_appender::non_blocking::Wo
|
||||
info!("Initialized FDOLL state (WebSocket client & user data initializing asynchronously)");
|
||||
}
|
||||
|
||||
/// Defines which parts of AppData should be refreshed from the server
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum AppDataRefreshScope {
|
||||
/// Refresh all data (user profile + friends list + dolls list)
|
||||
All,
|
||||
/// Refresh only user profile
|
||||
User,
|
||||
/// Refresh only friends list
|
||||
Friends,
|
||||
/// Refresh only dolls list
|
||||
Dolls,
|
||||
}
|
||||
|
||||
/// To be called in init state or need to refresh data.
|
||||
/// Populate user data in app state from the server.
|
||||
///
|
||||
/// This is a convenience wrapper that refreshes all data.
|
||||
/// For more control, use `init_app_data_scoped`.
|
||||
pub async fn init_app_data() {
|
||||
let user_remote = UserRemote::new();
|
||||
let friend_remote = FriendRemote::new();
|
||||
init_app_data_scoped(AppDataRefreshScope::All).await;
|
||||
}
|
||||
|
||||
// Fetch user profile
|
||||
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};
|
||||
/// 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<Mutex<HashSet<AppDataRefreshScope>>> =
|
||||
LazyLock::new(|| Mutex::new(HashSet::new()));
|
||||
static REFRESH_PENDING: LazyLock<Mutex<HashSet<AppDataRefreshScope>>> =
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user