From 280b42d1c616abed04d799b5decc7f55e5dd5153 Mon Sep 17 00:00:00 2001 From: triniwiz Date: Sun, 8 Nov 2026 16:16:22 -0400 Subject: [PATCH] fix(runtime): pump messages during HTTP imports until dev boot completes A dev boot imports the app graph from the dev server with blocking fetches on the UI thread. On a cold server that takes seconds and the window stops responding. In debug builds, until ns:module setDevBootComplete(true), run the fetches on a helper thread that signals an event when done, while the UI thread waits in MsgWaitForMultipleObjectsEx and dispatches messages as they arrive. setDevBootComplete is exposed on ns:module for the classic and Node-API engines. --- packages/common/src/prelude.rs | 3 ++ packages/windows-v8/src/esm.rs | 6 +++ runtime/src/esm_http.rs | 65 ++++++++++++++++++++++-- runtime/src/global_fns.rs | 4 ++ runtime/src/lib.rs | 17 ++++++- runtime/src/napi_engine/module_runner.rs | 6 +++ 6 files changed, 96 insertions(+), 5 deletions(-) diff --git a/packages/common/src/prelude.rs b/packages/common/src/prelude.rs index b97192b..366c70e 100644 --- a/packages/common/src/prelude.rs +++ b/packages/common/src/prelude.rs @@ -280,6 +280,9 @@ pub const PRELUDE: &str = r#" getLoadedModuleUrls: function getLoadedModuleUrls() { return g.__nsModuleLoadedUrls(); }, + setDevBootComplete: function setDevBootComplete(done) { + if (typeof g.__nsModuleSetDevBootComplete === 'function') { g.__nsModuleSetDevBootComplete(!!done); } + }, createRequire: function createRequire(filenameOrURL) { var value = filenameOrURL && typeof filenameOrURL === 'object' ? filenameOrURL.href : filenameOrURL; if (typeof value !== 'string') { diff --git a/packages/windows-v8/src/esm.rs b/packages/windows-v8/src/esm.rs index 0a7dba8..d9acd7f 100644 --- a/packages/windows-v8/src/esm.rs +++ b/packages/windows-v8/src/esm.rs @@ -118,6 +118,12 @@ pub fn install_ns_module(env: &napi::Env) -> napi::Result<()> { })?; global.set_named_property("__nsModuleConfigureLoader", configure)?; + let boot_complete = env.create_function_from_closure("__nsModuleSetDevBootComplete", |ctx: CallContext| { + esm_loader::set_dev_boot_complete(ctx.get::(0)?.coerce_to_bool()?.get_value()?); + ctx.env.get_undefined() + })?; + global.set_named_property("__nsModuleSetDevBootComplete", boot_complete)?; + // Registry eviction plus a one-shot cache-bust nonce on each evicted URL's next fetch. let invalidate = env.create_function_from_closure("__nsModuleInvalidate", |ctx: CallContext| { let list = ctx.get::(0)?; diff --git a/runtime/src/esm_http.rs b/runtime/src/esm_http.rs index facbf63..882b7b6 100644 --- a/runtime/src/esm_http.rs +++ b/runtime/src/esm_http.rs @@ -11,7 +11,7 @@ //! //! Mechanism mirrors ios/NativeScript/runtime/HttpLoader.mm + ModuleInternalCallbacks.mm. -use std::cell::RefCell; +use std::cell::{Cell, RefCell}; use std::collections::{HashMap, HashSet}; use std::sync::{Mutex, OnceLock}; @@ -348,10 +348,63 @@ fn fetch_once(url: &str, key: &str) -> Result Ok(response) } +static DEV_BOOT_COMPLETE: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false); + +thread_local! { + static PUMPING: Cell = const { Cell::new(false) }; +} + +/// `ns:module` `setDevBootComplete`: stops pumping messages during HTTP imports. +pub fn set_dev_boot_complete(done: bool) { + DEV_BOOT_COMPLETE.store(done, std::sync::atomic::Ordering::Relaxed); +} + +/// Until dev boot completes, runs `fetch` off-thread and pumps messages meanwhile. +fn pumping(fetch: impl FnOnce() -> R + Send) -> R { + if !is_debug_build() || DEV_BOOT_COMPLETE.load(std::sync::atomic::Ordering::Relaxed) || PUMPING.get() { + return fetch(); + } + use windows::core::PCWSTR; + use windows::Win32::Foundation::{CloseHandle, HANDLE, WAIT_OBJECT_0}; + use windows::Win32::System::Threading::{CreateEventW, SetEvent, INFINITE}; + use windows::Win32::UI::WindowsAndMessaging::{MsgWaitForMultipleObjectsEx, MWMO_INPUTAVAILABLE, QS_ALLINPUT}; + + let Ok(done) = (unsafe { CreateEventW(None, true, false, PCWSTR::null()) }) else { + return fetch(); + }; + let event = done.0 as usize; + PUMPING.set(true); + let result = std::thread::scope(|s| { + let handle = s.spawn(move || { + let result = fetch(); + unsafe { + let _ = SetEvent(HANDLE(event as *mut _)); + } + result + }); + // WAIT_OBJECT_0 + 1: messages arrived; the event or a failure ends the wait + while unsafe { MsgWaitForMultipleObjectsEx(Some(&[done]), INFINITE, QS_ALLINPUT, MWMO_INPUTAVAILABLE) }.0 + == WAIT_OBJECT_0.0 + 1 + { + crate::pump_messages(); + } + handle.join() + }); + PUMPING.set(false); + unsafe { + let _ = CloseHandle(done); + } + result.unwrap_or_else(|panic| std::panic::resume_unwind(panic)) +} + +pub(crate) fn fetch_module(url: &str, key: &str) -> Result { + pumping(|| fetch_module_now(url, key)) +} + /// Fetch one module over HTTP (one retry on transport error) and classify the response the way /// the iOS runtime does: 2xx JavaScript (an empty body is the empty module) or JSON; anything else /// is an import error naming the URL. -pub(crate) fn fetch_module(url: &str, key: &str) -> Result { +fn fetch_module_now(url: &str, key: &str) -> Result { if !remote_url_allowed(url) { return Err(format!( "HTTP import blocked: remote module loading is not allowed for {url} (set security.allowRemoteModules in the app's package.json)" @@ -399,16 +452,20 @@ pub(crate) fn fetch_module(url: &str, key: &str) -> Result Vec> { + pumping(|| fetch_modules_now(items)) +} + +fn fetch_modules_now(items: &[(String, String)]) -> Vec> { const MAX_PARALLEL: usize = 8; if items.len() <= 1 { - return items.iter().map(|(u, k)| fetch_module(u, k)).collect(); + return items.iter().map(|(u, k)| fetch_module_now(u, k)).collect(); } let mut out: Vec>> = (0..items.len()).map(|_| None).collect(); for (chunk_index, chunk) in items.chunks(MAX_PARALLEL).enumerate() { std::thread::scope(|s| { let handles: Vec<_> = chunk .iter() - .map(|(u, k)| s.spawn(move || fetch_module(u, k))) + .map(|(u, k)| s.spawn(move || fetch_module_now(u, k))) .collect(); for (i, h) in handles.into_iter().enumerate() { out[chunk_index * MAX_PARALLEL + i] = diff --git a/runtime/src/global_fns.rs b/runtime/src/global_fns.rs index 2f44535..373f9dd 100644 --- a/runtime/src/global_fns.rs +++ b/runtime/src/global_fns.rs @@ -2656,6 +2656,9 @@ include_str!("js/crypto.js"), r#" getLoadedModuleUrls: function getLoadedModuleUrls() { return globalThis.__nsModuleLoadedUrls(); }, + setDevBootComplete: function setDevBootComplete(done) { + if (typeof globalThis.__nsModuleSetDevBootComplete === 'function') globalThis.__nsModuleSetDevBootComplete(!!done); + }, createRequire: function createRequire(filenameOrURL) { var value = filenameOrURL && typeof filenameOrURL === 'object' ? filenameOrURL.href : filenameOrURL; if (typeof value !== 'string') { @@ -4415,6 +4418,7 @@ pub(crate) fn init_async_helpers( register!("__nsModuleConfigureLoader", crate::handle_ns_module_configure_loader); register!("__nsModuleInvalidate", crate::handle_ns_module_invalidate); register!("__nsModuleLoadedUrls", crate::handle_ns_module_loaded_urls); + register!("__nsModuleSetDevBootComplete", crate::handle_ns_module_set_dev_boot_complete); // DevTools host hooks: allow JS to register domain dispatchers and // post events/timestamps to the DevTools server when enabled. diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 0a3507e..99b0155 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -2298,7 +2298,8 @@ fn js_error_value<'s>(scope: &mut v8::PinScope<'s, '_>, msg: &str) -> v8::Local< const NS_MODULE_SOURCE: &str = "const m = globalThis.__nsModuleBuiltin;\n\ export default m;\n\ export const configureLoader = m.configureLoader, invalidateModules = m.invalidateModules, \ -getLoadedModuleUrls = m.getLoadedModuleUrls, createRequire = m.createRequire;\n"; +getLoadedModuleUrls = m.getLoadedModuleUrls, createRequire = m.createRequire, \ +setDevBootComplete = m.setDevBootComplete;\n"; /// Registry key for a URL handed in by JS (`invalidateModules`): canonical for HTTP, a path for /// `file://`, verbatim otherwise. @@ -2344,6 +2345,16 @@ pub(crate) fn handle_ns_module_configure_loader( } } +/// `ns:module` `setDevBootComplete(done)`. +#[cfg(feature = "classic")] +pub(crate) fn handle_ns_module_set_dev_boot_complete( + scope: &mut v8::PinScope<'_, '_>, + args: v8::FunctionCallbackArguments, + _rv: v8::ReturnValue, +) { + crate::esm_http::set_dev_boot_complete(args.get(0).boolean_value(scope)); +} + /// `ns:module` `invalidateModules(urls)`: registry eviction plus a one-shot cache-bust nonce on /// each evicted URL's next fetch. Returns the number of registry entries removed. #[cfg(feature = "classic")] @@ -2527,6 +2538,10 @@ pub mod esm_loader { crate::esm_http::configure_loader(json) } + pub fn set_dev_boot_complete(done: bool) { + crate::esm_http::set_dev_boot_complete(done) + } + pub fn mark_keys_for_cache_bust(keys: &[String]) { crate::esm_http::mark_keys_for_cache_bust(keys) } diff --git a/runtime/src/napi_engine/module_runner.rs b/runtime/src/napi_engine/module_runner.rs index 6561bb6..673e7d0 100644 --- a/runtime/src/napi_engine/module_runner.rs +++ b/runtime/src/napi_engine/module_runner.rs @@ -227,6 +227,12 @@ pub fn install(env: &Env, eval: EvalScript) -> napi::Result<()> { })?; global.set_named_property("__nsModuleConfigureLoader", configure)?; + let boot_complete = env.create_function_from_closure("__nsModuleSetDevBootComplete", |ctx: CallContext| { + esm_loader::set_dev_boot_complete(ctx.get::(0)?.coerce_to_bool()?.get_value()?); + ctx.env.get_undefined() + })?; + global.set_named_property("__nsModuleSetDevBootComplete", boot_complete)?; + eval(env, RUNNER, "").map_err(|()| { napi::Error::from_reason(format!("module runner failed to install: {}", take_exception(env))) })?;