Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/common/src/prelude.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand Down
6 changes: 6 additions & 0 deletions packages/windows-v8/src/esm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<JsUnknown>(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::<JsUnknown>(0)?;
Expand Down
65 changes: 61 additions & 4 deletions runtime/src/esm_http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -348,10 +348,63 @@ fn fetch_once(url: &str, key: &str) -> Result<crate::winhttp::Response, String>
Ok(response)
}

static DEV_BOOT_COMPLETE: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);

thread_local! {
static PUMPING: Cell<bool> = 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<R: Send>(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<FetchedModule, String> {
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<FetchedModule, String> {
fn fetch_module_now(url: &str, key: &str) -> Result<FetchedModule, String> {
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)"
Expand Down Expand Up @@ -399,16 +452,20 @@ pub(crate) fn fetch_module(url: &str, key: &str) -> Result<FetchedModule, String

/// Fetch several modules concurrently (bounded), preserving input order.
pub(crate) fn fetch_modules(items: &[(String, String)]) -> Vec<Result<FetchedModule, String>> {
pumping(|| fetch_modules_now(items))
}

fn fetch_modules_now(items: &[(String, String)]) -> Vec<Result<FetchedModule, String>> {
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<Option<Result<FetchedModule, String>>> = (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] =
Expand Down
4 changes: 4 additions & 0 deletions runtime/src/global_fns.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand Down Expand Up @@ -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.
Expand Down
17 changes: 16 additions & 1 deletion runtime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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")]
Expand Down Expand Up @@ -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)
}
Expand Down
6 changes: 6 additions & 0 deletions runtime/src/napi_engine/module_runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<JsUnknown>(0)?.coerce_to_bool()?.get_value()?);
ctx.env.get_undefined()
})?;
global.set_named_property("__nsModuleSetDevBootComplete", boot_complete)?;

eval(env, RUNNER, "<module-runner>").map_err(|()| {
napi::Error::from_reason(format!("module runner failed to install: {}", take_exception(env)))
})?;
Expand Down