Type Synchronization & Thread Safety
PlayerState (Rust <-> TypeScript)
Rust:
#![allow(unused)] fn main() { pub enum PlayerState { Idle, Loading { media: MediaItem }, Playing { media: MediaItem, position: f64, duration: f64 }, Paused { media: MediaItem, position: f64, duration: f64 }, Seeking { media: MediaItem, target: f64 }, Error { media: Option<MediaItem>, error: String }, } }
TypeScript:
type PlayerState =
| { kind: "idle" }
| { kind: "loading"; media: MediaItem }
| { kind: "playing"; media: MediaItem; position: number; duration: number }
| { kind: "paused"; media: MediaItem; position: number; duration: number }
| { kind: "seeking"; media: MediaItem; target: number }
| { kind: "error"; media: MediaItem | null; error: string };
MediaItem Serialization
#![allow(unused)] fn main() { // Rust (serde serialization) #[derive(Serialize, Deserialize)] pub struct MediaItem { pub id: String, pub title: String, #[serde(skip_serializing_if = "Option::is_none")] pub artist: Option<String>, // ... } }
// TypeScript
interface MediaItem {
id: string;
title: string;
artist?: string;
// ...
}
Tauri v2 IPC Parameter Naming Convention
CRITICAL: Tauri v2's #[tauri::command] macro automatically converts snake_case Rust parameter names to camelCase for the frontend. All invoke() calls must use camelCase for top-level parameters.
Rule: Rust fn cmd(repository_handle: String) -> Frontend sends { repositoryHandle: "..." }
// CORRECT - Tauri v2 auto-converts snake_case -> camelCase
await invoke("player_play_tracks", {
repositoryHandle: "handle-123", // Rust: repository_handle
request: { trackIds: ["id1"], startIndex: 0 }
});
await invoke("remote_send_command", {
sessionId: "session-123", // Rust: session_id
command: "PlayPause"
});
await invoke("pin_item", {
itemId: "item-123" // Rust: item_id
});
// WRONG - snake_case causes "invalid args request" error on Android
await invoke("player_play_tracks", {
repository_handle: "handle-123", // Will fail!
});
Parameter Name Mapping (Rust -> Frontend):
| Rust Parameter | Frontend Parameter | Used By |
|---|---|---|
repository_handle | repositoryHandle | player_play_tracks, player_add_track_by_id, player_play_album_track |
session_id | sessionId | remote_send_command, remote_play_on_session, remote_session_seek |
item_id | itemId | pin_item, unpin_item |
current_item_id | currentItemId | playback_mode_transfer_to_local |
position_ticks | positionTicks | playback_mode_transfer_to_local, remote_session_seek |
item_ids | itemIds | remote_play_on_session |
start_index | startIndex | remote_play_on_session |
Nested struct fields use #[serde(rename_all = "camelCase")] separately - this is serde deserialization, not the command macro. Both layers convert independently.
Test Coverage: Integration tests in src/lib/utils/tauriIntegration.test.ts validate all invoke calls use correct camelCase parameter names.
Rust Backend Thread Safety
#![allow(unused)] fn main() { // Shared state wrapped in Arc<Mutex<>> pub struct PlayerController { backend: Arc<Mutex<Box<dyn PlayerBackend>>>, queue: Arc<Mutex<QueueManager>>, // ... } // Tauri state wrapper pub struct PlayerStateWrapper(pub Mutex<PlayerController>); // Command handler pattern #[tauri::command] pub fn player_play(state: State<PlayerStateWrapper>) -> Result<PlayerStatus, String> { let mut controller = state.0.lock().unwrap(); // Acquire lock controller.play()?; // Operate Ok(get_player_status(&controller)) // Lock released } }
Frontend Stores
Svelte stores are inherently reactive and thread-safe for UI updates:
const { subscribe, update } = writable<PlayerStore>(initialState);
// Atomic updates
function setPlaying(media: MediaItem, position: number, duration: number) {
update(state => ({
...state,
state: { kind: "playing", media, position, duration }
}));
}