diff --git a/crates/lanspread-peer/ARCHITECTURE.md b/crates/lanspread-peer/ARCHITECTURE.md index 50ab449..274fa2a 100644 --- a/crates/lanspread-peer/ARCHITECTURE.md +++ b/crates/lanspread-peer/ARCHITECTURE.md @@ -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 diff --git a/crates/lanspread-peer/src/call_to_play.rs b/crates/lanspread-peer/src/call_to_play.rs index 8bf5dd2..bd9105f 100644 --- a/crates/lanspread-peer/src/call_to_play.rs +++ b/crates/lanspread-peer/src/call_to_play.rs @@ -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}" + ); + } } } });