fix(peer): settle current-protocol local state cleanup

The follow-up backlog had drifted into three settled peer/runtime issues: the
legacy game-list fallback contradicted the one-wire-version policy, the Tauri
shell still re-derived local install state from disk after peer snapshots, and
`Availability::Downloading` existed even though active operations are already
reported through a separate operation table.

Remove the legacy `AnnounceGames` request and fallback service. Discovery now
ignores peers that do not advertise the current protocol and a peer id, and
library changes are sent through the current delta path only. This keeps the
runtime aligned with the documented current-build-only interoperability model.

Make peer `LocalGamesUpdated` snapshots authoritative for local fields in the
Tauri database. The GUI-side catalog still owns static metadata such as names,
sizes, and descriptions, but downloaded, installed, local version, and
availability now come from the peer runtime instead of a second whole-library
filesystem scan. Snapshot reconciliation also pins the missing-begin and
missing-finish lifecycle cases in tests.

Collapse availability back to the settled `Ready` and `LocalOnly` states.
Aggregation now counts only `Ready` peers as download sources, and the frontend
no longer carries a dead `Downloading` enum value.

The core peer also exposes the small non-GUI hooks needed by scripted callers:
startup options for state and mDNS, a local-ready event, direct connection, peer
snapshots, and an explicit post-download install policy. Those hooks reuse the
same current protocol path and do not add compatibility shims.

Test Plan:
- `git diff --check`
- `just fmt`
- `just clippy`
- `just test`

Refs: BACKLOG.md, FINDINGS.md, IMPL_DECISIONS.md
This commit is contained in:
2026-05-16 18:32:24 +02:00
parent 6242d64583
commit e711cf3454
23 changed files with 531 additions and 723 deletions
+1 -130
View File
@@ -11,136 +11,7 @@ here" cleanups that grow beyond the in-scope change.
---
## Legacy peer protocol fallback contradicts the wire policy
CLAUDE.md / AGENTS.md: *"There is only one wire version — the current one.
No legacy peers, no compatibility shims, no fallback paths for older
builds."*
Live legacy paths:
- `crates/lanspread-peer/src/services/legacy.rs` exists and is called from
`discovery.rs:183-188` when the `Hello` handshake fails.
- `discovery.rs:134` synthesizes `legacy-{addr}` peer IDs when `proto_ver`
is absent from mDNS TXT records.
- `discovery.rs:169` treats `proto_ver.is_none()` as handshake-eligible.
- `update_and_announce_games` (`handlers.rs:605-624`) branches on
`FEATURE_LIBRARY_DELTA` and falls back to `announce_games_to_peer`
(sending `Request::AnnounceGames`) for peers that don't advertise the
feature.
- `Request::AnnounceGames` is still defined in
`lanspread-proto/src/lib.rs:75` and handled in
`services/stream.rs:116`.
Functionally inert today — current-build peers don't drop `Hello` — but
code and stated policy disagree. Either delete the paths or revert the
policy.
---
## Tauri keeps a parallel filesystem-derived scan
The peer now owns the install state machine (per PLAN.md:11), but Tauri
still re-derives local install/download state from disk on every event:
- `refresh_games_list` (`src-tauri/src/lib.rs:489`) fires after every
`update_game_db`, `update_local_games_in_db`, and
`update_game_directory`. It calls `set_all_uninstalled()` and re-runs
`update_game_installation_state` over every bundled-DB entry,
re-reading `version.ini`, re-checking `local/`, re-parsing version
strings.
- `update_local_games_in_db` (`src-tauri/src/lib.rs:667-704`) just merged
the peer's authoritative `Game` values into the Tauri-side `GameDB`.
Immediately after, `refresh_games_list` re-derives the same fields
from disk and overwrites the merged result.
- The per-ID rescan optimization in `local_monitor.rs` is completely
undone on the Tauri side: every peer event triggers a whole-library
disk walk.
Today both paths reach the same conclusion. The risk is forward-looking:
the moment one of the two derivation rules changes (a new `availability`
rule, a new sentinel, a new ignore name), the two scanners can disagree
silently with no rule for which wins.
**Fix when convenient:** `refresh_games_list` accepts the peer's `Game`
slice and trusts it for local fields. Tauri's bundled DB stays as the
source of truth for static metadata (name, description, max_players,
thumbnail mapping), but `downloaded`/`installed`/`local_version`/
`availability` come from the peer. `update_game_installation_state` and
`set_all_uninstalled` go away. The dead log branch at
`src-tauri/src/lib.rs:397-402` is obviated naturally by this.
---
## `Availability::Downloading` is wire-defined but unreachable
`crates/lanspread-db/src/db.rs:36-41`. The variant exists and serializes
but `build_game_summary` only emits `Ready` or `LocalOnly`.
Operation-table gating handles the in-progress case instead.
`peer_db::get_all_games` has a code path that lets a remote-advertised
`Downloading` summary contribute `eti_version` to aggregation. If a
future maintainer re-enables emitting `Downloading` from
`build_game_summary`, aggregation will treat such peers as
not-downloadable but still pull their version info.
**Decide-and-document task:** either remove the variant (matches the
"current wire only" policy) or add a comment in the proto enum naming
the contract.
---
## `update_game_installation_state` dead log branch
`src-tauri/src/lib.rs:397-402`:
```rust
if eti_package_exists(&game_path, &game.id) && !downloaded {
log::debug!("Game ... has archives but no version.ini sentinel; treating as not downloaded");
}
```
Side-effect-only log line. Either delete or wire to a UI affordance
("partial download — retry?"). Obviated naturally if/when the Tauri
parallel scan goes away.
---
## Untested edge: Tauri reconciliation with dropped lifecycle events
The Rust `reconcile_active_operations` test
(`src-tauri/src/lib.rs:1117-1153`) covers map replacement but not the
realistic case of an event sequence with a missing `begin` or `finish`.
The TS side merges in `App.tsx`. Real failure mode: a missing `finish`
followed by a `LocalGamesUpdated` snapshot should clear the spinner,
and today's code does — but it's not pinned by a test.
Add a test if the spinner ever gets stuck in practice.
---
## Documentation drift
`FOLLOW_UP_2.md` still lists two items as "Still open" that have
landed:
- **#10 `save_library_index` non-atomic** — landed in `fdad162`, atomic
temp+fsync+rename at `local_games.rs:169-184`.
- **#11 Split `download.rs`** — landed in `a251233`, split under
`crates/lanspread-peer/src/download/`.
Either mark the doc complete or delete it. Anyone reading it as status
will be misled.
The collection of plan/follow-up/review docs in the repo root
(`PLAN.md`, `PLAN_AVAILABILITY.md`, `PLAN_ATOMIC_INDEX.md`,
`PLAN_DOWNLOAD_SPLIT.md`, `FOLLOW_UP_PLAN.md`, `FOLLOW_UP_2.md`,
`REVIEW_STEP_1..4.md`, `IMPL_DECISIONS.md`) is also getting noisy. A
single retrospective archive folder for completed plans would help; a
future PLAN.md should be self-terminating with explicit acceptance
criteria so it doesn't spawn this many trailing docs.
---
No open backlog items.
## How items leave this file