#[cfg(debug_assertions)]
use std::sync::mpsc;
use std::{
cell::RefCell,
collections::HashMap,
sync::{Arc, Mutex},
};
#[derive(Debug)]
pub struct AsyncResult {
pub id: u32,
pub url: String,
pub outcome: Result<Arc<Vec<u8>>, String>,
}
#[derive(Debug, thiserror::Error)]
pub enum ResourceError {
#[error("virtual resource path must not be empty")]
EmptyPath,
#[error("virtual resource not registered: {path}")]
MemNotFound { path: String },
#[error("no resource registry installed on this thread")]
NoRegistry,
}
#[derive(Debug, Default)]
pub struct ResourceRegistry {
map: Mutex<HashMap<String, Arc<Vec<u8>>>>,
#[cfg(debug_assertions)]
reload_txs: Mutex<Vec<mpsc::Sender<()>>>,
}
impl ResourceRegistry {
pub fn new() -> Self {
Self::default()
}
pub fn register(&self, path: impl Into<String>, bytes: Vec<u8>) -> Result<(), ResourceError> {
let path = path.into();
if path.trim().is_empty() {
return Err(ResourceError::EmptyPath);
}
let key = normalize(&path);
self.map
.lock()
.unwrap_or_else(|e| e.into_inner())
.insert(key, Arc::new(bytes));
#[cfg(debug_assertions)]
self.notify_reload();
Ok(())
}
pub fn unregister(&self, path: &str) -> bool {
let removed = self
.map
.lock()
.unwrap_or_else(|e| e.into_inner())
.remove(&normalize(path))
.is_some();
#[cfg(debug_assertions)]
if removed {
self.notify_reload();
}
removed
}
pub fn load(&self, path: &str) -> Result<Arc<Vec<u8>>, ResourceError> {
let key = normalize(path);
self.map
.lock()
.unwrap_or_else(|e| e.into_inner())
.get(&key)
.cloned()
.ok_or(ResourceError::MemNotFound { path: key })
}
#[cfg(debug_assertions)]
pub(crate) fn subscribe_reloads(&self) -> mpsc::Receiver<()> {
let (tx, rx) = mpsc::channel();
self.reload_txs
.lock()
.unwrap_or_else(|e| e.into_inner())
.push(tx);
rx
}
#[cfg(debug_assertions)]
fn notify_reload(&self) {
let mut senders = self.reload_txs.lock().unwrap_or_else(|e| e.into_inner());
let mut notified = false;
senders.retain(|tx| match tx.send(()) {
Ok(()) => {
notified = true;
true
}
Err(_) => false,
});
if !notified {
log::trace!("mem:// resource changed without an active reload subscription");
}
}
pub fn load_async(&self, id: u32, path: String, tx: std::sync::mpsc::Sender<AsyncResult>) {
let outcome = self.load(&path).map_err(|e| e.to_string());
if tx
.send(AsyncResult {
id,
url: path,
outcome,
})
.is_err()
{
log::debug!("async resource load skipped: consumer already dropped");
}
}
}
thread_local! {
static CURRENT: RefCell<Option<Arc<ResourceRegistry>>> = const { RefCell::new(None) };
}
pub fn with_registry<R>(registry: Arc<ResourceRegistry>, f: impl FnOnce() -> R) -> R {
CURRENT.with(|c| {
let prev = c.borrow_mut().replace(registry);
let result = f();
if let Some(prev) = prev {
c.borrow_mut().replace(prev);
} else {
c.borrow_mut().take();
}
result
})
}
pub(crate) fn current() -> Option<Arc<ResourceRegistry>> {
CURRENT.with(|c| c.borrow().clone())
}
pub(crate) fn load(path: &str) -> Result<Arc<Vec<u8>>, ResourceError> {
match current() {
Some(r) => r.load(path),
None => Err(ResourceError::NoRegistry),
}
}
pub(crate) fn load_async(id: u32, path: String, tx: std::sync::mpsc::Sender<AsyncResult>) {
match current() {
Some(r) => r.load_async(id, path, tx),
None => {
let outcome = Err(ResourceError::NoRegistry.to_string());
if tx
.send(AsyncResult {
id,
url: path,
outcome,
})
.is_err()
{
log::debug!("async resource load skipped: consumer already dropped");
}
}
}
}
fn normalize(path: &str) -> String {
if path.starts_with("mem://") {
path.to_string()
} else {
format!("mem://{path}")
}
}
pub fn resolve_url(base_href: Option<&str>, url: &str) -> String {
if url.contains("://") {
return url.to_string();
}
let Some(base) = base_href.filter(|b| !b.trim().is_empty()) else {
return normalize(url);
};
if url.starts_with('/') {
return format!("mem://{}", url.trim_start_matches('/'));
}
let base = normalize(base);
format!("{}/{url}", base.trim_end_matches('/'))
}
#[cfg(all(test, debug_assertions))]
mod reload_tests {
use super::ResourceRegistry;
#[test]
fn register_and_unregister_notify_reload_subscribers() {
let registry = ResourceRegistry::new();
let rx = registry.subscribe_reloads();
registry
.register("mem://reload.js", b"globalThis.__x = 1".to_vec())
.expect("register resource");
assert!(
rx.try_recv().is_ok(),
"register must invalidate an active debug session"
);
assert!(registry.unregister("mem://reload.js"));
assert!(
rx.try_recv().is_ok(),
"removing an existing resource must invalidate an active debug session"
);
assert!(
rx.try_recv().is_err(),
"unregistering a missing key must not invalidate"
);
}
}