use alloc::rc::Rc;
use core::cell::RefCell;
use core::panic::Location;
use crate::{DiagnosticEvent, NodeId, NodeKind, Reactor, current, trace_targets};
#[track_caller]
pub fn signal<T: 'static>(initial: T) -> Signal<T> {
current().signal(initial)
}
#[track_caller]
pub fn signal_in<T: 'static>(reactor: &Reactor, initial: T) -> Signal<T> {
reactor.signal(initial)
}
pub struct Signal<T> {
inner: Rc<SignalInner<T>>,
}
impl<T> Clone for Signal<T> {
fn clone(&self) -> Self {
Self {
inner: Rc::clone(&self.inner),
}
}
}
impl Reactor {
#[track_caller]
pub fn signal<T: 'static>(&self, initial: T) -> Signal<T> {
Signal::new(self.clone(), initial)
}
}
#[cold]
#[inline(never)]
fn report_suppressed_write(
reactor: &Reactor,
node: NodeId,
write_origin: &'static Location<'static>,
) {
reactor.record_flush(|stats| {
stats.writes_suppressed = stats.writes_suppressed.saturating_add(1);
});
let Some(node_origin) = reactor.node_origin(node) else {
return;
};
reactor.emit_diagnostic(DiagnosticEvent::WriteSuppressed {
reactor: reactor.diagnostic_id(),
node,
kind: NodeKind::Signal,
node_origin,
write_origin,
});
}
impl<T: 'static> Signal<T> {
#[track_caller]
fn new(reactor: Reactor, initial: T) -> Self {
let id = reactor.allocate_node(NodeKind::Signal);
tracing::debug!(
target: trace_targets::SIGNAL,
event = "create_signal",
node_id = id.0,
"created reactive signal"
);
Self {
inner: Rc::new(SignalInner {
reactor,
id,
value: RefCell::new(initial),
}),
}
}
pub fn with<R>(&self, f: impl FnOnce(&T) -> R) -> R {
#[cfg(debug_assertions)]
tracing::trace!(
target: trace_targets::SIGNAL,
event = "read_signal",
node_id = self.inner.id.0,
"reading reactive signal"
);
self.inner.reactor.observe(self.inner.id);
let value = self.inner.value.borrow();
f(&value)
}
pub fn with_peek<R>(&self, f: impl FnOnce(&T) -> R) -> R {
let value = self.inner.value.borrow();
f(&value)
}
pub fn reactor(&self) -> Reactor {
self.inner.reactor.clone()
}
pub fn id(&self) -> NodeId {
self.inner.id
}
#[track_caller]
pub fn replace(&self, value: T) -> T {
let previous = self.inner.value.replace(value);
tracing::debug!(
target: trace_targets::SIGNAL,
event = "replace_signal",
node_id = self.inner.id.0,
"replaced signal value"
);
self.inner.reactor.trigger(self.inner.id);
previous
}
#[track_caller]
pub fn update<R>(&self, f: impl FnOnce(&mut T) -> R) -> R {
let output = {
let mut value = self.inner.value.borrow_mut();
f(&mut value)
};
tracing::debug!(
target: trace_targets::SIGNAL,
event = "update_signal",
node_id = self.inner.id.0,
"updated signal value in place"
);
self.inner.reactor.trigger(self.inner.id);
output
}
}
impl<T: Clone + 'static> Signal<T> {
pub fn get(&self) -> T {
self.with(T::clone)
}
pub fn peek(&self) -> T {
self.with_peek(T::clone)
}
}
impl<T: PartialEq + 'static> Signal<T> {
#[track_caller]
pub fn set(&self, value: T) -> Option<T> {
let unchanged = {
let current = self.inner.value.borrow();
crate::untrack(|| *current == value)
};
if unchanged {
if self.inner.reactor.diagnostics_enabled() {
report_suppressed_write(&self.inner.reactor, self.inner.id, Location::caller());
}
#[cfg(debug_assertions)]
tracing::trace!(
target: trace_targets::SIGNAL,
event = "set_signal",
node_id = self.inner.id.0,
changed = false,
"suppressed unchanged signal write"
);
return None;
}
let previous = self.inner.value.replace(value);
tracing::debug!(
target: trace_targets::SIGNAL,
event = "set_signal",
node_id = self.inner.id.0,
changed = true,
"set signal value"
);
self.inner.reactor.trigger(self.inner.id);
Some(previous)
}
}
impl<T> core::fmt::Debug for Signal<T> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("Signal")
.field("id", &self.inner.id)
.finish_non_exhaustive()
}
}
struct SignalInner<T> {
reactor: Reactor,
id: NodeId,
value: RefCell<T>,
}
impl<T> Drop for SignalInner<T> {
fn drop(&mut self) {
self.reactor.dispose(self.id);
}
}
#[cfg(test)]
mod tests {
use super::Signal;
use crate::Reactor;
#[test]
fn set_suppresses_unchanged_writes() {
let reactor = Reactor::new();
let value = reactor.signal(10usize);
assert_eq!(value.set(10), None);
assert_eq!(value.get(), 10);
assert_eq!(value.set(11), Some(10));
assert_eq!(value.get(), 11);
}
#[test]
fn replace_and_update_write_values() {
let reactor = Reactor::new();
let value: Signal<Vec<usize>> = reactor.signal(vec![1, 2]);
let old = value.replace(vec![3]);
assert_eq!(old, vec![1, 2]);
assert_eq!(value.with(|items| items.clone()), vec![3]);
value.update(|items| items.push(4));
assert_eq!(value.with(|items| items.clone()), vec![3, 4]);
}
}