fixed WS race condition issue
This commit is contained in:
@@ -1,11 +1,13 @@
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use tokio::time::{sleep, Instant};
|
use tokio::time::{sleep, Instant};
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
services::{
|
services::{
|
||||||
auth::get_tokens,
|
auth::{get_access_token, get_tokens},
|
||||||
scene::{close_splash_window, open_scene_window, open_splash_window},
|
scene::{close_splash_window, open_scene_window, open_splash_window},
|
||||||
|
ws::init_ws_client,
|
||||||
},
|
},
|
||||||
state::init_app_data,
|
state::init_app_data,
|
||||||
system_tray::init_system_tray,
|
system_tray::init_system_tray,
|
||||||
@@ -16,30 +18,31 @@ pub async fn start_fdoll() {
|
|||||||
bootstrap().await;
|
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() {
|
async fn construct_app() {
|
||||||
open_splash_window();
|
open_splash_window();
|
||||||
|
|
||||||
// Record start time for minimum splash duration
|
// Record start time for minimum splash duration
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
|
|
||||||
// Spawn initialization tasks in parallel
|
// Initialize app data first so we only start WebSocket after auth is fully available
|
||||||
// We want to wait for them to finish, but they run concurrently
|
init_app_data().await;
|
||||||
let init_data = tauri::async_runtime::spawn(async {
|
|
||||||
init_app_data().await;
|
|
||||||
});
|
|
||||||
|
|
||||||
let init_ws = tauri::async_runtime::spawn(async {
|
// Initialize WebSocket client after we know auth is present
|
||||||
// init_ws_client calls get_access_token().await.
|
init_ws_after_auth().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);
|
|
||||||
|
|
||||||
// Ensure splash stays visible for at least 3 seconds
|
// Ensure splash stays visible for at least 3 seconds
|
||||||
let elapsed = start.elapsed();
|
let elapsed = start.elapsed();
|
||||||
@@ -54,16 +57,15 @@ async fn construct_app() {
|
|||||||
|
|
||||||
pub async fn bootstrap() {
|
pub async fn bootstrap() {
|
||||||
match get_tokens().await {
|
match get_tokens().await {
|
||||||
Some(_) => {
|
Some(tokens) => {
|
||||||
info!("User session restored");
|
info!("Tokens found in keyring - restoring user session");
|
||||||
construct_app().await;
|
construct_app().await;
|
||||||
}
|
}
|
||||||
None => {
|
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(|| {
|
match crate::services::auth::init_auth_code_retrieval(|| {
|
||||||
info!("Authentication successful, creating scene...");
|
info!("Authentication successful, creating scene...");
|
||||||
tauri::async_runtime::spawn(async {
|
tauri::async_runtime::spawn(async {
|
||||||
info!("Creating scene after auth success...");
|
|
||||||
construct_app().await;
|
construct_app().await;
|
||||||
});
|
});
|
||||||
}) {
|
}) {
|
||||||
|
|||||||
@@ -60,11 +60,10 @@ fn on_initialized(payload: Payload, _socket: RawClient) {
|
|||||||
if let Some(first_value) = values.first() {
|
if let Some(first_value) = values.first() {
|
||||||
info!("Received initialized event: {:?}", first_value);
|
info!("Received initialized event: {:?}", first_value);
|
||||||
|
|
||||||
// Mark WebSocket as initialized
|
// Mark WebSocket as initialized and reset backoff timer
|
||||||
let mut guard = lock_w!(FDOLL);
|
let mut guard = lock_w!(FDOLL);
|
||||||
if let Some(clients) = guard.clients.as_mut() {
|
if let Some(clients) = guard.clients.as_mut() {
|
||||||
clients.is_ws_initialized = true;
|
clients.is_ws_initialized = true;
|
||||||
info!("WebSocket marked as initialized and ready for business");
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
info!("Received initialized event with empty payload");
|
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)
|
get_app_handle().emit(WS_EVENT::FRIEND_ACTIVE_DOLL_CHANGED, first_value)
|
||||||
{
|
{
|
||||||
error!("Failed to emit friend-active-doll-changed event: {:?}", e);
|
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)
|
// 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) {
|
pub async fn report_cursor_data(cursor_position: CursorPosition) {
|
||||||
// Only attempt to get clients if lock_r succeeds (it should, but safety first)
|
// Only attempt to get clients if lock_r succeeds (it should, but safety first)
|
||||||
// and if clients are actually initialized.
|
// and if clients are actually initialized.
|
||||||
let client_opt = {
|
let (client_opt, is_initialized) = {
|
||||||
let guard = lock_r!(FDOLL);
|
let guard = lock_r!(FDOLL);
|
||||||
if let Some(clients) = &guard.clients {
|
if let Some(clients) = &guard.clients {
|
||||||
if clients.is_ws_initialized {
|
(
|
||||||
clients.ws_client.as_ref().cloned()
|
clients.ws_client.as_ref().cloned(),
|
||||||
} else {
|
clients.is_ws_initialized,
|
||||||
None
|
)
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
None
|
(None, false)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(client) = client_opt {
|
if let Some(client) = client_opt {
|
||||||
|
if !is_initialized {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
match async_runtime::spawn_blocking(move || {
|
match async_runtime::spawn_blocking(move || {
|
||||||
client.emit(
|
client.emit(
|
||||||
WS_EVENT::CURSOR_REPORT_POSITION,
|
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),
|
Ok(Err(e)) => error!("Failed to emit cursor report: {}", e),
|
||||||
Err(e) => error!("Failed to execute blocking task for 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);
|
let mut guard = lock_w!(FDOLL);
|
||||||
if let Some(clients) = guard.clients.as_mut() {
|
if let Some(clients) = guard.clients.as_mut() {
|
||||||
clients.ws_client = Some(ws_client);
|
clients.ws_client = Some(ws_client);
|
||||||
|
clients.is_ws_initialized = false; // wait for initialized event
|
||||||
}
|
}
|
||||||
info!("WebSocket client initialized after authentication");
|
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Failed to initialize WebSocket client: {}", 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()),
|
None => return Err("No access token available for WebSocket connection".to_string()),
|
||||||
};
|
};
|
||||||
|
|
||||||
info!("Building WebSocket client with authentication");
|
|
||||||
|
|
||||||
let api_base_url = app_config
|
let api_base_url = app_config
|
||||||
.api_base_url
|
.api_base_url
|
||||||
.clone()
|
.clone()
|
||||||
|
|||||||
@@ -25,7 +25,9 @@ type FriendCursorData = {
|
|||||||
// The exported store will only expose the position part to consumers,
|
// The exported store will only expose the position part to consumers,
|
||||||
// but internally we manage the full data.
|
// but internally we manage the full data.
|
||||||
// Actually, it's easier if we just export the positions and manage state internally.
|
// 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>>({});
|
export let friendsActiveDolls = writable<Record<string, DollDto | null>>({});
|
||||||
|
|
||||||
let unlistenCursor: UnlistenFn | null = null;
|
let unlistenCursor: UnlistenFn | null = null;
|
||||||
@@ -90,10 +92,7 @@ export async function initCursorTracking() {
|
|||||||
try {
|
try {
|
||||||
payload = JSON.parse(payload);
|
payload = JSON.parse(payload);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(
|
console.error("Failed to parse friend disconnected payload:", e);
|
||||||
"[Cursor] Failed to parse friend disconnected payload:",
|
|
||||||
e,
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -129,7 +128,7 @@ export async function initCursorTracking() {
|
|||||||
data = JSON.parse(data);
|
data = JSON.parse(data);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(
|
console.error(
|
||||||
"[Cursor] Failed to parse friend-active-doll-changed payload:",
|
"Failed to parse friend-active-doll-changed payload:",
|
||||||
e,
|
e,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
@@ -139,16 +138,8 @@ export async function initCursorTracking() {
|
|||||||
// Cast to expected type after parsing
|
// Cast to expected type after parsing
|
||||||
const payload = data as { friendId: string; doll: DollDto | null };
|
const payload = data as { friendId: string; doll: DollDto | null };
|
||||||
|
|
||||||
console.log(
|
|
||||||
"[Cursor] Received friend-active-doll-changed event:",
|
|
||||||
payload,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!payload.doll) {
|
if (!payload.doll) {
|
||||||
// If doll is null, it means the friend deactivated their 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
|
// 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
|
// We MUST set it to null instead of deleting it, otherwise the UI might
|
||||||
@@ -167,10 +158,6 @@ export async function initCursorTracking() {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Update or add the new doll configuration
|
// Update or add the new doll configuration
|
||||||
console.log(
|
|
||||||
`[Cursor] Updating doll for friend ${payload.friendId}:`,
|
|
||||||
payload.doll,
|
|
||||||
);
|
|
||||||
friendsActiveDolls.update((current) => {
|
friendsActiveDolls.update((current) => {
|
||||||
return {
|
return {
|
||||||
...current,
|
...current,
|
||||||
@@ -182,7 +169,7 @@ export async function initCursorTracking() {
|
|||||||
|
|
||||||
isListening = true;
|
isListening = true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("[Cursor] Failed to initialize cursor tracking:", err);
|
console.error("Failed to initialize cursor tracking:", err);
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
await initCursorTracking();
|
await initCursorTracking();
|
||||||
await initAppDataListener();
|
await initAppDataListener();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("[Scene] Failed to initialize event listeners:", err);
|
console.error("Failed to initialize event listeners:", err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -17,15 +17,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getFriendDollConfig(userId: string) {
|
function getFriendDollConfig(userId: string) {
|
||||||
// 1. Try to get from real-time store (most up-to-date)
|
if (userId in $friendsActiveDolls) {
|
||||||
// Check if key exists to distinguish between "unknown" (undefined) and "no doll" (null)
|
return $friendsActiveDolls[userId]?.configuration;
|
||||||
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);
|
||||||
const friend = $appData?.friends?.find((f) => f.friend.id === userId);
|
return friend?.friend.activeDoll?.configuration;
|
||||||
return friend?.friend.activeDoll?.configuration;
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -55,6 +52,7 @@
|
|||||||
<p class="text-sm font-semibold opacity-75">Friends Online</p>
|
<p class="text-sm font-semibold opacity-75">Friends Online</p>
|
||||||
<div>
|
<div>
|
||||||
{#each Object.entries($friendsCursorPositions) as [userId, position]}
|
{#each Object.entries($friendsCursorPositions) as [userId, position]}
|
||||||
|
{@const dollConfig = getFriendDollConfig(userId)}
|
||||||
<div
|
<div
|
||||||
class="bg-base-200/50 p-2 rounded text-xs text-left flex gap-2 flex-col"
|
class="bg-base-200/50 p-2 rounded text-xs text-left flex gap-2 flex-col"
|
||||||
>
|
>
|
||||||
|
|||||||
Reference in New Issue
Block a user