use alloc::rc::Rc;
use crate::{Memo, Signal, Thunk};
pub trait Observable {
type Item;
fn with<R>(&self, f: impl FnOnce(&Self::Item) -> R) -> R;
fn with_peek<R>(&self, f: impl FnOnce(&Self::Item) -> R) -> R;
fn get(&self) -> Self::Item
where
Self::Item: Clone,
{
self.with(Self::Item::clone)
}
fn peek(&self) -> Self::Item
where
Self::Item: Clone,
{
self.with_peek(Self::Item::clone)
}
fn into_dyn(self) -> DynObservable<Self::Item>
where
Self: Sized + 'static,
{
DynObservable {
inner: Rc::new(self),
}
}
}
impl<T: 'static> Observable for Signal<T> {
type Item = T;
fn with<R>(&self, f: impl FnOnce(&T) -> R) -> R {
Signal::with(self, f)
}
fn with_peek<R>(&self, f: impl FnOnce(&T) -> R) -> R {
Signal::with_peek(self, f)
}
}
impl<T: 'static> Observable for Thunk<T> {
type Item = T;
fn with<R>(&self, f: impl FnOnce(&T) -> R) -> R {
Thunk::with(self, f)
}
fn with_peek<R>(&self, f: impl FnOnce(&T) -> R) -> R {
Thunk::with_peek(self, f)
}
}
impl<T: 'static> Observable for Memo<T> {
type Item = T;
fn with<R>(&self, f: impl FnOnce(&T) -> R) -> R {
Memo::with(self, f)
}
fn with_peek<R>(&self, f: impl FnOnce(&T) -> R) -> R {
Memo::with_peek(self, f)
}
}
trait ErasedObservable<T> {
fn with_erased(&self, f: &mut dyn FnMut(&T));
fn with_peek_erased(&self, f: &mut dyn FnMut(&T));
}
impl<O: Observable> ErasedObservable<O::Item> for O {
fn with_erased(&self, f: &mut dyn FnMut(&O::Item)) {
self.with(|value| f(value));
}
fn with_peek_erased(&self, f: &mut dyn FnMut(&O::Item)) {
self.with_peek(|value| f(value));
}
}
pub struct DynObservable<T> {
inner: Rc<dyn ErasedObservable<T>>,
}
impl<T> Clone for DynObservable<T> {
fn clone(&self) -> Self {
Self {
inner: Rc::clone(&self.inner),
}
}
}
impl<T: 'static> DynObservable<T> {
pub fn constant(value: T) -> Self {
struct Constant<T>(T);
impl<T> Observable for Constant<T> {
type Item = T;
fn with<R>(&self, f: impl FnOnce(&T) -> R) -> R {
f(&self.0)
}
fn with_peek<R>(&self, f: impl FnOnce(&T) -> R) -> R {
f(&self.0)
}
}
Constant(value).into_dyn()
}
}
impl<T: 'static> Observable for DynObservable<T> {
type Item = T;
fn into_dyn(self) -> DynObservable<T> {
self
}
fn with<R>(&self, f: impl FnOnce(&T) -> R) -> R {
let mut f = Some(f);
let mut output = None;
self.inner.with_erased(&mut |value| {
if let Some(f) = f.take() {
output = Some(f(value));
}
});
output.expect("erased observable must invoke the reader exactly once")
}
fn with_peek<R>(&self, f: impl FnOnce(&T) -> R) -> R {
let mut f = Some(f);
let mut output = None;
self.inner.with_peek_erased(&mut |value| {
if let Some(f) = f.take() {
output = Some(f(value));
}
});
output.expect("erased observable must invoke the reader exactly once")
}
}
impl<T> core::fmt::Debug for DynObservable<T> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("DynObservable").finish_non_exhaustive()
}
}
#[cfg(test)]
mod tests {
use super::{DynObservable, Observable};
use crate::{Reactor, memo_in, signal_in, thunk_in};
#[test]
fn signals_thunks_and_memos_share_the_observable_interface() {
fn read<O: Observable<Item = i32>>(observable: &O) -> i32 {
observable.get()
}
let reactor = Reactor::new();
let base = signal_in(&reactor, 7);
let doubled = thunk_in(&reactor, {
let base = base.clone();
move || base.get() * 2
});
let capped = memo_in(&reactor, {
let base = base.clone();
move || base.get().min(10)
});
assert_eq!(read(&base), 7);
assert_eq!(read(&doubled), 14);
assert_eq!(read(&capped), 7);
}
#[test]
fn dyn_observables_erase_and_track() {
let reactor = Reactor::new();
let base = signal_in(&reactor, 1);
let erased: Vec<DynObservable<i32>> = vec![
base.clone().into_dyn(),
DynObservable::constant(100),
memo_in(&reactor, {
let base = base.clone();
move || base.get() * 10
})
.into_dyn(),
];
let total = || erased.iter().map(DynObservable::get).sum::<i32>();
assert_eq!(total(), 111);
base.set(2);
assert_eq!(total(), 122, "erased handles still track updates");
}
}