4.1 KiB
4.1 KiB
Type Synchronization & Thread Safety
PlayerState (Rust <-> TypeScript)
Rust:
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
// 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
// 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 }
}));
}