refactor(peer): split local library and operation UI events
Replace the `a9f9845` local-update dedup cache with explicit peer event semantics. Local scans now emit `LocalLibraryChanged` when the library changes, while operation mutations emit `ActiveOperationsChanged` from the mutation path. Tauri keeps joining those facts into the existing `games-list-updated` payload, so the frontend contract stays stable. This removes the cache/invalidation coupling between scan emission and operation state. The remaining forced local snapshot is explicit: accepted game directory changes can refresh the UI for an equivalent new path without sending a peer library delta. Operation guard cleanup and liveness cancellation now publish the same active operation snapshot as normal command-handler transitions. The peer CLI JSONL events follow the same split with `local-library-changed` and `active-operations-changed`. Test Plan: - `just fmt` - `CARGO_BUILD_RUSTC_WRAPPER= just test` - `CARGO_BUILD_RUSTC_WRAPPER= just clippy` - `git diff --check` Refs: CLEAN_CODE_PLAN_1.md
This commit is contained in:
@@ -202,12 +202,13 @@ async fn handle_active_downloads_without_peers(
|
||||
return;
|
||||
}
|
||||
|
||||
let mut changed = false;
|
||||
for id in active_ids {
|
||||
if peers_still_have_game(peer_game_db, &id).await {
|
||||
continue;
|
||||
}
|
||||
|
||||
active_operations.write().await.remove(&id);
|
||||
changed |= active_operations.write().await.remove(&id).is_some();
|
||||
let Some(cancel_token) = active_downloads.write().await.remove(&id) else {
|
||||
continue;
|
||||
};
|
||||
@@ -215,9 +216,13 @@ async fn handle_active_downloads_without_peers(
|
||||
|
||||
events::send(
|
||||
tx_notify_ui,
|
||||
PeerEvent::DownloadGameFilesAllPeersGone { id },
|
||||
PeerEvent::DownloadGameFilesAllPeersGone { id: id.clone() },
|
||||
);
|
||||
}
|
||||
|
||||
if changed {
|
||||
events::emit_active_operations(active_operations, tx_notify_ui).await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn peers_still_have_game(peer_game_db: &Arc<RwLock<PeerGameDB>>, game_id: &str) -> bool {
|
||||
@@ -233,10 +238,16 @@ mod tests {
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use super::handle_active_downloads_without_peers;
|
||||
use crate::{PeerEvent, context::OperationKind, peer_db::PeerGameDB};
|
||||
use crate::{
|
||||
ActiveOperation,
|
||||
ActiveOperationKind,
|
||||
PeerEvent,
|
||||
context::OperationKind,
|
||||
peer_db::PeerGameDB,
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
async fn all_peers_gone_cancels_download_and_emits_only_peers_gone() {
|
||||
async fn all_peers_gone_cancels_download_and_emits_peers_gone_then_active_snapshot() {
|
||||
let peer_game_db = Arc::new(RwLock::new(PeerGameDB::new()));
|
||||
let active_operations = Arc::new(RwLock::new(HashMap::from([(
|
||||
"game".to_string(),
|
||||
@@ -266,9 +277,17 @@ mod tests {
|
||||
event,
|
||||
PeerEvent::DownloadGameFilesAllPeersGone { id } if id == "game"
|
||||
));
|
||||
let event = rx
|
||||
.recv()
|
||||
.await
|
||||
.expect("active operation snapshot should be emitted");
|
||||
assert!(matches!(
|
||||
event,
|
||||
PeerEvent::ActiveOperationsChanged { active_operations } if active_operations.is_empty()
|
||||
));
|
||||
assert!(
|
||||
rx.try_recv().is_err(),
|
||||
"peers-gone cancellation must not emit a duplicate failure event"
|
||||
"peers-gone cancellation must not emit extra events"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -318,9 +337,21 @@ mod tests {
|
||||
}
|
||||
cancelled_ids.sort();
|
||||
assert_eq!(cancelled_ids, vec!["first", "second"]);
|
||||
let event = rx
|
||||
.recv()
|
||||
.await
|
||||
.expect("active operation snapshot should be emitted");
|
||||
assert!(matches!(
|
||||
event,
|
||||
PeerEvent::ActiveOperationsChanged { active_operations }
|
||||
if active_operations == vec![ActiveOperation {
|
||||
id: "installing".to_string(),
|
||||
operation: ActiveOperationKind::Installing,
|
||||
}]
|
||||
));
|
||||
assert!(
|
||||
rx.try_recv().is_err(),
|
||||
"multiple peers-gone cancellations must not emit duplicate failure events"
|
||||
"multiple peers-gone cancellations must not emit extra events"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -391,19 +391,15 @@ mod tests {
|
||||
|
||||
async fn recv_local_update(
|
||||
rx: &mut mpsc::UnboundedReceiver<PeerEvent>,
|
||||
) -> (Vec<lanspread_db::db::Game>, Vec<crate::ActiveOperation>) {
|
||||
) -> Vec<lanspread_db::db::Game> {
|
||||
let event = tokio::time::timeout(Duration::from_secs(1), rx.recv())
|
||||
.await
|
||||
.expect("local update event should arrive")
|
||||
.expect("event channel should stay open");
|
||||
let PeerEvent::LocalGamesUpdated {
|
||||
games,
|
||||
active_operations,
|
||||
} = event
|
||||
else {
|
||||
panic!("expected LocalGamesUpdated");
|
||||
let PeerEvent::LocalLibraryChanged { games } = event else {
|
||||
panic!("expected LocalLibraryChanged");
|
||||
};
|
||||
(games, active_operations)
|
||||
games
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -537,7 +533,7 @@ mod tests {
|
||||
ctx.task_tracker.wait().await;
|
||||
|
||||
let mut update_count = 0;
|
||||
while let Ok(Some(PeerEvent::LocalGamesUpdated { .. })) =
|
||||
while let Ok(Some(PeerEvent::LocalLibraryChanged { .. })) =
|
||||
tokio::time::timeout(Duration::from_millis(50), rx.recv()).await
|
||||
{
|
||||
update_count += 1;
|
||||
@@ -560,8 +556,7 @@ mod tests {
|
||||
|
||||
run_fallback_scan(&ctx, &tx).await;
|
||||
|
||||
let (games, active_operations) = recv_local_update(&mut rx).await;
|
||||
assert!(active_operations.is_empty());
|
||||
let games = recv_local_update(&mut rx).await;
|
||||
let game = games
|
||||
.iter()
|
||||
.find(|game| game.id == "game")
|
||||
@@ -585,9 +580,12 @@ mod tests {
|
||||
|
||||
run_fallback_scan(&ctx, &tx).await;
|
||||
|
||||
let (games, active_operations) = recv_local_update(&mut rx).await;
|
||||
assert!(games.is_empty());
|
||||
assert!(active_operations.is_empty());
|
||||
assert!(
|
||||
tokio::time::timeout(Duration::from_millis(50), rx.recv())
|
||||
.await
|
||||
.is_err(),
|
||||
"non-catalog scan should not emit a local library event"
|
||||
);
|
||||
let library = ctx.local_library.read().await;
|
||||
assert!(library.games.is_empty());
|
||||
assert!(library.recent_deltas.is_empty());
|
||||
|
||||
Reference in New Issue
Block a user