use alloc::rc::Rc;
use crate::{NodeId, Reactor, current, trace_targets};
#[track_caller]
pub fn source() -> Source {
current().source()
}
#[track_caller]
pub fn source_in(reactor: &Reactor) -> Source {
reactor.source()
}
#[track_caller]
pub fn source_with_hooks(on_watch: impl Fn() + 'static, on_unwatch: impl Fn() + 'static) -> Source {
current().source_with_hooks(on_watch, on_unwatch)
}
#[track_caller]
pub fn source_with_hooks_in(
reactor: &Reactor,
on_watch: impl Fn() + 'static,
on_unwatch: impl Fn() + 'static,
) -> Source {
reactor.source_with_hooks(on_watch, on_unwatch)
}
#[derive(Clone)]
pub struct Source {
inner: Rc<SourceInner>,
}
impl Reactor {
#[track_caller]
pub fn source(&self) -> Source {
Source::new(self.clone())
}
#[track_caller]
pub fn source_with_hooks(
&self,
on_watch: impl Fn() + 'static,
on_unwatch: impl Fn() + 'static,
) -> Source {
let source = Source::new(self.clone());
self.register_observation_hooks(source.id(), on_watch, on_unwatch);
source
}
}
impl Source {
#[track_caller]
fn new(reactor: Reactor) -> Self {
let id = reactor.allocate_node();
tracing::debug!(
target: trace_targets::GRAPH,
event = "create_source",
node_id = id.0,
"created low-level reactive source"
);
Self {
inner: Rc::new(SourceInner { reactor, id }),
}
}
pub fn observe(&self) {
self.inner.reactor.observe(self.inner.id);
}
#[track_caller]
pub fn trigger(&self) {
self.inner.reactor.trigger(self.inner.id);
}
pub fn id(&self) -> NodeId {
self.inner.id
}
pub fn is_observed(&self) -> bool {
self.inner.reactor.is_observed(self.inner.id)
}
}
impl core::fmt::Debug for Source {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("Source")
.field("id", &self.inner.id)
.finish()
}
}
struct SourceInner {
reactor: Reactor,
id: NodeId,
}
impl Drop for SourceInner {
fn drop(&mut self) {
self.reactor.dispose(self.id);
}
}
#[cfg(test)]
mod tests {
use std::cell::{Cell, RefCell};
use std::rc::Rc;
use runite::{queue_macrotask, run};
use crate::{EffectHandle, Reactor, Source, source_in, source_with_hooks_in, thunk_in};
fn hooked(reactor: &Reactor) -> (Source, Rc<RefCell<Vec<&'static str>>>) {
let log = Rc::new(RefCell::new(Vec::new()));
let node = source_with_hooks_in(
reactor,
{
let log = Rc::clone(&log);
move || log.borrow_mut().push("watch")
},
{
let log = Rc::clone(&log);
move || log.borrow_mut().push("unwatch")
},
);
(node, log)
}
#[test]
fn hooks_fire_on_the_first_observer_and_the_last_departure() {
let reactor = Reactor::new();
let (node, log) = hooked(&reactor);
reactor.flush_now();
assert!(log.borrow().is_empty(), "an unread source is not watched");
let reader = thunk_in(&reactor, {
let node = node.clone();
move || {
node.observe();
1
}
});
assert!(log.borrow().is_empty());
let _ = reader.get();
reactor.flush_now();
assert_eq!(*log.borrow(), ["watch"]);
drop(reader);
reactor.flush_now();
assert_eq!(*log.borrow(), ["watch", "unwatch"]);
}
#[test]
fn hooks_are_deferred_not_inline() {
let reactor = Reactor::new();
let (node, log) = hooked(&reactor);
let reader = thunk_in(&reactor, {
let node = node.clone();
move || {
node.observe();
1
}
});
let _ = reader.get();
assert!(
log.borrow().is_empty(),
"delivery waits for the flush, even for the watch edge"
);
reactor.flush_now();
assert_eq!(*log.borrow(), ["watch"]);
}
#[test]
fn a_leave_and_arrive_within_one_flush_collapses() {
let reactor = Reactor::new();
let (node, log) = hooked(&reactor);
let toggle = crate::signal_in(&reactor, 0);
let reader = thunk_in(&reactor, {
let node = node.clone();
let toggle = toggle.clone();
move || {
toggle.get();
node.observe();
1
}
});
let _ = reader.get();
reactor.flush_now();
assert_eq!(*log.borrow(), ["watch"]);
toggle.set(1);
let _ = reader.get();
reactor.flush_now();
assert_eq!(
*log.borrow(),
["watch"],
"a rerunning reader must not disconnect and reconnect"
);
drop(reader);
reactor.flush_now();
assert_eq!(*log.borrow(), ["watch", "unwatch"]);
}
#[test]
fn neither_hook_is_delivered_twice_in_a_row() {
let reactor = Reactor::new();
let (node, log) = hooked(&reactor);
let readers: Vec<_> = (0..2)
.map(|_| {
thunk_in(&reactor, {
let node = node.clone();
move || {
node.observe();
1
}
})
})
.collect();
for reader in &readers {
let _ = reader.get();
}
reactor.flush_now();
assert_eq!(*log.borrow(), ["watch"], "one watch, not two");
let mut readers = readers;
drop(readers.pop());
reactor.flush_now();
assert_eq!(*log.borrow(), ["watch"], "one reader remains");
drop(readers);
reactor.flush_now();
assert_eq!(*log.borrow(), ["watch", "unwatch"]);
}
#[test]
fn dropping_the_source_cancels_a_queued_hook() {
let reactor = Reactor::new();
let (node, log) = hooked(&reactor);
let reader = thunk_in(&reactor, {
let node = node.clone();
move || {
node.observe();
1
}
});
let _ = reader.get();
assert!(log.borrow().is_empty());
drop(node);
drop(reader);
reactor.flush_now();
assert!(
log.borrow().is_empty(),
"a disposed source must not deliver hooks"
);
}
#[test]
fn is_observed_agrees_with_the_hooks_but_answers_immediately() {
let reactor = Reactor::new();
let (node, log) = hooked(&reactor);
let reader = thunk_in(&reactor, {
let node = node.clone();
move || {
node.observe();
1
}
});
let _ = reader.get();
assert!(node.is_observed(), "the edge exists immediately");
assert!(log.borrow().is_empty(), "the hook has not been delivered");
reactor.flush_now();
assert!(node.is_observed());
assert_eq!(*log.borrow(), ["watch"]);
}
#[test]
fn sources_have_distinct_ids() {
let reactor = Reactor::new();
let one = source_in(&reactor);
let two = source_in(&reactor);
assert_ne!(one.id(), two.id());
}
#[test]
fn observe_and_trigger_drive_an_effect_around_external_state() {
let seen = Rc::new(RefCell::new(Vec::new()));
let keep_alive = Rc::new(RefCell::new(None::<(Source, EffectHandle)>));
queue_macrotask({
let seen = Rc::clone(&seen);
let keep_alive = Rc::clone(&keep_alive);
move || {
let reactor = Reactor::new();
let source = source_in(&reactor);
let external = Rc::new(Cell::new(1usize));
let effect = reactor.effect({
let source = source.clone();
let external = Rc::clone(&external);
let seen = Rc::clone(&seen);
move || {
source.observe();
seen.borrow_mut().push(external.get());
}
});
*keep_alive.borrow_mut() = Some((source.clone(), effect));
runite::queue_macrotask(move || {
external.set(2);
source.trigger();
});
}
});
run();
assert_eq!(&*seen.borrow(), &[1, 2]);
}
}