fixed WS race condition issue

This commit is contained in:
2025-12-25 14:12:39 +08:00
parent 42f798c8b7
commit b168d674bd
5 changed files with 50 additions and 67 deletions

View File

@@ -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;
});
}) {

View File

@@ -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()

View File

@@ -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<Record<string, CursorPositions>>({});
export let friendsCursorPositions = writable<Record<string, CursorPositions>>(
{},
);
export let friendsActiveDolls = writable<Record<string, DollDto | null>>({});
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;
}
}

View File

@@ -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);
}
});

View File

@@ -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;
}
</script>
@@ -55,6 +52,7 @@
<p class="text-sm font-semibold opacity-75">Friends Online</p>
<div>
{#each Object.entries($friendsCursorPositions) as [userId, position]}
{@const dollConfig = getFriendDollConfig(userId)}
<div
class="bg-base-200/50 p-2 rounded text-xs text-left flex gap-2 flex-col"
>