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:
2026-05-18 21:25:20 +02:00
parent be00a7a298
commit 41e9a0efc1
14 changed files with 657 additions and 255 deletions
@@ -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());