feat(call-to-play)!: coordinate game sessions across peers
Implement the launcher design as a production peer-to-peer feature. Call to Play actions are immutable, validated events broadcast over the existing QUIC control channel, deduplicated in a bounded in-memory history, and exchanged in Hello/HelloAck so late joiners reconstruct current calls. Add the Tauri bridge and modular launcher surfaces for play-now and scheduled calls, check-in, readiness buffers, role-aware controls, chat, tickers, and actual caller launch. A deterministic frontend reducer derives presentation state from replicated history. Extend the JSONL peer harness with publish/list commands and a three-peer live-delivery and late-join scenario. This intentionally raises the only supported wire protocol from version 5 to version 6; older builds are not supported. Document the transport architecture and exclude generated peer-test state from Docker build contexts. Test Plan: - `just fmt` -- passed - `just clippy` -- passed - `just test` -- passed - `just frontend-test` -- passed, 20 tests - `just build` -- passed - `just peer-cli-tests S2 S48` -- passed - `git diff --cached --check` -- passed
This commit is contained in:
@@ -16,6 +16,7 @@ use lanspread_db::db::{Game, GameCatalog, GameFileDescription};
|
||||
use lanspread_peer::{
|
||||
ActiveOperation,
|
||||
ActiveOperationKind,
|
||||
CallToPlayEvent,
|
||||
ExternalUnrarStreamProvider,
|
||||
NoopStreamInstallProvider,
|
||||
OutboundTransfers,
|
||||
@@ -101,6 +102,8 @@ struct CliState {
|
||||
game_files: HashMap<String, Vec<GameFileDescription>>,
|
||||
unavailable_games: HashSet<String>,
|
||||
downloads: HashMap<String, DownloadMeasurement>,
|
||||
call_to_play_events: Vec<CallToPlayEvent>,
|
||||
call_to_play_generation: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, serde::Serialize)]
|
||||
@@ -243,6 +246,26 @@ async fn handle_command(
|
||||
CliCommand::Status => status(shared).await,
|
||||
CliCommand::ListPeers => list_peers(shared).await,
|
||||
CliCommand::ListGames => list_games(shared).await,
|
||||
CliCommand::ListCallToPlay => {
|
||||
let generation = shared.state.read().await.call_to_play_generation;
|
||||
sender.send(PeerCommand::GetCallToPlayEvents)?;
|
||||
tokio::time::timeout(Duration::from_secs(1), async {
|
||||
loop {
|
||||
if shared.state.read().await.call_to_play_generation > generation {
|
||||
break;
|
||||
}
|
||||
shared.notify.notified().await;
|
||||
}
|
||||
})
|
||||
.await
|
||||
.wrap_err("timed out waiting for Call to Play history")?;
|
||||
let events = shared.state.read().await.call_to_play_events.clone();
|
||||
Ok(json!({ "events": events }))
|
||||
}
|
||||
CliCommand::PublishCallToPlay { event } => {
|
||||
sender.send(PeerCommand::PublishCallToPlay(event.clone()))?;
|
||||
Ok(json!({"queued": true, "event_id": event.id}))
|
||||
}
|
||||
CliCommand::SetGameDir { path } => {
|
||||
sender.send(PeerCommand::SetGameDir(path.clone()))?;
|
||||
Ok(json!({"queued": true, "path": path}))
|
||||
@@ -498,6 +521,22 @@ async fn update_state_from_event(shared: &SharedState, event: PeerEvent) -> (&'s
|
||||
json!({ "active_operations": active_operations_json(&active_operations) }),
|
||||
)
|
||||
}
|
||||
PeerEvent::CallToPlayEvents(events) => {
|
||||
let mut state = shared.state.write().await;
|
||||
let mut known = state
|
||||
.call_to_play_events
|
||||
.iter()
|
||||
.map(|event| event.id.clone())
|
||||
.collect::<HashSet<_>>();
|
||||
state.call_to_play_events.extend(
|
||||
events
|
||||
.iter()
|
||||
.filter(|event| known.insert(event.id.clone()))
|
||||
.cloned(),
|
||||
);
|
||||
state.call_to_play_generation = state.call_to_play_generation.saturating_add(1);
|
||||
("call-to-play-events", json!({ "events": events }))
|
||||
}
|
||||
PeerEvent::GotGameFiles {
|
||||
id,
|
||||
file_descriptions,
|
||||
|
||||
Reference in New Issue
Block a user