jellytau/docs/architecture/04-type-sync-and-threading.md
Duncan Tourolle 09780103a7
Some checks failed
🏗️ Build and Test JellyTau / Run Tests (push) Failing after 12s
🏗️ Build and Test JellyTau / Build Android APK (push) Has been skipped
Traceability Validation / Check Requirement Traces (push) Failing after 1s
Split software arch desc for easier manintenance. Many fixes related to next video playing and remote playback
2026-03-01 19:47:46 +01:00

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 }
  }));
}