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        struct Guard {
44            prev: Option<Weak<ScopeInner>>,
45        }
46        impl Drop for Guard {
47            fn drop(&mut self) {
48                CURRENT_SCOPE.with(|current| {
49                    if let Ok(mut b) = current.try_borrow_mut() {
50                        *b = self.prev.take();
51                    } else {
52                        log::error!(
53                            "scope: CURRENT_SCOPE busy during scope exit; stale scope reference retained"
54                        );
55                    }
56                });
57            }
58        }
59        let prev = CURRENT_SCOPE.with(|current| current.borrow().clone());
60        CURRENT_SCOPE.with(|current| {
61            *current.borrow_mut() = Some(Rc::downgrade(&self.inner));
62        });
63        let _guard = Guard { prev };
64        f()
65    }
66
67    pub fn add_disposer(&self, disposer: impl FnOnce() + 'static) {
68        self.inner.disposers.borrow_mut().push(Box::new(disposer));
69    }
70
71    /// Returns a cached value from this scope's memo cache, or creates it with
72    /// `init` and stores it. The value persists for the lifetime of this scope
73    /// (i.e., until the scope key is no longer composed or the root is replaced).
74    pub fn memo<T: 'static>(&self, key: &str, init: impl FnOnce() -> T) -> Rc<T> {
75        let mut cache = self.inner.memo_cache.borrow_mut();
76        if let Some(existing) = cache.get(key)
77            && let Some(v) = existing.downcast_ref::<Rc<T>>()
78        {
79            return v.clone();
80        }
81        let val: Rc<T> = Rc::new(init());
82        cache.insert(key.to_string(), Box::new(val.clone()));
83        val
84    }
85
86    pub fn child(&self) -> Scope {
87        let child = Scope::new();
88        self.inner.children.borrow_mut().push(child.clone());
89        child
90    }
91
92    pub fn dispose(self) {
93        if self.inner.disposed.replace(true) {
94            return; // already disposed (or being dropped)
95        }
96        // Dispose children first
97        let children = std::mem::take(&mut *self.inner.children.borrow_mut());
98        for child in children {
99            let res = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| child.dispose()));
100            if let Err(e) = res {
101                let msg = e
102                    .downcast_ref::<String>()
103                    .map(|s| s.as_str())
104                    .or_else(|| e.downcast_ref::<&str>().copied())
105                    .unwrap_or("unknown");
106                log::error!("Scope child dispose panicked: {msg}");
107            }
108        }
109
110        let disposers = std::mem::take(&mut *self.inner.disposers.borrow_mut());
111        for disposer in disposers {
112            let res = std::panic::catch_unwind(std::panic::AssertUnwindSafe(disposer));
113            if let Err(e) = res {
114                let msg = e
115                    .downcast_ref::<String>()
116                    .map(|s| s.as_str())
117                    .or_else(|| e.downcast_ref::<&str>().copied())
118                    .unwrap_or("unknown");
119                log::error!("Scope disposer panicked: {msg}");
120            }
121        }
122    }
123}
124
125impl Clone for Scope {
126    fn clone(&self) -> Self {
127        Self {
128            inner: self.inner.clone(),
129        }
130    }
131}
132
133pub fn current_scope() -> Option<Scope> {
134    CURRENT_SCOPE.with(|current| {
135        current
136            .borrow()
137            .as_ref()
138            .and_then(|weak| weak.upgrade().map(|inner| Scope { inner }))
139    })
140}
141
142/// Access this scope's memo cache from anywhere inside a `scope!` body.
143/// Returns the cached value for `key`, or creates it with `init` and stores it.
144/// The value persists until the scope is disposed.
145///
146/// Unlike `remember_with_key`, this is scoped to the current composition scope
147/// and is automatically cleaned up when the scope is no longer composed.
148pub fn scope_memo<T: 'static>(key: &str, init: impl FnOnce() -> T) -> Rc<T> {
149    match current_scope() {
150        Some(scope) => scope.memo(key, init),
151        None => Rc::new(init()),
152    }
153}
154
155/// Scoped effect that auto-cleans up.
156///
157/// Runs `f()` immediately and registers the returned `Dispose` to run when the
158/// current scope is disposed.
159pub fn scoped_effect<F>(f: F)
160where
161    F: FnOnce() -> Dispose + 'static,
162{
163    if let Some(scope) = current_scope() {
164        let cleanup = f();
165        scope.add_disposer(move || cleanup.run());
166    } else {
167        // No scope, run setup now, but drop cleanup (legacy "leak" behavior).
168        let _cleanup = f();
169    }
170}
171
172impl Drop for ScopeInner {
173    fn drop(&mut self) {
174        if self.disposed.replace(true) {
175            return; // already disposed via explicit dispose() call
176        }
177        let children = std::mem::take(&mut *self.children.borrow_mut());
178        for child in children {
179            let res = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| drop(child)));
180            if let Err(e) = res {
181                log::error!(
182                    "ScopeInner drop child panicked: {}",
183                    e.downcast_ref::<String>()
184                        .map(|s| s.as_str())
185                        .or_else(|| e.downcast_ref::<&str>().copied())
186                        .unwrap_or("unknown")
187                );
188            }
189        }
190
191        let disposers = std::mem::take(&mut *self.disposers.borrow_mut());
192        for disposer in disposers {
193            let res = std::panic::catch_unwind(std::panic::AssertUnwindSafe(disposer));
194            if let Err(e) = res {
195                log::error!(
196                    "ScopeInner drop disposer panicked: {}",
197                    e.downcast_ref::<String>()
198                        .map(|s| s.as_str())
199                        .or_else(|| e.downcast_ref::<&str>().copied())
200                        .unwrap_or("unknown")
201                );
202            }
203        }
204    }
205}