fix(peer): coalesce full UI view publications

Keep one queued and one replaceable pending snapshot for remote-library and Call-to-Play views while preserving lifecycle-event FIFO delivery. Generation barriers and fenced drains prevent stale nonempty views from crossing disable or acknowledgement boundaries.

Test Plan:
- just test
- just clippy
- focused burst, lifecycle ordering, fence, repeated-barrier, and stale-view tests
- independent ordering review
- git diff --check
This commit is contained in:
ddidderr committed 2026-09-12 13:06:10 +02:00
1 parent 470e119515
commit 37420e26ed
17 files changed
+828 -251

No files matched your search

+104 -104
View File
@@ -14,7 +14,7 @@ use lanspread_db::{
db::GameDB,
};
use lanspread_proto::PeerEndpoint;
use tokio::sync::mpsc::UnboundedSender;
use crate::PeerEventSender;
use tokio_util::sync::CancellationToken;
#[cfg(test)]
@@ -82,7 +82,7 @@ where
async fn register_download_attempt(
ctx: &Ctx,
tx_notify_ui: &UnboundedSender<PeerEvent>,
tx_notify_ui: &PeerEventSender,
key: DownloadAttemptKey,
cancellation: CancellationToken,
) -> DownloadAttemptStatus {
@@ -202,7 +202,7 @@ impl fmt::Display for StreamDownloadError {
}
/// Handles the `ListGames` command.
pub async fn handle_list_games_command(ctx: &Ctx, tx_notify_ui: &UnboundedSender<PeerEvent>) {
pub async fn handle_list_games_command(ctx: &Ctx, tx_notify_ui: &PeerEventSender) {
log::info!("ListGames command received");
events::emit_peer_game_list(&ctx.peer_game_db, tx_notify_ui).await;
}
@@ -211,7 +211,7 @@ pub async fn handle_list_games_command(ctx: &Ctx, tx_notify_ui: &UnboundedSender
#[allow(clippy::too_many_lines)]
pub async fn handle_download_game_files_command(
ctx: &Ctx,
tx_notify_ui: &UnboundedSender<PeerEvent>,
tx_notify_ui: &PeerEventSender,
id: String,
install_after_download: bool,
) {
@@ -494,7 +494,7 @@ pub async fn handle_download_game_files_command(
fn finish_cached_download(
ctx: &Ctx,
tx_notify_ui: &UnboundedSender<PeerEvent>,
tx_notify_ui: &PeerEventSender,
attempt: &DownloadAttemptKey,
install_after_download: bool,
target: OperationTarget,
@@ -547,7 +547,7 @@ async fn settle_download_completion(
/// Handles the `InstallGame` command.
pub async fn handle_install_game_command(
ctx: &Ctx,
tx_notify_ui: &UnboundedSender<PeerEvent>,
tx_notify_ui: &PeerEventSender,
id: String,
) {
let games_folder = ctx.game_dir.read().await.clone();
@@ -575,7 +575,7 @@ async fn stream_install_target_is_ready(ctx: &Ctx, target: &OperationTarget) ->
async fn begin_stream_install_operation(
ctx: &Ctx,
tx_notify_ui: &UnboundedSender<PeerEvent>,
tx_notify_ui: &PeerEventSender,
target: &OperationTarget,
attempt: &DownloadAttemptKey,
) -> bool {
@@ -631,7 +631,7 @@ async fn begin_stream_install_operation(
pub async fn handle_stream_install_game_command(
ctx: &Ctx,
tx_notify_ui: &UnboundedSender<PeerEvent>,
tx_notify_ui: &PeerEventSender,
id: String,
settings: StreamInstallSettings,
) {
@@ -742,7 +742,7 @@ pub async fn handle_stream_install_game_command(
struct StreamInstallOperation {
ctx: Ctx,
tx_notify_ui: UnboundedSender<PeerEvent>,
tx_notify_ui: PeerEventSender,
target: OperationTarget,
manifest: Arc<CatalogContentManifest>,
settings: StreamInstallSettings,
@@ -768,7 +768,7 @@ async fn stream_install_sources(
async fn select_stream_install_sources_or_finish(
ctx: &Ctx,
tx_notify_ui: &UnboundedSender<PeerEvent>,
tx_notify_ui: &PeerEventSender,
target: &OperationTarget,
manifest: &CatalogContentManifest,
download_guard: OperationGuard,
@@ -798,7 +798,7 @@ async fn select_stream_install_sources_or_finish(
/// Handles the `UninstallGame` command.
pub async fn handle_uninstall_game_command(
ctx: &Ctx,
tx_notify_ui: &UnboundedSender<PeerEvent>,
tx_notify_ui: &PeerEventSender,
id: String,
) {
let games_folder = ctx.game_dir.read().await.clone();
@@ -812,7 +812,7 @@ pub async fn handle_uninstall_game_command(
pub async fn handle_remove_downloaded_game_command(
ctx: &Ctx,
tx_notify_ui: &UnboundedSender<PeerEvent>,
tx_notify_ui: &PeerEventSender,
id: String,
) {
let games_folder = ctx.game_dir.read().await.clone();
@@ -826,7 +826,7 @@ pub async fn handle_remove_downloaded_game_command(
pub async fn handle_cancel_download_command(
ctx: &Ctx,
_tx_notify_ui: &UnboundedSender<PeerEvent>,
_tx_notify_ui: &PeerEventSender,
id: String,
) {
let signal = ctx.active_downloads.read().await.get(&id).cloned();
@@ -945,7 +945,7 @@ async fn run_stream_install_operation(operation: StreamInstallOperation) {
async fn finish_stream_receive_error(
ctx: &Ctx,
tx_notify_ui: &UnboundedSender<PeerEvent>,
tx_notify_ui: &PeerEventSender,
target: &OperationTarget,
download_guard: OperationGuard,
download_attempt: &DownloadAttemptStatus,
@@ -975,7 +975,7 @@ async fn finish_stream_receive_error(
struct StreamInstallPromotionPreparation<'a> {
ctx: &'a Ctx,
tx_notify_ui: &'a UnboundedSender<PeerEvent>,
tx_notify_ui: &'a PeerEventSender,
target: &'a OperationTarget,
transaction: install::StreamedInstallTransaction,
settings: &'a StreamInstallSettings,
@@ -1055,7 +1055,7 @@ async fn prepare_streamed_install_for_promotion(
struct StreamInstallReceiveRequest<'a> {
ctx: &'a Ctx,
tx_notify_ui: &'a UnboundedSender<PeerEvent>,
tx_notify_ui: &'a PeerEventSender,
target: &'a OperationTarget,
manifest: &'a Arc<CatalogContentManifest>,
sources: &'a [PeerEndpoint],
@@ -1218,7 +1218,7 @@ async fn receive_streamed_install_from_peers(
async fn finish_failed_stream_download(
ctx: &Ctx,
tx_notify_ui: &UnboundedSender<PeerEvent>,
tx_notify_ui: &PeerEventSender,
target: &OperationTarget,
guard: OperationGuard,
status: &DownloadAttemptStatus,
@@ -1243,7 +1243,7 @@ async fn finish_failed_stream_download(
struct StreamInstallCommit {
ctx: Ctx,
tx_notify_ui: UnboundedSender<PeerEvent>,
tx_notify_ui: PeerEventSender,
target: OperationTarget,
transaction: install::StreamedInstallTransaction,
manifest: Arc<CatalogContentManifest>,
@@ -1346,7 +1346,7 @@ fn promote_streamed_install(
fn spawn_install_operation(
ctx: &Ctx,
tx_notify_ui: &UnboundedSender<PeerEvent>,
tx_notify_ui: &PeerEventSender,
target: OperationTarget,
) {
let ctx = ctx.clone();
@@ -1358,7 +1358,7 @@ fn spawn_install_operation(
async fn run_install_operation(
ctx: &Ctx,
tx_notify_ui: &UnboundedSender<PeerEvent>,
tx_notify_ui: &PeerEventSender,
target: OperationTarget,
) {
let id = target.game_id().to_owned();
@@ -1442,7 +1442,7 @@ struct PreparedInstallOperation {
async fn prepare_install_operation(
ctx: &Ctx,
tx_notify_ui: &UnboundedSender<PeerEvent>,
tx_notify_ui: &PeerEventSender,
target: &OperationTarget,
) -> Option<PreparedInstallOperation> {
let id = target.game_id();
@@ -1499,7 +1499,7 @@ async fn prepare_install_operation(
async fn revalidate_install_operation(
ctx: &Ctx,
tx_notify_ui: &UnboundedSender<PeerEvent>,
tx_notify_ui: &PeerEventSender,
target: &OperationTarget,
expected_kind: OperationKind,
) -> Option<PreparedInstallOperation> {
@@ -1522,7 +1522,7 @@ async fn revalidate_install_operation(
async fn run_started_install_operation(
ctx: &Ctx,
tx_notify_ui: &UnboundedSender<PeerEvent>,
tx_notify_ui: &PeerEventSender,
target: OperationTarget,
prepared: PreparedInstallOperation,
operation_guard: OperationGuard,
@@ -1599,7 +1599,7 @@ async fn run_started_install_operation(
async fn run_uninstall_operation(
ctx: &Ctx,
tx_notify_ui: &UnboundedSender<PeerEvent>,
tx_notify_ui: &PeerEventSender,
target: OperationTarget,
) {
let id = target.game_id().to_owned();
@@ -1696,7 +1696,7 @@ async fn run_uninstall_operation(
async fn run_remove_downloaded_operation(
ctx: &Ctx,
tx_notify_ui: &UnboundedSender<PeerEvent>,
tx_notify_ui: &PeerEventSender,
target: OperationTarget,
) {
let id = target.game_id().to_owned();
@@ -1804,7 +1804,7 @@ enum BeginOperationResult {
async fn begin_operation(
ctx: &Ctx,
tx_notify_ui: &UnboundedSender<PeerEvent>,
tx_notify_ui: &PeerEventSender,
target: &OperationTarget,
operation: OperationKind,
) -> BeginOperationResult {
@@ -1820,7 +1820,7 @@ async fn begin_operation(
async fn begin_operation_with_drain_timeout(
ctx: &Ctx,
tx_notify_ui: &UnboundedSender<PeerEvent>,
tx_notify_ui: &PeerEventSender,
target: &OperationTarget,
operation: OperationKind,
drain_timeout: Duration,
@@ -2009,7 +2009,7 @@ async fn cancel_and_wait_for_outbound_transfers(
async fn transition_download_to_install(
ctx: &Ctx,
tx_notify_ui: &UnboundedSender<PeerEvent>,
tx_notify_ui: &PeerEventSender,
id: &str,
operation: OperationKind,
) -> bool {
@@ -2042,7 +2042,7 @@ async fn transition_download_to_install(
transitioned
}
async fn end_operation(ctx: &Ctx, tx_notify_ui: &UnboundedSender<PeerEvent>, id: &str) {
async fn end_operation(ctx: &Ctx, tx_notify_ui: &PeerEventSender, id: &str) {
if ctx.active_operations.write().await.remove(id).is_some() {
events::emit_active_operations(&ctx.active_operations, tx_notify_ui).await;
}
@@ -2059,7 +2059,7 @@ async fn clear_active_download(ctx: &Ctx, attempt: &DownloadAttemptKey) {
}
fn send_download_failed(
tx_notify_ui: &UnboundedSender<PeerEvent>,
tx_notify_ui: &PeerEventSender,
attempt: &DownloadAttemptKey,
reason: DownloadFailureReason,
) {
@@ -2074,7 +2074,7 @@ fn send_download_failed(
async fn end_download_operation(
ctx: &Ctx,
tx_notify_ui: &UnboundedSender<PeerEvent>,
tx_notify_ui: &PeerEventSender,
attempt: &DownloadAttemptKey,
) {
clear_active_download(ctx, attempt).await;
@@ -2083,7 +2083,7 @@ async fn end_download_operation(
async fn settle_target_state(
ctx: &Ctx,
tx_notify_ui: &UnboundedSender<PeerEvent>,
tx_notify_ui: &PeerEventSender,
target: &OperationTarget,
) -> eyre::Result<()> {
install::recover_game_root(
@@ -2097,7 +2097,7 @@ async fn settle_target_state(
async fn settle_and_end_operation(
ctx: &Ctx,
tx_notify_ui: &UnboundedSender<PeerEvent>,
tx_notify_ui: &PeerEventSender,
target: &OperationTarget,
guard: OperationGuard,
label: &str,
@@ -2121,7 +2121,7 @@ async fn settle_and_end_operation(
async fn settle_and_end_download(
ctx: &Ctx,
tx_notify_ui: &UnboundedSender<PeerEvent>,
tx_notify_ui: &PeerEventSender,
target: &OperationTarget,
guard: OperationGuard,
attempt: &DownloadAttemptKey,
@@ -2142,7 +2142,7 @@ async fn settle_and_end_download(
async fn finish_successful_download_after_refresh(
ctx: &Ctx,
tx_notify_ui: &UnboundedSender<PeerEvent>,
tx_notify_ui: &PeerEventSender,
target: &OperationTarget,
guard: OperationGuard,
status: &DownloadAttemptStatus,
@@ -2164,7 +2164,7 @@ async fn finish_successful_download_after_refresh(
async fn finish_failed_download_after_refresh(
ctx: &Ctx,
tx_notify_ui: &UnboundedSender<PeerEvent>,
tx_notify_ui: &PeerEventSender,
target: &OperationTarget,
guard: OperationGuard,
status: &DownloadAttemptStatus,
@@ -2193,7 +2193,7 @@ fn catalog_contains(ctx: &Ctx, id: &str) -> bool {
async fn begin_local_recovery(
ctx: &Ctx,
tx_notify_ui: &UnboundedSender<PeerEvent>,
tx_notify_ui: &PeerEventSender,
game_dir: &Path,
force_empty_snapshot: bool,
) -> eyre::Result<()> {
@@ -2225,7 +2225,7 @@ async fn begin_local_recovery(
/// quarantined. `Err` is returned only before changing the configured root.
pub async fn handle_set_game_dir_command(
ctx: &Ctx,
tx_notify_ui: &UnboundedSender<PeerEvent>,
tx_notify_ui: &PeerEventSender,
game_dir: PathBuf,
) -> Result<PathBuf, String> {
handle_set_game_dir_command_with_drain_timeout(
@@ -2239,7 +2239,7 @@ pub async fn handle_set_game_dir_command(
async fn handle_set_game_dir_command_with_drain_timeout(
ctx: &Ctx,
tx_notify_ui: &UnboundedSender<PeerEvent>,
tx_notify_ui: &PeerEventSender,
requested_game_dir: PathBuf,
drain_timeout: Duration,
) -> Result<PathBuf, String> {
@@ -2344,14 +2344,14 @@ async fn handle_set_game_dir_command_with_drain_timeout(
/// Loads the configured local library and announces the result.
pub async fn load_local_library(
ctx: &Ctx,
tx_notify_ui: &UnboundedSender<PeerEvent>,
tx_notify_ui: &PeerEventSender,
) -> eyre::Result<()> {
load_local_library_with_policy(ctx, tx_notify_ui, LocalLibraryEventPolicy::OnChange).await
}
async fn load_local_library_with_policy(
ctx: &Ctx,
tx_notify_ui: &UnboundedSender<PeerEvent>,
tx_notify_ui: &PeerEventSender,
event_policy: LocalLibraryEventPolicy,
) -> eyre::Result<()> {
let game_dir = { ctx.game_dir.read().await.clone() };
@@ -2385,7 +2385,7 @@ async fn load_local_library_with_policy(
async fn scan_and_announce_local_library(
ctx: &Ctx,
tx_notify_ui: &UnboundedSender<PeerEvent>,
tx_notify_ui: &PeerEventSender,
game_dir: &Path,
event_policy: LocalLibraryEventPolicy,
recovery_failed_ids: &HashSet<String>,
@@ -2410,7 +2410,7 @@ async fn scan_and_announce_local_library(
/// active-operation snapshot, while preserving freeze behavior for other games.
async fn refresh_local_game_for_ending_operation(
ctx: &Ctx,
tx_notify_ui: &UnboundedSender<PeerEvent>,
tx_notify_ui: &PeerEventSender,
target: &OperationTarget,
) -> eyre::Result<()> {
let catalog = ctx.catalog.catalog();
@@ -2456,7 +2456,7 @@ async fn active_operation_ids(ctx: &Ctx) -> HashSet<String> {
}
/// Handles the `GetPeerCount` command.
pub async fn handle_get_peer_count_command(ctx: &Ctx, tx_notify_ui: &UnboundedSender<PeerEvent>) {
pub async fn handle_get_peer_count_command(ctx: &Ctx, tx_notify_ui: &PeerEventSender) {
log::info!("GetPeerCount command received");
events::emit_peer_count(&ctx.peer_game_db, tx_notify_ui).await;
}
@@ -2464,7 +2464,7 @@ pub async fn handle_get_peer_count_command(ctx: &Ctx, tx_notify_ui: &UnboundedSe
/// Connects to a peer directly, bypassing mDNS discovery.
pub async fn handle_connect_peer_command(
ctx: &Ctx,
tx_notify_ui: &UnboundedSender<PeerEvent>,
tx_notify_ui: &PeerEventSender,
endpoint: PeerEndpoint,
) {
log::info!("Direct connect command received for {}", endpoint.addr);
@@ -2509,7 +2509,7 @@ pub async fn handle_connect_peer_command(
/// Updates the local game database and announces changes to peers.
pub async fn update_and_announce_games(
ctx: &Ctx,
tx_notify_ui: &UnboundedSender<PeerEvent>,
tx_notify_ui: &PeerEventSender,
scan: LocalLibraryScan,
) {
let _ = update_and_announce_games_with_policy(
@@ -2524,7 +2524,7 @@ pub async fn update_and_announce_games(
async fn update_and_announce_games_with_policy(
ctx: &Ctx,
tx_notify_ui: &UnboundedSender<PeerEvent>,
tx_notify_ui: &PeerEventSender,
scan: LocalLibraryScan,
event_policy: LocalLibraryEventPolicy,
ending_operation_id: Option<&str>,
@@ -2659,7 +2659,7 @@ mod tests {
PeerId,
RuntimeSessionId,
};
use tokio::sync::{RwLock, mpsc, oneshot};
use tokio::sync::{RwLock, oneshot};
use tokio_util::{sync::CancellationToken, task::TaskTracker};
use super::*;
@@ -2834,7 +2834,7 @@ mod tests {
async fn register_test_download(
ctx: &Ctx,
tx_notify_ui: &UnboundedSender<PeerEvent>,
tx_notify_ui: &PeerEventSender,
) -> (DownloadAttemptStatus, CancellationToken) {
let cancellation = CancellationToken::new();
let status = register_download_attempt(
@@ -2868,7 +2868,7 @@ mod tests {
#[test]
fn cancelled_download_owner_does_not_emit_failed_event() {
let (tx, mut rx) = mpsc::unbounded_channel();
let (tx, mut rx) = crate::peer_event_channel();
let status = DownloadAttemptStatus::new(
DownloadAttemptKey::next("game".to_owned()),
CancellationToken::new(),
@@ -2885,7 +2885,7 @@ mod tests {
#[test]
fn uncancelled_download_error_emits_failed_event() {
let (tx, mut rx) = mpsc::unbounded_channel();
let (tx, mut rx) = crate::peer_event_channel();
let attempt = DownloadAttemptKey::next("game".to_owned());
send_download_failed(&tx, &attempt, DownloadFailureReason::OperationFailed);
@@ -2959,7 +2959,7 @@ mod tests {
);
seed_exact_download_ownership(&ctx, games.path(), content_id).await;
let peer_db = ctx.peer_game_db.write().await;
let (tx, mut rx) = mpsc::unbounded_channel();
let (tx, mut rx) = crate::peer_event_channel();
tokio::time::timeout(
Duration::from_secs(1),
@@ -2993,7 +2993,7 @@ mod tests {
NetworkControl::disabled_for_test(),
);
let peer_db = ctx.peer_game_db.write().await;
let (tx, mut rx) = mpsc::unbounded_channel();
let (tx, mut rx) = crate::peer_event_channel();
tokio::time::timeout(
Duration::from_secs(1),
@@ -3033,7 +3033,7 @@ mod tests {
NetworkControl::disabled_for_test(),
);
let peer_db = ctx.peer_game_db.write().await;
let (tx, mut rx) = mpsc::unbounded_channel();
let (tx, mut rx) = crate::peer_event_channel();
tokio::time::timeout(
Duration::from_secs(1),
@@ -3070,7 +3070,7 @@ mod tests {
NetworkControl::disabled_for_test(),
);
let peer_db = ctx.peer_game_db.write().await;
let (tx, mut rx) = mpsc::unbounded_channel();
let (tx, mut rx) = crate::peer_event_channel();
tokio::time::timeout(
Duration::from_secs(1),
@@ -3100,7 +3100,7 @@ mod tests {
.current_publication()
.expect("initial Call-to-Play projection should load")
.view;
let (tx, mut rx) = mpsc::unbounded_channel();
let (tx, mut rx) = crate::peer_event_channel();
let (reply_tx, reply_rx) = oneshot::channel();
tokio::time::timeout(
@@ -3136,7 +3136,7 @@ mod tests {
let games = TempDir::new("lanspread-handler-enabled-call-to-play");
let ctx = test_ctx(games.path().to_path_buf());
let store = ctx.call_to_play.write().await;
let (tx, mut rx) = mpsc::unbounded_channel();
let (tx, mut rx) = crate::peer_event_channel();
let (reply_tx, mut reply_rx) = oneshot::channel();
let task = tokio::spawn({
let ctx = ctx.clone();
@@ -3177,7 +3177,7 @@ mod tests {
async fn streamed_install_without_extracted_catalog_manifest_fails_before_admission() {
let games = TempDir::new("lanspread-handler-stream-capability");
let ctx = test_ctx(games.path().to_path_buf());
let (tx, mut rx) = mpsc::unbounded_channel();
let (tx, mut rx) = crate::peer_event_channel();
handle_stream_install_game_command(
&ctx,
@@ -3226,7 +3226,7 @@ mod tests {
)
.await
);
let (tx, mut rx) = mpsc::unbounded_channel();
let (tx, mut rx) = crate::peer_event_channel();
handle_download_game_files_command(&ctx, &tx, "game".to_string(), false).await;
@@ -3264,7 +3264,7 @@ mod tests {
.write()
.await
.insert("game".to_owned(), OperationKind::Downloading);
let (tx, mut rx) = mpsc::unbounded_channel();
let (tx, mut rx) = crate::peer_event_channel();
handle_download_game_files_command(&ctx, &tx, "game".to_owned(), false).await;
@@ -3401,7 +3401,7 @@ mod tests {
games.path().to_path_buf(),
catalog_bundle([("broken", "20250101"), ("healthy", "20250101")]),
);
let (tx, _rx) = mpsc::unbounded_channel();
let (tx, _rx) = crate::peer_event_channel();
let error = load_local_library(&ctx, &tx)
.await
@@ -3445,7 +3445,7 @@ mod tests {
.expect("game-root symlink should be created");
let ctx = test_ctx(games.path().to_path_buf());
let (tx, _rx) = mpsc::unbounded_channel();
let (tx, _rx) = crate::peer_event_channel();
let error = load_local_library(&ctx, &tx)
.await
@@ -3470,7 +3470,7 @@ mod tests {
async fn operation_admission_rejects_recovering_and_failed_games() {
let games = TempDir::new("lanspread-handler-recovery-admission");
let ctx = test_ctx(games.path().to_path_buf());
let (tx, _rx) = mpsc::unbounded_channel();
let (tx, _rx) = crate::peer_event_channel();
ctx.recovery_quarantine.begin(games.path().to_path_buf());
let target = operation_target(games.path());
@@ -3496,7 +3496,7 @@ mod tests {
let games = TempDir::new("lanspread-handler-download-refresh-failure");
write_file(&games.game_root(), b"not a directory");
let ctx = test_ctx(games.path().to_path_buf());
let (tx, mut rx) = mpsc::unbounded_channel();
let (tx, mut rx) = crate::peer_event_channel();
let (status, cancel) = register_test_download(&ctx, &tx).await;
ctx.active_operations
.write()
@@ -3519,14 +3519,14 @@ mod tests {
assert_no_event(&mut rx).await;
}
async fn recv_event(rx: &mut mpsc::UnboundedReceiver<PeerEvent>) -> PeerEvent {
async fn recv_event(rx: &mut crate::PeerEventReceiver) -> PeerEvent {
tokio::time::timeout(Duration::from_secs(1), rx.recv())
.await
.expect("event should arrive")
.expect("event channel should remain open")
}
async fn assert_no_event(rx: &mut mpsc::UnboundedReceiver<PeerEvent>) {
async fn assert_no_event(rx: &mut crate::PeerEventReceiver) {
assert!(
tokio::time::timeout(Duration::from_millis(50), rx.recv())
.await
@@ -3535,7 +3535,7 @@ mod tests {
);
}
fn drain_events(rx: &mut mpsc::UnboundedReceiver<PeerEvent>) -> Vec<PeerEvent> {
fn drain_events(rx: &mut crate::PeerEventReceiver) -> Vec<PeerEvent> {
let mut events = Vec::new();
while let Ok(event) = rx.try_recv() {
events.push(event);
@@ -3682,7 +3682,7 @@ mod tests {
let state = TempDir::new("lanspread-handler-stream-retry-setup-state");
std::fs::create_dir_all(games.game_root().join("local"))
.expect("installed tree should be created");
let (tx, mut rx) = mpsc::unbounded_channel();
let (tx, mut rx) = crate::peer_event_channel();
let status = DownloadAttemptStatus::new(
DownloadAttemptKey::next("game".to_owned()),
CancellationToken::new(),
@@ -3719,7 +3719,7 @@ mod tests {
write_file(&root.join("game.eti"), b"archive");
let ctx = test_ctx(temp.path().to_path_buf());
let (tx, mut rx) = mpsc::unbounded_channel();
let (tx, mut rx) = crate::peer_event_channel();
let catalog = ctx.catalog.catalog();
// 1. Initial scan: the game is ready and announced
@@ -3845,7 +3845,7 @@ mod tests {
.expect("catalog construction should defer manifest body parsing"),
);
let ctx = test_ctx_with_catalog(games.path().to_path_buf(), catalog);
let (tx, mut rx) = mpsc::unbounded_channel();
let (tx, mut rx) = crate::peer_event_channel();
let scan = scan_local_library(games.path(), ctx.state_dir.as_ref(), ctx.catalog.catalog())
.await
.expect("ready game should scan before manifest priming");
@@ -3884,7 +3884,7 @@ mod tests {
);
let ctx = test_ctx(current.path().to_path_buf());
let (tx, mut rx) = mpsc::unbounded_channel();
let (tx, mut rx) = crate::peer_event_channel();
let catalog = ctx.catalog.catalog();
let current_scan = scan_local_library(current.path(), ctx.state_dir.as_ref(), catalog)
@@ -3921,7 +3921,7 @@ mod tests {
write_file(&root.join("game.eti"), b"archive");
let ctx = test_ctx(temp.path().to_path_buf());
let (tx, mut rx) = mpsc::unbounded_channel();
let (tx, mut rx) = crate::peer_event_channel();
let catalog = ctx.catalog.catalog();
let older_scan = scan_local_library(temp.path(), ctx.state_dir.as_ref(), catalog)
.await
@@ -3973,7 +3973,7 @@ mod tests {
write_file(&root.join("game.eti"), b"archive");
let ctx = test_ctx(temp.path().to_path_buf());
let (tx, mut rx) = mpsc::unbounded_channel();
let (tx, mut rx) = crate::peer_event_channel();
let target = operation_target(temp.path());
assert_eq!(
@@ -3997,7 +3997,7 @@ mod tests {
write_file(&root.join("game.eti"), b"archive");
let ctx = test_ctx(temp.path().to_path_buf());
let (tx, mut rx) = mpsc::unbounded_channel();
let (tx, mut rx) = crate::peer_event_channel();
let scan = scan_local_library(temp.path(), ctx.state_dir.as_ref(), ctx.catalog.catalog())
.await
.expect("initial scan should succeed");
@@ -4057,7 +4057,7 @@ mod tests {
write_file(&root.join("game.eti"), b"archive");
let ctx = test_ctx(temp.path().to_path_buf());
let (tx, mut rx) = mpsc::unbounded_channel();
let (tx, mut rx) = crate::peer_event_channel();
let target = operation_target(temp.path());
let scan = scan_local_library(temp.path(), ctx.state_dir.as_ref(), ctx.catalog.catalog())
.await
@@ -4122,7 +4122,7 @@ mod tests {
write_file(&root.join("game.eti"), b"archive");
let ctx = test_ctx(temp.path().to_path_buf());
let (tx, mut rx) = mpsc::unbounded_channel();
let (tx, mut rx) = crate::peer_event_channel();
let scan = scan_local_library(temp.path(), ctx.state_dir.as_ref(), ctx.catalog.catalog())
.await
.expect("initial scan should succeed");
@@ -4195,7 +4195,7 @@ mod tests {
write_file(&root.join("game.eti"), b"archive");
let ctx = test_ctx(temp.path().to_path_buf());
let (tx, mut rx) = mpsc::unbounded_channel();
let (tx, mut rx) = crate::peer_event_channel();
let catalog = ctx.catalog.catalog();
let scan = scan_local_library(temp.path(), ctx.state_dir.as_ref(), catalog)
@@ -4221,7 +4221,7 @@ mod tests {
write_file(&root.join("local").join("old.txt"), b"old");
let ctx = test_ctx(temp.path().to_path_buf());
let (tx, mut rx) = mpsc::unbounded_channel();
let (tx, mut rx) = crate::peer_event_channel();
let catalog = ctx.catalog.catalog();
let scan = scan_local_library(temp.path(), ctx.state_dir.as_ref(), catalog)
.await
@@ -4256,7 +4256,7 @@ mod tests {
write_file(&root.join("game.eti"), b"archive");
let ctx = test_ctx(temp.path().to_path_buf());
let (tx, mut rx) = mpsc::unbounded_channel();
let (tx, mut rx) = crate::peer_event_channel();
run_install_operation(&ctx, &tx, operation_target(temp.path())).await;
@@ -4281,7 +4281,7 @@ mod tests {
write_file(&root.join("game.eti"), b"archive");
let ctx = test_ctx(temp.path().to_path_buf());
let target = operation_target(temp.path());
let (tx, mut rx) = mpsc::unbounded_channel();
let (tx, mut rx) = crate::peer_event_channel();
let prepared = prepare_install_operation(&ctx, &tx, &target)
.await
@@ -4334,7 +4334,7 @@ mod tests {
write_file(&root.join("game.eti"), b"archive");
let ctx = test_ctx(temp.path().to_path_buf());
let target = operation_target(temp.path());
let (tx, mut rx) = mpsc::unbounded_channel();
let (tx, mut rx) = crate::peer_event_channel();
assert!(stream_install_target_is_ready(&ctx, &target).await);
assert_eq!(
@@ -4383,7 +4383,7 @@ mod tests {
write_file(&root.join("game.eti"), b"archive");
let ctx = test_ctx(temp.path().to_path_buf());
let (prepare_tx, _prepare_rx) = mpsc::unbounded_channel();
let (prepare_tx, _prepare_rx) = crate::peer_event_channel();
let (download_status, _download_cancel) = register_test_download(&ctx, &prepare_tx).await;
ctx.active_operations
.write()
@@ -4394,7 +4394,7 @@ mod tests {
.await
.expect("downloaded game should be installable");
let read_guard = ctx.active_operations.read().await;
let (tx, mut rx) = mpsc::unbounded_channel();
let (tx, mut rx) = crate::peer_event_channel();
let install_task = tokio::spawn({
let ctx = ctx.clone();
@@ -4445,7 +4445,7 @@ mod tests {
async fn cancel_download_command_only_cancels_active_token() {
let temp = TempDir::new("lanspread-handler-cancel-download");
let ctx = test_ctx(temp.path().to_path_buf());
let (tx, mut rx) = mpsc::unbounded_channel();
let (tx, mut rx) = crate::peer_event_channel();
let (_status, cancel) = register_test_download(&ctx, &tx).await;
ctx.active_operations
.write()
@@ -4472,7 +4472,7 @@ mod tests {
write_file(&root.join("local").join("old.txt"), b"old");
let ctx = test_ctx(temp.path().to_path_buf());
let (tx, mut rx) = mpsc::unbounded_channel();
let (tx, mut rx) = crate::peer_event_channel();
run_install_operation(&ctx, &tx, operation_target(temp.path())).await;
@@ -4497,7 +4497,7 @@ mod tests {
write_file(&root.join("game.eti"), b"old archive");
let ctx = test_ctx(temp.path().to_path_buf());
let (tx, mut rx) = mpsc::unbounded_channel();
let (tx, mut rx) = crate::peer_event_channel();
run_install_operation(&ctx, &tx, operation_target(temp.path())).await;
assert_active_update(
@@ -4560,7 +4560,7 @@ mod tests {
write_file(&root.join("local").join("old.txt"), b"old");
let ctx = test_ctx(temp.path().to_path_buf());
let (tx, mut rx) = mpsc::unbounded_channel();
let (tx, mut rx) = crate::peer_event_channel();
run_uninstall_operation(&ctx, &tx, operation_target(temp.path())).await;
@@ -4592,7 +4592,7 @@ mod tests {
&["game.eti"],
)
.await;
let (tx, mut rx) = mpsc::unbounded_channel();
let (tx, mut rx) = crate::peer_event_channel();
let catalog = ctx.catalog.catalog();
let scan = scan_local_library(temp.path(), ctx.state_dir.as_ref(), catalog)
.await
@@ -4630,7 +4630,7 @@ mod tests {
write_file(&root.join("game.eti"), b"archive");
}
let ctx = test_ctx(current.path().to_path_buf());
let (tx, mut rx) = mpsc::unbounded_channel();
let (tx, mut rx) = crate::peer_event_channel();
// On the current-thread test runtime, this ready lock acquisition does
// not yield to the newly spawned command. Move the configured root
@@ -4671,7 +4671,7 @@ mod tests {
write_file(&root.join("local/payload.txt"), b"installed");
}
let ctx = test_ctx(current.path().to_path_buf());
let (tx, mut rx) = mpsc::unbounded_channel();
let (tx, mut rx) = crate::peer_event_channel();
handle_uninstall_game_command(&ctx, &tx, "game".to_string()).await;
*ctx.game_dir.write().await = next.path().to_path_buf();
@@ -4715,7 +4715,7 @@ mod tests {
&["game.eti"],
)
.await;
let (tx, mut rx) = mpsc::unbounded_channel();
let (tx, mut rx) = crate::peer_event_channel();
handle_remove_downloaded_game_command(&ctx, &tx, "game".to_string()).await;
*ctx.game_dir.write().await = next.path().to_path_buf();
@@ -4757,7 +4757,7 @@ mod tests {
.write()
.await
.insert("game".to_string(), OperationKind::Downloading);
let (tx, _rx) = mpsc::unbounded_channel();
let (tx, _rx) = crate::peer_event_channel();
let error = handle_set_game_dir_command(&ctx, &tx, next.path().to_path_buf())
.await
@@ -4778,7 +4778,7 @@ mod tests {
symlink(current.path(), &alias).expect("same-root alias should be created");
let current = std::fs::canonicalize(current.path()).expect("root should canonicalize");
let ctx = test_ctx(current.clone());
let (tx, _rx) = mpsc::unbounded_channel();
let (tx, _rx) = crate::peer_event_channel();
let accepted = handle_set_game_dir_command(&ctx, &tx, alias)
.await
@@ -4801,7 +4801,7 @@ mod tests {
let current = std::fs::canonicalize(current.path()).expect("root should canonicalize");
let next = std::fs::canonicalize(next.path()).expect("new root should canonicalize");
let ctx = test_ctx(current);
let (tx, _rx) = mpsc::unbounded_channel();
let (tx, _rx) = crate::peer_event_channel();
let accepted = handle_set_game_dir_command(&ctx, &tx, alias.clone())
.await
@@ -4817,7 +4817,7 @@ mod tests {
let current = TempDir::new("lanspread-handler-invalid-dir-current");
let missing = current.path().join("missing");
let ctx = test_ctx(current.path().to_path_buf());
let (tx, mut rx) = mpsc::unbounded_channel();
let (tx, mut rx) = crate::peer_event_channel();
let error = handle_set_game_dir_command(&ctx, &tx, missing)
.await
@@ -4837,7 +4837,7 @@ mod tests {
let candidate_path =
std::fs::canonicalize(candidate.path()).expect("candidate root should canonicalize");
let ctx = test_ctx(current_path.clone());
let (tx, mut rx) = mpsc::unbounded_channel();
let (tx, mut rx) = crate::peer_event_channel();
let published = summary("game", "20250101", Availability::Ready);
ctx.local_library
.write()
@@ -4884,7 +4884,7 @@ mod tests {
let temp = TempDir::new("lanspread-handler-same-dir");
write_file(&temp.game_root().join(".version.ini.tmp"), b"tmp");
let ctx = test_ctx(temp.path().to_path_buf());
let (tx, _rx) = mpsc::unbounded_channel();
let (tx, _rx) = crate::peer_event_channel();
let first = CancellationToken::new();
let second = CancellationToken::new();
*ctx.active_outbound_transfers.write().await = HashMap::from([
@@ -4936,7 +4936,7 @@ mod tests {
let temp = TempDir::new("lanspread-handler-same-dir-timeout");
write_file(&temp.game_root().join(".version.ini.tmp"), b"tmp");
let ctx = test_ctx(temp.path().to_path_buf());
let (tx, mut rx) = mpsc::unbounded_channel();
let (tx, mut rx) = crate::peer_event_channel();
let token = CancellationToken::new();
ctx.active_outbound_transfers
.write()
@@ -4969,7 +4969,7 @@ mod tests {
.write()
.await
.insert("game".to_string(), OperationKind::Downloading);
let (tx, _rx) = mpsc::unbounded_channel();
let (tx, _rx) = crate::peer_event_channel();
let error = handle_set_game_dir_command(&ctx, &tx, temp.path().to_path_buf())
.await
@@ -4985,7 +4985,7 @@ mod tests {
let next = TempDir::new("lanspread-handler-new-dir");
write_file(&next.game_root().join(".version.ini.tmp"), b"tmp");
let ctx = test_ctx(current.path().to_path_buf());
let (tx, _rx) = mpsc::unbounded_channel();
let (tx, _rx) = crate::peer_event_channel();
let first = CancellationToken::new();
let second = CancellationToken::new();
*ctx.active_outbound_transfers.write().await = HashMap::from([
@@ -5041,7 +5041,7 @@ mod tests {
write_file(&root.join("version.ini"), b"20250101");
write_file(&root.join("game.eti"), b"archive");
let ctx = test_ctx(current.path().to_path_buf());
let (tx, mut rx) = mpsc::unbounded_channel();
let (tx, mut rx) = crate::peer_event_channel();
let catalog = ctx.catalog.catalog();
let scan = scan_local_library(current.path(), ctx.state_dir.as_ref(), catalog)
@@ -5095,7 +5095,7 @@ mod tests {
let current = TempDir::new("lanspread-handler-revision-exhausted-current");
let next = TempDir::new("lanspread-handler-revision-exhausted-next");
let ctx = test_ctx(current.path().to_path_buf());
let (tx, mut rx) = mpsc::unbounded_channel();
let (tx, mut rx) = crate::peer_event_channel();
let published = summary("game", "20250101", Availability::Ready);
ctx.local_library
.write()
@@ -5138,7 +5138,7 @@ mod tests {
std::fs::create_dir_all(next_root.join(".version.ini.tmp"))
.expect("invalid recovery scratch directory should be created");
let ctx = test_ctx(current.path().to_path_buf());
let (tx, mut rx) = mpsc::unbounded_channel();
let (tx, mut rx) = crate::peer_event_channel();
assert_eq!(
handle_set_game_dir_command_with_drain_timeout(
@@ -5198,7 +5198,7 @@ mod tests {
}
let ctx = test_ctx(current.path().to_path_buf());
let (tx, mut rx) = mpsc::unbounded_channel();
let (tx, mut rx) = crate::peer_event_channel();
let catalog = ctx.catalog.catalog();
let scan = scan_local_library(current.path(), ctx.state_dir.as_ref(), catalog)
.await