#[macro_export]
macro_rules! tls_group {
($( $(#[$attr:meta])* $vis:vis static $name:ident : $ty:ty = $init:expr ; )+) => {
#[allow(non_snake_case)]
struct TlsGroupSlots { $( $name: $ty, )+ }
::std::thread_local! {
static TLS_GROUP: TlsGroupSlots = TlsGroupSlots { $( $name: $init, )+ };
}
$(
$(#[$attr])*
#[allow(non_camel_case_types, dead_code)]
#[derive(Clone, Copy)]
$vis struct $name;
impl $name {
#[inline]
#[allow(dead_code)]
$vis fn with<R>(self, f: impl ::core::ops::FnOnce(&$ty) -> R) -> R {
TLS_GROUP.with(|g| f(&g.$name))
}
#[inline]
#[allow(dead_code)]
$vis fn try_with<R>(
self,
f: impl ::core::ops::FnOnce(&$ty) -> R,
) -> ::core::result::Result<R, ::std::thread::AccessError> {
TLS_GROUP.try_with(|g| f(&g.$name))
}
}
)+
};
}
#[macro_export]
macro_rules! tls_root {
($( $(#[$attr:meta])* $field:ident : $ty:ty ),+ $(,)?) => {
#[allow(non_snake_case)]
pub(crate) struct TlsRoot { $( $(#[$attr])* pub(crate) $field: $ty, )+ }
::std::thread_local! {
static TLS_ROOT: TlsRoot =
TlsRoot { $( $(#[$attr])* $field: ::core::default::Default::default(), )+ };
}
#[inline]
pub(crate) fn tls_root<R>(f: impl ::core::ops::FnOnce(&TlsRoot) -> R) -> R {
TLS_ROOT.with(f)
}
#[inline]
#[allow(dead_code)]
pub(crate) fn try_tls_root<R>(
f: impl ::core::ops::FnOnce(&TlsRoot) -> R,
) -> ::core::result::Result<R, ::std::thread::AccessError> {
TLS_ROOT.try_with(f)
}
};
}
#[allow(clippy::crate_in_macro_def)]
#[macro_export]
macro_rules! tls_slots {
($field:ident; $( $(#[$attr:meta])* $vis:vis static $name:ident : $ty:ty = $init:expr ; )+) => {
#[allow(non_snake_case)]
pub(crate) struct TlsGroupSlots { $( $name: $ty, )+ }
impl ::core::default::Default for TlsGroupSlots {
fn default() -> Self { Self { $( $name: $init, )+ } }
}
$(
$(#[$attr])*
#[allow(non_camel_case_types, dead_code)]
#[derive(Clone, Copy)]
$vis struct $name;
impl $name {
#[inline]
#[allow(dead_code)]
$vis fn with<R>(self, f: impl ::core::ops::FnOnce(&$ty) -> R) -> R {
crate::tls_root(|r| f(&r.$field.$name))
}
#[inline]
#[allow(dead_code)]
$vis fn try_with<R>(
self,
f: impl ::core::ops::FnOnce(&$ty) -> R,
) -> ::core::result::Result<R, ::std::thread::AccessError> {
crate::try_tls_root(|r| f(&r.$field.$name))
}
}
)+
};
}
use std::any::{Any, TypeId};
use std::cell::{Cell, RefCell};
use std::collections::{HashMap, HashSet};
use std::future::Future;
use std::marker::PhantomData;
use std::panic::Location;
use std::pin::Pin;
use std::rc::Rc;
use std::sync::Arc;
use slotmap::{Key, SlotMap, new_key_type};
new_key_type! {
pub struct NodeKey;
pub struct ScopeKey;
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug)]
enum NodeState {
Clean,
Check,
Dirty,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum NodeKind {
Signal,
Memo,
Reaction,
}
type MemoEq = fn(&dyn Any, &dyn Any) -> bool;
type NodeValue = Rc<RefCell<Box<dyn Any>>>;
struct Node {
kind: NodeKind,
state: NodeState,
value: Option<NodeValue>,
memo_compute: Option<Rc<dyn Fn() -> Box<dyn Any>>>,
memo_eq: Option<MemoEq>,
reaction: Option<Rc<dyn Fn()>>,
sources: Vec<NodeKey>,
run_cleanups: Vec<Box<dyn FnOnce()>>,
observers: Vec<NodeKey>,
#[allow(dead_code)] scope: ScopeKey,
priority: u8,
depth: u32,
seq: u64,
last_changed: u64,
last_run: u64,
queued: bool,
created_at: &'static Location<'static>,
}
struct ScopeData {
parent: ScopeKey,
children: Vec<ScopeKey>,
nodes: Vec<NodeKey>,
cleanups: Vec<Box<dyn FnOnce()>>,
context: HashMap<TypeId, Box<dyn Any>>,
depth: u32,
}
struct Runtime {
nodes: SlotMap<NodeKey, Node>,
scopes: SlotMap<ScopeKey, ScopeData>,
root_scope: ScopeKey,
current_scope: ScopeKey,
observers: Vec<Option<NodeKey>>,
pending: Vec<NodeKey>,
batch_depth: u32,
draining: bool,
tick: u64,
next_seq: u64,
scheduler: Option<Rc<dyn Fn()>>,
schedule_posted: bool,
spawner: Option<Spawner>,
turn_end: Vec<Rc<dyn Fn()>>,
turn_end_once: Vec<Box<dyn FnOnce()>>,
warned_writes: HashSet<*const Location<'static>>,
}
impl Runtime {
fn new() -> Self {
let mut scopes = SlotMap::with_key();
let root_scope = scopes.insert(ScopeData {
parent: ScopeKey::null(),
children: Vec::new(),
nodes: Vec::new(),
cleanups: Vec::new(),
context: HashMap::new(),
depth: 0,
});
Runtime {
nodes: SlotMap::with_key(),
scopes,
root_scope,
current_scope: root_scope,
observers: Vec::new(),
pending: Vec::new(),
batch_depth: 0,
draining: false,
tick: 1,
next_seq: 0,
scheduler: None,
schedule_posted: false,
turn_end_once: Vec::new(),
spawner: None,
turn_end: Vec::new(),
warned_writes: HashSet::new(),
}
}
}
crate::tls_group! {
static RT: RefCell<Runtime> = RefCell::new(Runtime::new());
}
const RERUN_CAP: u32 = 100;
const TURN_ROUND_CAP: u32 = 8;
fn with_rt<R>(f: impl FnOnce(&mut Runtime) -> R) -> R {
RT.with(|rt| f(&mut rt.borrow_mut()))
}
#[track_caller]
fn create_node(rt: &mut Runtime, kind: NodeKind, scope: ScopeKey) -> NodeKey {
let depth = rt.scopes.get(scope).map(|s| s.depth).unwrap_or(0);
let seq = rt.next_seq;
rt.next_seq += 1;
let key = rt.nodes.insert(Node {
kind,
state: NodeState::Clean,
value: None,
memo_compute: None,
memo_eq: None,
reaction: None,
sources: Vec::new(),
run_cleanups: Vec::new(),
observers: Vec::new(),
scope,
priority: 1,
depth,
seq,
last_changed: 0,
last_run: 0,
queued: false,
created_at: Location::caller(),
});
if let Some(s) = rt.scopes.get_mut(scope) {
s.nodes.push(key);
}
key
}
fn track_read(rt: &mut Runtime, source: NodeKey) {
if let Some(Some(obs)) = rt.observers.last().copied() {
if obs == source {
return;
}
let already = rt.nodes[obs].sources.contains(&source);
if !already {
rt.nodes[obs].sources.push(source);
rt.nodes[source].observers.push(obs);
}
}
}
#[must_use]
fn clear_sources(rt: &mut Runtime, key: NodeKey) -> Vec<Box<dyn FnOnce()>> {
let sources = std::mem::take(&mut rt.nodes[key].sources);
for s in sources {
if let Some(n) = rt.nodes.get_mut(s)
&& let Some(pos) = n.observers.iter().position(|&o| o == key)
{
n.observers.swap_remove(pos);
}
}
std::mem::take(&mut rt.nodes[key].run_cleanups)
}
fn mark_observers(rt: &mut Runtime, source: NodeKey, level: NodeState) {
let mut stack: Vec<(NodeKey, NodeState)> = rt.nodes[source]
.observers
.iter()
.map(|&o| (o, level))
.collect();
while let Some((key, level)) = stack.pop() {
let Some(node) = rt.nodes.get_mut(key) else {
continue;
};
if node.state >= level {
continue; }
node.state = level;
match node.kind {
NodeKind::Reaction => {
if !node.queued {
node.queued = true;
rt.pending.push(key);
}
}
NodeKind::Memo => {
for &o in rt.nodes[key].observers.iter() {
stack.push((o, NodeState::Check));
}
}
NodeKind::Signal => {}
}
}
}
fn refresh_memo(key: NodeKey) {
let (state, kind) = match with_rt(|rt| rt.nodes.get(key).map(|n| (n.state, n.kind))) {
Some(v) => v,
None => return,
};
if kind != NodeKind::Memo || state == NodeState::Clean {
return;
}
if state == NodeState::Check {
let (sources, last_run) =
with_rt(|rt| (rt.nodes[key].sources.clone(), rt.nodes[key].last_run));
let mut any_changed = false;
for s in sources {
refresh_memo(s);
if with_rt(|rt| {
rt.nodes
.get(s)
.map(|n| n.last_changed > last_run)
.unwrap_or(false)
}) {
any_changed = true;
}
}
if !any_changed {
with_rt(|rt| {
if let Some(n) = rt.nodes.get_mut(key) {
n.state = NodeState::Clean;
}
});
return;
}
}
let compute = with_rt(|rt| rt.nodes[key].memo_compute.clone());
let Some(compute) = compute else { return };
let cleanups = with_rt(|rt| clear_sources(rt, key));
for f in cleanups {
f();
}
with_rt(|rt| rt.observers.push(Some(key)));
let new_value = {
let _g = RtGuard(|rt: &mut Runtime| {
rt.observers.pop();
});
compute()
};
with_rt(|rt| {
let tick = rt.tick;
let node = &mut rt.nodes[key];
let changed = match (node.value.as_ref(), node.memo_eq) {
(Some(old), Some(eq)) => {
let old = old.borrow();
!eq(old.as_ref(), new_value.as_ref())
}
_ => true,
};
node.last_run = tick;
node.state = NodeState::Clean;
if changed {
node.value = Some(Rc::new(RefCell::new(new_value)));
node.last_changed = tick;
rt.tick += 1;
}
});
}
fn run_reaction(key: NodeKey) {
let info = with_rt(|rt| {
rt.nodes.get_mut(key).map(|n| {
n.queued = false;
(n.state, n.sources.clone(), n.last_run)
})
});
let Some((state, sources, last_run)) = info else {
return;
};
if state == NodeState::Clean {
return;
}
if state == NodeState::Check {
let mut any_changed = false;
for s in &sources {
refresh_memo(*s);
if with_rt(|rt| {
rt.nodes
.get(*s)
.map(|n| n.last_changed > last_run)
.unwrap_or(false)
}) {
any_changed = true;
break;
}
}
if !any_changed {
with_rt(|rt| {
if let Some(n) = rt.nodes.get_mut(key) {
n.state = NodeState::Clean;
}
});
return;
}
}
let reaction = with_rt(|rt| rt.nodes.get(key).and_then(|n| n.reaction.clone()));
let Some(reaction) = reaction else { return };
let cleanups = with_rt(|rt| {
let cleanups = clear_sources(rt, key);
if let Some(n) = rt.nodes.get_mut(key) {
n.state = NodeState::Clean;
}
cleanups
});
for f in cleanups {
f();
}
with_rt(|rt| rt.observers.push(Some(key)));
{
let _g = RtGuard(|rt: &mut Runtime| {
rt.observers.pop();
});
reaction();
}
with_rt(|rt| {
let tick = rt.tick;
rt.tick += 1;
if let Some(n) = rt.nodes.get_mut(key) {
n.last_run = tick;
}
});
}
pub fn flush_sync() {
let already = with_rt(|rt| {
if rt.draining {
return true;
}
rt.draining = true;
false
});
if already {
return; }
let mut round = 0u32;
'turn: loop {
round += 1;
let mut run_counts: HashMap<NodeKey, u32> = HashMap::new();
loop {
let mut batch = with_rt(|rt| std::mem::take(&mut rt.pending));
if batch.is_empty() {
break;
}
with_rt(|rt| {
batch.sort_by_key(|&k| {
rt.nodes
.get(k)
.map(|n| (n.priority, n.depth, n.seq))
.unwrap_or((u8::MAX, u32::MAX, u64::MAX))
})
});
for key in batch {
let count = run_counts.entry(key).or_insert(0);
*count += 1;
if *count > RERUN_CAP {
if *count == RERUN_CAP + 1 {
let loc = with_rt(|rt| rt.nodes.get(key).map(|n| n.created_at));
if let Some(loc) = loc {
if cfg!(debug_assertions) {
panic!(
"day-reactive: effect created at {loc} re-ran more than {RERUN_CAP} times in one drain (reactive cycle?)"
);
} else {
log::warn!(
"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)"
);
}
}
}
with_rt(|rt| {
if let Some(n) = rt.nodes.get_mut(key) {
n.queued = false;
n.state = NodeState::Clean;
}
});
continue;
}
run_reaction(key);
}
}
let (once, turn_end) = with_rt(|rt| {
rt.draining = false;
rt.schedule_posted = false;
(std::mem::take(&mut rt.turn_end_once), rt.turn_end.clone())
});
for cb in once {
cb();
}
for cb in turn_end {
cb();
}
let more = with_rt(|rt| {
if rt.pending.is_empty() {
return false;
}
rt.draining = true;
true
});
if !more {
return;
}
if round >= TURN_ROUND_CAP {
with_rt(|rt| rt.draining = false);
if cfg!(debug_assertions) {
panic!(
"day-reactive: turn-end callbacks scheduled new reactive work for {TURN_ROUND_CAP} straight rounds in one turn (hooks feeding each other?)"
);
} else {
log::warn!(
"day-reactive: turn-end callbacks still scheduling work after {TURN_ROUND_CAP} rounds; deferring the rest to the next turn"
);
}
return;
}
continue 'turn;
}
}
pub fn recover_from_panic() {
with_rt(|rt| {
rt.draining = false;
rt.schedule_posted = false;
rt.batch_depth = 0;
rt.turn_end_once.clear();
rt.pending.clear();
rt.observers.clear();
rt.current_scope = rt.root_scope;
});
}
fn schedule_after_write(rt: &mut Runtime) -> Option<Rc<dyn Fn()>> {
if rt.batch_depth > 0 || rt.draining {
return None;
}
if rt.schedule_posted {
return None;
}
rt.schedule_posted = true;
rt.scheduler.clone()
}
fn signal_write_boxed(key: NodeKey, apply: impl FnOnce(&mut Box<dyn Any>) -> bool) {
let poster = with_rt(|rt| {
let node = rt.nodes.get_mut(key)?;
let cell = node.value.as_ref()?.clone();
let mut value = match cell.try_borrow_mut() {
Ok(v) => v,
Err(_) => panic!(
"day-reactive: wrote to a Signal while a with()/try_with() read of it is still \
in flight — the write would be lost. Finish the read closure first (copy the \
value out with get()) and write after it returns."
),
};
let changed = apply(&mut value);
drop(value);
let node = rt.nodes.get_mut(key)?;
if !changed {
return None;
}
node.last_changed = rt.tick;
rt.tick += 1;
mark_observers(rt, key, NodeState::Dirty);
schedule_after_write(rt)
});
if let Some(post) = poster {
post();
}
}
struct RtGuard<F: FnMut(&mut Runtime)>(F);
impl<F: FnMut(&mut Runtime)> Drop for RtGuard<F> {
fn drop(&mut self) {
with_rt(|rt| (self.0)(rt));
}
}
pub fn batch<R>(f: impl FnOnce() -> R) -> R {
with_rt(|rt| rt.batch_depth += 1);
let r = {
let _g = RtGuard(|rt: &mut Runtime| rt.batch_depth = rt.batch_depth.saturating_sub(1));
f()
};
let should_drain = with_rt(|rt| {
rt.batch_depth == 0
&& !rt.draining
&& (!rt.pending.is_empty() || !rt.turn_end_once.is_empty())
});
if should_drain {
flush_sync();
}
r
}
pub fn flush_now() {
let saved = with_rt(|rt| std::mem::replace(&mut rt.batch_depth, 0));
let _g = RtGuard(move |rt: &mut Runtime| rt.batch_depth = saved);
flush_sync();
}
pub fn untrack<R>(f: impl FnOnce() -> R) -> R {
with_rt(|rt| rt.observers.push(None));
let _g = RtGuard(|rt: &mut Runtime| {
rt.observers.pop();
});
f()
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub struct RunId(NodeKey);
pub fn active_run() -> Option<RunId> {
with_rt(|rt| rt.observers.last().copied().flatten()).map(RunId)
}
pub fn on_run_retrack(f: impl FnOnce() + 'static) -> bool {
with_rt(|rt| {
let Some(Some(key)) = rt.observers.last().copied() else {
return false;
};
match rt.nodes.get_mut(key) {
Some(n) => {
n.run_cleanups.push(Box::new(f));
true
}
None => false,
}
})
}
pub fn install_scheduler(post: impl Fn() + 'static) {
with_rt(|rt| rt.scheduler = Some(Rc::new(post)));
}
pub type LocalBoxFuture = Pin<Box<dyn Future<Output = ()> + 'static>>;
type Spawner = Rc<dyn Fn(LocalBoxFuture) -> Box<dyn FnOnce()>>;
pub fn install_spawner(spawn: impl Fn(LocalBoxFuture) -> Box<dyn FnOnce()> + 'static) {
with_rt(|rt| rt.spawner = Some(Rc::new(spawn)));
}
fn spawn_local(fut: LocalBoxFuture) -> Box<dyn FnOnce()> {
let spawner = with_rt(|rt| rt.spawner.clone());
match spawner {
Some(s) => s(fut),
None => panic!("day-reactive: no spawner installed (backend not started)"),
}
}
pub fn on_turn_end(cb: impl Fn() + 'static) {
with_rt(|rt| rt.turn_end.push(Rc::new(cb)));
}
pub fn at_turn_end(cb: impl FnOnce() + 'static) {
let now = with_rt(|rt| {
if rt.draining || rt.batch_depth > 0 {
rt.turn_end_once.push(Box::new(cb));
None
} else {
Some(cb)
}
});
if let Some(cb) = now {
cb();
}
}
type MainPoster = Box<dyn Fn(Box<dyn FnOnce() + Send>) + Send + Sync>;
static MAIN_POSTER: std::sync::OnceLock<MainPoster> = std::sync::OnceLock::new();
pub fn install_main_poster(post: impl Fn(Box<dyn FnOnce() + Send>) + Send + Sync + 'static) {
let _ = MAIN_POSTER.set(Box::new(post));
}
pub fn has_main_poster() -> bool {
MAIN_POSTER.get().is_some()
}
pub fn on_main(f: impl FnOnce() + Send + 'static) {
match MAIN_POSTER.get() {
Some(post) => post(Box::new(f)),
None => panic!("day-reactive: no main poster installed (backend not started)"),
}
}
type DelayedPoster = Box<dyn Fn(u32, Box<dyn FnOnce() + Send>) + Send + Sync>;
static DELAYED_POSTER: std::sync::OnceLock<DelayedPoster> = std::sync::OnceLock::new();
pub fn install_delayed_poster(
post: impl Fn(u32, Box<dyn FnOnce() + Send>) + Send + Sync + 'static,
) {
let _ = DELAYED_POSTER.set(Box::new(post));
}
pub fn on_main_delayed(ms: u32, f: impl FnOnce() + Send + 'static) {
match DELAYED_POSTER.get() {
Some(post) => post(ms, Box::new(f)),
None => panic!("day-reactive: no delayed poster installed (backend not started)"),
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub struct Scope {
key: ScopeKey,
_not_send: PhantomData<*const ()>,
}
impl Scope {
pub fn current() -> Scope {
Scope {
key: with_rt(|rt| rt.current_scope),
_not_send: PhantomData,
}
}
pub fn root() -> Scope {
Scope {
key: with_rt(|rt| rt.root_scope),
_not_send: PhantomData,
}
}
pub fn child() -> Scope {
Scope::current().create_child()
}
pub fn create_child(self) -> Scope {
let key = with_rt(|rt| {
let depth = rt.scopes.get(self.key).map(|s| s.depth + 1).unwrap_or(1);
let child = rt.scopes.insert(ScopeData {
parent: self.key,
children: Vec::new(),
nodes: Vec::new(),
cleanups: Vec::new(),
context: HashMap::new(),
depth,
});
if let Some(p) = rt.scopes.get_mut(self.key) {
p.children.push(child);
}
child
});
Scope {
key,
_not_send: PhantomData,
}
}
pub fn detached() -> Scope {
let key = with_rt(|rt| {
rt.scopes.insert(ScopeData {
parent: ScopeKey::null(),
children: Vec::new(),
nodes: Vec::new(),
cleanups: Vec::new(),
context: HashMap::new(),
depth: 0,
})
});
Scope {
key,
_not_send: PhantomData,
}
}
pub fn enter<R>(self, f: impl FnOnce() -> R) -> R {
let prev = with_rt(|rt| std::mem::replace(&mut rt.current_scope, self.key));
let _g = RtGuard(move |rt: &mut Runtime| rt.current_scope = prev);
f()
}
pub fn on_cleanup(self, f: impl FnOnce() + 'static) {
with_rt(|rt| {
if let Some(s) = rt.scopes.get_mut(self.key) {
s.cleanups.push(Box::new(f));
}
});
}
pub fn is_alive(self) -> bool {
with_rt(|rt| rt.scopes.contains_key(self.key))
}
pub fn dispose(self) {
let children = match with_rt(|rt| rt.scopes.get(self.key).map(|s| s.children.clone())) {
Some(c) => c,
None => return,
};
for c in children {
(Scope {
key: c,
_not_send: PhantomData,
})
.dispose();
}
let (nodes, cleanups, parent) = match with_rt(|rt| {
rt.scopes
.remove(self.key)
.map(|s| (s.nodes, s.cleanups, s.parent))
}) {
Some(v) => v,
None => return,
};
let mut run_cleanups: Vec<Box<dyn FnOnce()>> = Vec::new();
with_rt(|rt| {
if let Some(p) = rt.scopes.get_mut(parent)
&& let Some(pos) = p.children.iter().position(|&c| c == self.key)
{
p.children.swap_remove(pos);
}
for key in nodes {
run_cleanups.extend(clear_sources(rt, key));
if let Some(node) = rt.nodes.get_mut(key) {
let observers = std::mem::take(&mut node.observers);
for o in observers {
if let Some(on) = rt.nodes.get_mut(o)
&& let Some(pos) = on.sources.iter().position(|&s| s == key)
{
on.sources.swap_remove(pos);
}
}
}
rt.nodes.remove(key);
}
});
for f in run_cleanups {
f();
}
for c in cleanups {
c();
}
}
pub fn provide<T: 'static>(self, value: T) {
with_rt(|rt| {
if let Some(s) = rt.scopes.get_mut(self.key) {
s.context.insert(TypeId::of::<T>(), Box::new(value));
}
});
}
pub fn use_context<T: Clone + 'static>(self) -> Option<T> {
with_rt(|rt| {
let mut cur = self.key;
while let Some(s) = rt.scopes.get(cur) {
if let Some(v) = s.context.get(&TypeId::of::<T>()) {
return v.downcast_ref::<T>().cloned();
}
cur = s.parent;
}
None
})
}
}
pub struct Signal<T: 'static> {
key: NodeKey,
created_at: &'static Location<'static>,
_m: PhantomData<*const T>,
}
impl<T: 'static> Clone for Signal<T> {
fn clone(&self) -> Self {
*self
}
}
impl<T: 'static> Copy for Signal<T> {}
impl<T: 'static> std::fmt::Debug for Signal<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Signal({:?})", self.key)
}
}
impl<T: 'static> Signal<T> {
#[track_caller]
pub fn new(value: T) -> Self {
Self::new_in(Scope::current(), value)
}
#[track_caller]
pub fn global(value: T) -> Self {
Self::new_in(Scope::root(), value)
}
#[track_caller]
pub fn new_in(scope: Scope, value: T) -> Self {
let created_at = Location::caller();
let key = with_rt(|rt| {
let k = create_node(rt, NodeKind::Signal, scope.key);
rt.nodes[k].value = Some(Rc::new(RefCell::new(Box::new(value) as Box<dyn Any>)));
rt.nodes[k].created_at = created_at;
k
});
Signal {
key,
created_at,
_m: PhantomData,
}
}
#[track_caller]
pub fn with<R>(self, f: impl FnOnce(&T) -> R) -> R {
match self.try_with(f) {
Some(r) => r,
None => panic!(
"day-reactive: read of disposed Signal created at {} (use try_with in closures that can outlive their scope)",
self.created_at
),
}
}
pub fn try_with<R>(self, f: impl FnOnce(&T) -> R) -> Option<R> {
let cell = with_rt(|rt| {
track_read(rt, self.key);
rt.nodes.get(self.key).and_then(|n| n.value.clone())
})?;
let value = cell.borrow();
value.downcast_ref::<T>().map(f)
}
pub fn with_untracked<R>(self, f: impl FnOnce(&T) -> R) -> R {
untrack(|| self.with(f))
}
#[track_caller]
pub fn get(self) -> T
where
T: Clone,
{
self.with(|v| v.clone())
}
pub fn try_get(self) -> Option<T>
where
T: Clone,
{
self.try_with(|v| v.clone())
}
pub fn get_untracked(self) -> T
where
T: Clone,
{
untrack(|| self.get())
}
pub fn track(self) {
with_rt(|rt| track_read(rt, self.key));
}
#[track_caller]
pub fn set(self, value: T) {
self.write_check(move |slot| {
*slot = value;
true
});
}
#[track_caller]
pub fn set_if_changed(self, value: T)
where
T: PartialEq,
{
self.write_check(move |slot| {
if *slot == value {
false
} else {
*slot = value;
true
}
});
}
#[track_caller]
pub fn update(self, f: impl FnOnce(&mut T)) {
self.write_check(move |slot| {
f(slot);
true
});
}
#[track_caller]
fn write_check(self, apply: impl FnOnce(&mut T) -> bool) {
let alive = with_rt(|rt| rt.nodes.contains_key(self.key));
if !alive {
let loc = Location::caller();
let first = with_rt(|rt| rt.warned_writes.insert(loc as *const _));
if first && cfg!(debug_assertions) {
log::warn!(
"day-reactive: write at {loc} to a disposed signal (created at {}) ignored",
self.created_at
);
}
return;
}
signal_write_boxed(self.key, |boxed| match boxed.downcast_mut::<T>() {
Some(slot) => apply(slot),
None => false,
});
}
pub fn setter(self) -> Setter<T>
where
T: Send,
{
Setter {
key: self.key,
_m: PhantomData,
}
}
}
pub struct Setter<T: Send + 'static> {
key: NodeKey,
_m: PhantomData<fn(T)>,
}
impl<T: Send + 'static> Clone for Setter<T> {
fn clone(&self) -> Self {
*self
}
}
impl<T: Send + 'static> Copy for Setter<T> {}
impl<T: Send + 'static> Setter<T> {
pub fn set(self, value: T) {
let key = self.key;
on_main(move || {
let poster = with_rt(|rt| {
let cell = rt.nodes.get(key)?.value.clone()?;
{
let mut boxed = cell.try_borrow_mut().ok()?;
let slot = boxed.downcast_mut::<T>()?;
*slot = value;
}
let node = rt.nodes.get_mut(key)?;
node.last_changed = rt.tick;
rt.tick += 1;
mark_observers(rt, key, NodeState::Dirty);
schedule_after_write(rt)
});
if let Some(post) = poster {
post();
}
});
}
}
pub struct Memo<T: 'static> {
key: NodeKey,
created_at: &'static Location<'static>,
_m: PhantomData<*const T>,
}
impl<T: 'static> Clone for Memo<T> {
fn clone(&self) -> Self {
*self
}
}
impl<T: 'static> Copy for Memo<T> {}
impl<T: 'static> Memo<T> {
#[track_caller]
pub fn new(f: impl Fn() -> T + 'static) -> Self
where
T: PartialEq,
{
Self::new_with_eq(f, |a, b| a == b)
}
#[track_caller]
pub fn new_with_eq(f: impl Fn() -> T + 'static, eq: fn(&T, &T) -> bool) -> Self {
let created_at = Location::caller();
let key = with_rt(|rt| {
let scope = rt.current_scope;
create_node(rt, NodeKind::Memo, scope)
});
let compute: Rc<dyn Fn() -> Box<dyn Any>> =
Rc::new(move || Box::new(EqBox { value: f(), eq }) as Box<dyn Any>);
with_rt(|rt| {
let node = &mut rt.nodes[key];
node.memo_compute = Some(compute);
node.memo_eq = Some(eqbox_eq::<T>);
node.state = NodeState::Dirty; node.created_at = created_at;
});
Memo {
key,
created_at,
_m: PhantomData,
}
}
#[track_caller]
pub fn with<R>(self, f: impl FnOnce(&T) -> R) -> R {
refresh_memo(self.key);
let value = with_rt(|rt| {
track_read(rt, self.key);
rt.nodes.get(self.key).and_then(|n| n.value.clone())
});
match value {
Some(cell) => {
let boxed = cell.borrow();
boxed
.downcast_ref::<EqBox<T>>()
.map(|e| f(&e.value))
.unwrap_or_else(|| panic!("day-reactive: memo type mismatch"))
}
None => panic!(
"day-reactive: read of disposed Memo created at {}",
self.created_at
),
}
}
#[track_caller]
pub fn get(self) -> T
where
T: Clone,
{
self.with(|v| v.clone())
}
}
struct EqBox<T> {
value: T,
eq: fn(&T, &T) -> bool,
}
fn eqbox_eq<T: 'static>(a: &dyn Any, b: &dyn Any) -> bool {
match (a.downcast_ref::<EqBox<T>>(), b.downcast_ref::<EqBox<T>>()) {
(Some(a), Some(b)) => (a.eq)(&a.value, &b.value),
_ => false,
}
}
pub struct Effect;
impl Effect {
#[allow(clippy::new_ret_no_self)]
#[track_caller]
pub fn new(f: impl Fn() + 'static) {
create_reaction(Rc::new(f), 1);
}
}
#[track_caller]
fn create_reaction(f: Rc<dyn Fn()>, priority: u8) -> NodeKey {
let created_at = Location::caller();
let key = with_rt(|rt| {
let scope = rt.current_scope;
let k = create_node(rt, NodeKind::Reaction, scope);
rt.nodes[k].reaction = Some(f);
rt.nodes[k].priority = priority;
rt.nodes[k].created_at = created_at;
k
});
with_rt(|rt| rt.observers.push(Some(key)));
let reaction = with_rt(|rt| rt.nodes[key].reaction.clone());
{
let _g = RtGuard(|rt: &mut Runtime| {
rt.observers.pop();
});
if let Some(r) = reaction {
r();
}
}
with_rt(|rt| {
let tick = rt.tick;
rt.tick += 1;
if let Some(n) = rt.nodes.get_mut(key) {
n.last_run = tick;
}
});
key
}
#[track_caller]
pub fn bind<V: PartialEq + 'static>(
compute: impl Fn() -> V + 'static,
apply: impl Fn(&V) + 'static,
) {
let last: RefCell<Option<V>> = RefCell::new(None);
create_reaction(
Rc::new(move || {
let v = compute();
if last.borrow().as_ref() != Some(&v) {
untrack(|| apply(&v));
*last.borrow_mut() = Some(v);
}
}),
0,
);
}
#[track_caller]
pub fn bind_seeded<V: PartialEq + 'static>(
seed: V,
compute: impl Fn() -> V + 'static,
apply: impl Fn(&V) + 'static,
) {
let last: RefCell<Option<V>> = RefCell::new(Some(seed));
create_reaction(
Rc::new(move || {
let v = compute();
if last.borrow().as_ref() != Some(&v) {
untrack(|| apply(&v));
*last.borrow_mut() = Some(v);
}
}),
0,
);
}
#[track_caller]
pub fn bind_always<V: 'static>(compute: impl Fn() -> V + 'static, apply: impl Fn(V) + 'static) {
create_reaction(
Rc::new(move || {
let v = compute();
untrack(|| apply(v));
}),
0,
);
}
#[track_caller]
pub fn watch<S: Clone + 'static>(
source: impl Fn() -> S + 'static,
cb: impl Fn(&S, Option<&S>) + 'static,
) {
let prev: RefCell<Option<S>> = RefCell::new(None);
create_reaction(
Rc::new(move || {
let new = source();
let old = prev.borrow_mut().replace(new.clone());
if old.is_some() {
untrack(|| cb(&new, old.as_ref()));
}
}),
1,
);
}
#[derive(Clone, Copy)]
pub struct Trigger {
signal: Signal<u64>,
}
impl Trigger {
#[track_caller]
pub fn new() -> Self {
Trigger {
signal: Signal::new(0),
}
}
pub fn track(self) {
self.signal.track();
}
pub fn notify(self) {
self.signal.update(|v| *v = v.wrapping_add(1));
}
}
impl Default for Trigger {
fn default() -> Self {
Self::new()
}
}
pub trait Binding<T: 'static>: Clone + 'static {
fn read(&self) -> T;
fn peek(&self) -> T;
fn write(&self, v: T);
fn write_preview(&self, v: T) {
self.write(v);
}
fn write_commit(&self, v: T) {
self.write(v);
}
fn write_teardown(&self, v: T) {
self.write_commit(v);
}
}
impl<T: Clone + 'static> Binding<T> for Signal<T> {
fn read(&self) -> T {
self.get()
}
fn peek(&self) -> T {
self.get_untracked()
}
fn write(&self, v: T) {
self.set(v);
}
fn write_teardown(&self, _v: T) {}
}
trait ErasedBinding<T: 'static> {
fn erased_read(&self) -> T;
fn erased_peek(&self) -> T;
fn erased_write(&self, v: T);
fn erased_write_preview(&self, v: T);
fn erased_write_commit(&self, v: T);
fn erased_write_teardown(&self, v: T);
}
impl<T: 'static, B: Binding<T>> ErasedBinding<T> for B {
fn erased_read(&self) -> T {
Binding::read(self)
}
fn erased_peek(&self) -> T {
Binding::peek(self)
}
fn erased_write(&self, v: T) {
Binding::write(self, v)
}
fn erased_write_preview(&self, v: T) {
Binding::write_preview(self, v)
}
fn erased_write_commit(&self, v: T) {
Binding::write_commit(self, v)
}
fn erased_write_teardown(&self, v: T) {
Binding::write_teardown(self, v)
}
}
pub struct AnyBinding<T: 'static> {
slot: Signal<Rc<dyn ErasedBinding<T>>>,
}
impl<T: 'static> Clone for AnyBinding<T> {
fn clone(&self) -> Self {
*self
}
}
impl<T: 'static> Copy for AnyBinding<T> {}
impl<T: 'static> AnyBinding<T> {
pub fn new(binding: impl Binding<T>) -> Self {
let erased: Rc<dyn ErasedBinding<T>> = Rc::new(binding);
AnyBinding {
slot: Signal::new(erased),
}
}
fn inner(self) -> Rc<dyn ErasedBinding<T>> {
self.slot.get_untracked()
}
pub fn get(self) -> T {
self.inner().erased_read()
}
pub fn try_get(self) -> Option<T> {
self.slot.try_get().map(|b| b.erased_read())
}
pub fn get_untracked(self) -> T {
self.inner().erased_peek()
}
pub fn with<R>(self, f: impl FnOnce(&T) -> R) -> R {
let v = self.inner().erased_read();
f(&v)
}
pub fn set(self, v: T) {
self.inner().erased_write(v)
}
pub fn set_if_changed(self, v: T)
where
T: PartialEq,
{
let b = self.inner();
if b.erased_peek() != v {
b.erased_write(v);
}
}
pub fn update(self, f: impl FnOnce(&mut T)) {
let b = self.inner();
let mut v = b.erased_peek();
f(&mut v);
b.erased_write(v);
}
}
impl<T: 'static> Binding<T> for AnyBinding<T> {
fn read(&self) -> T {
self.inner().erased_read()
}
fn peek(&self) -> T {
self.inner().erased_peek()
}
fn write(&self, v: T) {
self.inner().erased_write(v)
}
fn write_preview(&self, v: T) {
self.inner().erased_write_preview(v)
}
fn write_commit(&self, v: T) {
self.inner().erased_write_commit(v)
}
fn write_teardown(&self, v: T) {
self.inner().erased_write_teardown(v)
}
}
#[derive(Clone)]
pub enum Load<T> {
Loading,
Ready(T),
Failed(Arc<dyn std::error::Error + Send + Sync>),
}
impl<T> Load<T> {
pub fn ready(&self) -> Option<&T> {
match self {
Load::Ready(v) => Some(v),
_ => None,
}
}
pub fn is_loading(&self) -> bool {
matches!(self, Load::Loading)
}
pub fn is_ready(&self) -> bool {
matches!(self, Load::Ready(_))
}
pub fn error(&self) -> Option<&(dyn std::error::Error + Send + Sync + 'static)> {
match self {
Load::Failed(e) => Some(e.as_ref()),
_ => None,
}
}
}
impl<T: std::fmt::Debug> std::fmt::Debug for Load<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Load::Loading => write!(f, "Loading"),
Load::Ready(v) => f.debug_tuple("Ready").field(v).finish(),
Load::Failed(e) => f.debug_tuple("Failed").field(&e.to_string()).finish(),
}
}
}
struct FetchState {
generation: Cell<u64>,
abort: RefCell<Option<Box<dyn FnOnce()>>>,
}
impl FetchState {
fn supersede(&self) -> u64 {
let next = self.generation.get() + 1;
self.generation.set(next);
let old = self.abort.borrow_mut().take();
if let Some(abort) = old {
abort();
}
next
}
}
pub struct Resource<T: 'static> {
state: Signal<Load<T>>,
refetch_count: Signal<u64>,
_not_send: PhantomData<*const T>,
}
impl<T> Clone for Resource<T> {
fn clone(&self) -> Self {
*self
}
}
impl<T> Copy for Resource<T> {}
impl<T: 'static> Resource<T> {
#[track_caller]
pub fn new<S, Fut, E>(
source: impl Fn() -> S + 'static,
fetcher: impl Fn(S) -> Fut + 'static,
) -> Resource<T>
where
S: Clone + PartialEq + 'static,
Fut: Future<Output = Result<T, E>> + 'static,
E: std::error::Error + Send + Sync + 'static,
{
let state: Signal<Load<T>> = Signal::new(Load::Loading);
let refetch_count: Signal<u64> = Signal::new(0);
let fs = Rc::new(FetchState {
generation: Cell::new(0),
abort: RefCell::new(None),
});
let reaction_fs = fs.clone();
let prev: RefCell<Option<(S, u64)>> = RefCell::new(None);
create_reaction(
Rc::new(move || {
let count = refetch_count.get();
let s = source();
let fs = reaction_fs.clone();
untrack(|| {
let same = prev
.borrow()
.as_ref()
.is_some_and(|(ps, pc)| ps == &s && *pc == count);
if same {
return;
}
*prev.borrow_mut() = Some((s.clone(), count));
let my_generation = fs.supersede();
state.set(Load::Loading);
let fut = fetcher(s);
let done = fs.clone();
let abort = spawn_local(Box::pin(async move {
let result = fut.await;
if done.generation.get() != my_generation {
return; }
done.abort.borrow_mut().take(); match result {
Ok(v) => state.set(Load::Ready(v)),
Err(e) => state.set(Load::Failed(Arc::new(e))),
}
}));
*fs.abort.borrow_mut() = Some(abort);
});
}),
1,
);
let cleanup_fs = fs;
Scope::current().on_cleanup(move || {
cleanup_fs.supersede();
});
Resource {
state,
refetch_count,
_not_send: PhantomData,
}
}
pub fn signal(self) -> Signal<Load<T>> {
self.state
}
pub fn get(self) -> Load<T>
where
T: Clone,
{
self.state.get()
}
pub fn with<R>(self, f: impl FnOnce(&Load<T>) -> R) -> R {
self.state.with(f)
}
pub fn loading(self) -> bool {
self.with(Load::is_loading)
}
pub fn ready(self) -> bool {
self.with(Load::is_ready)
}
pub fn refetch(self) {
self.refetch_count.update(|c| *c = c.wrapping_add(1));
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::Cell as StdCell;
use std::rc::Rc;
fn counter() -> (Rc<StdCell<u32>>, impl Fn()) {
let c = Rc::new(StdCell::new(0));
let c2 = c.clone();
(c, move || c2.set(c2.get() + 1))
}
#[test]
fn signal_get_set() {
let s = Signal::new(1);
assert_eq!(s.get(), 1);
s.set(5);
assert_eq!(s.get(), 5);
s.update(|v| *v += 1);
assert_eq!(s.get(), 6);
}
#[test]
fn signal_teardown_does_not_write_after_scope_disposal() {
let scope = Scope::child();
let warned = with_rt(|rt| rt.warned_writes.len());
scope.enter(|| {
let signal = Signal::new(String::new());
signal.write_preview("Fixture final edit".into());
assert_eq!(signal.peek(), "Fixture final edit");
Scope::current().on_cleanup(move || {
signal.write_teardown("Fixture final edit".into());
});
});
scope.dispose();
assert_eq!(with_rt(|rt| rt.warned_writes.len()), warned);
}
#[test]
fn persistent_binding_teardown_seals_preview() {
#[derive(Clone)]
struct FixtureBinding(Rc<RefCell<Vec<String>>>);
impl Binding<String> for FixtureBinding {
fn read(&self) -> String {
String::new()
}
fn peek(&self) -> String {
self.read()
}
fn write(&self, value: String) {
self.0.borrow_mut().push(value);
}
fn write_preview(&self, _value: String) {}
}
let committed = Rc::new(RefCell::new(Vec::new()));
let binding = FixtureBinding(committed.clone());
let scope = Scope::child();
scope.enter(|| {
binding.write_preview("Fixture edit".into());
Scope::current().on_cleanup(move || binding.write_teardown("Fixture edit".into()));
});
assert!(committed.borrow().is_empty());
scope.dispose();
assert_eq!(*committed.borrow(), ["Fixture edit"]);
}
#[test]
fn effect_reruns_on_change() {
let s = Signal::new(1);
let (count, bump) = counter();
Effect::new(move || {
s.track();
bump();
});
assert_eq!(count.get(), 1); batch(|| s.set(2));
assert_eq!(count.get(), 2);
batch(|| s.set(3));
assert_eq!(count.get(), 3);
}
#[test]
fn batch_coalesces() {
let s = Signal::new(0);
let (count, bump) = counter();
Effect::new(move || {
s.track();
bump();
});
batch(|| {
s.set(1);
s.set(2);
s.set(3);
});
assert_eq!(count.get(), 2); assert_eq!(s.get(), 3);
}
#[test]
fn set_if_changed_no_op() {
let s = Signal::new(7);
let (count, bump) = counter();
Effect::new(move || {
s.track();
bump();
});
batch(|| s.set_if_changed(7));
assert_eq!(count.get(), 1); }
#[test]
fn memo_caches_and_diffs() {
let s = Signal::new(1);
let computes = Rc::new(StdCell::new(0));
let c2 = computes.clone();
let doubled = Memo::new(move || {
c2.set(c2.get() + 1);
s.get() * 2
});
assert_eq!(doubled.get(), 2);
assert_eq!(doubled.get(), 2);
assert_eq!(computes.get(), 1); let (effect_runs, bump) = counter();
Effect::new(move || {
let _ = doubled.get();
bump();
});
assert_eq!(effect_runs.get(), 1);
batch(|| s.set(1)); assert_eq!(effect_runs.get(), 1); batch(|| s.set(5));
assert_eq!(doubled.get(), 10);
assert_eq!(effect_runs.get(), 2);
}
#[test]
fn bind_applies_on_change_only() {
let s = Signal::new(1);
let applied: Rc<RefCell<Vec<String>>> = Rc::new(RefCell::new(Vec::new()));
let a2 = applied.clone();
bind(
move || s.get().to_string(),
move |v| a2.borrow_mut().push(v.clone()),
);
assert_eq!(*applied.borrow(), vec!["1"]);
batch(|| s.set(2));
assert_eq!(*applied.borrow(), vec!["1", "2"]);
batch(|| s.update(|v| *v = 2)); assert_eq!(*applied.borrow(), vec!["1", "2"]);
}
#[test]
fn watch_skips_initial_and_passes_old() {
let s = Signal::new(10);
type Log = Rc<RefCell<Vec<(i32, Option<i32>)>>>;
let log: Log = Rc::new(RefCell::new(Vec::new()));
let l2 = log.clone();
watch(
move || s.get(),
move |new, old| l2.borrow_mut().push((*new, old.copied())),
);
assert!(log.borrow().is_empty());
batch(|| s.set(11));
assert_eq!(*log.borrow(), vec![(11, Some(10))]);
}
#[test]
fn scope_dispose_stops_effects_and_write_is_noop() {
let s = Signal::new(0);
let (count, bump) = counter();
let scope = Scope::child();
let inner = scope.enter(|| {
let inner = Signal::new(1);
Effect::new(move || {
s.track();
bump();
});
inner
});
assert_eq!(count.get(), 1);
batch(|| s.set(1));
assert_eq!(count.get(), 2);
scope.dispose();
batch(|| s.set(2));
assert_eq!(count.get(), 2); inner.set(9); assert_eq!(inner.try_get(), None);
}
#[test]
fn dispose_during_drain_skips_pending() {
let s = Signal::new(0);
let scope = Scope::child();
let (count, bump) = counter();
let scope_cell = Rc::new(StdCell::new(Some(scope)));
let sc = scope_cell.clone();
Effect::new(move || {
if s.get() == 1
&& let Some(scope) = sc.take()
{
scope.dispose();
}
});
scope.enter(|| {
Effect::new(move || {
s.track();
bump();
});
});
assert_eq!(count.get(), 1);
batch(|| s.set(1));
assert_eq!(count.get(), 1);
}
#[test]
fn fixpoint_extends_drain() {
let a = Signal::new(0);
let b = Signal::new(0);
let (count, bump) = counter();
Effect::new(move || {
if a.get() == 1 {
b.set(1); }
});
Effect::new(move || {
b.track();
bump();
});
assert_eq!(count.get(), 1);
batch(|| a.set(1));
assert_eq!(count.get(), 2);
assert_eq!(b.get(), 1);
}
#[cfg(debug_assertions)]
#[test]
#[should_panic(expected = "re-ran more than")]
fn rerun_cap_panics_in_debug() {
let s = Signal::new(0);
Effect::new(move || {
let v = s.get();
s.set(v + 1); });
batch(|| s.set(1));
}
#[test]
fn context_provides_down() {
#[derive(Clone, PartialEq, Debug)]
struct Theme(u32);
let scope = Scope::child();
scope.provide(Theme(7));
let child = scope.create_child();
assert_eq!(child.use_context::<Theme>(), Some(Theme(7)));
scope.dispose();
}
#[test]
fn trigger_notifies() {
let t = Trigger::new();
let (count, bump) = counter();
Effect::new(move || {
t.track();
bump();
});
batch(|| t.notify());
assert_eq!(count.get(), 2);
}
#[test]
fn turn_end_runs_after_fixpoint() {
let s = Signal::new(0);
let order: Rc<RefCell<Vec<&'static str>>> = Rc::new(RefCell::new(Vec::new()));
let o1 = order.clone();
Effect::new(move || {
s.track();
o1.borrow_mut().push("effect");
});
let o2 = order.clone();
on_turn_end(move || o2.borrow_mut().push("turn-end"));
order.borrow_mut().clear();
batch(|| s.set(1));
assert_eq!(*order.borrow(), vec!["effect", "turn-end"]);
}
#[test]
fn at_turn_end_waits_for_the_turn_and_runs_once() {
let s = Signal::new(0);
let order: Rc<RefCell<Vec<String>>> = Rc::new(RefCell::new(Vec::new()));
let o = order.clone();
at_turn_end(move || o.borrow_mut().push("now".into()));
assert_eq!(*order.borrow(), vec!["now".to_string()]);
order.borrow_mut().clear();
let o1 = order.clone();
Effect::new(move || {
let v = s.get();
o1.borrow_mut().push(format!("effect {v}"));
});
let o2 = order.clone();
on_turn_end(move || o2.borrow_mut().push("standing".into()));
order.borrow_mut().clear();
let o3 = order.clone();
batch(|| {
at_turn_end(move || o3.borrow_mut().push("once".into()));
s.set(1);
});
assert_eq!(*order.borrow(), vec!["effect 1", "once", "standing"]);
order.borrow_mut().clear();
let o4 = order.clone();
batch(|| at_turn_end(move || o4.borrow_mut().push("quiet".into())));
assert_eq!(*order.borrow(), vec!["quiet", "standing"]);
}
#[test]
fn bindings_run_before_effects() {
let s = Signal::new(0);
let order: Rc<RefCell<Vec<&'static str>>> = Rc::new(RefCell::new(Vec::new()));
let o1 = order.clone();
Effect::new(move || {
s.track();
o1.borrow_mut().push("effect");
});
let o2 = order.clone();
bind(move || s.get(), move |_| o2.borrow_mut().push("bind"));
order.borrow_mut().clear();
batch(|| s.set(1));
assert_eq!(*order.borrow(), vec!["bind", "effect"]);
}
}
#[cfg(test)]
mod resource_tests {
use super::*;
use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker};
thread_local! {
static TEST_TASKS: RefCell<Vec<(u64, Option<LocalBoxFuture>)>> =
const { RefCell::new(Vec::new()) };
static NEXT_ID: Cell<u64> = const { Cell::new(1) };
static ABORTED: RefCell<Vec<u64>> = const { RefCell::new(Vec::new()) };
static INSTALLED: Cell<bool> = const { Cell::new(false) };
}
fn noop_waker() -> Waker {
fn raw() -> RawWaker {
RawWaker::new(std::ptr::null(), &VT)
}
static VT: RawWakerVTable = RawWakerVTable::new(|_| raw(), |_| {}, |_| {}, |_| {});
unsafe { Waker::from_raw(raw()) }
}
fn poll_one(fut: &mut LocalBoxFuture) -> bool {
let waker = noop_waker();
let mut cx = Context::from_waker(&waker);
fut.as_mut().poll(&mut cx).is_ready()
}
fn install_test_spawner() {
if INSTALLED.with(|c| c.replace(true)) {
return;
}
install_spawner(|mut fut| {
let id = NEXT_ID.with(|c| {
let v = c.get();
c.set(v + 1);
v
});
if !poll_one(&mut fut) {
TEST_TASKS.with(|t| t.borrow_mut().push((id, Some(fut))));
}
Box::new(move || {
let removed = TEST_TASKS.with(|t| {
let mut t = t.borrow_mut();
let before = t.len();
t.retain(|(i, _)| *i != id);
before != t.len()
});
if removed {
ABORTED.with(|a| a.borrow_mut().push(id));
}
})
});
}
fn aborted_count() -> usize {
ABORTED.with(|a| a.borrow().len())
}
fn pump() {
let ids: Vec<u64> = TEST_TASKS.with(|t| t.borrow().iter().map(|(i, _)| *i).collect());
for id in ids {
let fut = TEST_TASKS.with(|t| {
t.borrow_mut()
.iter_mut()
.find(|(i, _)| *i == id)
.and_then(|(_, s)| s.take())
});
let Some(mut fut) = fut else { continue };
if poll_one(&mut fut) {
TEST_TASKS.with(|t| t.borrow_mut().retain(|(i, _)| *i != id));
} else {
TEST_TASKS.with(|t| {
if let Some((_, s)) = t.borrow_mut().iter_mut().find(|(i, _)| *i == id) {
*s = Some(fut);
}
});
}
}
flush_sync();
}
type Slot<T> = Rc<RefCell<Option<Result<T, TestErr>>>>;
fn manual_future<T: 'static>(slot: Slot<T>) -> impl Future<Output = Result<T, TestErr>> {
std::future::poll_fn(move |_| match slot.borrow_mut().take() {
Some(r) => Poll::Ready(r),
None => Poll::Pending,
})
}
#[derive(Debug)]
struct TestErr(&'static str);
impl std::fmt::Display for TestErr {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl std::error::Error for TestErr {}
#[test]
fn resource_sync_fetcher_is_ready_immediately() {
install_test_spawner();
let r = Resource::new(|| (), |_| async { Ok::<_, std::convert::Infallible>(42) });
assert!(r.ready());
assert_eq!(r.with(|l| l.ready().copied()), Some(42));
}
#[test]
fn resource_loading_then_ready() {
install_test_spawner();
let slot: Slot<i32> = Rc::new(RefCell::new(None));
let s2 = slot.clone();
let r = Resource::new(move || (), move |_| manual_future(s2.clone()));
assert!(r.loading());
*slot.borrow_mut() = Some(Ok(7));
pump();
assert_eq!(r.with(|l| l.ready().copied()), Some(7));
}
#[test]
fn resource_failed_preserves_error() {
install_test_spawner();
let r = Resource::new(|| (), |_| async { Err::<i32, _>(TestErr("boom")) });
assert!(!r.ready());
assert_eq!(
r.with(|l| l.error().map(|e| e.to_string())),
Some("boom".into())
);
}
#[test]
fn resource_refetches_on_source_change() {
install_test_spawner();
let src = Signal::new(1i32);
let fetches = Rc::new(Cell::new(0));
let f = fetches.clone();
let r = Resource::new(
move || src.get(),
move |v| {
f.set(f.get() + 1);
async move { Ok::<_, TestErr>(v * 10) }
},
);
assert_eq!(fetches.get(), 1);
assert_eq!(r.with(|l| l.ready().copied()), Some(10));
batch(|| src.set(2));
assert_eq!(fetches.get(), 2);
assert_eq!(r.with(|l| l.ready().copied()), Some(20));
}
#[test]
fn resource_source_equality_gates() {
install_test_spawner();
let a = Signal::new(1i32);
let b = Signal::new(10i32);
let fetches = Rc::new(Cell::new(0));
let f = fetches.clone();
let r = Resource::new(
move || {
let _ = b.get(); a.get()
},
move |v| {
f.set(f.get() + 1);
async move { Ok::<_, TestErr>(v) }
},
);
assert_eq!(fetches.get(), 1);
batch(|| b.set(20)); assert_eq!(fetches.get(), 1);
assert_eq!(r.with(|l| l.ready().copied()), Some(1));
batch(|| a.set(2)); assert_eq!(fetches.get(), 2);
}
#[test]
fn resource_refetch_forces() {
install_test_spawner();
let fetches = Rc::new(Cell::new(0));
let f = fetches.clone();
let r = Resource::new(
|| (),
move |_| {
f.set(f.get() + 1);
async { Ok::<_, TestErr>(0) }
},
);
assert_eq!(fetches.get(), 1);
batch(|| r.refetch()); assert_eq!(fetches.get(), 2);
}
#[test]
fn resource_latest_wins() {
install_test_spawner();
let src = Signal::new(1i32);
let slots: Rc<RefCell<Vec<Slot<i32>>>> = Rc::new(RefCell::new(Vec::new()));
let sl = slots.clone();
let r = Resource::new(
move || src.get(),
move |_| {
let slot: Slot<i32> = Rc::new(RefCell::new(None));
sl.borrow_mut().push(slot.clone());
manual_future(slot)
},
);
assert!(r.loading());
let before = aborted_count();
batch(|| src.set(2)); assert_eq!(aborted_count(), before + 1, "the stale task was aborted");
*slots.borrow()[0].borrow_mut() = Some(Ok(111));
pump();
assert!(r.loading(), "a stale result writes nothing");
*slots.borrow()[1].borrow_mut() = Some(Ok(222));
pump();
assert_eq!(r.with(|l| l.ready().copied()), Some(222));
}
#[test]
fn resource_scope_disposal_aborts() {
install_test_spawner();
let scope = Scope::child();
let slot: Slot<i32> = Rc::new(RefCell::new(None));
let s2 = slot.clone();
let r = scope.enter(|| Resource::new(move || (), move |_| manual_future(s2.clone())));
assert!(r.loading());
let before = aborted_count();
scope.dispose();
assert_eq!(aborted_count(), before + 1, "disposal aborted the fetch");
*slot.borrow_mut() = Some(Ok(1));
pump();
}
#[test]
fn resource_ready_is_tracked() {
install_test_spawner();
let slot: Slot<i32> = Rc::new(RefCell::new(None));
let s2 = slot.clone();
let r = Resource::new(move || (), move |_| manual_future(s2.clone()));
let runs = Rc::new(Cell::new(0));
let rn = runs.clone();
Effect::new(move || {
let _ = r.ready();
rn.set(rn.get() + 1);
});
assert_eq!(runs.get(), 1);
*slot.borrow_mut() = Some(Ok(1));
pump();
assert!(runs.get() >= 2, "ready() re-ran the effect on completion");
}
#[test]
#[should_panic(expected = "no spawner installed")]
fn spawner_missing_panics() {
let _r = Resource::new(|| (), |_| async { Ok::<_, std::convert::Infallible>(1) });
}
}
#[cfg(test)]
mod unwind_safety_tests {
use super::*;
fn contained(f: impl FnOnce() + std::panic::UnwindSafe) {
let prev = std::panic::take_hook();
std::panic::set_hook(Box::new(|_| {}));
let r = std::panic::catch_unwind(f);
std::panic::set_hook(prev);
assert!(r.is_err(), "expected the closure to panic");
}
#[test]
fn panic_in_scope_enter_restores_current_scope() {
let before = with_rt(|rt| rt.current_scope);
let scope = Scope::detached();
contained(|| scope.enter(|| panic!("boom in enter")));
assert_eq!(with_rt(|rt| rt.current_scope), before, "scope not restored");
let s = Signal::new(7u32);
assert_eq!(s.get(), 7);
}
#[test]
fn panic_in_effect_body_pops_its_observer_frame() {
let depth = with_rt(|rt| rt.observers.len());
let boom = Signal::new(false);
Effect::new(move || {
if boom.get() {
panic!("effect boom");
}
});
assert_eq!(with_rt(|rt| rt.observers.len()), depth);
contained(|| batch(|| boom.set(true)));
assert_eq!(
with_rt(|rt| rt.observers.len()),
depth,
"observer frame stranded by the panicking effect run"
);
recover_from_panic(); }
#[test]
fn panic_in_untrack_restores_tracking() {
let depth = with_rt(|rt| rt.observers.len());
contained(|| untrack(|| panic!("boom in untrack")));
assert_eq!(with_rt(|rt| rt.observers.len()), depth, "observer stranded");
let src = Signal::new(0i32);
let runs = Rc::new(RefCell::new(0));
let r2 = runs.clone();
Effect::new(move || {
src.get();
*r2.borrow_mut() += 1;
});
flush_sync();
let before = *runs.borrow();
src.set(1);
flush_sync();
assert!(
*runs.borrow() > before,
"effect did not re-run — tracking is off"
);
}
#[test]
fn panic_in_batch_restores_depth() {
let depth = with_rt(|rt| rt.batch_depth);
contained(|| batch(|| panic!("boom in batch")));
assert_eq!(with_rt(|rt| rt.batch_depth), depth, "batch depth stranded");
}
#[test]
fn panic_in_with_leaves_the_value_intact() {
let s = Signal::new(String::from("intact"));
contained(|| s.with(|_| panic!("boom in with")));
assert_eq!(s.with(|v| v.clone()), "intact");
}
#[test]
fn reentrant_read_inside_with_succeeds() {
let s = Signal::new(5u32);
let doubled = s.with(|outer| *outer + s.with(|inner| *inner));
assert_eq!(doubled, 10);
assert_eq!(
s.with(|v| *v),
5,
"value still in place after the nested read"
);
}
#[test]
fn nested_reads_of_distinct_signals_succeed() {
let a = Signal::new(2u32);
let b = Signal::new(3u32);
assert_eq!(a.with(|x| *x * b.with(|y| *y)), 6);
}
#[test]
fn write_during_read_panics_with_a_clear_message() {
let s = Signal::new(1u32);
let prev = std::panic::take_hook();
std::panic::set_hook(Box::new(|_| {}));
let r = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
s.with(|_| s.set(2));
}));
std::panic::set_hook(prev);
let payload = r.unwrap_err();
let msg = payload
.downcast_ref::<&str>()
.map(|s| s.to_string())
.or_else(|| payload.downcast_ref::<String>().cloned())
.expect("panic payload should be a string");
assert!(
msg.contains("in flight") && msg.contains("Signal"),
"message should name the cause, got: {msg}"
);
}
#[cfg(not(debug_assertions))]
#[test]
fn rerun_cap_defers_instead_of_killing_the_binding() {
let s = Signal::new(0u32);
let runs = std::rc::Rc::new(std::cell::Cell::new(0u32));
let r2 = runs.clone();
Effect::new(move || {
let v = s.get();
r2.set(r2.get() + 1);
if v < 1_000_000 {
s.set(v + 1); }
});
flush_sync();
let after_cycle = runs.get();
assert!(
after_cycle <= RERUN_CAP + 1,
"cap did not bound the drain: {after_cycle} runs"
);
batch(|| s.set(2_000_000)); assert_eq!(
runs.get(),
after_cycle + 1,
"binding stayed dead after hitting the re-run cap"
);
}
}