minor websocket refactoring
This commit is contained in:
@@ -2,44 +2,43 @@ use rust_socketio::{Payload, RawClient};
|
|||||||
use tracing::info;
|
use tracing::info;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
lock_w,
|
lock_w, services::health_manager::close_health_manager_window,
|
||||||
services::health_manager::close_health_manager_window,
|
services::scene::open_scene_window, state::FDOLL,
|
||||||
services::scene::open_scene_window,
|
|
||||||
state::FDOLL,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::WS_EVENT;
|
use super::{types::WS_EVENT, utils};
|
||||||
|
|
||||||
|
/// Emit initialization request to WebSocket server
|
||||||
fn emit_initialize(socket: &RawClient) {
|
fn emit_initialize(socket: &RawClient) {
|
||||||
if let Err(e) = socket.emit(WS_EVENT::CLIENT_INITIALIZE, serde_json::json!({})) {
|
if let Err(e) = socket.emit(WS_EVENT::CLIENT_INITIALIZE, serde_json::json!({})) {
|
||||||
tracing::error!("Failed to emit client-initialize: {:?}", e);
|
tracing::error!("Failed to emit client-initialize: {:?}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Handler for WebSocket connection event
|
||||||
pub fn on_connected(_payload: Payload, socket: RawClient) {
|
pub fn on_connected(_payload: Payload, socket: RawClient) {
|
||||||
info!("WebSocket connected. Sending initialization request.");
|
info!("WebSocket connected. Sending initialization request.");
|
||||||
emit_initialize(&socket);
|
emit_initialize(&socket);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Handler for initialized event
|
||||||
pub fn on_initialized(payload: Payload, _socket: RawClient) {
|
pub fn on_initialized(payload: Payload, _socket: RawClient) {
|
||||||
match payload {
|
if utils::extract_text_value(payload, "initialized").is_ok() {
|
||||||
Payload::Text(values) => {
|
mark_ws_initialized();
|
||||||
if let Some(first_value) = values.first() {
|
restore_connection_ui();
|
||||||
info!("Received initialized event: {:?}", first_value);
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Mark WebSocket as initialized and reset backoff timer
|
/// Mark WebSocket as initialized in app state
|
||||||
|
fn mark_ws_initialized() {
|
||||||
let mut guard = lock_w!(FDOLL);
|
let mut guard = lock_w!(FDOLL);
|
||||||
if let Some(clients) = guard.network.clients.as_mut() {
|
if let Some(clients) = guard.network.clients.as_mut() {
|
||||||
clients.is_ws_initialized = true;
|
clients.is_ws_initialized = true;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Connection restored: close health manager and reopen scene
|
/// Restore UI after successful connection
|
||||||
|
fn restore_connection_ui() {
|
||||||
close_health_manager_window();
|
close_health_manager_window();
|
||||||
open_scene_window();
|
open_scene_window();
|
||||||
} else {
|
|
||||||
info!("Received initialized event with empty payload");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => tracing::error!("Received unexpected payload format for initialized"),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,51 +1,8 @@
|
|||||||
use rust_socketio::Payload;
|
use crate::services::cursor::CursorPosition;
|
||||||
use tauri::async_runtime;
|
|
||||||
use tracing::error;
|
|
||||||
|
|
||||||
use crate::{
|
use super::{emitter, types::WS_EVENT};
|
||||||
init::lifecycle::handle_disasterous_failure, lock_r, services::cursor::CursorPosition,
|
|
||||||
state::FDOLL,
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::WS_EVENT;
|
|
||||||
|
|
||||||
|
/// Report cursor position to WebSocket server
|
||||||
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)
|
let _ = emitter::ws_emit(WS_EVENT::CURSOR_REPORT_POSITION, cursor_position).await;
|
||||||
// and if clients are actually initialized.
|
|
||||||
let (client_opt, is_initialized) = {
|
|
||||||
let guard = lock_r!(FDOLL);
|
|
||||||
if let Some(clients) = &guard.network.clients {
|
|
||||||
(
|
|
||||||
clients.ws_client.as_ref().cloned(),
|
|
||||||
clients.is_ws_initialized,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
(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,
|
|
||||||
Payload::Text(vec![serde_json::json!(cursor_position)]),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(Ok(_)) => (),
|
|
||||||
Ok(Err(e)) => {
|
|
||||||
error!("Failed to emit cursor report: {}", e);
|
|
||||||
handle_disasterous_failure(Some(format!("WebSocket emit failed: {}", e))).await;
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
error!("Failed to execute blocking task for cursor report: {}", e);
|
|
||||||
handle_disasterous_failure(Some(format!("WebSocket task failed: {}", e))).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,101 +1,26 @@
|
|||||||
use rust_socketio::{Payload, RawClient};
|
use rust_socketio::{Payload, RawClient};
|
||||||
use tracing::{error, info};
|
|
||||||
|
|
||||||
use crate::{
|
use super::{refresh, utils};
|
||||||
lock_r,
|
|
||||||
state::{init_app_data_scoped, AppDataRefreshScope, FDOLL},
|
|
||||||
};
|
|
||||||
|
|
||||||
|
/// Handler for doll.created event
|
||||||
pub fn on_doll_created(payload: Payload, _socket: RawClient) {
|
pub fn on_doll_created(payload: Payload, _socket: RawClient) {
|
||||||
match payload {
|
if utils::extract_text_value(payload, "doll.created").is_ok() {
|
||||||
Payload::Text(values) => {
|
refresh::refresh_app_data(crate::state::AppDataRefreshScope::Dolls);
|
||||||
if let Some(first_value) = values.first() {
|
|
||||||
info!("Received doll.created event: {:?}", first_value);
|
|
||||||
|
|
||||||
// Refresh dolls list
|
|
||||||
tauri::async_runtime::spawn(async {
|
|
||||||
init_app_data_scoped(AppDataRefreshScope::Dolls).await;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
info!("Received doll.created event with empty payload");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => error!("Received unexpected payload format for doll.created"),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Handler for doll.updated event
|
||||||
pub fn on_doll_updated(payload: Payload, _socket: RawClient) {
|
pub fn on_doll_updated(payload: Payload, _socket: RawClient) {
|
||||||
match payload {
|
if let Ok(value) = utils::extract_text_value(payload, "doll.updated") {
|
||||||
Payload::Text(values) => {
|
let doll_id = utils::extract_doll_id(&value);
|
||||||
if let Some(first_value) = values.first() {
|
refresh::refresh_with_active_doll_check(doll_id.as_deref());
|
||||||
info!("Received doll.updated event: {:?}", first_value);
|
|
||||||
|
|
||||||
// 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
|
|
||||||
.user_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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => error!("Received unexpected payload format for doll.updated"),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Handler for doll.deleted event
|
||||||
pub fn on_doll_deleted(payload: Payload, _socket: RawClient) {
|
pub fn on_doll_deleted(payload: Payload, _socket: RawClient) {
|
||||||
match payload {
|
if let Ok(value) = utils::extract_text_value(payload, "doll.deleted") {
|
||||||
Payload::Text(values) => {
|
let doll_id = utils::extract_doll_id(&value);
|
||||||
if let Some(first_value) = values.first() {
|
refresh::refresh_with_active_doll_check(doll_id.as_deref());
|
||||||
info!("Received doll.deleted event: {:?}", first_value);
|
|
||||||
|
|
||||||
// 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
|
|
||||||
.user_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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => error!("Received unexpected payload format for doll.deleted"),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
67
src-tauri/src/services/ws/emitter.rs
Normal file
67
src-tauri/src/services/ws/emitter.rs
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
use rust_socketio::Payload;
|
||||||
|
use serde::Serialize;
|
||||||
|
use tauri::{async_runtime, Emitter};
|
||||||
|
use tracing::error;
|
||||||
|
|
||||||
|
use crate::{get_app_handle, init::lifecycle::handle_disasterous_failure, lock_r, state::FDOLL};
|
||||||
|
|
||||||
|
/// Emit data to WebSocket server
|
||||||
|
///
|
||||||
|
/// Handles client acquisition, initialization checks, blocking emit, and error handling.
|
||||||
|
/// Returns Ok(()) on success, Err with message on failure.
|
||||||
|
pub async fn ws_emit<T: Serialize + Send + 'static>(
|
||||||
|
event: &'static str,
|
||||||
|
payload: T,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let (client_opt, is_initialized) = {
|
||||||
|
let guard = lock_r!(FDOLL);
|
||||||
|
if let Some(clients) = &guard.network.clients {
|
||||||
|
(
|
||||||
|
clients.ws_client.as_ref().cloned(),
|
||||||
|
clients.is_ws_initialized,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(None, false)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(client) = client_opt else {
|
||||||
|
return Ok(()); // Client not available, silent skip
|
||||||
|
};
|
||||||
|
|
||||||
|
if !is_initialized {
|
||||||
|
return Ok(()); // Not initialized yet, silent skip
|
||||||
|
}
|
||||||
|
|
||||||
|
let payload_value = serde_json::to_value(&payload)
|
||||||
|
.map_err(|e| format!("Failed to serialize payload: {}", e))?;
|
||||||
|
|
||||||
|
match async_runtime::spawn_blocking(move || {
|
||||||
|
client.emit(event, Payload::Text(vec![payload_value]))
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(Ok(_)) => Ok(()),
|
||||||
|
Ok(Err(e)) => {
|
||||||
|
let err_msg = format!("WebSocket emit failed: {}", e);
|
||||||
|
error!("{}", err_msg);
|
||||||
|
handle_disasterous_failure(Some(err_msg.clone())).await;
|
||||||
|
Err(err_msg)
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
let err_msg = format!("WebSocket task failed: {}", e);
|
||||||
|
error!("Failed to execute blocking task for {}: {}", event, e);
|
||||||
|
handle_disasterous_failure(Some(err_msg.clone())).await;
|
||||||
|
Err(err_msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Emit event to frontend (Tauri window)
|
||||||
|
///
|
||||||
|
/// Handles error logging consistently.
|
||||||
|
pub fn emit_to_frontend<T: Serialize + Clone>(event: &str, payload: T) {
|
||||||
|
if let Err(e) = get_app_handle().emit(event, payload) {
|
||||||
|
error!("Failed to emit {} event to frontend: {:?}", event, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,191 +1,106 @@
|
|||||||
use rust_socketio::{Payload, RawClient};
|
use rust_socketio::{Payload, RawClient};
|
||||||
use tauri::Emitter;
|
use tracing::info;
|
||||||
use tracing::{error, info};
|
|
||||||
|
|
||||||
use crate::{
|
use crate::services::cursor::{normalized_to_absolute, CursorPositions};
|
||||||
get_app_handle,
|
use crate::state::AppDataRefreshScope;
|
||||||
services::cursor::{normalized_to_absolute, CursorPositions},
|
|
||||||
state::{init_app_data_scoped, AppDataRefreshScope},
|
use super::{
|
||||||
|
emitter, refresh,
|
||||||
|
types::{IncomingFriendCursorPayload, OutgoingFriendCursorPayload, WS_EVENT},
|
||||||
|
utils,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{IncomingFriendCursorPayload, OutgoingFriendCursorPayload, WS_EVENT};
|
/// Handler for friend-request-received event
|
||||||
|
|
||||||
pub fn on_friend_request_received(payload: Payload, _socket: RawClient) {
|
pub fn on_friend_request_received(payload: Payload, _socket: RawClient) {
|
||||||
match payload {
|
if let Ok(value) = utils::extract_text_value(payload, "friend-request-received") {
|
||||||
Payload::Text(str) => {
|
emitter::emit_to_frontend(WS_EVENT::FRIEND_REQUEST_RECEIVED, value);
|
||||||
println!("Received friend request: {:?}", str);
|
|
||||||
if let Err(e) = get_app_handle().emit(WS_EVENT::FRIEND_REQUEST_RECEIVED, str) {
|
|
||||||
error!("Failed to emit friend request received event: {:?}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => error!("Received unexpected payload format for friend request received"),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Handler for friend-request-accepted event
|
||||||
pub fn on_friend_request_accepted(payload: Payload, _socket: RawClient) {
|
pub fn on_friend_request_accepted(payload: Payload, _socket: RawClient) {
|
||||||
match payload {
|
if let Ok(value) = utils::extract_text_value(payload, "friend-request-accepted") {
|
||||||
Payload::Text(str) => {
|
emitter::emit_to_frontend(WS_EVENT::FRIEND_REQUEST_ACCEPTED, value);
|
||||||
println!("Received friend request accepted: {:?}", str);
|
refresh::refresh_app_data(AppDataRefreshScope::Friends);
|
||||||
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"),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Handler for friend-request-denied event
|
||||||
pub fn on_friend_request_denied(payload: Payload, _socket: RawClient) {
|
pub fn on_friend_request_denied(payload: Payload, _socket: RawClient) {
|
||||||
match payload {
|
if let Ok(value) = utils::extract_text_value(payload, "friend-request-denied") {
|
||||||
Payload::Text(str) => {
|
emitter::emit_to_frontend(WS_EVENT::FRIEND_REQUEST_DENIED, value);
|
||||||
println!("Received friend request denied: {:?}", str);
|
|
||||||
if let Err(e) = get_app_handle().emit(WS_EVENT::FRIEND_REQUEST_DENIED, str) {
|
|
||||||
error!("Failed to emit friend request denied event: {:?}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => error!("Received unexpected payload format for friend request denied"),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Handler for unfriended event
|
||||||
pub fn on_unfriended(payload: Payload, _socket: RawClient) {
|
pub fn on_unfriended(payload: Payload, _socket: RawClient) {
|
||||||
match payload {
|
if let Ok(value) = utils::extract_text_value(payload, "unfriended") {
|
||||||
Payload::Text(str) => {
|
emitter::emit_to_frontend(WS_EVENT::UNFRIENDED, value);
|
||||||
println!("Received unfriended: {:?}", str);
|
refresh::refresh_app_data(AppDataRefreshScope::Friends);
|
||||||
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"),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Handler for friend-cursor-position event
|
||||||
pub fn on_friend_cursor_position(payload: Payload, _socket: RawClient) {
|
pub fn on_friend_cursor_position(payload: Payload, _socket: RawClient) {
|
||||||
match payload {
|
if let Ok(friend_data) =
|
||||||
Payload::Text(values) => {
|
utils::extract_and_parse::<IncomingFriendCursorPayload>(payload, "friend-cursor-position")
|
||||||
// values is Vec<serde_json::Value>
|
{
|
||||||
if let Some(first_value) = values.first() {
|
|
||||||
let incoming_data: Result<IncomingFriendCursorPayload, _> =
|
|
||||||
serde_json::from_value(first_value.clone());
|
|
||||||
|
|
||||||
match incoming_data {
|
|
||||||
Ok(friend_data) => {
|
|
||||||
// We received normalized coordinates (mapped)
|
|
||||||
let mapped_pos = &friend_data.position;
|
let mapped_pos = &friend_data.position;
|
||||||
|
|
||||||
// Convert normalized coordinates back to absolute screen coordinates (raw)
|
|
||||||
let raw_pos = normalized_to_absolute(mapped_pos);
|
let raw_pos = normalized_to_absolute(mapped_pos);
|
||||||
|
|
||||||
let outgoing_payload = OutgoingFriendCursorPayload {
|
let outgoing_payload = OutgoingFriendCursorPayload {
|
||||||
user_id: friend_data.user_id.clone(),
|
user_id: friend_data.user_id,
|
||||||
position: CursorPositions {
|
position: CursorPositions {
|
||||||
raw: raw_pos,
|
raw: raw_pos,
|
||||||
mapped: mapped_pos.clone(),
|
mapped: mapped_pos.clone(),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Err(e) = get_app_handle()
|
emitter::emit_to_frontend(WS_EVENT::FRIEND_CURSOR_POSITION, outgoing_payload);
|
||||||
.emit(WS_EVENT::FRIEND_CURSOR_POSITION, outgoing_payload)
|
|
||||||
{
|
|
||||||
error!("Failed to emit friend cursor position event: {:?}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
error!("Failed to parse friend cursor position data: {}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
error!("Received empty text payload for friend cursor position");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => error!("Received unexpected payload format for friend cursor position"),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Handler for friend-disconnected event
|
||||||
pub fn on_friend_disconnected(payload: Payload, _socket: RawClient) {
|
pub fn on_friend_disconnected(payload: Payload, _socket: RawClient) {
|
||||||
match payload {
|
if let Ok(value) = utils::extract_text_value(payload, "friend-disconnected") {
|
||||||
Payload::Text(str) => {
|
emitter::emit_to_frontend(WS_EVENT::FRIEND_DISCONNECTED, value);
|
||||||
println!("Received friend disconnected: {:?}", str);
|
|
||||||
if let Err(e) = get_app_handle().emit(WS_EVENT::FRIEND_DISCONNECTED, str) {
|
|
||||||
error!("Failed to emit friend disconnected event: {:?}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => error!("Received unexpected payload format for friend disconnected"),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Handler for friend-doll-created event
|
||||||
pub fn on_friend_doll_created(payload: Payload, _socket: RawClient) {
|
pub fn on_friend_doll_created(payload: Payload, _socket: RawClient) {
|
||||||
handle_friend_doll_change(WS_EVENT::FRIEND_DOLL_CREATED, payload);
|
handle_friend_doll_change("friend-doll-created", payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Handler for friend-doll-updated event
|
||||||
pub fn on_friend_doll_updated(payload: Payload, _socket: RawClient) {
|
pub fn on_friend_doll_updated(payload: Payload, _socket: RawClient) {
|
||||||
handle_friend_doll_change(WS_EVENT::FRIEND_DOLL_UPDATED, payload);
|
handle_friend_doll_change("friend-doll-updated", payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Handler for friend-doll-deleted event
|
||||||
pub fn on_friend_doll_deleted(payload: Payload, _socket: RawClient) {
|
pub fn on_friend_doll_deleted(payload: Payload, _socket: RawClient) {
|
||||||
handle_friend_doll_change(WS_EVENT::FRIEND_DOLL_DELETED, payload);
|
handle_friend_doll_change("friend-doll-deleted", payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Common handler for friend doll change events
|
||||||
fn handle_friend_doll_change(event_name: &str, payload: Payload) {
|
fn handle_friend_doll_change(event_name: &str, payload: Payload) {
|
||||||
match payload {
|
if let Ok(value) = utils::extract_text_value(payload, event_name) {
|
||||||
Payload::Text(values) => {
|
info!("Friend doll changed: {:?}", value);
|
||||||
if let Some(first_value) = values.first() {
|
// Future: Could emit to frontend or trigger specific actions
|
||||||
info!("Received {} event: {:?}", event_name, first_value);
|
|
||||||
// Future: Trigger re-fetch or emit to frontend
|
|
||||||
} else {
|
|
||||||
info!("Received {} event with empty payload", event_name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => error!("Received unexpected payload format for {}", event_name),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Handler for friend-active-doll-changed event
|
||||||
pub fn on_friend_active_doll_changed(payload: Payload, _socket: RawClient) {
|
pub fn on_friend_active_doll_changed(payload: Payload, _socket: RawClient) {
|
||||||
match payload {
|
if let Ok(value) = utils::extract_text_value(payload, "friend-active-doll-changed") {
|
||||||
Payload::Text(values) => {
|
emitter::emit_to_frontend(WS_EVENT::FRIEND_ACTIVE_DOLL_CHANGED, value);
|
||||||
if let Some(first_value) = values.first() {
|
refresh::refresh_app_data(AppDataRefreshScope::Friends);
|
||||||
info!(
|
|
||||||
"Received friend-active-doll-changed event: {:?}",
|
|
||||||
first_value
|
|
||||||
);
|
|
||||||
if let Err(e) =
|
|
||||||
get_app_handle().emit(WS_EVENT::FRIEND_ACTIVE_DOLL_CHANGED, first_value)
|
|
||||||
{
|
|
||||||
error!("Failed to emit friend-active-doll-changed event: {:?}", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => error!("Received unexpected payload format for friend-active-doll-changed"),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Handler for friend-user-status event
|
||||||
pub fn on_friend_user_status(payload: Payload, _socket: RawClient) {
|
pub fn on_friend_user_status(payload: Payload, _socket: RawClient) {
|
||||||
match payload {
|
if let Ok(value) = utils::extract_text_value(payload, "friend-user-status") {
|
||||||
Payload::Text(values) => {
|
emitter::emit_to_frontend(WS_EVENT::FRIEND_USER_STATUS, value);
|
||||||
if let Some(first_value) = values.first() {
|
|
||||||
if let Err(e) = get_app_handle().emit(WS_EVENT::FRIEND_USER_STATUS, first_value) {
|
|
||||||
error!("Failed to emit friend-user-status event: {:?}", e);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
info!("Received friend-user-status event with empty payload");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => error!("Received unexpected payload format for friend-user-status"),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use rust_socketio::{ClientBuilder, Event};
|
use rust_socketio::{ClientBuilder, Event};
|
||||||
|
|
||||||
use crate::services::ws::WS_EVENT;
|
use super::types::WS_EVENT;
|
||||||
|
|
||||||
pub fn register_event_handlers(builder: ClientBuilder) -> ClientBuilder {
|
pub fn register_event_handlers(builder: ClientBuilder) -> ClientBuilder {
|
||||||
builder
|
builder
|
||||||
@@ -41,7 +41,10 @@ pub fn register_event_handlers(builder: ClientBuilder) -> ClientBuilder {
|
|||||||
WS_EVENT::FRIEND_ACTIVE_DOLL_CHANGED,
|
WS_EVENT::FRIEND_ACTIVE_DOLL_CHANGED,
|
||||||
super::friend::on_friend_active_doll_changed,
|
super::friend::on_friend_active_doll_changed,
|
||||||
)
|
)
|
||||||
.on(WS_EVENT::FRIEND_USER_STATUS, super::friend::on_friend_user_status)
|
.on(
|
||||||
|
WS_EVENT::FRIEND_USER_STATUS,
|
||||||
|
super::friend::on_friend_user_status,
|
||||||
|
)
|
||||||
.on(WS_EVENT::DOLL_CREATED, super::doll::on_doll_created)
|
.on(WS_EVENT::DOLL_CREATED, super::doll::on_doll_created)
|
||||||
.on(WS_EVENT::DOLL_UPDATED, super::doll::on_doll_updated)
|
.on(WS_EVENT::DOLL_UPDATED, super::doll::on_doll_updated)
|
||||||
.on(WS_EVENT::DOLL_DELETED, super::doll::on_doll_deleted)
|
.on(WS_EVENT::DOLL_DELETED, super::doll::on_doll_deleted)
|
||||||
|
|||||||
@@ -1,70 +1,24 @@
|
|||||||
use rust_socketio::{Payload, RawClient};
|
use rust_socketio::{Payload, RawClient};
|
||||||
use tauri::Emitter;
|
|
||||||
use tracing::{error, info};
|
|
||||||
|
|
||||||
use crate::{
|
use crate::models::interaction::{InteractionDeliveryFailedDto, InteractionPayloadDto};
|
||||||
get_app_handle,
|
|
||||||
models::interaction::{InteractionDeliveryFailedDto, InteractionPayloadDto},
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::WS_EVENT;
|
use super::{emitter, types::WS_EVENT, utils};
|
||||||
|
|
||||||
|
/// Handler for interaction-received event
|
||||||
pub fn on_interaction_received(payload: Payload, _socket: RawClient) {
|
pub fn on_interaction_received(payload: Payload, _socket: RawClient) {
|
||||||
match payload {
|
if let Ok(data) =
|
||||||
Payload::Text(values) => {
|
utils::extract_and_parse::<InteractionPayloadDto>(payload, "interaction-received")
|
||||||
if let Some(first_value) = values.first() {
|
|
||||||
info!("Received interaction-received event: {:?}", first_value);
|
|
||||||
|
|
||||||
let interaction_data: Result<InteractionPayloadDto, _> =
|
|
||||||
serde_json::from_value(first_value.clone());
|
|
||||||
|
|
||||||
match interaction_data {
|
|
||||||
Ok(data) => {
|
|
||||||
if let Err(e) = get_app_handle().emit(WS_EVENT::INTERACTION_RECEIVED, data)
|
|
||||||
{
|
{
|
||||||
error!("Failed to emit interaction-received event: {:?}", e);
|
emitter::emit_to_frontend(WS_EVENT::INTERACTION_RECEIVED, data);
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
error!("Failed to parse interaction payload: {}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
info!("Received interaction-received event with empty payload");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => error!("Received unexpected payload format for interaction-received"),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Handler for interaction-delivery-failed event
|
||||||
pub fn on_interaction_delivery_failed(payload: Payload, _socket: RawClient) {
|
pub fn on_interaction_delivery_failed(payload: Payload, _socket: RawClient) {
|
||||||
match payload {
|
if let Ok(data) = utils::extract_and_parse::<InteractionDeliveryFailedDto>(
|
||||||
Payload::Text(values) => {
|
payload,
|
||||||
if let Some(first_value) = values.first() {
|
"interaction-delivery-failed",
|
||||||
info!(
|
) {
|
||||||
"Received interaction-delivery-failed event: {:?}",
|
emitter::emit_to_frontend(WS_EVENT::INTERACTION_DELIVERY_FAILED, data);
|
||||||
first_value
|
|
||||||
);
|
|
||||||
|
|
||||||
let failure_data: Result<InteractionDeliveryFailedDto, _> =
|
|
||||||
serde_json::from_value(first_value.clone());
|
|
||||||
|
|
||||||
match failure_data {
|
|
||||||
Ok(data) => {
|
|
||||||
if let Err(e) =
|
|
||||||
get_app_handle().emit(WS_EVENT::INTERACTION_DELIVERY_FAILED, data)
|
|
||||||
{
|
|
||||||
error!("Failed to emit interaction-delivery-failed event: {:?}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
error!("Failed to parse interaction failure payload: {}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
info!("Received interaction-delivery-failed event with empty payload");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => error!("Received unexpected payload format for interaction-delivery-failed"),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,54 +1,32 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
/// WebSocket module for real-time communication
|
||||||
|
///
|
||||||
#[allow(non_camel_case_types)] // pretend to be a const like in js
|
/// Organized into focused submodules:
|
||||||
pub struct WS_EVENT;
|
/// - types: Event constants and payload structures
|
||||||
|
/// - utils: Common payload handling and parsing utilities
|
||||||
#[derive(Debug, Deserialize)]
|
/// - emitter: WebSocket and frontend event emission
|
||||||
pub struct IncomingFriendCursorPayload {
|
/// - refresh: Data refresh orchestration
|
||||||
#[serde(rename = "userId")]
|
/// - handlers: Event handler registration
|
||||||
user_id: String,
|
/// - connection: Connection lifecycle handlers
|
||||||
position: crate::services::cursor::CursorPosition,
|
/// - doll: Doll-related event handlers
|
||||||
}
|
/// - friend: Friend-related event handlers
|
||||||
|
/// - interaction: Interaction event handlers
|
||||||
#[derive(Clone, Serialize)]
|
/// - cursor: Cursor position reporting
|
||||||
#[serde(rename_all = "camelCase")]
|
/// - user_status: User status reporting
|
||||||
pub struct OutgoingFriendCursorPayload {
|
|
||||||
user_id: String,
|
|
||||||
position: crate::services::cursor::CursorPositions,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WS_EVENT {
|
|
||||||
pub const CURSOR_REPORT_POSITION: &str = "cursor-report-position";
|
|
||||||
pub const FRIEND_REQUEST_RECEIVED: &str = "friend-request-received";
|
|
||||||
pub const FRIEND_REQUEST_ACCEPTED: &str = "friend-request-accepted";
|
|
||||||
pub const FRIEND_REQUEST_DENIED: &str = "friend-request-denied";
|
|
||||||
pub const UNFRIENDED: &str = "unfriended";
|
|
||||||
pub const FRIEND_CURSOR_POSITION: &str = "friend-cursor-position";
|
|
||||||
pub const FRIEND_DISCONNECTED: &str = "friend-disconnected";
|
|
||||||
pub const FRIEND_DOLL_CREATED: &str = "friend-doll-created";
|
|
||||||
pub const FRIEND_DOLL_UPDATED: &str = "friend-doll-updated";
|
|
||||||
pub const FRIEND_DOLL_DELETED: &str = "friend-doll-deleted";
|
|
||||||
pub const FRIEND_ACTIVE_DOLL_CHANGED: &str = "friend-active-doll-changed";
|
|
||||||
pub const FRIEND_USER_STATUS: &str = "friend-user-status";
|
|
||||||
pub const CLIENT_REPORT_USER_STATUS: &str = "client-report-user-status";
|
|
||||||
pub const DOLL_CREATED: &str = "doll_created";
|
|
||||||
pub const DOLL_UPDATED: &str = "doll_updated";
|
|
||||||
pub const DOLL_DELETED: &str = "doll_deleted";
|
|
||||||
pub const CLIENT_INITIALIZE: &str = "client-initialize";
|
|
||||||
pub const INITIALIZED: &str = "initialized";
|
|
||||||
pub const INTERACTION_RECEIVED: &str = "interaction-received";
|
|
||||||
pub const INTERACTION_DELIVERY_FAILED: &str = "interaction-delivery-failed";
|
|
||||||
pub const CLIENT_SEND_INTERACTION: &str = "client-send-interaction";
|
|
||||||
}
|
|
||||||
|
|
||||||
pub mod client;
|
|
||||||
mod connection;
|
mod connection;
|
||||||
mod cursor;
|
mod cursor;
|
||||||
mod doll;
|
mod doll;
|
||||||
|
mod emitter;
|
||||||
mod friend;
|
mod friend;
|
||||||
mod handlers;
|
mod handlers;
|
||||||
mod interaction;
|
mod interaction;
|
||||||
|
mod refresh;
|
||||||
|
mod types;
|
||||||
mod user_status;
|
mod user_status;
|
||||||
|
mod utils;
|
||||||
|
|
||||||
|
pub mod client;
|
||||||
|
|
||||||
|
// Re-export public API
|
||||||
pub use cursor::report_cursor_data;
|
pub use cursor::report_cursor_data;
|
||||||
|
pub use types::WS_EVENT;
|
||||||
pub use user_status::{report_user_status, UserStatusPayload};
|
pub use user_status::{report_user_status, UserStatusPayload};
|
||||||
|
|||||||
33
src-tauri/src/services/ws/refresh.rs
Normal file
33
src-tauri/src/services/ws/refresh.rs
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
use tauri::async_runtime;
|
||||||
|
|
||||||
|
use crate::state::{init_app_data_scoped, AppDataRefreshScope};
|
||||||
|
|
||||||
|
/// Refresh app data with the given scope
|
||||||
|
pub fn refresh_app_data(scope: AppDataRefreshScope) {
|
||||||
|
async_runtime::spawn(async move {
|
||||||
|
init_app_data_scoped(scope).await;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Refresh multiple scopes sequentially
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn refresh_app_data_multi(scopes: Vec<AppDataRefreshScope>) {
|
||||||
|
async_runtime::spawn(async move {
|
||||||
|
for scope in scopes {
|
||||||
|
init_app_data_scoped(scope).await;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Refresh dolls and optionally user/friends if doll was active
|
||||||
|
pub fn refresh_with_active_doll_check(doll_id: Option<&str>) {
|
||||||
|
let is_active = doll_id.map(super::utils::is_active_doll).unwrap_or(false);
|
||||||
|
|
||||||
|
async_runtime::spawn(async move {
|
||||||
|
init_app_data_scoped(AppDataRefreshScope::Dolls).await;
|
||||||
|
if is_active {
|
||||||
|
init_app_data_scoped(AppDataRefreshScope::User).await;
|
||||||
|
init_app_data_scoped(AppDataRefreshScope::Friends).await;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
45
src-tauri/src/services/ws/types.rs
Normal file
45
src-tauri/src/services/ws/types.rs
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
/// WebSocket event constants
|
||||||
|
#[allow(non_camel_case_types)]
|
||||||
|
pub struct WS_EVENT;
|
||||||
|
|
||||||
|
impl WS_EVENT {
|
||||||
|
pub const CURSOR_REPORT_POSITION: &str = "cursor-report-position";
|
||||||
|
pub const FRIEND_REQUEST_RECEIVED: &str = "friend-request-received";
|
||||||
|
pub const FRIEND_REQUEST_ACCEPTED: &str = "friend-request-accepted";
|
||||||
|
pub const FRIEND_REQUEST_DENIED: &str = "friend-request-denied";
|
||||||
|
pub const UNFRIENDED: &str = "unfriended";
|
||||||
|
pub const FRIEND_CURSOR_POSITION: &str = "friend-cursor-position";
|
||||||
|
pub const FRIEND_DISCONNECTED: &str = "friend-disconnected";
|
||||||
|
pub const FRIEND_DOLL_CREATED: &str = "friend-doll-created";
|
||||||
|
pub const FRIEND_DOLL_UPDATED: &str = "friend-doll-updated";
|
||||||
|
pub const FRIEND_DOLL_DELETED: &str = "friend-doll-deleted";
|
||||||
|
pub const FRIEND_ACTIVE_DOLL_CHANGED: &str = "friend-active-doll-changed";
|
||||||
|
pub const FRIEND_USER_STATUS: &str = "friend-user-status";
|
||||||
|
pub const CLIENT_REPORT_USER_STATUS: &str = "client-report-user-status";
|
||||||
|
pub const DOLL_CREATED: &str = "doll_created";
|
||||||
|
pub const DOLL_UPDATED: &str = "doll_updated";
|
||||||
|
pub const DOLL_DELETED: &str = "doll_deleted";
|
||||||
|
pub const CLIENT_INITIALIZE: &str = "client-initialize";
|
||||||
|
pub const INITIALIZED: &str = "initialized";
|
||||||
|
pub const INTERACTION_RECEIVED: &str = "interaction-received";
|
||||||
|
pub const INTERACTION_DELIVERY_FAILED: &str = "interaction-delivery-failed";
|
||||||
|
pub const CLIENT_SEND_INTERACTION: &str = "client-send-interaction";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Incoming friend cursor position from WebSocket
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct IncomingFriendCursorPayload {
|
||||||
|
#[serde(rename = "userId")]
|
||||||
|
pub user_id: String,
|
||||||
|
pub position: crate::services::cursor::CursorPosition,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Outgoing friend cursor position to frontend
|
||||||
|
#[derive(Clone, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct OutgoingFriendCursorPayload {
|
||||||
|
pub user_id: String,
|
||||||
|
pub position: crate::services::cursor::CursorPositions,
|
||||||
|
}
|
||||||
@@ -1,86 +1,39 @@
|
|||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use rust_socketio::Payload;
|
use serde::Serialize;
|
||||||
use tauri::async_runtime::{self};
|
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
use tokio::task::JoinHandle;
|
use tokio::task::JoinHandle;
|
||||||
use tokio::time::Duration;
|
use tokio::time::Duration;
|
||||||
use tracing::error;
|
|
||||||
|
|
||||||
use crate::{init::lifecycle::handle_disasterous_failure, lock_r, state::FDOLL};
|
|
||||||
use crate::services::active_app::AppMetadata;
|
use crate::services::active_app::AppMetadata;
|
||||||
|
|
||||||
use super::WS_EVENT;
|
use super::{emitter, types::WS_EVENT};
|
||||||
|
|
||||||
#[derive(Clone, serde::Serialize)]
|
/// User status payload sent to WebSocket server
|
||||||
|
#[derive(Clone, Serialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct UserStatusPayload {
|
pub struct UserStatusPayload {
|
||||||
pub app_metadata: AppMetadata,
|
pub app_metadata: AppMetadata,
|
||||||
pub state: String,
|
pub state: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Debouncer for user status reports
|
||||||
static USER_STATUS_REPORT_DEBOUNCE: Lazy<Mutex<Option<JoinHandle<()>>>> =
|
static USER_STATUS_REPORT_DEBOUNCE: Lazy<Mutex<Option<JoinHandle<()>>>> =
|
||||||
Lazy::new(|| Mutex::new(None));
|
Lazy::new(|| Mutex::new(None));
|
||||||
|
|
||||||
|
/// Report user status to WebSocket server with debouncing
|
||||||
pub async fn report_user_status(status: UserStatusPayload) {
|
pub async fn report_user_status(status: UserStatusPayload) {
|
||||||
let payload_value = match serde_json::to_value(&status) {
|
|
||||||
Ok(val) => val,
|
|
||||||
Err(e) => {
|
|
||||||
error!("Failed to serialize user status payload: {}", e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let (client_opt, is_initialized) = {
|
|
||||||
let guard = lock_r!(FDOLL);
|
|
||||||
if let Some(clients) = &guard.network.clients {
|
|
||||||
(
|
|
||||||
clients.ws_client.as_ref().cloned(),
|
|
||||||
clients.is_ws_initialized,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
(None, false)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
{
|
|
||||||
let mut debouncer = USER_STATUS_REPORT_DEBOUNCE.lock().await;
|
let mut debouncer = USER_STATUS_REPORT_DEBOUNCE.lock().await;
|
||||||
|
|
||||||
|
// Cancel previous pending report
|
||||||
if let Some(handle) = debouncer.take() {
|
if let Some(handle) = debouncer.take() {
|
||||||
handle.abort();
|
handle.abort();
|
||||||
}
|
}
|
||||||
let payload_value_clone = payload_value.clone();
|
|
||||||
let client_opt_clone = client_opt.clone();
|
// Schedule new report after 500ms
|
||||||
let is_initialized_clone = is_initialized;
|
|
||||||
let handle = tokio::spawn(async move {
|
let handle = tokio::spawn(async move {
|
||||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||||
if let Some(client) = client_opt_clone {
|
let _ = emitter::ws_emit(WS_EVENT::CLIENT_REPORT_USER_STATUS, status).await;
|
||||||
if !is_initialized_clone {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
match async_runtime::spawn_blocking(move || {
|
|
||||||
client.emit(
|
|
||||||
WS_EVENT::CLIENT_REPORT_USER_STATUS,
|
|
||||||
Payload::Text(vec![payload_value_clone]),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(Ok(_)) => (),
|
|
||||||
Ok(Err(e)) => {
|
|
||||||
error!("Failed to emit user status report: {}", e);
|
|
||||||
handle_disasterous_failure(Some(format!("WebSocket emit failed: {}", e)))
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
error!(
|
|
||||||
"Failed to execute blocking task for user status report: {}",
|
|
||||||
e
|
|
||||||
);
|
|
||||||
handle_disasterous_failure(Some(format!("WebSocket task failed: {}", e)))
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
*debouncer = Some(handle);
|
*debouncer = Some(handle);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
70
src-tauri/src/services/ws/utils.rs
Normal file
70
src-tauri/src/services/ws/utils.rs
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
use rust_socketio::Payload;
|
||||||
|
use serde::de::DeserializeOwned;
|
||||||
|
use tracing::{error, info};
|
||||||
|
|
||||||
|
/// Result type for payload operations
|
||||||
|
pub type PayloadResult<T> = Result<T, PayloadError>;
|
||||||
|
|
||||||
|
/// Errors that can occur during payload handling
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum PayloadError {
|
||||||
|
InvalidFormat,
|
||||||
|
EmptyPayload,
|
||||||
|
ParseError(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extract the first value from a Text payload
|
||||||
|
pub fn extract_text_value(payload: Payload, event_name: &str) -> PayloadResult<serde_json::Value> {
|
||||||
|
match payload {
|
||||||
|
Payload::Text(values) => {
|
||||||
|
if let Some(first_value) = values.first() {
|
||||||
|
Ok(first_value.clone())
|
||||||
|
} else {
|
||||||
|
Err(PayloadError::EmptyPayload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
error!("Received unexpected payload format for {}", event_name);
|
||||||
|
Err(PayloadError::InvalidFormat)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse payload value into a specific type
|
||||||
|
pub fn parse_payload<T: DeserializeOwned>(
|
||||||
|
value: serde_json::Value,
|
||||||
|
event_name: &str,
|
||||||
|
) -> PayloadResult<T> {
|
||||||
|
serde_json::from_value(value).map_err(|e| {
|
||||||
|
error!("Failed to parse {} payload: {}", event_name, e);
|
||||||
|
PayloadError::ParseError(e.to_string())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extract and parse payload in one step
|
||||||
|
pub fn extract_and_parse<T: DeserializeOwned>(
|
||||||
|
payload: Payload,
|
||||||
|
event_name: &str,
|
||||||
|
) -> PayloadResult<T> {
|
||||||
|
let value = extract_text_value(payload, event_name)?;
|
||||||
|
parse_payload(value, event_name)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if a doll ID matches the current user's active doll
|
||||||
|
pub fn is_active_doll(doll_id: &str) -> bool {
|
||||||
|
use crate::{lock_r, state::FDOLL};
|
||||||
|
|
||||||
|
let guard = lock_r!(FDOLL);
|
||||||
|
guard
|
||||||
|
.user_data
|
||||||
|
.user
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|u| u.active_doll_id.as_ref())
|
||||||
|
.map(|active_id| active_id == doll_id)
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extract doll ID from a JSON value
|
||||||
|
pub fn extract_doll_id(value: &serde_json::Value) -> Option<String> {
|
||||||
|
value.get("id").and_then(|v| v.as_str()).map(String::from)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user