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        if self.inner.disposed.get() {
69            let res = std::panic::catch_unwind(std::panic::AssertUnwindSafe(disposer));
70            if let Err(e) = res {
71                let msg = e
72                    .downcast_ref::<String>()
73                    .map(|s| s.as_str())
74                    .or_else(|| e.downcast_ref::<&str>().copied())
75                    .unwrap_or("unknown");
76                log::error!("Scope disposer after disposal panicked: {msg}");
77            }
78            return;
79        }
80        self.inner.disposers.borrow_mut().push(Box::new(disposer));
81    }
82
83    /// Returns a cached value from this scope's memo cache, or creates it with
84    /// `init` and stores it. The value persists until this `Scope` is disposed.
85    pub fn memo<T: 'static>(&self, key: &str, init: impl FnOnce() -> T) -> Rc<T> {
86        if let Some(existing) = self.inner.memo_cache.borrow().get(key)
87            && let Some(v) = existing.downcast_ref::<Rc<T>>()
88        {
89            return v.clone();
90        }
91        let val: Rc<T> = Rc::new(init());
92        if !self.inner.disposed.get() {
93            let old = {
94                let mut cache = self.inner.memo_cache.borrow_mut();
95                cache.insert(key.to_string(), Box::new(val.clone()))
96            };
97            drop(old);
98        }
99        val
100    }
101
102    pub fn child(&self) -> Scope {
103        let child = Scope::new();
104        if self.inner.disposed.get() {
105            child.inner.disposed.set(true);
106        } else {
107            self.inner.children.borrow_mut().push(child.clone());
108        }
109        child
110    }
111
112    pub fn dispose(self) {
113        if self.inner.disposed.replace(true) {
114            return; // already disposed (or being dropped)
115        }
116        // Dispose children first
117        let children = std::mem::take(&mut *self.inner.children.borrow_mut());
118        for child in children.into_iter().rev() {
119            let res = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| child.dispose()));
120            if let Err(e) = res {
121                let msg = e
122                    .downcast_ref::<String>()
123                    .map(|s| s.as_str())
124                    .or_else(|| e.downcast_ref::<&str>().copied())
125                    .unwrap_or("unknown");
126                log::error!("Scope child dispose panicked: {msg}");
127            }
128        }
129
130        let disposers = std::mem::take(&mut *self.inner.disposers.borrow_mut());
131        for disposer in disposers.into_iter().rev() {
132            let res = std::panic::catch_unwind(std::panic::AssertUnwindSafe(disposer));
133            if let Err(e) = res {
134                let msg = e
135                    .downcast_ref::<String>()
136                    .map(|s| s.as_str())
137                    .or_else(|| e.downcast_ref::<&str>().copied())
138                    .unwrap_or("unknown");
139                log::error!("Scope disposer panicked: {msg}");
140            }
141        }
142        let memo = std::mem::take(&mut *self.inner.memo_cache.borrow_mut());
143        drop(memo);
144    }
145}
146
147impl Clone for Scope {
148    fn clone(&self) -> Self {
149        Self {
150            inner: self.inner.clone(),
151        }
152    }
153}
154
155pub fn current_scope() -> Option<Scope> {
156    CURRENT_SCOPE.with(|current| {
157        current
158            .borrow()
159            .as_ref()
160            .and_then(|weak| weak.upgrade().map(|inner| Scope { inner }))
161    })
162}
163
164/// Access the current `Scope`'s memo cache (`Scope::memo`).
165/// The value lives until that `Scope` is disposed (navigation pop, root
166/// shutdown) - not merely until a `scope!` cache key stops composing,
167/// since `scope!` is a memo cache, not a `Scope`.
168pub fn scope_memo<T: 'static>(key: &str, init: impl FnOnce() -> T) -> Rc<T> {
169    match current_scope() {
170        Some(scope) => scope.memo(key, init),
171        None => Rc::new(init()),
172    }
173}
174
175/// Mount-once scoped effect: runs `f` only the first time this call site
176/// composes, registering cleanup on the current scope. Later recompositions
177/// are no-ops. Requires composition context.
178#[track_caller]
179pub fn scoped_effect_once(f: impl FnOnce() -> Dispose + 'static) {
180    let loc = std::panic::Location::caller();
181    let mut key = format!(
182        "scoped_effect:{}:{}:{}",
183        loc.file(),
184        loc.line(),
185        loc.column()
186    );
187    if let Some(identity) = crate::shortcuts::active_runtime_identity() {
188        key.push_str(&format!(":runtime:{identity}"));
189    }
190    if current_scope().is_none() {
191        debug_assert!(
192            false,
193            "scoped_effect called without a current Scope; setup skipped so cleanup cannot leak"
194        );
195        log::error!("scoped_effect called without a current Scope; setup skipped");
196        return;
197    }
198    crate::effects::effect_once_with_key(key, f);
199}
200
201/// Scoped effect that auto-cleans up.
202///
203/// Runs `f()` immediately and registers the returned `Dispose` to run when the
204/// current scope is disposed. This runs on every call - for mount-once
205/// semantics use `scoped_effect_once`, `disposable_effect`, or keyed variants.
206pub fn scoped_effect<F>(f: F)
207where
208    F: FnOnce() -> Dispose + 'static,
209{
210    if let Some(scope) = current_scope() {
211        let cleanup = f();
212        scope.add_disposer(move || cleanup.run());
213    } else {
214        debug_assert!(
215            false,
216            "scoped_effect called without a current Scope; setup skipped so cleanup cannot leak"
217        );
218        log::error!("scoped_effect called without a current Scope; setup skipped");
219    }
220}
221
222impl Drop for ScopeInner {
223    fn drop(&mut self) {
224        if self.disposed.replace(true) {
225            return; // already disposed via explicit dispose() call
226        }
227        let children = std::mem::take(&mut *self.children.borrow_mut());
228        for child in children.into_iter().rev() {
229            let res = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| drop(child)));
230            if let Err(e) = res {
231                log::error!(
232                    "ScopeInner drop child panicked: {}",
233                    e.downcast_ref::<String>()
234                        .map(|s| s.as_str())
235                        .or_else(|| e.downcast_ref::<&str>().copied())
236                        .unwrap_or("unknown")
237                );
238            }
239        }
240
241        let disposers = std::mem::take(&mut *self.disposers.borrow_mut());
242        for disposer in disposers.into_iter().rev() {
243            let res = std::panic::catch_unwind(std::panic::AssertUnwindSafe(disposer));
244            if let Err(e) = res {
245                log::error!(
246                    "ScopeInner drop disposer panicked: {}",
247                    e.downcast_ref::<String>()
248                        .map(|s| s.as_str())
249                        .or_else(|| e.downcast_ref::<&str>().copied())
250                        .unwrap_or("unknown")
251                );
252            }
253        }
254        let memo = std::mem::take(&mut *self.memo_cache.borrow_mut());
255        drop(memo);
256    }
257}