diff --git a/src-tauri/src/app.rs b/src-tauri/src/app.rs index 4bc0c31..49b588f 100644 --- a/src-tauri/src/app.rs +++ b/src-tauri/src/app.rs @@ -1,11 +1,13 @@ use std::time::Duration; + use tokio::time::{sleep, Instant}; use tracing::info; use crate::{ services::{ - auth::get_tokens, + auth::{get_access_token, get_tokens}, scene::{close_splash_window, open_scene_window, open_splash_window}, + ws::init_ws_client, }, state::init_app_data, system_tray::init_system_tray, @@ -16,30 +18,31 @@ pub async fn start_fdoll() { bootstrap().await; } +async fn init_ws_after_auth() { + const MAX_ATTEMPTS: u8 = 5; + const BACKOFF: Duration = Duration::from_millis(300); + + for attempt in 1..=MAX_ATTEMPTS { + if get_access_token().await.is_some() { + init_ws_client().await; + return; + } + + sleep(BACKOFF).await; + } +} + async fn construct_app() { open_splash_window(); // Record start time for minimum splash duration let start = Instant::now(); - // Spawn initialization tasks in parallel - // We want to wait for them to finish, but they run concurrently - let init_data = tauri::async_runtime::spawn(async { - init_app_data().await; - }); + // Initialize app data first so we only start WebSocket after auth is fully available + init_app_data().await; - let init_ws = tauri::async_runtime::spawn(async { - // init_ws_client calls get_access_token().await. - // During a fresh login, this token might be in the process of being saved/refreshed - // or the client initialization might be racing. - // However, construct_app is called after auth success, so tokens should be there. - // The issue might be that init_ws_client is idempotent but if called twice or early... - // Actually, init_ws_client handles creating the socket. - crate::services::ws::init_ws_client().await; - }); - - // Wait for both to complete - let _ = tokio::join!(init_data, init_ws); + // Initialize WebSocket client after we know auth is present + init_ws_after_auth().await; // Ensure splash stays visible for at least 3 seconds let elapsed = start.elapsed(); @@ -54,16 +57,15 @@ async fn construct_app() { pub async fn bootstrap() { match get_tokens().await { - Some(_) => { - info!("User session restored"); + Some(tokens) => { + info!("Tokens found in keyring - restoring user session"); construct_app().await; } None => { - info!("No active session, user needs to authenticate"); + info!("No active session found - user needs to authenticate"); match crate::services::auth::init_auth_code_retrieval(|| { info!("Authentication successful, creating scene..."); tauri::async_runtime::spawn(async { - info!("Creating scene after auth success..."); construct_app().await; }); }) { diff --git a/src-tauri/src/services/ws.rs b/src-tauri/src/services/ws.rs index b84a150..33e2e59 100644 --- a/src-tauri/src/services/ws.rs +++ b/src-tauri/src/services/ws.rs @@ -60,11 +60,10 @@ fn on_initialized(payload: Payload, _socket: RawClient) { if let Some(first_value) = values.first() { info!("Received initialized event: {:?}", first_value); - // Mark WebSocket as initialized + // Mark WebSocket as initialized and reset backoff timer let mut guard = lock_w!(FDOLL); if let Some(clients) = guard.clients.as_mut() { clients.is_ws_initialized = true; - info!("WebSocket marked as initialized and ready for business"); } } else { info!("Received initialized event with empty payload"); @@ -239,8 +238,6 @@ fn on_friend_active_doll_changed(payload: Payload, _socket: RawClient) { get_app_handle().emit(WS_EVENT::FRIEND_ACTIVE_DOLL_CHANGED, first_value) { error!("Failed to emit friend-active-doll-changed event: {:?}", e); - } else { - info!("Emitted friend-active-doll-changed to frontend"); } // Refresh friends list only (optimized - friend's active doll is part of friends data) @@ -353,20 +350,23 @@ fn on_doll_deleted(payload: Payload, _socket: RawClient) { pub async fn report_cursor_data(cursor_position: CursorPosition) { // Only attempt to get clients if lock_r succeeds (it should, but safety first) // and if clients are actually initialized. - let client_opt = { + let (client_opt, is_initialized) = { let guard = lock_r!(FDOLL); if let Some(clients) = &guard.clients { - if clients.is_ws_initialized { - clients.ws_client.as_ref().cloned() - } else { - None - } + ( + clients.ws_client.as_ref().cloned(), + clients.is_ws_initialized, + ) } else { - None + (None, false) } }; if let Some(client) = client_opt { + if !is_initialized { + return; + } + match async_runtime::spawn_blocking(move || { client.emit( WS_EVENT::CURSOR_REPORT_POSITION, @@ -379,8 +379,6 @@ pub async fn report_cursor_data(cursor_position: CursorPosition) { Ok(Err(e)) => error!("Failed to emit cursor report: {}", e), Err(e) => error!("Failed to execute blocking task for cursor report: {}", e), } - } else { - // Quietly fail if client is not ready (e.g. during startup/shutdown or before initialization) } } @@ -395,8 +393,8 @@ pub async fn init_ws_client() { let mut guard = lock_w!(FDOLL); if let Some(clients) = guard.clients.as_mut() { clients.ws_client = Some(ws_client); + clients.is_ws_initialized = false; // wait for initialized event } - info!("WebSocket client initialized after authentication"); } Err(e) => { error!("Failed to initialize WebSocket client: {}", e); @@ -415,8 +413,6 @@ pub async fn build_ws_client( None => return Err("No access token available for WebSocket connection".to_string()), }; - info!("Building WebSocket client with authentication"); - let api_base_url = app_config .api_base_url .clone() diff --git a/src/events/cursor.ts b/src/events/cursor.ts index 4bd28eb..76e41ce 100644 --- a/src/events/cursor.ts +++ b/src/events/cursor.ts @@ -25,7 +25,9 @@ type FriendCursorData = { // The exported store will only expose the position part to consumers, // but internally we manage the full data. // Actually, it's easier if we just export the positions and manage state internally. -export let friendsCursorPositions = writable>({}); +export let friendsCursorPositions = writable>( + {}, +); export let friendsActiveDolls = writable>({}); let unlistenCursor: UnlistenFn | null = null; @@ -90,10 +92,7 @@ export async function initCursorTracking() { try { payload = JSON.parse(payload); } catch (e) { - console.error( - "[Cursor] Failed to parse friend disconnected payload:", - e, - ); + console.error("Failed to parse friend disconnected payload:", e); return; } } @@ -129,7 +128,7 @@ export async function initCursorTracking() { data = JSON.parse(data); } catch (e) { console.error( - "[Cursor] Failed to parse friend-active-doll-changed payload:", + "Failed to parse friend-active-doll-changed payload:", e, ); return; @@ -139,16 +138,8 @@ export async function initCursorTracking() { // Cast to expected type after parsing const payload = data as { friendId: string; doll: DollDto | null }; - console.log( - "[Cursor] Received friend-active-doll-changed event:", - payload, - ); - if (!payload.doll) { // If doll is null, it means the friend deactivated their doll. - console.log( - `[Cursor] Removing doll for friend ${payload.friendId} due to deactivation`, - ); // Update the active dolls store to explicitly set this friend's doll to null // We MUST set it to null instead of deleting it, otherwise the UI might @@ -167,10 +158,6 @@ export async function initCursorTracking() { }); } else { // Update or add the new doll configuration - console.log( - `[Cursor] Updating doll for friend ${payload.friendId}:`, - payload.doll, - ); friendsActiveDolls.update((current) => { return { ...current, @@ -182,7 +169,7 @@ export async function initCursorTracking() { isListening = true; } catch (err) { - console.error("[Cursor] Failed to initialize cursor tracking:", err); + console.error("Failed to initialize cursor tracking:", err); throw err; } } diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index b329caf..69cc512 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -11,7 +11,7 @@ await initCursorTracking(); await initAppDataListener(); } catch (err) { - console.error("[Scene] Failed to initialize event listeners:", err); + console.error("Failed to initialize event listeners:", err); } }); diff --git a/src/routes/scene/+page.svelte b/src/routes/scene/+page.svelte index 81223e4..73a2109 100644 --- a/src/routes/scene/+page.svelte +++ b/src/routes/scene/+page.svelte @@ -17,15 +17,12 @@ } function getFriendDollConfig(userId: string) { - // 1. Try to get from real-time store (most up-to-date) - // Check if key exists to distinguish between "unknown" (undefined) and "no doll" (null) - if (userId in $friendsActiveDolls) { - return $friendsActiveDolls[userId]?.configuration; - } - - // 2. Fallback to initial app data (snapshot on load) - const friend = $appData?.friends?.find((f) => f.friend.id === userId); - return friend?.friend.activeDoll?.configuration; + if (userId in $friendsActiveDolls) { + return $friendsActiveDolls[userId]?.configuration; + } + + const friend = $appData?.friends?.find((f) => f.friend.id === userId); + return friend?.friend.activeDoll?.configuration; } @@ -55,6 +52,7 @@

Friends Online

{#each Object.entries($friendsCursorPositions) as [userId, position]} + {@const dollConfig = getFriendDollConfig(userId)}