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