fix(peer): settle current-protocol local state cleanup
The follow-up backlog had drifted into three settled peer/runtime issues: the legacy game-list fallback contradicted the one-wire-version policy, the Tauri shell still re-derived local install state from disk after peer snapshots, and `Availability::Downloading` existed even though active operations are already reported through a separate operation table. Remove the legacy `AnnounceGames` request and fallback service. Discovery now ignores peers that do not advertise the current protocol and a peer id, and library changes are sent through the current delta path only. This keeps the runtime aligned with the documented current-build-only interoperability model. Make peer `LocalGamesUpdated` snapshots authoritative for local fields in the Tauri database. The GUI-side catalog still owns static metadata such as names, sizes, and descriptions, but downloaded, installed, local version, and availability now come from the peer runtime instead of a second whole-library filesystem scan. Snapshot reconciliation also pins the missing-begin and missing-finish lifecycle cases in tests. Collapse availability back to the settled `Ready` and `LocalOnly` states. Aggregation now counts only `Ready` peers as download sources, and the frontend no longer carries a dead `Downloading` enum value. The core peer also exposes the small non-GUI hooks needed by scripted callers: startup options for state and mDNS, a local-ready event, direct connection, peer snapshots, and an explicit post-download install policy. Those hooks reuse the same current protocol path and do not add compatibility shims. Test Plan: - `git diff --check` - `just fmt` - `just clippy` - `just test` Refs: BACKLOG.md, FINDINGS.md, IMPL_DECISIONS.md
This commit is contained in:
@@ -8,7 +8,8 @@ use std::{
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use lanspread_db::db::{GameDB, GameFileDescription};
|
||||
use lanspread_db::db::{Game, GameDB, GameFileDescription};
|
||||
use lanspread_proto::GameSummary;
|
||||
use tokio::sync::{RwLock, mpsc::UnboundedSender};
|
||||
|
||||
use crate::{
|
||||
@@ -19,7 +20,6 @@ use crate::{
|
||||
context::{Ctx, OperationGuard, OperationKind},
|
||||
download::download_game_files,
|
||||
events,
|
||||
identity::FEATURE_LIBRARY_DELTA,
|
||||
install,
|
||||
local_games::{
|
||||
LocalLibraryScan,
|
||||
@@ -31,9 +31,10 @@ use crate::{
|
||||
scan_local_library,
|
||||
version_ini_is_regular_file,
|
||||
},
|
||||
network::{announce_games_to_peer, request_game_details_from_peer, send_library_delta},
|
||||
network::{request_game_details_from_peer, request_game_list_from_peer, send_library_delta},
|
||||
peer_db::PeerGameDB,
|
||||
remote_peer::ensure_peer_id_for_addr,
|
||||
services::perform_handshake_with_peer,
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
@@ -197,6 +198,7 @@ pub async fn handle_download_game_files_command(
|
||||
tx_notify_ui: &UnboundedSender<PeerEvent>,
|
||||
id: String,
|
||||
file_descriptions: Vec<GameFileDescription>,
|
||||
install_after_download: bool,
|
||||
) {
|
||||
log::info!("Got PeerCommand::DownloadGameFiles");
|
||||
let games_folder = { ctx.game_dir.read().await.clone() };
|
||||
@@ -262,7 +264,9 @@ pub async fn handle_download_game_files_command(
|
||||
{
|
||||
log::error!("Failed to send DownloadGameFilesFinished event: {e}");
|
||||
}
|
||||
spawn_install_operation(ctx, tx_notify_ui, id.clone());
|
||||
if install_after_download {
|
||||
spawn_install_operation(ctx, tx_notify_ui, id.clone());
|
||||
}
|
||||
} else {
|
||||
log::error!("No trusted peers available after majority validation for game {id}");
|
||||
}
|
||||
@@ -319,19 +323,32 @@ pub async fn handle_download_game_files_command(
|
||||
return;
|
||||
};
|
||||
|
||||
if transition_download_to_install(&ctx_clone, &download_id, prepared.operation_kind)
|
||||
.await
|
||||
{
|
||||
clear_active_download(&ctx_clone, &download_id).await;
|
||||
run_started_install_operation(
|
||||
if install_after_download {
|
||||
if transition_download_to_install(
|
||||
&ctx_clone,
|
||||
&tx_notify_ui_clone,
|
||||
download_id,
|
||||
prepared,
|
||||
&download_id,
|
||||
prepared.operation_kind,
|
||||
)
|
||||
.await;
|
||||
.await
|
||||
{
|
||||
clear_active_download(&ctx_clone, &download_id).await;
|
||||
run_started_install_operation(
|
||||
&ctx_clone,
|
||||
&tx_notify_ui_clone,
|
||||
download_id,
|
||||
prepared,
|
||||
)
|
||||
.await;
|
||||
} else {
|
||||
clear_active_download(&ctx_clone, &download_id).await;
|
||||
}
|
||||
} else {
|
||||
clear_active_download(&ctx_clone, &download_id).await;
|
||||
end_download_operation(&ctx_clone, &download_id).await;
|
||||
if let Err(err) =
|
||||
refresh_local_game(&ctx_clone, &tx_notify_ui_clone, &download_id).await
|
||||
{
|
||||
log::error!("Failed to refresh local library after download: {err}");
|
||||
}
|
||||
}
|
||||
download_state_guard.disarm();
|
||||
}
|
||||
@@ -681,6 +698,69 @@ pub async fn handle_get_peer_count_command(ctx: &Ctx, tx_notify_ui: &UnboundedSe
|
||||
events::emit_peer_count(&ctx.peer_game_db, tx_notify_ui).await;
|
||||
}
|
||||
|
||||
/// Connects to a peer directly, bypassing mDNS discovery.
|
||||
pub async fn handle_connect_peer_command(
|
||||
ctx: &Ctx,
|
||||
tx_notify_ui: &UnboundedSender<PeerEvent>,
|
||||
addr: SocketAddr,
|
||||
) {
|
||||
log::info!("Direct connect command received for {addr}");
|
||||
let peer_id = ctx.peer_id.clone();
|
||||
let local_library = ctx.local_library.clone();
|
||||
let peer_game_db = ctx.peer_game_db.clone();
|
||||
let tx_notify_ui = tx_notify_ui.clone();
|
||||
|
||||
ctx.task_tracker.spawn(async move {
|
||||
if let Err(err) = perform_handshake_with_peer(
|
||||
peer_id,
|
||||
local_library,
|
||||
peer_game_db.clone(),
|
||||
tx_notify_ui.clone(),
|
||||
addr,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
{
|
||||
log::warn!("Failed direct connect to {addr}: {err}");
|
||||
return;
|
||||
}
|
||||
|
||||
if let Err(err) = refresh_direct_peer_games(&peer_game_db, &tx_notify_ui, addr).await {
|
||||
log::warn!("Failed to refresh direct peer games from {addr}: {err}");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async fn refresh_direct_peer_games(
|
||||
peer_game_db: &Arc<RwLock<PeerGameDB>>,
|
||||
tx_notify_ui: &UnboundedSender<PeerEvent>,
|
||||
addr: SocketAddr,
|
||||
) -> eyre::Result<()> {
|
||||
let games = request_game_list_from_peer(addr).await?;
|
||||
let summaries = games.into_iter().map(game_to_summary).collect::<Vec<_>>();
|
||||
let peer_id = ensure_peer_id_for_addr(peer_game_db, addr).await;
|
||||
{
|
||||
let mut db = peer_game_db.write().await;
|
||||
db.update_peer_games(&peer_id, summaries);
|
||||
}
|
||||
events::emit_peer_game_list(peer_game_db, tx_notify_ui).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn game_to_summary(game: Game) -> GameSummary {
|
||||
let availability = game.normalized_availability();
|
||||
GameSummary {
|
||||
id: game.id,
|
||||
name: game.name,
|
||||
size: game.size,
|
||||
downloaded: game.downloaded,
|
||||
installed: game.installed,
|
||||
eti_version: game.eti_game_version,
|
||||
manifest_hash: 0,
|
||||
availability,
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Game announcement helpers
|
||||
// =============================================================================
|
||||
@@ -737,32 +817,17 @@ pub async fn update_and_announce_games(
|
||||
let db = ctx.peer_game_db.read().await;
|
||||
db.peer_identities()
|
||||
.into_iter()
|
||||
.map(|(peer_id, addr)| {
|
||||
let features = db.peer_features(&peer_id);
|
||||
(peer_id, addr, features)
|
||||
})
|
||||
.map(|(_peer_id, addr)| addr)
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
|
||||
for (_peer_id, peer_addr, features) in peer_targets {
|
||||
if features
|
||||
.iter()
|
||||
.any(|feature| feature == FEATURE_LIBRARY_DELTA)
|
||||
{
|
||||
let delta = delta.clone();
|
||||
ctx.task_tracker.spawn(async move {
|
||||
if let Err(e) = send_library_delta(peer_addr, delta).await {
|
||||
log::warn!("Failed to send library delta to {peer_addr}: {e}");
|
||||
}
|
||||
});
|
||||
} else {
|
||||
let games_clone = all_games.clone();
|
||||
ctx.task_tracker.spawn(async move {
|
||||
if let Err(e) = announce_games_to_peer(peer_addr, games_clone).await {
|
||||
log::warn!("Failed to announce games to {peer_addr}: {e}");
|
||||
}
|
||||
});
|
||||
}
|
||||
for peer_addr in peer_targets {
|
||||
let delta = delta.clone();
|
||||
ctx.task_tracker.spawn(async move {
|
||||
if let Err(e) = send_library_delta(peer_addr, delta).await {
|
||||
log::warn!("Failed to send library delta to {peer_addr}: {e}");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -832,6 +897,7 @@ mod tests {
|
||||
CancellationToken::new(),
|
||||
TaskTracker::new(),
|
||||
Arc::new(RwLock::new(HashSet::from(["game".to_string()]))),
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -851,7 +917,7 @@ mod tests {
|
||||
id: id.to_string(),
|
||||
name: id.to_string(),
|
||||
size: 42,
|
||||
downloaded: availability.is_downloaded(),
|
||||
downloaded: availability == Availability::Ready,
|
||||
installed: true,
|
||||
eti_version: Some(version.to_string()),
|
||||
manifest_hash: 7,
|
||||
|
||||
Reference in New Issue
Block a user