refactor(qconnect): relocate QconnectRemoteSyncState struct to qconnect-app (slice 2+4)

Move the cross-submodule remote-sync accumulator into the new
frontend-agnostic qconnect_app::sync_state module. All field types are
now frontend-agnostic (primitives / qconnect-core / qconnect-app), so
the struct moves cleanly with pub fields; the Tauri qconnect module
re-exports it so existing super::QconnectRemoteSyncState references and
the Arc<Mutex<>> creation/threading in service.rs compile unchanged.

It stays ONE struct under ONE lock by design: capture_session_state_takeover_input
reads session topology together with last_renderer_playing_state, and the
materialize path reads last_renderer_* with last_applied_queue_state, all
atomically. Splitting them would tear the takeover decision, so the whole
accumulator relocates as a unit (single-lock locking is byte-identical).

Two test-only re-exports (find_unique_renderer_id, QconnectSessionRendererState)
are gated #[cfg(test)] since their non-test consumers moved to qconnect-app.

qconnect-app + src-tauri qconnect tests green (33 / 42), no new warnings.

cc
This commit is contained in:
vicrodh committed 2026-06-02 12:58:51 -06:00
1 parent 979a09e7b2
commit a79971e470
4 files changed
+83 -48

No files matched your search

+2
View File
@@ -9,6 +9,7 @@ mod feature_flags;
pub mod session;
mod state;
pub mod startup;
mod sync_state;
pub use app::QconnectApp;
pub use startup::{compute_effective_startup, QconnectStartupMode};
@@ -35,3 +36,4 @@ pub use qconnect_core::{
};
pub use qconnect_protocol::{QueueCommandType, RendererReport, RendererReportType};
pub use state::QconnectRuntimeState;
pub use sync_state::QconnectRemoteSyncState;
+63
View File
@@ -0,0 +1,63 @@
//! Cross-frontend QConnect remote-sync accumulator.
//!
//! Caches the cloud's renderer/queue snapshots, the most recent
//! materialization, the session topology, the per-renderer cached state, the
//! load-attempt dedup window, and the renderer-liveness watchdog epoch.
//!
//! This is held behind a SINGLE Mutex shared by the session/liveness path and
//! the renderer-materialize path. That single-lock sharing is load-bearing:
//! `capture_session_state_takeover_input` reads `session` together with
//! `last_renderer_playing_state`, and the materialize path reads `last_renderer_*`
//! together with `last_applied_queue_state`, all atomically. Splitting these
//! across two locks would tear the takeover decision. So it stays ONE struct
//! under ONE lock. Relocated here (slice 2+4) so both the Tauri and Slint
//! adapters share it; the owning `Mutex` is held by the adapter / `QconnectApp`.
//!
//! Mutated by the event sink (on inbound events), the renderer engine (on
//! materialize/apply), track loading (on load attempt), and the service loop
//! (on outbound report completions).
use std::collections::HashMap;
use std::time::Instant;
use qconnect_core::QConnectQueueState;
use crate::session::{
QconnectFileAudioQualitySnapshot, QconnectSessionRendererState, QconnectSessionState,
};
#[derive(Debug, Default)]
pub struct QconnectRemoteSyncState {
pub last_renderer_queue_item_id: Option<u64>,
pub last_renderer_next_queue_item_id: Option<u64>,
pub last_renderer_track_id: Option<u64>,
pub last_renderer_next_track_id: Option<u64>,
pub last_renderer_playing_state: Option<i32>,
pub last_materialized_start_index: Option<usize>,
pub last_materialized_core_shuffle_order: Option<Vec<usize>>,
pub last_reported_file_audio_quality: Option<QconnectFileAudioQualitySnapshot>,
/// Last reported device (DAC output) audio quality: (sampling_rate, bit_depth, nb_channels).
/// Used to dedup outbound RndrSrvrDeviceAudioQualityChanged(27) reports.
pub last_reported_device_audio_quality: Option<(i32, i32, i32)>,
pub last_applied_queue_state: Option<QConnectQueueState>,
pub last_remote_queue_state: Option<QConnectQueueState>,
pub session_loop_mode: Option<i32>,
/// Session topology — stored from session management events (types 81-87).
pub session: QconnectSessionState,
/// The session_uuid for which we last ran the full deferred renderer-join
/// body. Used to make the deferred join idempotent (P1-8): when a SESSION_STATE
/// arrives with the same session_uuid we skip the join reports but still
/// re-AskForRendererState.
pub last_joined_session_uuid: Option<String>,
pub session_renderer_states: HashMap<i32, QconnectSessionRendererState>,
/// Track of the most recent load attempt across paths (V2 play handoff and
/// ensure_remote_track_loaded). Used to suppress redundant reloads when an
/// echo SetState arrives during the in-progress buffer/decode window of a
/// previously triggered load.
pub last_load_attempt: Option<(u64, Instant)>,
/// Monotonic epoch for the renderer-liveness watchdog (P0-1). Every armed
/// RENDERER_STATE_UPDATED bumps this; a spawned 12s task captures the value
/// and no-ops on wake if it was superseded (reset/disarm). Disarm =
/// pause/stop/active-change/disconnect, which also bump it.
pub watchdog_generation: u64,
}
+11 -46
View File
@@ -21,14 +21,15 @@ mod types;
pub use commands::*;
pub use service::QconnectServiceState;
pub use session::{QconnectRendererInfo, QconnectSessionState};
// QconnectSessionRendererState is referenced via `super::…` only by the test
// module now (the QconnectRemoteSyncState struct that consumed it in non-test
// code moved to qconnect-app), so gate the re-export to test builds to avoid an
// unused-import warning.
#[cfg(test)]
pub(super) use session::QconnectSessionRendererState;
pub use types::*;
use std::collections::HashMap;
use qbz_models::{QueueTrack, RepeatMode, Track};
use qconnect_app::QConnectQueueState;
use session::{QconnectFileAudioQualitySnapshot, QconnectSessionRendererState};
const QCONNECT_REMOTE_QUEUE_SOURCE: &str = "qobuz_connect_remote";
@@ -54,47 +55,11 @@ pub(super) const AUDIO_QUALITY_HIRES_LEVEL2: i32 = 4;
pub(super) const AUDIO_QUALITY_HIRES_LEVEL3: i32 = 5;
pub(super) const DEFAULT_QCONNECT_CHANNEL_COUNT: i32 = 2;
/// Cross-submodule accumulator: caches the cloud's renderer/queue
/// snapshots, the most recent materialization, the topology of all
/// renderers in the session, and the load-attempt dedup window. Mutated
/// by event_sink (on inbound events), corebridge (on materialize/apply),
/// track_loading (on load attempt), and service (on outbound report
/// completions).
#[derive(Debug, Default)]
pub(super) struct QconnectRemoteSyncState {
pub(super) last_renderer_queue_item_id: Option<u64>,
pub(super) last_renderer_next_queue_item_id: Option<u64>,
pub(super) last_renderer_track_id: Option<u64>,
pub(super) last_renderer_next_track_id: Option<u64>,
pub(super) last_renderer_playing_state: Option<i32>,
pub(super) last_materialized_start_index: Option<usize>,
pub(super) last_materialized_core_shuffle_order: Option<Vec<usize>>,
pub(super) last_reported_file_audio_quality: Option<QconnectFileAudioQualitySnapshot>,
/// Last reported device (DAC output) audio quality: (sampling_rate, bit_depth, nb_channels).
/// Used to dedup outbound RndrSrvrDeviceAudioQualityChanged(27) reports.
pub(super) last_reported_device_audio_quality: Option<(i32, i32, i32)>,
pub(super) last_applied_queue_state: Option<QConnectQueueState>,
pub(super) last_remote_queue_state: Option<QConnectQueueState>,
pub(super) session_loop_mode: Option<i32>,
/// Session topology — stored from session management events (types 81-87).
pub(super) session: QconnectSessionState,
/// The session_uuid for which we last ran the full deferred renderer-join
/// body. Used to make the deferred join idempotent (P1-8): when a SESSION_STATE
/// arrives with the same session_uuid we skip the join reports but still
/// re-AskForRendererState.
pub(super) last_joined_session_uuid: Option<String>,
pub(super) session_renderer_states: HashMap<i32, QconnectSessionRendererState>,
/// Track of the most recent load attempt across paths (V2 play
/// handoff and ensure_remote_track_loaded). Used to suppress
/// redundant reloads when an echo SetState arrives during the
/// in-progress buffer/decode window of a previously triggered load.
pub(super) last_load_attempt: Option<(u64, std::time::Instant)>,
/// Monotonic epoch for the renderer-liveness watchdog (P0-1). Every armed
/// RENDERER_STATE_UPDATED bumps this; a spawned 12s task captures the value
/// and no-ops on wake if it was superseded (reset/disarm). Disarm =
/// pause/stop/active-change/disconnect, which also bump it.
pub(super) watchdog_generation: u64,
}
/// The cross-submodule remote-sync accumulator now lives in the frontend-agnostic
/// `qconnect_app::sync_state` module (slice 2+4) so both the Tauri and Slint
/// adapters share one struct under one lock. Re-exported here so existing
/// `super::QconnectRemoteSyncState` references compile unchanged.
pub(super) use qconnect_app::QconnectRemoteSyncState;
pub(super) fn qconnect_now_ms() -> u64 {
use std::time::{SystemTime, UNIX_EPOCH};
+7 -2
View File
@@ -35,9 +35,14 @@ pub use qconnect_app::{QconnectRendererInfo, QconnectSessionRendererState, Qconn
/// qconnect-app (slice 2+4). Re-exported so existing `super::session::…` /
/// `super::…` references compile unchanged.
pub(super) use qconnect_app::{
find_unique_renderer_id, is_local_renderer_active, is_peer_renderer_active,
normalize_active_renderer_id, renderer_allows_remote_volume, QconnectFileAudioQualitySnapshot,
is_local_renderer_active, is_peer_renderer_active, normalize_active_renderer_id,
renderer_allows_remote_volume, QconnectFileAudioQualitySnapshot,
};
// find_unique_renderer_id's sole non-test caller (refresh_local_renderer_id)
// moved to qconnect-app, so it is referenced only by the test module now; gate
// the re-export to test builds.
#[cfg(test)]
pub(super) use qconnect_app::find_unique_renderer_id;
pub(super) fn queue_item_snapshot_for_cursor(
queue: &QConnectQueueState,