use avril_core::{Extension, Op}; use std::convert::Infallible; #[cfg(test)] pub fn init() -> Extension { with_broker(std::sync::Arc::default()) } pub fn with_broker(broker: std::sync::Arc) -> Extension { let metadata = ( match std::env::consts::OS { "windows" => "Win32".into(), "macos" => "MacIntel".into(), "linux" => format!("Linux {}", std::env::consts::ARCH), os => os.into(), }, std::thread::available_parallelism() .map(|parallelism| parallelism.get()) .unwrap_or(1), concat!("avril/", env!("CARGO_PKG_VERSION")), ); #[derive(Clone, serde::Serialize)] struct Brand { brand: String, version: String, } #[derive(Clone, serde::Serialize)] #[serde(rename_all = "camelCase")] struct UserAgentData { brands: Vec, mobile: bool, platform: String, architecture: String, bitness: String, model: String, platform_version: String, ua_full_version: String, full_version_list: Vec, wow64: bool, } const FULL_VERSION: &str = env!("CARGO_PKG_VERSION"); let major = FULL_VERSION.split('.').next().unwrap_or(FULL_VERSION); let user_agent_data = UserAgentData { brands: vec![Brand { brand: "avril".into(), version: major.into(), }], mobile: false, platform: match std::env::consts::OS { "windows" => "Windows".into(), "macos" => "macOS".into(), "linux" => "Linux".into(), os => os.into(), }, architecture: match std::env::consts::ARCH { "x86" | "i686" | "x86_64" => "x86".into(), "arm" | "aarch64" => "arm".into(), arch => arch.into(), }, bitness: match std::env::consts::ARCH { "x86_64" | "aarch64" => "64".into(), _ => "32".into(), }, model: String::new(), platform_version: String::new(), ua_full_version: FULL_VERSION.into(), full_version_list: vec![Brand { brand: "avril".into(), version: FULL_VERSION.into(), }], wow64: false, }; let mut ops = avril_host::messaging::ops(broker); ops.push(Op::new("op_navigator_metadata", move |(): ()| { Ok::<_, Infallible>(metadata.clone()) })); ops.push(Op::new("op_navigator_user_agent_data", move |(): ()| { Ok::<_, Infallible>(user_agent_data.clone()) })); Extension { name: "ext:web", dependencies: &["ext:timers"], ops, modules: &[ ( "ext:web/main.js", include_str!(concat!(env!("OUT_DIR"), "/main.js")), ), ( "ext:web/idl.js", include_str!(concat!(env!("OUT_DIR"), "/idl.js")), ), ( "ext:web/interface.js", include_str!(concat!(env!("OUT_DIR"), "/interface.js")), ), ( "ext:web/events.js", include_str!(concat!(env!("OUT_DIR"), "/events.js")), ), ( "ext:web/messaging.js", include_str!(concat!(env!("OUT_DIR"), "/messaging.js")), ), ( "ext:web/broadcast.js", include_str!(concat!(env!("OUT_DIR"), "/broadcast.js")), ), ( "ext:web/serialization.js", include_str!(concat!(env!("OUT_DIR"), "/serialization.js")), ), ( "ext:web/base64.js", include_str!(concat!(env!("OUT_DIR"), "/base64.js")), ), ( "ext:web/navigator.js", include_str!(concat!(env!("OUT_DIR"), "/navigator.js")), ), ( "ext:web/performance.js", include_str!(concat!(env!("OUT_DIR"), "/performance.js")), ), ], entry_point: Some("ext:web/main.js"), } } #[cfg(test)] mod tests { use avril_core::{Extension, Op, Runtime, RuntimeOptions}; use std::convert::Infallible; fn runtime() -> Runtime { Runtime::new(RuntimeOptions { extensions: vec![ super::init(), avril_ext_timers::init().unwrap(), Extension { name: "ext:gc-test", dependencies: &[], ops: vec![Op::with_scope("collect", |scope, (): ()| { scope.low_memory_notification(); Ok::<_, Infallible>(()) })], modules: &[( "ext:gc-test/main.js", "import {collect} from 'ext:core/ops'; globalThis.collect=collect;", )], entry_point: Some("ext:gc-test/main.js"), }, ], ..Default::default() }) .unwrap() } fn collect(runtime: &mut Runtime) { for _ in 0..4 { runtime.execute_script::<()>("gc", "collect();").unwrap(); runtime.run_event_loop().unwrap(); } } #[test] fn unobserved_channels_and_started_ports_can_be_collected() { let mut runtime = runtime(); runtime .execute_script::<()>( "objects", r#" globalThis.cleaned = {channels:0,ports:0,control:0}; globalThis.registry = new FinalizationRegistry(kind => cleaned[kind]++); (() => { for (let i=0;i<100;i++) { registry.register(new BroadcastChannel('gc'), 'channels'); const channel = new MessageChannel(); channel.port1.start(); registry.register(channel.port1, 'ports'); registry.register({}, 'control'); } })(); "#, ) .unwrap(); collect(&mut runtime); assert!( runtime .execute_script::( "counts", "cleaned.channels > 50 && cleaned.ports > 50 && cleaned.control > 50" ) .unwrap() ); } #[test] fn channel_listener_lifetimes_include_once_abort_and_borrowed_methods() { let mut runtime = runtime(); runtime .execute_script::<()>( "objects", r#" globalThis.cleaned = []; globalThis.registry = new FinalizationRegistry(kind => cleaned.push(kind)); globalThis.sender = new BroadcastChannel('lifetime'); globalThis.controller = new AbortController(); (() => { const channel = new BroadcastChannel('lifetime'); channel.onmessage = () => {}; registry.register(channel,'handler'); globalThis.weak = new WeakRef(channel); const once = new BroadcastChannel('lifetime'); EventTarget.prototype.addEventListener.call(once,'message',()=>{}, {once:true}); registry.register(once,'once'); const aborted = new BroadcastChannel('lifetime'); aborted.addEventListener('message',()=>{}, {signal:controller.signal}); registry.register(aborted,'abort'); })(); "#, ) .unwrap(); collect(&mut runtime); assert!( runtime .execute_script::( "alive", "weak.deref() !== undefined && cleaned.length === 0" ) .unwrap() ); runtime .execute_script::<()>( "release", r#" weak.deref().onmessage = null; controller.abort(); sender.postMessage('event'); "#, ) .unwrap(); runtime.run_event_loop().unwrap(); collect(&mut runtime); assert!( runtime .execute_script::( "released", "['handler','once','abort'].every(kind => cleaned.includes(kind))" ) .unwrap() ); runtime .execute_script::<()>("close", "sender.close();") .unwrap(); } #[test] fn collection_can_race_pending_port_deliveries() { let mut runtime = runtime(); runtime .execute_script::<()>( "deliveries", r#" globalThis.cleaned = 0; globalThis.registry = new FinalizationRegistry(()=>cleaned++); globalThis.done = false; (async()=> { const peers = []; for (let round=0;round<10;round++) { (()=> { for (let i=0;i<100;i++) { const channel = new MessageChannel(); if (i % 2) { channel.port1.onmessage = ()=>{}; channel.port1.onmessage = null; } else channel.port1.start(); registry.register(channel.port1,0); channel.port2.postMessage({data:new Uint8Array(64)}); peers.push(channel.port2); } })(); await new Promise(resolve=>setTimeout(()=>{collect();resolve();},0)); collect(); } peers.forEach(peer=>peer.close()); done = true; })(); "#, ) .unwrap(); runtime.run_event_loop().unwrap(); collect(&mut runtime); assert!( runtime .execute_script::("done", "done && cleaned > 500") .unwrap() ); } #[test] fn ports_with_listeners_stay_alive_until_the_peer_closes() { let mut runtime = runtime(); runtime .execute_script::<()>( "objects", r#" globalThis.cleaned = false; globalThis.registry = new FinalizationRegistry(()=> cleaned=true); (() => { const channel = new MessageChannel(); channel.port1.onmessage = ()=> {}; registry.register(channel.port1,0); globalThis.weak = new WeakRef(channel.port1); globalThis.peer = channel.port2; })(); "#, ) .unwrap(); collect(&mut runtime); assert!( runtime .execute_script::("alive", "weak.deref() !== undefined && !cleaned") .unwrap() ); runtime .execute_script::<()>("close", "peer.close();") .unwrap(); runtime.run_event_loop().unwrap(); collect(&mut runtime); assert!( runtime .execute_script::("released", "cleaned") .unwrap() ); } }