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:
2026-09-12 13:06:10 +02:00
parent 470e119515
commit 37420e26ed
17 changed files with 828 additions and 251 deletions
+4 -2
View File
@@ -34,6 +34,7 @@ use lanspread_peer::{
PeerCommand, PeerCommand,
PeerEndpoint, PeerEndpoint,
PeerEvent, PeerEvent,
PeerEventReceiver,
PeerGameDB, PeerGameDB,
PeerIdentity, PeerIdentity,
PeerRuntimeComponent, PeerRuntimeComponent,
@@ -44,6 +45,7 @@ use lanspread_peer::{
StreamInstallSettings, StreamInstallSettings,
load_peer_identity, load_peer_identity,
migrate_legacy_state, migrate_legacy_state,
peer_event_channel,
start_peer_with_options, start_peer_with_options,
}; };
use lanspread_peer_cli::{ use lanspread_peer_cli::{
@@ -308,7 +310,7 @@ async fn main() -> eyre::Result<()> {
let fixture_seeds = seed_fixtures(&args.games_dir, &args.fixtures)?; let fixture_seeds = seed_fixtures(&args.games_dir, &args.fixtures)?;
let migration = migrate_legacy_state(&args.games_dir, &args.state_dir).await; let migration = migrate_legacy_state(&args.games_dir, &args.state_dir).await;
let (tx_events, rx_events) = mpsc::unbounded_channel(); let (tx_events, rx_events) = peer_event_channel();
let peer_game_db = Arc::new(RwLock::new(PeerGameDB::new())); let peer_game_db = Arc::new(RwLock::new(PeerGameDB::new()));
let catalog_game_db = Arc::new(catalog_game_db); let catalog_game_db = Arc::new(catalog_game_db);
let active_outbound_transfers: OutboundTransfers = Arc::new(RwLock::new(HashMap::new())); let active_outbound_transfers: OutboundTransfers = Arc::new(RwLock::new(HashMap::new()));
@@ -865,7 +867,7 @@ async fn wait_peers(shared: &SharedState, count: usize, timeout: Duration) -> ey
} }
async fn event_loop( async fn event_loop(
mut rx_events: mpsc::UnboundedReceiver<PeerEvent>, mut rx_events: PeerEventReceiver,
shared: Arc<SharedState>, shared: Arc<SharedState>,
writer: JsonlWriter, writer: JsonlWriter,
) { ) {
+9 -8
View File
@@ -18,6 +18,7 @@ use tokio_util::{sync::CancellationToken, task::TaskTracker};
use crate::{ use crate::{
PeerEvent, PeerEvent,
PeerEventSender,
StreamInstallProvider, StreamInstallProvider,
Unpacker, Unpacker,
call_to_play::CallToPlayStore, call_to_play::CallToPlayStore,
@@ -45,7 +46,7 @@ const OUTBOUND_CHANGE_DIRTY: u8 = 2;
#[derive(Debug)] #[derive(Debug)]
pub struct OutboundTransferChange { pub struct OutboundTransferChange {
state: Arc<AtomicU8>, state: Arc<AtomicU8>,
tx_notify_ui: tokio::sync::mpsc::UnboundedSender<PeerEvent>, tx_notify_ui: PeerEventSender,
} }
impl Drop for OutboundTransferChange { impl Drop for OutboundTransferChange {
@@ -105,13 +106,13 @@ impl Drop for OutboundTransferChange {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub(crate) struct OutboundTransferNotifier { pub(crate) struct OutboundTransferNotifier {
state: Arc<AtomicU8>, state: Arc<AtomicU8>,
tx_notify_ui: tokio::sync::mpsc::UnboundedSender<PeerEvent>, tx_notify_ui: PeerEventSender,
} }
impl OutboundTransferNotifier { impl OutboundTransferNotifier {
fn new( fn new(
state: Arc<AtomicU8>, state: Arc<AtomicU8>,
tx_notify_ui: tokio::sync::mpsc::UnboundedSender<PeerEvent>, tx_notify_ui: PeerEventSender,
) -> Self { ) -> Self {
Self { Self {
state, state,
@@ -242,7 +243,7 @@ impl NetworkServiceCtx {
pub(crate) fn to_peer_ctx( pub(crate) fn to_peer_ctx(
&self, &self,
tx_notify_ui: tokio::sync::mpsc::UnboundedSender<PeerEvent>, tx_notify_ui: PeerEventSender,
) -> PeerCtx { ) -> PeerCtx {
self.core.to_peer_ctx(tx_notify_ui, self.shutdown.clone()) self.core.to_peer_ctx(tx_notify_ui, self.shutdown.clone())
} }
@@ -271,7 +272,7 @@ pub struct PeerCtx {
pub peer_id: PeerId, pub peer_id: PeerId,
pub(crate) runtime_session_id: RuntimeSessionId, pub(crate) runtime_session_id: RuntimeSessionId,
pub(crate) state_sync: StateSyncHandle, pub(crate) state_sync: StateSyncHandle,
pub tx_notify_ui: tokio::sync::mpsc::UnboundedSender<PeerEvent>, pub tx_notify_ui: PeerEventSender,
pub stream_install_provider: Arc<dyn StreamInstallProvider>, pub stream_install_provider: Arc<dyn StreamInstallProvider>,
pub shutdown: CancellationToken, pub shutdown: CancellationToken,
pub active_outbound_transfers: OutboundTransfers, pub active_outbound_transfers: OutboundTransfers,
@@ -345,7 +346,7 @@ impl Ctx {
/// Creates a `PeerCtx` from this context. /// Creates a `PeerCtx` from this context.
pub fn to_peer_ctx( pub fn to_peer_ctx(
&self, &self,
tx_notify_ui: tokio::sync::mpsc::UnboundedSender<PeerEvent>, tx_notify_ui: PeerEventSender,
shutdown: CancellationToken, shutdown: CancellationToken,
) -> PeerCtx { ) -> PeerCtx {
let outbound_transfer_notifier = OutboundTransferNotifier::new( let outbound_transfer_notifier = OutboundTransferNotifier::new(
@@ -459,7 +460,7 @@ mod tests {
sync::{Arc, atomic::AtomicU8}, sync::{Arc, atomic::AtomicU8},
}; };
use tokio::sync::{RwLock, mpsc}; use tokio::sync::RwLock;
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
use super::{OUTBOUND_CHANGE_IDLE, OperationGuard, OperationKind, OutboundTransferNotifier}; use super::{OUTBOUND_CHANGE_IDLE, OperationGuard, OperationKind, OutboundTransferNotifier};
@@ -467,7 +468,7 @@ mod tests {
#[test] #[test]
fn outbound_transfer_churn_keeps_at_most_one_edge_queued() { fn outbound_transfer_churn_keeps_at_most_one_edge_queued() {
let (tx, mut rx) = mpsc::unbounded_channel(); let (tx, mut rx) = crate::peer_event_channel();
let notifier = let notifier =
OutboundTransferNotifier::new(Arc::new(AtomicU8::new(OUTBOUND_CHANGE_IDLE)), tx); OutboundTransferNotifier::new(Arc::new(AtomicU8::new(OUTBOUND_CHANGE_IDLE)), tx);
@@ -9,7 +9,7 @@ use std::{
use futures::stream::FuturesUnordered; use futures::stream::FuturesUnordered;
use lanspread_db::content_manifest::ContentId; use lanspread_db::content_manifest::ContentId;
use lanspread_proto::PeerEndpoint; use lanspread_proto::PeerEndpoint;
use tokio::sync::mpsc::UnboundedSender; use crate::PeerEventSender;
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
use super::{ use super::{
@@ -153,7 +153,7 @@ pub(crate) struct DownloadGameRequest<'a> {
pub(crate) sources: &'a [PeerEndpoint], pub(crate) sources: &'a [PeerEndpoint],
pub(crate) content_id: ContentId, pub(crate) content_id: ContentId,
pub(crate) quarantine: &'a ContentQuarantine, pub(crate) quarantine: &'a ContentQuarantine,
pub(crate) tx_notify_ui: UnboundedSender<PeerEvent>, pub(crate) tx_notify_ui: PeerEventSender,
pub(crate) cancel_token: CancellationToken, pub(crate) cancel_token: CancellationToken,
pub(crate) quic: QuicConnector, pub(crate) quic: QuicConnector,
} }
@@ -372,7 +372,7 @@ struct TransferContext<'a> {
sources: &'a [PeerEndpoint], sources: &'a [PeerEndpoint],
content_id: ContentId, content_id: ContentId,
quarantine: &'a ContentQuarantine, quarantine: &'a ContentQuarantine,
tx_notify_ui: &'a UnboundedSender<PeerEvent>, tx_notify_ui: &'a PeerEventSender,
cancel_token: &'a CancellationToken, cancel_token: &'a CancellationToken,
quic: &'a QuicConnector, quic: &'a QuicConnector,
version_buffer: Arc<VersionIniBuffer>, version_buffer: Arc<VersionIniBuffer>,
+582 -14
View File
@@ -2,15 +2,20 @@
use std::{ use std::{
collections::{BTreeMap, HashMap}, collections::{BTreeMap, HashMap},
sync::Arc, fmt,
sync::{Arc, Mutex, MutexGuard},
}; };
use lanspread_db::content_manifest::ContentId; use lanspread_db::content_manifest::ContentId;
use tokio::sync::{RwLock, mpsc::UnboundedSender}; use tokio::sync::{
RwLock,
mpsc::{self, error::TryRecvError},
};
use crate::{ use crate::{
ActiveOperation, ActiveOperation,
ActiveOperationKind, ActiveOperationKind,
CallToPlayView,
PeerEvent, PeerEvent,
RemoteGameAvailability, RemoteGameAvailability,
RemoteLibraryView, RemoteLibraryView,
@@ -18,10 +23,431 @@ use crate::{
peer_db::PeerGameDB, peer_db::PeerGameDB,
}; };
pub fn send(tx_notify_ui: &UnboundedSender<PeerEvent>, event: PeerEvent) { /// Sender side of the peer-to-UI event transport.
///
/// Lifecycle events are queued losslessly in send order. The two complete-view
/// variants keep one concrete queued delivery plus one replaceable latest
/// pending snapshot, so a slow UI cannot make complete snapshots accumulate.
pub struct PeerEventSender {
queue: mpsc::UnboundedSender<QueuedPeerEvent>,
views: Arc<Mutex<CoalescedViews>>,
}
impl Clone for PeerEventSender {
fn clone(&self) -> Self {
Self {
queue: self.queue.clone(),
views: Arc::clone(&self.views),
}
}
}
impl fmt::Debug for PeerEventSender {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("PeerEventSender")
.field("closed", &self.is_closed())
.finish_non_exhaustive()
}
}
/// Receiver side of the peer-to-UI event transport.
pub struct PeerEventReceiver {
queue: mpsc::UnboundedReceiver<QueuedPeerEvent>,
views: Arc<Mutex<CoalescedViews>>,
}
#[derive(Debug)]
pub struct PeerEventSendError {
kind: &'static str,
}
impl PeerEventSendError {
#[must_use]
pub const fn kind(&self) -> &'static str {
self.kind
}
}
impl fmt::Display for PeerEventSendError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
formatter,
"peer event channel closed while sending {}",
self.kind
)
}
}
impl std::error::Error for PeerEventSendError {}
enum QueuedPeerEvent {
Lifecycle(PeerEvent),
RemoteLibrary(ViewWake),
CallToPlay(ViewWake),
}
struct ViewWake {
queue: mpsc::UnboundedSender<QueuedPeerEvent>,
}
struct QueueClosed;
struct VersionedView<T> {
generation: Arc<()>,
snapshot: T,
}
struct CoalescedViews {
generation: Arc<()>,
remote_library: ViewSlot<RemoteLibraryView>,
call_to_play: ViewSlot<CallToPlayView>,
}
struct ViewSlot<T> {
marker_queued: bool,
queued_delivery: Option<VersionedView<T>>,
pending_latest: Option<VersionedView<T>>,
}
impl<T> Default for ViewSlot<T> {
fn default() -> Self {
Self {
marker_queued: false,
queued_delivery: None,
pending_latest: None,
}
}
}
impl Default for CoalescedViews {
fn default() -> Self {
Self {
generation: Arc::new(()),
remote_library: ViewSlot::default(),
call_to_play: ViewSlot::default(),
}
}
}
/// Creates the event transport used by one peer runtime embedding.
#[must_use]
pub fn peer_event_channel() -> (PeerEventSender, PeerEventReceiver) {
let (queue_tx, queue_rx) = mpsc::unbounded_channel();
let views = Arc::new(Mutex::new(CoalescedViews::default()));
(
PeerEventSender {
queue: queue_tx,
views: Arc::clone(&views),
},
PeerEventReceiver {
queue: queue_rx,
views,
},
)
}
impl PeerEventSender {
/// Sends one event, coalescing only complete remote-library and Call-to-Play
/// projections.
pub fn send(&self, event: PeerEvent) -> Result<(), PeerEventSendError> {
let kind: &'static str = (&event).into();
if self.queue.is_closed() {
return Err(PeerEventSendError { kind });
}
let mut views = lock_views(&self.views);
let generation = Arc::clone(&views.generation);
let mut failed_lifecycle = None;
let result = match event {
PeerEvent::RemoteLibraryView(view) => enqueue_view(
&self.queue,
&generation,
&mut views.remote_library,
view,
QueuedPeerEvent::RemoteLibrary,
),
PeerEvent::CallToPlayView(view) => enqueue_view(
&self.queue,
&generation,
&mut views.call_to_play,
view,
QueuedPeerEvent::CallToPlay,
),
event => self
.queue
.send(QueuedPeerEvent::Lifecycle(event))
.map_err(|error| {
failed_lifecycle = Some(error);
QueueClosed
}),
};
drop(views);
drop(failed_lifecycle);
result.map_err(|_| PeerEventSendError { kind })
}
#[must_use]
pub fn is_closed(&self) -> bool {
self.queue.is_closed()
}
/// Starts a new view-publication generation and atomically installs both
/// authoritative replacement views.
///
/// The network manager calls this after the prior generation has drained and
/// before it publishes authoritative clear views. New clear deliveries can
/// therefore be queued ahead of `Disabled` even when stale markers remain in
/// the underlying FIFO.
pub(crate) fn publish_view_generation(
&self,
remote_library: RemoteLibraryView,
call_to_play: CallToPlayView,
) -> Result<(), PeerEventSendError> {
if self.queue.is_closed() {
return Err(PeerEventSendError {
kind: "ViewGenerationBarrier",
});
}
let mut views = lock_views(&self.views);
views.generation = Arc::new(());
let generation = Arc::clone(&views.generation);
replace_view_at_barrier(
&self.queue,
&generation,
&mut views.remote_library,
remote_library,
QueuedPeerEvent::RemoteLibrary,
)
.map_err(|_| PeerEventSendError {
kind: "RemoteLibraryView",
})?;
replace_view_at_barrier(
&self.queue,
&generation,
&mut views.call_to_play,
call_to_play,
QueuedPeerEvent::CallToPlay,
)
.map_err(|_| PeerEventSendError {
kind: "CallToPlayView",
})
}
}
fn enqueue_view<T>(
queue: &mpsc::UnboundedSender<QueuedPeerEvent>,
generation: &Arc<()>,
slot: &mut ViewSlot<T>,
snapshot: T,
wrap: impl FnOnce(ViewWake) -> QueuedPeerEvent,
) -> Result<(), QueueClosed> {
let versioned = VersionedView {
generation: Arc::clone(generation),
snapshot,
};
if slot.marker_queued {
slot.pending_latest = Some(versioned);
return Ok(());
}
slot.marker_queued = true;
slot.queued_delivery = Some(versioned);
slot.pending_latest = None;
let queued = wrap(ViewWake {
queue: queue.clone(),
});
if queue.send(queued).is_ok() {
Ok(())
} else {
slot.marker_queued = false;
slot.queued_delivery = None;
Err(QueueClosed)
}
}
fn replace_view_at_barrier<T>(
queue: &mpsc::UnboundedSender<QueuedPeerEvent>,
generation: &Arc<()>,
slot: &mut ViewSlot<T>,
snapshot: T,
wrap: impl FnOnce(ViewWake) -> QueuedPeerEvent,
) -> Result<(), QueueClosed> {
slot.queued_delivery = Some(VersionedView {
generation: Arc::clone(generation),
snapshot,
});
slot.pending_latest = None;
if slot.marker_queued {
return Ok(());
}
slot.marker_queued = true;
let queued = wrap(ViewWake {
queue: queue.clone(),
});
if queue.send(queued).is_ok() {
Ok(())
} else {
slot.marker_queued = false;
slot.queued_delivery = None;
Err(QueueClosed)
}
}
impl PeerEventReceiver {
pub async fn recv(&mut self) -> Option<PeerEvent> {
while let Some(queued) = self.queue.recv().await {
if let Some(event) = resolve_queued_event(queued, &self.views) {
return Some(event);
}
}
None
}
pub fn try_recv(&mut self) -> Result<PeerEvent, TryRecvError> {
loop {
let queued = self.queue.try_recv()?;
if let Some(event) = resolve_queued_event(queued, &self.views) {
return Ok(event);
}
}
}
/// Consumes exactly one item from the underlying FIFO. A stale view marker
/// resolves to `None`; lifecycle fences use this to drain an exact captured
/// queue prefix without consuming later traffic.
pub fn try_recv_queued(&mut self) -> Result<Option<PeerEvent>, TryRecvError> {
self.queue
.try_recv()
.map(|queued| resolve_queued_event(queued, &self.views))
}
#[must_use]
pub fn len(&self) -> usize {
self.queue.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.queue.is_empty()
}
/// Drains every event and coalesced snapshot admitted before this call
/// acquired the shared transport lock. Producers cannot replace pending
/// views until the captured prefix and both pending slots have been taken.
pub fn drain_ready(&mut self) -> Result<Vec<PeerEvent>, TryRecvError> {
let mut views = lock_views(&self.views);
let generation = Arc::clone(&views.generation);
let mut events = Vec::with_capacity(self.queue.len() + 2);
loop {
let queued = match self.queue.try_recv() {
Ok(queued) => queued,
Err(TryRecvError::Empty) => break,
Err(error) => {
drop(views);
return Err(error);
}
};
match queued {
QueuedPeerEvent::Lifecycle(event) => events.push(event),
QueuedPeerEvent::RemoteLibrary(wake) => {
if let Some(event) = resolve_view_locked(
&wake,
&generation,
&mut views.remote_library,
QueuedPeerEvent::RemoteLibrary,
PeerEvent::RemoteLibraryView,
) {
events.push(event);
}
}
QueuedPeerEvent::CallToPlay(wake) => {
if let Some(event) = resolve_view_locked(
&wake,
&generation,
&mut views.call_to_play,
QueuedPeerEvent::CallToPlay,
PeerEvent::CallToPlayView,
) {
events.push(event);
}
}
}
}
Ok(events)
}
}
fn resolve_queued_event(
queued: QueuedPeerEvent,
views: &Mutex<CoalescedViews>,
) -> Option<PeerEvent> {
match queued {
QueuedPeerEvent::Lifecycle(event) => Some(event),
QueuedPeerEvent::RemoteLibrary(wake) => resolve_view(
&wake,
views,
|views| &mut views.remote_library,
QueuedPeerEvent::RemoteLibrary,
PeerEvent::RemoteLibraryView,
),
QueuedPeerEvent::CallToPlay(wake) => resolve_view(
&wake,
views,
|views| &mut views.call_to_play,
QueuedPeerEvent::CallToPlay,
PeerEvent::CallToPlayView,
),
}
}
fn resolve_view<T>(
wake: &ViewWake,
views: &Mutex<CoalescedViews>,
slot: impl FnOnce(&mut CoalescedViews) -> &mut ViewSlot<T>,
wrap_queue: impl FnOnce(ViewWake) -> QueuedPeerEvent,
wrap_event: impl FnOnce(T) -> PeerEvent,
) -> Option<PeerEvent> {
let mut views = lock_views(views);
let current_generation = Arc::clone(&views.generation);
let slot = slot(&mut views);
resolve_view_locked(wake, &current_generation, slot, wrap_queue, wrap_event)
}
fn resolve_view_locked<T>(
wake: &ViewWake,
current_generation: &Arc<()>,
slot: &mut ViewSlot<T>,
wrap_queue: impl FnOnce(ViewWake) -> QueuedPeerEvent,
wrap_event: impl FnOnce(T) -> PeerEvent,
) -> Option<PeerEvent> {
let delivery = slot.queued_delivery.take();
if let Some(pending) = slot.pending_latest.take() {
slot.queued_delivery = Some(pending);
let next = wrap_queue(ViewWake {
queue: wake.queue.clone(),
});
if wake.queue.send(next).is_err() {
slot.marker_queued = false;
slot.queued_delivery = None;
}
} else {
slot.marker_queued = false;
}
delivery
.filter(|delivery| Arc::ptr_eq(&delivery.generation, current_generation))
.map(|delivery| wrap_event(delivery.snapshot))
}
fn lock_views(views: &Mutex<CoalescedViews>) -> MutexGuard<'_, CoalescedViews> {
views
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
pub fn send(tx_notify_ui: &PeerEventSender, event: PeerEvent) {
if let Err(err) = tx_notify_ui.send(event) { if let Err(err) = tx_notify_ui.send(event) {
let kind: &'static str = (&err.0).into(); log::error!("Failed to send {} event: channel closed", err.kind());
log::error!("Failed to send {kind} event: channel closed");
} }
} }
@@ -40,7 +466,7 @@ pub(crate) fn active_operation_snapshot_from_map(
} }
pub(crate) fn send_active_operations_snapshot( pub(crate) fn send_active_operations_snapshot(
tx_notify_ui: &UnboundedSender<PeerEvent>, tx_notify_ui: &PeerEventSender,
active_operations: &HashMap<String, OperationKind>, active_operations: &HashMap<String, OperationKind>,
) { ) {
send( send(
@@ -53,7 +479,7 @@ pub(crate) fn send_active_operations_snapshot(
pub(crate) async fn emit_active_operations( pub(crate) async fn emit_active_operations(
active_operations: &Arc<RwLock<HashMap<String, OperationKind>>>, active_operations: &Arc<RwLock<HashMap<String, OperationKind>>>,
tx_notify_ui: &UnboundedSender<PeerEvent>, tx_notify_ui: &PeerEventSender,
) { ) {
let active_operations = active_operations.read().await; let active_operations = active_operations.read().await;
send_active_operations_snapshot(tx_notify_ui, &active_operations); send_active_operations_snapshot(tx_notify_ui, &active_operations);
@@ -71,16 +497,13 @@ fn active_operation_kind(operation: OperationKind) -> ActiveOperationKind {
pub async fn emit_peer_game_list( pub async fn emit_peer_game_list(
peer_game_db: &Arc<RwLock<PeerGameDB>>, peer_game_db: &Arc<RwLock<PeerGameDB>>,
tx_notify_ui: &UnboundedSender<PeerEvent>, tx_notify_ui: &PeerEventSender,
) { ) {
let db = peer_game_db.read().await; let db = peer_game_db.read().await;
send_remote_library_view_locked(&db, tx_notify_ui); send_remote_library_view_locked(&db, tx_notify_ui);
} }
fn send_remote_library_view_locked( fn send_remote_library_view_locked(peer_game_db: &PeerGameDB, tx_notify_ui: &PeerEventSender) {
peer_game_db: &PeerGameDB,
tx_notify_ui: &UnboundedSender<PeerEvent>,
) {
send( send(
tx_notify_ui, tx_notify_ui,
PeerEvent::RemoteLibraryView(remote_library_view(peer_game_db)), PeerEvent::RemoteLibraryView(remote_library_view(peer_game_db)),
@@ -113,7 +536,7 @@ pub(crate) fn remote_library_view(peer_game_db: &PeerGameDB) -> RemoteLibraryVie
pub async fn emit_peer_count( pub async fn emit_peer_count(
peer_game_db: &Arc<RwLock<PeerGameDB>>, peer_game_db: &Arc<RwLock<PeerGameDB>>,
tx_notify_ui: &UnboundedSender<PeerEvent>, tx_notify_ui: &PeerEventSender,
) { ) {
let db = peer_game_db.read().await; let db = peer_game_db.read().await;
let peer_count = db.peer_endpoints().len(); let peer_count = db.peer_endpoints().len();
@@ -124,6 +547,151 @@ pub async fn emit_peer_count(
mod tests { mod tests {
use super::*; use super::*;
fn remote_view(game_id: &str) -> RemoteLibraryView {
RemoteLibraryView {
games: vec![RemoteGameAvailability {
game_id: game_id.to_owned(),
content_id: ContentId::from_bytes(
[u8::try_from(game_id.len()).expect("test game ID length should fit u8"); 32],
),
peer_count: 1,
}],
}
}
fn call_to_play_view(author_name: &str, seed: u8) -> CallToPlayView {
let author_id = crate::PeerId::from_bytes([seed; 32]);
CallToPlayView {
events: vec![crate::CallToPlayViewEvent {
id: crate::EventNonce::from_bytes([seed; 16]),
call_id: crate::CallId::new(
author_id,
lanspread_proto::CallNonce::from_bytes([seed; 16]),
),
author_id,
author_name: author_name.to_owned(),
at: i64::from(seed),
action: crate::CallToPlayAction::Rsvp,
}],
}
}
#[tokio::test]
async fn complete_views_keep_first_queued_and_only_latest_pending() {
let (tx, mut rx) = peer_event_channel();
tx.send(PeerEvent::RemoteLibraryView(remote_view("first")))
.expect("first remote view should enqueue");
tx.send(PeerEvent::RemoteLibraryView(remote_view("middle")))
.expect("middle remote view should coalesce");
tx.send(PeerEvent::RemoteLibraryView(remote_view("latest")))
.expect("latest remote view should replace pending");
tx.send(PeerEvent::PeerCountUpdated(1))
.expect("first lifecycle event should enqueue");
tx.send(PeerEvent::PeerCountUpdated(2))
.expect("second lifecycle event should enqueue");
tx.send(PeerEvent::CallToPlayView(call_to_play_view("first", 1)))
.expect("first Call-to-Play view should enqueue");
tx.send(PeerEvent::CallToPlayView(call_to_play_view("latest", 2)))
.expect("latest Call-to-Play view should become pending");
assert_eq!(rx.len(), 4, "pending snapshots must not occupy FIFO slots");
assert!(matches!(
rx.recv().await,
Some(PeerEvent::RemoteLibraryView(view)) if view.games[0].game_id == "first"
));
assert!(matches!(
rx.recv().await,
Some(PeerEvent::PeerCountUpdated(1))
));
assert!(matches!(
rx.recv().await,
Some(PeerEvent::PeerCountUpdated(2))
));
assert!(matches!(
rx.recv().await,
Some(PeerEvent::CallToPlayView(view)) if view.events[0].author_name == "first"
));
assert!(matches!(
rx.recv().await,
Some(PeerEvent::RemoteLibraryView(view)) if view.games[0].game_id == "latest"
));
assert!(matches!(
rx.recv().await,
Some(PeerEvent::CallToPlayView(view)) if view.events[0].author_name == "latest"
));
assert!(matches!(rx.try_recv(), Err(TryRecvError::Empty)));
}
#[test]
fn ready_fence_drains_queued_and_pending_views_without_later_traffic() {
let (tx, mut rx) = peer_event_channel();
tx.send(PeerEvent::RemoteLibraryView(remote_view("queued")))
.expect("queued view should enqueue");
tx.send(PeerEvent::PeerCountUpdated(7))
.expect("lifecycle event should enqueue");
tx.send(PeerEvent::RemoteLibraryView(remote_view("pending")))
.expect("pending view should coalesce");
let drained = rx.drain_ready().expect("ready prefix should drain");
assert_eq!(drained.len(), 3);
assert!(matches!(
&drained[0],
PeerEvent::RemoteLibraryView(view) if view.games[0].game_id == "queued"
));
assert!(matches!(&drained[1], PeerEvent::PeerCountUpdated(7)));
assert!(matches!(
&drained[2],
PeerEvent::RemoteLibraryView(view) if view.games[0].game_id == "pending"
));
assert!(matches!(rx.try_recv(), Err(TryRecvError::Empty)));
tx.send(PeerEvent::PeerCountUpdated(8))
.expect("later lifecycle traffic should enqueue independently");
assert!(matches!(rx.try_recv(), Ok(PeerEvent::PeerCountUpdated(8))));
}
#[tokio::test]
async fn generation_barrier_discards_old_views_before_clear_and_disabled() {
let (tx, mut rx) = peer_event_channel();
tx.send(PeerEvent::RemoteLibraryView(remote_view("old-first")))
.expect("old remote view should enqueue");
tx.send(PeerEvent::RemoteLibraryView(remote_view("old-pending")))
.expect("old remote pending view should coalesce");
tx.send(PeerEvent::CallToPlayView(call_to_play_view("old-first", 1)))
.expect("old Call-to-Play view should enqueue");
tx.send(PeerEvent::CallToPlayView(call_to_play_view(
"old-pending",
2,
)))
.expect("old Call-to-Play pending view should coalesce");
tx.publish_view_generation(RemoteLibraryView::default(), CallToPlayView::default())
.expect("clear views should enqueue in the new generation");
tx.publish_view_generation(RemoteLibraryView::default(), CallToPlayView::default())
.expect("repeated barriers should replace in place");
assert_eq!(rx.len(), 2, "barriers must not accumulate view markers");
tx.send(PeerEvent::LocalNetworkSharingStateChanged(
crate::LocalNetworkSharingState::Disabled,
))
.expect("Disabled should remain a lifecycle event");
assert!(matches!(
rx.recv().await,
Some(PeerEvent::RemoteLibraryView(view)) if view.games.is_empty()
));
assert!(matches!(
rx.recv().await,
Some(PeerEvent::CallToPlayView(view)) if view.events.is_empty()
));
assert!(matches!(
rx.recv().await,
Some(PeerEvent::LocalNetworkSharingStateChanged(
crate::LocalNetworkSharingState::Disabled
))
));
assert!(matches!(rx.try_recv(), Err(TryRecvError::Empty)));
}
#[tokio::test] #[tokio::test]
async fn remote_library_view_is_enqueued_before_a_waiting_writer_can_commit() { async fn remote_library_view_is_enqueued_before_a_waiting_writer_can_commit() {
let peer_game_db = Arc::new(RwLock::new(PeerGameDB::new())); let peer_game_db = Arc::new(RwLock::new(PeerGameDB::new()));
@@ -135,7 +703,7 @@ mod tests {
tokio::task::yield_now().await; tokio::task::yield_now().await;
assert!(!writer.is_finished(), "writer must wait for the read guard"); assert!(!writer.is_finished(), "writer must wait for the read guard");
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel(); let (tx, mut rx) = peer_event_channel();
send_remote_library_view_locked(&guard, &tx); send_remote_library_view_locked(&guard, &tx);
assert!(matches!( assert!(matches!(
rx.try_recv(), rx.try_recv(),
+104 -104
View File
@@ -14,7 +14,7 @@ use lanspread_db::{
db::GameDB, db::GameDB,
}; };
use lanspread_proto::PeerEndpoint; use lanspread_proto::PeerEndpoint;
use tokio::sync::mpsc::UnboundedSender; use crate::PeerEventSender;
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
#[cfg(test)] #[cfg(test)]
@@ -82,7 +82,7 @@ where
async fn register_download_attempt( async fn register_download_attempt(
ctx: &Ctx, ctx: &Ctx,
tx_notify_ui: &UnboundedSender<PeerEvent>, tx_notify_ui: &PeerEventSender,
key: DownloadAttemptKey, key: DownloadAttemptKey,
cancellation: CancellationToken, cancellation: CancellationToken,
) -> DownloadAttemptStatus { ) -> DownloadAttemptStatus {
@@ -202,7 +202,7 @@ impl fmt::Display for StreamDownloadError {
} }
/// Handles the `ListGames` command. /// 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"); log::info!("ListGames command received");
events::emit_peer_game_list(&ctx.peer_game_db, tx_notify_ui).await; 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)] #[allow(clippy::too_many_lines)]
pub async fn handle_download_game_files_command( pub async fn handle_download_game_files_command(
ctx: &Ctx, ctx: &Ctx,
tx_notify_ui: &UnboundedSender<PeerEvent>, tx_notify_ui: &PeerEventSender,
id: String, id: String,
install_after_download: bool, install_after_download: bool,
) { ) {
@@ -494,7 +494,7 @@ pub async fn handle_download_game_files_command(
fn finish_cached_download( fn finish_cached_download(
ctx: &Ctx, ctx: &Ctx,
tx_notify_ui: &UnboundedSender<PeerEvent>, tx_notify_ui: &PeerEventSender,
attempt: &DownloadAttemptKey, attempt: &DownloadAttemptKey,
install_after_download: bool, install_after_download: bool,
target: OperationTarget, target: OperationTarget,
@@ -547,7 +547,7 @@ async fn settle_download_completion(
/// Handles the `InstallGame` command. /// Handles the `InstallGame` command.
pub async fn handle_install_game_command( pub async fn handle_install_game_command(
ctx: &Ctx, ctx: &Ctx,
tx_notify_ui: &UnboundedSender<PeerEvent>, tx_notify_ui: &PeerEventSender,
id: String, id: String,
) { ) {
let games_folder = ctx.game_dir.read().await.clone(); 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( async fn begin_stream_install_operation(
ctx: &Ctx, ctx: &Ctx,
tx_notify_ui: &UnboundedSender<PeerEvent>, tx_notify_ui: &PeerEventSender,
target: &OperationTarget, target: &OperationTarget,
attempt: &DownloadAttemptKey, attempt: &DownloadAttemptKey,
) -> bool { ) -> bool {
@@ -631,7 +631,7 @@ async fn begin_stream_install_operation(
pub async fn handle_stream_install_game_command( pub async fn handle_stream_install_game_command(
ctx: &Ctx, ctx: &Ctx,
tx_notify_ui: &UnboundedSender<PeerEvent>, tx_notify_ui: &PeerEventSender,
id: String, id: String,
settings: StreamInstallSettings, settings: StreamInstallSettings,
) { ) {
@@ -742,7 +742,7 @@ pub async fn handle_stream_install_game_command(
struct StreamInstallOperation { struct StreamInstallOperation {
ctx: Ctx, ctx: Ctx,
tx_notify_ui: UnboundedSender<PeerEvent>, tx_notify_ui: PeerEventSender,
target: OperationTarget, target: OperationTarget,
manifest: Arc<CatalogContentManifest>, manifest: Arc<CatalogContentManifest>,
settings: StreamInstallSettings, settings: StreamInstallSettings,
@@ -768,7 +768,7 @@ async fn stream_install_sources(
async fn select_stream_install_sources_or_finish( async fn select_stream_install_sources_or_finish(
ctx: &Ctx, ctx: &Ctx,
tx_notify_ui: &UnboundedSender<PeerEvent>, tx_notify_ui: &PeerEventSender,
target: &OperationTarget, target: &OperationTarget,
manifest: &CatalogContentManifest, manifest: &CatalogContentManifest,
download_guard: OperationGuard, download_guard: OperationGuard,
@@ -798,7 +798,7 @@ async fn select_stream_install_sources_or_finish(
/// Handles the `UninstallGame` command. /// Handles the `UninstallGame` command.
pub async fn handle_uninstall_game_command( pub async fn handle_uninstall_game_command(
ctx: &Ctx, ctx: &Ctx,
tx_notify_ui: &UnboundedSender<PeerEvent>, tx_notify_ui: &PeerEventSender,
id: String, id: String,
) { ) {
let games_folder = ctx.game_dir.read().await.clone(); 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( pub async fn handle_remove_downloaded_game_command(
ctx: &Ctx, ctx: &Ctx,
tx_notify_ui: &UnboundedSender<PeerEvent>, tx_notify_ui: &PeerEventSender,
id: String, id: String,
) { ) {
let games_folder = ctx.game_dir.read().await.clone(); 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( pub async fn handle_cancel_download_command(
ctx: &Ctx, ctx: &Ctx,
_tx_notify_ui: &UnboundedSender<PeerEvent>, _tx_notify_ui: &PeerEventSender,
id: String, id: String,
) { ) {
let signal = ctx.active_downloads.read().await.get(&id).cloned(); 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( async fn finish_stream_receive_error(
ctx: &Ctx, ctx: &Ctx,
tx_notify_ui: &UnboundedSender<PeerEvent>, tx_notify_ui: &PeerEventSender,
target: &OperationTarget, target: &OperationTarget,
download_guard: OperationGuard, download_guard: OperationGuard,
download_attempt: &DownloadAttemptStatus, download_attempt: &DownloadAttemptStatus,
@@ -975,7 +975,7 @@ async fn finish_stream_receive_error(
struct StreamInstallPromotionPreparation<'a> { struct StreamInstallPromotionPreparation<'a> {
ctx: &'a Ctx, ctx: &'a Ctx,
tx_notify_ui: &'a UnboundedSender<PeerEvent>, tx_notify_ui: &'a PeerEventSender,
target: &'a OperationTarget, target: &'a OperationTarget,
transaction: install::StreamedInstallTransaction, transaction: install::StreamedInstallTransaction,
settings: &'a StreamInstallSettings, settings: &'a StreamInstallSettings,
@@ -1055,7 +1055,7 @@ async fn prepare_streamed_install_for_promotion(
struct StreamInstallReceiveRequest<'a> { struct StreamInstallReceiveRequest<'a> {
ctx: &'a Ctx, ctx: &'a Ctx,
tx_notify_ui: &'a UnboundedSender<PeerEvent>, tx_notify_ui: &'a PeerEventSender,
target: &'a OperationTarget, target: &'a OperationTarget,
manifest: &'a Arc<CatalogContentManifest>, manifest: &'a Arc<CatalogContentManifest>,
sources: &'a [PeerEndpoint], sources: &'a [PeerEndpoint],
@@ -1218,7 +1218,7 @@ async fn receive_streamed_install_from_peers(
async fn finish_failed_stream_download( async fn finish_failed_stream_download(
ctx: &Ctx, ctx: &Ctx,
tx_notify_ui: &UnboundedSender<PeerEvent>, tx_notify_ui: &PeerEventSender,
target: &OperationTarget, target: &OperationTarget,
guard: OperationGuard, guard: OperationGuard,
status: &DownloadAttemptStatus, status: &DownloadAttemptStatus,
@@ -1243,7 +1243,7 @@ async fn finish_failed_stream_download(
struct StreamInstallCommit { struct StreamInstallCommit {
ctx: Ctx, ctx: Ctx,
tx_notify_ui: UnboundedSender<PeerEvent>, tx_notify_ui: PeerEventSender,
target: OperationTarget, target: OperationTarget,
transaction: install::StreamedInstallTransaction, transaction: install::StreamedInstallTransaction,
manifest: Arc<CatalogContentManifest>, manifest: Arc<CatalogContentManifest>,
@@ -1346,7 +1346,7 @@ fn promote_streamed_install(
fn spawn_install_operation( fn spawn_install_operation(
ctx: &Ctx, ctx: &Ctx,
tx_notify_ui: &UnboundedSender<PeerEvent>, tx_notify_ui: &PeerEventSender,
target: OperationTarget, target: OperationTarget,
) { ) {
let ctx = ctx.clone(); let ctx = ctx.clone();
@@ -1358,7 +1358,7 @@ fn spawn_install_operation(
async fn run_install_operation( async fn run_install_operation(
ctx: &Ctx, ctx: &Ctx,
tx_notify_ui: &UnboundedSender<PeerEvent>, tx_notify_ui: &PeerEventSender,
target: OperationTarget, target: OperationTarget,
) { ) {
let id = target.game_id().to_owned(); let id = target.game_id().to_owned();
@@ -1442,7 +1442,7 @@ struct PreparedInstallOperation {
async fn prepare_install_operation( async fn prepare_install_operation(
ctx: &Ctx, ctx: &Ctx,
tx_notify_ui: &UnboundedSender<PeerEvent>, tx_notify_ui: &PeerEventSender,
target: &OperationTarget, target: &OperationTarget,
) -> Option<PreparedInstallOperation> { ) -> Option<PreparedInstallOperation> {
let id = target.game_id(); let id = target.game_id();
@@ -1499,7 +1499,7 @@ async fn prepare_install_operation(
async fn revalidate_install_operation( async fn revalidate_install_operation(
ctx: &Ctx, ctx: &Ctx,
tx_notify_ui: &UnboundedSender<PeerEvent>, tx_notify_ui: &PeerEventSender,
target: &OperationTarget, target: &OperationTarget,
expected_kind: OperationKind, expected_kind: OperationKind,
) -> Option<PreparedInstallOperation> { ) -> Option<PreparedInstallOperation> {
@@ -1522,7 +1522,7 @@ async fn revalidate_install_operation(
async fn run_started_install_operation( async fn run_started_install_operation(
ctx: &Ctx, ctx: &Ctx,
tx_notify_ui: &UnboundedSender<PeerEvent>, tx_notify_ui: &PeerEventSender,
target: OperationTarget, target: OperationTarget,
prepared: PreparedInstallOperation, prepared: PreparedInstallOperation,
operation_guard: OperationGuard, operation_guard: OperationGuard,
@@ -1599,7 +1599,7 @@ async fn run_started_install_operation(
async fn run_uninstall_operation( async fn run_uninstall_operation(
ctx: &Ctx, ctx: &Ctx,
tx_notify_ui: &UnboundedSender<PeerEvent>, tx_notify_ui: &PeerEventSender,
target: OperationTarget, target: OperationTarget,
) { ) {
let id = target.game_id().to_owned(); let id = target.game_id().to_owned();
@@ -1696,7 +1696,7 @@ async fn run_uninstall_operation(
async fn run_remove_downloaded_operation( async fn run_remove_downloaded_operation(
ctx: &Ctx, ctx: &Ctx,
tx_notify_ui: &UnboundedSender<PeerEvent>, tx_notify_ui: &PeerEventSender,
target: OperationTarget, target: OperationTarget,
) { ) {
let id = target.game_id().to_owned(); let id = target.game_id().to_owned();
@@ -1804,7 +1804,7 @@ enum BeginOperationResult {
async fn begin_operation( async fn begin_operation(
ctx: &Ctx, ctx: &Ctx,
tx_notify_ui: &UnboundedSender<PeerEvent>, tx_notify_ui: &PeerEventSender,
target: &OperationTarget, target: &OperationTarget,
operation: OperationKind, operation: OperationKind,
) -> BeginOperationResult { ) -> BeginOperationResult {
@@ -1820,7 +1820,7 @@ async fn begin_operation(
async fn begin_operation_with_drain_timeout( async fn begin_operation_with_drain_timeout(
ctx: &Ctx, ctx: &Ctx,
tx_notify_ui: &UnboundedSender<PeerEvent>, tx_notify_ui: &PeerEventSender,
target: &OperationTarget, target: &OperationTarget,
operation: OperationKind, operation: OperationKind,
drain_timeout: Duration, drain_timeout: Duration,
@@ -2009,7 +2009,7 @@ async fn cancel_and_wait_for_outbound_transfers(
async fn transition_download_to_install( async fn transition_download_to_install(
ctx: &Ctx, ctx: &Ctx,
tx_notify_ui: &UnboundedSender<PeerEvent>, tx_notify_ui: &PeerEventSender,
id: &str, id: &str,
operation: OperationKind, operation: OperationKind,
) -> bool { ) -> bool {
@@ -2042,7 +2042,7 @@ async fn transition_download_to_install(
transitioned 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() { if ctx.active_operations.write().await.remove(id).is_some() {
events::emit_active_operations(&ctx.active_operations, tx_notify_ui).await; 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( fn send_download_failed(
tx_notify_ui: &UnboundedSender<PeerEvent>, tx_notify_ui: &PeerEventSender,
attempt: &DownloadAttemptKey, attempt: &DownloadAttemptKey,
reason: DownloadFailureReason, reason: DownloadFailureReason,
) { ) {
@@ -2074,7 +2074,7 @@ fn send_download_failed(
async fn end_download_operation( async fn end_download_operation(
ctx: &Ctx, ctx: &Ctx,
tx_notify_ui: &UnboundedSender<PeerEvent>, tx_notify_ui: &PeerEventSender,
attempt: &DownloadAttemptKey, attempt: &DownloadAttemptKey,
) { ) {
clear_active_download(ctx, attempt).await; clear_active_download(ctx, attempt).await;
@@ -2083,7 +2083,7 @@ async fn end_download_operation(
async fn settle_target_state( async fn settle_target_state(
ctx: &Ctx, ctx: &Ctx,
tx_notify_ui: &UnboundedSender<PeerEvent>, tx_notify_ui: &PeerEventSender,
target: &OperationTarget, target: &OperationTarget,
) -> eyre::Result<()> { ) -> eyre::Result<()> {
install::recover_game_root( install::recover_game_root(
@@ -2097,7 +2097,7 @@ async fn settle_target_state(
async fn settle_and_end_operation( async fn settle_and_end_operation(
ctx: &Ctx, ctx: &Ctx,
tx_notify_ui: &UnboundedSender<PeerEvent>, tx_notify_ui: &PeerEventSender,
target: &OperationTarget, target: &OperationTarget,
guard: OperationGuard, guard: OperationGuard,
label: &str, label: &str,
@@ -2121,7 +2121,7 @@ async fn settle_and_end_operation(
async fn settle_and_end_download( async fn settle_and_end_download(
ctx: &Ctx, ctx: &Ctx,
tx_notify_ui: &UnboundedSender<PeerEvent>, tx_notify_ui: &PeerEventSender,
target: &OperationTarget, target: &OperationTarget,
guard: OperationGuard, guard: OperationGuard,
attempt: &DownloadAttemptKey, attempt: &DownloadAttemptKey,
@@ -2142,7 +2142,7 @@ async fn settle_and_end_download(
async fn finish_successful_download_after_refresh( async fn finish_successful_download_after_refresh(
ctx: &Ctx, ctx: &Ctx,
tx_notify_ui: &UnboundedSender<PeerEvent>, tx_notify_ui: &PeerEventSender,
target: &OperationTarget, target: &OperationTarget,
guard: OperationGuard, guard: OperationGuard,
status: &DownloadAttemptStatus, status: &DownloadAttemptStatus,
@@ -2164,7 +2164,7 @@ async fn finish_successful_download_after_refresh(
async fn finish_failed_download_after_refresh( async fn finish_failed_download_after_refresh(
ctx: &Ctx, ctx: &Ctx,
tx_notify_ui: &UnboundedSender<PeerEvent>, tx_notify_ui: &PeerEventSender,
target: &OperationTarget, target: &OperationTarget,
guard: OperationGuard, guard: OperationGuard,
status: &DownloadAttemptStatus, status: &DownloadAttemptStatus,
@@ -2193,7 +2193,7 @@ fn catalog_contains(ctx: &Ctx, id: &str) -> bool {
async fn begin_local_recovery( async fn begin_local_recovery(
ctx: &Ctx, ctx: &Ctx,
tx_notify_ui: &UnboundedSender<PeerEvent>, tx_notify_ui: &PeerEventSender,
game_dir: &Path, game_dir: &Path,
force_empty_snapshot: bool, force_empty_snapshot: bool,
) -> eyre::Result<()> { ) -> eyre::Result<()> {
@@ -2225,7 +2225,7 @@ async fn begin_local_recovery(
/// quarantined. `Err` is returned only before changing the configured root. /// quarantined. `Err` is returned only before changing the configured root.
pub async fn handle_set_game_dir_command( pub async fn handle_set_game_dir_command(
ctx: &Ctx, ctx: &Ctx,
tx_notify_ui: &UnboundedSender<PeerEvent>, tx_notify_ui: &PeerEventSender,
game_dir: PathBuf, game_dir: PathBuf,
) -> Result<PathBuf, String> { ) -> Result<PathBuf, String> {
handle_set_game_dir_command_with_drain_timeout( 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( async fn handle_set_game_dir_command_with_drain_timeout(
ctx: &Ctx, ctx: &Ctx,
tx_notify_ui: &UnboundedSender<PeerEvent>, tx_notify_ui: &PeerEventSender,
requested_game_dir: PathBuf, requested_game_dir: PathBuf,
drain_timeout: Duration, drain_timeout: Duration,
) -> Result<PathBuf, String> { ) -> 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. /// Loads the configured local library and announces the result.
pub async fn load_local_library( pub async fn load_local_library(
ctx: &Ctx, ctx: &Ctx,
tx_notify_ui: &UnboundedSender<PeerEvent>, tx_notify_ui: &PeerEventSender,
) -> eyre::Result<()> { ) -> eyre::Result<()> {
load_local_library_with_policy(ctx, tx_notify_ui, LocalLibraryEventPolicy::OnChange).await load_local_library_with_policy(ctx, tx_notify_ui, LocalLibraryEventPolicy::OnChange).await
} }
async fn load_local_library_with_policy( async fn load_local_library_with_policy(
ctx: &Ctx, ctx: &Ctx,
tx_notify_ui: &UnboundedSender<PeerEvent>, tx_notify_ui: &PeerEventSender,
event_policy: LocalLibraryEventPolicy, event_policy: LocalLibraryEventPolicy,
) -> eyre::Result<()> { ) -> eyre::Result<()> {
let game_dir = { ctx.game_dir.read().await.clone() }; 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( async fn scan_and_announce_local_library(
ctx: &Ctx, ctx: &Ctx,
tx_notify_ui: &UnboundedSender<PeerEvent>, tx_notify_ui: &PeerEventSender,
game_dir: &Path, game_dir: &Path,
event_policy: LocalLibraryEventPolicy, event_policy: LocalLibraryEventPolicy,
recovery_failed_ids: &HashSet<String>, 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. /// active-operation snapshot, while preserving freeze behavior for other games.
async fn refresh_local_game_for_ending_operation( async fn refresh_local_game_for_ending_operation(
ctx: &Ctx, ctx: &Ctx,
tx_notify_ui: &UnboundedSender<PeerEvent>, tx_notify_ui: &PeerEventSender,
target: &OperationTarget, target: &OperationTarget,
) -> eyre::Result<()> { ) -> eyre::Result<()> {
let catalog = ctx.catalog.catalog(); let catalog = ctx.catalog.catalog();
@@ -2456,7 +2456,7 @@ async fn active_operation_ids(ctx: &Ctx) -> HashSet<String> {
} }
/// Handles the `GetPeerCount` command. /// 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"); log::info!("GetPeerCount command received");
events::emit_peer_count(&ctx.peer_game_db, tx_notify_ui).await; 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. /// Connects to a peer directly, bypassing mDNS discovery.
pub async fn handle_connect_peer_command( pub async fn handle_connect_peer_command(
ctx: &Ctx, ctx: &Ctx,
tx_notify_ui: &UnboundedSender<PeerEvent>, tx_notify_ui: &PeerEventSender,
endpoint: PeerEndpoint, endpoint: PeerEndpoint,
) { ) {
log::info!("Direct connect command received for {}", endpoint.addr); 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. /// Updates the local game database and announces changes to peers.
pub async fn update_and_announce_games( pub async fn update_and_announce_games(
ctx: &Ctx, ctx: &Ctx,
tx_notify_ui: &UnboundedSender<PeerEvent>, tx_notify_ui: &PeerEventSender,
scan: LocalLibraryScan, scan: LocalLibraryScan,
) { ) {
let _ = update_and_announce_games_with_policy( 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( async fn update_and_announce_games_with_policy(
ctx: &Ctx, ctx: &Ctx,
tx_notify_ui: &UnboundedSender<PeerEvent>, tx_notify_ui: &PeerEventSender,
scan: LocalLibraryScan, scan: LocalLibraryScan,
event_policy: LocalLibraryEventPolicy, event_policy: LocalLibraryEventPolicy,
ending_operation_id: Option<&str>, ending_operation_id: Option<&str>,
@@ -2659,7 +2659,7 @@ mod tests {
PeerId, PeerId,
RuntimeSessionId, RuntimeSessionId,
}; };
use tokio::sync::{RwLock, mpsc, oneshot}; use tokio::sync::{RwLock, oneshot};
use tokio_util::{sync::CancellationToken, task::TaskTracker}; use tokio_util::{sync::CancellationToken, task::TaskTracker};
use super::*; use super::*;
@@ -2834,7 +2834,7 @@ mod tests {
async fn register_test_download( async fn register_test_download(
ctx: &Ctx, ctx: &Ctx,
tx_notify_ui: &UnboundedSender<PeerEvent>, tx_notify_ui: &PeerEventSender,
) -> (DownloadAttemptStatus, CancellationToken) { ) -> (DownloadAttemptStatus, CancellationToken) {
let cancellation = CancellationToken::new(); let cancellation = CancellationToken::new();
let status = register_download_attempt( let status = register_download_attempt(
@@ -2868,7 +2868,7 @@ mod tests {
#[test] #[test]
fn cancelled_download_owner_does_not_emit_failed_event() { 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( let status = DownloadAttemptStatus::new(
DownloadAttemptKey::next("game".to_owned()), DownloadAttemptKey::next("game".to_owned()),
CancellationToken::new(), CancellationToken::new(),
@@ -2885,7 +2885,7 @@ mod tests {
#[test] #[test]
fn uncancelled_download_error_emits_failed_event() { 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()); let attempt = DownloadAttemptKey::next("game".to_owned());
send_download_failed(&tx, &attempt, DownloadFailureReason::OperationFailed); send_download_failed(&tx, &attempt, DownloadFailureReason::OperationFailed);
@@ -2959,7 +2959,7 @@ mod tests {
); );
seed_exact_download_ownership(&ctx, games.path(), content_id).await; seed_exact_download_ownership(&ctx, games.path(), content_id).await;
let peer_db = ctx.peer_game_db.write().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( tokio::time::timeout(
Duration::from_secs(1), Duration::from_secs(1),
@@ -2993,7 +2993,7 @@ mod tests {
NetworkControl::disabled_for_test(), NetworkControl::disabled_for_test(),
); );
let peer_db = ctx.peer_game_db.write().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( tokio::time::timeout(
Duration::from_secs(1), Duration::from_secs(1),
@@ -3033,7 +3033,7 @@ mod tests {
NetworkControl::disabled_for_test(), NetworkControl::disabled_for_test(),
); );
let peer_db = ctx.peer_game_db.write().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( tokio::time::timeout(
Duration::from_secs(1), Duration::from_secs(1),
@@ -3070,7 +3070,7 @@ mod tests {
NetworkControl::disabled_for_test(), NetworkControl::disabled_for_test(),
); );
let peer_db = ctx.peer_game_db.write().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( tokio::time::timeout(
Duration::from_secs(1), Duration::from_secs(1),
@@ -3100,7 +3100,7 @@ mod tests {
.current_publication() .current_publication()
.expect("initial Call-to-Play projection should load") .expect("initial Call-to-Play projection should load")
.view; .view;
let (tx, mut rx) = mpsc::unbounded_channel(); let (tx, mut rx) = crate::peer_event_channel();
let (reply_tx, reply_rx) = oneshot::channel(); let (reply_tx, reply_rx) = oneshot::channel();
tokio::time::timeout( tokio::time::timeout(
@@ -3136,7 +3136,7 @@ mod tests {
let games = TempDir::new("lanspread-handler-enabled-call-to-play"); let games = TempDir::new("lanspread-handler-enabled-call-to-play");
let ctx = test_ctx(games.path().to_path_buf()); let ctx = test_ctx(games.path().to_path_buf());
let store = ctx.call_to_play.write().await; 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 (reply_tx, mut reply_rx) = oneshot::channel();
let task = tokio::spawn({ let task = tokio::spawn({
let ctx = ctx.clone(); let ctx = ctx.clone();
@@ -3177,7 +3177,7 @@ mod tests {
async fn streamed_install_without_extracted_catalog_manifest_fails_before_admission() { async fn streamed_install_without_extracted_catalog_manifest_fails_before_admission() {
let games = TempDir::new("lanspread-handler-stream-capability"); let games = TempDir::new("lanspread-handler-stream-capability");
let ctx = test_ctx(games.path().to_path_buf()); 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( handle_stream_install_game_command(
&ctx, &ctx,
@@ -3226,7 +3226,7 @@ mod tests {
) )
.await .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; handle_download_game_files_command(&ctx, &tx, "game".to_string(), false).await;
@@ -3264,7 +3264,7 @@ mod tests {
.write() .write()
.await .await
.insert("game".to_owned(), OperationKind::Downloading); .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; handle_download_game_files_command(&ctx, &tx, "game".to_owned(), false).await;
@@ -3401,7 +3401,7 @@ mod tests {
games.path().to_path_buf(), games.path().to_path_buf(),
catalog_bundle([("broken", "20250101"), ("healthy", "20250101")]), 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) let error = load_local_library(&ctx, &tx)
.await .await
@@ -3445,7 +3445,7 @@ mod tests {
.expect("game-root symlink should be created"); .expect("game-root symlink should be created");
let ctx = test_ctx(games.path().to_path_buf()); 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) let error = load_local_library(&ctx, &tx)
.await .await
@@ -3470,7 +3470,7 @@ mod tests {
async fn operation_admission_rejects_recovering_and_failed_games() { async fn operation_admission_rejects_recovering_and_failed_games() {
let games = TempDir::new("lanspread-handler-recovery-admission"); let games = TempDir::new("lanspread-handler-recovery-admission");
let ctx = test_ctx(games.path().to_path_buf()); 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()); ctx.recovery_quarantine.begin(games.path().to_path_buf());
let target = operation_target(games.path()); let target = operation_target(games.path());
@@ -3496,7 +3496,7 @@ mod tests {
let games = TempDir::new("lanspread-handler-download-refresh-failure"); let games = TempDir::new("lanspread-handler-download-refresh-failure");
write_file(&games.game_root(), b"not a directory"); write_file(&games.game_root(), b"not a directory");
let ctx = test_ctx(games.path().to_path_buf()); 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; let (status, cancel) = register_test_download(&ctx, &tx).await;
ctx.active_operations ctx.active_operations
.write() .write()
@@ -3519,14 +3519,14 @@ mod tests {
assert_no_event(&mut rx).await; 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()) tokio::time::timeout(Duration::from_secs(1), rx.recv())
.await .await
.expect("event should arrive") .expect("event should arrive")
.expect("event channel should remain open") .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!( assert!(
tokio::time::timeout(Duration::from_millis(50), rx.recv()) tokio::time::timeout(Duration::from_millis(50), rx.recv())
.await .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(); let mut events = Vec::new();
while let Ok(event) = rx.try_recv() { while let Ok(event) = rx.try_recv() {
events.push(event); events.push(event);
@@ -3682,7 +3682,7 @@ mod tests {
let state = TempDir::new("lanspread-handler-stream-retry-setup-state"); let state = TempDir::new("lanspread-handler-stream-retry-setup-state");
std::fs::create_dir_all(games.game_root().join("local")) std::fs::create_dir_all(games.game_root().join("local"))
.expect("installed tree should be created"); .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( let status = DownloadAttemptStatus::new(
DownloadAttemptKey::next("game".to_owned()), DownloadAttemptKey::next("game".to_owned()),
CancellationToken::new(), CancellationToken::new(),
@@ -3719,7 +3719,7 @@ mod tests {
write_file(&root.join("game.eti"), b"archive"); write_file(&root.join("game.eti"), b"archive");
let ctx = test_ctx(temp.path().to_path_buf()); 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 catalog = ctx.catalog.catalog();
// 1. Initial scan: the game is ready and announced // 1. Initial scan: the game is ready and announced
@@ -3845,7 +3845,7 @@ mod tests {
.expect("catalog construction should defer manifest body parsing"), .expect("catalog construction should defer manifest body parsing"),
); );
let ctx = test_ctx_with_catalog(games.path().to_path_buf(), catalog); 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()) let scan = scan_local_library(games.path(), ctx.state_dir.as_ref(), ctx.catalog.catalog())
.await .await
.expect("ready game should scan before manifest priming"); .expect("ready game should scan before manifest priming");
@@ -3884,7 +3884,7 @@ mod tests {
); );
let ctx = test_ctx(current.path().to_path_buf()); 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 catalog = ctx.catalog.catalog();
let current_scan = scan_local_library(current.path(), ctx.state_dir.as_ref(), 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"); write_file(&root.join("game.eti"), b"archive");
let ctx = test_ctx(temp.path().to_path_buf()); 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 catalog = ctx.catalog.catalog();
let older_scan = scan_local_library(temp.path(), ctx.state_dir.as_ref(), catalog) let older_scan = scan_local_library(temp.path(), ctx.state_dir.as_ref(), catalog)
.await .await
@@ -3973,7 +3973,7 @@ mod tests {
write_file(&root.join("game.eti"), b"archive"); write_file(&root.join("game.eti"), b"archive");
let ctx = test_ctx(temp.path().to_path_buf()); 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 target = operation_target(temp.path());
assert_eq!( assert_eq!(
@@ -3997,7 +3997,7 @@ mod tests {
write_file(&root.join("game.eti"), b"archive"); write_file(&root.join("game.eti"), b"archive");
let ctx = test_ctx(temp.path().to_path_buf()); 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()) let scan = scan_local_library(temp.path(), ctx.state_dir.as_ref(), ctx.catalog.catalog())
.await .await
.expect("initial scan should succeed"); .expect("initial scan should succeed");
@@ -4057,7 +4057,7 @@ mod tests {
write_file(&root.join("game.eti"), b"archive"); write_file(&root.join("game.eti"), b"archive");
let ctx = test_ctx(temp.path().to_path_buf()); 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 target = operation_target(temp.path());
let scan = scan_local_library(temp.path(), ctx.state_dir.as_ref(), ctx.catalog.catalog()) let scan = scan_local_library(temp.path(), ctx.state_dir.as_ref(), ctx.catalog.catalog())
.await .await
@@ -4122,7 +4122,7 @@ mod tests {
write_file(&root.join("game.eti"), b"archive"); write_file(&root.join("game.eti"), b"archive");
let ctx = test_ctx(temp.path().to_path_buf()); 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()) let scan = scan_local_library(temp.path(), ctx.state_dir.as_ref(), ctx.catalog.catalog())
.await .await
.expect("initial scan should succeed"); .expect("initial scan should succeed");
@@ -4195,7 +4195,7 @@ mod tests {
write_file(&root.join("game.eti"), b"archive"); write_file(&root.join("game.eti"), b"archive");
let ctx = test_ctx(temp.path().to_path_buf()); 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 catalog = ctx.catalog.catalog();
let scan = scan_local_library(temp.path(), ctx.state_dir.as_ref(), 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"); write_file(&root.join("local").join("old.txt"), b"old");
let ctx = test_ctx(temp.path().to_path_buf()); 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 catalog = ctx.catalog.catalog();
let scan = scan_local_library(temp.path(), ctx.state_dir.as_ref(), catalog) let scan = scan_local_library(temp.path(), ctx.state_dir.as_ref(), catalog)
.await .await
@@ -4256,7 +4256,7 @@ mod tests {
write_file(&root.join("game.eti"), b"archive"); write_file(&root.join("game.eti"), b"archive");
let ctx = test_ctx(temp.path().to_path_buf()); 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; run_install_operation(&ctx, &tx, operation_target(temp.path())).await;
@@ -4281,7 +4281,7 @@ mod tests {
write_file(&root.join("game.eti"), b"archive"); write_file(&root.join("game.eti"), b"archive");
let ctx = test_ctx(temp.path().to_path_buf()); let ctx = test_ctx(temp.path().to_path_buf());
let target = operation_target(temp.path()); 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) let prepared = prepare_install_operation(&ctx, &tx, &target)
.await .await
@@ -4334,7 +4334,7 @@ mod tests {
write_file(&root.join("game.eti"), b"archive"); write_file(&root.join("game.eti"), b"archive");
let ctx = test_ctx(temp.path().to_path_buf()); let ctx = test_ctx(temp.path().to_path_buf());
let target = operation_target(temp.path()); 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!(stream_install_target_is_ready(&ctx, &target).await);
assert_eq!( assert_eq!(
@@ -4383,7 +4383,7 @@ mod tests {
write_file(&root.join("game.eti"), b"archive"); write_file(&root.join("game.eti"), b"archive");
let ctx = test_ctx(temp.path().to_path_buf()); 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; let (download_status, _download_cancel) = register_test_download(&ctx, &prepare_tx).await;
ctx.active_operations ctx.active_operations
.write() .write()
@@ -4394,7 +4394,7 @@ mod tests {
.await .await
.expect("downloaded game should be installable"); .expect("downloaded game should be installable");
let read_guard = ctx.active_operations.read().await; 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 install_task = tokio::spawn({
let ctx = ctx.clone(); let ctx = ctx.clone();
@@ -4445,7 +4445,7 @@ mod tests {
async fn cancel_download_command_only_cancels_active_token() { async fn cancel_download_command_only_cancels_active_token() {
let temp = TempDir::new("lanspread-handler-cancel-download"); let temp = TempDir::new("lanspread-handler-cancel-download");
let ctx = test_ctx(temp.path().to_path_buf()); 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; let (_status, cancel) = register_test_download(&ctx, &tx).await;
ctx.active_operations ctx.active_operations
.write() .write()
@@ -4472,7 +4472,7 @@ mod tests {
write_file(&root.join("local").join("old.txt"), b"old"); write_file(&root.join("local").join("old.txt"), b"old");
let ctx = test_ctx(temp.path().to_path_buf()); 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; 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"); write_file(&root.join("game.eti"), b"old archive");
let ctx = test_ctx(temp.path().to_path_buf()); 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; run_install_operation(&ctx, &tx, operation_target(temp.path())).await;
assert_active_update( assert_active_update(
@@ -4560,7 +4560,7 @@ mod tests {
write_file(&root.join("local").join("old.txt"), b"old"); write_file(&root.join("local").join("old.txt"), b"old");
let ctx = test_ctx(temp.path().to_path_buf()); 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; run_uninstall_operation(&ctx, &tx, operation_target(temp.path())).await;
@@ -4592,7 +4592,7 @@ mod tests {
&["game.eti"], &["game.eti"],
) )
.await; .await;
let (tx, mut rx) = mpsc::unbounded_channel(); let (tx, mut rx) = crate::peer_event_channel();
let catalog = ctx.catalog.catalog(); let catalog = ctx.catalog.catalog();
let scan = scan_local_library(temp.path(), ctx.state_dir.as_ref(), catalog) let scan = scan_local_library(temp.path(), ctx.state_dir.as_ref(), catalog)
.await .await
@@ -4630,7 +4630,7 @@ mod tests {
write_file(&root.join("game.eti"), b"archive"); write_file(&root.join("game.eti"), b"archive");
} }
let ctx = test_ctx(current.path().to_path_buf()); 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 // On the current-thread test runtime, this ready lock acquisition does
// not yield to the newly spawned command. Move the configured root // 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"); write_file(&root.join("local/payload.txt"), b"installed");
} }
let ctx = test_ctx(current.path().to_path_buf()); 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; handle_uninstall_game_command(&ctx, &tx, "game".to_string()).await;
*ctx.game_dir.write().await = next.path().to_path_buf(); *ctx.game_dir.write().await = next.path().to_path_buf();
@@ -4715,7 +4715,7 @@ mod tests {
&["game.eti"], &["game.eti"],
) )
.await; .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; handle_remove_downloaded_game_command(&ctx, &tx, "game".to_string()).await;
*ctx.game_dir.write().await = next.path().to_path_buf(); *ctx.game_dir.write().await = next.path().to_path_buf();
@@ -4757,7 +4757,7 @@ mod tests {
.write() .write()
.await .await
.insert("game".to_string(), OperationKind::Downloading); .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()) let error = handle_set_game_dir_command(&ctx, &tx, next.path().to_path_buf())
.await .await
@@ -4778,7 +4778,7 @@ mod tests {
symlink(current.path(), &alias).expect("same-root alias should be created"); symlink(current.path(), &alias).expect("same-root alias should be created");
let current = std::fs::canonicalize(current.path()).expect("root should canonicalize"); let current = std::fs::canonicalize(current.path()).expect("root should canonicalize");
let ctx = test_ctx(current.clone()); 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) let accepted = handle_set_game_dir_command(&ctx, &tx, alias)
.await .await
@@ -4801,7 +4801,7 @@ mod tests {
let current = std::fs::canonicalize(current.path()).expect("root should canonicalize"); let current = std::fs::canonicalize(current.path()).expect("root should canonicalize");
let next = std::fs::canonicalize(next.path()).expect("new root should canonicalize"); let next = std::fs::canonicalize(next.path()).expect("new root should canonicalize");
let ctx = test_ctx(current); 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()) let accepted = handle_set_game_dir_command(&ctx, &tx, alias.clone())
.await .await
@@ -4817,7 +4817,7 @@ mod tests {
let current = TempDir::new("lanspread-handler-invalid-dir-current"); let current = TempDir::new("lanspread-handler-invalid-dir-current");
let missing = current.path().join("missing"); let missing = current.path().join("missing");
let ctx = test_ctx(current.path().to_path_buf()); 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) let error = handle_set_game_dir_command(&ctx, &tx, missing)
.await .await
@@ -4837,7 +4837,7 @@ mod tests {
let candidate_path = let candidate_path =
std::fs::canonicalize(candidate.path()).expect("candidate root should canonicalize"); std::fs::canonicalize(candidate.path()).expect("candidate root should canonicalize");
let ctx = test_ctx(current_path.clone()); 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); let published = summary("game", "20250101", Availability::Ready);
ctx.local_library ctx.local_library
.write() .write()
@@ -4884,7 +4884,7 @@ mod tests {
let temp = TempDir::new("lanspread-handler-same-dir"); let temp = TempDir::new("lanspread-handler-same-dir");
write_file(&temp.game_root().join(".version.ini.tmp"), b"tmp"); write_file(&temp.game_root().join(".version.ini.tmp"), b"tmp");
let ctx = test_ctx(temp.path().to_path_buf()); 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 first = CancellationToken::new();
let second = CancellationToken::new(); let second = CancellationToken::new();
*ctx.active_outbound_transfers.write().await = HashMap::from([ *ctx.active_outbound_transfers.write().await = HashMap::from([
@@ -4936,7 +4936,7 @@ mod tests {
let temp = TempDir::new("lanspread-handler-same-dir-timeout"); let temp = TempDir::new("lanspread-handler-same-dir-timeout");
write_file(&temp.game_root().join(".version.ini.tmp"), b"tmp"); write_file(&temp.game_root().join(".version.ini.tmp"), b"tmp");
let ctx = test_ctx(temp.path().to_path_buf()); 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(); let token = CancellationToken::new();
ctx.active_outbound_transfers ctx.active_outbound_transfers
.write() .write()
@@ -4969,7 +4969,7 @@ mod tests {
.write() .write()
.await .await
.insert("game".to_string(), OperationKind::Downloading); .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()) let error = handle_set_game_dir_command(&ctx, &tx, temp.path().to_path_buf())
.await .await
@@ -4985,7 +4985,7 @@ mod tests {
let next = TempDir::new("lanspread-handler-new-dir"); let next = TempDir::new("lanspread-handler-new-dir");
write_file(&next.game_root().join(".version.ini.tmp"), b"tmp"); write_file(&next.game_root().join(".version.ini.tmp"), b"tmp");
let ctx = test_ctx(current.path().to_path_buf()); 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 first = CancellationToken::new();
let second = CancellationToken::new(); let second = CancellationToken::new();
*ctx.active_outbound_transfers.write().await = HashMap::from([ *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("version.ini"), b"20250101");
write_file(&root.join("game.eti"), b"archive"); write_file(&root.join("game.eti"), b"archive");
let ctx = test_ctx(current.path().to_path_buf()); 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 catalog = ctx.catalog.catalog();
let scan = scan_local_library(current.path(), ctx.state_dir.as_ref(), 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 current = TempDir::new("lanspread-handler-revision-exhausted-current");
let next = TempDir::new("lanspread-handler-revision-exhausted-next"); let next = TempDir::new("lanspread-handler-revision-exhausted-next");
let ctx = test_ctx(current.path().to_path_buf()); 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); let published = summary("game", "20250101", Availability::Ready);
ctx.local_library ctx.local_library
.write() .write()
@@ -5138,7 +5138,7 @@ mod tests {
std::fs::create_dir_all(next_root.join(".version.ini.tmp")) std::fs::create_dir_all(next_root.join(".version.ini.tmp"))
.expect("invalid recovery scratch directory should be created"); .expect("invalid recovery scratch directory should be created");
let ctx = test_ctx(current.path().to_path_buf()); 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!( assert_eq!(
handle_set_game_dir_command_with_drain_timeout( handle_set_game_dir_command_with_drain_timeout(
@@ -5198,7 +5198,7 @@ mod tests {
} }
let ctx = test_ctx(current.path().to_path_buf()); 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 catalog = ctx.catalog.catalog();
let scan = scan_local_library(current.path(), ctx.state_dir.as_ref(), catalog) let scan = scan_local_library(current.path(), ctx.state_dir.as_ref(), catalog)
.await .await
+12 -11
View File
@@ -87,7 +87,7 @@ pub use scoped_blocking::scoped_blocking;
pub use scoped_process::{ScopedProcess, ScopedProcessOutput}; pub use scoped_process::{ScopedProcess, ScopedProcessOutput};
use tokio::sync::{ use tokio::sync::{
RwLock, RwLock,
mpsc::{UnboundedReceiver, UnboundedSender}, mpsc::UnboundedReceiver,
oneshot, oneshot,
}; };
use tokio_util::{sync::CancellationToken, task::TaskTracker}; use tokio_util::{sync::CancellationToken, task::TaskTracker};
@@ -134,6 +134,7 @@ pub use crate::{
StreamInstallProvider, StreamInstallProvider,
}, },
}; };
pub use events::{PeerEventReceiver, PeerEventSender, peer_event_channel};
// ============================================================================= // =============================================================================
// Public API types // Public API types
@@ -563,7 +564,7 @@ impl std::fmt::Debug for PeerStartOptions {
#[allow(clippy::implicit_hasher)] #[allow(clippy::implicit_hasher)]
pub fn start_peer( pub fn start_peer(
game_dir: impl Into<PathBuf>, game_dir: impl Into<PathBuf>,
tx_notify_ui: UnboundedSender<PeerEvent>, tx_notify_ui: PeerEventSender,
peer_game_db: Arc<RwLock<PeerGameDB>>, peer_game_db: Arc<RwLock<PeerGameDB>>,
unpacker: Arc<dyn Unpacker>, unpacker: Arc<dyn Unpacker>,
catalog: Arc<CatalogBundle>, catalog: Arc<CatalogBundle>,
@@ -582,7 +583,7 @@ pub fn start_peer(
#[allow(clippy::implicit_hasher)] #[allow(clippy::implicit_hasher)]
pub fn start_peer_with_options( pub fn start_peer_with_options(
game_dir: impl Into<PathBuf>, game_dir: impl Into<PathBuf>,
tx_notify_ui: UnboundedSender<PeerEvent>, tx_notify_ui: PeerEventSender,
peer_game_db: Arc<RwLock<PeerGameDB>>, peer_game_db: Arc<RwLock<PeerGameDB>>,
unpacker: Arc<dyn Unpacker>, unpacker: Arc<dyn Unpacker>,
catalog: Arc<CatalogBundle>, catalog: Arc<CatalogBundle>,
@@ -669,7 +670,7 @@ const fn identity_durability(persistence: &PeerIdentityPersistence) -> PeerIdent
#[allow(clippy::too_many_arguments, clippy::implicit_hasher)] #[allow(clippy::too_many_arguments, clippy::implicit_hasher)]
async fn run_peer( async fn run_peer(
mut rx_control: UnboundedReceiver<PeerCommand>, mut rx_control: UnboundedReceiver<PeerCommand>,
tx_notify_ui: UnboundedSender<PeerEvent>, tx_notify_ui: PeerEventSender,
peer_game_db: Arc<RwLock<PeerGameDB>>, peer_game_db: Arc<RwLock<PeerGameDB>>,
peer_identity: Arc<PeerIdentity>, peer_identity: Arc<PeerIdentity>,
game_dir: PathBuf, game_dir: PathBuf,
@@ -724,7 +725,7 @@ async fn run_peer(
async fn handle_peer_commands( async fn handle_peer_commands(
ctx: &Ctx, ctx: &Ctx,
tx_notify_ui: &UnboundedSender<PeerEvent>, tx_notify_ui: &PeerEventSender,
rx_control: &mut UnboundedReceiver<PeerCommand>, rx_control: &mut UnboundedReceiver<PeerCommand>,
) -> eyre::Result<()> { ) -> eyre::Result<()> {
loop { loop {
@@ -805,7 +806,7 @@ async fn handle_peer_commands(
async fn handle_apply_call_to_play_intent( async fn handle_apply_call_to_play_intent(
ctx: &Ctx, ctx: &Ctx,
tx_notify_ui: &UnboundedSender<PeerEvent>, tx_notify_ui: &PeerEventSender,
intent: CallToPlayLocalIntent, intent: CallToPlayLocalIntent,
display_name: String, display_name: String,
reply: oneshot::Sender<Result<CallToPlayReceipt, String>>, reply: oneshot::Sender<Result<CallToPlayReceipt, String>>,
@@ -837,7 +838,7 @@ async fn handle_apply_call_to_play_intent(
async fn handle_set_call_to_play_display_name( async fn handle_set_call_to_play_display_name(
ctx: &Ctx, ctx: &Ctx,
tx_notify_ui: &UnboundedSender<PeerEvent>, tx_notify_ui: &PeerEventSender,
display_name: String, display_name: String,
reply: oneshot::Sender<Result<bool, String>>, reply: oneshot::Sender<Result<bool, String>>,
) { ) {
@@ -863,7 +864,7 @@ async fn handle_set_call_to_play_display_name(
async fn handle_get_call_to_play_view( async fn handle_get_call_to_play_view(
ctx: &Ctx, ctx: &Ctx,
tx_notify_ui: &UnboundedSender<PeerEvent>, tx_notify_ui: &PeerEventSender,
reply: Option<oneshot::Sender<Result<CallToPlayView, String>>>, reply: Option<oneshot::Sender<Result<CallToPlayView, String>>>,
) { ) {
let result = { let result = {
@@ -999,7 +1000,7 @@ mod configured_game_dir_tests {
let state = TempDir::new("lanspread-startup-state-root"); let state = TempDir::new("lanspread-startup-state-root");
let alias = target.path().join("."); let alias = target.path().join(".");
let expected = std::fs::canonicalize(target.path()).expect("target should canonicalize"); let expected = std::fs::canonicalize(target.path()).expect("target should canonicalize");
let (events, _event_rx) = mpsc::unbounded_channel(); let (events, _event_rx) = crate::peer_event_channel();
let mut handle = start_peer_with_options( let mut handle = start_peer_with_options(
alias, alias,
@@ -1044,7 +1045,7 @@ mod configured_game_dir_tests {
let identity = Arc::new(loaded.identity); let identity = Arc::new(loaded.identity);
let expected_peer_id = identity.peer_id(); let expected_peer_id = identity.peer_id();
let (tx_control, rx_control) = mpsc::unbounded_channel(); let (tx_control, rx_control) = mpsc::unbounded_channel();
let (tx_events, mut rx_events) = mpsc::unbounded_channel(); let (tx_events, mut rx_events) = crate::peer_event_channel();
let mut handle = crate::startup::spawn_peer_runtime( let mut handle = crate::startup::spawn_peer_runtime(
tx_control, tx_control,
@@ -1128,7 +1129,7 @@ mod configured_game_dir_tests {
) )
.expect("old-root intent should be valid"); .expect("old-root intent should be valid");
write_intent(state.path(), "orphan", &intent).expect("old-root intent should be persisted"); write_intent(state.path(), "orphan", &intent).expect("old-root intent should be persisted");
let (events, _event_rx) = mpsc::unbounded_channel(); let (events, _event_rx) = crate::peer_event_channel();
let result = start_peer_with_options( let result = start_peer_with_options(
candidate.path().to_path_buf(), candidate.path().to_path_buf(),
+24 -23
View File
@@ -20,6 +20,7 @@ use tokio_util::{
use crate::{ use crate::{
LocalNetworkSharingState, LocalNetworkSharingState,
PeerEvent, PeerEvent,
PeerEventSender,
PeerRuntimeComponent, PeerRuntimeComponent,
context::{Ctx, NetworkServiceCtx}, context::{Ctx, NetworkServiceCtx},
events, events,
@@ -283,7 +284,7 @@ impl EnableFailure {
struct GenerationStart { struct GenerationStart {
id: NetworkGenerationId, id: NetworkGenerationId,
core: Ctx, core: Ctx,
tx_notify_ui: mpsc::UnboundedSender<PeerEvent>, tx_notify_ui: PeerEventSender,
shutdown: CancellationToken, shutdown: CancellationToken,
tasks: TaskTracker, tasks: TaskTracker,
failure_tx: mpsc::UnboundedSender<GenerationFailure>, failure_tx: mpsc::UnboundedSender<GenerationFailure>,
@@ -410,7 +411,7 @@ impl NetworkManager {
pub(crate) async fn run( pub(crate) async fn run(
mut self, mut self,
ctx: Ctx, ctx: Ctx,
tx_notify_ui: mpsc::UnboundedSender<PeerEvent>, tx_notify_ui: PeerEventSender,
initially_enabled: bool, initially_enabled: bool,
) { ) {
let mut current = None; let mut current = None;
@@ -445,7 +446,7 @@ impl NetworkManager {
async fn run_body( async fn run_body(
&mut self, &mut self,
ctx: &Ctx, ctx: &Ctx,
tx_notify_ui: &mpsc::UnboundedSender<PeerEvent>, tx_notify_ui: &PeerEventSender,
initially_enabled: bool, initially_enabled: bool,
current: &mut Option<NetworkGeneration>, current: &mut Option<NetworkGeneration>,
) { ) {
@@ -495,7 +496,7 @@ impl NetworkManager {
async fn enable( async fn enable(
&mut self, &mut self,
ctx: &Ctx, ctx: &Ctx,
tx_notify_ui: &mpsc::UnboundedSender<PeerEvent>, tx_notify_ui: &PeerEventSender,
current: &mut Option<NetworkGeneration>, current: &mut Option<NetworkGeneration>,
) -> Result<bool, EnableFailure> { ) -> Result<bool, EnableFailure> {
if current.is_some() { if current.is_some() {
@@ -585,7 +586,7 @@ impl NetworkManager {
async fn start_generation( async fn start_generation(
&self, &self,
ctx: &Ctx, ctx: &Ctx,
tx_notify_ui: &mpsc::UnboundedSender<PeerEvent>, tx_notify_ui: &PeerEventSender,
id: NetworkGenerationId, id: NetworkGenerationId,
shutdown: &CancellationToken, shutdown: &CancellationToken,
tasks: &TaskTracker, tasks: &TaskTracker,
@@ -673,7 +674,7 @@ impl NetworkManager {
async fn disable( async fn disable(
&self, &self,
ctx: &Ctx, ctx: &Ctx,
tx_notify_ui: &mpsc::UnboundedSender<PeerEvent>, tx_notify_ui: &PeerEventSender,
current: &mut Option<NetworkGeneration>, current: &mut Option<NetworkGeneration>,
) { ) {
let Some(generation) = current.as_mut() else { let Some(generation) = current.as_mut() else {
@@ -693,7 +694,7 @@ impl NetworkManager {
async fn stop_generation( async fn stop_generation(
&self, &self,
ctx: &Ctx, ctx: &Ctx,
tx_notify_ui: &mpsc::UnboundedSender<PeerEvent>, tx_notify_ui: &PeerEventSender,
generation: &mut NetworkGeneration, generation: &mut NetworkGeneration,
) { ) {
self.begin_stop_generation(generation); self.begin_stop_generation(generation);
@@ -800,7 +801,7 @@ async fn clear_local_peer_addr(ctx: &Ctx) {
*ctx.local_peer_addr.write().await = None; *ctx.local_peer_addr.write().await = None;
} }
fn send_state(tx_notify_ui: &mpsc::UnboundedSender<PeerEvent>, state: LocalNetworkSharingState) { fn send_state(tx_notify_ui: &PeerEventSender, state: LocalNetworkSharingState) {
events::send( events::send(
tx_notify_ui, tx_notify_ui,
PeerEvent::LocalNetworkSharingStateChanged(state), PeerEvent::LocalNetworkSharingStateChanged(state),
@@ -808,7 +809,7 @@ fn send_state(tx_notify_ui: &mpsc::UnboundedSender<PeerEvent>, state: LocalNetwo
} }
fn report_failure( fn report_failure(
tx_notify_ui: &mpsc::UnboundedSender<PeerEvent>, tx_notify_ui: &PeerEventSender,
component: PeerRuntimeComponent, component: PeerRuntimeComponent,
error: String, error: String,
) { ) {
@@ -991,7 +992,7 @@ mod tests {
} }
async fn wait_for_state( async fn wait_for_state(
events: &mut mpsc::UnboundedReceiver<PeerEvent>, events: &mut crate::PeerEventReceiver,
expected: LocalNetworkSharingState, expected: LocalNetworkSharingState,
) { ) {
loop { loop {
@@ -1055,7 +1056,7 @@ mod tests {
async fn startup_disabled_does_not_construct_a_network_generation() { async fn startup_disabled_does_not_construct_a_network_generation() {
let harness = test_harness(); let harness = test_harness();
let (_temp, ctx) = test_ctx(harness.control.clone()); let (_temp, ctx) = test_ctx(harness.control.clone());
let (event_tx, mut events) = mpsc::unbounded_channel(); let (event_tx, mut events) = crate::peer_event_channel();
let manager_task = tokio::spawn(harness.manager.run(ctx.clone(), event_tx, false)); let manager_task = tokio::spawn(harness.manager.run(ctx.clone(), event_tx, false));
wait_for_state(&mut events, LocalNetworkSharingState::Disabled).await; wait_for_state(&mut events, LocalNetworkSharingState::Disabled).await;
@@ -1071,7 +1072,7 @@ mod tests {
async fn enable_reply_waits_for_readiness_and_atomic_admission() { async fn enable_reply_waits_for_readiness_and_atomic_admission() {
let mut harness = test_harness(); let mut harness = test_harness();
let (_temp, ctx) = test_ctx(harness.control.clone()); let (_temp, ctx) = test_ctx(harness.control.clone());
let (event_tx, mut events) = mpsc::unbounded_channel(); let (event_tx, mut events) = crate::peer_event_channel();
let manager_task = tokio::spawn(harness.manager.run(ctx.clone(), event_tx, false)); let manager_task = tokio::spawn(harness.manager.run(ctx.clone(), event_tx, false));
wait_for_state(&mut events, LocalNetworkSharingState::Disabled).await; wait_for_state(&mut events, LocalNetworkSharingState::Disabled).await;
@@ -1105,7 +1106,7 @@ mod tests {
async fn disable_reply_waits_for_every_admitted_permit() { async fn disable_reply_waits_for_every_admitted_permit() {
let mut harness = test_harness(); let mut harness = test_harness();
let (_temp, ctx) = test_ctx(harness.control.clone()); let (_temp, ctx) = test_ctx(harness.control.clone());
let (event_tx, mut events) = mpsc::unbounded_channel(); let (event_tx, mut events) = crate::peer_event_channel();
let manager_task = tokio::spawn(harness.manager.run(ctx.clone(), event_tx, false)); let manager_task = tokio::spawn(harness.manager.run(ctx.clone(), event_tx, false));
wait_for_state(&mut events, LocalNetworkSharingState::Disabled).await; wait_for_state(&mut events, LocalNetworkSharingState::Disabled).await;
@@ -1188,7 +1189,7 @@ mod tests {
async fn readiness_failure_drains_partial_generation_and_allows_reenable() { async fn readiness_failure_drains_partial_generation_and_allows_reenable() {
let mut harness = test_harness(); let mut harness = test_harness();
let (_temp, ctx) = test_ctx(harness.control.clone()); let (_temp, ctx) = test_ctx(harness.control.clone());
let (event_tx, mut events) = mpsc::unbounded_channel(); let (event_tx, mut events) = crate::peer_event_channel();
let manager_task = tokio::spawn(harness.manager.run(ctx.clone(), event_tx, false)); let manager_task = tokio::spawn(harness.manager.run(ctx.clone(), event_tx, false));
wait_for_state(&mut events, LocalNetworkSharingState::Disabled).await; wait_for_state(&mut events, LocalNetworkSharingState::Disabled).await;
@@ -1223,7 +1224,7 @@ mod tests {
async fn disable_interrupts_initial_enable_without_a_failure_diagnostic() { async fn disable_interrupts_initial_enable_without_a_failure_diagnostic() {
let mut harness = test_harness(); let mut harness = test_harness();
let (_temp, ctx) = test_ctx(harness.control.clone()); let (_temp, ctx) = test_ctx(harness.control.clone());
let (event_tx, mut events) = mpsc::unbounded_channel(); let (event_tx, mut events) = crate::peer_event_channel();
let manager_task = tokio::spawn(harness.manager.run(ctx.clone(), event_tx, true)); let manager_task = tokio::spawn(harness.manager.run(ctx.clone(), event_tx, true));
let pending_ready = harness let pending_ready = harness
.started .started
@@ -1273,7 +1274,7 @@ mod tests {
async fn current_required_failure_auto_disables_and_closes_admission() { async fn current_required_failure_auto_disables_and_closes_admission() {
let mut harness = test_harness(); let mut harness = test_harness();
let (_temp, ctx) = test_ctx(harness.control.clone()); let (_temp, ctx) = test_ctx(harness.control.clone());
let (event_tx, mut events) = mpsc::unbounded_channel(); let (event_tx, mut events) = crate::peer_event_channel();
let manager_task = tokio::spawn(harness.manager.run(ctx.clone(), event_tx, false)); let manager_task = tokio::spawn(harness.manager.run(ctx.clone(), event_tx, false));
wait_for_state(&mut events, LocalNetworkSharingState::Disabled).await; wait_for_state(&mut events, LocalNetworkSharingState::Disabled).await;
let (id, failures) = enable_fake_generation(&harness.control, &mut harness.started).await; let (id, failures) = enable_fake_generation(&harness.control, &mut harness.started).await;
@@ -1297,7 +1298,7 @@ mod tests {
async fn root_shutdown_waits_for_held_permit_and_joins_runtime() { async fn root_shutdown_waits_for_held_permit_and_joins_runtime() {
let mut harness = test_harness(); let mut harness = test_harness();
let (_temp, ctx) = test_ctx(harness.control.clone()); let (_temp, ctx) = test_ctx(harness.control.clone());
let (event_tx, mut events) = mpsc::unbounded_channel(); let (event_tx, mut events) = crate::peer_event_channel();
let manager_task = tokio::spawn(harness.manager.run(ctx.clone(), event_tx, false)); let manager_task = tokio::spawn(harness.manager.run(ctx.clone(), event_tx, false));
wait_for_state(&mut events, LocalNetworkSharingState::Disabled).await; wait_for_state(&mut events, LocalNetworkSharingState::Disabled).await;
enable_fake_generation(&harness.control, &mut harness.started).await; enable_fake_generation(&harness.control, &mut harness.started).await;
@@ -1321,7 +1322,7 @@ mod tests {
async fn dropping_enable_reply_does_not_cancel_the_transition() { async fn dropping_enable_reply_does_not_cancel_the_transition() {
let mut harness = test_harness(); let mut harness = test_harness();
let (_temp, ctx) = test_ctx(harness.control.clone()); let (_temp, ctx) = test_ctx(harness.control.clone());
let (event_tx, mut events) = mpsc::unbounded_channel(); let (event_tx, mut events) = crate::peer_event_channel();
let manager_task = tokio::spawn(harness.manager.run(ctx.clone(), event_tx, false)); let manager_task = tokio::spawn(harness.manager.run(ctx.clone(), event_tx, false));
wait_for_state(&mut events, LocalNetworkSharingState::Disabled).await; wait_for_state(&mut events, LocalNetworkSharingState::Disabled).await;
@@ -1362,7 +1363,7 @@ mod tests {
async fn idempotent_stable_requests_do_not_restart_or_republish() { async fn idempotent_stable_requests_do_not_restart_or_republish() {
let mut harness = test_harness(); let mut harness = test_harness();
let (_temp, ctx) = test_ctx(harness.control.clone()); let (_temp, ctx) = test_ctx(harness.control.clone());
let (event_tx, mut events) = mpsc::unbounded_channel(); let (event_tx, mut events) = crate::peer_event_channel();
let manager_task = tokio::spawn(harness.manager.run(ctx.clone(), event_tx, false)); let manager_task = tokio::spawn(harness.manager.run(ctx.clone(), event_tx, false));
wait_for_state(&mut events, LocalNetworkSharingState::Disabled).await; wait_for_state(&mut events, LocalNetworkSharingState::Disabled).await;
@@ -1388,7 +1389,7 @@ mod tests {
async fn client_cleanup_error_is_diagnostic_but_off_ack_is_authoritative() { async fn client_cleanup_error_is_diagnostic_but_off_ack_is_authoritative() {
let mut harness = test_harness(); let mut harness = test_harness();
let (_temp, ctx) = test_ctx(harness.control.clone()); let (_temp, ctx) = test_ctx(harness.control.clone());
let (event_tx, mut events) = mpsc::unbounded_channel(); let (event_tx, mut events) = crate::peer_event_channel();
let manager_task = tokio::spawn(harness.manager.run(ctx.clone(), event_tx, false)); let manager_task = tokio::spawn(harness.manager.run(ctx.clone(), event_tx, false));
wait_for_state(&mut events, LocalNetworkSharingState::Disabled).await; wait_for_state(&mut events, LocalNetworkSharingState::Disabled).await;
enable_fake_generation(&harness.control, &mut harness.started).await; enable_fake_generation(&harness.control, &mut harness.started).await;
@@ -1423,7 +1424,7 @@ mod tests {
let harness = test_harness(); let harness = test_harness();
harness.start_panics.store(true, Ordering::SeqCst); harness.start_panics.store(true, Ordering::SeqCst);
let (_temp, ctx) = test_ctx(harness.control.clone()); let (_temp, ctx) = test_ctx(harness.control.clone());
let (event_tx, mut events) = mpsc::unbounded_channel(); let (event_tx, mut events) = crate::peer_event_channel();
let manager_task = tokio::spawn(harness.manager.run(ctx.clone(), event_tx, false)); let manager_task = tokio::spawn(harness.manager.run(ctx.clone(), event_tx, false));
wait_for_state(&mut events, LocalNetworkSharingState::Disabled).await; wait_for_state(&mut events, LocalNetworkSharingState::Disabled).await;
@@ -1448,7 +1449,7 @@ mod tests {
async fn stale_generation_failure_cannot_close_current_admission() { async fn stale_generation_failure_cannot_close_current_admission() {
let mut harness = test_harness(); let mut harness = test_harness();
let (_temp, ctx) = test_ctx(harness.control.clone()); let (_temp, ctx) = test_ctx(harness.control.clone());
let (event_tx, mut events) = mpsc::unbounded_channel(); let (event_tx, mut events) = crate::peer_event_channel();
let manager_task = tokio::spawn(harness.manager.run(ctx.clone(), event_tx, false)); let manager_task = tokio::spawn(harness.manager.run(ctx.clone(), event_tx, false));
wait_for_state(&mut events, LocalNetworkSharingState::Disabled).await; wait_for_state(&mut events, LocalNetworkSharingState::Disabled).await;
@@ -1513,7 +1514,7 @@ mod tests {
let mut harness = test_harness(); let mut harness = test_harness();
harness.manager.last_generation = u64::MAX; harness.manager.last_generation = u64::MAX;
let (_temp, ctx) = test_ctx(harness.control.clone()); let (_temp, ctx) = test_ctx(harness.control.clone());
let (event_tx, mut events) = mpsc::unbounded_channel(); let (event_tx, mut events) = crate::peer_event_channel();
let manager_task = tokio::spawn(harness.manager.run(ctx.clone(), event_tx, false)); let manager_task = tokio::spawn(harness.manager.run(ctx.clone(), event_tx, false));
wait_for_state(&mut events, LocalNetworkSharingState::Disabled).await; wait_for_state(&mut events, LocalNetworkSharingState::Disabled).await;
@@ -12,13 +12,14 @@ use futures::{StreamExt as _, stream::FuturesUnordered};
use lanspread_mdns::{LANSPREAD_SERVICE_TYPE, MdnsBrowser, MdnsService, MdnsServicePoll}; use lanspread_mdns::{LANSPREAD_SERVICE_TYPE, MdnsBrowser, MdnsService, MdnsServicePoll};
use lanspread_proto::{PROTOCOL_VERSION, PeerEndpoint, PeerId}; use lanspread_proto::{PROTOCOL_VERSION, PeerEndpoint, PeerId};
use tokio::sync::{ use tokio::sync::{
mpsc::{self, UnboundedSender}, mpsc,
oneshot, oneshot,
}; };
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
use crate::{ use crate::{
PeerEvent, PeerEvent,
PeerEventSender,
context::NetworkServiceCtx, context::NetworkServiceCtx,
events, events,
services::{ services::{
@@ -188,7 +189,7 @@ impl Drop for DiscoveryWorker {
/// Runs the peer discovery service using mDNS. /// Runs the peer discovery service using mDNS.
#[allow(clippy::too_many_lines)] #[allow(clippy::too_many_lines)]
pub async fn run_peer_discovery( pub async fn run_peer_discovery(
tx_notify_ui: UnboundedSender<PeerEvent>, tx_notify_ui: PeerEventSender,
ctx: NetworkServiceCtx, ctx: NetworkServiceCtx,
) -> eyre::Result<()> { ) -> eyre::Result<()> {
log::info!("Starting peer discovery task"); log::info!("Starting peer discovery task");
@@ -3,10 +3,8 @@
use std::{sync::Arc, time::Duration}; use std::{sync::Arc, time::Duration};
use futures::{StreamExt as _, stream}; use futures::{StreamExt as _, stream};
use tokio::sync::mpsc::UnboundedSender;
use crate::{ use crate::{
PeerEvent, PeerEventSender,
config::{PEER_PING_IDLE_SECS, PEER_PING_INTERVAL_SECS, peer_stale_timeout}, config::{PEER_PING_IDLE_SECS, PEER_PING_INTERVAL_SECS, peer_stale_timeout},
content_quarantine::ContentQuarantine, content_quarantine::ContentQuarantine,
context::{NetworkServiceCtx, OperationKind}, context::{NetworkServiceCtx, OperationKind},
@@ -22,7 +20,7 @@ const MAX_CONCURRENT_PINGS: usize = 8;
/// uses `last_revision_check`; inbound and content traffic only affect /// uses `last_revision_check`; inbound and content traffic only affect
/// `last_seen`, which remains the stale-pruning clock. /// `last_seen`, which remains the stale-pruning clock.
pub async fn run_ping_service( pub async fn run_ping_service(
tx_notify_ui: UnboundedSender<PeerEvent>, tx_notify_ui: PeerEventSender,
ctx: NetworkServiceCtx, ctx: NetworkServiceCtx,
) -> eyre::Result<()> { ) -> eyre::Result<()> {
log::info!( log::info!(
@@ -14,13 +14,13 @@ use std::{
use futures::FutureExt; use futures::FutureExt;
use tokio::{ use tokio::{
sync::{RwLock, mpsc::UnboundedSender}, sync::RwLock,
task::{JoinError, JoinSet}, task::{JoinError, JoinSet},
time::{Instant, MissedTickBehavior}, time::{Instant, MissedTickBehavior},
}; };
use crate::{ use crate::{
PeerEvent, PeerEventSender,
config::{LOCAL_GAME_FALLBACK_SCAN_SECS, LOCAL_GAME_POLL_INTERVAL_SECS}, config::{LOCAL_GAME_FALLBACK_SCAN_SECS, LOCAL_GAME_POLL_INTERVAL_SECS},
context::Ctx, context::Ctx,
game_paths::{is_download_protected_root_name, is_ignored_games_root_name}, game_paths::{is_download_protected_root_name, is_ignored_games_root_name},
@@ -67,10 +67,7 @@ struct RescanGate {
} }
/// Monitors the local game directory for changes. /// Monitors the local game directory for changes.
pub async fn run_local_game_monitor( pub async fn run_local_game_monitor(tx_notify_ui: PeerEventSender, ctx: Ctx) -> eyre::Result<()> {
tx_notify_ui: UnboundedSender<PeerEvent>,
ctx: Ctx,
) -> eyre::Result<()> {
log::info!("Starting polling-based local game directory monitor"); log::info!("Starting polling-based local game directory monitor");
let mut snapshot = initial_poll_snapshot(&ctx).await; let mut snapshot = initial_poll_snapshot(&ctx).await;
@@ -171,7 +168,7 @@ async fn initial_poll_snapshot(ctx: &Ctx) -> Option<PollSnapshot> {
async fn poll_local_game_changes( async fn poll_local_game_changes(
ctx: &Ctx, ctx: &Ctx,
tx_notify_ui: &UnboundedSender<PeerEvent>, tx_notify_ui: &PeerEventSender,
gate: &RescanGate, gate: &RescanGate,
rescans: &mut JoinSet<()>, rescans: &mut JoinSet<()>,
previous: &mut Option<PollSnapshot>, previous: &mut Option<PollSnapshot>,
@@ -328,7 +325,7 @@ fn changed_game_ids(previous: &PollSnapshot, current: &PollSnapshot) -> BTreeSet
async fn queue_changed_games( async fn queue_changed_games(
ctx: &Ctx, ctx: &Ctx,
tx_notify_ui: &UnboundedSender<PeerEvent>, tx_notify_ui: &PeerEventSender,
gate: &RescanGate, gate: &RescanGate,
rescans: &mut JoinSet<()>, rescans: &mut JoinSet<()>,
changed_ids: BTreeSet<String>, changed_ids: BTreeSet<String>,
@@ -354,7 +351,7 @@ async fn queue_changed_games(
async fn queue_rescan( async fn queue_rescan(
ctx: &Ctx, ctx: &Ctx,
tx_notify_ui: &UnboundedSender<PeerEvent>, tx_notify_ui: &PeerEventSender,
gate: &RescanGate, gate: &RescanGate,
rescans: &mut JoinSet<()>, rescans: &mut JoinSet<()>,
id: String, id: String,
@@ -376,12 +373,7 @@ async fn queue_rescan(
}); });
} }
async fn run_gated_rescan( async fn run_gated_rescan(ctx: Ctx, tx_notify_ui: PeerEventSender, gate: RescanGate, id: String) {
ctx: Ctx,
tx_notify_ui: UnboundedSender<PeerEvent>,
gate: RescanGate,
id: String,
) {
loop { loop {
gate.pending.write().await.remove(&id); gate.pending.write().await.remove(&id);
@@ -418,7 +410,7 @@ async fn run_gated_rescan(
gate.running.write().await.remove(&id); gate.running.write().await.remove(&id);
} }
async fn run_fallback_scan(ctx: &Ctx, tx_notify_ui: &UnboundedSender<PeerEvent>) { async fn run_fallback_scan(ctx: &Ctx, tx_notify_ui: &PeerEventSender) {
let _admission = ctx.operation_admission.lock().await; let _admission = ctx.operation_admission.lock().await;
let game_dir = ctx.game_dir.read().await.clone(); let game_dir = ctx.game_dir.read().await.clone();
let catalog = ctx.catalog.catalog(); let catalog = ctx.catalog.catalog();
@@ -445,11 +437,12 @@ mod tests {
}; };
use lanspread_db::content_manifest::CatalogBundle; use lanspread_db::content_manifest::CatalogBundle;
use tokio::sync::{RwLock, mpsc}; use tokio::sync::RwLock;
use tokio_util::{sync::CancellationToken, task::TaskTracker}; use tokio_util::{sync::CancellationToken, task::TaskTracker};
use super::*; use super::*;
use crate::{ use crate::{
PeerEvent,
UnpackFuture, UnpackFuture,
Unpacker, Unpacker,
context::OperationKind, context::OperationKind,
@@ -512,9 +505,7 @@ mod tests {
panic!("injected monitor loop panic"); panic!("injected monitor loop panic");
} }
async fn recv_local_update( async fn recv_local_update(rx: &mut crate::PeerEventReceiver) -> Vec<lanspread_db::db::Game> {
rx: &mut mpsc::UnboundedReceiver<PeerEvent>,
) -> Vec<lanspread_db::db::Game> {
let event = tokio::time::timeout(Duration::from_secs(1), rx.recv()) let event = tokio::time::timeout(Duration::from_secs(1), rx.recv())
.await .await
.expect("local update event should arrive") .expect("local update event should arrive")
@@ -604,7 +595,7 @@ mod tests {
.insert("game".to_string(), OperationKind::Downloading); .insert("game".to_string(), OperationKind::Downloading);
let rescan_gate = RescanGate::default(); let rescan_gate = RescanGate::default();
let mut rescans = JoinSet::new(); let mut rescans = JoinSet::new();
let (tx, mut rx) = mpsc::unbounded_channel(); let (tx, mut rx) = crate::peer_event_channel();
queue_changed_games( queue_changed_games(
&ctx, &ctx,
@@ -635,7 +626,7 @@ mod tests {
); );
let rescan_gate = RescanGate::default(); let rescan_gate = RescanGate::default();
let mut rescans = JoinSet::new(); let mut rescans = JoinSet::new();
let (tx, mut rx) = mpsc::unbounded_channel(); let (tx, mut rx) = crate::peer_event_channel();
let mut state = Some( let mut state = Some(
capture_poll_snapshot(&ctx) capture_poll_snapshot(&ctx)
.await .await
@@ -669,7 +660,7 @@ mod tests {
); );
let gate = RescanGate::default(); let gate = RescanGate::default();
let mut rescans = JoinSet::new(); let mut rescans = JoinSet::new();
let (tx, mut rx) = mpsc::unbounded_channel(); let (tx, mut rx) = crate::peer_event_channel();
let library_guard = ctx.local_library.write().await; let library_guard = ctx.local_library.write().await;
queue_rescan(&ctx, &tx, &gate, &mut rescans, "game".to_string()).await; queue_rescan(&ctx, &tx, &gate, &mut rescans, "game".to_string()).await;
@@ -707,7 +698,7 @@ mod tests {
); );
let gate = RescanGate::default(); let gate = RescanGate::default();
let mut rescans = JoinSet::new(); let mut rescans = JoinSet::new();
let (tx, mut rx) = mpsc::unbounded_channel(); let (tx, mut rx) = crate::peer_event_channel();
let admission = ctx.operation_admission.lock().await; let admission = ctx.operation_admission.lock().await;
queue_rescan(&ctx, &tx, &gate, &mut rescans, "game".to_string()).await; queue_rescan(&ctx, &tx, &gate, &mut rescans, "game".to_string()).await;
@@ -751,7 +742,7 @@ mod tests {
); );
let gate = RescanGate::default(); let gate = RescanGate::default();
let mut rescans = JoinSet::new(); let mut rescans = JoinSet::new();
let (tx, mut rx) = mpsc::unbounded_channel(); let (tx, mut rx) = crate::peer_event_channel();
let admission = ctx.operation_admission.lock().await; let admission = ctx.operation_admission.lock().await;
queue_rescan(&ctx, &tx, &gate, &mut rescans, "game".to_string()).await; queue_rescan(&ctx, &tx, &gate, &mut rescans, "game".to_string()).await;
@@ -792,7 +783,7 @@ mod tests {
let temp = TempDir::new("lanspread-local-monitor-structured-shutdown"); let temp = TempDir::new("lanspread-local-monitor-structured-shutdown");
let ctx = test_ctx(temp.path().to_path_buf(), empty_catalog_bundle()); let ctx = test_ctx(temp.path().to_path_buf(), empty_catalog_bundle());
let monitor_ctx = ctx.clone(); let monitor_ctx = ctx.clone();
let (tx, _rx) = mpsc::unbounded_channel(); let (tx, _rx) = crate::peer_event_channel();
let monitor = tokio::spawn(run_local_game_monitor(tx, monitor_ctx)); let monitor = tokio::spawn(run_local_game_monitor(tx, monitor_ctx));
tokio::task::yield_now().await; tokio::task::yield_now().await;
@@ -813,7 +804,7 @@ mod tests {
temp.path().to_path_buf(), temp.path().to_path_buf(),
catalog_bundle([("game", "20250101")]), catalog_bundle([("game", "20250101")]),
); );
let (tx, mut rx) = mpsc::unbounded_channel(); let (tx, mut rx) = crate::peer_event_channel();
run_fallback_scan(&ctx, &tx).await; run_fallback_scan(&ctx, &tx).await;
@@ -837,7 +828,7 @@ mod tests {
temp.path().to_path_buf(), temp.path().to_path_buf(),
catalog_bundle([("game", "20250101")]), catalog_bundle([("game", "20250101")]),
); );
let (tx, mut rx) = mpsc::unbounded_channel(); let (tx, mut rx) = crate::peer_event_channel();
run_fallback_scan(&ctx, &tx).await; run_fallback_scan(&ctx, &tx).await;
@@ -10,12 +10,13 @@ use lanspread_proto::{
PeerStateSnapshot, PeerStateSnapshot,
RuntimeSessionId, RuntimeSessionId,
}; };
use tokio::sync::{RwLock, mpsc::UnboundedSender}; use tokio::sync::RwLock;
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
use crate::{ use crate::{
CallToPlayView, CallToPlayView,
PeerEvent, PeerEvent,
PeerEventSender,
call_to_play::{ call_to_play::{
CallToPlayPublication, CallToPlayPublication,
CallToPlayStore, CallToPlayStore,
@@ -45,7 +46,7 @@ use crate::{
pub(crate) struct RemoteStateCtx { pub(crate) struct RemoteStateCtx {
local_peer_id: PeerId, local_peer_id: PeerId,
peer_game_db: Arc<RwLock<PeerGameDB>>, peer_game_db: Arc<RwLock<PeerGameDB>>,
tx_notify_ui: UnboundedSender<PeerEvent>, tx_notify_ui: PeerEventSender,
call_to_play: Arc<RwLock<CallToPlayStore>>, call_to_play: Arc<RwLock<CallToPlayStore>>,
quic: QuicConnector, quic: QuicConnector,
cancellation: CancellationToken, cancellation: CancellationToken,
@@ -56,7 +57,7 @@ impl RemoteStateCtx {
#[must_use] #[must_use]
pub(crate) fn from_network( pub(crate) fn from_network(
ctx: &NetworkServiceCtx, ctx: &NetworkServiceCtx,
tx_notify_ui: &UnboundedSender<PeerEvent>, tx_notify_ui: &PeerEventSender,
) -> Self { ) -> Self {
Self { Self {
local_peer_id: ctx.peer_id, local_peer_id: ctx.peer_id,
@@ -310,7 +311,7 @@ fn log_call_to_play_outcome(peer_id: PeerId, outcome: &ObserveRemoteAuthorOutcom
fn enqueue_commit_transition( fn enqueue_commit_transition(
db: &PeerGameDB, db: &PeerGameDB,
call_to_play: &mut CallToPlayStore, call_to_play: &mut CallToPlayStore,
tx_notify_ui: &UnboundedSender<PeerEvent>, tx_notify_ui: &PeerEventSender,
state_sync: &StateSyncHandle, state_sync: &StateSyncHandle,
publication: PreparedCallToPlayPublication, publication: PreparedCallToPlayPublication,
endpoint: PeerEndpoint, endpoint: PeerEndpoint,
@@ -351,7 +352,7 @@ fn enqueue_commit_transition(
fn enqueue_final_views( fn enqueue_final_views(
db: &PeerGameDB, db: &PeerGameDB,
call_to_play: &mut CallToPlayStore, call_to_play: &mut CallToPlayStore,
tx_notify_ui: &UnboundedSender<PeerEvent>, tx_notify_ui: &PeerEventSender,
state_sync: &StateSyncHandle, state_sync: &StateSyncHandle,
publication: PreparedCallToPlayPublication, publication: PreparedCallToPlayPublication,
) { ) {
@@ -368,11 +369,11 @@ fn enqueue_final_views(
} }
/// Clears every remote projection after one network generation has fully /// Clears every remote projection after one network generation has fully
/// drained, then queues the authoritative empty replacement views while the /// drained, then queues the authoritative replacement views while the
/// database-to-Call-to-Play lock order is still held. /// database-to-Call-to-Play lock order is still held.
pub(crate) async fn clear_remote_state_and_publish( pub(crate) async fn clear_remote_state_and_publish(
ctx: &Ctx, ctx: &Ctx,
tx_notify_ui: &UnboundedSender<PeerEvent>, tx_notify_ui: &PeerEventSender,
) { ) {
let mut db = ctx.peer_game_db.write().await; let mut db = ctx.peer_game_db.write().await;
let mut call_to_play = ctx.call_to_play.write().await; let mut call_to_play = ctx.call_to_play.write().await;
@@ -383,11 +384,14 @@ pub(crate) async fn clear_remote_state_and_publish(
events::send(tx_notify_ui, PeerEvent::PeerLost(endpoint)); events::send(tx_notify_ui, PeerEvent::PeerLost(endpoint));
} }
events::send(tx_notify_ui, PeerEvent::PeerCountUpdated(0)); events::send(tx_notify_ui, PeerEvent::PeerCountUpdated(0));
events::send( ctx.state_sync
tx_notify_ui, .publish_call_to_play_revision(publication.local_revision);
PeerEvent::RemoteLibraryView(events::remote_library_view(&db)), if let Err(error) = tx_notify_ui.publish_view_generation(
); events::remote_library_view(&db),
enqueue_call_to_play_publication(publication, tx_notify_ui, &ctx.state_sync, true); CallToPlayView::from(publication.view),
) {
log::error!("Failed to publish cleared peer view generation: {error}");
}
if let Some(error) = preparation_error { if let Some(error) = preparation_error {
log::warn!( log::warn!(
@@ -398,7 +402,7 @@ pub(crate) async fn clear_remote_state_and_publish(
fn enqueue_local_prune_if_changed( fn enqueue_local_prune_if_changed(
call_to_play: &mut CallToPlayStore, call_to_play: &mut CallToPlayStore,
tx_notify_ui: &UnboundedSender<PeerEvent>, tx_notify_ui: &PeerEventSender,
state_sync: &StateSyncHandle, state_sync: &StateSyncHandle,
publication: PreparedCallToPlayPublication, publication: PreparedCallToPlayPublication,
) { ) {
@@ -411,7 +415,7 @@ fn enqueue_local_prune_if_changed(
fn enqueue_call_to_play_publication( fn enqueue_call_to_play_publication(
publication: CallToPlayPublication, publication: CallToPlayPublication,
tx_notify_ui: &UnboundedSender<PeerEvent>, tx_notify_ui: &PeerEventSender,
state_sync: &StateSyncHandle, state_sync: &StateSyncHandle,
force_view: bool, force_view: bool,
) { ) {
@@ -8,7 +8,7 @@ use tokio::sync::{Mutex, mpsc, watch};
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
use crate::{ use crate::{
PeerEvent, PeerEventSender,
context::NetworkServiceCtx, context::NetworkServiceCtx,
network::{send_call_to_play_changed, send_library_changed}, network::{send_call_to_play_changed, send_library_changed},
peer_db::PeerRevisionSnapshot, peer_db::PeerRevisionSnapshot,
@@ -170,7 +170,7 @@ type FanoutFuture = Pin<Box<dyn Future<Output = ()> + Send>>;
/// Runs the bounded pull scheduler and local hint fanout in one lexical scope. /// Runs the bounded pull scheduler and local hint fanout in one lexical scope.
pub(crate) async fn run_state_sync( pub(crate) async fn run_state_sync(
ctx: NetworkServiceCtx, ctx: NetworkServiceCtx,
tx_notify_ui: tokio::sync::mpsc::UnboundedSender<PeerEvent>, tx_notify_ui: PeerEventSender,
cancellation: CancellationToken, cancellation: CancellationToken,
) -> eyre::Result<()> { ) -> eyre::Result<()> {
let mut pinned_rx = ctx.state_sync.inbox.pinned_rx.lock().await; let mut pinned_rx = ctx.state_sync.inbox.pinned_rx.lock().await;
@@ -504,7 +504,7 @@ mod tests {
CatalogContentManifestBody, CatalogContentManifestBody,
CatalogExtractedEntry, CatalogExtractedEntry,
}; };
use tokio::sync::{RwLock, mpsc}; use tokio::sync::RwLock;
use tokio_util::task::TaskTracker; use tokio_util::task::TaskTracker;
use super::*; use super::*;
@@ -611,7 +611,7 @@ mod tests {
root: &Path, root: &Path,
manifest: &CatalogContentManifest, manifest: &CatalogContentManifest,
provider: Arc<CountingStreamInstallProvider>, provider: Arc<CountingStreamInstallProvider>,
) -> (PeerCtx, mpsc::UnboundedReceiver<crate::PeerEvent>) { ) -> (PeerCtx, crate::PeerEventReceiver) {
let catalog = Arc::new( let catalog = Arc::new(
CatalogBundle::from_manifests([manifest.clone()]) CatalogBundle::from_manifests([manifest.clone()])
.expect("test catalog should be complete"), .expect("test catalog should be complete"),
@@ -643,7 +643,7 @@ mod tests {
availability: Availability::Ready, availability: Availability::Ready,
}, },
); );
let (tx, rx) = mpsc::unbounded_channel(); let (tx, rx) = crate::peer_event_channel();
(ctx.to_peer_ctx(tx, CancellationToken::new()), rx) (ctx.to_peer_ctx(tx, CancellationToken::new()), rx)
} }
+12 -9
View File
@@ -23,9 +23,9 @@ use tokio_util::{sync::CancellationToken, task::TaskTracker};
use crate::{ use crate::{
PeerCommand, PeerCommand,
PeerEvent,
PeerId, PeerId,
PeerIdentityDurability, PeerIdentityDurability,
PeerEventSender,
PeerRuntimeComponent, PeerRuntimeComponent,
StreamInstallProvider, StreamInstallProvider,
Unpacker, Unpacker,
@@ -284,7 +284,7 @@ pub(crate) enum SupervisionPolicy {
pub(crate) fn spawn_peer_runtime( pub(crate) fn spawn_peer_runtime(
tx_control: UnboundedSender<PeerCommand>, tx_control: UnboundedSender<PeerCommand>,
rx_control: UnboundedReceiver<PeerCommand>, rx_control: UnboundedReceiver<PeerCommand>,
tx_notify_ui: UnboundedSender<PeerEvent>, tx_notify_ui: PeerEventSender,
peer_game_db: Arc<RwLock<PeerGameDB>>, peer_game_db: Arc<RwLock<PeerGameDB>>,
peer_identity: Arc<PeerIdentity>, peer_identity: Arc<PeerIdentity>,
identity_durability: PeerIdentityDurability, identity_durability: PeerIdentityDurability,
@@ -380,11 +380,11 @@ async fn run_runtime_root<Root, Cleanup>(
} }
} }
pub(crate) fn spawn_core_services(ctx: &Ctx, tx_notify_ui: &UnboundedSender<PeerEvent>) { pub(crate) fn spawn_core_services(ctx: &Ctx, tx_notify_ui: &PeerEventSender) {
spawn_local_library_monitor(ctx, tx_notify_ui); spawn_local_library_monitor(ctx, tx_notify_ui);
} }
fn spawn_local_library_monitor(ctx: &Ctx, tx_notify_ui: &UnboundedSender<PeerEvent>) { fn spawn_local_library_monitor(ctx: &Ctx, tx_notify_ui: &PeerEventSender) {
let ctx = ctx.clone(); let ctx = ctx.clone();
let tx_notify_ui = tx_notify_ui.clone(); let tx_notify_ui = tx_notify_ui.clone();
let task_tracker = ctx.task_tracker.clone(); let task_tracker = ctx.task_tracker.clone();
@@ -408,7 +408,7 @@ fn spawn_local_library_monitor(ctx: &Ctx, tx_notify_ui: &UnboundedSender<PeerEve
fn spawn_supervised_service<F, Fut>( fn spawn_supervised_service<F, Fut>(
task_tracker: &TaskTracker, task_tracker: &TaskTracker,
shutdown: &CancellationToken, shutdown: &CancellationToken,
tx_notify_ui: &UnboundedSender<PeerEvent>, tx_notify_ui: &PeerEventSender,
component: PeerRuntimeComponent, component: PeerRuntimeComponent,
policy: SupervisionPolicy, policy: SupervisionPolicy,
mut make_service: F, mut make_service: F,
@@ -505,13 +505,16 @@ fn spawn_supervised_service<F, Fut>(
#[cfg(test)] #[cfg(test)]
fn report_required_service_failure( fn report_required_service_failure(
tx_notify_ui: &UnboundedSender<PeerEvent>, tx_notify_ui: &PeerEventSender,
component: PeerRuntimeComponent, component: PeerRuntimeComponent,
error: String, error: String,
shutdown: &CancellationToken, shutdown: &CancellationToken,
) { ) {
log::error!("{component:?} failed: {error}"); log::error!("{component:?} failed: {error}");
crate::events::send(tx_notify_ui, PeerEvent::RuntimeFailed { component, error }); crate::events::send(
tx_notify_ui,
crate::PeerEvent::RuntimeFailed { component, error },
);
shutdown.cancel(); shutdown.cancel();
} }
@@ -611,7 +614,7 @@ mod tests {
async fn required_service_failure_cancels_runtime_and_emits_event() { async fn required_service_failure_cancels_runtime_and_emits_event() {
let tracker = TaskTracker::new(); let tracker = TaskTracker::new();
let shutdown = CancellationToken::new(); let shutdown = CancellationToken::new();
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel(); let (tx, mut rx) = crate::peer_event_channel();
spawn_supervised_service( spawn_supervised_service(
&tracker, &tracker,
@@ -646,7 +649,7 @@ mod tests {
async fn restart_service_restarts_until_shutdown() { async fn restart_service_restarts_until_shutdown() {
let tracker = TaskTracker::new(); let tracker = TaskTracker::new();
let shutdown = CancellationToken::new(); let shutdown = CancellationToken::new();
let (tx, _rx) = tokio::sync::mpsc::unbounded_channel(); let (tx, _rx) = crate::peer_event_channel();
let attempts = Arc::new(AtomicUsize::new(0)); let attempts = Arc::new(AtomicUsize::new(0));
spawn_supervised_service( spawn_supervised_service(
+7 -6
View File
@@ -36,7 +36,7 @@ use s2n_quic::{
use tokio::{ use tokio::{
io::{AsyncRead, AsyncReadExt}, io::{AsyncRead, AsyncReadExt},
process::Command, process::Command,
sync::{mpsc, mpsc::UnboundedSender}, sync::mpsc,
time::{self, Instant as TokioInstant, MissedTickBehavior}, time::{self, Instant as TokioInstant, MissedTickBehavior},
}; };
use tokio_util::{ use tokio_util::{
@@ -47,6 +47,7 @@ use tokio_util::{
use crate::{ use crate::{
DownloadProgress, DownloadProgress,
PeerEvent, PeerEvent,
PeerEventSender,
install::root_eti_archives, install::root_eti_archives,
network::connect_to_peer, network::connect_to_peer,
path_validation::validate_game_file_path, path_validation::validate_game_file_path,
@@ -1274,7 +1275,7 @@ impl StreamInstallReceiveState {
game_id: &str, game_id: &str,
peer_endpoint: PeerEndpoint, peer_endpoint: PeerEndpoint,
content_id: ContentId, content_id: ContentId,
tx_notify_ui: &UnboundedSender<PeerEvent>, tx_notify_ui: &PeerEventSender,
) -> StreamInstallReceiveResult<ReceiveFrameOutcome> { ) -> StreamInstallReceiveResult<ReceiveFrameOutcome> {
match frame { match frame {
StreamInstallFrame::ArchiveBegin { StreamInstallFrame::ArchiveBegin {
@@ -1381,7 +1382,7 @@ pub(crate) struct ReceiveStreamedInstallRequest<'a> {
pub(crate) manifest: Arc<CatalogContentManifest>, pub(crate) manifest: Arc<CatalogContentManifest>,
pub(crate) staging_dir: &'a Path, pub(crate) staging_dir: &'a Path,
pub(crate) attempt: DownloadAttemptReporter, pub(crate) attempt: DownloadAttemptReporter,
pub(crate) tx_notify_ui: UnboundedSender<PeerEvent>, pub(crate) tx_notify_ui: PeerEventSender,
pub(crate) quic: &'a QuicConnector, pub(crate) quic: &'a QuicConnector,
pub(crate) cancel_token: CancellationToken, pub(crate) cancel_token: CancellationToken,
} }
@@ -1854,7 +1855,7 @@ impl<W: std::io::Write> IncomingFile<W> {
game_id: &str, game_id: &str,
peer_endpoint: PeerEndpoint, peer_endpoint: PeerEndpoint,
content_id: ContentId, content_id: ContentId,
tx_notify_ui: &UnboundedSender<PeerEvent>, tx_notify_ui: &PeerEventSender,
) -> StreamInstallReceiveResult<()> { ) -> StreamInstallReceiveResult<()> {
if &self.relative_path != relative_path { if &self.relative_path != relative_path {
return Err(StreamInstallReceiveError::integrity(eyre::eyre!( return Err(StreamInstallReceiveError::integrity(eyre::eyre!(
@@ -3051,7 +3052,7 @@ mod tests {
.begin_archive(&canonical_path("a.eti"), 1) .begin_archive(&canonical_path("a.eti"), 1)
.expect("sender telemetry need not equal the catalog-owned total"); .expect("sender telemetry need not equal the catalog-owned total");
let (tx, _rx) = tokio::sync::mpsc::unbounded_channel(); let (tx, _rx) = crate::peer_event_channel();
let status = crate::transfer_status::DownloadAttemptStatus::new( let status = crate::transfer_status::DownloadAttemptStatus::new(
crate::DownloadAttemptKey::next("game".to_owned()), crate::DownloadAttemptKey::next("game".to_owned()),
CancellationToken::new(), CancellationToken::new(),
@@ -3146,7 +3147,7 @@ mod tests {
let payload = b"payload"; let payload = b"payload";
let endpoint = peer_endpoint(); let endpoint = peer_endpoint();
let exact_content_id = content_id(); let exact_content_id = content_id();
let (event_tx, mut event_rx) = tokio::sync::mpsc::unbounded_channel(); let (event_tx, mut event_rx) = crate::peer_event_channel();
let mut accepted = IncomingFile::new( let mut accepted = IncomingFile::new(
canonical_path("accepted.bin"), canonical_path("accepted.bin"),
PathBuf::from("accepted.bin"), PathBuf::from("accepted.bin"),
+5 -5
View File
@@ -10,7 +10,7 @@ use std::{
}; };
use serde::{Deserialize, Deserializer, Serialize, Serializer, de}; use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
use tokio::sync::mpsc::UnboundedSender; use crate::PeerEventSender;
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
use crate::{DownloadProgress, PeerEvent, events}; use crate::{DownloadProgress, PeerEvent, events};
@@ -140,7 +140,7 @@ struct AttemptRuntimeState {
struct DownloadAttemptState { struct DownloadAttemptState {
key: DownloadAttemptKey, key: DownloadAttemptKey,
cancellation: CancellationToken, cancellation: CancellationToken,
tx_notify_ui: UnboundedSender<PeerEvent>, tx_notify_ui: PeerEventSender,
runtime: Mutex<AttemptRuntimeState>, runtime: Mutex<AttemptRuntimeState>,
} }
@@ -153,7 +153,7 @@ impl DownloadAttemptStatus {
pub(crate) fn new( pub(crate) fn new(
key: DownloadAttemptKey, key: DownloadAttemptKey,
cancellation: CancellationToken, cancellation: CancellationToken,
tx_notify_ui: UnboundedSender<PeerEvent>, tx_notify_ui: PeerEventSender,
) -> Self { ) -> Self {
Self { Self {
state: Arc::new(DownloadAttemptState { state: Arc::new(DownloadAttemptState {
@@ -459,9 +459,9 @@ mod tests {
fn attempt() -> ( fn attempt() -> (
DownloadAttemptStatus, DownloadAttemptStatus,
tokio::sync::mpsc::UnboundedReceiver<PeerEvent>, crate::PeerEventReceiver,
) { ) {
let (tx, rx) = tokio::sync::mpsc::unbounded_channel(); let (tx, rx) = crate::peer_event_channel();
( (
DownloadAttemptStatus::new( DownloadAttemptStatus::new(
DownloadAttemptKey::next("game".to_owned()), DownloadAttemptKey::next("game".to_owned()),
@@ -35,6 +35,8 @@ use lanspread_peer::{
NoopStreamInstallProvider, NoopStreamInstallProvider,
PeerCommand, PeerCommand,
PeerEvent, PeerEvent,
PeerEventReceiver,
PeerEventSender,
PeerGameDB, PeerGameDB,
PeerIdentity, PeerIdentity,
PeerIdentityDurability, PeerIdentityDurability,
@@ -47,6 +49,7 @@ use lanspread_peer::{
UnpackFuture, UnpackFuture,
Unpacker, Unpacker,
migrate_legacy_state, migrate_legacy_state,
peer_event_channel,
scoped_blocking, scoped_blocking,
start_peer_with_options, start_peer_with_options,
}; };
@@ -687,7 +690,7 @@ struct InstallSettings {
language: String, language: String,
} }
struct PeerEventTx(UnboundedSender<PeerEvent>); struct PeerEventTx(PeerEventSender);
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)] #[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)]
enum UiOperationKind { enum UiOperationKind {
@@ -4086,7 +4089,7 @@ fn emit_game_id_event(app_handle: &AppHandle, event: &str, id: &str, label: &str
fn spawn_peer_event_loop( fn spawn_peer_event_loop(
app_handle: AppHandle, app_handle: AppHandle,
mut rx_peer_event: UnboundedReceiver<PeerEvent>, mut rx_peer_event: PeerEventReceiver,
mut rx_ui_state: UnboundedReceiver<UiStateCommand>, mut rx_ui_state: UnboundedReceiver<UiStateCommand>,
) { ) {
let tasks = app_handle let tasks = app_handle
@@ -4228,6 +4231,7 @@ fn set_identity_diagnostic_in_loop(
Ok(next) Ok(next)
} }
#[cfg(test)]
fn take_exactly_queued<T>( fn take_exactly_queued<T>(
receiver: &mut UnboundedReceiver<T>, receiver: &mut UnboundedReceiver<T>,
count: usize, count: usize,
@@ -4243,15 +4247,17 @@ fn take_exactly_queued<T>(
async fn drain_queued_peer_events( async fn drain_queued_peer_events(
app_handle: &AppHandle, app_handle: &AppHandle,
receiver: &mut UnboundedReceiver<PeerEvent>, receiver: &mut PeerEventReceiver,
) -> Result<(), String> { ) -> Result<(), String> {
// The core replies only after all events for the requested transition have // The core replies only after all events for the requested transition have
// been enqueued. Once this UI command wins the fair select, snapshot the // been enqueued. Once this UI command wins the fair select, hold the shared
// peer queue and process that exact FIFO prefix. Later autonomous traffic // producer fence while draining every event already admitted, including a
// remains for the normal event-loop turn and cannot contaminate this // coalesced pending view. Later autonomous traffic remains for the normal
// transition's acknowledgement boundary. // event-loop turn and cannot contaminate this transition's acknowledgement
let count = receiver.len(); // boundary.
let queued = take_exactly_queued(receiver, count)?; let queued = receiver
.drain_ready()
.map_err(|error| format!("peer-event queue changed while applying its fence: {error}"))?;
for event in queued { for event in queued {
handle_peer_event(app_handle, event).await; handle_peer_event(app_handle, event).await;
} }
@@ -4260,7 +4266,7 @@ async fn drain_queued_peer_events(
async fn fence_queued_peer_events( async fn fence_queued_peer_events(
app_handle: &AppHandle, app_handle: &AppHandle,
receiver: &mut UnboundedReceiver<PeerEvent>, receiver: &mut PeerEventReceiver,
) -> Result<LocalNetworkSharingSnapshot, String> { ) -> Result<LocalNetworkSharingSnapshot, String> {
drain_queued_peer_events(app_handle, receiver).await?; drain_queued_peer_events(app_handle, receiver).await?;
current_local_network_sharing(app_handle.state::<LanSpreadState>().inner()) current_local_network_sharing(app_handle.state::<LanSpreadState>().inner())
@@ -4268,7 +4274,7 @@ async fn fence_queued_peer_events(
async fn reset_game_transfer_status_in_loop( async fn reset_game_transfer_status_in_loop(
app_handle: &AppHandle, app_handle: &AppHandle,
receiver: &mut UnboundedReceiver<PeerEvent>, receiver: &mut PeerEventReceiver,
) -> Result<GameTransferStatusSnapshot, String> { ) -> Result<GameTransferStatusSnapshot, String> {
// A successful game-root command has already caused core to enqueue its // A successful game-root command has already caused core to enqueue its
// preceding events. Drain that bounded prefix before terminalizing the old // preceding events. Drain that bounded prefix before terminalizing the old
@@ -4290,7 +4296,7 @@ async fn reset_game_transfer_status_in_loop(
async fn handle_ui_state_command( async fn handle_ui_state_command(
app_handle: &AppHandle, app_handle: &AppHandle,
command: UiStateCommand, command: UiStateCommand,
peer_events: &mut UnboundedReceiver<PeerEvent>, peer_events: &mut PeerEventReceiver,
) { ) {
match command { match command {
UiStateCommand::MutateSharing { mutation, reply } => { UiStateCommand::MutateSharing { mutation, reply } => {
@@ -4629,7 +4635,7 @@ struct ProtocolMismatchSnapshot {
#[cfg_attr(mobile, tauri::mobile_entry_point)] #[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() { pub fn run() {
// channel to receive events from the peer // channel to receive events from the peer
let (tx_peer_event, rx_peer_event) = tokio::sync::mpsc::unbounded_channel::<PeerEvent>(); let (tx_peer_event, rx_peer_event) = peer_event_channel();
let (tx_ui_state, rx_ui_state) = tokio::sync::mpsc::unbounded_channel::<UiStateCommand>(); let (tx_ui_state, rx_ui_state) = tokio::sync::mpsc::unbounded_channel::<UiStateCommand>();
tauri::Builder::default() tauri::Builder::default()