use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use wasm_bindgen::JsCast;
use crate::scope::Scope;
thread_local! {
static ISLAND_REGISTRY: RefCell<HashMap<String, js_sys::Function>> =
RefCell::new(HashMap::new());
static SCOPE_REGISTRY: RefCell<HashMap<u32, Rc<Scope>>> =
RefCell::new(HashMap::new());
static SCOPE_ID_COUNTER: RefCell<u32> = const { RefCell::new(0) };
}
pub(crate) fn register_island_fn(name: String, mount: js_sys::Function) {
ISLAND_REGISTRY.with(|r| r.borrow_mut().insert(name, mount));
}
pub(crate) fn get_island_fn(name: &str) -> Option<js_sys::Function> {
ISLAND_REGISTRY.with(|r| r.borrow().get(name).cloned())
}
pub(crate) fn store_scope(element: &web_sys::Element, scope: Rc<Scope>) {
let id = element_handle(element);
store_scope_by_id(id, scope);
}
pub(crate) fn store_scope_by_id(id: u32, scope: Rc<Scope>) {
SCOPE_REGISTRY.with(|registry| registry.borrow_mut().insert(id, scope));
}
pub(crate) fn remove_scope(id: u32) -> Option<Rc<Scope>> {
SCOPE_REGISTRY.with(|registry| registry.borrow_mut().remove(&id))
}
pub(crate) fn scope_registry_len() -> usize {
SCOPE_REGISTRY.with(|registry| registry.borrow().len())
}
pub(crate) fn element_handle(element: &web_sys::Element) -> u32 {
if let Some(id) = lookup_scope_id(element) {
return id;
}
let id = SCOPE_ID_COUNTER.with(|counter| {
let next = *counter.borrow() + 1;
*counter.borrow_mut() = next;
next
});
let _ = element.set_attribute("data-island-scope-id", &id.to_string());
id
}
pub(crate) fn lookup_scope_id(element: &web_sys::Element) -> Option<u32> {
element
.get_attribute("data-island-scope-id")
.and_then(|attribute| attribute.parse::<u32>().ok())
}
pub(crate) fn mark_mounted(element: &web_sys::Element, scope: Rc<Scope>) {
let _ = element.set_attribute("data-island-mounted", "true");
store_scope(element, scope);
}
pub(crate) fn node_to_element(node: wasm_bindgen::JsValue) -> Option<web_sys::Element> {
node.dyn_into::<web_sys::Element>().ok()
}
#[cfg(test)]
mod tests {
use std::cell::Cell;
use std::rc::Rc;
use crate::scope::Scope;
use super::{remove_scope, scope_registry_len, store_scope_by_id};
#[test]
fn remove_scope_drops_owner_and_runs_cleanups_exactly_once() {
let cleanup_run_count = Rc::new(Cell::new(0));
let scope = Scope::new();
let counter_for_cleanup = cleanup_run_count.clone();
scope.on_cleanup(move || {
counter_for_cleanup.set(counter_for_cleanup.get() + 1);
});
let identifier = 9_001;
store_scope_by_id(identifier, scope);
assert_eq!(cleanup_run_count.get(), 0);
let removed_scope = remove_scope(identifier);
assert!(removed_scope.is_some());
drop(removed_scope);
assert_eq!(cleanup_run_count.get(), 1);
let removed_again = remove_scope(identifier);
assert!(removed_again.is_none());
assert_eq!(cleanup_run_count.get(), 1);
}
#[test]
fn cleanups_run_in_lifo_order_on_drop() {
let order = Rc::new(std::cell::RefCell::new(Vec::new()));
let scope = Scope::new();
for step in 0..3 {
let order_for_cleanup = order.clone();
scope.on_cleanup(move || {
order_for_cleanup.borrow_mut().push(step);
});
}
let identifier = 9_100;
store_scope_by_id(identifier, scope);
drop(remove_scope(identifier));
assert_eq!(*order.borrow(), vec![2, 1, 0]);
}
#[test]
fn registry_length_returns_to_baseline_after_round_trips() {
let baseline = scope_registry_len();
let round_trips = 50;
let first_identifier = 10_000;
for offset in 0..round_trips {
let identifier = first_identifier + offset;
store_scope_by_id(identifier, Scope::new());
assert_eq!(scope_registry_len(), baseline + 1);
drop(remove_scope(identifier));
assert_eq!(scope_registry_len(), baseline);
}
assert_eq!(scope_registry_len(), baseline);
}
}