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:
@@ -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
|
currently known peer. An incoming live event is applied once and sent to the UI
|
||||||
without being rebroadcast, which prevents forwarding loops.
|
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
|
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
|
origin peer overwrites the actor ID on local actions, and live-event envelopes
|
||||||
must match the known sending peer. This prevents duplicate default usernames
|
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,
|
join after a call was created reconstruct the same nominations, responses,
|
||||||
RSVPs, chat, and terminal actions. The launcher reducer sorts the event stream
|
RSVPs, chat, and terminal actions. The launcher reducer sorts the event stream
|
||||||
deterministically and derives deadlines and check-in phases from timestamps.
|
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.
|
There is deliberately no compatibility path for older protocol versions.
|
||||||
|
|
||||||
### 4) Shutdown
|
### 4) Shutdown
|
||||||
|
|||||||
@@ -8,7 +8,13 @@ use std::{
|
|||||||
use lanspread_proto::{CallToPlayAction, CallToPlayEvent};
|
use lanspread_proto::{CallToPlayAction, CallToPlayEvent};
|
||||||
use tokio::sync::mpsc::UnboundedSender;
|
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_EVENTS: usize = 4_096;
|
||||||
const MAX_ID_CHARS: usize = 128;
|
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_addresses = ctx.peer_game_db.read().await.get_peer_addresses();
|
||||||
let peer_id = ctx.peer_id.clone();
|
let peer_id = ctx.peer_id.clone();
|
||||||
|
let handshake_ctx = HandshakeCtx::from_ctx(ctx, tx_notify_ui);
|
||||||
ctx.task_tracker.spawn(async move {
|
ctx.task_tracker.spawn(async move {
|
||||||
let deliveries = peer_addresses.into_iter().map(|peer_addr| {
|
let deliveries = peer_addresses.into_iter().map(|peer_addr| {
|
||||||
let event = event.clone();
|
let event = event.clone();
|
||||||
let peer_id = peer_id.clone();
|
let peer_id = peer_id.clone();
|
||||||
|
let handshake_ctx = handshake_ctx.clone();
|
||||||
async move {
|
async move {
|
||||||
if let Err(err) =
|
if let Err(err) =
|
||||||
send_call_to_play_events(peer_addr, peer_id.as_ref(), vec![event]).await
|
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}");
|
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}"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user