use std::cell::RefCell; use std::collections::{HashMap, HashSet}; use std::rc::Rc; use std::task::Poll; use avril_core::{Error, ModuleLoader, ModuleSource, Runtime, RuntimeOptions}; use futures::future::LocalBoxFuture; struct AcquiringLoader { sources: HashMap, ready: Rc>>, } impl ModuleLoader for AcquiringLoader { fn resolve(&self, specifier: &str, _: &str) -> Result { Ok(specifier.trim_start_matches("./").to_owned()) } fn load(&self, name: &str) -> Result { if !self.ready.borrow().contains(name) { return Err(Error::Module( "V8 linked before acquisition completed".into(), )); } self.sources .get(name) .cloned() .ok_or_else(|| Error::Module("Missing source".into())) } fn prepare( &self, specifier: String, referrer: String, ) -> LocalBoxFuture<'static, Result> { let name = if referrer.is_empty() { specifier } else { self.resolve(&specifier, &referrer).unwrap() }; let ready = self.ready.clone(); let mut yielded = false; Box::pin(futures::future::poll_fn(move |cx| { if !yielded { yielded = true; cx.waker().wake_by_ref(); return Poll::Pending; } if name == "missing.js" { return Poll::Ready(Err(Error::Module("Acquisition failed".into()))); } ready.borrow_mut().insert(name.clone()); if name == "dep.js" { ready.borrow_mut().insert("static.js".into()); } Poll::Ready(Ok(name.clone())) })) } } #[test] fn asynchronous_preparation_precedes_linking_for_entries_and_computed_imports() { let sources = [ ( "main.js", r#" const name = './' + 'dep.js'; const [a, b] = await Promise.all([import(name), import(name)]); globalThis.result = [a === b, a.value, a.count, a.main]; try { await import('./missing.js'); } catch(e) { globalThis.failure = e.message; } globalThis.again = (await import(name)) === a; "#, ), ( "dep.js", r#" import {value} from './static.js'; export {value}; export const count = globalThis.count = (globalThis.count ?? 0) + 1; export const main = import.meta.main; "#, ), ("static.js", "export const value = 42;"), ] .into_iter() .map(|(name, source)| (name.into(), source.into())) .collect(); let mut runtime = Runtime::new(RuntimeOptions { module_loader: Some(Rc::new(AcquiringLoader { sources, ready: Rc::default() })), ..Default::default() }) .unwrap(); runtime.execute_module("main.js").unwrap(); let result: (bool, u32, u32, bool) = runtime.execute_script("result", "result").unwrap(); assert_eq!(result, (true, 42, 1, false)); assert_eq!( runtime .execute_script::("failure", "failure") .unwrap(), "Acquisition failed" ); assert!(runtime.execute_script::("again", "again").unwrap()); } #[test] fn started_modules_allow_host_progress_during_top_level_await() { for reject in [false, true] { let source = format!( "await new Promise(resolve => globalThis.resume = resolve); {}", if reject { "throw Error('entry rejection');" } else { "globalThis.result = 42;" } ); let mut runtime = Runtime::new(RuntimeOptions { module_loader: Some(Rc::new(AcquiringLoader { sources: HashMap::from([("main.js".into(), source.into())]), ready: Rc::default(), })), ..Default::default() }) .unwrap(); runtime.start_module("main.js").unwrap(); assert!(runtime.has_pending_work()); assert!(runtime.start_module("main.js").is_err()); runtime.execute_script::<()>("resume", "resume()").unwrap(); let mut context = std::task::Context::from_waker(futures::task::noop_waker_ref()); let polled = runtime.poll_event_loop(&mut context); if reject { assert!(polled.unwrap_err().to_string().contains("entry rejection")); } else { polled.unwrap(); assert_eq!( runtime.execute_script::("result", "result").unwrap(), 42 ); } assert!(!runtime.has_pending_work()); } }