From 0af394c8ee76280ed1aac555fcaa318b4ecb63cc Mon Sep 17 00:00:00 2001 From: sychic <47618543+Sychic@users.noreply.github.com> Date: Tue, 6 Oct 2026 15:50:15 -0400 Subject: [PATCH 1/2] refactor(app-lib): use state helpers instead of raw state --- packages/app-lib/src/state/dirs.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/app-lib/src/state/dirs.rs b/packages/app-lib/src/state/dirs.rs index b4fb7e21713..181c89a146b 100644 --- a/packages/app-lib/src/state/dirs.rs +++ b/packages/app-lib/src/state/dirs.rs @@ -2,8 +2,7 @@ pub(crate) mod move_app_directory; use crate::LoadingBarType; use crate::event::emit::{emit_loading, init_loading}; -use crate::state::LAUNCHER_STATE; -use crate::state::Settings; +use crate::state::{Settings, State}; use crate::util::fetch::IoSemaphore; use std::path::PathBuf; use tokio::fs; @@ -23,8 +22,8 @@ pub struct DirectoryInfo { } impl DirectoryInfo { - pub fn global_handle_if_ready() -> Option<&'static Self> { - LAUNCHER_STATE.get().map(|x| &x.directories) + pub fn global_handle_if_ready() -> Option { + State::get_if_initialized().map(|state| state.directories.clone()) } pub fn get_initial_settings_dir(&self) -> Option { From 39530e046b83ac28c16105802dbcad50b4e7dffe Mon Sep 17 00:00:00 2001 From: sychic <47618543+Sychic@users.noreply.github.com> Date: Tue, 6 Oct 2026 16:44:37 -0400 Subject: [PATCH 2/2] fix(app-lib): await state instead of polling --- packages/app-lib/src/error.rs | 9 +++++ packages/app-lib/src/state/mod.rs | 57 ++++++++++++++++++++++--------- 2 files changed, 50 insertions(+), 16 deletions(-) diff --git a/packages/app-lib/src/error.rs b/packages/app-lib/src/error.rs index 7f9c718d074..1b957f0a235 100644 --- a/packages/app-lib/src/error.rs +++ b/packages/app-lib/src/error.rs @@ -265,6 +265,15 @@ impl std::fmt::Display for Error { } } +impl Clone for Error { + fn clone(&self) -> Self { + Self { + raw: Arc::clone(&self.raw), + source: Arc::clone(&self.raw).in_current_span(), + } + } +} + impl> From for Error { fn from(source: E) -> Self { let error = Into::::into(source); diff --git a/packages/app-lib/src/state/mod.rs b/packages/app-lib/src/state/mod.rs index f2c6cf6970f..22c4e11e17b 100644 --- a/packages/app-lib/src/state/mod.rs +++ b/packages/app-lib/src/state/mod.rs @@ -1,8 +1,9 @@ //! Theseus state management system use crate::util::fetch::{FetchSemaphore, IoSemaphore}; use dashmap::DashMap; -use std::sync::Arc; use std::sync::atomic::AtomicBool; +use std::sync::{Arc, LazyLock}; +use tokio::sync::watch::Sender; use tokio::sync::{Mutex, MutexGuard, OnceCell, OwnedMutexGuard, Semaphore}; use crate::state::instances::watcher::FileWatcher; @@ -67,9 +68,15 @@ mod legacy_converter; pub mod attached_world_data; pub mod server_join_log; -// Global state -// RwLock on state only has concurrent reads, except for config dir change which takes control of the State +enum StartupPhase { + Pending, + Ready, + Failed(crate::Error), +} + static LAUNCHER_STATE: OnceCell> = OnceCell::const_new(); +static STATE_STARTUP: LazyLock> = + LazyLock::new(|| Sender::new(StartupPhase::Pending)); static STATE_STARTUP_LOCK: Mutex<()> = Mutex::const_new(()); const MAX_CONCURRENT_INSTALL_JOBS: usize = 3; pub struct State { @@ -172,9 +179,17 @@ impl State { pub async fn init(app_identifier: String) -> crate::Result<()> { let _startup = STATE_STARTUP_LOCK.lock().await; - let state = LAUNCHER_STATE - .get_or_try_init(move || Self::initialize_state(app_identifier)) - .await?; + let result = LAUNCHER_STATE + .get_or_try_init(|| async { + STATE_STARTUP.send_replace(StartupPhase::Pending); + Self::initialize_state(app_identifier).await + }) + .await; + STATE_STARTUP.send_replace(match &result { + Ok(_) => StartupPhase::Ready, + Err(error) => StartupPhase::Failed(error.clone()), + }); + let state = result?; if state .startup_complete @@ -297,18 +312,28 @@ impl State { /// Get the current launcher state, waiting for initialization. pub async fn get() -> crate::Result> { - if !LAUNCHER_STATE.initialized() { - tracing::error!( - "Attempted to get state before it is initialized - this should never happen!" - ); - while !LAUNCHER_STATE.initialized() { - tokio::time::sleep(std::time::Duration::from_millis(100)).await; - } + if let Some(state) = LAUNCHER_STATE.get() { + return Ok(Arc::clone(state)); } - Ok(Arc::clone( - LAUNCHER_STATE.get().expect("State is not initialized!"), - )) + let mut phases = STATE_STARTUP.subscribe(); + let phase = phases + .wait_for(|phase| !matches!(phase, StartupPhase::Pending)) + .await + .expect("the startup sender is static and never dropped"); + match &*phase { + StartupPhase::Ready => Ok(Arc::clone( + LAUNCHER_STATE + .get() + .expect("state is set before startup is marked ready"), + )), + StartupPhase::Failed(error) => Err(error.clone()), + StartupPhase::Pending => { + unreachable!( + "wait_for returns only once the phase has left Pending" + ) + } + } } pub fn initialized() -> bool {