feat(qconnect): surface server queue_hash onto queue state
Decoder now reads field #100 (previously decoded-then-dropped) on QueueState and carries it through to QConnectQueueState.last_server_queue_hash via the QueueStateReplaced reducer path (full-state replace already clones it). cc
This commit is contained in:
3 files changed
+56
-1
No files matched your search
@@ -637,6 +637,12 @@ fn map_server_event(event: &QueueServerEvent, current: &QConnectQueueState) -> Q
|
||||
next.shuffle_order = None;
|
||||
}
|
||||
|
||||
// Surface the server queue_hash (field #100). Only overwrite when the
|
||||
// payload actually carries it, so an omitted hash leaves prior state.
|
||||
if let Some(server_hash) = parse_bytes(&event.payload, "server_queue_hash") {
|
||||
next.last_server_queue_hash = Some(server_hash);
|
||||
}
|
||||
|
||||
QueueEvent::QueueStateReplaced {
|
||||
action_uuid: event.action_uuid.clone(),
|
||||
state: next,
|
||||
@@ -827,6 +833,21 @@ fn parse_u64(payload: &Value, field: &str) -> Option<u64> {
|
||||
payload.get(field).and_then(value_as_u64)
|
||||
}
|
||||
|
||||
/// Parse a JSON byte array (e.g. the server `queue_hash` field #100) into bytes.
|
||||
/// Returns `None` when absent/null so an omitted hash does not clobber state.
|
||||
fn parse_bytes(payload: &Value, field: &str) -> Option<Vec<u8>> {
|
||||
payload
|
||||
.get(field)
|
||||
.and_then(Value::as_array)
|
||||
.map(|entries| {
|
||||
entries
|
||||
.iter()
|
||||
.filter_map(value_as_u64)
|
||||
.filter_map(|value| u8::try_from(value).ok())
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_bool(payload: &Value, field: &str, default: bool) -> bool {
|
||||
payload
|
||||
.get(field)
|
||||
@@ -1165,6 +1186,31 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn queue_state_event_surfaces_server_queue_hash() {
|
||||
let (app, _sink, _transport, _events_rx) = build_connected_app().await;
|
||||
|
||||
app.apply_server_event(QueueServerEvent {
|
||||
event_type: QueueEventType::SrvrCtrlQueueState,
|
||||
action_uuid: None,
|
||||
queue_version: Some(QueueVersion::new(1, 0)),
|
||||
payload: json!({
|
||||
"tracks": [],
|
||||
"shuffle_mode": false,
|
||||
"autoplay_tracks": [],
|
||||
"server_queue_hash": [1, 2, 3, 4]
|
||||
}),
|
||||
})
|
||||
.await
|
||||
.expect("apply queue-state event");
|
||||
|
||||
let snap = app.queue_state_snapshot().await;
|
||||
assert_eq!(
|
||||
snap.last_server_queue_hash.as_deref(),
|
||||
Some(&[1u8, 2, 3, 4][..])
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn autoplay_growth_triggers_queue_state_resync() {
|
||||
let (app, sink, transport, _events_rx) = build_connected_app().await;
|
||||
|
||||
@@ -36,6 +36,11 @@ pub struct QConnectQueueState {
|
||||
pub autoplay_loading: bool,
|
||||
pub autoplay_items: Vec<QueueItem>,
|
||||
pub updated_at_ms: u64,
|
||||
/// Last `queue_hash` (field #100) reported by the server on a queue-state
|
||||
/// confirmation. Surfaced for divergence detection; see the algorithm-agnostic
|
||||
/// `queue_hashes_diverge` seam in qconnect-app (algorithm is a BLOCKING unknown).
|
||||
#[serde(default)]
|
||||
pub last_server_queue_hash: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
impl Default for QConnectQueueState {
|
||||
@@ -49,6 +54,7 @@ impl Default for QConnectQueueState {
|
||||
autoplay_loading: false,
|
||||
autoplay_items: Vec::new(),
|
||||
updated_at_ms: 0,
|
||||
last_server_queue_hash: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -544,7 +544,10 @@ fn map_queue_state(payload: QueueStateMessage) -> Result<QueueServerEvent, Proto
|
||||
"shuffled_track_indexes": shuffled_indexes,
|
||||
"autoplay_mode": payload.autoplay_mode.unwrap_or(false),
|
||||
"autoplay_loading": payload.autoplay_loading.unwrap_or(false),
|
||||
"autoplay_tracks": autoplay_tracks
|
||||
"autoplay_tracks": autoplay_tracks,
|
||||
// Field #100: previously decoded-then-dropped. Surfaced for the
|
||||
// queue_hash divergence seam (algorithm is a BLOCKING unknown).
|
||||
"server_queue_hash": payload.queue_hash
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user