use std::sync::Mutex;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Weak};
use reactive_graph::graph::{
AnySource, AnySubscriber, ReactiveNode, Source, Subscriber, WithObserver,
};
use crate::ReactiveRuntime;
pub struct TrackedScope {
inner: Arc<ScopeInner>,
}
struct ScopeInner {
sources: Mutex<Vec<AnySource>>,
dirty: AtomicBool,
}
impl ScopeInner {
fn new() -> Self {
Self {
sources: Mutex::new(Vec::new()),
dirty: AtomicBool::new(false),
}
}
fn notify_dirty(&self) {
let rt = ReactiveRuntime::get();
if let Some(rt) = rt {
rt.mark_signals_dirty();
}
let was_dirty = self.dirty.swap(true, Ordering::SeqCst);
if !was_dirty && let Some(rt) = rt {
rt.wake();
}
}
}
impl ReactiveNode for ScopeInner {
fn mark_dirty(&self) {
self.notify_dirty();
}
fn mark_check(&self) {
self.notify_dirty();
}
fn mark_subscribers_check(&self) {}
fn update_if_necessary(&self) -> bool {
self.dirty.load(Ordering::SeqCst)
}
}
impl Subscriber for ScopeInner {
fn add_source(&self, source: AnySource) {
let mut sources = self.sources.lock().expect("TrackedScope sources poisoned");
if !sources.contains(&source) {
sources.push(source);
}
}
fn clear_sources(&self, subscriber: &AnySubscriber) {
let drained: Vec<AnySource> = {
let mut sources = self.sources.lock().expect("TrackedScope sources poisoned");
std::mem::take(&mut *sources)
};
for source in drained {
source.remove_subscriber(subscriber);
}
}
}
impl TrackedScope {
pub fn new() -> Self {
Self {
inner: Arc::new(ScopeInner::new()),
}
}
fn any_subscriber(&self) -> AnySubscriber {
AnySubscriber(
Arc::as_ptr(&self.inner) as usize,
Arc::downgrade(&self.inner) as Weak<dyn Subscriber + Send + Sync>,
)
}
pub fn track<R>(&self, f: impl FnOnce() -> R) -> R {
let any = self.any_subscriber();
any.clear_sources(&any);
self.inner.dirty.store(false, Ordering::SeqCst);
any.with_observer(f)
}
pub fn is_dirty(&self) -> bool {
self.inner.dirty.load(Ordering::SeqCst)
}
}
impl Default for TrackedScope {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use reactive_graph::computed::Memo;
use reactive_graph::signal::RwSignal;
use reactive_graph::traits::{Get, Set};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::Duration;
use crate::FrameWaker;
fn recording_waker() -> (FrameWaker, Arc<AtomicUsize>) {
let counter = Arc::new(AtomicUsize::new(0));
let seen = counter.clone();
let waker: FrameWaker = Arc::new(move || {
counter.fetch_add(1, Ordering::SeqCst);
});
(waker, seen)
}
#[test]
fn tracked_scope_dirty_and_wake_bridge() {
let _guard = crate::WAKER_TEST_LOCK
.lock()
.unwrap_or_else(|e| e.into_inner());
let (waker, wakes) = recording_waker();
let rt = ReactiveRuntime::init(waker);
let scope = TrackedScope::new();
let sig = rt.with_owner(|| RwSignal::new(0));
scope.track(|| sig.get());
assert!(!scope.is_dirty(), "fresh track starts clean");
let before = wakes.load(Ordering::SeqCst);
sig.set(1);
sig.set(2);
sig.set(3);
assert!(scope.is_dirty(), "a tracked write must dirty the scope");
assert_eq!(
wakes.load(Ordering::SeqCst) - before,
1,
"N writes between tracks must coalesce to exactly one wake"
);
let other = rt.with_owner(|| RwSignal::new(0));
scope.track(|| sig.get()); assert!(!scope.is_dirty());
let before = wakes.load(Ordering::SeqCst);
other.set(99);
assert!(
!scope.is_dirty(),
"an untracked signal must not dirty the scope"
);
assert_eq!(
wakes.load(Ordering::SeqCst),
before,
"an untracked write must not wake"
);
scope.track(|| other.get()); assert!(!scope.is_dirty());
let before = wakes.load(Ordering::SeqCst);
sig.set(4); assert!(
!scope.is_dirty(),
"a source dropped on re-track must no longer dirty the scope"
);
assert_eq!(
wakes.load(Ordering::SeqCst),
before,
"dropped source must not wake"
);
other.set(100);
assert!(
scope.is_dirty(),
"the freshly-tracked source must still dirty"
);
let scope2 = TrackedScope::new();
let cross = rt.with_owner(|| RwSignal::new(0));
scope2.track(|| cross.get());
assert!(!scope2.is_dirty());
let before = wakes.load(Ordering::SeqCst);
std::thread::spawn(move || {
cross.set(7);
})
.join()
.expect("cross-thread writer panicked");
assert!(
scope2.is_dirty(),
"a write from another thread must dirty the scope"
);
assert_eq!(
wakes.load(Ordering::SeqCst) - before,
1,
"a cross-thread write must fire the waker once"
);
let scope3 = TrackedScope::new();
let base = rt.with_owner(|| RwSignal::new(2));
let doubled = rt.with_owner(|| Memo::new(move |_| base.get() * 2));
let seen = scope3.track(|| doubled.get());
assert_eq!(seen, 4, "memo computes from the signal");
assert!(!scope3.is_dirty());
let before = wakes.load(Ordering::SeqCst);
base.set(5);
assert!(
scope3.is_dirty(),
"writing a memo's upstream signal must dirty the tracking scope"
);
assert!(
wakes.load(Ordering::SeqCst) > before,
"a memo-chain change must fire the waker"
);
std::thread::sleep(Duration::from_millis(1));
}
}