use std::{ collections::{HashMap, VecDeque}, hash::{Hash, Hasher}, }; use lanspread_proto::{GameSummary, LibraryDelta, LibrarySnapshot, LibrarySummary}; const MAX_DELTA_HISTORY: usize = 8; #[derive(Debug, Clone)] pub struct LocalLibraryState { pub revision: u64, pub digest: u64, pub games: HashMap, pub recent_deltas: VecDeque, } impl LocalLibraryState { pub fn empty() -> Self { Self { revision: 0, digest: 0, games: HashMap::new(), recent_deltas: VecDeque::new(), } } pub fn update_from_scan( &mut self, summaries: HashMap, revision: u64, ) -> Option { let new_digest = compute_library_digest(&summaries); let changed = self.revision != revision || self.digest != new_digest || self.games != summaries; if !changed { return None; } let delta = compute_library_delta(self.revision, revision, &self.games, &summaries); self.revision = revision; self.digest = new_digest; self.games = summaries; self.recent_deltas.push_back(delta.clone()); while self.recent_deltas.len() > MAX_DELTA_HISTORY { self.recent_deltas.pop_front(); } Some(delta) } pub fn delta_since(&self, from_rev: u64) -> Option { self.recent_deltas .iter() .find(|delta| delta.from_rev == from_rev) .cloned() } } pub fn compute_library_digest(games: &HashMap) -> u64 { let mut entries: Vec<&GameSummary> = games.values().collect(); entries.sort_by(|a, b| a.id.cmp(&b.id)); let mut hasher = std::collections::hash_map::DefaultHasher::new(); for summary in entries { summary.id.hash(&mut hasher); summary.name.hash(&mut hasher); summary.size.hash(&mut hasher); summary.downloaded.hash(&mut hasher); summary.installed.hash(&mut hasher); summary.eti_version.hash(&mut hasher); summary.manifest_hash.hash(&mut hasher); summary.availability.hash(&mut hasher); } hasher.finish() } pub fn build_library_summary(state: &LocalLibraryState) -> LibrarySummary { LibrarySummary { library_rev: state.revision, library_digest: state.digest, game_count: state.games.len(), } } pub fn build_library_snapshot(state: &LocalLibraryState) -> LibrarySnapshot { let mut games: Vec = state.games.values().cloned().collect(); games.sort_by(|a, b| a.id.cmp(&b.id)); LibrarySnapshot { library_rev: state.revision, games, } } pub fn compute_library_delta( from_rev: u64, to_rev: u64, previous: &HashMap, next: &HashMap, ) -> LibraryDelta { let mut added = Vec::new(); let mut updated = Vec::new(); let mut removed = Vec::new(); for (game_id, summary) in next { match previous.get(game_id) { None => added.push(summary.clone()), Some(existing) => { if existing != summary { updated.push(summary.clone()); } } } } for game_id in previous.keys() { if !next.contains_key(game_id) { removed.push(game_id.clone()); } } LibraryDelta { from_rev, to_rev, added, updated, removed, } }