use std::any::Any;
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use js_sys::{Array, Function, Object, Proxy, Reflect};
use wasm_bindgen::closure::Closure;
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsValue;
use web_sys::Element;
use crate::magics;
use crate::reactive::{next_scope_id, track, trigger, trigger_scope, ScopeId};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum StaticPropKind {
Auto,
String,
Bool,
Number,
}
pub trait ComponentState: 'static {
fn get(&self, key: &str) -> JsValue;
fn cacheable_fields(&self) -> bool {
true
}
fn set(&mut self, key: &str, value: JsValue);
fn keys(&self) -> &'static [&'static str];
fn is_prop(&self, key: &str) -> bool {
let _ = key;
false
}
fn static_prop_kind(&self, key: &str) -> StaticPropKind {
let _ = key;
StaticPropKind::Auto
}
fn flatten_container_of(&self, key: &str) -> Option<&'static str> {
let _ = key;
None
}
fn is_model(&self, key: &str) -> bool {
let _ = key;
false
}
fn model_name(&self, key: &str) -> Option<&'static str> {
let _ = key;
None
}
fn get_model_value(&self, key: &str) -> JsValue {
self.get(key)
}
fn invoke(&mut self, key: &str, args: &Array) -> JsValue;
fn setup(&mut self, ctx: crate::lifecycle::LifecycleContext<'_>) {
let _ = ctx;
}
fn mount(&mut self, ctx: crate::lifecycle::LifecycleContext<'_>) {
let _ = ctx;
}
fn on_ready(&self, ctx: crate::lifecycle::LifecycleContext<'_>) {
let _ = ctx;
}
fn unmount(&mut self, ctx: crate::lifecycle::LifecycleContext<'_>) {
let _ = ctx;
}
fn has_setup(&self) -> bool {
false
}
fn has_on_mount(&self) -> bool {
false
}
fn has_on_ready(&self) -> bool {
false
}
fn has_on_unmount(&self) -> bool {
false
}
fn transition_in_preset(&self) -> &'static str {
""
}
fn transition_out_preset(&self) -> &'static str {
""
}
fn animate_kind(&self) -> &'static str {
""
}
fn type_name(&self) -> &'static str {
"?"
}
}
#[derive(Clone)]
pub struct Scope {
pub id: ScopeId,
pub state: Rc<RefCell<dyn ComponentState>>,
pub typed: Rc<dyn Any>,
}
type AnyClosures = Vec<Box<dyn Any>>;
thread_local! {
static SCOPES: RefCell<HashMap<ScopeId, Scope>> = RefCell::new(HashMap::new());
static PROXY_CLOSURES: RefCell<HashMap<ScopeId, AnyClosures>> =
RefCell::new(HashMap::new());
static FIELD_CACHE: RefCell<HashMap<ScopeId, HashMap<String, JsValue>>> =
RefCell::new(HashMap::new());
static FRESH_FIELDS: RefCell<HashMap<ScopeId, std::collections::HashSet<String>>> =
RefCell::new(HashMap::new());
static CURRENT_EL: RefCell<Option<Element>> = const { RefCell::new(None) };
static CURRENT_SCOPE_ID: std::cell::Cell<Option<ScopeId>> =
const { std::cell::Cell::new(None) };
}
impl Scope {
pub fn new<T: ComponentState + 'static>(state: Rc<RefCell<T>>) -> Self {
let id = next_scope_id();
let erased: Rc<RefCell<dyn ComponentState>> = state.clone();
let typed: Rc<dyn Any> = Rc::new(state);
let scope = Scope {
id,
state: erased,
typed,
};
SCOPES.with(|s| s.borrow_mut().insert(id, scope.clone()));
scope
}
pub fn find(id: ScopeId) -> Option<Scope> {
SCOPES.with(|s| s.borrow().get(&id).cloned())
}
pub fn all() -> Vec<Scope> {
SCOPES.with(|s| {
let mut out: Vec<Scope> = s.borrow().values().cloned().collect();
out.sort_by_key(|sc| sc.id.0);
out
})
}
pub fn remove(id: ScopeId) {
SCOPES.with(|s| s.borrow_mut().remove(&id));
crate::refs::clear_scope(id);
crate::mount::clear_light_dom_slots(id);
crate::slot_fragment::clear(id);
crate::id::clear_scope(id);
crate::context::clear_scope(id);
crate::events::clear_scope(id);
crate::reactive::clear_scope(id);
crate::component_computed::clear_scope(id);
crate::model_runtime::clear_scope(id);
crate::task::clear_scope(id);
PROXY_CLOSURES.with(|m| {
m.borrow_mut().remove(&id);
});
FIELD_CACHE.with(|c| {
c.borrow_mut().remove(&id);
});
FRESH_FIELDS.with(|f| {
f.borrow_mut().remove(&id);
});
}
pub fn remove_compiled_rows(ids: &[ScopeId]) {
if ids.is_empty() {
return;
}
SCOPES.with(|s| {
let mut map = s.borrow_mut();
for id in ids {
map.remove(id);
}
});
crate::reactive::clear_scopes(ids);
PROXY_CLOSURES.with(|m| {
let mut map = m.borrow_mut();
if !map.is_empty() {
for id in ids {
map.remove(id);
}
}
});
FIELD_CACHE.with(|c| {
let mut map = c.borrow_mut();
if !map.is_empty() {
for id in ids {
map.remove(id);
}
}
});
FRESH_FIELDS.with(|f| {
let mut map = f.borrow_mut();
if !map.is_empty() {
for id in ids {
map.remove(id);
}
}
});
crate::lifecycle::__clear_mount_epochs(ids);
}
pub fn typed<T: 'static>(&self) -> Option<Rc<RefCell<T>>> {
self.typed.downcast_ref::<Rc<RefCell<T>>>().cloned()
}
pub fn cached_field(&self, field: &str) -> Option<JsValue> {
FIELD_CACHE.with(|c| c.borrow().get(&self.id).and_then(|m| m.get(field).cloned()))
}
pub fn set_cached_field(&self, field: &str, value: JsValue) {
FIELD_CACHE.with(|c| {
c.borrow_mut()
.entry(self.id)
.or_default()
.insert(field.to_string(), value);
});
}
pub fn into_proxy(&self) -> JsValue {
let target = Object::new();
let handler = Object::new();
let scope_id = self.id;
let state_for_get = self.state.clone();
let state_for_set = self.state.clone();
let get_closure = Closure::wrap(Box::new(
move |_target: JsValue, key: JsValue, _receiver: JsValue| -> JsValue {
let Some(key_str) = key.as_string() else {
return JsValue::UNDEFINED;
};
if key_str.starts_with('$') {
if matches!(key_str.as_str(), "$index" | "$first" | "$last") {
let local = state_for_get.borrow().get(&key_str);
if !local.is_undefined() {
track(scope_id, &key_str);
return local;
}
}
return magics::resolve(&key_str, scope_id);
}
track(scope_id, &key_str);
let cacheable = state_for_get.borrow().cacheable_fields();
if !cacheable {
return state_for_get.borrow().get(&key_str);
}
if let Some(cached) = FIELD_CACHE.with(|c| {
c.borrow()
.get(&scope_id)
.and_then(|m| m.get(&key_str).cloned())
}) {
return cached;
}
let v = state_for_get.borrow().get(&key_str);
FIELD_CACHE.with(|c| {
c.borrow_mut()
.entry(scope_id)
.or_default()
.insert(key_str.clone(), v.clone());
});
v
},
)
as Box<dyn Fn(JsValue, JsValue, JsValue) -> JsValue>);
let set_closure = Closure::wrap(Box::new(
move |_target: JsValue, key: JsValue, value: JsValue, _receiver: JsValue| -> bool {
let Some(key_str) = key.as_string() else {
return false;
};
let origin = crate::model_runtime::current_write_origin();
crate::model_runtime::with_scope_write(scope_id, origin, || {
state_for_set.borrow_mut().set(&key_str, value);
});
let flatten_container = state_for_set.borrow().flatten_container_of(&key_str);
FIELD_CACHE.with(|c| {
if let Some(m) = c.borrow_mut().get_mut(&scope_id) {
m.remove(&key_str);
if let Some(container) = flatten_container {
m.remove(container);
}
}
});
trigger(scope_id, &key_str);
if let Some(container) = flatten_container {
trigger(scope_id, container);
}
true
},
)
as Box<dyn Fn(JsValue, JsValue, JsValue, JsValue) -> bool>);
Reflect::set(
&handler,
&"get".into(),
get_closure.as_ref().unchecked_ref(),
)
.expect("set get trap");
Reflect::set(
&handler,
&"set".into(),
set_closure.as_ref().unchecked_ref(),
)
.expect("set set trap");
PROXY_CLOSURES.with(|m| {
m.borrow_mut().entry(scope_id).or_default().extend([
Box::new(get_closure) as Box<dyn Any>,
Box::new(set_closure) as Box<dyn Any>,
]);
});
Proxy::new(&target, &handler).into()
}
pub fn invoke(&self, key: &str, args: &Array) -> JsValue {
let prev = CURRENT_SCOPE_ID.with(|c| c.replace(Some(self.id)));
#[cfg(feature = "devtools")]
let start = crate::devtools::ring::now_ms_for_scope();
let out = crate::model_runtime::with_scope_write(
self.id,
crate::model_runtime::WriteOrigin::LocalHandler,
|| self.state.borrow_mut().invoke(key, args),
);
CURRENT_SCOPE_ID.with(|c| c.set(prev));
invalidate_field_cache(self.id);
trigger_scope(self.id);
#[cfg(feature = "devtools")]
{
let dur = std::time::Duration::from_micros(
((crate::devtools::ring::now_ms_for_scope() - start).max(0.0) * 1000.0) as u64,
);
crate::devtools::hooks::fire_handler_invoke(self.id, key, args, dur);
}
out
}
}
pub fn invalidate_field_cache(scope_id: ScopeId) {
let fresh = FRESH_FIELDS.with(|f| {
let mut map = f.borrow_mut();
map.remove(&scope_id).unwrap_or_default()
});
FIELD_CACHE.with(|c| {
if let Some(m) = c.borrow_mut().get_mut(&scope_id) {
if fresh.is_empty() {
m.clear();
} else {
m.retain(|k, _| fresh.contains(k));
}
}
});
}
pub fn invalidate_field(scope_id: ScopeId, field: &str) {
FIELD_CACHE.with(|c| {
if let Some(m) = c.borrow_mut().get_mut(&scope_id) {
m.remove(field);
}
});
}
pub fn patch_list_at_inline<T: serde::Serialize>(field: &str, idx: usize, row: &T) {
let Some(sid) = current_scope_id() else {
return;
};
let cached = FIELD_CACHE.with(|c| c.borrow().get(&sid).and_then(|m| m.get(field).cloned()));
if let Some(arr) = cached {
if arr.is_object() {
if let Ok(new_js) = serde_wasm_bindgen::to_value(row) {
let _ = Reflect::set(&arr, &(idx as u32).into(), &new_js);
}
}
FRESH_FIELDS.with(|f| {
f.borrow_mut()
.entry(sid)
.or_default()
.insert(field.to_string());
});
}
crate::reactive::trigger(sid, field);
}
pub fn patch_list_indices_inline<T: serde::Serialize>(field: &str, patches: &[(usize, &T)]) {
let Some(sid) = current_scope_id() else {
return;
};
let cached = FIELD_CACHE.with(|c| c.borrow().get(&sid).and_then(|m| m.get(field).cloned()));
if let Some(arr) = cached {
if arr.is_object() {
for (idx, row) in patches {
if let Ok(new_js) = serde_wasm_bindgen::to_value(row) {
let _ = Reflect::set(&arr, &(*idx as u32).into(), &new_js);
}
}
}
keep_field_fresh(sid, field);
}
crate::reactive::trigger(sid, field);
}
fn keep_field_fresh(scope_id: ScopeId, field: &str) {
FRESH_FIELDS.with(|f| {
f.borrow_mut()
.entry(scope_id)
.or_default()
.insert(field.to_string());
});
}
pub fn swap_list_indices_inline(field: &str, a: usize, b: usize) {
let Some(sid) = current_scope_id() else {
return;
};
let cached = FIELD_CACHE.with(|c| c.borrow().get(&sid).and_then(|m| m.get(field).cloned()));
if let Some(arr) = cached {
if arr.is_object() {
let a_key = JsValue::from_f64(a as f64);
let b_key = JsValue::from_f64(b as f64);
let a_val = Reflect::get(&arr, &a_key).unwrap_or(JsValue::UNDEFINED);
let b_val = Reflect::get(&arr, &b_key).unwrap_or(JsValue::UNDEFINED);
let _ = Reflect::set(&arr, &a_key, &b_val);
let _ = Reflect::set(&arr, &b_key, &a_val);
}
keep_field_fresh(sid, field);
}
crate::reactive::trigger(sid, field);
}
pub fn remove_list_at_inline(field: &str, idx: usize) {
let Some(sid) = current_scope_id() else {
return;
};
let cached = FIELD_CACHE.with(|c| c.borrow().get(&sid).and_then(|m| m.get(field).cloned()));
if let Some(arr) = cached {
if arr.is_object() {
let array = js_sys::Array::from(&arr);
let len = array.length();
let idx = idx as u32;
if idx < len {
if let Ok(splice) = Reflect::get(&arr, &JsValue::from_str("splice")).and_then(|v| {
v.dyn_into::<Function>()
.map_err(|_| JsValue::from_str("Array.splice is not callable"))
}) {
let _ = splice.call2(
&arr,
&JsValue::from_f64(idx as f64),
&JsValue::from_f64(1.0),
);
}
}
}
keep_field_fresh(sid, field);
}
crate::reactive::trigger(sid, field);
}
pub fn append_list_inline<T: serde::Serialize>(field: &str, start_idx: usize, rows: &[T]) {
let Some(sid) = current_scope_id() else {
return;
};
let cached = FIELD_CACHE.with(|c| c.borrow().get(&sid).and_then(|m| m.get(field).cloned()));
if let Some(arr) = cached {
if arr.is_object() {
for (offset, row) in rows.iter().enumerate() {
if let Ok(new_js) = serde_wasm_bindgen::to_value(row) {
let _ = Reflect::set(&arr, &((start_idx + offset) as u32).into(), &new_js);
}
}
}
keep_field_fresh(sid, field);
}
crate::reactive::trigger(sid, field);
}
pub fn prepend_list_inline<T: serde::Serialize>(field: &str, rows: &[T]) {
let Some(sid) = current_scope_id() else {
return;
};
let cached = FIELD_CACHE.with(|c| c.borrow().get(&sid).and_then(|m| m.get(field).cloned()));
if let Some(arr) = cached {
if arr.is_object() {
let array = js_sys::Array::from(&arr);
let added = rows.len() as u32;
let old_len = array.length();
for idx in (0..old_len).rev() {
let value = Reflect::get(&arr, &idx.into()).unwrap_or(JsValue::UNDEFINED);
let _ = Reflect::set(&arr, &(idx + added).into(), &value);
}
for (idx, row) in rows.iter().enumerate() {
if let Ok(new_js) = serde_wasm_bindgen::to_value(row) {
let _ = Reflect::set(&arr, &(idx as u32).into(), &new_js);
}
}
}
keep_field_fresh(sid, field);
}
crate::reactive::trigger(sid, field);
}
pub fn replace_field_inline<T: serde::Serialize>(field: &str, value: &T) {
let Some(sid) = current_scope_id() else {
return;
};
match serde_wasm_bindgen::to_value(value) {
Ok(new_js) => {
FIELD_CACHE.with(|c| {
c.borrow_mut()
.entry(sid)
.or_default()
.insert(field.to_string(), new_js);
});
FRESH_FIELDS.with(|f| {
f.borrow_mut()
.entry(sid)
.or_default()
.insert(field.to_string());
});
}
Err(_) => invalidate_field(sid, field),
}
crate::reactive::trigger(sid, field);
}
pub fn current_scope_id() -> Option<ScopeId> {
CURRENT_SCOPE_ID.with(|c| c.get())
}
pub fn with_current_scope_id<R>(id: ScopeId, f: impl FnOnce() -> R) -> R {
let prev = CURRENT_SCOPE_ID.with(|c| c.replace(Some(id)));
let out = f();
CURRENT_SCOPE_ID.with(|c| c.set(prev));
out
}
pub fn with_current_el<R>(el: &Element, f: impl FnOnce() -> R) -> R {
let prev = CURRENT_EL.with(|c| c.replace(Some(el.clone())));
let out = f();
CURRENT_EL.with(|c| *c.borrow_mut() = prev);
out
}
pub fn current_el() -> Option<Element> {
CURRENT_EL.with(|c| c.borrow().clone())
}
pub fn invoke_handler(scope_id: ScopeId, key: &str, args: &Array) -> JsValue {
match Scope::find(scope_id) {
Some(s) => s.invoke(key, args),
None => JsValue::UNDEFINED,
}
}
#[allow(dead_code)]
fn _type_check(_: &Function) {}