Treat directory/file shape as part of each peer manifest vote and reject portable aliases before aggregating descriptors. This keeps majority selection deterministic and avoids collapsing conflicting entries that share a path or size. This preserves the previously staged consensus hardening before the protocol-8 catalog-authority cutover layered in the working tree. Test Plan: - `git diff --cached --check` -- passed
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,StreamInstallGame,InstallGame,UninstallGame,RemoveDownloadedGame,CancelDownload,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,Request::GetGameFileChunk, andRequest::StreamInstallby 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, emits local-library changes separately from active operation snapshots, 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 requests the download by game ID only. The peer core validates every source manifest before consensus, chooses the complete authoritative description, and constructs one root-confinedValidatedDownloadManifestbefore any filesystem mutation. download_game_filesrecovers any earlier attempt, parks an oldversion.inias.version.ini.discarded, and durably journals the exact old and proposed downloader-owned file sets before preparing non-sentinel files. It then emitsPeerEvent::DownloadGameFilesBeginand 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. DownloadProgressTrackersamples byte counters, transfer speed, and the number of unique peers that are actively streaming chunks. The Tauri UI sees those values together through the regular download-progress event.version.inichunks are buffered in memory. After transfer, paths owned by the previous successful download but absent from the new manifest are removed. Payload files and their directories are synced before the new sentinel is committed last via.version.ini.tmpfollowed by an atomic rename. A sentinel rename whose directory sync fails leaves ownership pending for recovery instead of being reported as a durable success. Transfer failures are accumulated and retried (up toMAX_RETRY_COUNT) viaretry_failed_chunks.- Failure, cancellation, and startup recovery use the journal to remove only
exact downloader-owned files. Unknown user files,
local/, and install transaction state are preserved. A regularversion.inibeside a pending journal proves that the final rename landed; otherwise recovery aborts the incomplete payload. - After a successful sentinel commit,
PeerEvent::DownloadGameFilesFinishedis emitted and the peer auto-runs the install transaction.
Streamed Install Pipeline
Low-disk installs use PeerCommand::StreamInstallGame instead of the normal
archive download pipeline. The peer core owns the whole operation: it refreshes
file metadata from catalog-version peers, runs the same majority file-size
validation used by normal downloads, selects a validated peer list, and emits
the regular download/install lifecycle events while streaming archive-expanded
bytes directly into a StreamedInstallTransaction.
The sender-side StreamInstallProvider writes control and chunk frames through
a cancellable StreamInstallFrameSink. If the QUIC writer fails because the
receiver cancelled or disconnected, the sink wakes any producer blocked on the
bounded frame channel and lets the transfer guard drop normally.
Each failed peer attempt rolls back its staging directory before trying the next
validated peer. A transaction that created a previously missing game root
removes that root again when rollback leaves it empty. Once staging has been
renamed to local/, post-promote intent or launch-settings cleanup failures are
logged for startup recovery rather than reported as a failed install.
PeerCommand::CancelDownload cancels the tracked download token for an active
transfer. The transfer task remains responsible for clearing
active_operations, discarding partial payload files, and refreshing the
settled local snapshot, so the UI continues to treat active-operation snapshots
as the single source of truth for whether a download is still running.
Install Transactions
Install, update, uninstall, and install-side startup recovery live under
src/install/. Install-side operation intent is stored atomically under the
configured peer state directory, at games/<game_id>/install_intent.json. Game
roots still use 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.
Download provenance is stored separately at
games/<game_id>/download_ownership.json in the peer state directory. It is
bound to the canonical configured games directory so switching library roots
cannot make an old record authorize deletion in a different tree. Downloaded-
file removal is deliberately separate from uninstall: it refuses installed or
in-flight roots, journals an empty pending generation, and deletes only the
regular sentinel plus paths proven by the last committed ownership set. Unknown
files, directories, and the game root remain untouched.
Legacy launcher-owned files in game directories are migrated by a dedicated pre-start phase. Normal install, recovery, scan, and transfer paths use only the configured state directory for launcher-owned metadata.
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,remove_downloaded_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.