feat(peer)!: cut over to authenticated catalog sharing
Replace address-only trust and pushed peer state with installation identities, SPKI-pinned QUIC, candidate-only discovery, and bounded responder-owned protocol-8 pulls. The runtime now owns each network generation and all admitted work through shutdown. Add exact bundled content identities, reproducible manifest publishing, capability-confined downloads, streaming BLAKE3 verification, quarantine and retry, and crash-recoverable download and install transactions. Ship generated fixture catalogs and fail closed when production manifests are absent. The Tauri backend exposes durable sharing policy, redacted identity state, and attempt-keyed transfer snapshots. Frontend consumption follows in the next commit. Repository-wide test certificates and protocol-7 paths are removed. BREAKING CHANGE: peers must use protocol 8 and exact catalog content artifacts; protocol-7 frames and shared-certificate identities are no longer accepted. Test Plan: - `just test` -- passed on the completed stack (708 workspace tests) - `just clippy` -- passed on the completed stack - `just build` -- passed with fixture catalogs on the completed stack - `just catalog-check-production` -- failed closed because the external production manifest corpus is absent - `git diff --cached --check` -- passed
This commit is contained in:
128 files changed
+51759
-10784
No files matched your search
@@ -1,14 +1,19 @@
|
||||
//! UI event helpers used by peer command and service code.
|
||||
|
||||
use std::{collections::HashMap, net::SocketAddr, sync::Arc};
|
||||
use std::{
|
||||
collections::{BTreeMap, HashMap},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use lanspread_db::db::GameCatalog;
|
||||
use lanspread_db::content_manifest::ContentId;
|
||||
use tokio::sync::{RwLock, mpsc::UnboundedSender};
|
||||
|
||||
use crate::{
|
||||
ActiveOperation,
|
||||
ActiveOperationKind,
|
||||
PeerEvent,
|
||||
RemoteGameAvailability,
|
||||
RemoteLibraryView,
|
||||
context::OperationKind,
|
||||
peer_db::PeerGameDB,
|
||||
};
|
||||
@@ -66,38 +71,85 @@ fn active_operation_kind(operation: OperationKind) -> ActiveOperationKind {
|
||||
|
||||
pub async fn emit_peer_game_list(
|
||||
peer_game_db: &Arc<RwLock<PeerGameDB>>,
|
||||
catalog: &Arc<RwLock<GameCatalog>>,
|
||||
tx_notify_ui: &UnboundedSender<PeerEvent>,
|
||||
) {
|
||||
let games = {
|
||||
let catalog = catalog.read().await;
|
||||
peer_game_db.read().await.get_catalog_games(&catalog)
|
||||
};
|
||||
send(tx_notify_ui, PeerEvent::ListGames(games));
|
||||
let db = peer_game_db.read().await;
|
||||
send_remote_library_view_locked(&db, tx_notify_ui);
|
||||
}
|
||||
|
||||
fn send_remote_library_view_locked(
|
||||
peer_game_db: &PeerGameDB,
|
||||
tx_notify_ui: &UnboundedSender<PeerEvent>,
|
||||
) {
|
||||
send(
|
||||
tx_notify_ui,
|
||||
PeerEvent::RemoteLibraryView(remote_library_view(peer_game_db)),
|
||||
);
|
||||
}
|
||||
|
||||
pub(crate) fn remote_library_view(peer_game_db: &PeerGameDB) -> RemoteLibraryView {
|
||||
let mut counts = BTreeMap::<(String, ContentId), u32>::new();
|
||||
for game in peer_game_db
|
||||
.peer_snapshots()
|
||||
.into_iter()
|
||||
.flat_map(|peer| peer.games)
|
||||
{
|
||||
let count = counts.entry((game.game_id, game.content_id)).or_default();
|
||||
*count = count.saturating_add(1);
|
||||
}
|
||||
RemoteLibraryView {
|
||||
games: counts
|
||||
.into_iter()
|
||||
.map(
|
||||
|((game_id, content_id), peer_count)| RemoteGameAvailability {
|
||||
game_id,
|
||||
content_id,
|
||||
peer_count,
|
||||
},
|
||||
)
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn emit_peer_count(
|
||||
peer_game_db: &Arc<RwLock<PeerGameDB>>,
|
||||
tx_notify_ui: &UnboundedSender<PeerEvent>,
|
||||
) {
|
||||
let peer_count = { peer_game_db.read().await.get_peer_addresses().len() };
|
||||
let db = peer_game_db.read().await;
|
||||
let peer_count = db.peer_endpoints().len();
|
||||
send(tx_notify_ui, PeerEvent::PeerCountUpdated(peer_count));
|
||||
}
|
||||
|
||||
pub async fn emit_peer_discovered(
|
||||
peer_game_db: &Arc<RwLock<PeerGameDB>>,
|
||||
tx_notify_ui: &UnboundedSender<PeerEvent>,
|
||||
peer_addr: SocketAddr,
|
||||
) {
|
||||
send(tx_notify_ui, PeerEvent::PeerDiscovered(peer_addr));
|
||||
emit_peer_count(peer_game_db, tx_notify_ui).await;
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
pub async fn emit_peer_lost(
|
||||
peer_game_db: &Arc<RwLock<PeerGameDB>>,
|
||||
tx_notify_ui: &UnboundedSender<PeerEvent>,
|
||||
peer_addr: SocketAddr,
|
||||
) {
|
||||
send(tx_notify_ui, PeerEvent::PeerLost(peer_addr));
|
||||
emit_peer_count(peer_game_db, tx_notify_ui).await;
|
||||
#[tokio::test]
|
||||
async fn remote_library_view_is_enqueued_before_a_waiting_writer_can_commit() {
|
||||
let peer_game_db = Arc::new(RwLock::new(PeerGameDB::new()));
|
||||
let guard = peer_game_db.read().await;
|
||||
let writer_db = Arc::clone(&peer_game_db);
|
||||
let writer = tokio::spawn(async move {
|
||||
let _guard = writer_db.write().await;
|
||||
});
|
||||
tokio::task::yield_now().await;
|
||||
assert!(!writer.is_finished(), "writer must wait for the read guard");
|
||||
|
||||
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
|
||||
send_remote_library_view_locked(&guard, &tx);
|
||||
assert!(matches!(
|
||||
rx.try_recv(),
|
||||
Ok(PeerEvent::RemoteLibraryView(RemoteLibraryView { games })) if games.is_empty()
|
||||
));
|
||||
assert!(
|
||||
!writer.is_finished(),
|
||||
"the view must be enqueued while the read guard still orders writers"
|
||||
);
|
||||
|
||||
drop(guard);
|
||||
tokio::time::timeout(std::time::Duration::from_secs(1), writer)
|
||||
.await
|
||||
.expect("writer should acquire after the view is enqueued")
|
||||
.expect("writer task should finish");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user