feat(peer): coordinate outbound transfers with local game mutations
Updating or removing a local game rewrites its on-disk files. Peers that were mid-download of that game would keep streaming bytes from files that are being deleted or replaced, handing them a corrupt or stale copy. There was also no authoritative notion of which game version a peer should serve or accept, so a peer could serve whatever happened to be on disk and downloaders could aggregate files from peers running mismatched versions. This introduces a reader-writer coordination scheme between outbound file transfers (readers) and local mutation operations (writers), and gates both serving and downloading on an authoritative game catalog version. Reader-writer coordination: - Track active outbound transfers per game in a shared `OutboundTransfers` map of (id, CancellationToken), threaded through `Ctx`/`PeerCtx` and registered by a `TransferGuard` in the stream service. The guard is registered *before* the serve-eligibility check to close a TOCTOU window where a writer could miss an in-flight reader. - `stream_file_bytes` now honors a cancellation token at every await point (file read, network send, stream close) via `tokio::select!`, so a transfer aborts promptly instead of hanging on a stalled receiver. - `begin_operation` marks a game active first, then cancels its outbound transfers and waits for the count to reach zero before any Updating/RemovingDownload work touches the filesystem. - Active games are now hidden from library snapshots entirely while an operation is in flight, instead of freezing their last announced state, so peers stop discovering a game that is being mutated. Authoritative version catalog: - Replace the `HashSet<String>` catalog with `GameCatalog`, mapping each game id to its expected version (from the bundled game.db / ETI data). - Serving requires the local `version.ini` to match the catalog version (`local_download_matches_catalog`); peer selection, file aggregation, and majority size validation all filter on the expected version (`peers_with_expected_version`, `aggregated_game_files`, and friends). User-visible changes: - The GUI shows confirmation dialogs before Update and Remove, and surfaces a sharing-status indicator on game cards and the detail modal. - A new `OutboundTransferCountChanged` event lets the UI reflect live outbound transfer activity. Test Plan: - just test - just frontend-test - just clippy
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { useCallback } from 'react';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { ask } from '@tauri-apps/plugin-dialog';
|
||||
|
||||
import { type UseGamesResult } from './useGames';
|
||||
import { type UISettings } from './useSettings';
|
||||
@@ -69,6 +70,14 @@ export const useGameActions = (
|
||||
|
||||
const update = useCallback(async (id: string) => {
|
||||
try {
|
||||
const game = games.games.find(item => item.id === id);
|
||||
if (game && game.active_outbound_transfers && game.active_outbound_transfers > 0) {
|
||||
const confirmed = await ask(
|
||||
`Peers are currently downloading this game from you. Updating will abort their downloads. Do you want to proceed?`,
|
||||
{ title: 'Active Transfers in Progress', kind: 'warning' }
|
||||
);
|
||||
if (!confirmed) return;
|
||||
}
|
||||
const success = await invoke<boolean>('update_game', {
|
||||
id,
|
||||
language: settings.language,
|
||||
@@ -90,11 +99,19 @@ export const useGameActions = (
|
||||
|
||||
const removeDownload = useCallback(async (id: string) => {
|
||||
try {
|
||||
const game = games.games.find(item => item.id === id);
|
||||
if (game && game.active_outbound_transfers && game.active_outbound_transfers > 0) {
|
||||
const confirmed = await ask(
|
||||
`Peers are currently downloading this game from you. Removing game files will abort their downloads. Do you want to proceed?`,
|
||||
{ title: 'Active Transfers in Progress', kind: 'warning' }
|
||||
);
|
||||
if (!confirmed) return;
|
||||
}
|
||||
await invoke('remove_downloaded_game', { id });
|
||||
} catch (err) {
|
||||
console.error('remove_downloaded_game failed:', err);
|
||||
}
|
||||
}, []);
|
||||
}, [games]);
|
||||
|
||||
const cancelDownload = useCallback(async (id: string) => {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user