1#[macro_export]
35macro_rules! tls_group {
36 ($( $(#[$attr:meta])* $vis:vis static $name:ident : $ty:ty = $init:expr ; )+) => {
37 #[allow(non_snake_case)]
38 struct TlsGroupSlots { $( $name: $ty, )+ }
39 ::std::thread_local! {
40 static TLS_GROUP: TlsGroupSlots = TlsGroupSlots { $( $name: $init, )+ };
41 }
42 $(
43 $(#[$attr])*
44 #[allow(non_camel_case_types, dead_code)]
45 #[derive(Clone, Copy)]
46 $vis struct $name;
47 impl $name {
48 #[inline]
49 #[allow(dead_code)]
50 $vis fn with<R>(self, f: impl ::core::ops::FnOnce(&$ty) -> R) -> R {
51 TLS_GROUP.with(|g| f(&g.$name))
52 }
53 #[inline]
54 #[allow(dead_code)]
55 $vis fn try_with<R>(
56 self,
57 f: impl ::core::ops::FnOnce(&$ty) -> R,
58 ) -> ::core::result::Result<R, ::std::thread::AccessError> {
59 TLS_GROUP.try_with(|g| f(&g.$name))
60 }
61 }
62 )+
63 };
64}
65
66#[macro_export]
78macro_rules! tls_root {
79 ($( $(#[$attr:meta])* $field:ident : $ty:ty ),+ $(,)?) => {
80 #[allow(non_snake_case)]
81 pub(crate) struct TlsRoot { $( $(#[$attr])* pub(crate) $field: $ty, )+ }
82 ::std::thread_local! {
83 static TLS_ROOT: TlsRoot =
84 TlsRoot { $( $(#[$attr])* $field: ::core::default::Default::default(), )+ };
85 }
86 #[inline]
89 pub(crate) fn tls_root<R>(f: impl ::core::ops::FnOnce(&TlsRoot) -> R) -> R {
90 TLS_ROOT.with(f)
91 }
92 #[inline]
94 #[allow(dead_code)]
95 pub(crate) fn try_tls_root<R>(
96 f: impl ::core::ops::FnOnce(&TlsRoot) -> R,
97 ) -> ::core::result::Result<R, ::std::thread::AccessError> {
98 TLS_ROOT.try_with(f)
99 }
100 };
101}
102
103#[allow(clippy::crate_in_macro_def)]
108#[macro_export]
109macro_rules! tls_slots {
110 ($field:ident; $( $(#[$attr:meta])* $vis:vis static $name:ident : $ty:ty = $init:expr ; )+) => {
111 #[allow(non_snake_case)]
112 pub(crate) struct TlsGroupSlots { $( $name: $ty, )+ }
113 impl ::core::default::Default for TlsGroupSlots {
114 fn default() -> Self { Self { $( $name: $init, )+ } }
115 }
116 $(
117 $(#[$attr])*
118 #[allow(non_camel_case_types, dead_code)]
119 #[derive(Clone, Copy)]
120 $vis struct $name;
121 impl $name {
122 #[inline]
123 #[allow(dead_code)]
124 $vis fn with<R>(self, f: impl ::core::ops::FnOnce(&$ty) -> R) -> R {
125 crate::tls_root(|r| f(&r.$field.$name))
126 }
127 #[inline]
128 #[allow(dead_code)]
129 $vis fn try_with<R>(
130 self,
131 f: impl ::core::ops::FnOnce(&$ty) -> R,
132 ) -> ::core::result::Result<R, ::std::thread::AccessError> {
133 crate::try_tls_root(|r| f(&r.$field.$name))
134 }
135 }
136 )+
137 };
138}
139
140use std::any::{Any, TypeId};
141use std::cell::{Cell, RefCell};
142use std::collections::{HashMap, HashSet};
143use std::future::Future;
144use std::marker::PhantomData;
145use std::panic::Location;
146use std::pin::Pin;
147use std::rc::Rc;
148use std::sync::Arc;
149
150use slotmap::{Key, SlotMap, new_key_type};
151
152new_key_type! {
153 pub struct NodeKey;
154 pub struct ScopeKey;
155}
156
157#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug)]
162enum NodeState {
163 Clean,
164 Check,
165 Dirty,
166}
167
168#[derive(Clone, Copy, PartialEq, Eq)]
169enum NodeKind {
170 Signal,
171 Memo,
172 Reaction,
174}
175
176type MemoEq = fn(&dyn Any, &dyn Any) -> bool;
178
179type NodeValue = Rc<RefCell<Box<dyn Any>>>;
188
189struct Node {
190 kind: NodeKind,
191 state: NodeState,
192 value: Option<NodeValue>,
193 memo_compute: Option<Rc<dyn Fn() -> Box<dyn Any>>>,
195 memo_eq: Option<MemoEq>,
196 reaction: Option<Rc<dyn Fn()>>,
198 sources: Vec<NodeKey>,
199 run_cleanups: Vec<Box<dyn FnOnce()>>,
203 observers: Vec<NodeKey>,
204 #[allow(dead_code)] scope: ScopeKey,
206 priority: u8,
208 depth: u32,
209 seq: u64,
210 last_changed: u64,
211 last_run: u64,
212 queued: bool,
213 created_at: &'static Location<'static>,
214}
215
216struct ScopeData {
217 parent: ScopeKey,
218 children: Vec<ScopeKey>,
219 nodes: Vec<NodeKey>,
220 cleanups: Vec<Box<dyn FnOnce()>>,
221 context: HashMap<TypeId, Box<dyn Any>>,
222 depth: u32,
223}
224
225struct Runtime {
226 nodes: SlotMap<NodeKey, Node>,
227 scopes: SlotMap<ScopeKey, ScopeData>,
228 root_scope: ScopeKey,
229 current_scope: ScopeKey,
230 observers: Vec<Option<NodeKey>>,
232 pending: Vec<NodeKey>,
233 batch_depth: u32,
234 draining: bool,
235 tick: u64,
236 next_seq: u64,
237 scheduler: Option<Rc<dyn Fn()>>,
238 schedule_posted: bool,
239 spawner: Option<Spawner>,
242 turn_end: Vec<Rc<dyn Fn()>>,
243 turn_end_once: Vec<Box<dyn FnOnce()>>,
245 warned_writes: HashSet<*const Location<'static>>,
246}
247
248impl Runtime {
249 fn new() -> Self {
250 let mut scopes = SlotMap::with_key();
251 let root_scope = scopes.insert(ScopeData {
252 parent: ScopeKey::null(),
253 children: Vec::new(),
254 nodes: Vec::new(),
255 cleanups: Vec::new(),
256 context: HashMap::new(),
257 depth: 0,
258 });
259 Runtime {
260 nodes: SlotMap::with_key(),
261 scopes,
262 root_scope,
263 current_scope: root_scope,
264 observers: Vec::new(),
265 pending: Vec::new(),
266 batch_depth: 0,
267 draining: false,
268 tick: 1,
269 next_seq: 0,
270 scheduler: None,
271 schedule_posted: false,
272 turn_end_once: Vec::new(),
273 spawner: None,
274 turn_end: Vec::new(),
275 warned_writes: HashSet::new(),
276 }
277 }
278}
279
280crate::tls_group! {
281 static RT: RefCell<Runtime> = RefCell::new(Runtime::new());
282
283}
284
285const RERUN_CAP: u32 = 100;
287
288const TURN_ROUND_CAP: u32 = 8;
293
294fn with_rt<R>(f: impl FnOnce(&mut Runtime) -> R) -> R {
295 RT.with(|rt| f(&mut rt.borrow_mut()))
296}
297
298#[track_caller]
303fn create_node(rt: &mut Runtime, kind: NodeKind, scope: ScopeKey) -> NodeKey {
304 let depth = rt.scopes.get(scope).map(|s| s.depth).unwrap_or(0);
305 let seq = rt.next_seq;
306 rt.next_seq += 1;
307 let key = rt.nodes.insert(Node {
308 kind,
309 state: NodeState::Clean,
310 value: None,
311 memo_compute: None,
312 memo_eq: None,
313 reaction: None,
314 sources: Vec::new(),
315 run_cleanups: Vec::new(),
316 observers: Vec::new(),
317 scope,
318 priority: 1,
319 depth,
320 seq,
321 last_changed: 0,
322 last_run: 0,
323 queued: false,
324 created_at: Location::caller(),
325 });
326 if let Some(s) = rt.scopes.get_mut(scope) {
327 s.nodes.push(key);
328 }
329 key
330}
331
332fn track_read(rt: &mut Runtime, source: NodeKey) {
334 if let Some(Some(obs)) = rt.observers.last().copied() {
335 if obs == source {
336 return;
337 }
338 let already = rt.nodes[obs].sources.contains(&source);
339 if !already {
340 rt.nodes[obs].sources.push(source);
341 rt.nodes[source].observers.push(obs);
342 }
343 }
344}
345
346#[must_use]
350fn clear_sources(rt: &mut Runtime, key: NodeKey) -> Vec<Box<dyn FnOnce()>> {
351 let sources = std::mem::take(&mut rt.nodes[key].sources);
352 for s in sources {
353 if let Some(n) = rt.nodes.get_mut(s)
354 && let Some(pos) = n.observers.iter().position(|&o| o == key)
355 {
356 n.observers.swap_remove(pos);
357 }
358 }
359 std::mem::take(&mut rt.nodes[key].run_cleanups)
360}
361
362fn mark_observers(rt: &mut Runtime, source: NodeKey, level: NodeState) {
365 let mut stack: Vec<(NodeKey, NodeState)> = rt.nodes[source]
367 .observers
368 .iter()
369 .map(|&o| (o, level))
370 .collect();
371 while let Some((key, level)) = stack.pop() {
372 let Some(node) = rt.nodes.get_mut(key) else {
373 continue;
374 };
375 if node.state >= level {
376 continue; }
378 node.state = level;
379 match node.kind {
380 NodeKind::Reaction => {
381 if !node.queued {
382 node.queued = true;
383 rt.pending.push(key);
384 }
385 }
386 NodeKind::Memo => {
387 for &o in rt.nodes[key].observers.iter() {
388 stack.push((o, NodeState::Check));
389 }
390 }
391 NodeKind::Signal => {}
392 }
393 }
394}
395
396fn refresh_memo(key: NodeKey) {
398 let (state, kind) = match with_rt(|rt| rt.nodes.get(key).map(|n| (n.state, n.kind))) {
399 Some(v) => v,
400 None => return,
401 };
402 if kind != NodeKind::Memo || state == NodeState::Clean {
403 return;
404 }
405 if state == NodeState::Check {
406 let (sources, last_run) =
408 with_rt(|rt| (rt.nodes[key].sources.clone(), rt.nodes[key].last_run));
409 let mut any_changed = false;
410 for s in sources {
411 refresh_memo(s);
412 if with_rt(|rt| {
413 rt.nodes
414 .get(s)
415 .map(|n| n.last_changed > last_run)
416 .unwrap_or(false)
417 }) {
418 any_changed = true;
419 }
420 }
421 if !any_changed {
422 with_rt(|rt| {
423 if let Some(n) = rt.nodes.get_mut(key) {
424 n.state = NodeState::Clean;
425 }
426 });
427 return;
428 }
429 }
430 let compute = with_rt(|rt| rt.nodes[key].memo_compute.clone());
432 let Some(compute) = compute else { return };
433 let cleanups = with_rt(|rt| clear_sources(rt, key));
434 for f in cleanups {
435 f();
436 }
437 with_rt(|rt| rt.observers.push(Some(key)));
438 let new_value = {
439 let _g = RtGuard(|rt: &mut Runtime| {
441 rt.observers.pop();
442 });
443 compute()
444 };
445 with_rt(|rt| {
446 let tick = rt.tick;
447 let node = &mut rt.nodes[key];
448 let changed = match (node.value.as_ref(), node.memo_eq) {
449 (Some(old), Some(eq)) => {
450 let old = old.borrow();
452 !eq(old.as_ref(), new_value.as_ref())
453 }
454 _ => true,
455 };
456 node.last_run = tick;
457 node.state = NodeState::Clean;
458 if changed {
459 node.value = Some(Rc::new(RefCell::new(new_value)));
462 node.last_changed = tick;
463 rt.tick += 1;
464 }
465 });
468}
469
470fn run_reaction(key: NodeKey) {
472 let info = with_rt(|rt| {
473 rt.nodes.get_mut(key).map(|n| {
474 n.queued = false;
475 (n.state, n.sources.clone(), n.last_run)
476 })
477 });
478 let Some((state, sources, last_run)) = info else {
479 return;
480 };
481 if state == NodeState::Clean {
482 return;
483 }
484 if state == NodeState::Check {
485 let mut any_changed = false;
486 for s in &sources {
487 refresh_memo(*s);
488 if with_rt(|rt| {
489 rt.nodes
490 .get(*s)
491 .map(|n| n.last_changed > last_run)
492 .unwrap_or(false)
493 }) {
494 any_changed = true;
495 break;
496 }
497 }
498 if !any_changed {
499 with_rt(|rt| {
500 if let Some(n) = rt.nodes.get_mut(key) {
501 n.state = NodeState::Clean;
502 }
503 });
504 return;
505 }
506 }
507 let reaction = with_rt(|rt| rt.nodes.get(key).and_then(|n| n.reaction.clone()));
508 let Some(reaction) = reaction else { return };
509 let cleanups = with_rt(|rt| {
510 let cleanups = clear_sources(rt, key);
511 if let Some(n) = rt.nodes.get_mut(key) {
512 n.state = NodeState::Clean;
513 }
514 cleanups
515 });
516 for f in cleanups {
518 f();
519 }
520 with_rt(|rt| rt.observers.push(Some(key)));
521 {
522 let _g = RtGuard(|rt: &mut Runtime| {
525 rt.observers.pop();
526 });
527 reaction();
528 }
529 with_rt(|rt| {
530 let tick = rt.tick;
531 rt.tick += 1;
532 if let Some(n) = rt.nodes.get_mut(key) {
533 n.last_run = tick;
534 }
535 });
536}
537
538pub fn flush_sync() {
548 let already = with_rt(|rt| {
549 if rt.draining {
550 return true;
551 }
552 rt.draining = true;
553 false
554 });
555 if already {
556 return; }
558 let mut round = 0u32;
559 'turn: loop {
560 round += 1;
561 let mut run_counts: HashMap<NodeKey, u32> = HashMap::new();
563 loop {
564 let mut batch = with_rt(|rt| std::mem::take(&mut rt.pending));
565 if batch.is_empty() {
566 break;
567 }
568 with_rt(|rt| {
570 batch.sort_by_key(|&k| {
571 rt.nodes
572 .get(k)
573 .map(|n| (n.priority, n.depth, n.seq))
574 .unwrap_or((u8::MAX, u32::MAX, u64::MAX))
575 })
576 });
577 for key in batch {
578 let count = run_counts.entry(key).or_insert(0);
579 *count += 1;
580 if *count > RERUN_CAP {
581 if *count == RERUN_CAP + 1 {
582 let loc = with_rt(|rt| rt.nodes.get(key).map(|n| n.created_at));
583 if let Some(loc) = loc {
584 if cfg!(debug_assertions) {
585 panic!(
586 "day-reactive: effect created at {loc} re-ran more than {RERUN_CAP} times in one drain (reactive cycle?)"
587 );
588 } else {
589 log::warn!(
590 "day-reactive: effect created at {loc} exceeded the re-run cap; skipping it for the rest of this drain (the next source write re-arms it)"
591 );
592 }
593 }
594 }
595 with_rt(|rt| {
601 if let Some(n) = rt.nodes.get_mut(key) {
602 n.queued = false;
603 n.state = NodeState::Clean;
604 }
605 });
606 continue;
607 }
608 run_reaction(key);
609 }
610 }
611 let (once, turn_end) = with_rt(|rt| {
612 rt.draining = false;
613 rt.schedule_posted = false;
614 (std::mem::take(&mut rt.turn_end_once), rt.turn_end.clone())
615 });
616 for cb in once {
619 cb();
620 }
621 for cb in turn_end {
622 cb();
623 }
624 let more = with_rt(|rt| {
626 if rt.pending.is_empty() {
627 return false;
628 }
629 rt.draining = true;
630 true
631 });
632 if !more {
633 return;
634 }
635 if round >= TURN_ROUND_CAP {
636 with_rt(|rt| rt.draining = false);
640 if cfg!(debug_assertions) {
641 panic!(
642 "day-reactive: turn-end callbacks scheduled new reactive work for {TURN_ROUND_CAP} straight rounds in one turn (hooks feeding each other?)"
643 );
644 } else {
645 log::warn!(
646 "day-reactive: turn-end callbacks still scheduling work after {TURN_ROUND_CAP} rounds; deferring the rest to the next turn"
647 );
648 }
649 return;
650 }
651 continue 'turn;
652 }
653}
654
655pub fn recover_from_panic() {
662 with_rt(|rt| {
663 rt.draining = false;
664 rt.schedule_posted = false;
665 rt.batch_depth = 0;
666 rt.turn_end_once.clear();
667 rt.pending.clear();
668 rt.observers.clear();
669 rt.current_scope = rt.root_scope;
673 });
674}
675
676fn schedule_after_write(rt: &mut Runtime) -> Option<Rc<dyn Fn()>> {
679 if rt.batch_depth > 0 || rt.draining {
680 return None;
681 }
682 if rt.schedule_posted {
683 return None;
684 }
685 rt.schedule_posted = true;
686 rt.scheduler.clone()
687}
688
689fn signal_write_boxed(key: NodeKey, apply: impl FnOnce(&mut Box<dyn Any>) -> bool) {
690 let poster = with_rt(|rt| {
691 let node = rt.nodes.get_mut(key)?;
692 let cell = node.value.as_ref()?.clone();
693 let mut value = match cell.try_borrow_mut() {
694 Ok(v) => v,
695 Err(_) => panic!(
699 "day-reactive: wrote to a Signal while a with()/try_with() read of it is still \
700 in flight — the write would be lost. Finish the read closure first (copy the \
701 value out with get()) and write after it returns."
702 ),
703 };
704 let changed = apply(&mut value);
705 drop(value);
706 let node = rt.nodes.get_mut(key)?;
707 if !changed {
708 return None;
709 }
710 node.last_changed = rt.tick;
711 rt.tick += 1;
712 mark_observers(rt, key, NodeState::Dirty);
713 schedule_after_write(rt)
714 });
715 if let Some(post) = poster {
716 post();
717 }
718}
719
720struct RtGuard<F: FnMut(&mut Runtime)>(F);
731
732impl<F: FnMut(&mut Runtime)> Drop for RtGuard<F> {
733 fn drop(&mut self) {
734 with_rt(|rt| (self.0)(rt));
735 }
736}
737
738pub fn batch<R>(f: impl FnOnce() -> R) -> R {
739 with_rt(|rt| rt.batch_depth += 1);
740 let r = {
741 let _g = RtGuard(|rt: &mut Runtime| rt.batch_depth = rt.batch_depth.saturating_sub(1));
746 f()
747 };
748 let should_drain = with_rt(|rt| {
749 rt.batch_depth == 0
750 && !rt.draining
751 && (!rt.pending.is_empty() || !rt.turn_end_once.is_empty())
752 });
753 if should_drain {
754 flush_sync();
755 }
756 r
757}
758
759pub fn flush_now() {
766 let saved = with_rt(|rt| std::mem::replace(&mut rt.batch_depth, 0));
767 let _g = RtGuard(move |rt: &mut Runtime| rt.batch_depth = saved);
770 flush_sync();
771}
772
773pub fn untrack<R>(f: impl FnOnce() -> R) -> R {
775 with_rt(|rt| rt.observers.push(None));
776 let _g = RtGuard(|rt: &mut Runtime| {
779 rt.observers.pop();
780 });
781 f()
782}
783
784#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
788pub struct RunId(NodeKey);
789
790pub fn active_run() -> Option<RunId> {
792 with_rt(|rt| rt.observers.last().copied().flatten()).map(RunId)
793}
794
795pub fn on_run_retrack(f: impl FnOnce() + 'static) -> bool {
800 with_rt(|rt| {
801 let Some(Some(key)) = rt.observers.last().copied() else {
802 return false;
803 };
804 match rt.nodes.get_mut(key) {
805 Some(n) => {
806 n.run_cleanups.push(Box::new(f));
807 true
808 }
809 None => false,
810 }
811 })
812}
813
814pub fn install_scheduler(post: impl Fn() + 'static) {
816 with_rt(|rt| rt.scheduler = Some(Rc::new(post)));
817}
818
819pub type LocalBoxFuture = Pin<Box<dyn Future<Output = ()> + 'static>>;
821
822type Spawner = Rc<dyn Fn(LocalBoxFuture) -> Box<dyn FnOnce()>>;
824
825pub fn install_spawner(spawn: impl Fn(LocalBoxFuture) -> Box<dyn FnOnce()> + 'static) {
831 with_rt(|rt| rt.spawner = Some(Rc::new(spawn)));
832}
833
834fn spawn_local(fut: LocalBoxFuture) -> Box<dyn FnOnce()> {
836 let spawner = with_rt(|rt| rt.spawner.clone());
837 match spawner {
838 Some(s) => s(fut),
839 None => panic!("day-reactive: no spawner installed (backend not started)"),
840 }
841}
842
843pub fn on_turn_end(cb: impl Fn() + 'static) {
845 with_rt(|rt| rt.turn_end.push(Rc::new(cb)));
846}
847
848pub fn at_turn_end(cb: impl FnOnce() + 'static) {
855 let now = with_rt(|rt| {
856 if rt.draining || rt.batch_depth > 0 {
857 rt.turn_end_once.push(Box::new(cb));
858 None
859 } else {
860 Some(cb)
861 }
862 });
863 if let Some(cb) = now {
864 cb();
865 }
866}
867
868type MainPoster = Box<dyn Fn(Box<dyn FnOnce() + Send>) + Send + Sync>;
873static MAIN_POSTER: std::sync::OnceLock<MainPoster> = std::sync::OnceLock::new();
874
875pub fn install_main_poster(post: impl Fn(Box<dyn FnOnce() + Send>) + Send + Sync + 'static) {
877 let _ = MAIN_POSTER.set(Box::new(post));
878}
879
880pub fn has_main_poster() -> bool {
886 MAIN_POSTER.get().is_some()
887}
888
889pub fn on_main(f: impl FnOnce() + Send + 'static) {
891 match MAIN_POSTER.get() {
892 Some(post) => post(Box::new(f)),
893 None => panic!("day-reactive: no main poster installed (backend not started)"),
894 }
895}
896
897type DelayedPoster = Box<dyn Fn(u32, Box<dyn FnOnce() + Send>) + Send + Sync>;
898static DELAYED_POSTER: std::sync::OnceLock<DelayedPoster> = std::sync::OnceLock::new();
899
900pub fn install_delayed_poster(
902 post: impl Fn(u32, Box<dyn FnOnce() + Send>) + Send + Sync + 'static,
903) {
904 let _ = DELAYED_POSTER.set(Box::new(post));
905}
906
907pub fn on_main_delayed(ms: u32, f: impl FnOnce() + Send + 'static) {
910 match DELAYED_POSTER.get() {
911 Some(post) => post(ms, Box::new(f)),
912 None => panic!("day-reactive: no delayed poster installed (backend not started)"),
913 }
914}
915
916#[derive(Clone, Copy, PartialEq, Eq)]
922pub struct Scope {
923 key: ScopeKey,
924 _not_send: PhantomData<*const ()>,
925}
926
927impl Scope {
928 pub fn current() -> Scope {
929 Scope {
930 key: with_rt(|rt| rt.current_scope),
931 _not_send: PhantomData,
932 }
933 }
934
935 pub fn root() -> Scope {
936 Scope {
937 key: with_rt(|rt| rt.root_scope),
938 _not_send: PhantomData,
939 }
940 }
941
942 pub fn child() -> Scope {
944 Scope::current().create_child()
945 }
946
947 pub fn create_child(self) -> Scope {
948 let key = with_rt(|rt| {
949 let depth = rt.scopes.get(self.key).map(|s| s.depth + 1).unwrap_or(1);
950 let child = rt.scopes.insert(ScopeData {
951 parent: self.key,
952 children: Vec::new(),
953 nodes: Vec::new(),
954 cleanups: Vec::new(),
955 context: HashMap::new(),
956 depth,
957 });
958 if let Some(p) = rt.scopes.get_mut(self.key) {
959 p.children.push(child);
960 }
961 child
962 });
963 Scope {
964 key,
965 _not_send: PhantomData,
966 }
967 }
968
969 pub fn detached() -> Scope {
971 let key = with_rt(|rt| {
972 rt.scopes.insert(ScopeData {
973 parent: ScopeKey::null(),
974 children: Vec::new(),
975 nodes: Vec::new(),
976 cleanups: Vec::new(),
977 context: HashMap::new(),
978 depth: 0,
979 })
980 });
981 Scope {
982 key,
983 _not_send: PhantomData,
984 }
985 }
986
987 pub fn enter<R>(self, f: impl FnOnce() -> R) -> R {
989 let prev = with_rt(|rt| std::mem::replace(&mut rt.current_scope, self.key));
990 let _g = RtGuard(move |rt: &mut Runtime| rt.current_scope = prev);
994 f()
995 }
996
997 pub fn on_cleanup(self, f: impl FnOnce() + 'static) {
998 with_rt(|rt| {
999 if let Some(s) = rt.scopes.get_mut(self.key) {
1000 s.cleanups.push(Box::new(f));
1001 }
1002 });
1003 }
1004
1005 pub fn is_alive(self) -> bool {
1006 with_rt(|rt| rt.scopes.contains_key(self.key))
1007 }
1008
1009 pub fn dispose(self) {
1011 let children = match with_rt(|rt| rt.scopes.get(self.key).map(|s| s.children.clone())) {
1012 Some(c) => c,
1013 None => return,
1014 };
1015 for c in children {
1016 (Scope {
1017 key: c,
1018 _not_send: PhantomData,
1019 })
1020 .dispose();
1021 }
1022 let (nodes, cleanups, parent) = match with_rt(|rt| {
1023 rt.scopes
1024 .remove(self.key)
1025 .map(|s| (s.nodes, s.cleanups, s.parent))
1026 }) {
1027 Some(v) => v,
1028 None => return,
1029 };
1030 let mut run_cleanups: Vec<Box<dyn FnOnce()>> = Vec::new();
1031 with_rt(|rt| {
1032 if let Some(p) = rt.scopes.get_mut(parent)
1033 && let Some(pos) = p.children.iter().position(|&c| c == self.key)
1034 {
1035 p.children.swap_remove(pos);
1036 }
1037 for key in nodes {
1038 run_cleanups.extend(clear_sources(rt, key));
1039 if let Some(node) = rt.nodes.get_mut(key) {
1041 let observers = std::mem::take(&mut node.observers);
1042 for o in observers {
1043 if let Some(on) = rt.nodes.get_mut(o)
1044 && let Some(pos) = on.sources.iter().position(|&s| s == key)
1045 {
1046 on.sources.swap_remove(pos);
1047 }
1048 }
1049 }
1050 rt.nodes.remove(key);
1051 }
1053 });
1054 for f in run_cleanups {
1057 f();
1058 }
1059 for c in cleanups {
1060 c();
1061 }
1062 }
1063
1064 pub fn provide<T: 'static>(self, value: T) {
1066 with_rt(|rt| {
1067 if let Some(s) = rt.scopes.get_mut(self.key) {
1068 s.context.insert(TypeId::of::<T>(), Box::new(value));
1069 }
1070 });
1071 }
1072
1073 pub fn use_context<T: Clone + 'static>(self) -> Option<T> {
1075 with_rt(|rt| {
1076 let mut cur = self.key;
1077 while let Some(s) = rt.scopes.get(cur) {
1078 if let Some(v) = s.context.get(&TypeId::of::<T>()) {
1079 return v.downcast_ref::<T>().cloned();
1080 }
1081 cur = s.parent;
1082 }
1083 None
1084 })
1085 }
1086}
1087
1088pub struct Signal<T: 'static> {
1094 key: NodeKey,
1095 created_at: &'static Location<'static>,
1096 _m: PhantomData<*const T>,
1097}
1098
1099impl<T: 'static> Clone for Signal<T> {
1100 fn clone(&self) -> Self {
1101 *self
1102 }
1103}
1104impl<T: 'static> Copy for Signal<T> {}
1105
1106impl<T: 'static> std::fmt::Debug for Signal<T> {
1107 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1108 write!(f, "Signal({:?})", self.key)
1109 }
1110}
1111
1112impl<T: 'static> Signal<T> {
1113 #[track_caller]
1114 pub fn new(value: T) -> Self {
1115 Self::new_in(Scope::current(), value)
1116 }
1117
1118 #[track_caller]
1125 pub fn global(value: T) -> Self {
1126 Self::new_in(Scope::root(), value)
1127 }
1128
1129 #[track_caller]
1130 pub fn new_in(scope: Scope, value: T) -> Self {
1131 let created_at = Location::caller();
1132 let key = with_rt(|rt| {
1133 let k = create_node(rt, NodeKind::Signal, scope.key);
1134 rt.nodes[k].value = Some(Rc::new(RefCell::new(Box::new(value) as Box<dyn Any>)));
1135 rt.nodes[k].created_at = created_at;
1136 k
1137 });
1138 Signal {
1139 key,
1140 created_at,
1141 _m: PhantomData,
1142 }
1143 }
1144
1145 #[track_caller]
1147 pub fn with<R>(self, f: impl FnOnce(&T) -> R) -> R {
1148 match self.try_with(f) {
1149 Some(r) => r,
1150 None => panic!(
1151 "day-reactive: read of disposed Signal created at {} (use try_with in closures that can outlive their scope)",
1152 self.created_at
1153 ),
1154 }
1155 }
1156
1157 pub fn try_with<R>(self, f: impl FnOnce(&T) -> R) -> Option<R> {
1158 let cell = with_rt(|rt| {
1163 track_read(rt, self.key);
1164 rt.nodes.get(self.key).and_then(|n| n.value.clone())
1165 })?;
1166 let value = cell.borrow();
1167 value.downcast_ref::<T>().map(f)
1168 }
1169
1170 pub fn with_untracked<R>(self, f: impl FnOnce(&T) -> R) -> R {
1171 untrack(|| self.with(f))
1172 }
1173
1174 #[track_caller]
1175 pub fn get(self) -> T
1176 where
1177 T: Clone,
1178 {
1179 self.with(|v| v.clone())
1180 }
1181
1182 pub fn try_get(self) -> Option<T>
1183 where
1184 T: Clone,
1185 {
1186 self.try_with(|v| v.clone())
1187 }
1188
1189 pub fn get_untracked(self) -> T
1190 where
1191 T: Clone,
1192 {
1193 untrack(|| self.get())
1194 }
1195
1196 pub fn track(self) {
1198 with_rt(|rt| track_read(rt, self.key));
1199 }
1200
1201 #[track_caller]
1202 pub fn set(self, value: T) {
1203 self.write_check(move |slot| {
1204 *slot = value;
1205 true
1206 });
1207 }
1208
1209 #[track_caller]
1210 pub fn set_if_changed(self, value: T)
1211 where
1212 T: PartialEq,
1213 {
1214 self.write_check(move |slot| {
1215 if *slot == value {
1216 false
1217 } else {
1218 *slot = value;
1219 true
1220 }
1221 });
1222 }
1223
1224 #[track_caller]
1225 pub fn update(self, f: impl FnOnce(&mut T)) {
1226 self.write_check(move |slot| {
1227 f(slot);
1228 true
1229 });
1230 }
1231
1232 #[track_caller]
1233 fn write_check(self, apply: impl FnOnce(&mut T) -> bool) {
1234 let alive = with_rt(|rt| rt.nodes.contains_key(self.key));
1235 if !alive {
1236 let loc = Location::caller();
1238 let first = with_rt(|rt| rt.warned_writes.insert(loc as *const _));
1239 if first && cfg!(debug_assertions) {
1240 log::warn!(
1241 "day-reactive: write at {loc} to a disposed signal (created at {}) ignored",
1242 self.created_at
1243 );
1244 }
1245 return;
1246 }
1247 signal_write_boxed(self.key, |boxed| match boxed.downcast_mut::<T>() {
1248 Some(slot) => apply(slot),
1249 None => false,
1250 });
1251 }
1252
1253 pub fn setter(self) -> Setter<T>
1255 where
1256 T: Send,
1257 {
1258 Setter {
1259 key: self.key,
1260 _m: PhantomData,
1261 }
1262 }
1263}
1264
1265pub struct Setter<T: Send + 'static> {
1268 key: NodeKey,
1269 _m: PhantomData<fn(T)>,
1270}
1271
1272impl<T: Send + 'static> Clone for Setter<T> {
1273 fn clone(&self) -> Self {
1274 *self
1275 }
1276}
1277impl<T: Send + 'static> Copy for Setter<T> {}
1278
1279impl<T: Send + 'static> Setter<T> {
1280 pub fn set(self, value: T) {
1281 let key = self.key;
1282 on_main(move || {
1283 let poster = with_rt(|rt| {
1284 let cell = rt.nodes.get(key)?.value.clone()?;
1285 {
1286 let mut boxed = cell.try_borrow_mut().ok()?;
1287 let slot = boxed.downcast_mut::<T>()?;
1288 *slot = value;
1289 }
1290 let node = rt.nodes.get_mut(key)?;
1291 node.last_changed = rt.tick;
1292 rt.tick += 1;
1293 mark_observers(rt, key, NodeState::Dirty);
1294 schedule_after_write(rt)
1295 });
1296 if let Some(post) = poster {
1297 post();
1298 }
1299 });
1300 }
1301}
1302
1303pub struct Memo<T: 'static> {
1309 key: NodeKey,
1310 created_at: &'static Location<'static>,
1311 _m: PhantomData<*const T>,
1312}
1313
1314impl<T: 'static> Clone for Memo<T> {
1315 fn clone(&self) -> Self {
1316 *self
1317 }
1318}
1319impl<T: 'static> Copy for Memo<T> {}
1320
1321impl<T: 'static> Memo<T> {
1322 #[track_caller]
1323 pub fn new(f: impl Fn() -> T + 'static) -> Self
1324 where
1325 T: PartialEq,
1326 {
1327 Self::new_with_eq(f, |a, b| a == b)
1328 }
1329
1330 #[track_caller]
1331 pub fn new_with_eq(f: impl Fn() -> T + 'static, eq: fn(&T, &T) -> bool) -> Self {
1332 let created_at = Location::caller();
1333 let key = with_rt(|rt| {
1336 let scope = rt.current_scope;
1337 create_node(rt, NodeKind::Memo, scope)
1338 });
1339 let compute: Rc<dyn Fn() -> Box<dyn Any>> =
1340 Rc::new(move || Box::new(EqBox { value: f(), eq }) as Box<dyn Any>);
1341 with_rt(|rt| {
1342 let node = &mut rt.nodes[key];
1343 node.memo_compute = Some(compute);
1344 node.memo_eq = Some(eqbox_eq::<T>);
1345 node.state = NodeState::Dirty; node.created_at = created_at;
1347 });
1348 Memo {
1349 key,
1350 created_at,
1351 _m: PhantomData,
1352 }
1353 }
1354
1355 #[track_caller]
1356 pub fn with<R>(self, f: impl FnOnce(&T) -> R) -> R {
1357 refresh_memo(self.key);
1358 let value = with_rt(|rt| {
1359 track_read(rt, self.key);
1360 rt.nodes.get(self.key).and_then(|n| n.value.clone())
1361 });
1362 match value {
1363 Some(cell) => {
1365 let boxed = cell.borrow();
1366 boxed
1367 .downcast_ref::<EqBox<T>>()
1368 .map(|e| f(&e.value))
1369 .unwrap_or_else(|| panic!("day-reactive: memo type mismatch"))
1370 }
1371 None => panic!(
1372 "day-reactive: read of disposed Memo created at {}",
1373 self.created_at
1374 ),
1375 }
1376 }
1377
1378 #[track_caller]
1379 pub fn get(self) -> T
1380 where
1381 T: Clone,
1382 {
1383 self.with(|v| v.clone())
1384 }
1385}
1386
1387struct EqBox<T> {
1389 value: T,
1390 eq: fn(&T, &T) -> bool,
1391}
1392
1393fn eqbox_eq<T: 'static>(a: &dyn Any, b: &dyn Any) -> bool {
1394 match (a.downcast_ref::<EqBox<T>>(), b.downcast_ref::<EqBox<T>>()) {
1395 (Some(a), Some(b)) => (a.eq)(&a.value, &b.value),
1396 _ => false,
1397 }
1398}
1399
1400pub struct Effect;
1406
1407impl Effect {
1408 #[allow(clippy::new_ret_no_self)]
1411 #[track_caller]
1412 pub fn new(f: impl Fn() + 'static) {
1413 create_reaction(Rc::new(f), 1);
1414 }
1415}
1416
1417#[track_caller]
1418fn create_reaction(f: Rc<dyn Fn()>, priority: u8) -> NodeKey {
1419 let created_at = Location::caller();
1420 let key = with_rt(|rt| {
1421 let scope = rt.current_scope;
1422 let k = create_node(rt, NodeKind::Reaction, scope);
1423 rt.nodes[k].reaction = Some(f);
1424 rt.nodes[k].priority = priority;
1425 rt.nodes[k].created_at = created_at;
1426 k
1427 });
1428 with_rt(|rt| rt.observers.push(Some(key)));
1430 let reaction = with_rt(|rt| rt.nodes[key].reaction.clone());
1431 {
1432 let _g = RtGuard(|rt: &mut Runtime| {
1434 rt.observers.pop();
1435 });
1436 if let Some(r) = reaction {
1437 r();
1438 }
1439 }
1440 with_rt(|rt| {
1441 let tick = rt.tick;
1442 rt.tick += 1;
1443 if let Some(n) = rt.nodes.get_mut(key) {
1444 n.last_run = tick;
1445 }
1446 });
1447 key
1448}
1449
1450#[track_caller]
1454pub fn bind<V: PartialEq + 'static>(
1455 compute: impl Fn() -> V + 'static,
1456 apply: impl Fn(&V) + 'static,
1457) {
1458 let last: RefCell<Option<V>> = RefCell::new(None);
1459 create_reaction(
1460 Rc::new(move || {
1461 let v = compute();
1462 if last.borrow().as_ref() != Some(&v) {
1463 untrack(|| apply(&v));
1464 *last.borrow_mut() = Some(v);
1465 }
1466 }),
1467 0,
1468 );
1469}
1470
1471#[track_caller]
1474pub fn bind_seeded<V: PartialEq + 'static>(
1475 seed: V,
1476 compute: impl Fn() -> V + 'static,
1477 apply: impl Fn(&V) + 'static,
1478) {
1479 let last: RefCell<Option<V>> = RefCell::new(Some(seed));
1480 create_reaction(
1481 Rc::new(move || {
1482 let v = compute();
1483 if last.borrow().as_ref() != Some(&v) {
1484 untrack(|| apply(&v));
1485 *last.borrow_mut() = Some(v);
1486 }
1487 }),
1488 0,
1489 );
1490}
1491
1492#[track_caller]
1494pub fn bind_always<V: 'static>(compute: impl Fn() -> V + 'static, apply: impl Fn(V) + 'static) {
1495 create_reaction(
1496 Rc::new(move || {
1497 let v = compute();
1498 untrack(|| apply(v));
1499 }),
1500 0,
1501 );
1502}
1503
1504#[track_caller]
1509pub fn watch<S: Clone + 'static>(
1510 source: impl Fn() -> S + 'static,
1511 cb: impl Fn(&S, Option<&S>) + 'static,
1512) {
1513 let prev: RefCell<Option<S>> = RefCell::new(None);
1514 create_reaction(
1515 Rc::new(move || {
1516 let new = source();
1517 let old = prev.borrow_mut().replace(new.clone());
1518 if old.is_some() {
1519 untrack(|| cb(&new, old.as_ref()));
1520 }
1521 }),
1522 1,
1523 );
1524}
1525
1526#[derive(Clone, Copy)]
1528pub struct Trigger {
1529 signal: Signal<u64>,
1530}
1531
1532impl Trigger {
1533 #[track_caller]
1534 pub fn new() -> Self {
1535 Trigger {
1536 signal: Signal::new(0),
1537 }
1538 }
1539 pub fn track(self) {
1540 self.signal.track();
1541 }
1542 pub fn notify(self) {
1543 self.signal.update(|v| *v = v.wrapping_add(1));
1544 }
1545}
1546
1547impl Default for Trigger {
1548 fn default() -> Self {
1549 Self::new()
1550 }
1551}
1552
1553pub trait Binding<T: 'static>: Clone + 'static {
1563 fn read(&self) -> T;
1565 fn peek(&self) -> T;
1567 fn write(&self, v: T);
1569 fn write_preview(&self, v: T) {
1573 self.write(v);
1574 }
1575 fn write_commit(&self, v: T) {
1579 self.write(v);
1580 }
1581 fn write_teardown(&self, v: T) {
1584 self.write_commit(v);
1585 }
1586}
1587
1588impl<T: Clone + 'static> Binding<T> for Signal<T> {
1589 fn read(&self) -> T {
1590 self.get()
1591 }
1592 fn peek(&self) -> T {
1593 self.get_untracked()
1594 }
1595 fn write(&self, v: T) {
1596 self.set(v);
1597 }
1598 fn write_teardown(&self, _v: T) {}
1599}
1600
1601trait ErasedBinding<T: 'static> {
1604 fn erased_read(&self) -> T;
1605 fn erased_peek(&self) -> T;
1606 fn erased_write(&self, v: T);
1607 fn erased_write_preview(&self, v: T);
1608 fn erased_write_commit(&self, v: T);
1609 fn erased_write_teardown(&self, v: T);
1610}
1611
1612impl<T: 'static, B: Binding<T>> ErasedBinding<T> for B {
1613 fn erased_read(&self) -> T {
1614 Binding::read(self)
1615 }
1616 fn erased_peek(&self) -> T {
1617 Binding::peek(self)
1618 }
1619 fn erased_write(&self, v: T) {
1620 Binding::write(self, v)
1621 }
1622 fn erased_write_preview(&self, v: T) {
1623 Binding::write_preview(self, v)
1624 }
1625 fn erased_write_commit(&self, v: T) {
1626 Binding::write_commit(self, v)
1627 }
1628 fn erased_write_teardown(&self, v: T) {
1629 Binding::write_teardown(self, v)
1630 }
1631}
1632
1633pub struct AnyBinding<T: 'static> {
1641 slot: Signal<Rc<dyn ErasedBinding<T>>>,
1642}
1643
1644impl<T: 'static> Clone for AnyBinding<T> {
1645 fn clone(&self) -> Self {
1646 *self
1647 }
1648}
1649impl<T: 'static> Copy for AnyBinding<T> {}
1650
1651impl<T: 'static> AnyBinding<T> {
1652 pub fn new(binding: impl Binding<T>) -> Self {
1654 let erased: Rc<dyn ErasedBinding<T>> = Rc::new(binding);
1655 AnyBinding {
1656 slot: Signal::new(erased),
1657 }
1658 }
1659 fn inner(self) -> Rc<dyn ErasedBinding<T>> {
1660 self.slot.get_untracked()
1661 }
1662 pub fn get(self) -> T {
1664 self.inner().erased_read()
1665 }
1666 pub fn try_get(self) -> Option<T> {
1668 self.slot.try_get().map(|b| b.erased_read())
1669 }
1670 pub fn get_untracked(self) -> T {
1672 self.inner().erased_peek()
1673 }
1674 pub fn with<R>(self, f: impl FnOnce(&T) -> R) -> R {
1676 let v = self.inner().erased_read();
1677 f(&v)
1678 }
1679 pub fn set(self, v: T) {
1681 self.inner().erased_write(v)
1682 }
1683 pub fn set_if_changed(self, v: T)
1685 where
1686 T: PartialEq,
1687 {
1688 let b = self.inner();
1689 if b.erased_peek() != v {
1690 b.erased_write(v);
1691 }
1692 }
1693 pub fn update(self, f: impl FnOnce(&mut T)) {
1695 let b = self.inner();
1696 let mut v = b.erased_peek();
1697 f(&mut v);
1698 b.erased_write(v);
1699 }
1700}
1701
1702impl<T: 'static> Binding<T> for AnyBinding<T> {
1703 fn read(&self) -> T {
1704 self.inner().erased_read()
1705 }
1706 fn peek(&self) -> T {
1707 self.inner().erased_peek()
1708 }
1709 fn write(&self, v: T) {
1710 self.inner().erased_write(v)
1711 }
1712 fn write_preview(&self, v: T) {
1713 self.inner().erased_write_preview(v)
1714 }
1715 fn write_commit(&self, v: T) {
1716 self.inner().erased_write_commit(v)
1717 }
1718 fn write_teardown(&self, v: T) {
1719 self.inner().erased_write_teardown(v)
1720 }
1721}
1722
1723#[derive(Clone)]
1730pub enum Load<T> {
1731 Loading,
1732 Ready(T),
1733 Failed(Arc<dyn std::error::Error + Send + Sync>),
1734}
1735
1736impl<T> Load<T> {
1737 pub fn ready(&self) -> Option<&T> {
1739 match self {
1740 Load::Ready(v) => Some(v),
1741 _ => None,
1742 }
1743 }
1744 pub fn is_loading(&self) -> bool {
1745 matches!(self, Load::Loading)
1746 }
1747 pub fn is_ready(&self) -> bool {
1748 matches!(self, Load::Ready(_))
1749 }
1750 pub fn error(&self) -> Option<&(dyn std::error::Error + Send + Sync + 'static)> {
1752 match self {
1753 Load::Failed(e) => Some(e.as_ref()),
1754 _ => None,
1755 }
1756 }
1757}
1758
1759impl<T: std::fmt::Debug> std::fmt::Debug for Load<T> {
1760 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1761 match self {
1762 Load::Loading => write!(f, "Loading"),
1763 Load::Ready(v) => f.debug_tuple("Ready").field(v).finish(),
1764 Load::Failed(e) => f.debug_tuple("Failed").field(&e.to_string()).finish(),
1765 }
1766 }
1767}
1768
1769struct FetchState {
1771 generation: Cell<u64>,
1772 abort: RefCell<Option<Box<dyn FnOnce()>>>,
1773}
1774
1775impl FetchState {
1776 fn supersede(&self) -> u64 {
1780 let next = self.generation.get() + 1;
1781 self.generation.set(next);
1782 let old = self.abort.borrow_mut().take();
1783 if let Some(abort) = old {
1784 abort();
1785 }
1786 next
1787 }
1788}
1789
1790pub struct Resource<T: 'static> {
1805 state: Signal<Load<T>>,
1806 refetch_count: Signal<u64>,
1807 _not_send: PhantomData<*const T>,
1808}
1809
1810impl<T> Clone for Resource<T> {
1811 fn clone(&self) -> Self {
1812 *self
1813 }
1814}
1815impl<T> Copy for Resource<T> {}
1816
1817impl<T: 'static> Resource<T> {
1818 #[track_caller]
1823 pub fn new<S, Fut, E>(
1824 source: impl Fn() -> S + 'static,
1825 fetcher: impl Fn(S) -> Fut + 'static,
1826 ) -> Resource<T>
1827 where
1828 S: Clone + PartialEq + 'static,
1829 Fut: Future<Output = Result<T, E>> + 'static,
1830 E: std::error::Error + Send + Sync + 'static,
1831 {
1832 let state: Signal<Load<T>> = Signal::new(Load::Loading);
1833 let refetch_count: Signal<u64> = Signal::new(0);
1834 let fs = Rc::new(FetchState {
1835 generation: Cell::new(0),
1836 abort: RefCell::new(None),
1837 });
1838 let reaction_fs = fs.clone();
1839 let prev: RefCell<Option<(S, u64)>> = RefCell::new(None);
1840 create_reaction(
1841 Rc::new(move || {
1842 let count = refetch_count.get();
1845 let s = source();
1846 let fs = reaction_fs.clone();
1847 untrack(|| {
1848 let same = prev
1852 .borrow()
1853 .as_ref()
1854 .is_some_and(|(ps, pc)| ps == &s && *pc == count);
1855 if same {
1856 return;
1857 }
1858 *prev.borrow_mut() = Some((s.clone(), count));
1859 let my_generation = fs.supersede();
1860 state.set(Load::Loading);
1861 let fut = fetcher(s);
1862 let done = fs.clone();
1863 let abort = spawn_local(Box::pin(async move {
1864 let result = fut.await;
1865 if done.generation.get() != my_generation {
1866 return; }
1868 done.abort.borrow_mut().take(); match result {
1870 Ok(v) => state.set(Load::Ready(v)),
1871 Err(e) => state.set(Load::Failed(Arc::new(e))),
1872 }
1873 }));
1874 *fs.abort.borrow_mut() = Some(abort);
1879 });
1880 }),
1881 1,
1882 );
1883 let cleanup_fs = fs;
1887 Scope::current().on_cleanup(move || {
1888 cleanup_fs.supersede();
1889 });
1890 Resource {
1891 state,
1892 refetch_count,
1893 _not_send: PhantomData,
1894 }
1895 }
1896
1897 pub fn signal(self) -> Signal<Load<T>> {
1899 self.state
1900 }
1901
1902 pub fn get(self) -> Load<T>
1904 where
1905 T: Clone,
1906 {
1907 self.state.get()
1908 }
1909
1910 pub fn with<R>(self, f: impl FnOnce(&Load<T>) -> R) -> R {
1912 self.state.with(f)
1913 }
1914
1915 pub fn loading(self) -> bool {
1917 self.with(Load::is_loading)
1918 }
1919
1920 pub fn ready(self) -> bool {
1922 self.with(Load::is_ready)
1923 }
1924
1925 pub fn refetch(self) {
1927 self.refetch_count.update(|c| *c = c.wrapping_add(1));
1928 }
1929}
1930
1931#[cfg(test)]
1936mod tests {
1937 use super::*;
1938 use std::cell::Cell as StdCell;
1939 use std::rc::Rc;
1940
1941 fn counter() -> (Rc<StdCell<u32>>, impl Fn()) {
1942 let c = Rc::new(StdCell::new(0));
1943 let c2 = c.clone();
1944 (c, move || c2.set(c2.get() + 1))
1945 }
1946
1947 #[test]
1948 fn signal_get_set() {
1949 let s = Signal::new(1);
1950 assert_eq!(s.get(), 1);
1951 s.set(5);
1952 assert_eq!(s.get(), 5);
1953 s.update(|v| *v += 1);
1954 assert_eq!(s.get(), 6);
1955 }
1956
1957 #[test]
1958 fn signal_teardown_does_not_write_after_scope_disposal() {
1959 let scope = Scope::child();
1960 let warned = with_rt(|rt| rt.warned_writes.len());
1961 scope.enter(|| {
1962 let signal = Signal::new(String::new());
1963 signal.write_preview("Fixture final edit".into());
1964 assert_eq!(signal.peek(), "Fixture final edit");
1965 Scope::current().on_cleanup(move || {
1966 signal.write_teardown("Fixture final edit".into());
1967 });
1968 });
1969 scope.dispose();
1970 assert_eq!(with_rt(|rt| rt.warned_writes.len()), warned);
1971 }
1972
1973 #[test]
1974 fn persistent_binding_teardown_seals_preview() {
1975 #[derive(Clone)]
1976 struct FixtureBinding(Rc<RefCell<Vec<String>>>);
1977 impl Binding<String> for FixtureBinding {
1978 fn read(&self) -> String {
1979 String::new()
1980 }
1981 fn peek(&self) -> String {
1982 self.read()
1983 }
1984 fn write(&self, value: String) {
1985 self.0.borrow_mut().push(value);
1986 }
1987 fn write_preview(&self, _value: String) {}
1988 }
1989 let committed = Rc::new(RefCell::new(Vec::new()));
1990 let binding = FixtureBinding(committed.clone());
1991 let scope = Scope::child();
1992 scope.enter(|| {
1993 binding.write_preview("Fixture edit".into());
1994 Scope::current().on_cleanup(move || binding.write_teardown("Fixture edit".into()));
1995 });
1996 assert!(committed.borrow().is_empty());
1997 scope.dispose();
1998 assert_eq!(*committed.borrow(), ["Fixture edit"]);
1999 }
2000
2001 #[test]
2002 fn effect_reruns_on_change() {
2003 let s = Signal::new(1);
2004 let (count, bump) = counter();
2005 Effect::new(move || {
2006 s.track();
2007 bump();
2008 });
2009 assert_eq!(count.get(), 1); batch(|| s.set(2));
2011 assert_eq!(count.get(), 2);
2012 batch(|| s.set(3));
2013 assert_eq!(count.get(), 3);
2014 }
2015
2016 #[test]
2017 fn batch_coalesces() {
2018 let s = Signal::new(0);
2019 let (count, bump) = counter();
2020 Effect::new(move || {
2021 s.track();
2022 bump();
2023 });
2024 batch(|| {
2025 s.set(1);
2026 s.set(2);
2027 s.set(3);
2028 });
2029 assert_eq!(count.get(), 2); assert_eq!(s.get(), 3);
2031 }
2032
2033 #[test]
2034 fn set_if_changed_no_op() {
2035 let s = Signal::new(7);
2036 let (count, bump) = counter();
2037 Effect::new(move || {
2038 s.track();
2039 bump();
2040 });
2041 batch(|| s.set_if_changed(7));
2042 assert_eq!(count.get(), 1); }
2044
2045 #[test]
2046 fn memo_caches_and_diffs() {
2047 let s = Signal::new(1);
2048 let computes = Rc::new(StdCell::new(0));
2049 let c2 = computes.clone();
2050 let doubled = Memo::new(move || {
2051 c2.set(c2.get() + 1);
2052 s.get() * 2
2053 });
2054 assert_eq!(doubled.get(), 2);
2055 assert_eq!(doubled.get(), 2);
2056 assert_eq!(computes.get(), 1); let (effect_runs, bump) = counter();
2058 Effect::new(move || {
2059 let _ = doubled.get();
2060 bump();
2061 });
2062 assert_eq!(effect_runs.get(), 1);
2063 batch(|| s.set(1)); assert_eq!(effect_runs.get(), 1); batch(|| s.set(5));
2067 assert_eq!(doubled.get(), 10);
2068 assert_eq!(effect_runs.get(), 2);
2069 }
2070
2071 #[test]
2072 fn bind_applies_on_change_only() {
2073 let s = Signal::new(1);
2074 let applied: Rc<RefCell<Vec<String>>> = Rc::new(RefCell::new(Vec::new()));
2075 let a2 = applied.clone();
2076 bind(
2077 move || s.get().to_string(),
2078 move |v| a2.borrow_mut().push(v.clone()),
2079 );
2080 assert_eq!(*applied.borrow(), vec!["1"]);
2081 batch(|| s.set(2));
2082 assert_eq!(*applied.borrow(), vec!["1", "2"]);
2083 batch(|| s.update(|v| *v = 2)); assert_eq!(*applied.borrow(), vec!["1", "2"]);
2085 }
2086
2087 #[test]
2088 fn watch_skips_initial_and_passes_old() {
2089 let s = Signal::new(10);
2090 type Log = Rc<RefCell<Vec<(i32, Option<i32>)>>>;
2091 let log: Log = Rc::new(RefCell::new(Vec::new()));
2092 let l2 = log.clone();
2093 watch(
2094 move || s.get(),
2095 move |new, old| l2.borrow_mut().push((*new, old.copied())),
2096 );
2097 assert!(log.borrow().is_empty());
2098 batch(|| s.set(11));
2099 assert_eq!(*log.borrow(), vec![(11, Some(10))]);
2100 }
2101
2102 #[test]
2103 fn scope_dispose_stops_effects_and_write_is_noop() {
2104 let s = Signal::new(0);
2105 let (count, bump) = counter();
2106 let scope = Scope::child();
2107 let inner = scope.enter(|| {
2108 let inner = Signal::new(1);
2109 Effect::new(move || {
2110 s.track();
2111 bump();
2112 });
2113 inner
2114 });
2115 assert_eq!(count.get(), 1);
2116 batch(|| s.set(1));
2117 assert_eq!(count.get(), 2);
2118 scope.dispose();
2119 batch(|| s.set(2));
2120 assert_eq!(count.get(), 2); inner.set(9); assert_eq!(inner.try_get(), None);
2123 }
2124
2125 #[test]
2126 fn dispose_during_drain_skips_pending() {
2127 let s = Signal::new(0);
2128 let scope = Scope::child();
2129 let (count, bump) = counter();
2130 let scope_cell = Rc::new(StdCell::new(Some(scope)));
2132 let sc = scope_cell.clone();
2133 Effect::new(move || {
2134 if s.get() == 1
2135 && let Some(scope) = sc.take()
2136 {
2137 scope.dispose();
2138 }
2139 });
2140 scope.enter(|| {
2141 Effect::new(move || {
2142 s.track();
2143 bump();
2144 });
2145 });
2146 assert_eq!(count.get(), 1);
2147 batch(|| s.set(1));
2148 assert_eq!(count.get(), 1);
2151 }
2152
2153 #[test]
2154 fn fixpoint_extends_drain() {
2155 let a = Signal::new(0);
2156 let b = Signal::new(0);
2157 let (count, bump) = counter();
2158 Effect::new(move || {
2159 if a.get() == 1 {
2160 b.set(1); }
2162 });
2163 Effect::new(move || {
2164 b.track();
2165 bump();
2166 });
2167 assert_eq!(count.get(), 1);
2168 batch(|| a.set(1));
2169 assert_eq!(count.get(), 2);
2170 assert_eq!(b.get(), 1);
2171 }
2172
2173 #[cfg(debug_assertions)]
2176 #[test]
2177 #[should_panic(expected = "re-ran more than")]
2178 fn rerun_cap_panics_in_debug() {
2179 let s = Signal::new(0);
2180 Effect::new(move || {
2181 let v = s.get();
2182 s.set(v + 1); });
2184 batch(|| s.set(1));
2185 }
2186
2187 #[test]
2188 fn context_provides_down() {
2189 #[derive(Clone, PartialEq, Debug)]
2190 struct Theme(u32);
2191 let scope = Scope::child();
2192 scope.provide(Theme(7));
2193 let child = scope.create_child();
2194 assert_eq!(child.use_context::<Theme>(), Some(Theme(7)));
2195 scope.dispose();
2196 }
2197
2198 #[test]
2199 fn trigger_notifies() {
2200 let t = Trigger::new();
2201 let (count, bump) = counter();
2202 Effect::new(move || {
2203 t.track();
2204 bump();
2205 });
2206 batch(|| t.notify());
2207 assert_eq!(count.get(), 2);
2208 }
2209
2210 #[test]
2211 fn turn_end_runs_after_fixpoint() {
2212 let s = Signal::new(0);
2213 let order: Rc<RefCell<Vec<&'static str>>> = Rc::new(RefCell::new(Vec::new()));
2214 let o1 = order.clone();
2215 Effect::new(move || {
2216 s.track();
2217 o1.borrow_mut().push("effect");
2218 });
2219 let o2 = order.clone();
2220 on_turn_end(move || o2.borrow_mut().push("turn-end"));
2221 order.borrow_mut().clear();
2222 batch(|| s.set(1));
2223 assert_eq!(*order.borrow(), vec!["effect", "turn-end"]);
2224 }
2225
2226 #[test]
2227 fn at_turn_end_waits_for_the_turn_and_runs_once() {
2228 let s = Signal::new(0);
2229 let order: Rc<RefCell<Vec<String>>> = Rc::new(RefCell::new(Vec::new()));
2230 let o = order.clone();
2232 at_turn_end(move || o.borrow_mut().push("now".into()));
2233 assert_eq!(*order.borrow(), vec!["now".to_string()]);
2234 order.borrow_mut().clear();
2235 let o1 = order.clone();
2237 Effect::new(move || {
2238 let v = s.get();
2239 o1.borrow_mut().push(format!("effect {v}"));
2240 });
2241 let o2 = order.clone();
2242 on_turn_end(move || o2.borrow_mut().push("standing".into()));
2243 order.borrow_mut().clear();
2244 let o3 = order.clone();
2245 batch(|| {
2246 at_turn_end(move || o3.borrow_mut().push("once".into()));
2247 s.set(1);
2248 });
2249 assert_eq!(*order.borrow(), vec!["effect 1", "once", "standing"]);
2250 order.borrow_mut().clear();
2252 let o4 = order.clone();
2253 batch(|| at_turn_end(move || o4.borrow_mut().push("quiet".into())));
2254 assert_eq!(*order.borrow(), vec!["quiet", "standing"]);
2255 }
2256
2257 #[test]
2258 fn bindings_run_before_effects() {
2259 let s = Signal::new(0);
2260 let order: Rc<RefCell<Vec<&'static str>>> = Rc::new(RefCell::new(Vec::new()));
2261 let o1 = order.clone();
2262 Effect::new(move || {
2264 s.track();
2265 o1.borrow_mut().push("effect");
2266 });
2267 let o2 = order.clone();
2268 bind(move || s.get(), move |_| o2.borrow_mut().push("bind"));
2269 order.borrow_mut().clear();
2270 batch(|| s.set(1));
2271 assert_eq!(*order.borrow(), vec!["bind", "effect"]);
2272 }
2273}
2274
2275#[cfg(test)]
2276mod resource_tests {
2277 use super::*;
2278 use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker};
2279
2280 thread_local! {
2286 static TEST_TASKS: RefCell<Vec<(u64, Option<LocalBoxFuture>)>> =
2287 const { RefCell::new(Vec::new()) };
2288 static NEXT_ID: Cell<u64> = const { Cell::new(1) };
2289 static ABORTED: RefCell<Vec<u64>> = const { RefCell::new(Vec::new()) };
2290 static INSTALLED: Cell<bool> = const { Cell::new(false) };
2291 }
2292
2293 fn noop_waker() -> Waker {
2294 fn raw() -> RawWaker {
2295 RawWaker::new(std::ptr::null(), &VT)
2296 }
2297 static VT: RawWakerVTable = RawWakerVTable::new(|_| raw(), |_| {}, |_| {}, |_| {});
2298 unsafe { Waker::from_raw(raw()) }
2300 }
2301
2302 fn poll_one(fut: &mut LocalBoxFuture) -> bool {
2303 let waker = noop_waker();
2304 let mut cx = Context::from_waker(&waker);
2305 fut.as_mut().poll(&mut cx).is_ready()
2306 }
2307
2308 fn install_test_spawner() {
2309 if INSTALLED.with(|c| c.replace(true)) {
2310 return;
2311 }
2312 install_spawner(|mut fut| {
2313 let id = NEXT_ID.with(|c| {
2314 let v = c.get();
2315 c.set(v + 1);
2316 v
2317 });
2318 if !poll_one(&mut fut) {
2319 TEST_TASKS.with(|t| t.borrow_mut().push((id, Some(fut))));
2320 }
2321 Box::new(move || {
2322 let removed = TEST_TASKS.with(|t| {
2323 let mut t = t.borrow_mut();
2324 let before = t.len();
2325 t.retain(|(i, _)| *i != id);
2326 before != t.len()
2327 });
2328 if removed {
2329 ABORTED.with(|a| a.borrow_mut().push(id));
2330 }
2331 })
2332 });
2333 }
2334
2335 fn aborted_count() -> usize {
2336 ABORTED.with(|a| a.borrow().len())
2337 }
2338
2339 fn pump() {
2341 let ids: Vec<u64> = TEST_TASKS.with(|t| t.borrow().iter().map(|(i, _)| *i).collect());
2342 for id in ids {
2343 let fut = TEST_TASKS.with(|t| {
2344 t.borrow_mut()
2345 .iter_mut()
2346 .find(|(i, _)| *i == id)
2347 .and_then(|(_, s)| s.take())
2348 });
2349 let Some(mut fut) = fut else { continue };
2350 if poll_one(&mut fut) {
2351 TEST_TASKS.with(|t| t.borrow_mut().retain(|(i, _)| *i != id));
2352 } else {
2353 TEST_TASKS.with(|t| {
2354 if let Some((_, s)) = t.borrow_mut().iter_mut().find(|(i, _)| *i == id) {
2355 *s = Some(fut);
2356 }
2357 });
2358 }
2359 }
2360 flush_sync();
2363 }
2364
2365 type Slot<T> = Rc<RefCell<Option<Result<T, TestErr>>>>;
2366
2367 fn manual_future<T: 'static>(slot: Slot<T>) -> impl Future<Output = Result<T, TestErr>> {
2370 std::future::poll_fn(move |_| match slot.borrow_mut().take() {
2371 Some(r) => Poll::Ready(r),
2372 None => Poll::Pending,
2373 })
2374 }
2375
2376 #[derive(Debug)]
2377 struct TestErr(&'static str);
2378 impl std::fmt::Display for TestErr {
2379 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2380 write!(f, "{}", self.0)
2381 }
2382 }
2383 impl std::error::Error for TestErr {}
2384
2385 #[test]
2386 fn resource_sync_fetcher_is_ready_immediately() {
2387 install_test_spawner();
2388 let r = Resource::new(|| (), |_| async { Ok::<_, std::convert::Infallible>(42) });
2389 assert!(r.ready());
2390 assert_eq!(r.with(|l| l.ready().copied()), Some(42));
2391 }
2392
2393 #[test]
2394 fn resource_loading_then_ready() {
2395 install_test_spawner();
2396 let slot: Slot<i32> = Rc::new(RefCell::new(None));
2397 let s2 = slot.clone();
2398 let r = Resource::new(move || (), move |_| manual_future(s2.clone()));
2399 assert!(r.loading());
2400 *slot.borrow_mut() = Some(Ok(7));
2401 pump();
2402 assert_eq!(r.with(|l| l.ready().copied()), Some(7));
2403 }
2404
2405 #[test]
2406 fn resource_failed_preserves_error() {
2407 install_test_spawner();
2408 let r = Resource::new(|| (), |_| async { Err::<i32, _>(TestErr("boom")) });
2409 assert!(!r.ready());
2410 assert_eq!(
2411 r.with(|l| l.error().map(|e| e.to_string())),
2412 Some("boom".into())
2413 );
2414 }
2415
2416 #[test]
2417 fn resource_refetches_on_source_change() {
2418 install_test_spawner();
2419 let src = Signal::new(1i32);
2420 let fetches = Rc::new(Cell::new(0));
2421 let f = fetches.clone();
2422 let r = Resource::new(
2423 move || src.get(),
2424 move |v| {
2425 f.set(f.get() + 1);
2426 async move { Ok::<_, TestErr>(v * 10) }
2427 },
2428 );
2429 assert_eq!(fetches.get(), 1);
2430 assert_eq!(r.with(|l| l.ready().copied()), Some(10));
2431 batch(|| src.set(2));
2432 assert_eq!(fetches.get(), 2);
2433 assert_eq!(r.with(|l| l.ready().copied()), Some(20));
2434 }
2435
2436 #[test]
2437 fn resource_source_equality_gates() {
2438 install_test_spawner();
2439 let a = Signal::new(1i32);
2440 let b = Signal::new(10i32);
2441 let fetches = Rc::new(Cell::new(0));
2442 let f = fetches.clone();
2443 let r = Resource::new(
2444 move || {
2445 let _ = b.get(); a.get()
2447 },
2448 move |v| {
2449 f.set(f.get() + 1);
2450 async move { Ok::<_, TestErr>(v) }
2451 },
2452 );
2453 assert_eq!(fetches.get(), 1);
2454 batch(|| b.set(20)); assert_eq!(fetches.get(), 1);
2456 assert_eq!(r.with(|l| l.ready().copied()), Some(1));
2457 batch(|| a.set(2)); assert_eq!(fetches.get(), 2);
2459 }
2460
2461 #[test]
2462 fn resource_refetch_forces() {
2463 install_test_spawner();
2464 let fetches = Rc::new(Cell::new(0));
2465 let f = fetches.clone();
2466 let r = Resource::new(
2467 || (),
2468 move |_| {
2469 f.set(f.get() + 1);
2470 async { Ok::<_, TestErr>(0) }
2471 },
2472 );
2473 assert_eq!(fetches.get(), 1);
2474 batch(|| r.refetch()); assert_eq!(fetches.get(), 2);
2476 }
2477
2478 #[test]
2479 fn resource_latest_wins() {
2480 install_test_spawner();
2481 let src = Signal::new(1i32);
2482 let slots: Rc<RefCell<Vec<Slot<i32>>>> = Rc::new(RefCell::new(Vec::new()));
2483 let sl = slots.clone();
2484 let r = Resource::new(
2485 move || src.get(),
2486 move |_| {
2487 let slot: Slot<i32> = Rc::new(RefCell::new(None));
2488 sl.borrow_mut().push(slot.clone());
2489 manual_future(slot)
2490 },
2491 );
2492 assert!(r.loading());
2493 let before = aborted_count();
2494 batch(|| src.set(2)); assert_eq!(aborted_count(), before + 1, "the stale task was aborted");
2496 *slots.borrow()[0].borrow_mut() = Some(Ok(111));
2498 pump();
2499 assert!(r.loading(), "a stale result writes nothing");
2500 *slots.borrow()[1].borrow_mut() = Some(Ok(222));
2501 pump();
2502 assert_eq!(r.with(|l| l.ready().copied()), Some(222));
2503 }
2504
2505 #[test]
2506 fn resource_scope_disposal_aborts() {
2507 install_test_spawner();
2508 let scope = Scope::child();
2509 let slot: Slot<i32> = Rc::new(RefCell::new(None));
2510 let s2 = slot.clone();
2511 let r = scope.enter(|| Resource::new(move || (), move |_| manual_future(s2.clone())));
2512 assert!(r.loading());
2513 let before = aborted_count();
2514 scope.dispose();
2515 assert_eq!(aborted_count(), before + 1, "disposal aborted the fetch");
2516 *slot.borrow_mut() = Some(Ok(1));
2518 pump();
2519 }
2520
2521 #[test]
2522 fn resource_ready_is_tracked() {
2523 install_test_spawner();
2524 let slot: Slot<i32> = Rc::new(RefCell::new(None));
2525 let s2 = slot.clone();
2526 let r = Resource::new(move || (), move |_| manual_future(s2.clone()));
2527 let runs = Rc::new(Cell::new(0));
2528 let rn = runs.clone();
2529 Effect::new(move || {
2530 let _ = r.ready();
2531 rn.set(rn.get() + 1);
2532 });
2533 assert_eq!(runs.get(), 1);
2534 *slot.borrow_mut() = Some(Ok(1));
2535 pump();
2536 assert!(runs.get() >= 2, "ready() re-ran the effect on completion");
2537 }
2538
2539 #[test]
2540 #[should_panic(expected = "no spawner installed")]
2541 fn spawner_missing_panics() {
2542 let _r = Resource::new(|| (), |_| async { Ok::<_, std::convert::Infallible>(1) });
2544 }
2545}
2546
2547#[cfg(test)]
2548mod unwind_safety_tests {
2549 use super::*;
2550
2551 fn contained(f: impl FnOnce() + std::panic::UnwindSafe) {
2553 let prev = std::panic::take_hook();
2554 std::panic::set_hook(Box::new(|_| {}));
2555 let r = std::panic::catch_unwind(f);
2556 std::panic::set_hook(prev);
2557 assert!(r.is_err(), "expected the closure to panic");
2558 }
2559
2560 #[test]
2564 fn panic_in_scope_enter_restores_current_scope() {
2565 let before = with_rt(|rt| rt.current_scope);
2566 let scope = Scope::detached();
2567 contained(|| scope.enter(|| panic!("boom in enter")));
2568 assert_eq!(with_rt(|rt| rt.current_scope), before, "scope not restored");
2569 let s = Signal::new(7u32);
2571 assert_eq!(s.get(), 7);
2572 }
2573
2574 #[test]
2581 fn panic_in_effect_body_pops_its_observer_frame() {
2582 let depth = with_rt(|rt| rt.observers.len());
2583 let boom = Signal::new(false);
2584 Effect::new(move || {
2585 if boom.get() {
2586 panic!("effect boom");
2587 }
2588 });
2589 assert_eq!(with_rt(|rt| rt.observers.len()), depth);
2590 contained(|| batch(|| boom.set(true)));
2591 assert_eq!(
2592 with_rt(|rt| rt.observers.len()),
2593 depth,
2594 "observer frame stranded by the panicking effect run"
2595 );
2596 recover_from_panic(); }
2598
2599 #[test]
2600 fn panic_in_untrack_restores_tracking() {
2601 let depth = with_rt(|rt| rt.observers.len());
2602 contained(|| untrack(|| panic!("boom in untrack")));
2603 assert_eq!(with_rt(|rt| rt.observers.len()), depth, "observer stranded");
2604
2605 let src = Signal::new(0i32);
2607 let runs = Rc::new(RefCell::new(0));
2608 let r2 = runs.clone();
2609 Effect::new(move || {
2610 src.get();
2611 *r2.borrow_mut() += 1;
2612 });
2613 flush_sync();
2614 let before = *runs.borrow();
2615 src.set(1);
2616 flush_sync();
2617 assert!(
2618 *runs.borrow() > before,
2619 "effect did not re-run — tracking is off"
2620 );
2621 }
2622
2623 #[test]
2626 fn panic_in_batch_restores_depth() {
2627 let depth = with_rt(|rt| rt.batch_depth);
2628 contained(|| batch(|| panic!("boom in batch")));
2629 assert_eq!(with_rt(|rt| rt.batch_depth), depth, "batch depth stranded");
2630 }
2631
2632 #[test]
2636 fn panic_in_with_leaves_the_value_intact() {
2637 let s = Signal::new(String::from("intact"));
2638 contained(|| s.with(|_| panic!("boom in with")));
2639 assert_eq!(s.with(|v| v.clone()), "intact");
2640 }
2641
2642 #[test]
2645 fn reentrant_read_inside_with_succeeds() {
2646 let s = Signal::new(5u32);
2647 let doubled = s.with(|outer| *outer + s.with(|inner| *inner));
2648 assert_eq!(doubled, 10);
2649 assert_eq!(
2650 s.with(|v| *v),
2651 5,
2652 "value still in place after the nested read"
2653 );
2654 }
2655
2656 #[test]
2659 fn nested_reads_of_distinct_signals_succeed() {
2660 let a = Signal::new(2u32);
2661 let b = Signal::new(3u32);
2662 assert_eq!(a.with(|x| *x * b.with(|y| *y)), 6);
2663 }
2664
2665 #[test]
2667 fn write_during_read_panics_with_a_clear_message() {
2668 let s = Signal::new(1u32);
2669 let prev = std::panic::take_hook();
2670 std::panic::set_hook(Box::new(|_| {}));
2671 let r = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
2672 s.with(|_| s.set(2));
2673 }));
2674 std::panic::set_hook(prev);
2675 let payload = r.unwrap_err();
2676 let msg = payload
2678 .downcast_ref::<&str>()
2679 .map(|s| s.to_string())
2680 .or_else(|| payload.downcast_ref::<String>().cloned())
2681 .expect("panic payload should be a string");
2682 assert!(
2683 msg.contains("in flight") && msg.contains("Signal"),
2684 "message should name the cause, got: {msg}"
2685 );
2686 }
2687
2688 #[cfg(not(debug_assertions))]
2693 #[test]
2694 fn rerun_cap_defers_instead_of_killing_the_binding() {
2695 let s = Signal::new(0u32);
2696 let runs = std::rc::Rc::new(std::cell::Cell::new(0u32));
2697 let r2 = runs.clone();
2698 Effect::new(move || {
2699 let v = s.get();
2700 r2.set(r2.get() + 1);
2701 if v < 1_000_000 {
2702 s.set(v + 1); }
2704 });
2705 flush_sync();
2706 let after_cycle = runs.get();
2707 assert!(
2708 after_cycle <= RERUN_CAP + 1,
2709 "cap did not bound the drain: {after_cycle} runs"
2710 );
2711
2712 batch(|| s.set(2_000_000)); assert_eq!(
2715 runs.get(),
2716 after_cycle + 1,
2717 "binding stayed dead after hitting the re-run cap"
2718 );
2719 }
2720}