//! UI event helpers used by peer command and service code. use std::{ collections::{BTreeMap, HashMap}, sync::Arc, }; use lanspread_db::content_manifest::ContentId; use tokio::sync::{RwLock, mpsc::UnboundedSender}; use crate::{ ActiveOperation, ActiveOperationKind, PeerEvent, RemoteGameAvailability, RemoteLibraryView, context::OperationKind, peer_db::PeerGameDB, }; pub fn send(tx_notify_ui: &UnboundedSender, event: PeerEvent) { if let Err(err) = tx_notify_ui.send(event) { let kind: &'static str = (&err.0).into(); log::error!("Failed to send {kind} event: channel closed"); } } pub(crate) fn active_operation_snapshot_from_map( active_operations: &HashMap, ) -> Vec { let mut snapshot = active_operations .iter() .map(|(id, operation)| ActiveOperation { id: id.clone(), operation: active_operation_kind(*operation), }) .collect::>(); snapshot.sort_by(|left, right| left.id.cmp(&right.id)); snapshot } pub(crate) fn send_active_operations_snapshot( tx_notify_ui: &UnboundedSender, active_operations: &HashMap, ) { send( tx_notify_ui, PeerEvent::ActiveOperationsChanged { active_operations: active_operation_snapshot_from_map(active_operations), }, ); } pub(crate) async fn emit_active_operations( active_operations: &Arc>>, tx_notify_ui: &UnboundedSender, ) { let active_operations = active_operations.read().await; send_active_operations_snapshot(tx_notify_ui, &active_operations); } fn active_operation_kind(operation: OperationKind) -> ActiveOperationKind { match operation { OperationKind::Downloading => ActiveOperationKind::Downloading, OperationKind::Installing => ActiveOperationKind::Installing, OperationKind::Updating => ActiveOperationKind::Updating, OperationKind::Uninstalling => ActiveOperationKind::Uninstalling, OperationKind::RemovingDownload => ActiveOperationKind::RemovingDownload, } } pub async fn emit_peer_game_list( peer_game_db: &Arc>, tx_notify_ui: &UnboundedSender, ) { let db = peer_game_db.read().await; send_remote_library_view_locked(&db, tx_notify_ui); } fn send_remote_library_view_locked( peer_game_db: &PeerGameDB, tx_notify_ui: &UnboundedSender, ) { send( tx_notify_ui, PeerEvent::RemoteLibraryView(remote_library_view(peer_game_db)), ); } pub(crate) fn remote_library_view(peer_game_db: &PeerGameDB) -> RemoteLibraryView { let mut counts = BTreeMap::<(String, ContentId), u32>::new(); for game in peer_game_db .peer_snapshots() .into_iter() .flat_map(|peer| peer.games) { let count = counts.entry((game.game_id, game.content_id)).or_default(); *count = count.saturating_add(1); } RemoteLibraryView { games: counts .into_iter() .map( |((game_id, content_id), peer_count)| RemoteGameAvailability { game_id, content_id, peer_count, }, ) .collect(), } } pub async fn emit_peer_count( peer_game_db: &Arc>, tx_notify_ui: &UnboundedSender, ) { let db = peer_game_db.read().await; let peer_count = db.peer_endpoints().len(); send(tx_notify_ui, PeerEvent::PeerCountUpdated(peer_count)); } #[cfg(test)] mod tests { use super::*; #[tokio::test] async fn remote_library_view_is_enqueued_before_a_waiting_writer_can_commit() { let peer_game_db = Arc::new(RwLock::new(PeerGameDB::new())); let guard = peer_game_db.read().await; let writer_db = Arc::clone(&peer_game_db); let writer = tokio::spawn(async move { let _guard = writer_db.write().await; }); tokio::task::yield_now().await; assert!(!writer.is_finished(), "writer must wait for the read guard"); let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel(); send_remote_library_view_locked(&guard, &tx); assert!(matches!( rx.try_recv(), Ok(PeerEvent::RemoteLibraryView(RemoteLibraryView { games })) if games.is_empty() )); assert!( !writer.is_finished(), "the view must be enqueued while the read guard still orders writers" ); drop(guard); tokio::time::timeout(std::time::Duration::from_secs(1), writer) .await .expect("writer should acquire after the view is enqueued") .expect("writer task should finish"); } }