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.
This commit is contained in:
SGavrl authored and GitHub committed 2026-07-10 20:36:08 +00:00
1 parent 68234970b1
commit d3a8b9a482
1 file changed
+48 -30
+48 -30
View File
@@ -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(())
}
}