From d3a8b9a4825cb0ae77c140ee370912cf8fc7e061 Mon Sep 17 00:00:00 2001 From: SGavrl <277843151+SGavrl@users.noreply.github.com> Date: Fri, 10 Jul 2026 20:36:08 +0000 Subject: [PATCH] fix(js): return module eval on completion, not loop idle (Vite mount) (#380) load_module and load_inline_module drove run_event_loop to full idle before reading the module's mod_evaluate result. A page timer keeps the loop busy forever (Vite's HMR / React-Refresh client installs a setInterval), so the preamble inline module burned the entire budget waiting for an idle that never came, and the later module that mounts the app was skipped, leaving #root empty while the production preview rendered fine (issue #374). Drive the event loop and the mod_evaluate result together and return as soon as the module finishes evaluating, so a busy background timer no longer starves the remaining modules. Post-eval async work is still drained by the normal post-load settle. Fixes #374. --- crates/obscura-js/src/runtime.rs | 78 ++++++++++++++++++++------------ 1 file changed, 48 insertions(+), 30 deletions(-) diff --git a/crates/obscura-js/src/runtime.rs b/crates/obscura-js/src/runtime.rs index f3a4e18..2a8361b 100644 --- a/crates/obscura-js/src/runtime.rs +++ b/crates/obscura-js/src/runtime.rs @@ -664,25 +664,33 @@ impl ObscuraJsRuntime { }; let result = self.runtime.mod_evaluate(module_id); + tokio::pin!(result); - let timeout = tokio::time::timeout( - budget, - self.runtime.run_event_loop(deno_core::PollEventLoopOptions::default()), - ).await; + // Return as soon as the module finishes evaluating instead of waiting + // for the event loop to go fully idle: a page timer (setInterval) keeps + // the loop busy forever and would otherwise burn the whole budget, so a + // module that had already evaluated got abandoned (issue #374). + let outcome = tokio::time::timeout(budget, async { + let event_loop = self + .runtime + .run_event_loop(deno_core::PollEventLoopOptions::default()); + tokio::pin!(event_loop); + tokio::select! { + biased; + r = &mut result => r, + e = &mut event_loop => { e?; (&mut result).await } + } + }) + .await; - match timeout { - Ok(Ok(())) => {} - Ok(Err(e)) => return Err(format!("Module event loop error: {}", e)), + match outcome { + Ok(Ok(())) => Ok(()), + Ok(Err(e)) => { + tracing::warn!("Module eval error: {}", e); + Ok(()) + } Err(_) => { tracing::warn!("Module evaluation timed out after {}ms: {}", budget_ms, url); - return Ok(()); - } - } - - match result.await { - Ok(()) => Ok(()), - Err(e) => { - tracing::warn!("Module eval error: {}", e); Ok(()) } } @@ -713,25 +721,35 @@ impl ObscuraJsRuntime { }; let result = self.runtime.mod_evaluate(module_id); + tokio::pin!(result); - let timeout = tokio::time::timeout( - budget, - self.runtime.run_event_loop(deno_core::PollEventLoopOptions::default()), - ).await; + // Drive the event loop, but return as soon as the module finishes + // evaluating rather than waiting for the loop to go fully idle. A page + // timer (Vite's HMR / React-Refresh client installs a setInterval) keeps + // the loop busy forever; waiting for idle burned the whole budget on this + // preamble module and starved the later module that mounts the app, + // leaving #root empty (issue #374). + let outcome = tokio::time::timeout(budget, async { + let event_loop = self + .runtime + .run_event_loop(deno_core::PollEventLoopOptions::default()); + tokio::pin!(event_loop); + tokio::select! { + biased; + r = &mut result => r, + e = &mut event_loop => { e?; (&mut result).await } + } + }) + .await; - match timeout { - Ok(Ok(())) => {} - Ok(Err(e)) => return Err(format!("Module event loop error: {}", e)), + match outcome { + Ok(Ok(())) => Ok(()), + Ok(Err(e)) => { + tracing::warn!("Inline module eval error: {}", e); + Ok(()) + } Err(_) => { tracing::warn!("Inline module timed out after {}ms", budget_ms); - return Ok(()); - } - } - - match result.await { - Ok(()) => Ok(()), - Err(e) => { - tracing::warn!("Inline module eval error: {}", e); Ok(()) } }