use core::cell::{Cell, RefCell};
use core::sync::atomic::{AtomicUsize, Ordering};
extern crate alloc;
use alloc::boxed::Box;
use alloc::collections::{BTreeMap, BTreeSet};
use alloc::vec::Vec;
#[derive(Clone, Copy, PartialEq, Eq)]
enum NotificationPhase {
Idle,
Propagating,
Consuming,
}
const MAX_NOTIFICATION_EPOCHS: usize = 32;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct NodeId(usize);
impl NodeId {
pub fn new() -> Self {
static COUNTER: AtomicUsize = AtomicUsize::new(0);
Self(COUNTER.fetch_add(1, Ordering::Relaxed))
}
pub fn as_u64(self) -> u64 {
self.0 as u64
}
}
impl Default for NodeId {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NodeType {
Signal,
Effect,
Memo,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum EffectTiming {
Layout,
#[default]
Passive,
}
pub struct Observer {
pub id: NodeId,
pub node_type: NodeType,
pub timing: EffectTiming,
pub cleanup: Option<()>,
}
impl Clone for Observer {
fn clone(&self) -> Self {
Self {
id: self.id,
node_type: self.node_type,
timing: self.timing,
cleanup: None, }
}
}
#[derive(Debug, Default)]
pub(crate) struct DependencyNode {
pub(crate) subscribers: Vec<NodeId>,
pub(crate) dependencies: Vec<NodeId>,
}
type SchedulerFn = Box<dyn Fn(Box<dyn FnOnce() + Send>) + Send + Sync>;
static SCHEDULER: std::sync::OnceLock<SchedulerFn> = std::sync::OnceLock::new();
pub fn set_scheduler<F>(scheduler: F)
where
F: Fn(Box<dyn FnOnce() + Send>) + Send + Sync + 'static,
{
let _ = SCHEDULER.set(Box::new(scheduler));
}
pub struct Runtime {
observer_stack: RefCell<Vec<Observer>>,
pub(crate) dependency_graph: RefCell<BTreeMap<NodeId, DependencyNode>>,
pub(crate) pending_updates: RefCell<Vec<NodeId>>,
pub(crate) update_scheduled: RefCell<bool>,
pub(crate) batch_depth: RefCell<usize>,
notification_phase: Cell<NotificationPhase>,
notification_sources: RefCell<Vec<NodeId>>,
notification_next_sources: RefCell<Vec<NodeId>>,
notification_recovery_sources: RefCell<Vec<NodeId>>,
notification_memos_seen: RefCell<BTreeSet<NodeId>>,
notification_consumers_seen: RefCell<BTreeSet<NodeId>>,
notification_layout_effects: RefCell<Vec<NodeId>>,
notification_passive: RefCell<Vec<NodeId>>,
signal_revisions: RefCell<BTreeMap<NodeId, usize>>,
}
impl Runtime {
pub fn new() -> Self {
Self {
observer_stack: RefCell::new(Vec::new()),
dependency_graph: RefCell::new(BTreeMap::new()),
pending_updates: RefCell::new(Vec::new()),
update_scheduled: RefCell::new(false),
batch_depth: RefCell::new(0),
notification_phase: Cell::new(NotificationPhase::Idle),
notification_sources: RefCell::new(Vec::new()),
notification_next_sources: RefCell::new(Vec::new()),
notification_recovery_sources: RefCell::new(Vec::new()),
notification_memos_seen: RefCell::new(BTreeSet::new()),
notification_consumers_seen: RefCell::new(BTreeSet::new()),
notification_layout_effects: RefCell::new(Vec::new()),
notification_passive: RefCell::new(Vec::new()),
signal_revisions: RefCell::new(BTreeMap::new()),
}
}
pub fn current_observer(&self) -> Option<NodeId> {
self.observer_stack
.borrow()
.last()
.map(|observer| observer.id)
}
pub fn push_observer(&self, observer: Observer) {
self.observer_stack.borrow_mut().push(observer);
}
pub fn pop_observer(&self) -> Option<Observer> {
self.observer_stack.borrow_mut().pop()
}
pub fn track_dependency(&self, signal_id: NodeId) {
if let Some(observer_id) = self.current_observer() {
let mut graph = self.dependency_graph.borrow_mut();
let signal_node = graph.entry(signal_id).or_default();
if !signal_node.subscribers.contains(&observer_id) {
signal_node.subscribers.push(observer_id);
}
let observer_node = graph.entry(observer_id).or_default();
if !observer_node.dependencies.contains(&signal_id) {
observer_node.dependencies.push(signal_id);
}
}
}
pub fn notify_signal_change(&self, signal_id: NodeId) {
self.notify_signal_changes(core::slice::from_ref(&signal_id));
}
pub(crate) fn notify_signal_changes(&self, signal_ids: &[NodeId]) {
if signal_ids.is_empty() {
return;
}
let mut revisions = self.signal_revisions.borrow_mut();
for &signal_id in signal_ids {
let revision = revisions.entry(signal_id).or_default();
*revision = revision.saturating_add(1);
}
drop(revisions);
match self.notification_phase.get() {
NotificationPhase::Idle => {
let recovery =
core::mem::take(&mut *self.notification_recovery_sources.borrow_mut());
let mut sources = self.notification_sources.borrow_mut();
sources.extend(recovery);
sources.extend(signal_ids.iter().copied());
drop(sources);
self.notification_phase.set(NotificationPhase::Propagating);
self.process_notification_epochs();
}
NotificationPhase::Propagating => {
self.notification_sources
.borrow_mut()
.extend(signal_ids.iter().copied());
}
NotificationPhase::Consuming => {
self.notification_next_sources
.borrow_mut()
.extend(signal_ids.iter().copied());
}
}
}
#[must_use]
pub fn signal_revision(&self, signal_id: NodeId) -> usize {
self.signal_revisions
.borrow()
.get(&signal_id)
.copied()
.unwrap_or_default()
}
fn process_notification_epochs(&self) {
struct NotificationWaveGuard<'a> {
runtime: &'a Runtime,
completed: bool,
discard_pending: bool,
}
impl Drop for NotificationWaveGuard<'_> {
fn drop(&mut self) {
self.runtime.notification_sources.borrow_mut().clear();
if self.discard_pending {
self.runtime.notification_next_sources.borrow_mut().clear();
self.runtime
.notification_recovery_sources
.borrow_mut()
.clear();
} else if self.completed {
self.runtime.notification_next_sources.borrow_mut().clear();
} else {
let pending =
core::mem::take(&mut *self.runtime.notification_next_sources.borrow_mut());
self.runtime
.notification_recovery_sources
.borrow_mut()
.extend(pending);
}
self.runtime.notification_memos_seen.borrow_mut().clear();
self.runtime
.notification_consumers_seen
.borrow_mut()
.clear();
self.runtime
.notification_layout_effects
.borrow_mut()
.clear();
self.runtime.notification_passive.borrow_mut().clear();
self.runtime.notification_phase.set(NotificationPhase::Idle);
}
}
let mut wave_guard = NotificationWaveGuard {
runtime: self,
completed: false,
discard_pending: false,
};
let mut epoch_count = 0_usize;
loop {
epoch_count += 1;
if epoch_count > MAX_NOTIFICATION_EPOCHS {
wave_guard.discard_pending = true;
panic!(
"reactive notification exceeded {MAX_NOTIFICATION_EPOCHS} epochs; possible non-converging layout update loop"
);
}
self.notification_phase.set(NotificationPhase::Propagating);
self.notification_memos_seen.borrow_mut().clear();
self.notification_consumers_seen.borrow_mut().clear();
self.notification_layout_effects.borrow_mut().clear();
self.notification_passive.borrow_mut().clear();
loop {
let source_id = { self.notification_sources.borrow_mut().pop() };
let Some(source_id) = source_id else {
break;
};
self.propagate_notification_source(source_id);
}
self.notification_phase.set(NotificationPhase::Consuming);
let layout_effects =
core::mem::take(&mut *self.notification_layout_effects.borrow_mut());
for effect_id in layout_effects {
super::effect::Effect::execute_effect(effect_id);
}
let passive = core::mem::take(&mut *self.notification_passive.borrow_mut());
for node_id in passive {
self.schedule_update(node_id);
}
let next_sources = core::mem::take(&mut *self.notification_next_sources.borrow_mut());
if next_sources.is_empty() {
break;
}
self.notification_sources.borrow_mut().extend(next_sources);
}
wave_guard.completed = true;
}
fn propagate_notification_source(&self, node_id: NodeId) {
let graph = self.dependency_graph.borrow();
let Some(node) = graph.get(&node_id) else {
return;
};
let subscribers = node.subscribers.clone();
drop(graph);
for subscriber_id in subscribers {
if let Some(timing) = super::effect::get_effect_timing(subscriber_id) {
if self
.notification_consumers_seen
.borrow_mut()
.insert(subscriber_id)
{
match timing {
EffectTiming::Layout => self
.notification_layout_effects
.borrow_mut()
.push(subscriber_id),
EffectTiming::Passive => {
self.notification_passive.borrow_mut().push(subscriber_id)
}
}
}
} else if super::memo::is_memo_registered(subscriber_id)
&& self
.notification_memos_seen
.borrow_mut()
.insert(subscriber_id)
{
super::memo::mark_memo_dirty_by_id(subscriber_id);
}
}
}
pub fn schedule_update(&self, node_id: NodeId) {
let mut pending = self.pending_updates.borrow_mut();
if !pending.contains(&node_id) {
pending.push(node_id);
}
drop(pending);
if *self.batch_depth.borrow() > 0 {
return;
}
if !*self.update_scheduled.borrow() {
*self.update_scheduled.borrow_mut() = true;
if let Some(scheduler) = SCHEDULER.get() {
scheduler(Box::new(|| {
RUNTIME.with(|rt| rt.flush_updates());
}));
}
}
}
pub fn clear_dependencies(&self, node_id: NodeId) {
let mut graph = self.dependency_graph.borrow_mut();
if let Some(node) = graph.get(&node_id) {
let dependencies = node.dependencies.clone();
for &dep_id in &dependencies {
if let Some(dep_node) = graph.get_mut(&dep_id) {
dep_node.subscribers.retain(|&id| id != node_id);
}
}
}
if let Some(node) = graph.get_mut(&node_id) {
node.dependencies.clear();
}
}
pub fn remove_node(&self, node_id: NodeId) {
self.clear_dependencies(node_id);
self.dependency_graph.borrow_mut().remove(&node_id);
self.signal_revisions.borrow_mut().remove(&node_id);
self.pending_updates
.borrow_mut()
.retain(|&id| id != node_id);
}
pub fn has_node(&self, node_id: NodeId) -> bool {
self.dependency_graph.borrow().contains_key(&node_id)
}
pub fn subscriber_count(&self, node_id: NodeId) -> usize {
self.dependency_graph
.borrow()
.get(&node_id)
.map(|node| node.subscribers.len())
.unwrap_or(0)
}
#[doc(hidden)]
pub fn debug_subscribers(&self, node_id: NodeId) -> alloc::vec::Vec<NodeId> {
self.dependency_graph
.borrow()
.get(&node_id)
.map(|n| n.subscribers.clone())
.unwrap_or_default()
}
#[doc(hidden)]
pub fn debug_dependencies(&self, node_id: NodeId) -> alloc::vec::Vec<NodeId> {
self.dependency_graph
.borrow()
.get(&node_id)
.map(|n| n.dependencies.clone())
.unwrap_or_default()
}
#[doc(hidden)]
pub fn debug_observer_stack(&self) -> alloc::vec::Vec<NodeId> {
self.observer_stack.borrow().iter().map(|o| o.id).collect()
}
#[doc(hidden)]
pub fn debug_pending_updates(&self) -> alloc::vec::Vec<NodeId> {
self.pending_updates.borrow().clone()
}
}
impl Default for Runtime {
fn default() -> Self {
Self::new()
}
}
thread_local! {
static RUNTIME: Runtime = Runtime::new();
}
pub fn with_runtime<F, R>(f: F) -> R
where
F: FnOnce(&Runtime) -> R,
{
RUNTIME.with(f)
}
pub fn batch<R>(f: impl FnOnce() -> R) -> R {
struct BatchGuard;
impl Drop for BatchGuard {
fn drop(&mut self) {
let _ = try_with_runtime(|rt| {
let should_flush = {
let mut depth = rt.batch_depth.borrow_mut();
debug_assert!(*depth > 0, "reactive batch depth underflow");
*depth -= 1;
*depth == 0 && !rt.pending_updates.borrow().is_empty()
};
if should_flush {
rt.flush_updates();
}
});
}
}
with_runtime(|rt| {
*rt.batch_depth.borrow_mut() += 1;
});
let _guard = BatchGuard;
f()
}
pub(crate) fn try_with_runtime<F, R>(f: F) -> Option<R>
where
F: FnOnce(&Runtime) -> R,
{
RUNTIME.try_with(f).ok()
}
#[allow(dead_code)]
pub(crate) fn run_without_observer<R>(f: impl FnOnce() -> R) -> R {
struct Restore {
saved: Vec<Observer>,
active: bool,
}
impl Drop for Restore {
fn drop(&mut self) {
if self.active {
let saved = core::mem::take(&mut self.saved);
let _ = try_with_runtime(|rt| {
*rt.observer_stack.borrow_mut() = saved;
});
}
}
}
let Some(saved) = try_with_runtime(|rt| core::mem::take(&mut *rt.observer_stack.borrow_mut()))
else {
return f();
};
let mut guard = Restore {
saved,
active: true,
};
let result = f();
let saved = core::mem::take(&mut guard.saved);
with_runtime(|rt| {
*rt.observer_stack.borrow_mut() = saved;
});
guard.active = false;
result
}
pub fn untracked<R>(f: impl FnOnce() -> R) -> R {
run_without_observer(f)
}
#[allow(dead_code)]
pub(crate) fn subscribe_node_to_observer(node: NodeId, observer: NodeId) {
with_runtime(|rt| {
let mut graph = rt.dependency_graph.borrow_mut();
let node_entry = graph.entry(node).or_default();
if !node_entry.subscribers.contains(&observer) {
node_entry.subscribers.push(observer);
}
let obs_entry = graph.entry(observer).or_default();
if !obs_entry.dependencies.contains(&node) {
obs_entry.dependencies.push(node);
}
});
}
#[cfg(test)]
mod tests {
use super::*;
use crate::reactive::{Effect, Memo, ReactiveScope, Signal};
use serial_test::serial;
use std::{cell::Cell, rc::Rc};
#[test]
#[serial]
fn test_node_id_uniqueness() {
let id1 = NodeId::new();
let id2 = NodeId::new();
let id3 = NodeId::new();
assert_ne!(id1, id2);
assert_ne!(id2, id3);
assert_ne!(id1, id3);
}
#[test]
#[serial]
fn test_runtime_observer_stack() {
let runtime = Runtime::new();
assert!(runtime.current_observer().is_none());
let observer1 = Observer {
id: NodeId::new(),
node_type: NodeType::Effect,
timing: EffectTiming::default(),
cleanup: None,
};
let id1 = observer1.id;
runtime.push_observer(observer1);
assert_eq!(runtime.current_observer(), Some(id1));
let observer2 = Observer {
id: NodeId::new(),
node_type: NodeType::Effect,
timing: EffectTiming::default(),
cleanup: None,
};
let id2 = observer2.id;
runtime.push_observer(observer2);
assert_eq!(runtime.current_observer(), Some(id2));
runtime.pop_observer();
assert_eq!(runtime.current_observer(), Some(id1));
runtime.pop_observer();
assert!(runtime.current_observer().is_none());
}
#[test]
#[serial]
fn test_dependency_tracking() {
let runtime = Runtime::new();
let signal_id = NodeId::new();
let effect_id = NodeId::new();
runtime.push_observer(Observer {
id: effect_id,
node_type: NodeType::Effect,
timing: EffectTiming::default(),
cleanup: None,
});
runtime.track_dependency(signal_id);
let graph = runtime.dependency_graph.borrow();
let signal_node = graph.get(&signal_id).unwrap();
assert!(signal_node.subscribers.contains(&effect_id));
let effect_node = graph.get(&effect_id).unwrap();
assert!(effect_node.dependencies.contains(&signal_id));
}
#[test]
#[serial(reactive_runtime)]
fn test_notify_signal_change() {
crate::reactive::ReactiveScope::run(|| {
let signal = crate::reactive::Signal::new(0_i32);
let run_count = Rc::new(Cell::new(0));
let signal_for_effect = signal;
let run_count_for_effect = Rc::clone(&run_count);
let effect = crate::reactive::Effect::new(move || {
let _ = signal_for_effect.get();
run_count_for_effect.set(run_count_for_effect.get() + 1);
});
assert_eq!(run_count.get(), 1);
with_runtime(|runtime| {
let graph = runtime.dependency_graph.borrow();
assert!(graph[&signal.id()].subscribers.contains(&effect.id()));
assert!(graph[&effect.id()].dependencies.contains(&signal.id()));
drop(graph);
runtime.notify_signal_change(signal.id());
assert!(runtime.pending_updates.borrow().contains(&effect.id()));
runtime.flush_updates();
});
assert_eq!(run_count.get(), 2);
});
}
#[test]
#[serial]
fn test_notify_signal_change_ignores_stale_subscribers() {
let runtime = Runtime::new();
let signal_id = NodeId::new();
let stale_effect_id = NodeId::new();
{
let mut graph = runtime.dependency_graph.borrow_mut();
graph
.entry(signal_id)
.or_default()
.subscribers
.push(stale_effect_id);
}
runtime.notify_signal_change(signal_id);
let pending = runtime.pending_updates.borrow();
assert!(!pending.contains(&stale_effect_id));
}
#[test]
#[serial]
fn test_clear_dependencies() {
let runtime = Runtime::new();
let signal_id = NodeId::new();
let effect_id = NodeId::new();
{
let mut graph = runtime.dependency_graph.borrow_mut();
graph
.entry(signal_id)
.or_default()
.subscribers
.push(effect_id);
graph
.entry(effect_id)
.or_default()
.dependencies
.push(signal_id);
}
runtime.clear_dependencies(effect_id);
let graph = runtime.dependency_graph.borrow();
let signal_node = graph.get(&signal_id).unwrap();
assert!(!signal_node.subscribers.contains(&effect_id));
let effect_node = graph.get(&effect_id).unwrap();
assert!(effect_node.dependencies.is_empty());
}
#[test]
#[serial]
fn debug_subscribers_returns_registered_observers_in_insertion_order() {
let runtime = Runtime::new();
let signal_id = NodeId::new();
let effect_id_a = NodeId::new();
let effect_id_b = NodeId::new();
{
let mut graph = runtime.dependency_graph.borrow_mut();
let node = graph.entry(signal_id).or_default();
node.subscribers.push(effect_id_a);
node.subscribers.push(effect_id_b);
}
let subs = runtime.debug_subscribers(signal_id);
assert_eq!(subs, alloc::vec![effect_id_a, effect_id_b]);
}
#[test]
#[serial]
fn debug_dependencies_returns_observer_dependency_list() {
let runtime = Runtime::new();
let observer_id = NodeId::new();
let signal_a = NodeId::new();
let signal_b = NodeId::new();
{
let mut graph = runtime.dependency_graph.borrow_mut();
let node = graph.entry(observer_id).or_default();
node.dependencies.push(signal_a);
node.dependencies.push(signal_b);
}
let deps = runtime.debug_dependencies(observer_id);
assert_eq!(deps, alloc::vec![signal_a, signal_b]);
}
#[test]
#[serial]
fn debug_observer_stack_returns_pushed_observers_bottom_to_top() {
let runtime = Runtime::new();
let outer_id = NodeId::new();
let inner_id = NodeId::new();
runtime.push_observer(Observer {
id: outer_id,
node_type: NodeType::Effect,
timing: EffectTiming::default(),
cleanup: None,
});
runtime.push_observer(Observer {
id: inner_id,
node_type: NodeType::Effect,
timing: EffectTiming::default(),
cleanup: None,
});
let stack = runtime.debug_observer_stack();
assert_eq!(stack, alloc::vec![outer_id, inner_id]);
}
#[test]
#[serial]
fn debug_pending_updates_returns_scheduled_node_ids_snapshot() {
let runtime = Runtime::new();
let pending_a = NodeId::new();
let pending_b = NodeId::new();
{
let mut p = runtime.pending_updates.borrow_mut();
p.push(pending_a);
p.push(pending_b);
}
let snapshot = runtime.debug_pending_updates();
assert_eq!(snapshot, alloc::vec![pending_a, pending_b]);
assert_eq!(runtime.pending_updates.borrow().len(), 2);
}
#[test]
#[serial]
fn run_without_observer_isolates_inner_signal_reads() {
ReactiveScope::run(|| {
let outer = crate::reactive::signal::Signal::new(0_i32);
let inner = crate::reactive::signal::Signal::new(0_i32);
let counter = std::rc::Rc::new(std::cell::Cell::new(0));
let counter_for_effect = counter.clone();
let outer_for_effect = outer.clone();
let inner_for_effect = inner.clone();
let _eff = crate::reactive::effect::Effect::new(move || {
let _ = outer_for_effect.get();
super::run_without_observer(|| {
let _ = inner_for_effect.get();
});
counter_for_effect.set(counter_for_effect.get() + 1);
});
let initial = counter.get();
inner.set(99);
super::with_runtime(|rt| rt.flush_updates());
assert_eq!(
counter.get(),
initial,
"run_without_observer must isolate Signal reads from outer Observer"
);
});
}
#[test]
#[serial]
fn subscribe_node_to_observer_wires_edges_both_directions() {
let node = NodeId::new();
let observer = NodeId::new();
super::subscribe_node_to_observer(node, observer);
let subs = super::with_runtime(|rt| rt.debug_subscribers(node));
let deps = super::with_runtime(|rt| rt.debug_dependencies(observer));
assert_eq!(
subs,
alloc::vec![observer],
"node must have observer as subscriber"
);
assert_eq!(deps, alloc::vec![node], "observer must depend on node");
super::subscribe_node_to_observer(node, observer);
let subs2 = super::with_runtime(|rt| rt.debug_subscribers(node));
assert_eq!(subs2.len(), 1, "subscribe must be idempotent");
}
#[rstest::rstest]
#[serial(reactive_runtime)]
fn layout_effect_write_runs_in_next_notification_epoch() {
ReactiveScope::run(|| {
let source = Signal::new(0_i32);
let runs = std::rc::Rc::new(std::cell::Cell::new(0_u8));
let observed = std::rc::Rc::new(std::cell::Cell::new(-1_i32));
let _effect = Effect::new_with_timing(
{
let source = source.clone();
let runs = std::rc::Rc::clone(&runs);
let observed = std::rc::Rc::clone(&observed);
move || {
let value = source.get();
observed.set(value);
runs.set(runs.get() + 1);
if value == 1 {
source.set(2);
}
}
},
EffectTiming::Layout,
);
source.set(1);
assert_eq!(source.get(), 2);
assert_eq!(observed.get(), 2);
assert_eq!(runs.get(), 3);
});
}
#[rstest::rstest]
#[serial(reactive_runtime)]
fn notification_panic_recovers_pending_consumer_on_next_change() {
use std::panic::{AssertUnwindSafe, catch_unwind};
ReactiveScope::run(|| {
let source = Signal::new(0_i32);
let memo = Memo::new({
let source = source.clone();
move || source.get() * 2
});
let panic_next = std::rc::Rc::new(std::cell::Cell::new(false));
let _panicking = Effect::new_with_timing(
{
let memo = memo.clone();
let panic_next = std::rc::Rc::clone(&panic_next);
move || {
assert!(!panic_next.replace(false), "notification consumer panic");
let _ = memo.get();
}
},
EffectTiming::Layout,
);
let observed = std::rc::Rc::new(std::cell::Cell::new(0_i32));
let _observer = Effect::new_with_timing(
{
let memo = memo.clone();
let observed = std::rc::Rc::clone(&observed);
move || observed.set(memo.get())
},
EffectTiming::Layout,
);
panic_next.set(true);
let result = catch_unwind(AssertUnwindSafe(|| source.set(1)));
assert!(result.is_err());
assert_eq!(observed.get(), 0);
source.set(2);
assert_eq!(observed.get(), 4);
});
}
#[rstest::rstest]
#[serial(reactive_runtime)]
fn consumer_write_before_panic_recovers_on_unrelated_notification() {
use std::panic::{AssertUnwindSafe, catch_unwind};
ReactiveScope::run(|| {
let secondary = Signal::new(0_i32);
let observed = std::rc::Rc::new(std::cell::Cell::new(0_i32));
let _secondary_effect = Effect::new_with_timing(
{
let secondary = secondary.clone();
let observed = std::rc::Rc::clone(&observed);
move || observed.set(secondary.get())
},
EffectTiming::Layout,
);
let root = Signal::new(0_i32);
let _panicking = Effect::new_with_timing(
{
let root = root.clone();
let secondary = secondary.clone();
move || {
if root.get() == 1 {
secondary.set(1);
panic!("consumer panic after write");
}
}
},
EffectTiming::Layout,
);
let unrelated = Signal::new(0_i32);
let result = catch_unwind(AssertUnwindSafe(|| root.set(1)));
assert!(result.is_err());
assert_eq!(secondary.get(), 1);
assert_eq!(observed.get(), 0);
unrelated.set(1);
assert_eq!(observed.get(), 1);
});
}
#[rstest::rstest]
#[serial(reactive_runtime)]
fn non_converging_layout_updates_panic_and_runtime_remains_reusable() {
use std::panic::{AssertUnwindSafe, catch_unwind};
const EXPECTED_MAX_NOTIFICATION_EPOCHS: usize = 32;
ReactiveScope::run(|| {
let looping = Signal::new(0_u32);
let loop_enabled = std::rc::Rc::new(std::cell::Cell::new(false));
let runs = std::rc::Rc::new(std::cell::Cell::new(0_usize));
let _looping_effect = Effect::new_with_timing(
{
let looping = looping.clone();
let loop_enabled = std::rc::Rc::clone(&loop_enabled);
let runs = std::rc::Rc::clone(&runs);
move || {
let value = looping.get();
runs.set(runs.get() + 1);
if loop_enabled.get() {
looping.set(value + 1);
}
}
},
EffectTiming::Layout,
);
let unrelated = Signal::new(0_i32);
let observed = std::rc::Rc::new(std::cell::Cell::new(0_i32));
let _unrelated_effect = Effect::new_with_timing(
{
let unrelated = unrelated.clone();
let observed = std::rc::Rc::clone(&observed);
move || observed.set(unrelated.get())
},
EffectTiming::Layout,
);
loop_enabled.set(true);
let result = catch_unwind(AssertUnwindSafe(|| looping.set(1)));
let panic = result.expect_err("non-converging notification must panic");
let message = panic
.downcast_ref::<String>()
.map(String::as_str)
.or_else(|| panic.downcast_ref::<&str>().copied())
.expect("notification limit panic must have a string message");
assert_eq!(
message,
format!(
"reactive notification exceeded {EXPECTED_MAX_NOTIFICATION_EPOCHS} epochs; possible non-converging layout update loop"
)
);
assert_eq!(runs.get(), EXPECTED_MAX_NOTIFICATION_EPOCHS + 1);
loop_enabled.set(false);
unrelated.set(1);
assert_eq!(observed.get(), 1);
});
}
}