Skip to main content

repose_core/
scope.rs

1use std::any::Any;
2use std::cell::{Cell, RefCell};
3use std::rc::{Rc, Weak};
4
5use rustc_hash::FxHashMap;
6
7use crate::effects::Dispose;
8
9thread_local! {
10    static CURRENT_SCOPE: RefCell<Option<Weak<ScopeInner>>> = const { RefCell::new(None) };
11}
12
13pub struct Scope {
14    inner: Rc<ScopeInner>,
15}
16
17struct ScopeInner {
18    disposers: RefCell<Vec<Box<dyn FnOnce()>>>,
19    children: RefCell<Vec<Scope>>,
20    memo_cache: RefCell<FxHashMap<String, Box<dyn Any>>>,
21    disposed: Cell<bool>,
22}
23
24impl Default for Scope {
25    fn default() -> Self {
26        Self::new()
27    }
28}
29
30impl Scope {
31    pub fn new() -> Self {
32        Self {
33            inner: Rc::new(ScopeInner {
34                disposers: RefCell::new(Vec::new()),
35                children: RefCell::new(Vec::new()),
36                memo_cache: RefCell::new(FxHashMap::default()),
37                disposed: Cell::new(false),
38            }),
39        }
40    }
41
42    pub fn run<R>(&self, f: impl FnOnce() -> R) -> R {
43        CURRENT_SCOPE.with(|current| {
44            let prev = current.borrow().clone();
45            *current.borrow_mut() = Some(Rc::downgrade(&self.inner));
46            let result = f();
47            *current.borrow_mut() = prev;
48            result
49        })
50    }
51
52    pub fn add_disposer(&self, disposer: impl FnOnce() + 'static) {
53        self.inner.disposers.borrow_mut().push(Box::new(disposer));
54    }
55
56    /// Returns a cached value from this scope's memo cache, or creates it with
57    /// `init` and stores it. The value persists for the lifetime of this scope
58    /// (i.e., until the scope key is no longer composed or the root is replaced).
59    pub fn memo<T: 'static>(&self, key: &str, init: impl FnOnce() -> T) -> Rc<T> {
60        let mut cache = self.inner.memo_cache.borrow_mut();
61        if let Some(existing) = cache.get(key)
62            && let Some(v) = existing.downcast_ref::<Rc<T>>()
63        {
64            return v.clone();
65        }
66        let val: Rc<T> = Rc::new(init());
67        cache.insert(key.to_string(), Box::new(val.clone()));
68        val
69    }
70
71    pub fn child(&self) -> Scope {
72        let child = Scope::new();
73        self.inner.children.borrow_mut().push(child.clone());
74        child
75    }
76
77    pub fn dispose(self) {
78        if self.inner.disposed.replace(true) {
79            return; // already disposed (or being dropped)
80        }
81        // Dispose children first
82        let children = std::mem::take(&mut *self.inner.children.borrow_mut());
83        for child in children {
84            let res = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| child.dispose()));
85            if let Err(e) = res {
86                let msg = e
87                    .downcast_ref::<String>()
88                    .map(|s| s.as_str())
89                    .or_else(|| e.downcast_ref::<&str>().copied())
90                    .unwrap_or("unknown");
91                log::error!("Scope child dispose panicked: {msg}");
92            }
93        }
94
95        let disposers = std::mem::take(&mut *self.inner.disposers.borrow_mut());
96        for disposer in disposers {
97            let res = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| disposer()));
98            if let Err(e) = res {
99                let msg = e
100                    .downcast_ref::<String>()
101                    .map(|s| s.as_str())
102                    .or_else(|| e.downcast_ref::<&str>().copied())
103                    .unwrap_or("unknown");
104                log::error!("Scope disposer panicked: {msg}");
105            }
106        }
107    }
108}
109
110impl Clone for Scope {
111    fn clone(&self) -> Self {
112        Self {
113            inner: self.inner.clone(),
114        }
115    }
116}
117
118pub fn current_scope() -> Option<Scope> {
119    CURRENT_SCOPE.with(|current| {
120        current
121            .borrow()
122            .as_ref()
123            .and_then(|weak| weak.upgrade().map(|inner| Scope { inner }))
124    })
125}
126
127/// Access this scope's memo cache from anywhere inside a `scope!` body.
128/// Returns the cached value for `key`, or creates it with `init` and stores it.
129/// The value persists until the scope is disposed.
130///
131/// Unlike `remember_with_key`, this is scoped to the current composition scope
132/// and is automatically cleaned up when the scope is no longer composed.
133pub fn scope_memo<T: 'static>(key: &str, init: impl FnOnce() -> T) -> Rc<T> {
134    match current_scope() {
135        Some(scope) => scope.memo(key, init),
136        None => Rc::new(init()),
137    }
138}
139
140/// Scoped effect that auto-cleans up.
141///
142/// Runs `f()` immediately and registers the returned `Dispose` to run when the
143/// current scope is disposed.
144pub fn scoped_effect<F>(f: F)
145where
146    F: FnOnce() -> Dispose + 'static,
147{
148    if let Some(scope) = current_scope() {
149        let cleanup = f();
150        scope.add_disposer(move || cleanup.run());
151    } else {
152        // No scope, run setup now, but drop cleanup (legacy "leak" behavior).
153        let _cleanup = f();
154    }
155}
156
157impl Drop for ScopeInner {
158    fn drop(&mut self) {
159        if self.disposed.replace(true) {
160            return; // already disposed via explicit dispose() call
161        }
162        let children = std::mem::take(&mut *self.children.borrow_mut());
163        for child in children {
164            let res = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| drop(child)));
165            if let Err(e) = res {
166                log::error!(
167                    "ScopeInner drop child panicked: {}",
168                    e.downcast_ref::<String>()
169                        .map(|s| s.as_str())
170                        .or_else(|| e.downcast_ref::<&str>().copied())
171                        .unwrap_or("unknown")
172                );
173            }
174        }
175
176        let disposers = std::mem::take(&mut *self.disposers.borrow_mut());
177        for disposer in disposers {
178            let res = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| disposer()));
179            if let Err(e) = res {
180                log::error!(
181                    "ScopeInner drop disposer panicked: {}",
182                    e.downcast_ref::<String>()
183                        .map(|s| s.as_str())
184                        .or_else(|| e.downcast_ref::<&str>().copied())
185                        .unwrap_or("unknown")
186                );
187            }
188        }
189    }
190}