Skip to main content

repose_core/
scope_cache.rs

1use std::cell::RefCell;
2
3use rustc_hash::{FxHashMap, FxHashSet};
4
5use crate::View;
6
7struct ScopeFrame {
8    key: String,
9    children: FxHashSet<String>,
10    locals: FxHashMap<usize, u64>,
11    animations: FxHashSet<String>,
12}
13
14thread_local! {
15    static CURRENT_SCOPE_STACK: RefCell<Vec<String>> = const { RefCell::new(Vec::new()) };
16    static CURRENT_SCOPE_KEY: RefCell<Option<String>> = const { RefCell::new(None) };
17    static SCOPE_SIGNAL_DEPS: RefCell<FxHashMap<usize, FxHashSet<String>>> =
18        RefCell::new(FxHashMap::default());
19    static SCOPE_TO_SIGNALS: RefCell<FxHashMap<String, FxHashSet<usize>>> =
20        RefCell::new(FxHashMap::default());
21    static SCOPE_DIRTY_EPOCHS: RefCell<FxHashMap<String, u64>> =
22        RefCell::new(FxHashMap::default());
23    static SCOPE_PENDING_DIRTY: RefCell<FxHashSet<String>> =
24        RefCell::new(FxHashSet::default());
25    static SCOPE_CACHE_CHILDREN: RefCell<FxHashMap<String, FxHashSet<String>>> =
26        RefCell::new(FxHashMap::default());
27    static SCOPE_CACHE_LOCALS: RefCell<FxHashMap<String, FxHashMap<usize, u64>>> =
28        RefCell::new(FxHashMap::default());
29    static SCOPE_CACHE_ANIMATIONS: RefCell<FxHashMap<String, FxHashSet<String>>> =
30        RefCell::new(FxHashMap::default());
31    static SCOPE_LAST_CHILDREN: RefCell<FxHashMap<String, FxHashSet<String>>> =
32        RefCell::new(FxHashMap::default());
33    static SCOPE_LAST_LOCALS: RefCell<FxHashMap<String, FxHashMap<usize, u64>>> =
34        RefCell::new(FxHashMap::default());
35    static SCOPE_LAST_ANIMATIONS: RefCell<FxHashMap<String, FxHashSet<String>>> =
36        RefCell::new(FxHashMap::default());
37    static SCOPE_RUN_EPOCHS: RefCell<FxHashMap<String, u64>> =
38        RefCell::new(FxHashMap::default());
39    static SCOPE_FRAMES: RefCell<Vec<ScopeFrame>> = const { RefCell::new(Vec::new()) };
40}
41
42fn current_scope_stack() -> Vec<String> {
43    let stack = CURRENT_SCOPE_STACK.with(|stack| stack.borrow().clone());
44    if stack.is_empty() {
45        CURRENT_SCOPE_KEY
46            .with(|key| key.borrow().clone())
47            .into_iter()
48            .collect()
49    } else {
50        stack
51    }
52}
53
54fn for_each_current_scope(mut f: impl FnMut(&str)) {
55    let has_stack = CURRENT_SCOPE_STACK.with(|stack| !stack.borrow().is_empty());
56    if has_stack {
57        CURRENT_SCOPE_STACK.with(|stack| {
58            for key in stack.borrow().iter() {
59                f(key);
60            }
61        });
62    } else if let Some(key) = CURRENT_SCOPE_KEY.with(|key| key.borrow().clone()) {
63        f(&key);
64    }
65}
66
67pub fn record_scope_signal_dep(signal: usize) {
68    for_each_current_scope(|key| {
69        SCOPE_SIGNAL_DEPS.with(|deps| {
70            deps.borrow_mut()
71                .entry(signal)
72                .or_default()
73                .insert(key.to_string());
74        });
75        SCOPE_TO_SIGNALS.with(|map| {
76            map.borrow_mut()
77                .entry(key.to_string())
78                .or_default()
79                .insert(signal);
80        });
81    });
82}
83
84fn mark_dirty(key: &str) {
85    SCOPE_DIRTY_EPOCHS.with(|epochs| {
86        let mut epochs = epochs.borrow_mut();
87        let epoch = epochs.entry(key.to_string()).or_insert(0);
88        *epoch = epoch.wrapping_add(1);
89    });
90    let marked = crate::runtime::COMPOSER
91        .try_with(|composer| {
92            composer
93                .try_borrow_mut()
94                .ok()
95                .map(|mut composer| {
96                    if let Some(cache) = composer.scope_caches.get_mut(key) {
97                        cache.clean = false;
98                    }
99                })
100                .is_some()
101        })
102        .unwrap_or(false);
103    if !marked {
104        let _ = SCOPE_PENDING_DIRTY.try_with(|pending| {
105            if let Ok(mut pending) = pending.try_borrow_mut() {
106                pending.insert(key.to_string());
107                true
108            } else {
109                false
110            }
111        });
112        crate::request_frame();
113    }
114}
115
116pub fn mark_current_scope_dirty() {
117    for_each_current_scope(mark_dirty);
118}
119
120pub fn mark_current_scope_dirty_for_signal(signal: usize) {
121    for_each_current_scope(|key| {
122        let dependent = SCOPE_SIGNAL_DEPS.with(|deps| {
123            deps.borrow()
124                .get(&signal)
125                .is_some_and(|keys| keys.contains(key))
126        });
127        if !dependent {
128            mark_dirty(key);
129        }
130    });
131}
132
133pub fn mark_scope_dirty(key: &str) {
134    mark_dirty(key);
135}
136
137pub fn mark_scope_deps_dirty(signal: usize) {
138    SCOPE_SIGNAL_DEPS.with(|deps| {
139        let deps = deps.borrow();
140        if let Some(keys) = deps.get(&signal) {
141            for key in keys {
142                mark_dirty(key);
143            }
144        }
145    });
146}
147
148pub fn with_scope_key<R>(key: &str, function: impl FnOnce() -> R) -> R {
149    struct Guard {
150        key: String,
151    }
152
153    impl Drop for Guard {
154        fn drop(&mut self) {
155            let frame = SCOPE_FRAMES
156                .try_with(|frames| match frames.try_borrow_mut() {
157                    Ok(mut frames) => frames
158                        .iter()
159                        .rposition(|frame| frame.key == self.key)
160                        .map(|index| frames.remove(index)),
161                    Err(_) => None,
162                })
163                .ok()
164                .flatten();
165            if let Some(frame) = frame {
166                let _ = SCOPE_LAST_CHILDREN.try_with(|map| {
167                    if let Ok(mut map) = map.try_borrow_mut() {
168                        map.insert(self.key.clone(), frame.children);
169                    }
170                });
171                let _ = SCOPE_LAST_LOCALS.try_with(|map| {
172                    if let Ok(mut map) = map.try_borrow_mut() {
173                        map.insert(self.key.clone(), frame.locals);
174                    }
175                });
176                let _ = SCOPE_LAST_ANIMATIONS.try_with(|map| {
177                    if let Ok(mut map) = map.try_borrow_mut() {
178                        map.insert(self.key.clone(), frame.animations);
179                    }
180                });
181            }
182            let _ = CURRENT_SCOPE_STACK.try_with(|stack| {
183                if let Ok(mut stack) = stack.try_borrow_mut()
184                    && stack.last() == Some(&self.key)
185                {
186                    stack.pop();
187                }
188            });
189            let top = CURRENT_SCOPE_STACK
190                .try_with(|stack| {
191                    stack
192                        .try_borrow()
193                        .ok()
194                        .and_then(|stack| stack.last().cloned())
195                })
196                .ok()
197                .flatten();
198            let _ = CURRENT_SCOPE_KEY.try_with(|current| {
199                if let Ok(mut current) = current.try_borrow_mut() {
200                    *current = top;
201                }
202            });
203        }
204    }
205
206    let key = key.to_string();
207    let parent = CURRENT_SCOPE_STACK.with(|stack| stack.borrow().last().cloned());
208    if let Some(parent) = parent {
209        let _ = SCOPE_FRAMES.try_with(|frames| {
210            if let Ok(mut frames) = frames.try_borrow_mut()
211                && let Some(frame) = frames.last_mut()
212                && frame.key == parent
213            {
214                frame.children.insert(key.clone());
215            }
216        });
217    }
218    CURRENT_SCOPE_STACK.with(|stack| stack.borrow_mut().push(key.clone()));
219    CURRENT_SCOPE_KEY.with(|current| *current.borrow_mut() = Some(key.clone()));
220    SCOPE_FRAMES.with(|frames| {
221        frames.borrow_mut().push(ScopeFrame {
222            key: key.clone(),
223            children: FxHashSet::default(),
224            locals: FxHashMap::default(),
225            animations: FxHashSet::default(),
226        })
227    });
228    let _guard = Guard { key };
229    function()
230}
231
232pub fn record_scope_local_read(local: usize, value: u64) {
233    let _ = SCOPE_FRAMES.try_with(|frames| {
234        if let Ok(mut frames) = frames.try_borrow_mut()
235            && let Some(frame) = frames.last_mut()
236        {
237            frame.locals.insert(local, value);
238        }
239    });
240}
241
242pub fn record_scope_animation_key(key: &str) {
243    let _ = SCOPE_FRAMES.try_with(|frames| {
244        if let Ok(mut frames) = frames.try_borrow_mut()
245            && let Some(frame) = frames.last_mut()
246        {
247            frame.animations.insert(key.to_string());
248        }
249    });
250}
251
252pub fn clear_scope_deps(key: &str) {
253    let signals = SCOPE_TO_SIGNALS.with(|map| map.borrow_mut().remove(key));
254    if let Some(signals) = signals {
255        SCOPE_SIGNAL_DEPS.with(|deps| {
256            let mut deps = deps.borrow_mut();
257            for signal in signals {
258                if let Some(scopes) = deps.get_mut(&signal) {
259                    scopes.remove(key);
260                    if scopes.is_empty() {
261                        deps.remove(&signal);
262                    }
263                }
264            }
265        });
266    }
267    let epoch = SCOPE_DIRTY_EPOCHS.with(|epochs| epochs.borrow().get(key).copied().unwrap_or(0));
268    SCOPE_RUN_EPOCHS.with(|epochs| epochs.borrow_mut().insert(key.to_string(), epoch));
269}
270
271pub struct ScopeCache {
272    pub input_hash: u64,
273    pub view: View,
274    pub slot_delta: usize,
275    pub clean: bool,
276}
277
278fn collect_view_scope_keys(view: &View, keys: &mut FxHashSet<String>) {
279    if let Some(key) = &view.scope_key {
280        keys.insert(key.clone());
281    }
282    for child in &view.children {
283        collect_view_scope_keys(child, keys);
284    }
285}
286
287fn view_has_nested_scope(view: &View, parent_key: &str) -> bool {
288    if view
289        .scope_key
290        .as_deref()
291        .is_some_and(|scope_key| scope_key != parent_key)
292    {
293        return true;
294    }
295    view.children
296        .iter()
297        .any(|child| view_has_nested_scope(child, parent_key))
298}
299
300fn has_cached_scope_children(key: &str) -> bool {
301    let cached = SCOPE_CACHE_CHILDREN.with(|map| {
302        map.borrow()
303            .get(key)
304            .is_some_and(|children| !children.is_empty())
305    });
306    if cached {
307        return true;
308    }
309    crate::runtime::COMPOSER
310        .try_with(|composer| {
311            composer.try_borrow().ok().is_some_and(|composer| {
312                composer
313                    .scope_caches
314                    .get(key)
315                    .is_some_and(|cache| view_has_nested_scope(&cache.view, key))
316            })
317        })
318        .ok()
319        .unwrap_or(false)
320}
321
322fn cached_scope_children(key: &str) -> FxHashSet<String> {
323    let mut children =
324        SCOPE_CACHE_CHILDREN.with(|map| map.borrow().get(key).cloned().unwrap_or_default());
325    if children.is_empty() {
326        let _ = crate::runtime::COMPOSER.try_with(|composer| {
327            if let Ok(composer) = composer.try_borrow()
328                && let Some(cache) = composer.scope_caches.get(key)
329            {
330                let mut keys = FxHashSet::default();
331                collect_view_scope_keys(&cache.view, &mut keys);
332                keys.remove(key);
333                children.extend(keys);
334            }
335        });
336    }
337    children
338}
339
340fn cached_locals_changed(key: &str) -> bool {
341    SCOPE_CACHE_LOCALS.with(|map| {
342        map.borrow().get(key).is_some_and(|values| {
343            values
344                .iter()
345                .any(|(local, value)| crate::locals::local_fingerprint(*local) != *value)
346        })
347    })
348}
349
350pub fn should_run(key: &str, input_hash: u64) -> bool {
351    if SCOPE_PENDING_DIRTY.with(|pending| pending.borrow().contains(key)) {
352        return true;
353    }
354    if has_cached_scope_children(key) {
355        return true;
356    }
357    let locals_changed = cached_locals_changed(key);
358    crate::runtime::COMPOSER.with(|composer| {
359        let composer = composer.borrow();
360        match composer.scope_caches.get(key) {
361            Some(cache) => !cache.clean || cache.input_hash != input_hash || locals_changed,
362            None => true,
363        }
364    })
365}
366
367pub fn current_scope_key() -> Option<String> {
368    CURRENT_SCOPE_KEY.with(|key| key.borrow().clone())
369}
370
371pub fn validate_scope_key(key: &str) {
372    assert!(!key.is_empty(), "scope keys must not be empty");
373    assert!(
374        crate::runtime::scope_owner_token(Some(key)) != crate::runtime::scope_owner_token(None),
375        "scope keys must not use the private root owner sentinel"
376    );
377}
378
379fn record_cached_child(key: &str) {
380    let _ = SCOPE_FRAMES.try_with(|frames| {
381        if let Ok(mut frames) = frames.try_borrow_mut()
382            && let Some(frame) = frames.last_mut()
383        {
384            frame.children.insert(key.to_string());
385        }
386    });
387}
388
389fn restore_cached_deps(key: &str) {
390    fn visit(key: &str, ancestors: &[String], seen: &mut FxHashSet<String>) {
391        if !seen.insert(key.to_string()) {
392            return;
393        }
394        let mut path = ancestors.to_vec();
395        if !path.iter().any(|entry| entry == key) {
396            path.push(key.to_string());
397        }
398        let signals = SCOPE_TO_SIGNALS.with(|map| map.borrow().get(key).cloned());
399        if let Some(signals) = signals {
400            for signal in signals {
401                record_signal_for_keys(signal, &path);
402            }
403        }
404        for child in cached_scope_children(key) {
405            visit(&child, &path, seen);
406        }
407    }
408
409    fn record_signal_for_keys(signal: usize, keys: &[String]) {
410        SCOPE_SIGNAL_DEPS.with(|deps| {
411            let mut deps = deps.borrow_mut();
412            for key in keys {
413                deps.entry(signal).or_default().insert(key.clone());
414            }
415        });
416        SCOPE_TO_SIGNALS.with(|map| {
417            let mut map = map.borrow_mut();
418            for key in keys {
419                map.entry(key.clone()).or_default().insert(signal);
420            }
421        });
422    }
423
424    let ancestors = current_scope_stack();
425    visit(key, &ancestors, &mut FxHashSet::default());
426}
427
428fn mark_scope_live(key: &str) {
429    mark_scopes(key, false);
430}
431
432/// A scope served from cache did not re-run, so the keyed slots its body would
433/// have read were never touched this frame. Record that, so
434/// `take_dead_keyed_slots` can tell "unread because cached" apart from "unread
435/// because abandoned" and only collect the latter.
436fn mark_scope_cache_preserved(key: &str) {
437    mark_scopes(key, true);
438}
439
440fn mark_scopes(key: &str, cache_preserved: bool) {
441    let mut pending = vec![key.to_string()];
442    let mut seen = FxHashSet::default();
443    while let Some(current) = pending.pop() {
444        if !seen.insert(current.clone()) {
445            continue;
446        }
447        let children = cached_scope_children(&current);
448        let exists = crate::runtime::COMPOSER.with(|composer| {
449            let mut composer = composer.borrow_mut();
450            let exists = composer.scope_caches.contains_key(&current);
451            if exists {
452                composer.live_scope_keys.insert(current.clone());
453                composer
454                    .live_keyed_owners
455                    .insert(crate::runtime::scope_owner_token(Some(&current)));
456                if cache_preserved {
457                    composer
458                        .cache_preserved_owners
459                        .insert(crate::runtime::scope_owner_token(Some(&current)));
460                }
461            }
462            exists
463        });
464        if exists {
465            let animations = SCOPE_CACHE_ANIMATIONS
466                .with(|map| map.borrow().get(&current).cloned().unwrap_or_default());
467            for animation in animations {
468                crate::animation_driver::touch_cached(&animation, &current);
469            }
470            pending.extend(children);
471        }
472    }
473}
474
475pub fn get_cached(key: &str, _scheduler: &mut crate::runtime::Scheduler) -> View {
476    let (slot_delta, view) = crate::runtime::COMPOSER.with(|composer| {
477        let composer = composer.borrow();
478        let cache = composer
479            .scope_caches
480            .get(key)
481            .expect("scope_cache::get_cached called but no cache entry found");
482        (cache.slot_delta, cache.view.clone())
483    });
484    crate::runtime::COMPOSER.with(|composer| composer.borrow_mut().cursor += slot_delta);
485    record_cached_child(key);
486    restore_cached_deps(key);
487    mark_scope_live(key);
488    mark_scope_cache_preserved(key);
489    view
490}
491
492pub fn set_cache(key: &str, input_hash: u64, view: View, slot_delta: usize) {
493    let dirty_before = SCOPE_RUN_EPOCHS.with(|epochs| epochs.borrow().get(key).copied());
494    let clean = dirty_before.is_none_or(|before| dirty_generation(key) == before);
495    set_cache_inner(key, input_hash, view, slot_delta, clean);
496}
497
498pub fn dirty_generation(key: &str) -> u64 {
499    SCOPE_DIRTY_EPOCHS.with(|epochs| epochs.borrow().get(key).copied().unwrap_or(0))
500}
501
502pub fn set_cache_preserving_dirty(
503    key: &str,
504    input_hash: u64,
505    view: View,
506    slot_delta: usize,
507    dirty_before: u64,
508) {
509    let clean = dirty_generation(key) == dirty_before;
510    set_cache_inner(key, input_hash, view, slot_delta, clean);
511}
512
513fn set_cache_inner(key: &str, input_hash: u64, view: View, slot_delta: usize, clean: bool) {
514    let children = SCOPE_LAST_CHILDREN
515        .with(|map| map.borrow_mut().remove(key))
516        .unwrap_or_default();
517    let locals = SCOPE_LAST_LOCALS
518        .with(|map| map.borrow_mut().remove(key))
519        .unwrap_or_default();
520    let animations = SCOPE_LAST_ANIMATIONS
521        .with(|map| map.borrow_mut().remove(key))
522        .unwrap_or_default();
523    let old_cache = crate::runtime::COMPOSER.with(|composer| {
524        let mut composer = composer.borrow_mut();
525        composer.scope_caches.insert(
526            key.to_string(),
527            ScopeCache {
528                input_hash,
529                view,
530                slot_delta,
531                clean,
532            },
533        )
534    });
535    drop(old_cache);
536    SCOPE_CACHE_CHILDREN.with(|map| {
537        let mut map = map.borrow_mut();
538        if children.is_empty() {
539            map.remove(key);
540        } else {
541            map.insert(key.to_string(), children);
542        }
543    });
544    SCOPE_CACHE_LOCALS.with(|map| {
545        let mut map = map.borrow_mut();
546        if locals.is_empty() {
547            map.remove(key);
548        } else {
549            map.insert(key.to_string(), locals);
550        }
551    });
552    SCOPE_CACHE_ANIMATIONS.with(|map| {
553        let mut map = map.borrow_mut();
554        if animations.is_empty() {
555            map.remove(key);
556        } else {
557            map.insert(key.to_string(), animations);
558        }
559    });
560    SCOPE_RUN_EPOCHS.with(|epochs| {
561        epochs.borrow_mut().remove(key);
562    });
563    SCOPE_PENDING_DIRTY.with(|pending| {
564        pending.borrow_mut().remove(key);
565    });
566    mark_scope_live(key);
567}
568
569pub fn gc_dead_scopes() {
570    let (dead_scopes, dead_keyed, removed_caches) = crate::runtime::COMPOSER.with(|composer| {
571        let mut composer = composer.borrow_mut();
572        let dead_scopes: Vec<String> = composer
573            .scope_caches
574            .keys()
575            .filter(|key| !composer.live_scope_keys.contains(*key))
576            .cloned()
577            .collect();
578        let mut dead_keyed = crate::runtime::take_dead_keyed_slots(&mut composer);
579        dead_keyed
580            .sort_by_key(|(key, _)| std::cmp::Reverse(crate::runtime::keyed_disposer_order(key)));
581        let mut removed_caches = Vec::with_capacity(dead_scopes.len());
582        for key in &dead_scopes {
583            if let Some(cache) = composer.scope_caches.remove(key) {
584                removed_caches.push(cache);
585            }
586        }
587        composer.live_scope_keys.clear();
588        composer.live_keyed_owners.clear();
589        composer.live_keyed_keys.clear();
590        composer.cache_preserved_owners.clear();
591        composer.live_scope_keys.insert(String::new());
592        composer
593            .live_keyed_owners
594            .insert(crate::runtime::scope_owner_token(None));
595        (dead_scopes, dead_keyed, removed_caches)
596    });
597
598    drop(removed_caches);
599
600    for key in dead_scopes {
601        clear_scope_deps(&key);
602        SCOPE_DIRTY_EPOCHS.with(|epochs| {
603            epochs.borrow_mut().remove(&key);
604        });
605        SCOPE_PENDING_DIRTY.with(|pending| {
606            pending.borrow_mut().remove(&key);
607        });
608        SCOPE_CACHE_CHILDREN.with(|map| {
609            map.borrow_mut().remove(&key);
610        });
611        SCOPE_CACHE_LOCALS.with(|map| {
612            map.borrow_mut().remove(&key);
613        });
614        SCOPE_CACHE_ANIMATIONS.with(|map| {
615            map.borrow_mut().remove(&key);
616        });
617        SCOPE_LAST_CHILDREN.with(|map| {
618            map.borrow_mut().remove(&key);
619        });
620        SCOPE_LAST_LOCALS.with(|map| {
621            map.borrow_mut().remove(&key);
622        });
623        SCOPE_LAST_ANIMATIONS.with(|map| {
624            map.borrow_mut().remove(&key);
625        });
626        SCOPE_RUN_EPOCHS.with(|epochs| {
627            epochs.borrow_mut().remove(&key);
628        });
629        crate::animation_driver::release_scope(&key);
630    }
631
632    for (key, _slot) in &dead_keyed {
633        clear_scope_deps(key);
634        if let Some(disposer) = crate::runtime::take_keyed_disposer(key) {
635            crate::runtime::run_keyed_disposer(disposer);
636        }
637    }
638    drop(dead_keyed);
639}
640
641pub fn touch_live_cached_animations() {
642    let live = crate::runtime::COMPOSER
643        .try_with(|composer| {
644            composer
645                .try_borrow()
646                .ok()
647                .map(|composer| composer.live_scope_keys.clone())
648        })
649        .ok()
650        .flatten()
651        .unwrap_or_default();
652    for key in live {
653        for animation in cached_animation_keys(&key) {
654            crate::animation_driver::touch_cached(&animation, &key);
655        }
656    }
657}
658
659pub fn cached_animation_keys(key: &str) -> Vec<String> {
660    SCOPE_CACHE_ANIMATIONS
661        .with(|map| {
662            map.borrow()
663                .get(key)
664                .map(|keys| keys.iter().cloned().collect())
665        })
666        .unwrap_or_default()
667}
668
669pub fn clear_all_scope_deps() {
670    SCOPE_SIGNAL_DEPS.with(|map| map.borrow_mut().clear());
671    SCOPE_TO_SIGNALS.with(|map| map.borrow_mut().clear());
672    SCOPE_DIRTY_EPOCHS.with(|map| map.borrow_mut().clear());
673    SCOPE_PENDING_DIRTY.with(|map| map.borrow_mut().clear());
674    SCOPE_CACHE_CHILDREN.with(|map| map.borrow_mut().clear());
675    SCOPE_CACHE_LOCALS.with(|map| map.borrow_mut().clear());
676    SCOPE_CACHE_ANIMATIONS.with(|map| map.borrow_mut().clear());
677    SCOPE_LAST_CHILDREN.with(|map| map.borrow_mut().clear());
678    SCOPE_LAST_LOCALS.with(|map| map.borrow_mut().clear());
679    SCOPE_LAST_ANIMATIONS.with(|map| map.borrow_mut().clear());
680    SCOPE_RUN_EPOCHS.with(|map| map.borrow_mut().clear());
681    SCOPE_FRAMES.with(|frames| frames.borrow_mut().clear());
682    CURRENT_SCOPE_STACK.with(|stack| stack.borrow_mut().clear());
683    CURRENT_SCOPE_KEY.with(|key| *key.borrow_mut() = None);
684}
685
686#[cfg(test)]
687mod tests {
688    use super::*;
689    use crate::signal::signal;
690
691    fn reset_maps() {
692        SCOPE_SIGNAL_DEPS.with(|map| map.borrow_mut().clear());
693        SCOPE_TO_SIGNALS.with(|map| map.borrow_mut().clear());
694        SCOPE_DIRTY_EPOCHS.with(|map| map.borrow_mut().clear());
695        SCOPE_PENDING_DIRTY.with(|map| map.borrow_mut().clear());
696    }
697
698    #[test]
699    fn scope_deps_deduplicate_keys() {
700        reset_maps();
701        let signal = signal(0);
702        with_scope_key("dedupe_scope", || {
703            let _ = signal.get();
704            let _ = signal.get();
705        });
706        SCOPE_SIGNAL_DEPS.with(|deps| {
707            assert_eq!(deps.borrow().get(&signal.id()).map(FxHashSet::len), Some(1));
708        });
709        clear_scope_deps("dedupe_scope");
710        SCOPE_SIGNAL_DEPS.with(|deps| assert!(deps.borrow().is_empty()));
711    }
712
713    #[test]
714    fn nested_scope_reads_dirty_ancestors() {
715        reset_maps();
716        let signal = signal(0);
717        with_scope_key("outer", || {
718            with_scope_key("inner", || {
719                let _ = signal.get();
720            });
721        });
722        signal.set(1);
723        assert_eq!(dirty_generation("outer"), 1);
724        assert_eq!(dirty_generation("inner"), 1);
725    }
726}