fix(peer): coalesce full UI view publications

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

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

No files matched your search

@@ -35,6 +35,8 @@ use lanspread_peer::{
NoopStreamInstallProvider,
PeerCommand,
PeerEvent,
PeerEventReceiver,
PeerEventSender,
PeerGameDB,
PeerIdentity,
PeerIdentityDurability,
@@ -47,6 +49,7 @@ use lanspread_peer::{
UnpackFuture,
Unpacker,
migrate_legacy_state,
peer_event_channel,
scoped_blocking,
start_peer_with_options,
};
@@ -687,7 +690,7 @@ struct InstallSettings {
language: String,
}
struct PeerEventTx(UnboundedSender<PeerEvent>);
struct PeerEventTx(PeerEventSender);
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)]
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(
app_handle: AppHandle,
mut rx_peer_event: UnboundedReceiver<PeerEvent>,
mut rx_peer_event: PeerEventReceiver,
mut rx_ui_state: UnboundedReceiver<UiStateCommand>,
) {
let tasks = app_handle
@@ -4228,6 +4231,7 @@ fn set_identity_diagnostic_in_loop(
Ok(next)
}
#[cfg(test)]
fn take_exactly_queued<T>(
receiver: &mut UnboundedReceiver<T>,
count: usize,
@@ -4243,15 +4247,17 @@ fn take_exactly_queued<T>(
async fn drain_queued_peer_events(
app_handle: &AppHandle,
receiver: &mut UnboundedReceiver<PeerEvent>,
receiver: &mut PeerEventReceiver,
) -> Result<(), String> {
// The core replies only after all events for the requested transition have
// been enqueued. Once this UI command wins the fair select, snapshot the
// peer queue and process that exact FIFO prefix. Later autonomous traffic
// remains for the normal event-loop turn and cannot contaminate this
// transition's acknowledgement boundary.
let count = receiver.len();
let queued = take_exactly_queued(receiver, count)?;
// been enqueued. Once this UI command wins the fair select, hold the shared
// producer fence while draining every event already admitted, including a
// coalesced pending view. Later autonomous traffic remains for the normal
// event-loop turn and cannot contaminate this transition's acknowledgement
// boundary.
let queued = receiver
.drain_ready()
.map_err(|error| format!("peer-event queue changed while applying its fence: {error}"))?;
for event in queued {
handle_peer_event(app_handle, event).await;
}
@@ -4260,7 +4266,7 @@ async fn drain_queued_peer_events(
async fn fence_queued_peer_events(
app_handle: &AppHandle,
receiver: &mut UnboundedReceiver<PeerEvent>,
receiver: &mut PeerEventReceiver,
) -> Result<LocalNetworkSharingSnapshot, String> {
drain_queued_peer_events(app_handle, receiver).await?;
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(
app_handle: &AppHandle,
receiver: &mut UnboundedReceiver<PeerEvent>,
receiver: &mut PeerEventReceiver,
) -> Result<GameTransferStatusSnapshot, String> {
// A successful game-root command has already caused core to enqueue its
// 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(
app_handle: &AppHandle,
command: UiStateCommand,
peer_events: &mut UnboundedReceiver<PeerEvent>,
peer_events: &mut PeerEventReceiver,
) {
match command {
UiStateCommand::MutateSharing { mutation, reply } => {
@@ -4629,7 +4635,7 @@ struct ProtocolMismatchSnapshot {
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
// 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>();
tauri::Builder::default()