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 the current `Scope`'s memo cache (`Scope::memo`).
143/// The value lives until that `Scope` is disposed (navigation pop, root
144/// shutdown) - not merely until a `scope!` cache key stops composing,
145/// since `scope!` is a memo cache, not a `Scope`.
146pub fn scope_memo<T: 'static>(key: &str, init: impl FnOnce() -> T) -> Rc<T> {
147    match current_scope() {
148        Some(scope) => scope.memo(key, init),
149        None => Rc::new(init()),
150    }
151}
152
153/// Mount-once scoped effect: runs `f` only the first time this call site
154/// composes, registering cleanup on the current scope. Later recompositions
155/// are no-ops. Requires composition context.
156#[track_caller]
157pub fn scoped_effect_once(f: impl FnOnce() -> Dispose + 'static) {
158    let loc = std::panic::Location::caller();
159    let key = format!(
160        "scoped_effect:{}:{}:{}",
161        loc.file(),
162        loc.line(),
163        loc.column()
164    );
165    let installed = crate::remember_with_key(key, || std::cell::Cell::new(false));
166    if !installed.get() {
167        installed.set(true);
168        scoped_effect(f);
169    }
170}
171
172/// Scoped effect that auto-cleans up.
173///
174/// Runs `f()` immediately and registers the returned `Dispose` to run when the
175/// current scope is disposed. This runs on every call - for mount-once
176/// semantics use `scoped_effect_once`, `disposable_effect`, or keyed variants.
177pub fn scoped_effect<F>(f: F)
178where
179    F: FnOnce() -> Dispose + 'static,
180{
181    if let Some(scope) = current_scope() {
182        let cleanup = f();
183        scope.add_disposer(move || cleanup.run());
184    } else {
185        debug_assert!(
186            false,
187            "scoped_effect called without a current Scope; setup skipped so cleanup cannot leak"
188        );
189        log::error!("scoped_effect called without a current Scope; setup skipped");
190    }
191}
192
193impl Drop for ScopeInner {
194    fn drop(&mut self) {
195        if self.disposed.replace(true) {
196            return; // already disposed via explicit dispose() call
197        }
198        let children = std::mem::take(&mut *self.children.borrow_mut());
199        for child in children {
200            let res = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| drop(child)));
201            if let Err(e) = res {
202                log::error!(
203                    "ScopeInner drop child panicked: {}",
204                    e.downcast_ref::<String>()
205                        .map(|s| s.as_str())
206                        .or_else(|| e.downcast_ref::<&str>().copied())
207                        .unwrap_or("unknown")
208                );
209            }
210        }
211
212        let disposers = std::mem::take(&mut *self.disposers.borrow_mut());
213        for disposer in disposers {
214            let res = std::panic::catch_unwind(std::panic::AssertUnwindSafe(disposer));
215            if let Err(e) = res {
216                log::error!(
217                    "ScopeInner drop disposer panicked: {}",
218                    e.downcast_ref::<String>()
219                        .map(|s| s.as_str())
220                        .or_else(|| e.downcast_ref::<&str>().copied())
221                        .unwrap_or("unknown")
222                );
223            }
224        }
225    }
226}