Peer A failed to learn Peer B's games. The handshake only carried
library_rev/library_digest metadata, and the post-handshake sync path
compared those revisions against per-peer revision numbers that were
never advanced via this code path, so the games map for the remote
peer stayed empty and the UI never showed them.
The fix is to put the authoritative library data into the handshake
itself. Hello and HelloAck now carry a LibrarySnapshot directly, and
both perform_handshake_with_peer (outbound) and accept_inbound_hello
(inbound) apply that snapshot to the peer DB before emitting the UI
events. The initial peer-game-list event is now driven by the
handshake rather than by a follow-up LibrarySummary/LibrarySnapshot
roundtrip.
Bumps PROTOCOL_VERSION to 4 because the wire layout of Hello/HelloAck
changed. Per CLAUDE.md's protocol policy there is no compatibility
shim; older peers will fail the version check and be ignored.
Cleanups that fall out of the new design:
- The Hello / HelloAck library_rev and library_digest fields were
duplicated by the embedded LibrarySnapshot (which carries its own
library_rev, and whose digest is recomputed on apply). Collapsed
both messages to just `library: LibrarySnapshot` to remove the
foot-gun where the two could diverge.
- Request::LibrarySummary and Request::LibrarySnapshot are now dead
on the sender side and were removed along with their stream.rs
handlers and the LibrarySummary struct. LibraryDelta stays — it
is still sent from handlers.rs when the local library changes.
- record_remote_library previously called update_peer_library and
then apply_library_snapshot, which immediately overwrote the
rev/digest just written. Added update_peer_features and rewired
the call site so each peer-DB field is written exactly once.
update_peer_library is retained because discovery.rs still uses
it for the mDNS TXT-record path, where no snapshot is available.
- Removed the now-unused LibraryUpdate enum, select_library_update,
send_local_library_summary, send_local_library_update_if_needed,
LocalLibraryState::delta_since, build_library_summary,
send_library_summary, and send_library_snapshot.
Behavior change visible to users: when two peers come up on the LAN
they now see each other's full game lists immediately after the
handshake instead of waiting for a follow-up sync that, in the broken
case, never made the games visible at all.
Test Plan
- just clippy (clean for the touched crates)
- just test (workspace: all suites pass, including the two new
handshake tests: outbound_hello_carries_local_library_snapshot
and inbound_hello_applies_remote_library_snapshot, the latter
asserting PeerDiscovered + PeerCountUpdated + ListGames events
fire with the remote game visible)
- Manual: start `just peer-cli-alpha` and `just peer-cli-bravo` in
separate terminals; confirm each peer's game list shows the
other's library entries after discovery completes, without
requiring any additional command.
Refs
- FINDINGS.md: triage note that Claude's review surfaced only
in-scope cleanups (dead variants, duplicated header fields,
redundant DB writes, stale test fixture), all addressed here.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
lanspread-peer
lanspread-peer is the networking runtime that lets Lanspread nodes find each
other on the local network, exchange library metadata, and transfer game files.
It is designed to run headless – other crates (most notably
lanspread-tauri-deno-ts) embed it and drive it through a channel-based API.
Runtime Overview
start_peer(game_dir, tx_events, peer_game_db, unpacker, catalog)boots the asynchronous runtime in the background and returns aPeerRuntimeHandlewhose sender controls the peer. The injectedUnpackerkeeps archive extraction out of the peer crate's platform layer, and the catalog set gates which local game roots are announced or served.PeerCommandrepresents the small control surface exposed to the UI layer:ListGames,GetGame,FetchLatestFromPeers,DownloadGameFiles,InstallGame,UninstallGame,SetGameDir, andGetPeerCount.PeerEventenumerates everything the peer runtime reports back to the UI: library snapshots, download/install/uninstall lifecycle updates, runtime failures, and peer membership changes.PeerGameDBcollects remote peer metadata. It aggregates discovered peers’Gamedefinitions, tracks the latest ETI version per title, and keeps the last seen list ofGameFileDescriptionentries for each peer.
Internally the peer runtime owns four long-lived tasks that run for the lifetime of the process:
- Server component (
run_server_component) – listens for QUIC connections, advertises via mDNS, and servesRequest::ListGames,Request::GetGame,Request::GetGameFileData, andRequest::GetGameFileChunkby reading from the local game directory. - Discovery loop (
run_peer_discovery) – uses thelanspread-mdnshelper to discover other peers. The blocking mDNS work is executed on a dedicated thread viatokio::task::spawn_blockingso that the Tokio runtime remains responsive. - Ping service (
run_ping_service) – periodically issues QUIC ping requests to keep peer liveness up to date and prunes stale entries fromPeerGameDB. - Local game monitor (
run_local_game_monitor) – watches the configured game directory and each game root non-recursively, gates per-ID rescans while operations are active, and runs a 300-second fallback scan for missed events.
scan_local_library maintains a lightweight on-disk index and produces both a
GameDB and protocol summaries. A game is downloaded only when its root-level
version.ini sentinel exists; local/ being a directory is the install signal.
Networking and File Transfer
- Transport is handled by
s2n-quic; TLS cert/key material is compiled in from the repository root. - Protocol messages are JSON-encoded structures defined in
lanspread-proto::{Request, Response}. - File transfers stream raw bytes over dedicated bidirectional QUIC streams.
peer::send_game_file_datasends entire files, whilepeer::send_game_file_chunkservices ranged requests.
Download Pipeline
When the UI asks to download a game:
- The UI first issues
PeerCommand::GetGamefor a new download, orPeerCommand::FetchLatestFromPeersfor an update that must bypass local archives. The selected peers are queried viarequest_game_details_from_peer, and their file manifests are merged insidePeerGameDB. - Once the UI receives
PeerEvent::GotGameFiles, it forwards the selected file list back withPeerCommand::DownloadGameFiles. download_game_filesstarts a version-sentinel transaction, parks any oldversion.inias.version.ini.discarded, prepares non-sentinel files, emitsPeerEvent::DownloadGameFilesBegin, and builds a per-peer plan (build_peer_plans) that round-robins file chunks across the available peers that advertise the latest version.- Each plan is executed in its own task (
download_from_peer). Chunk requests use per-chunk QUIC streams and write into pre-created files. The chunk writer keeps existing data intact and only truncates when we intentionally fall back to a full file transfer, which prevents corruption when multiple peers fill different regions of the same file. version.inichunks are buffered in memory and committed last via.version.ini.tmpfollowed by an atomic rename. Failures are accumulated and retried (up toMAX_RETRY_COUNT) viaretry_failed_chunks; failed or cancelled downloads sweep.version.ini.tmpand.version.ini.discardedwithout restoring the previous sentinel.- After a successful sentinel commit,
PeerEvent::DownloadGameFilesFinishedis emitted and the peer auto-runs the install transaction.
Install Transactions
Install, update, uninstall, and startup recovery live under src/install/.
Each game root has an atomic .lanspread.json intent log for install-side
operations and uses Lanspread-owned .local.installing/ and .local.backup/
directories marked by .lanspread_owned. Startup recovery combines the recorded
intent with the observed filesystem state and only deletes reserved directories
when intent or marker ownership proves they belong to Lanspread.
Integration with lanspread-tauri-deno-ts
The Tauri application embeds this crate in
crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs:
LanSpreadStateholds onto the peer control channel, the latest aggregatedGameDB, per-game operation state, the catalog set, and the user-selected game directory.- The Tauri commands (
request_games,install_game,update_game, andupdate_game_directory) translate UI actions intoPeerCommands. In particular,update_game_directoryvalidates the filesystem path before storing it, loads the bundled catalog on first use, kicks off the peer runtime on demand, and mirrors the installed/uninstalled state into the UI-facing database. - A background task consumes
PeerEvents and fans them out to the front-end via Tauri publish/subscribe events (games-list-updated,game-download-*,game-install-*,game-uninstall-*,peer-*). The Tauri crate now only provides the unrar sidecar through the injectedUnpacker; rollback and cleanup live in the peer transaction code.
Security & Operational Notes
- All QUIC connections are TLS encrypted; the shipped certificates are suitable for local-network trust but should be rotated for production deployments.
- Peer discovery is restricted to the local link via mDNS.
- Long-running blocking mDNS calls are isolated on dedicated threads which keeps the async runtime responsive even when discovery takes a long time.
- File writes are chunk-safe: partial chunk downloads open files without
truncating existing data, and root-level
version.iniis written only after the rest of the download has succeeded.
Known Limitations
PeerGameDBcurrently models the latest metadata that other peers advertise. If the UI needs to surface titles that only exist locally, additional merging with the locally scannedGameDBwill be required.- The download planner uses a simple round-robin and does not yet take per-peer throughput or failures into account when distributing work.
Refer to the source (particularly src/lib.rs) for the exact message shapes and
state machines.