minor websocket refactoring
This commit is contained in:
@@ -2,44 +2,43 @@ use rust_socketio::{Payload, RawClient};
|
||||
use tracing::info;
|
||||
|
||||
use crate::{
|
||||
lock_w,
|
||||
services::health_manager::close_health_manager_window,
|
||||
services::scene::open_scene_window,
|
||||
state::FDOLL,
|
||||
lock_w, services::health_manager::close_health_manager_window,
|
||||
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) {
|
||||
if let Err(e) = socket.emit(WS_EVENT::CLIENT_INITIALIZE, serde_json::json!({})) {
|
||||
tracing::error!("Failed to emit client-initialize: {:?}", e);
|
||||
}
|
||||
}
|
||||
|
||||
/// Handler for WebSocket connection event
|
||||
pub fn on_connected(_payload: Payload, socket: RawClient) {
|
||||
info!("WebSocket connected. Sending initialization request.");
|
||||
emit_initialize(&socket);
|
||||
}
|
||||
|
||||
/// Handler for initialized event
|
||||
pub fn on_initialized(payload: Payload, _socket: RawClient) {
|
||||
match payload {
|
||||
Payload::Text(values) => {
|
||||
if let Some(first_value) = values.first() {
|
||||
info!("Received initialized event: {:?}", first_value);
|
||||
|
||||
// Mark WebSocket as initialized and reset backoff timer
|
||||
let mut guard = lock_w!(FDOLL);
|
||||
if let Some(clients) = guard.network.clients.as_mut() {
|
||||
clients.is_ws_initialized = true;
|
||||
}
|
||||
|
||||
// Connection restored: close health manager and reopen scene
|
||||
close_health_manager_window();
|
||||
open_scene_window();
|
||||
} else {
|
||||
info!("Received initialized event with empty payload");
|
||||
}
|
||||
}
|
||||
_ => tracing::error!("Received unexpected payload format for initialized"),
|
||||
if utils::extract_text_value(payload, "initialized").is_ok() {
|
||||
mark_ws_initialized();
|
||||
restore_connection_ui();
|
||||
}
|
||||
}
|
||||
|
||||
/// Mark WebSocket as initialized in app state
|
||||
fn mark_ws_initialized() {
|
||||
let mut guard = lock_w!(FDOLL);
|
||||
if let Some(clients) = guard.network.clients.as_mut() {
|
||||
clients.is_ws_initialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// Restore UI after successful connection
|
||||
fn restore_connection_ui() {
|
||||
close_health_manager_window();
|
||||
open_scene_window();
|
||||
}
|
||||
|
||||
@@ -1,51 +1,8 @@
|
||||
use rust_socketio::Payload;
|
||||
use tauri::async_runtime;
|
||||
use tracing::error;
|
||||
use crate::services::cursor::CursorPosition;
|
||||
|
||||
use crate::{
|
||||
init::lifecycle::handle_disasterous_failure, lock_r, services::cursor::CursorPosition,
|
||||
state::FDOLL,
|
||||
};
|
||||
|
||||
use super::WS_EVENT;
|
||||
use super::{emitter, types::WS_EVENT};
|
||||
|
||||
/// Report cursor position to WebSocket server
|
||||
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, 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
let _ = emitter::ws_emit(WS_EVENT::CURSOR_REPORT_POSITION, cursor_position).await;
|
||||
}
|
||||
|
||||
@@ -1,101 +1,26 @@
|
||||
use rust_socketio::{Payload, RawClient};
|
||||
use tracing::{error, info};
|
||||
|
||||
use crate::{
|
||||
lock_r,
|
||||
state::{init_app_data_scoped, AppDataRefreshScope, FDOLL},
|
||||
};
|
||||
use super::{refresh, utils};
|
||||
|
||||
/// Handler for doll.created event
|
||||
pub fn on_doll_created(payload: Payload, _socket: RawClient) {
|
||||
match payload {
|
||||
Payload::Text(values) => {
|
||||
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"),
|
||||
if utils::extract_text_value(payload, "doll.created").is_ok() {
|
||||
refresh::refresh_app_data(crate::state::AppDataRefreshScope::Dolls);
|
||||
}
|
||||
}
|
||||
|
||||
/// Handler for doll.updated event
|
||||
pub fn on_doll_updated(payload: Payload, _socket: RawClient) {
|
||||
match payload {
|
||||
Payload::Text(values) => {
|
||||
if let Some(first_value) = values.first() {
|
||||
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"),
|
||||
if let Ok(value) = utils::extract_text_value(payload, "doll.updated") {
|
||||
let doll_id = utils::extract_doll_id(&value);
|
||||
refresh::refresh_with_active_doll_check(doll_id.as_deref());
|
||||
}
|
||||
}
|
||||
|
||||
/// Handler for doll.deleted event
|
||||
pub fn on_doll_deleted(payload: Payload, _socket: RawClient) {
|
||||
match payload {
|
||||
Payload::Text(values) => {
|
||||
if let Some(first_value) = values.first() {
|
||||
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"),
|
||||
if let Ok(value) = utils::extract_text_value(payload, "doll.deleted") {
|
||||
let doll_id = utils::extract_doll_id(&value);
|
||||
refresh::refresh_with_active_doll_check(doll_id.as_deref());
|
||||
}
|
||||
}
|
||||
|
||||
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 tauri::Emitter;
|
||||
use tracing::{error, info};
|
||||
use tracing::info;
|
||||
|
||||
use crate::{
|
||||
get_app_handle,
|
||||
services::cursor::{normalized_to_absolute, CursorPositions},
|
||||
state::{init_app_data_scoped, AppDataRefreshScope},
|
||||
use crate::services::cursor::{normalized_to_absolute, CursorPositions};
|
||||
use crate::state::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) {
|
||||
match payload {
|
||||
Payload::Text(str) => {
|
||||
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"),
|
||||
if let Ok(value) = utils::extract_text_value(payload, "friend-request-received") {
|
||||
emitter::emit_to_frontend(WS_EVENT::FRIEND_REQUEST_RECEIVED, value);
|
||||
}
|
||||
}
|
||||
|
||||
/// Handler for friend-request-accepted event
|
||||
pub fn on_friend_request_accepted(payload: Payload, _socket: RawClient) {
|
||||
match payload {
|
||||
Payload::Text(str) => {
|
||||
println!("Received friend request accepted: {:?}", str);
|
||||
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"),
|
||||
if let Ok(value) = utils::extract_text_value(payload, "friend-request-accepted") {
|
||||
emitter::emit_to_frontend(WS_EVENT::FRIEND_REQUEST_ACCEPTED, value);
|
||||
refresh::refresh_app_data(AppDataRefreshScope::Friends);
|
||||
}
|
||||
}
|
||||
|
||||
/// Handler for friend-request-denied event
|
||||
pub fn on_friend_request_denied(payload: Payload, _socket: RawClient) {
|
||||
match payload {
|
||||
Payload::Text(str) => {
|
||||
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"),
|
||||
if let Ok(value) = utils::extract_text_value(payload, "friend-request-denied") {
|
||||
emitter::emit_to_frontend(WS_EVENT::FRIEND_REQUEST_DENIED, value);
|
||||
}
|
||||
}
|
||||
|
||||
/// Handler for unfriended event
|
||||
pub fn on_unfriended(payload: Payload, _socket: RawClient) {
|
||||
match payload {
|
||||
Payload::Text(str) => {
|
||||
println!("Received unfriended: {:?}", str);
|
||||
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"),
|
||||
if let Ok(value) = utils::extract_text_value(payload, "unfriended") {
|
||||
emitter::emit_to_frontend(WS_EVENT::UNFRIENDED, value);
|
||||
refresh::refresh_app_data(AppDataRefreshScope::Friends);
|
||||
}
|
||||
}
|
||||
|
||||
/// Handler for friend-cursor-position event
|
||||
pub fn on_friend_cursor_position(payload: Payload, _socket: RawClient) {
|
||||
match payload {
|
||||
Payload::Text(values) => {
|
||||
// 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());
|
||||
if let Ok(friend_data) =
|
||||
utils::extract_and_parse::<IncomingFriendCursorPayload>(payload, "friend-cursor-position")
|
||||
{
|
||||
let mapped_pos = &friend_data.position;
|
||||
let raw_pos = normalized_to_absolute(mapped_pos);
|
||||
|
||||
match incoming_data {
|
||||
Ok(friend_data) => {
|
||||
// We received normalized coordinates (mapped)
|
||||
let mapped_pos = &friend_data.position;
|
||||
let outgoing_payload = OutgoingFriendCursorPayload {
|
||||
user_id: friend_data.user_id,
|
||||
position: CursorPositions {
|
||||
raw: raw_pos,
|
||||
mapped: mapped_pos.clone(),
|
||||
},
|
||||
};
|
||||
|
||||
// Convert normalized coordinates back to absolute screen coordinates (raw)
|
||||
let raw_pos = normalized_to_absolute(mapped_pos);
|
||||
|
||||
let outgoing_payload = OutgoingFriendCursorPayload {
|
||||
user_id: friend_data.user_id.clone(),
|
||||
position: CursorPositions {
|
||||
raw: raw_pos,
|
||||
mapped: mapped_pos.clone(),
|
||||
},
|
||||
};
|
||||
|
||||
if let Err(e) = get_app_handle()
|
||||
.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"),
|
||||
emitter::emit_to_frontend(WS_EVENT::FRIEND_CURSOR_POSITION, outgoing_payload);
|
||||
}
|
||||
}
|
||||
|
||||
/// Handler for friend-disconnected event
|
||||
pub fn on_friend_disconnected(payload: Payload, _socket: RawClient) {
|
||||
match payload {
|
||||
Payload::Text(str) => {
|
||||
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"),
|
||||
if let Ok(value) = utils::extract_text_value(payload, "friend-disconnected") {
|
||||
emitter::emit_to_frontend(WS_EVENT::FRIEND_DISCONNECTED, value);
|
||||
}
|
||||
}
|
||||
|
||||
/// Handler for friend-doll-created event
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
match payload {
|
||||
Payload::Text(values) => {
|
||||
if let Some(first_value) = values.first() {
|
||||
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),
|
||||
if let Ok(value) = utils::extract_text_value(payload, event_name) {
|
||||
info!("Friend doll changed: {:?}", value);
|
||||
// Future: Could emit to frontend or trigger specific actions
|
||||
}
|
||||
}
|
||||
|
||||
/// Handler for friend-active-doll-changed event
|
||||
pub fn on_friend_active_doll_changed(payload: Payload, _socket: RawClient) {
|
||||
match payload {
|
||||
Payload::Text(values) => {
|
||||
if let Some(first_value) = values.first() {
|
||||
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"),
|
||||
if let Ok(value) = utils::extract_text_value(payload, "friend-active-doll-changed") {
|
||||
emitter::emit_to_frontend(WS_EVENT::FRIEND_ACTIVE_DOLL_CHANGED, value);
|
||||
refresh::refresh_app_data(AppDataRefreshScope::Friends);
|
||||
}
|
||||
}
|
||||
|
||||
/// Handler for friend-user-status event
|
||||
pub fn on_friend_user_status(payload: Payload, _socket: RawClient) {
|
||||
match payload {
|
||||
Payload::Text(values) => {
|
||||
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"),
|
||||
if let Ok(value) = utils::extract_text_value(payload, "friend-user-status") {
|
||||
emitter::emit_to_frontend(WS_EVENT::FRIEND_USER_STATUS, value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use rust_socketio::{ClientBuilder, Event};
|
||||
|
||||
use crate::services::ws::WS_EVENT;
|
||||
use super::types::WS_EVENT;
|
||||
|
||||
pub fn register_event_handlers(builder: ClientBuilder) -> ClientBuilder {
|
||||
builder
|
||||
@@ -41,7 +41,10 @@ pub fn register_event_handlers(builder: ClientBuilder) -> ClientBuilder {
|
||||
WS_EVENT::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_UPDATED, super::doll::on_doll_updated)
|
||||
.on(WS_EVENT::DOLL_DELETED, super::doll::on_doll_deleted)
|
||||
|
||||
@@ -1,70 +1,24 @@
|
||||
use rust_socketio::{Payload, RawClient};
|
||||
use tauri::Emitter;
|
||||
use tracing::{error, info};
|
||||
|
||||
use crate::{
|
||||
get_app_handle,
|
||||
models::interaction::{InteractionDeliveryFailedDto, InteractionPayloadDto},
|
||||
};
|
||||
use crate::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) {
|
||||
match payload {
|
||||
Payload::Text(values) => {
|
||||
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);
|
||||
}
|
||||
}
|
||||
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"),
|
||||
if let Ok(data) =
|
||||
utils::extract_and_parse::<InteractionPayloadDto>(payload, "interaction-received")
|
||||
{
|
||||
emitter::emit_to_frontend(WS_EVENT::INTERACTION_RECEIVED, data);
|
||||
}
|
||||
}
|
||||
|
||||
/// Handler for interaction-delivery-failed event
|
||||
pub fn on_interaction_delivery_failed(payload: Payload, _socket: RawClient) {
|
||||
match payload {
|
||||
Payload::Text(values) => {
|
||||
if let Some(first_value) = values.first() {
|
||||
info!(
|
||||
"Received interaction-delivery-failed event: {:?}",
|
||||
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"),
|
||||
if let Ok(data) = utils::extract_and_parse::<InteractionDeliveryFailedDto>(
|
||||
payload,
|
||||
"interaction-delivery-failed",
|
||||
) {
|
||||
emitter::emit_to_frontend(WS_EVENT::INTERACTION_DELIVERY_FAILED, data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,54 +1,32 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[allow(non_camel_case_types)] // pretend to be a const like in js
|
||||
pub struct WS_EVENT;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct IncomingFriendCursorPayload {
|
||||
#[serde(rename = "userId")]
|
||||
user_id: String,
|
||||
position: crate::services::cursor::CursorPosition,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
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;
|
||||
/// WebSocket module for real-time communication
|
||||
///
|
||||
/// Organized into focused submodules:
|
||||
/// - types: Event constants and payload structures
|
||||
/// - utils: Common payload handling and parsing utilities
|
||||
/// - emitter: WebSocket and frontend event emission
|
||||
/// - refresh: Data refresh orchestration
|
||||
/// - handlers: Event handler registration
|
||||
/// - connection: Connection lifecycle handlers
|
||||
/// - doll: Doll-related event handlers
|
||||
/// - friend: Friend-related event handlers
|
||||
/// - interaction: Interaction event handlers
|
||||
/// - cursor: Cursor position reporting
|
||||
/// - user_status: User status reporting
|
||||
mod connection;
|
||||
mod cursor;
|
||||
mod doll;
|
||||
mod emitter;
|
||||
mod friend;
|
||||
mod handlers;
|
||||
mod interaction;
|
||||
mod refresh;
|
||||
mod types;
|
||||
mod user_status;
|
||||
mod utils;
|
||||
|
||||
pub mod client;
|
||||
|
||||
// Re-export public API
|
||||
pub use cursor::report_cursor_data;
|
||||
pub use types::WS_EVENT;
|
||||
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 rust_socketio::Payload;
|
||||
use tauri::async_runtime::{self};
|
||||
use serde::Serialize;
|
||||
use tokio::sync::Mutex;
|
||||
use tokio::task::JoinHandle;
|
||||
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 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")]
|
||||
pub struct UserStatusPayload {
|
||||
pub app_metadata: AppMetadata,
|
||||
pub state: String,
|
||||
}
|
||||
|
||||
/// Debouncer for user status reports
|
||||
static USER_STATUS_REPORT_DEBOUNCE: Lazy<Mutex<Option<JoinHandle<()>>>> =
|
||||
Lazy::new(|| Mutex::new(None));
|
||||
|
||||
/// Report user status to WebSocket server with debouncing
|
||||
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 mut debouncer = USER_STATUS_REPORT_DEBOUNCE.lock().await;
|
||||
|
||||
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;
|
||||
if let Some(handle) = debouncer.take() {
|
||||
handle.abort();
|
||||
}
|
||||
let payload_value_clone = payload_value.clone();
|
||||
let client_opt_clone = client_opt.clone();
|
||||
let is_initialized_clone = is_initialized;
|
||||
let handle = tokio::spawn(async move {
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
if let Some(client) = client_opt_clone {
|
||||
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);
|
||||
// Cancel previous pending report
|
||||
if let Some(handle) = debouncer.take() {
|
||||
handle.abort();
|
||||
}
|
||||
|
||||
// Schedule new report after 500ms
|
||||
let handle = tokio::spawn(async move {
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
let _ = emitter::ws_emit(WS_EVENT::CLIENT_REPORT_USER_STATUS, status).await;
|
||||
});
|
||||
|
||||
*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