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:
@@ -42,7 +42,14 @@ pub use config::{CHUNK_SIZE, MAX_RETRY_COUNT};
|
||||
pub use error::PeerError;
|
||||
pub use install::{UnpackFuture, Unpacker};
|
||||
use lanspread_db::db::{Game, GameFileDescription};
|
||||
pub use peer_db::{MajorityValidationResult, PeerGameDB, PeerId, PeerInfo, PeerUpsert};
|
||||
pub use peer_db::{
|
||||
MajorityValidationResult,
|
||||
PeerGameDB,
|
||||
PeerId,
|
||||
PeerInfo,
|
||||
PeerSnapshot,
|
||||
PeerUpsert,
|
||||
};
|
||||
use tokio::sync::{
|
||||
RwLock,
|
||||
mpsc::{UnboundedReceiver, UnboundedSender},
|
||||
@@ -54,6 +61,7 @@ use crate::{
|
||||
context::Ctx,
|
||||
handlers::{
|
||||
GameDetailSource,
|
||||
handle_connect_peer_command,
|
||||
handle_download_game_files_command,
|
||||
handle_get_game_command,
|
||||
handle_get_peer_count_command,
|
||||
@@ -72,6 +80,8 @@ use crate::{
|
||||
/// Events sent from the peer system to the UI.
|
||||
#[derive(Debug, strum::IntoStaticStr)]
|
||||
pub enum PeerEvent {
|
||||
/// The local QUIC server is listening and ready to accept peer connections.
|
||||
LocalPeerReady { peer_id: String, addr: SocketAddr },
|
||||
/// List of available games from peers.
|
||||
ListGames(Vec<Game>),
|
||||
/// File descriptions for a specific game.
|
||||
@@ -184,6 +194,12 @@ pub enum PeerCommand {
|
||||
id: String,
|
||||
file_descriptions: Vec<GameFileDescription>,
|
||||
},
|
||||
/// Download game files with an explicit install policy.
|
||||
DownloadGameFilesWithOptions {
|
||||
id: String,
|
||||
file_descriptions: Vec<GameFileDescription>,
|
||||
install_after_download: bool,
|
||||
},
|
||||
/// Install already-downloaded archives into `local/`.
|
||||
InstallGame { id: String },
|
||||
/// Remove only the `local/` install for a game.
|
||||
@@ -192,6 +208,26 @@ pub enum PeerCommand {
|
||||
SetGameDir(PathBuf),
|
||||
/// Request the current peer count.
|
||||
GetPeerCount,
|
||||
/// Connect directly to a peer address without waiting for mDNS discovery.
|
||||
ConnectPeer(SocketAddr),
|
||||
}
|
||||
|
||||
/// Optional startup settings for non-GUI callers and tests.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct PeerStartOptions {
|
||||
/// Directory used for peer identity and other state.
|
||||
pub state_dir: Option<PathBuf>,
|
||||
/// Whether to advertise and discover peers via mDNS.
|
||||
pub enable_mdns: bool,
|
||||
}
|
||||
|
||||
impl Default for PeerStartOptions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
state_dir: None,
|
||||
enable_mdns: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
@@ -218,12 +254,36 @@ pub fn start_peer(
|
||||
unpacker: Arc<dyn Unpacker>,
|
||||
catalog: Arc<RwLock<HashSet<String>>>,
|
||||
) -> eyre::Result<PeerRuntimeHandle> {
|
||||
start_peer_with_options(
|
||||
game_dir,
|
||||
tx_notify_ui,
|
||||
peer_game_db,
|
||||
unpacker,
|
||||
catalog,
|
||||
PeerStartOptions::default(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Initialize and start the peer system with explicit startup settings.
|
||||
#[allow(clippy::implicit_hasher)]
|
||||
pub fn start_peer_with_options(
|
||||
game_dir: impl Into<PathBuf>,
|
||||
tx_notify_ui: UnboundedSender<PeerEvent>,
|
||||
peer_game_db: Arc<RwLock<PeerGameDB>>,
|
||||
unpacker: Arc<dyn Unpacker>,
|
||||
catalog: Arc<RwLock<HashSet<String>>>,
|
||||
options: PeerStartOptions,
|
||||
) -> eyre::Result<PeerRuntimeHandle> {
|
||||
let PeerStartOptions {
|
||||
state_dir,
|
||||
enable_mdns,
|
||||
} = options;
|
||||
let game_dir = game_dir.into();
|
||||
log::info!(
|
||||
"Starting peer system with game directory: {}",
|
||||
game_dir.display()
|
||||
);
|
||||
let peer_id = identity::load_or_create_peer_id()?;
|
||||
let peer_id = identity::load_or_create_peer_id(state_dir.as_deref())?;
|
||||
|
||||
let (tx_control, rx_control) = tokio::sync::mpsc::unbounded_channel();
|
||||
|
||||
@@ -236,6 +296,7 @@ pub fn start_peer(
|
||||
game_dir,
|
||||
unpacker,
|
||||
catalog,
|
||||
enable_mdns,
|
||||
))
|
||||
}
|
||||
|
||||
@@ -251,6 +312,7 @@ async fn run_peer(
|
||||
shutdown: CancellationToken,
|
||||
task_tracker: TaskTracker,
|
||||
catalog: Arc<RwLock<HashSet<String>>>,
|
||||
enable_mdns: bool,
|
||||
) -> eyre::Result<()> {
|
||||
let ctx = Ctx::new(
|
||||
peer_game_db,
|
||||
@@ -260,6 +322,7 @@ async fn run_peer(
|
||||
shutdown,
|
||||
task_tracker,
|
||||
catalog,
|
||||
enable_mdns,
|
||||
);
|
||||
if let Err(err) = load_local_library(&ctx, &tx_notify_ui).await {
|
||||
log::error!("Failed to load initial local game database: {err}");
|
||||
@@ -316,7 +379,22 @@ async fn handle_peer_commands(
|
||||
id,
|
||||
file_descriptions,
|
||||
} => {
|
||||
handle_download_game_files_command(ctx, tx_notify_ui, id, file_descriptions).await;
|
||||
handle_download_game_files_command(ctx, tx_notify_ui, id, file_descriptions, true)
|
||||
.await;
|
||||
}
|
||||
PeerCommand::DownloadGameFilesWithOptions {
|
||||
id,
|
||||
file_descriptions,
|
||||
install_after_download,
|
||||
} => {
|
||||
handle_download_game_files_command(
|
||||
ctx,
|
||||
tx_notify_ui,
|
||||
id,
|
||||
file_descriptions,
|
||||
install_after_download,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
PeerCommand::InstallGame { id } => {
|
||||
handle_install_game_command(ctx, tx_notify_ui, id).await;
|
||||
@@ -330,6 +408,9 @@ async fn handle_peer_commands(
|
||||
PeerCommand::GetPeerCount => {
|
||||
handle_get_peer_count_command(ctx, tx_notify_ui).await;
|
||||
}
|
||||
PeerCommand::ConnectPeer(addr) => {
|
||||
handle_connect_peer_command(ctx, tx_notify_ui, addr).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user