From f65d8378417679487c8ef6dd325f1a0b81e47931 Mon Sep 17 00:00:00 2001 From: Wind-Explorer Date: Sat, 7 Mar 2026 14:48:19 +0800 Subject: [PATCH] Improved tauri events type safety --- src-tauri/src/models/dolls.rs | 2 +- src-tauri/src/models/event_payloads.rs | 79 ++++++++++++++++ src-tauri/src/models/health.rs | 2 +- src-tauri/src/models/mod.rs | 3 +- .../src/services/presence_modules/runtime.rs | 7 +- src-tauri/src/services/ws/friend.rs | 46 +++++++--- src-tauri/src/services/ws/user_status.rs | 11 +-- src/events/friend-cursor.ts | 89 ++++++++----------- src/events/listener-utils.ts | 16 ---- src/events/user-status.ts | 63 +++++-------- src/routes/scene/components/debug-bar.svelte | 4 +- .../FriendActiveDollChangedPayload.ts | 4 + .../bindings/FriendDisconnectedPayload.ts | 3 + .../bindings/FriendRequestAcceptedPayload.ts | 4 + .../bindings/FriendRequestDeniedPayload.ts | 4 + .../bindings/FriendRequestReceivedPayload.ts | 4 + src/types/bindings/FriendUserStatusPayload.ts | 4 + src/types/bindings/UnfriendedPayload.ts | 3 + src/types/bindings/UserStatusPayload.ts | 5 ++ src/types/bindings/UserStatusState.ts | 3 + 20 files changed, 215 insertions(+), 141 deletions(-) create mode 100644 src-tauri/src/models/event_payloads.rs create mode 100644 src/types/bindings/FriendActiveDollChangedPayload.ts create mode 100644 src/types/bindings/FriendDisconnectedPayload.ts create mode 100644 src/types/bindings/FriendRequestAcceptedPayload.ts create mode 100644 src/types/bindings/FriendRequestDeniedPayload.ts create mode 100644 src/types/bindings/FriendRequestReceivedPayload.ts create mode 100644 src/types/bindings/FriendUserStatusPayload.ts create mode 100644 src/types/bindings/UnfriendedPayload.ts create mode 100644 src/types/bindings/UserStatusPayload.ts create mode 100644 src/types/bindings/UserStatusState.ts diff --git a/src-tauri/src/models/dolls.rs b/src-tauri/src/models/dolls.rs index 4d5c935..45b3e93 100644 --- a/src-tauri/src/models/dolls.rs +++ b/src-tauri/src/models/dolls.rs @@ -41,4 +41,4 @@ pub struct DollDto { pub configuration: DollConfigurationDto, pub created_at: String, pub updated_at: String, -} \ No newline at end of file +} diff --git a/src-tauri/src/models/event_payloads.rs b/src-tauri/src/models/event_payloads.rs new file mode 100644 index 0000000..39800d4 --- /dev/null +++ b/src-tauri/src/models/event_payloads.rs @@ -0,0 +1,79 @@ +use serde::{Deserialize, Serialize}; +use ts_rs::TS; + +use super::dolls::DollDto; +use super::friends::UserBasicDto; +use crate::services::presence_modules::models::PresenceStatus; + +#[derive(Clone, Serialize, Deserialize, Debug, TS)] +#[serde(rename_all = "lowercase")] +#[ts(export)] +pub enum UserStatusState { + Idle, + Resting, +} + +#[derive(Clone, Serialize, Deserialize, Debug, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct UserStatusPayload { + pub presence_status: PresenceStatus, + pub state: UserStatusState, +} + +#[derive(Clone, Serialize, Deserialize, Debug, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct FriendUserStatusPayload { + pub user_id: String, + pub status: UserStatusPayload, +} + +#[derive(Clone, Serialize, Deserialize, Debug, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct FriendDisconnectedPayload { + pub user_id: String, +} + +#[derive(Clone, Serialize, Deserialize, Debug, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct FriendActiveDollChangedPayload { + pub friend_id: String, + pub doll: Option, +} + +#[derive(Clone, Serialize, Deserialize, Debug, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct FriendRequestReceivedPayload { + pub id: String, + pub sender: UserBasicDto, + pub created_at: String, +} + +#[derive(Clone, Serialize, Deserialize, Debug, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct FriendRequestAcceptedPayload { + pub id: String, + pub friend: UserBasicDto, + pub accepted_at: String, +} + +#[derive(Clone, Serialize, Deserialize, Debug, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct FriendRequestDeniedPayload { + pub id: String, + pub denier: UserBasicDto, + pub denied_at: String, +} + +#[derive(Clone, Serialize, Deserialize, Debug, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct UnfriendedPayload { + pub friend_id: String, +} diff --git a/src-tauri/src/models/health.rs b/src-tauri/src/models/health.rs index c2bf5e5..ce55c44 100644 --- a/src-tauri/src/models/health.rs +++ b/src-tauri/src/models/health.rs @@ -25,4 +25,4 @@ pub enum HealthError { NonOkStatus(String), #[error("health response decode failed: {0}")] Decode(reqwest::Error), -} \ No newline at end of file +} diff --git a/src-tauri/src/models/mod.rs b/src-tauri/src/models/mod.rs index 44dc13e..a40165e 100644 --- a/src-tauri/src/models/mod.rs +++ b/src-tauri/src/models/mod.rs @@ -1,7 +1,8 @@ pub mod app_data; -pub mod remote_error; pub mod dolls; +pub mod event_payloads; pub mod friends; pub mod health; pub mod interaction; +pub mod remote_error; pub mod user; diff --git a/src-tauri/src/services/presence_modules/runtime.rs b/src-tauri/src/services/presence_modules/runtime.rs index 7445f3c..93396ce 100644 --- a/src-tauri/src/services/presence_modules/runtime.rs +++ b/src-tauri/src/services/presence_modules/runtime.rs @@ -3,7 +3,8 @@ use std::{path::Path, thread, time::Duration}; use tokio::runtime::Runtime; use tracing::{error, info, warn}; -use crate::services::ws::user_status::{report_user_status, UserStatusPayload}; +use crate::models::event_payloads::{UserStatusPayload, UserStatusState}; +use crate::services::ws::user_status::report_user_status; use crate::services::ws::{ws_emit_soft, WS_EVENT}; use super::models::PresenceStatus; @@ -45,7 +46,7 @@ impl UserData for Engine { async fn update_status(status: PresenceStatus) { let user_status = UserStatusPayload { presence_status: status, - state: String::from("idle"), + state: UserStatusState::Idle, }; report_user_status(user_status).await; } @@ -53,7 +54,7 @@ async fn update_status(status: PresenceStatus) { async fn update_status_async(status: PresenceStatus) { let payload = UserStatusPayload { presence_status: status, - state: String::from("idle"), + state: UserStatusState::Idle, }; if let Err(e) = ws_emit_soft(WS_EVENT::CLIENT_REPORT_USER_STATUS, payload).await { warn!("User status report failed: {}", e); diff --git a/src-tauri/src/services/ws/friend.rs b/src-tauri/src/services/ws/friend.rs index ae21be3..b2e9753 100644 --- a/src-tauri/src/services/ws/friend.rs +++ b/src-tauri/src/services/ws/friend.rs @@ -1,6 +1,11 @@ use rust_socketio::{Payload, RawClient}; use tracing::info; +use crate::models::event_payloads::{ + FriendActiveDollChangedPayload, FriendDisconnectedPayload, FriendRequestAcceptedPayload, + FriendRequestDeniedPayload, FriendRequestReceivedPayload, FriendUserStatusPayload, + UnfriendedPayload, +}; use crate::services::app_events::AppEvents; use crate::services::cursor::{normalized_to_absolute, CursorPositions}; use crate::state::AppDataRefreshScope; @@ -13,30 +18,36 @@ use super::{ /// Handler for friend-request-received event pub fn on_friend_request_received(payload: Payload, _socket: RawClient) { - if let Ok(value) = utils::extract_text_value(payload, "friend-request-received") { - emitter::emit_to_frontend(AppEvents::FriendRequestReceived.as_str(), value); + if let Ok(data) = + utils::extract_and_parse::(payload, "friend-request-received") + { + emitter::emit_to_frontend(AppEvents::FriendRequestReceived.as_str(), data); } } /// Handler for friend-request-accepted event pub fn on_friend_request_accepted(payload: Payload, _socket: RawClient) { - if let Ok(value) = utils::extract_text_value(payload, "friend-request-accepted") { - emitter::emit_to_frontend(AppEvents::FriendRequestAccepted.as_str(), value); + if let Ok(data) = + utils::extract_and_parse::(payload, "friend-request-accepted") + { + emitter::emit_to_frontend(AppEvents::FriendRequestAccepted.as_str(), data); refresh::refresh_app_data(AppDataRefreshScope::Friends); } } /// Handler for friend-request-denied event pub fn on_friend_request_denied(payload: Payload, _socket: RawClient) { - if let Ok(value) = utils::extract_text_value(payload, "friend-request-denied") { - emitter::emit_to_frontend(AppEvents::FriendRequestDenied.as_str(), value); + if let Ok(data) = + utils::extract_and_parse::(payload, "friend-request-denied") + { + emitter::emit_to_frontend(AppEvents::FriendRequestDenied.as_str(), data); } } /// Handler for unfriended event pub fn on_unfriended(payload: Payload, _socket: RawClient) { - if let Ok(value) = utils::extract_text_value(payload, "unfriended") { - emitter::emit_to_frontend(AppEvents::Unfriended.as_str(), value); + if let Ok(data) = utils::extract_and_parse::(payload, "unfriended") { + emitter::emit_to_frontend(AppEvents::Unfriended.as_str(), data); refresh::refresh_app_data(AppDataRefreshScope::Friends); } } @@ -63,8 +74,10 @@ pub fn on_friend_cursor_position(payload: Payload, _socket: RawClient) { /// Handler for friend-disconnected event pub fn on_friend_disconnected(payload: Payload, _socket: RawClient) { - if let Ok(value) = utils::extract_text_value(payload, "friend-disconnected") { - emitter::emit_to_frontend(AppEvents::FriendDisconnected.as_str(), value); + if let Ok(data) = + utils::extract_and_parse::(payload, "friend-disconnected") + { + emitter::emit_to_frontend(AppEvents::FriendDisconnected.as_str(), data); } } @@ -93,15 +106,20 @@ fn handle_friend_doll_change(event_name: &str, payload: Payload) { /// Handler for friend-active-doll-changed event pub fn on_friend_active_doll_changed(payload: Payload, _socket: RawClient) { - if let Ok(value) = utils::extract_text_value(payload, "friend-active-doll-changed") { - emitter::emit_to_frontend(AppEvents::FriendActiveDollChanged.as_str(), value); + if let Ok(data) = utils::extract_and_parse::( + payload, + "friend-active-doll-changed", + ) { + emitter::emit_to_frontend(AppEvents::FriendActiveDollChanged.as_str(), data); refresh::refresh_app_data(AppDataRefreshScope::Friends); } } /// Handler for friend-user-status event pub fn on_friend_user_status(payload: Payload, _socket: RawClient) { - if let Ok(value) = utils::extract_text_value(payload, "friend-user-status") { - emitter::emit_to_frontend(AppEvents::FriendUserStatus.as_str(), value); + if let Ok(data) = + utils::extract_and_parse::(payload, "friend-user-status") + { + emitter::emit_to_frontend(AppEvents::FriendUserStatus.as_str(), data); } } diff --git a/src-tauri/src/services/ws/user_status.rs b/src-tauri/src/services/ws/user_status.rs index 2968199..a9abe66 100644 --- a/src-tauri/src/services/ws/user_status.rs +++ b/src-tauri/src/services/ws/user_status.rs @@ -1,24 +1,15 @@ use once_cell::sync::Lazy; -use serde::Serialize; use tauri::async_runtime::{self, JoinHandle}; use tokio::sync::Mutex; use tokio::time::Duration; use tracing::warn; -use crate::services::presence_modules::models::PresenceStatus; +use crate::models::event_payloads::UserStatusPayload; use crate::services::app_events::AppEvents; use super::{emitter, types::WS_EVENT}; -/// User status payload sent to WebSocket server -#[derive(Clone, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct UserStatusPayload { - pub presence_status: PresenceStatus, - pub state: String, -} - /// Debouncer for user status reports static USER_STATUS_REPORT_DEBOUNCE: Lazy>>> = Lazy::new(|| Mutex::new(None)); diff --git a/src/events/friend-cursor.ts b/src/events/friend-cursor.ts index b47eb3a..648bd8d 100644 --- a/src/events/friend-cursor.ts +++ b/src/events/friend-cursor.ts @@ -2,10 +2,11 @@ import { listen } from "@tauri-apps/api/event"; import { writable } from "svelte/store"; import type { CursorPositions } from "../types/bindings/CursorPositions"; import type { DollDto } from "../types/bindings/DollDto"; +import type { FriendDisconnectedPayload } from "../types/bindings/FriendDisconnectedPayload"; +import type { FriendActiveDollChangedPayload } from "../types/bindings/FriendActiveDollChangedPayload"; import { AppEvents } from "../types/bindings/AppEventsConstants"; import { createMultiListenerSubscription, - parseEventPayload, removeFromStore, setupHmrCleanup, } from "./listener-utils"; @@ -59,63 +60,51 @@ export async function startFriendCursorTracking() { ); subscription.addUnlisten(unlistenFriendCursor); - const unlistenFriendDisconnected = await listen< - [{ userId: string }] | { userId: string } | string - >(AppEvents.FriendDisconnected, (event) => { - const payload = parseEventPayload< - [{ userId: string }] | { userId: string } - >(event.payload, AppEvents.FriendDisconnected); - if (!payload) return; + const unlistenFriendDisconnected = await listen( + AppEvents.FriendDisconnected, + (event) => { + const data = event.payload; - const data = Array.isArray(payload) ? payload[0] : payload; + if (friendCursorState[data.userId]) { + delete friendCursorState[data.userId]; + } - if (friendCursorState[data.userId]) { - delete friendCursorState[data.userId]; - } - - friendsCursorPositions.update((current) => - removeFromStore(current, data.userId), + friendsCursorPositions.update((current) => + removeFromStore(current, data.userId), + ); + }, ); - }); - subscription.addUnlisten(unlistenFriendDisconnected); + subscription.addUnlisten(unlistenFriendDisconnected); - const unlistenFriendActiveDollChanged = await listen< - | string - | { - friendId: string; - doll: DollDto | null; - } - >(AppEvents.FriendActiveDollChanged, (event) => { - const data = parseEventPayload<{ - friendId: string; - doll: DollDto | null; - }>(event.payload, AppEvents.FriendActiveDollChanged); - if (!data) return; + const unlistenFriendActiveDollChanged = + await listen( + AppEvents.FriendActiveDollChanged, + (event) => { + const payload = event.payload; - const payload = data as { friendId: string; doll: DollDto | null }; + if (!payload.doll) { + friendsActiveDolls.update((current) => { + const next = { ...current }; + next[payload.friendId] = null; + return next; + }); - if (!payload.doll) { - friendsActiveDolls.update((current) => { - const next = { ...current }; - next[payload.friendId] = null; - return next; - }); - - friendsCursorPositions.update((current) => - removeFromStore(current, payload.friendId), + friendsCursorPositions.update((current) => + removeFromStore(current, payload.friendId), + ); + } else { + friendsActiveDolls.update((current) => { + return { + ...current, + [payload.friendId]: payload.doll, + }; + }); + } + }, ); - } else { - friendsActiveDolls.update((current) => { - return { - ...current, - [payload.friendId]: payload.doll!, - }; - }); - } - }); - subscription.addUnlisten(unlistenFriendActiveDollChanged); + subscription.addUnlisten(unlistenFriendActiveDollChanged); - subscription.setListening(true); + subscription.setListening(true); } catch (err) { console.error("Failed to initialize friend cursor tracking:", err); throw err; diff --git a/src/events/listener-utils.ts b/src/events/listener-utils.ts index 69efa41..3a087a3 100644 --- a/src/events/listener-utils.ts +++ b/src/events/listener-utils.ts @@ -74,22 +74,6 @@ export function setupHmrCleanup(cleanup: () => void) { } } -export function parseEventPayload( - payload: unknown, - errorLabel: string, -): T | null { - if (typeof payload === "string") { - try { - return JSON.parse(payload) as T; - } catch (error) { - console.error(`Failed to parse ${errorLabel} payload`, error); - return null; - } - } - - return payload as T; -} - export function removeFromStore( current: Record, key: string, diff --git a/src/events/user-status.ts b/src/events/user-status.ts index 2604e6f..d6757b3 100644 --- a/src/events/user-status.ts +++ b/src/events/user-status.ts @@ -1,21 +1,19 @@ import { listen } from "@tauri-apps/api/event"; import { writable } from "svelte/store"; -import type { PresenceStatus } from "../types/bindings/PresenceStatus"; +import type { FriendDisconnectedPayload } from "../types/bindings/FriendDisconnectedPayload"; +import type { FriendUserStatusPayload } from "../types/bindings/FriendUserStatusPayload"; +import type { UserStatusPayload } from "../types/bindings/UserStatusPayload"; import { AppEvents } from "../types/bindings/AppEventsConstants"; import { createMultiListenerSubscription, - parseEventPayload, removeFromStore, setupHmrCleanup, } from "./listener-utils"; -export type PresenceState = { - presenceStatus: PresenceStatus; - state: "idle" | "resting"; -}; - -export const friendsPresenceStates = writable>({}); -export const currentPresenceState = writable(null); +export const friendsPresenceStates = writable< + Record +>({}); +export const currentPresenceState = writable(null); const subscription = createMultiListenerSubscription(); @@ -26,22 +24,11 @@ export async function startUserStatus() { if (subscription.isListening()) return; try { - const unlistenStatus = await listen( + const unlistenStatus = await listen( AppEvents.FriendUserStatus, (event) => { - const payload = parseEventPayload<{ - userId?: string; - status?: PresenceState; - }>(event.payload, AppEvents.FriendUserStatus); - if (!payload) return; + const { userId, status } = event.payload; - const userId = payload.userId; - const status = payload.status; - - if (!userId || !status) return; - if (!status.presenceStatus) return; - - // Validate that appMetadata has at least one valid name const hasValidName = (typeof status.presenceStatus.title === "string" && status.presenceStatus.title.trim() !== "") || @@ -49,20 +36,15 @@ export async function startUserStatus() { status.presenceStatus.subtitle.trim() !== ""); if (!hasValidName) return; - if (status.state !== "idle" && status.state !== "resting") return; - friendsPresenceStates.update((current) => ({ ...current, - [userId]: { - presenceStatus: status.presenceStatus, - state: status.state, - }, + [userId]: status, })); }, ); subscription.addUnlisten(unlistenStatus); - const unlistenUserStatusChanged = await listen( + const unlistenUserStatusChanged = await listen( AppEvents.UserStatusChanged, (event) => { currentPresenceState.set(event.payload); @@ -70,20 +52,15 @@ export async function startUserStatus() { ); subscription.addUnlisten(unlistenUserStatusChanged); - const unlistenFriendDisconnected = await listen< - [{ userId: string }] | { userId: string } | string - >(AppEvents.FriendDisconnected, (event) => { - const payload = parseEventPayload< - [{ userId: string }] | { userId: string } - >(event.payload, AppEvents.FriendDisconnected); - if (!payload) return; - - const data = Array.isArray(payload) ? payload[0] : payload; - const userId = data?.userId as string | undefined; - if (!userId) return; - - friendsPresenceStates.update((current) => removeFromStore(current, userId)); - }); + const unlistenFriendDisconnected = await listen( + AppEvents.FriendDisconnected, + (event) => { + const { userId } = event.payload; + friendsPresenceStates.update((current) => + removeFromStore(current, userId), + ); + }, + ); subscription.addUnlisten(unlistenFriendDisconnected); subscription.setListening(true); diff --git a/src/routes/scene/components/debug-bar.svelte b/src/routes/scene/components/debug-bar.svelte index 02e9559..b49fd75 100644 --- a/src/routes/scene/components/debug-bar.svelte +++ b/src/routes/scene/components/debug-bar.svelte @@ -1,6 +1,6 @@