Skip to main content

repose_core/
scope_cache.rs

1use std::cell::RefCell;
2use std::collections::HashMap;
3
4use crate::View;
5
6thread_local! {
7    /// The scope key currently being composed (set by `scope!`).
8    static CURRENT_SCOPE_KEY: RefCell<Option<String>> =
9        const { RefCell::new(None) };
10
11    /// signal_id → set of scope keys that read it during composition.
12    /// Cleaned up when a scope re-executes (old deps are replaced) or when
13    /// the app disposes.
14    static SCOPE_SIGNAL_DEPS: RefCell<HashMap<usize, Vec<String>>> =
15        RefCell::new(HashMap::new());
16}
17
18/// Record that the current composition scope (if any) depends on `signal_id`.
19/// Called from `reactive::register_signal_read`.
20pub fn record_scope_signal_dep(signal_id: usize) {
21    CURRENT_SCOPE_KEY.with(|key| {
22        if let Some(ref key) = *key.borrow() {
23            SCOPE_SIGNAL_DEPS.with(|deps| {
24                let mut deps = deps.borrow_mut();
25                deps.entry(signal_id).or_default().push(key.clone());
26            });
27        }
28    });
29}
30
31/// Mark all scopes that depend on `signal_id` as dirty.
32/// Called from `reactive::signal_changed`.
33pub fn mark_scope_deps_dirty(signal_id: usize) {
34    SCOPE_SIGNAL_DEPS.with(|deps| {
35        let deps = deps.borrow();
36        if let Some(keys) = deps.get(&signal_id) {
37            for key in keys {
38                crate::runtime::COMPOSER.with(|c| {
39                    let mut c = c.borrow_mut();
40                    if let Some(cache) = c.scope_caches.get_mut(key) {
41                        cache.clean = false;
42                    }
43                });
44            }
45        }
46    });
47}
48
49/// Run `f` with the given scope key tracking any signal reads inside.
50pub fn with_scope_key<R>(key: &str, f: impl FnOnce() -> R) -> R {
51    CURRENT_SCOPE_KEY.with(|k| {
52        let prev = k.borrow_mut().take();
53        *k.borrow_mut() = Some(key.to_string());
54        let result = f();
55        *k.borrow_mut() = prev;
56        result
57    })
58}
59
60/// Clear all signal→scope tracking for the given scope key.
61/// Called after the scope body executes, so old deps from a previous run are
62/// replaced by the new deps registered during the just-completed run.
63pub fn clear_scope_deps(key: &str) {
64    SCOPE_SIGNAL_DEPS.with(|deps| {
65        let mut deps = deps.borrow_mut();
66        deps.retain(|_, keys| {
67            keys.retain(|k| k != key);
68            !keys.is_empty()
69        });
70    });
71}
72
73/// Cached state for a single `scope!` invocation.
74pub struct ScopeCache {
75    /// Combined hash of all scope inputs from the last execution.
76    pub input_hash: u64,
77    /// The cached View tree produced by the last execution.
78    pub view: View,
79    /// How many `remember` slots the body consumed.
80    pub slot_delta: usize,
81    /// `true` if cached output is valid (no signal deps invalidated, inputs unchanged).
82    pub clean: bool,
83}
84
85/// Check whether a scope should re-execute.
86pub fn should_run(key: &str, input_hash: u64) -> bool {
87    crate::runtime::COMPOSER.with(|c| {
88        let c = c.borrow();
89        match c.scope_caches.get(key) {
90            Some(cache) => !cache.clean || cache.input_hash != input_hash,
91            None => true,
92        }
93    })
94}
95
96/// Retrieve the cached View for a scope being skipped, advancing the remember-slot
97/// cursor so sibling scopes remain consistent. IDs are self-contained in the cached
98/// View (packed scope-local IDs), so no global ID advance is needed.
99pub fn get_cached(key: &str, _s: &mut crate::runtime::Scheduler) -> View {
100    crate::runtime::COMPOSER.with(|c| {
101        let mut c = c.borrow_mut();
102        let (slot_delta, view) = {
103            let cache = c
104                .scope_caches
105                .get(key)
106                .expect("scope_cache::get_cached called but no cache entry found");
107            (cache.slot_delta, cache.view.clone())
108        };
109
110        c.cursor += slot_delta;
111        view
112    })
113}
114
115/// Store a new or updated cache entry after executing the scope body.
116pub fn set_cache(key: &str, input_hash: u64, view: View, slot_delta: usize) {
117    crate::runtime::COMPOSER.with(|c| {
118        let mut c = c.borrow_mut();
119        c.scope_caches.insert(
120            key.to_string(),
121            ScopeCache {
122                input_hash,
123                view,
124                slot_delta,
125                clean: true,
126            },
127        );
128    });
129}