nami/reactive_core/signal.rs
1//! This module provides a framework for reactive computations that can track dependencies
2//! and automatically update when their inputs change.
3//!
4//! The core abstractions include:
5//! - `Signal` - A trait for values that can be computed and watched for changes
6//! - `IntoSignal` - Conversion trait for working with signals
7//! - `IntoComputed` - Conversion trait for creating computed values
8//!
9//! This system enables building reactive data flows where computations automatically
10//! re-execute when their dependencies change, similar to reactive programming models
11//! found in front-end frameworks.
12
13use core::panic::Location;
14
15mod computed;
16pub use computed::*;
17
18use crate::{
19 map::{Map, map},
20 watcher::Context,
21};
22
23pub use nami_core::{Signal, SignalIdentity};
24
25/// A trait for converting a value into a computation.
26pub trait IntoSignal<Output> {
27 /// The specific computation type that will be produced.
28 type Signal: Signal<Output = Output>;
29
30 /// Convert this value into a computation.
31 fn into_signal(self) -> Self::Signal;
32}
33
34/// A trait for converting a value directly into a `Computed<Output>`.
35///
36/// This is a convenience trait that builds on `IntoSignal`.
37pub trait IntoComputed<Output>: IntoSignal<Output> + 'static {
38 /// Convert this value into a `Computed<Output>`.
39 fn into_computed(self) -> Computed<Output>;
40}
41
42/// Blanket implementation of `IntoSignal` for any type that implements `Signal`.
43///
44/// This allows for automatic conversion between compatible computation types.
45impl<C, Output> IntoSignal<Output> for C
46where
47 C: Signal,
48 C::Output: 'static + Clone,
49 Output: From<C::Output> + 'static,
50{
51 type Signal = Map<C, fn(C::Output) -> Output, Output>;
52
53 /// Convert this computation into one that produces the desired output type.
54 fn into_signal(self) -> Self::Signal {
55 map(self, core::convert::Into::into)
56 }
57}
58
59/// Blanket implementation of `IntoComputed` for any type that implements `IntoSignal`.
60impl<C, Output> IntoComputed<Output> for C
61where
62 C: IntoSignal<Output> + 'static,
63 C::Signal: Clone + 'static,
64{
65 /// Convert this value into a `Computed<Output>`.
66 fn into_computed(self) -> Computed<Output> {
67 Computed::new(self.into_signal())
68 }
69}
70
71/// A wrapper for a computation that attaches additional metadata.
72///
73/// This can be used to carry extra information alongside a computation.
74#[derive(Debug, Clone)]
75pub struct WithMetadata<C, T> {
76 /// The metadata to be associated with the computation.
77 metadata: T,
78
79 /// The underlying computation.
80 signal: C,
81 discriminator: usize,
82}
83
84impl<C, T: 'static> WithMetadata<C, T> {
85 /// Create a new computation with associated metadata.
86 #[track_caller]
87 pub fn new(metadata: T, signal: C) -> Self {
88 Self {
89 metadata,
90 signal,
91 discriminator: SignalIdentity::call_site_discriminator::<T>(Location::caller()),
92 }
93 }
94}
95
96/// Implementation of `signal` for `WithMetadata`.
97///
98/// This delegates the computation to the wrapped value but enriches
99/// the watcher notifications with the metadata.
100impl<C: Signal, T: Clone + 'static> Signal for WithMetadata<C, T> {
101 type Output = C::Output;
102 type Guard = C::Guard;
103
104 /// Execute the underlying computation.
105 fn get(&self) -> Self::Output {
106 self.signal.get()
107 }
108
109 fn identity(&self) -> Option<SignalIdentity> {
110 self.signal
111 .identity()
112 .map(|identity| identity.with_discriminator(self.discriminator))
113 }
114
115 /// Register a watcher, enriching notifications with the metadata.
116 fn watch(&self, watcher: impl Fn(Context<Self::Output>) + 'static) -> Self::Guard {
117 let with = self.metadata.clone();
118 self.signal
119 .watch(move |context: Context<<C as Signal>::Output>| {
120 watcher(context.with(with.clone()));
121 })
122 }
123}
124
125impl_signal_wrapper_ops!(WithMetadata<C, T>, [C, T], C);