use tokio::sync::RwLock; use oneclient_common::paths; use oneclient_net::{EtagPolicy, fetch_cached}; use crate::state::LauncherServices; use crate::versions::manifest::{RemoteMigration, VersionMetadata, VersionsManifest}; use crate::LauncherResult; pub struct VersionsManager { manifest: RwLock, } impl VersionsManager { #[must_use] pub fn new() -> Self { Self { manifest: RwLock::new(VersionsManifest::default()), } } /// Seeds the manifest from the last synced copy on disk. /// /// Without this the manifest is empty until the network answers, and /// everything read off it — a version's art, name, description — waits on a /// request that, for a returning user, ends in a 304 and the very same file. /// The UI would rather render slightly stale metadata now and pick up the /// sync when it lands. #[must_use] pub async fn from_cache() -> Self { let manifest = Self::cached_manifest() .await .unwrap_or_else(VersionsManifest::default); Self { manifest: RwLock::new(manifest), } } async fn cached_manifest() -> Option { let path = paths::caches_dir().ok()?.join("versions.json"); let bytes = polyio::read(&path).await.ok()?; match serde_json::from_slice(&bytes) { Ok(manifest) => Some(manifest), Err(err) => { tracing::warn!("cached versions manifest is unreadable: {err}"); None } } } #[tracing::instrument(level = "debug", skip(self, services))] pub async fn sync(&self, services: &LauncherServices) -> LauncherResult { let Some((manifest, changed)) = Self::fetch_manifest(services).await? else { tracing::debug!("skipping versions sync; no remote or cached manifest available"); return Ok(false); }; *self.manifest.write().await = manifest; Ok(changed) } pub async fn metadata(&self, meta_url_base: &str) -> Vec { self.manifest.read().await.metadata(meta_url_base) } pub async fn migrations(&self) -> Vec { self.manifest.read().await.migrations.clone() } #[tracing::instrument(level = "debug", skip(services))] async fn fetch_manifest( services: &LauncherServices, ) -> LauncherResult> { let manifest_path = paths::caches_dir()?.join("versions.json"); let url = format!( "{}/oneclient/versions/metadata.json", services.requester.config().meta_url_base ); let Some(fetched) = fetch_cached(&services.requester, &url, &manifest_path, EtagPolicy::CommitNow).await? else { return Ok(None); }; Ok(Some((fetched.json()?, fetched.changed))) } } impl Default for VersionsManager { fn default() -> Self { Self::new() } }