refactor(qconnect): move loop helpers to qconnect-app (slice 5)
Relocate the transport-loop helpers out of the Tauri adapter into QconnectApp
methods + qconnect-app free items, so the shared session loop (slice 5.2) and a
future Slint adapter use one definition:
- QconnectApp::capture_session_state_takeover_input (locks self.sync)
- QconnectApp::clear_pending_if_matches (locks self.state)
- QconnectApp::send_set_active_renderer -- inline {"renderer_id": i32} payload,
byte-identical to the prior QconnectSetActiveRendererRequest serialization
- QconnectApp::ask_for_active_renderer_state / QconnectApp::ask_for_queue_state
- SessionStateTakeoverInput struct + queue_payload_track_preview (pure free fn)
Deleted the service.rs free fns (capture_session_state_takeover_input,
send_set_active_renderer_via_app, ask_for_active_renderer_state,
ask_for_queue_state, queue_payload_track_preview, clear_pending_if_matches) and
the SessionStateTakeoverInput struct; rewired every call site (the loop +
deferred_renderer_join + the reconnect/lagged paths) to the methods. The request
structs serialize identically to the inline json!, so the wire is unchanged.
qconnect-app 38 green, src-tauri qconnect 42 green.
cc
This commit is contained in:
4 files changed
+183
-187
No files matched your search
@@ -19,9 +19,10 @@ use tokio::sync::Mutex;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::session::{
|
||||
is_peer_renderer_active, normalize_active_renderer_id, refresh_local_renderer_id,
|
||||
should_arm_renderer_watchdog, LocalIdentity, QconnectRendererInfo, RendererStatus,
|
||||
PLAYING_STATE_PLAYING, PLAYING_STATE_UNKNOWN, QCONNECT_RENDERER_LOST_TIMEOUT_MS,
|
||||
is_local_renderer_active, is_peer_renderer_active, normalize_active_renderer_id,
|
||||
refresh_local_renderer_id, should_arm_renderer_watchdog, LocalIdentity, QconnectRendererInfo,
|
||||
RendererStatus, ServerActiveState, PLAYING_STATE_PLAYING, PLAYING_STATE_UNKNOWN,
|
||||
QCONNECT_RENDERER_LOST_TIMEOUT_MS,
|
||||
};
|
||||
use crate::{
|
||||
ensure_session_renderer_state, sync_session_renderer_active_flags, QconnectAppError,
|
||||
@@ -1445,6 +1446,163 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// Prior-state snapshot captured from the sync accumulator BEFORE a SESSION_STATE
|
||||
/// frame is applied, plus the server's classified active state derived from the
|
||||
/// incoming payload. Drives P1-3 takeover arbitration after the apply. Relocated
|
||||
/// from the Tauri adapter (slice 5) so the shared session loop owns it.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct SessionStateTakeoverInput {
|
||||
pub was_active: bool,
|
||||
pub was_playing: bool,
|
||||
pub server: ServerActiveState,
|
||||
}
|
||||
|
||||
/// Preview the first 8 `track_id`s under `payload[key]` for diagnostic emits.
|
||||
/// Pure; relocated from the Tauri adapter (slice 5).
|
||||
pub fn queue_payload_track_preview(payload: &Value, key: &str) -> Vec<i64> {
|
||||
payload
|
||||
.get(key)
|
||||
.and_then(Value::as_array)
|
||||
.map(|tracks| {
|
||||
tracks
|
||||
.iter()
|
||||
.filter_map(|track| track.get("track_id").and_then(Value::as_i64))
|
||||
.take(8)
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
impl<TTransport, TSink> QconnectApp<TTransport, TSink>
|
||||
where
|
||||
TTransport: WsTransport + 'static,
|
||||
TSink: QconnectEventSink + 'static,
|
||||
{
|
||||
/// Capture `was_active`/`was_playing` from the current (pre-apply) sync state
|
||||
/// and classify the server's active-renderer state from the incoming
|
||||
/// SESSION_STATE payload, relative to our local renderer id. Pure-ish read;
|
||||
/// takes no decision. Relocated from the Tauri adapter (slice 5); locks
|
||||
/// `self.sync` (the same accumulator the apply path mutates).
|
||||
pub async fn capture_session_state_takeover_input(
|
||||
&self,
|
||||
payload: &Value,
|
||||
) -> SessionStateTakeoverInput {
|
||||
let st = self.sync.lock().await;
|
||||
let local_id = st.session.local_renderer_id;
|
||||
let was_active = is_local_renderer_active(&st.session);
|
||||
let was_playing = st.last_renderer_playing_state == Some(PLAYING_STATE_PLAYING);
|
||||
let incoming_active = payload
|
||||
.get("active_renderer_id")
|
||||
.and_then(Value::as_i64)
|
||||
.and_then(|value| i32::try_from(value).ok());
|
||||
let server = match incoming_active {
|
||||
None => ServerActiveState::None,
|
||||
Some(id) if Some(id) == local_id => ServerActiveState::Me,
|
||||
Some(_) => {
|
||||
// Another renderer owns the slot. Classify playing vs paused from
|
||||
// the last observed peer playing_state; default OtherPaused when unknown.
|
||||
if st.last_renderer_playing_state == Some(PLAYING_STATE_PLAYING) {
|
||||
ServerActiveState::OtherPlaying
|
||||
} else {
|
||||
ServerActiveState::OtherPaused
|
||||
}
|
||||
}
|
||||
};
|
||||
SessionStateTakeoverInput {
|
||||
was_active,
|
||||
was_playing,
|
||||
server,
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear the pending action slot iff it still matches `action_uuid`. Used for
|
||||
/// replies that are not reducer-correlated (set-active, ask-for-state). Locks
|
||||
/// `self.state` (NEVER co-locked with `self.sync`). Relocated from the Tauri
|
||||
/// adapter (slice 5).
|
||||
pub async fn clear_pending_if_matches(&self, action_uuid: &str) {
|
||||
let mut state = self.state.lock().await;
|
||||
let pending_matches = state
|
||||
.pending
|
||||
.current()
|
||||
.map(|pending| pending.uuid == action_uuid)
|
||||
.unwrap_or(false);
|
||||
if pending_matches {
|
||||
state.pending.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// Guarded `CtrlSrvrSetActiveRenderer` sender. No-ops when the target equals
|
||||
/// the current active renderer or the id is invalid. Never resets reconnect
|
||||
/// counters (#358 latch untouched). Relocated from the Tauri adapter's
|
||||
/// `send_set_active_renderer_via_app` (slice 5); the request payload
|
||||
/// `{ "renderer_id": <i32> }` is byte-identical to the prior
|
||||
/// `QconnectSetActiveRendererRequest` serialization.
|
||||
pub async fn send_set_active_renderer(&self, target_renderer_id: i32) -> Result<bool, String> {
|
||||
if target_renderer_id < 0 {
|
||||
return Ok(false);
|
||||
}
|
||||
{
|
||||
let st = self.sync.lock().await;
|
||||
if st.session.active_renderer_id == Some(target_renderer_id) {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
let payload = serde_json::json!({ "renderer_id": target_renderer_id });
|
||||
let command = self
|
||||
.build_queue_command(QueueCommandType::CtrlSrvrSetActiveRenderer, payload)
|
||||
.await;
|
||||
let uuid = self
|
||||
.send_queue_command(command)
|
||||
.await
|
||||
.map_err(|err| format!("send set_active_renderer failed: {err}"))?;
|
||||
self.clear_pending_if_matches(&uuid).await;
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// Ask the currently-active renderer for its full state (P1-8). No-op when no
|
||||
/// active renderer is known. The reply is not reducer-correlated, so its
|
||||
/// pending slot is cleared immediately. Relocated from the Tauri adapter
|
||||
/// (slice 5); payload byte-identical to the prior
|
||||
/// `QconnectAskForRendererStateRequest` serialization.
|
||||
pub async fn ask_for_active_renderer_state(&self) -> Result<bool, String> {
|
||||
let active_id = {
|
||||
let st = self.sync.lock().await;
|
||||
st.session.active_renderer_id
|
||||
};
|
||||
let Some(active_id) = active_id else {
|
||||
return Ok(false);
|
||||
};
|
||||
let payload = serde_json::json!({ "renderer_id": active_id });
|
||||
let cmd = self
|
||||
.build_queue_command(QueueCommandType::CtrlSrvrAskForRendererState, payload)
|
||||
.await;
|
||||
let uuid = self
|
||||
.send_queue_command(cmd)
|
||||
.await
|
||||
.map_err(|err| format!("send ask_for_renderer_state failed: {err}"))?;
|
||||
self.clear_pending_if_matches(&uuid).await;
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// Ask the server for the current queue state (P1-8 Lagged-recovery / generic
|
||||
/// re-sync). The reply is not reducer-correlated, so its pending slot is
|
||||
/// cleared. Relocated from the Tauri adapter (slice 5).
|
||||
pub async fn ask_for_queue_state(&self) -> Result<(), String> {
|
||||
let cmd = self
|
||||
.build_queue_command(
|
||||
QueueCommandType::CtrlSrvrAskForQueueState,
|
||||
serde_json::json!({}),
|
||||
)
|
||||
.await;
|
||||
let uuid = self
|
||||
.send_queue_command(cmd)
|
||||
.await
|
||||
.map_err(|err| format!("send ask_for_queue_state failed: {err}"))?;
|
||||
self.clear_pending_if_matches(&uuid).await;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -11,7 +11,9 @@ mod state;
|
||||
pub mod startup;
|
||||
mod sync_state;
|
||||
|
||||
pub use app::{QconnectApp, SessionApplyOutcome};
|
||||
pub use app::{
|
||||
queue_payload_track_preview, QconnectApp, SessionApplyOutcome, SessionStateTakeoverInput,
|
||||
};
|
||||
pub use startup::{compute_effective_startup, QconnectStartupMode};
|
||||
pub use error::QconnectAppError;
|
||||
pub use events::{NoOpEventSink, QconnectAppEvent, QconnectEventSink};
|
||||
|
||||
Reference in new issue
Block a user