use std::cell::RefCell; use std::pin::Pin; use std::rc::{Rc, Weak}; use std::sync::Arc; use deno_core::error::ModuleLoaderError; use deno_core::ModuleLoadResponse; use deno_core::ModuleLoader; use deno_core::ModuleSource; use deno_core::ModuleSourceCode; use deno_core::ModuleSpecifier; use deno_core::RequestedModuleType; use crate::import_map::ImportMap; use crate::ops::ObscuraState; /// Observable network activity for ES-module graphs. /// /// deno_core keeps dynamic-import state inside its private module map. The /// browser lifecycle still needs to distinguish a genuinely idle page from a /// graph whose fetch future is being advanced in short event-loop slices. A /// loader-owned counter provides that signal without treating unrelated /// fetch/XHR analytics as render-blocking work. #[derive(Debug, Default)] pub(crate) struct ModuleLoadActivity { pending: std::sync::atomic::AtomicUsize, last_activity: std::sync::Mutex>, } impl ModuleLoadActivity { fn begin(self: &Arc) -> ModuleLoadGuard { self.pending .fetch_add(1, std::sync::atomic::Ordering::Relaxed); *self .last_activity .lock() .unwrap_or_else(|poisoned| poisoned.into_inner()) = Some(std::time::Instant::now()); ModuleLoadGuard(self.clone()) } pub(crate) fn is_pending_or_recent(&self, grace: std::time::Duration) -> bool { if self.pending.load(std::sync::atomic::Ordering::Relaxed) != 0 { return true; } self.last_activity .lock() .unwrap_or_else(|poisoned| poisoned.into_inner()) .is_some_and(|last| last.elapsed() <= grace) } } struct ModuleLoadGuard(Arc); impl Drop for ModuleLoadGuard { fn drop(&mut self) { let previous = self .0 .pending .fetch_sub(1, std::sync::atomic::Ordering::Relaxed); debug_assert!(previous > 0, "module load activity counter underflow"); *self .0 .last_activity .lock() .unwrap_or_else(|poisoned| poisoned.into_inner()) = Some(std::time::Instant::now()); } } pub struct ObscuraModuleLoader { pub base_url: String, /// Proxy URL threaded through to every dynamic ES-module fetch (#139). /// `None` keeps the pre-#139 direct-connection behaviour for callers /// that haven't been updated. pub proxy_url: Option, /// The owning page's network context. Production runtimes always install /// this so every module in a graph uses the same cookie jar, configured /// identity, redirect/security policy, interception, and callbacks as the /// entry module. Directly-constructed standalone loaders remain supported. page_state: Option>>, /// Directly-constructed loaders still use Obscura's network policy and /// connection pool; they simply have an isolated cookie jar. standalone_client: Option>, import_map: Rc>, activity: Arc, } impl ObscuraModuleLoader { pub fn new(base_url: &str) -> Self { Self::with_proxy(base_url, None) } pub fn with_proxy(base_url: &str, proxy_url: Option) -> Self { let import_map = Rc::new(RefCell::new(ImportMap::default())); Self::with_proxy_and_import_map(base_url, proxy_url, import_map) } fn with_proxy_and_import_map( base_url: &str, proxy_url: Option, import_map: Rc>, ) -> Self { let standalone_client = Arc::new(obscura_net::ObscuraHttpClient::with_options( Arc::new(obscura_net::CookieJar::new()), proxy_url.as_deref(), )); ObscuraModuleLoader { base_url: base_url.to_string(), proxy_url, page_state: None, standalone_client: Some(standalone_client), import_map, activity: Arc::new(ModuleLoadActivity::default()), } } pub(crate) fn with_page_state( base_url: &str, proxy_url: Option, page_state: &Rc>, import_map: Rc>, ) -> Self { ObscuraModuleLoader { base_url: base_url.to_string(), proxy_url, page_state: Some(Rc::downgrade(page_state)), standalone_client: None, import_map, activity: Arc::new(ModuleLoadActivity::default()), } } pub(crate) fn activity(&self) -> Arc { self.activity.clone() } } fn io_err(msg: String) -> ModuleLoaderError { std::io::Error::new(std::io::ErrorKind::Other, msg).into() } impl ModuleLoader for ObscuraModuleLoader { fn resolve( &self, specifier: &str, referrer: &str, _kind: deno_core::ResolutionKind, ) -> Result { // deno_core represents the root passed to load_side_es_module with a // synthetic "." referrer. A browser resolves