markdown formatting

This commit is contained in:
2026-08-09 16:16:45 +02:00
parent f4a6259cf3
commit 18dd3b7e07
21 changed files with 1630 additions and 893 deletions
+35 -34
View File
@@ -1,8 +1,8 @@
# lanspread-peer proposed protocol and architecture
This document proposes a tighter, more fault-tolerant protocol while keeping
the current idea: mDNS discovery, QUIC transport, on-demand metadata, and
chunked file transfers.
This document proposes a tighter, more fault-tolerant protocol while keeping the
current idea: mDNS discovery, QUIC transport, on-demand metadata, and chunked
file transfers.
## Goals (unchanged)
@@ -26,14 +26,15 @@ chunked file transfers.
When a peer is discovered:
1. Connect and send `Hello { peer_id, proto_ver, listen_addr, library_rev,
library_digest, features }`. `listen_addr` is mandatory; the QUIC source port
is only a temporary transport port and must not be recorded as the peer's
listener.
2. Receive `HelloAck { peer_id, proto_ver, listen_addr, library_rev,
library_digest, features }`.
1. Connect and send
`Hello { peer_id, proto_ver, listen_addr, library_rev, library_digest, features }`.
`listen_addr` is mandatory; the QUIC source port is only a temporary
transport port and must not be recorded as the peer's listener.
2. Receive
`HelloAck { peer_id, proto_ver, listen_addr, library_rev, library_digest, features }`.
3. If the remote `peer_id` is already known but the address changed, update it.
4. If protocol versions are incompatible, drop the peer (and keep mDNS watching).
4. If protocol versions are incompatible, drop the peer (and keep mDNS
watching).
5. If library digests match, do nothing else.
6. If digests differ:
- If we have a known `library_rev` for that peer, request `LibraryDelta`.
@@ -50,16 +51,16 @@ When a peer is discovered:
Call to Play is transient peer-session state rather than database state. The
peer keeps a bounded event history, deduplicated by event ID. Every event and
chat message remains in the snapshot for the full lifetime of an active call,
so a peer joining mid-call receives the complete context. A creator's `Start`
or `Cancel` makes the call terminal and read-only, but its complete roster and
chat history remain in snapshots for 15 minutes so late joiners can see the
outcome. After that display window the history compacts to the Start or Cancel
tombstone for the rest of the peer session. Active and recently terminal calls
are never partially trimmed. If genuinely active history reaches the bound,
local publishes return an error to the caller instead of appearing to succeed;
terminal histories and tombstones do not consume that active-history capacity.
A call whose deadline elapses remains available for five minutes so the creator
chat message remains in the snapshot for the full lifetime of an active call, so
a peer joining mid-call receives the complete context. A creator's `Start` or
`Cancel` makes the call terminal and read-only, but its complete roster and chat
history remain in snapshots for 15 minutes so late joiners can see the outcome.
After that display window the history compacts to the Start or Cancel tombstone
for the rest of the peer session. Active and recently terminal calls are never
partially trimmed. If genuinely active history reaches the bound, local
publishes return an error to the caller instead of appearing to succeed;
terminal histories and tombstones do not consume that active-history capacity. A
call whose deadline elapses remains available for five minutes so the creator
can start or extend it, then its unresolved history is evicted as a unit. A
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
@@ -89,8 +90,8 @@ 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.
the same amount. There is deliberately no compatibility path for older protocol
versions.
### 4) Shutdown
@@ -137,8 +138,8 @@ There is deliberately no compatibility path for older protocol versions.
1. Maintain a persistent on-disk index (per game):
- `manifest_hash`, total size, file list (optional), and a fingerprint
(root-level `version.ini` mtime, root-level `.eti` mtime/size, and
`local/` directory presence).
(root-level `version.ini` mtime, root-level `.eti` mtime/size, and `local/`
directory presence).
2. Use filesystem watchers to update only changed games.
3. Keep a 300-second fallback scan to recover from missed events.
@@ -164,8 +165,8 @@ Downloaded and installed are independent predicates:
`local/` are user-owned and are skipped by manifests, fingerprints, and file
serving.
- Install and update transactions unpack into staging, then overwrite the first
discovered game-provided `account_name.txt` and `language.txt` files under
the staged tree from launcher settings before promoting it to `local/`.
discovered game-provided `account_name.txt` and `language.txt` files under the
staged tree from launcher settings before promoting it to `local/`.
Reserved per-game paths:
@@ -239,8 +240,8 @@ Most scans become O(number of game dirs), with full recursion only when needed.
1. Protocol updates in `lanspread-proto`:
- Define `Hello`, `HelloAck`, `LibrarySummary`, `LibrarySnapshot`,
`LibraryDelta`, and optional `Goodbye` messages.
- Thread `peer_id`, `library_rev`, and `manifest_hash` through all
library and manifest-bearing types.
- Thread `peer_id`, `library_rev`, and `manifest_hash` through all library
and manifest-bearing types.
- Make `Hello` and `HelloAck` carry the sender's `listen_addr`,
`library_rev`, and `library_digest` so both sides can record stable
listener addresses and immediately select `LibraryDelta` vs
@@ -248,11 +249,11 @@ Most scans become O(number of game dirs), with full recursion only when needed.
2. Peer identity:
- Persist a stable `peer_id` (UUID) in the peer config and inject it into
`PeerInfo` and `PeerGameDB` at startup.
- Track `peer_id -> SocketAddr` in the discovery table and update the
address on any incoming handshake or mDNS refresh.
- Track `peer_id -> SocketAddr` in the discovery table and update the address
on any incoming handshake or mDNS refresh.
3. Discovery handshake:
- Publish `peer_id` and `library_rev` in mDNS TXT records to avoid
immediate TCP/QUIC roundtrips when nothing changed.
- Publish `peer_id` and `library_rev` in mDNS TXT records to avoid immediate
TCP/QUIC roundtrips when nothing changed.
- Add a lightweight handshake in `run_peer_discovery` that exchanges
`Hello`/`HelloAck` before any library sync.
- Ignore peers that do not advertise the current protocol version.
@@ -261,8 +262,8 @@ Most scans become O(number of game dirs), with full recursion only when needed.
successful index refresh completes.
- Apply `LibraryDelta` when `library_rev` matches; reject stale or future
revisions and request `LibrarySnapshot` instead.
- Cache the last accepted `manifest_hash` per peer to short-circuit
manifest requests when unchanged.
- Cache the last accepted `manifest_hash` per peer to short-circuit manifest
requests when unchanged.
5. Local index + scan optimizations:
- Use the cached `local_library/index.json` file in the configured state
directory to store per-root fingerprints and computed manifests.
+32 -32
View File
@@ -23,19 +23,20 @@ It is designed to run headless other crates (most notably
`Game` definitions, tracks the latest ETI version per title, and keeps the
last seen list of `GameFileDescription` entries for each peer.
Internally the peer runtime owns four long-lived tasks that run for the
lifetime of the process:
Internally the peer runtime owns four long-lived tasks that run for the lifetime
of the process:
1. **Server component** (`run_server_component`) listens for QUIC connections,
advertises via mDNS, and serves `Request::ListGames`, `Request::GetGame`,
`Request::GetGameFileData`, `Request::GetGameFileChunk`, and
`Request::StreamInstall` by reading from the local game directory.
2. **Discovery loop** (`run_peer_discovery`) uses the `lanspread-mdns`
helper to discover other peers. The blocking mDNS work is executed on a
dedicated thread via `tokio::task::spawn_blocking` so that the Tokio runtime
remains responsive.
3. **Ping service** (`run_ping_service`) periodically issues QUIC ping requests
to keep peer liveness up to date and prunes stale entries from `PeerGameDB`.
2. **Discovery loop** (`run_peer_discovery`) uses the `lanspread-mdns` helper
to discover other peers. The blocking mDNS work is executed on a dedicated
thread via `tokio::task::spawn_blocking` so that the Tokio runtime remains
responsive.
3. **Ping service** (`run_ping_service`) periodically issues QUIC ping
requests to keep peer liveness up to date and prunes stale entries from
`PeerGameDB`.
4. **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
@@ -61,8 +62,9 @@ When the UI asks to download a game:
1. The UI first issues `PeerCommand::GetGame` for a new download, or
`PeerCommand::FetchLatestFromPeers` for an update that must bypass local
archives. The selected peers are queried via `request_game_details_from_peer`,
and their file manifests are merged inside `PeerGameDB`.
archives. The selected peers are queried via
`request_game_details_from_peer`, and their file manifests are merged inside
`PeerGameDB`.
2. Once the UI receives `PeerEvent::GotGameFiles`, it forwards the selected file
list back with `PeerCommand::DownloadGameFiles`.
3. `download_game_files` starts a version-sentinel transaction, parks any old
@@ -84,8 +86,8 @@ When the UI asks to download a game:
sweep `.version.ini.tmp` and `.version.ini.discarded` without restoring the
previous sentinel. Cancelled downloads also discard the peer-owned download
payload while preserving `local/` and install transaction metadata.
7. After a successful sentinel commit, `PeerEvent::DownloadGameFilesFinished`
is emitted and the peer auto-runs the install transaction.
7. After a successful sentinel commit, `PeerEvent::DownloadGameFilesFinished` is
emitted and the peer auto-runs the install transaction.
### Streamed Install Pipeline
@@ -108,25 +110,24 @@ 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.
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, downloaded-file removal, and 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.
Downloaded-file removal is deliberately separate from uninstall: it only accepts
catalog IDs that are direct children of the configured game directory, refuses
installed or in-flight roots, and deletes the whole game root only after finding
a regular root-level `version.ini` sentinel.
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. Downloaded-file
removal is deliberately separate from uninstall: it only accepts catalog IDs
that are direct children of the configured game directory, refuses installed or
in-flight roots, and deletes the whole game root only after finding a regular
root-level `version.ini` sentinel.
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
@@ -142,11 +143,10 @@ The Tauri application embeds this crate in
game directory.
- The Tauri commands (`request_games`, `install_game`, `update_game`,
`remove_downloaded_game`, and `update_game_directory`) translate UI actions
into `PeerCommand`s. In
particular, `update_game_directory` validates 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.
into `PeerCommand`s. In particular, `update_game_directory` validates 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 `PeerEvent`s 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