fix(launcher): keep active installations visible in game filters
Operation admission withdraws local availability before changing game files. The UI previously received that withdrawal before the busy state, then filtered out installations because only downloads counted as local activity. Publish the active operation first while preserving atomic network withdrawal. Keep active operations in All Games and Local, retain prior Installed filter membership until settlement, and show operation-specific status labels. Local availability and playability remain derived from the backend. Document the installation presentation and cover withdrawal, settlement, and filter counts. Test Plan: - just fmt and just clippy: passed. - just test: passed all workspace suites. - just frontend-test: 98 passed, including installation lifecycle regressions. - deno task build: TypeScript and Vite production build passed. - Native GUI interaction was not manually exercised.
This commit is contained in:
1 parent
6b65a66465
commit
1a394d2825
6 files changed
+227
-39
No files matched your search
@@ -199,7 +199,10 @@ pull.
|
||||
except that accepted game-directory changes can force a UI snapshot for the
|
||||
new path without changing peer availability.
|
||||
- Active operation mutations emit `ActiveOperationsChanged` from the mutation
|
||||
path instead of riding on local library scans.
|
||||
path instead of riding on local library scans. Admission withdraws network
|
||||
availability under the operation and library locks, then emits the active
|
||||
operation before the withdrawn `LocalLibraryChanged` UI snapshot. This keeps
|
||||
the game visible as busy while peers can no longer request its changing files.
|
||||
- The remote UI projection is a wholesale replacement derived from all current
|
||||
authenticated per-peer slices; two peers offering the same game remain
|
||||
distinct exact-content sources.
|
||||
|
||||
@@ -1940,6 +1940,11 @@ async fn begin_operation_with_drain_timeout(
|
||||
};
|
||||
active_operations.insert(target.game_id.clone(), operation);
|
||||
|
||||
// The network projection is already withdrawn under both guards.
|
||||
// Tell the UI that the game is busy before its local snapshot loses
|
||||
// availability, so it can keep the operation visible in the list.
|
||||
events::send_active_operations_snapshot(tx_notify_ui, &active_operations);
|
||||
|
||||
if let Some(revision) = withdrawn_revision {
|
||||
let game_db = GameDB::from(
|
||||
library
|
||||
@@ -1966,11 +1971,9 @@ async fn begin_operation_with_drain_timeout(
|
||||
}
|
||||
|
||||
// Once admitted, a directory change observes the active operation and is
|
||||
// rejected. Release the admission barrier before emitting or draining.
|
||||
// rejected. Release the admission barrier before draining transfers.
|
||||
drop(admission);
|
||||
|
||||
events::emit_active_operations(&ctx.active_operations, tx_notify_ui).await;
|
||||
|
||||
if operation_requires_outbound_drain(operation)
|
||||
&& !cancel_and_wait_for_outbound_transfers(
|
||||
ctx,
|
||||
@@ -4113,7 +4116,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn begin_operation_withdraws_fresh_library_snapshot_before_mutation() {
|
||||
async fn begin_operation_announces_busy_before_withdrawn_ui_snapshot() {
|
||||
let temp = TempDir::new("lanspread-handler-active-withdrawal");
|
||||
let root = temp.game_root();
|
||||
write_file(&root.join("version.ini"), b"20250101");
|
||||
@@ -4142,14 +4145,14 @@ mod tests {
|
||||
BeginOperationResult::Started
|
||||
);
|
||||
|
||||
let PeerEvent::LocalLibraryChanged { games } = recv_event(&mut rx).await else {
|
||||
panic!("availability withdrawal must precede the active-operation event");
|
||||
};
|
||||
assert!(games.is_empty());
|
||||
assert_active_update(
|
||||
recv_event(&mut rx).await,
|
||||
&active_update("game", ActiveOperationKind::Installing),
|
||||
);
|
||||
let PeerEvent::LocalLibraryChanged { games } = recv_event(&mut rx).await else {
|
||||
panic!("withdrawn UI snapshot must follow the active-operation event");
|
||||
};
|
||||
assert!(games.is_empty());
|
||||
|
||||
let snapshot = {
|
||||
let library = ctx.local_library.read().await;
|
||||
@@ -4207,10 +4210,6 @@ mod tests {
|
||||
);
|
||||
|
||||
assert!(token.is_cancelled());
|
||||
let PeerEvent::LocalLibraryChanged { games } = recv_event(&mut rx).await else {
|
||||
panic!("operation admission should withdraw availability first");
|
||||
};
|
||||
assert!(games.is_empty());
|
||||
assert_active_update(
|
||||
recv_event(&mut rx).await,
|
||||
&[ActiveOperation {
|
||||
@@ -4218,6 +4217,10 @@ mod tests {
|
||||
operation: ActiveOperationKind::Updating,
|
||||
}],
|
||||
);
|
||||
let PeerEvent::LocalLibraryChanged { games } = recv_event(&mut rx).await else {
|
||||
panic!("withdrawn UI snapshot must follow the active-operation event");
|
||||
};
|
||||
assert!(games.is_empty());
|
||||
assert_local_update(recv_event(&mut rx).await, false, true);
|
||||
assert_active_update(recv_event(&mut rx).await, &[]);
|
||||
assert!(
|
||||
@@ -4354,14 +4357,14 @@ mod tests {
|
||||
|
||||
run_install_operation(&ctx, &tx, operation_target(temp.path())).await;
|
||||
|
||||
let PeerEvent::LocalLibraryChanged { games } = recv_event(&mut rx).await else {
|
||||
panic!("operation admission should withdraw the ready game");
|
||||
};
|
||||
assert!(games.is_empty());
|
||||
assert_active_update(
|
||||
recv_event(&mut rx).await,
|
||||
&active_update("game", ActiveOperationKind::Updating),
|
||||
);
|
||||
let PeerEvent::LocalLibraryChanged { games } = recv_event(&mut rx).await else {
|
||||
panic!("operation admission should withdraw the ready game");
|
||||
};
|
||||
assert!(games.is_empty());
|
||||
assert_local_update(recv_event(&mut rx).await, true, true);
|
||||
assert_active_update(recv_event(&mut rx).await, &[]);
|
||||
assert!(matches!(
|
||||
@@ -4639,14 +4642,14 @@ mod tests {
|
||||
write_file(&root.join("game.eti"), b"new archive");
|
||||
|
||||
run_install_operation(&ctx, &tx, operation_target(temp.path())).await;
|
||||
let PeerEvent::LocalLibraryChanged { games } = recv_event(&mut rx).await else {
|
||||
panic!("update admission should withdraw the ready game");
|
||||
};
|
||||
assert!(games.is_empty());
|
||||
assert_active_update(
|
||||
recv_event(&mut rx).await,
|
||||
&active_update("game", ActiveOperationKind::Updating),
|
||||
);
|
||||
let PeerEvent::LocalLibraryChanged { games } = recv_event(&mut rx).await else {
|
||||
panic!("update admission should withdraw the ready game");
|
||||
};
|
||||
assert!(games.is_empty());
|
||||
let game = local_update_game(recv_event(&mut rx).await, true, true);
|
||||
assert_eq!(game.local_version.as_deref(), Some("20250101"));
|
||||
assert_active_update(recv_event(&mut rx).await, &[]);
|
||||
@@ -4656,14 +4659,14 @@ mod tests {
|
||||
));
|
||||
|
||||
run_uninstall_operation(&ctx, &tx, operation_target(temp.path())).await;
|
||||
let PeerEvent::LocalLibraryChanged { games } = recv_event(&mut rx).await else {
|
||||
panic!("uninstall admission should withdraw the ready game");
|
||||
};
|
||||
assert!(games.is_empty());
|
||||
assert_active_update(
|
||||
recv_event(&mut rx).await,
|
||||
&active_update("game", ActiveOperationKind::Uninstalling),
|
||||
);
|
||||
let PeerEvent::LocalLibraryChanged { games } = recv_event(&mut rx).await else {
|
||||
panic!("uninstall admission should withdraw the ready game");
|
||||
};
|
||||
assert!(games.is_empty());
|
||||
let game = local_update_game(recv_event(&mut rx).await, false, true);
|
||||
assert_eq!(game.local_version.as_deref(), Some("20250101"));
|
||||
assert_active_update(recv_event(&mut rx).await, &[]);
|
||||
@@ -4725,14 +4728,14 @@ mod tests {
|
||||
|
||||
run_remove_downloaded_operation(&ctx, &tx, operation_target(temp.path())).await;
|
||||
|
||||
let PeerEvent::LocalLibraryChanged { games } = recv_event(&mut rx).await else {
|
||||
panic!("download removal admission should withdraw the ready game");
|
||||
};
|
||||
assert!(games.is_empty());
|
||||
assert_active_update(
|
||||
recv_event(&mut rx).await,
|
||||
&active_update("game", ActiveOperationKind::RemovingDownload),
|
||||
);
|
||||
let PeerEvent::LocalLibraryChanged { games } = recv_event(&mut rx).await else {
|
||||
panic!("download removal admission should withdraw the ready game");
|
||||
};
|
||||
assert!(games.is_empty());
|
||||
assert_active_update(recv_event(&mut rx).await, &[]);
|
||||
assert!(matches!(
|
||||
recv_event(&mut rx).await,
|
||||
|
||||
Reference in new issue
Block a user