fix(call-to-play): resync after live delivery failure

A failed fire-and-forget event left one peer's active call state divergent until
some later discovery or reconnect happened. During a LAN party that could leave
different players looking at different rosters or chat.

Fall back to the existing bidirectional handshake whenever a live Call to Play
send fails. Its active-history exchange heals both sides immediately when the
failure was transient. Document the related wall-clock synchronization
assumption for deadline and countdown presentation.

Test Plan:
- `just fmt` -- passed
- `just clippy` -- passed
- `just test` -- passed, 147 peer tests
- `just peer-cli-tests S48` -- passed
- `git diff --cached --check` -- passed
This commit is contained in:
2026-07-21 22:58:19 +02:00
parent cc7dacf6c3
commit 383e8f4855
2 changed files with 25 additions and 1 deletions
+9
View File
@@ -63,6 +63,11 @@ local action is applied to that history, sent to the UI, and broadcast to every
currently known peer. An incoming live event is applied once and sent to the UI
without being rebroadcast, which prevents forwarding loops.
If a live Call to Play delivery fails, the sender immediately falls back to a
normal `Hello` / `HelloAck` exchange with that peer. The handshake carries the
full active history in both directions, so a transient request failure heals
without waiting for mDNS rediscovery or a later reconnect.
Actors are keyed by the peer's stable ID and carry a separate display name. The
origin peer overwrites the actor ID on local actions, and live-event envelopes
must match the known sending peer. This prevents duplicate default usernames
@@ -74,6 +79,10 @@ uses the project's trusted-LAN identity model.
join after a call was created reconstruct the same nominations, responses,
RSVPs, chat, and terminal actions. The launcher reducer sorts the event stream
deterministically and derives deadlines and check-in phases from timestamps.
Those phases compare creator-supplied wall-clock timestamps with each viewer's
local wall clock, so LAN machines are assumed to be synchronized closely enough
for human-scale minute countdowns; clock skew shifts the displayed boundary by
the same amount.
There is deliberately no compatibility path for older protocol versions.
### 4) Shutdown
+16 -1
View File
@@ -8,7 +8,13 @@ use std::{
use lanspread_proto::{CallToPlayAction, CallToPlayEvent};
use tokio::sync::mpsc::UnboundedSender;
use crate::{PeerEvent, context::Ctx, events, network::send_call_to_play_events};
use crate::{
PeerEvent,
context::Ctx,
events,
network::send_call_to_play_events,
services::{HandshakeCtx, perform_handshake_with_peer},
};
const MAX_EVENTS: usize = 4_096;
const MAX_ID_CHARS: usize = 128;
@@ -173,15 +179,24 @@ pub(crate) async fn publish(
let peer_addresses = ctx.peer_game_db.read().await.get_peer_addresses();
let peer_id = ctx.peer_id.clone();
let handshake_ctx = HandshakeCtx::from_ctx(ctx, tx_notify_ui);
ctx.task_tracker.spawn(async move {
let deliveries = peer_addresses.into_iter().map(|peer_addr| {
let event = event.clone();
let peer_id = peer_id.clone();
let handshake_ctx = handshake_ctx.clone();
async move {
if let Err(err) =
send_call_to_play_events(peer_addr, peer_id.as_ref(), vec![event]).await
{
log::warn!("Failed to send Call to Play event to {peer_addr}: {err}");
if let Err(resync_err) =
perform_handshake_with_peer(handshake_ctx, peer_addr, None).await
{
log::warn!(
"Failed to resync Call to Play history with {peer_addr}: {resync_err}"
);
}
}
}
});