use std::borrow::Cow;
use std::cell::{Cell, RefCell};
use std::collections::{HashMap, HashSet};
use std::rc::Rc;
use js_sys::Promise;
use wasm_bindgen::closure::Closure;
use wasm_bindgen::JsValue;
use wasm_bindgen_futures::JsFuture;
pub type Key = Cow<'static, str>;
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
pub struct ScopeId(pub u64);
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
pub struct EffectId(pub u64);
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
pub struct SignalId(pub u64);
pub const SIGNAL_SCOPE: ScopeId = ScopeId(0);
type EffectFn = Rc<dyn Fn()>;
type SchedulerFn = Rc<dyn Fn(EffectId)>;
type CleanupFn = Box<dyn FnOnce()>;
#[derive(Default, Clone)]
pub struct EffectOptions {
pub lazy: bool,
pub scheduler: Option<SchedulerFn>,
}
thread_local! {
static NEXT_ID: Cell<u64> = const { Cell::new(1) };
static CURRENT_EFFECT: Cell<Option<EffectId>> = const { Cell::new(None) };
static EFFECTS: RefCell<HashMap<EffectId, EffectFn>> = RefCell::new(HashMap::new());
static SCHEDULERS: RefCell<HashMap<EffectId, SchedulerFn>> = RefCell::new(HashMap::new());
static DEPS: RefCell<HashMap<ScopeId, HashMap<Key, HashSet<EffectId>>>> =
RefCell::new(HashMap::new());
static REVERSE: RefCell<HashMap<EffectId, HashSet<(ScopeId, Key)>>> =
RefCell::new(HashMap::new());
static SIGNAL_DEPS: RefCell<HashMap<SignalId, HashSet<EffectId>>> = RefCell::new(HashMap::new());
static SIGNAL_REVERSE: RefCell<HashMap<EffectId, HashSet<SignalId>>> = RefCell::new(HashMap::new());
static QUEUE: RefCell<HashSet<EffectId>> = RefCell::new(HashSet::new());
static FLUSH_SCHEDULED: Cell<bool> = const { Cell::new(false) };
static CLEANUPS: RefCell<HashMap<EffectId, Vec<CleanupFn>>> = RefCell::new(HashMap::new());
static BATCHING: Cell<u32> = const { Cell::new(0) };
static AUTO_FLUSH: Cell<bool> = const { Cell::new(true) };
static TRIGGER_SCRATCH: RefCell<Vec<EffectId>> = RefCell::new(Vec::with_capacity(16));
}
pub fn set_auto_flush(enabled: bool) {
AUTO_FLUSH.with(|a| a.set(enabled));
}
pub fn next_scope_id() -> ScopeId {
NEXT_ID.with(|c| {
let id = c.get();
c.set(id + 1);
let out = ScopeId(id);
debug_assert_ne!(out, SIGNAL_SCOPE, "scope id collided with SIGNAL_SCOPE");
out
})
}
fn next_effect_id() -> EffectId {
NEXT_ID.with(|c| {
let id = c.get();
c.set(id + 1);
EffectId(id)
})
}
pub fn next_signal_id() -> SignalId {
NEXT_ID.with(|c| {
let id = c.get();
c.set(id + 1);
SignalId(id)
})
}
pub fn current_effect() -> Option<EffectId> {
CURRENT_EFFECT.with(|c| c.get())
}
pub fn effect(f: impl Fn() + 'static) -> EffectId {
effect_with(f, EffectOptions::default())
}
pub fn effect_scoped(f: impl Fn() + 'static) {
let id = effect(f);
crate::events::on_scope_unmount(move || release(id));
}
pub fn effect_with(f: impl Fn() + 'static, opts: EffectOptions) -> EffectId {
effect_with_dyn(Rc::new(f), opts)
}
fn effect_with_dyn(f: EffectFn, opts: EffectOptions) -> EffectId {
let id = next_effect_id();
EFFECTS.with(|e| e.borrow_mut().insert(id, f.clone()));
if let Some(sched) = opts.scheduler {
SCHEDULERS.with(|s| s.borrow_mut().insert(id, sched));
}
if !opts.lazy {
run_effect(id, &f);
}
id
}
fn run_effect(id: EffectId, f: &EffectFn) {
run_cleanups(id);
clear_deps_for(id);
let prev = CURRENT_EFFECT.with(|c| c.replace(Some(id)));
#[cfg(feature = "devtools")]
let start = now_ms();
f();
CURRENT_EFFECT.with(|c| c.set(prev));
#[cfg(feature = "devtools")]
{
let dur = std::time::Duration::from_micros(((now_ms() - start).max(0.0) * 1000.0) as u64);
crate::devtools::hooks::fire_effect_run(id, None, dur);
}
}
#[cfg(feature = "devtools")]
fn now_ms() -> f64 {
#[cfg(target_arch = "wasm32")]
{
web_sys::window()
.and_then(|w| w.performance())
.map(|p| p.now())
.unwrap_or(0.0)
}
#[cfg(not(target_arch = "wasm32"))]
{
0.0
}
}
fn clear_deps_for(id: EffectId) {
let keys: Option<HashSet<(ScopeId, Key)>> = REVERSE.with(|r| r.borrow_mut().remove(&id));
if let Some(keys) = keys {
DEPS.with(|d| {
let mut d = d.borrow_mut();
for (scope, key) in keys {
if let Some(inner) = d.get_mut(&scope) {
if let Some(set) = inner.get_mut(&key) {
set.remove(&id);
if set.is_empty() {
inner.remove(&key);
}
}
if inner.is_empty() {
d.remove(&scope);
}
}
}
});
}
let sig_keys: Option<HashSet<SignalId>> = SIGNAL_REVERSE.with(|r| r.borrow_mut().remove(&id));
if let Some(sig_keys) = sig_keys {
SIGNAL_DEPS.with(|d| {
let mut d = d.borrow_mut();
for sid in sig_keys {
if let Some(set) = d.get_mut(&sid) {
set.remove(&id);
if set.is_empty() {
d.remove(&sid);
}
}
}
});
}
}
fn run_cleanups(id: EffectId) {
let pending: Option<Vec<CleanupFn>> = CLEANUPS.with(|c| c.borrow_mut().remove(&id));
if let Some(pending) = pending {
for f in pending {
f();
}
}
}
pub fn release(id: EffectId) {
run_cleanups(id);
clear_deps_for(id);
EFFECTS.with(|e| e.borrow_mut().remove(&id));
SCHEDULERS.with(|s| s.borrow_mut().remove(&id));
QUEUE.with(|q| {
q.borrow_mut().remove(&id);
});
}
pub fn on_cleanup(f: impl FnOnce() + 'static) {
let Some(id) = current_effect() else { return };
CLEANUPS.with(|c| c.borrow_mut().entry(id).or_default().push(Box::new(f)));
}
pub fn track(scope_id: ScopeId, key: &str) {
let Some(id) = current_effect() else { return };
let already_present = DEPS.with(|d| {
d.borrow()
.get(&scope_id)
.and_then(|inner| inner.get(key))
.map(|set| set.contains(&id))
.unwrap_or(false)
});
if already_present {
return;
}
let owned: Key = Cow::Owned(key.to_owned());
DEPS.with(|d| {
d.borrow_mut()
.entry(scope_id)
.or_default()
.entry(owned.clone())
.or_default()
.insert(id);
});
REVERSE.with(|r| {
r.borrow_mut()
.entry(id)
.or_default()
.insert((scope_id, owned));
});
}
pub fn track_signal(signal_id: SignalId) {
let Some(id) = current_effect() else { return };
let already = SIGNAL_DEPS.with(|d| {
d.borrow()
.get(&signal_id)
.map(|set| set.contains(&id))
.unwrap_or(false)
});
if already {
return;
}
SIGNAL_DEPS.with(|d| {
d.borrow_mut().entry(signal_id).or_default().insert(id);
});
SIGNAL_REVERSE.with(|r| {
r.borrow_mut().entry(id).or_default().insert(signal_id);
});
}
fn dispatch_subs(subs: &HashSet<EffectId>) {
if subs.is_empty() {
return;
}
let mut local = TRIGGER_SCRATCH.with(|s| std::mem::take(&mut *s.borrow_mut()));
local.clear();
local.extend(subs.iter().copied());
let ids_len = local.len();
if ids_len == 0 {
TRIGGER_SCRATCH.with(|s| {
let mut current = s.borrow_mut();
if local.capacity() > current.capacity() {
*current = local;
}
});
return;
}
let mut any_queued = false;
#[cfg(feature = "devtools")]
let mut newly_queued: Vec<EffectId> = Vec::new();
for &eid in local.iter().take(ids_len) {
let sched = SCHEDULERS.with(|s| s.borrow().get(&eid).cloned());
match sched {
Some(s) => s(eid),
None => {
QUEUE.with(|q| {
q.borrow_mut().insert(eid);
});
any_queued = true;
#[cfg(feature = "devtools")]
newly_queued.push(eid);
}
}
}
local.clear();
TRIGGER_SCRATCH.with(|s| {
let mut current = s.borrow_mut();
if local.capacity() > current.capacity() {
*current = local;
}
});
#[cfg(feature = "devtools")]
if !newly_queued.is_empty() {
crate::devtools::hooks::fire_queue_change(&newly_queued);
}
if !any_queued {
return;
}
if BATCHING.with(|b| b.get()) > 0 {
return;
}
schedule_flush();
}
pub fn trigger(scope_id: ScopeId, key: &str) {
let subs: Option<HashSet<EffectId>> = DEPS.with(|d| {
d.borrow()
.get(&scope_id)
.and_then(|inner| inner.get(key))
.cloned()
});
if let Some(subs) = subs {
dispatch_subs(&subs);
}
}
pub fn trigger_signal(signal_id: SignalId) {
let subs: Option<HashSet<EffectId>> = SIGNAL_DEPS.with(|d| d.borrow().get(&signal_id).cloned());
if let Some(subs) = subs {
dispatch_subs(&subs);
}
#[cfg(feature = "devtools")]
crate::devtools::hooks::fire_signal_trigger(signal_id);
}
pub fn clear_scope(scope_id: ScopeId) {
DEPS.with(|d| {
d.borrow_mut().remove(&scope_id);
});
}
pub fn clear_scopes(scope_ids: &[ScopeId]) {
if scope_ids.is_empty() {
return;
}
DEPS.with(|d| {
let mut map = d.borrow_mut();
for id in scope_ids {
map.remove(id);
}
});
}
pub fn trigger_scope(scope_id: ScopeId) {
let keys: Vec<Key> = DEPS.with(|d| {
d.borrow()
.get(&scope_id)
.map(|inner| inner.keys().cloned().collect())
.unwrap_or_default()
});
for k in keys {
trigger(scope_id, k.as_ref());
}
}
pub fn batch<R>(f: impl FnOnce() -> R) -> R {
BATCHING.with(|b| b.set(b.get() + 1));
let out = f();
let remaining = BATCHING.with(|b| {
let n = b.get() - 1;
b.set(n);
n
});
if remaining == 0 {
let pending = QUEUE.with(|q| !q.borrow().is_empty());
if pending {
schedule_flush();
}
}
out
}
fn schedule_flush() {
if !AUTO_FLUSH.with(|a| a.get()) {
return;
}
if FLUSH_SCHEDULED.with(|f| f.get()) {
return;
}
FLUSH_SCHEDULED.with(|f| f.set(true));
wasm_bindgen_futures::spawn_local(async {
let _ = JsFuture::from(Promise::resolve(&JsValue::NULL)).await;
flush();
});
}
fn flush() {
FLUSH_SCHEDULED.with(|f| f.set(false));
let ids: Vec<EffectId> = QUEUE.with(|q| q.borrow_mut().drain().collect());
for id in ids {
let f = EFFECTS.with(|e| e.borrow().get(&id).cloned());
if let Some(f) = f {
run_effect(id, &f);
}
}
}
pub fn run_now(id: EffectId) {
let f = EFFECTS.with(|e| e.borrow().get(&id).cloned());
if let Some(f) = f {
run_effect(id, &f);
}
}
pub fn flush_sync() {
flush();
}
#[cfg(any(debug_assertions, feature = "devtools"))]
pub fn stats() -> (usize, usize) {
let dep_count = DEPS.with(|d| d.borrow().values().map(|inner| inner.len()).sum::<usize>());
(EFFECTS.with(|e| e.borrow().len()), dep_count)
}
#[cfg(feature = "devtools")]
pub fn queue_snapshot() -> Vec<EffectId> {
QUEUE.with(|q| q.borrow().iter().copied().collect())
}
#[cfg(feature = "devtools")]
#[derive(Debug, Clone)]
pub struct SignalSnapshot {
pub id: SignalId,
pub subscribers: usize,
}
#[cfg(feature = "devtools")]
pub fn signal_graph_snapshot() -> Vec<SignalSnapshot> {
SIGNAL_DEPS.with(|d| {
d.borrow()
.iter()
.map(|(id, subs)| SignalSnapshot {
id: *id,
subscribers: subs.len(),
})
.collect()
})
}
#[cfg(feature = "devtools")]
pub fn is_scheduler_routed(id: EffectId) -> bool {
SCHEDULERS.with(|s| s.borrow().contains_key(&id))
}
#[allow(dead_code)]
fn _unused(_: Closure<dyn FnMut()>) {}