mirror of
https://github.com/Zenithsiz/dynatos.git
synced 2026-02-04 02:18:40 +00:00
Added dynatos_reactive::{TryMappedSignal, MappedSignal}.
This commit is contained in:
parent
81ade7080a
commit
00c3fd5c3a
@ -24,7 +24,12 @@
|
||||
stmt_expr_attributes,
|
||||
proc_macro_hygiene,
|
||||
type_alias_impl_trait,
|
||||
macro_metavar_expr
|
||||
macro_metavar_expr,
|
||||
try_trait_v2,
|
||||
try_trait_v2_residual,
|
||||
assert_matches,
|
||||
never_type,
|
||||
unwrap_infallible
|
||||
)]
|
||||
|
||||
// Modules
|
||||
@ -32,6 +37,7 @@ pub mod async_signal;
|
||||
pub mod derived;
|
||||
pub mod effect;
|
||||
pub mod enum_split;
|
||||
pub mod mapped_signal;
|
||||
pub mod memo;
|
||||
pub mod signal;
|
||||
pub mod trigger;
|
||||
@ -44,6 +50,7 @@ pub use self::{
|
||||
derived::Derived,
|
||||
effect::{Effect, EffectRun, EffectRunCtx, WeakEffect},
|
||||
enum_split::{EnumSplitSignal, SignalEnumSplit},
|
||||
mapped_signal::TryMappedSignal,
|
||||
memo::Memo,
|
||||
signal::{
|
||||
Signal,
|
||||
|
||||
327
dynatos-reactive/src/mapped_signal.rs
Normal file
327
dynatos-reactive/src/mapped_signal.rs
Normal file
@ -0,0 +1,327 @@
|
||||
//! Mapped signal
|
||||
|
||||
// TODO: Support other worlds
|
||||
|
||||
// Lints
|
||||
#![expect(type_alias_bounds, reason = "We can't use `T::Residual` without the bound")]
|
||||
|
||||
// Imports
|
||||
use {
|
||||
crate::{
|
||||
world::ReactiveWorldInner,
|
||||
Effect,
|
||||
EffectRun,
|
||||
Signal,
|
||||
SignalGetCloned,
|
||||
SignalSet,
|
||||
SignalUpdate,
|
||||
SignalWith,
|
||||
Trigger,
|
||||
WeakEffect,
|
||||
},
|
||||
core::{
|
||||
cell::OnceCell,
|
||||
ops::{ControlFlow, FromResidual, Residual, Try},
|
||||
},
|
||||
dynatos_world::{IMut, IMutLike, Rc, WorldDefault},
|
||||
zutil_cloned::cloned,
|
||||
};
|
||||
|
||||
/// Mapped signal.
|
||||
///
|
||||
/// Maps a signal, fallibly.
|
||||
///
|
||||
/// ```
|
||||
/// # use dynatos_reactive::{Signal, SignalGetCloned, SignalGet, SignalSet, TryMappedSignal};
|
||||
/// let outer = Signal::new(Some(5));
|
||||
/// let mapped = TryMappedSignal::new(outer.clone(), |opt| *opt, |opt, &value| *opt = Some(value));
|
||||
/// let inner = mapped.get_cloned().expect("Signal exists");
|
||||
/// assert_eq!(inner.get(), 5);
|
||||
///
|
||||
/// // Writes into the inner signal change the outer signal
|
||||
/// inner.set(6);
|
||||
/// assert_eq!(outer.get(), Some(6));
|
||||
///
|
||||
/// // Writes into the outer signal change the inner signal,
|
||||
/// // without re-running the current context...
|
||||
/// outer.set(Some(6));
|
||||
/// assert_eq!(inner.get(), 6);
|
||||
///
|
||||
/// // ... unless an error happens
|
||||
/// outer.set(None);
|
||||
/// assert!(mapped.get_cloned().is_none());
|
||||
/// ```
|
||||
pub struct TryMappedSignal<T>
|
||||
where
|
||||
T: Try<Residual: Residual<Signal<T::Output>>>,
|
||||
{
|
||||
/// Output signal
|
||||
output: OutputSignal<T>,
|
||||
|
||||
// TODO: Make the effects not dynamic?
|
||||
/// Get effect
|
||||
_get_effect: Effect<dyn EffectRun>,
|
||||
|
||||
/// Set effect
|
||||
_set_effect: Effect<dyn EffectRun>,
|
||||
|
||||
/// Trigger
|
||||
trigger: Trigger,
|
||||
}
|
||||
|
||||
impl<T> TryMappedSignal<T>
|
||||
where
|
||||
T: Try<Residual: Residual<Signal<T::Output>>>,
|
||||
{
|
||||
/// Creates a new mapped signal from a fallible getter
|
||||
pub fn new<S, TryGet, Set>(input: S, try_get: TryGet, set: Set) -> Self
|
||||
where
|
||||
T: 'static,
|
||||
S: for<'a> SignalWith<Value<'a>: Sized> + for<'a> SignalUpdate<Value<'a>: Sized> + Clone + 'static,
|
||||
TryGet: Fn(<S as SignalWith>::Value<'_>) -> T + 'static,
|
||||
Set: Fn(<S as SignalUpdate>::Value<'_>, &T::Output) + 'static,
|
||||
{
|
||||
// Output signal
|
||||
let output_sig = Rc::<_, WorldDefault>::new(IMut::<_, WorldDefault>::new(None::<SignalTry<T>>));
|
||||
|
||||
// Trigger for gathering dependencies on retrieving the output signal,
|
||||
// but *not* on output signal changes.
|
||||
let trigger = Trigger::new();
|
||||
|
||||
// Weak reference to the `set_effect`, to ensure that we don't end
|
||||
// up with a loop and leak memory
|
||||
let set_weak_effect = Rc::<_, WorldDefault>::new(OnceCell::<
|
||||
WeakEffect<<WorldDefault as ReactiveWorldInner>::F, WorldDefault>,
|
||||
>::new());
|
||||
|
||||
// The getter effect that sets the output signal
|
||||
#[cloned(input, output_sig, trigger, set_weak_effect)]
|
||||
let get_effect = Effect::new(move || {
|
||||
input.with(|input| {
|
||||
let value = try_get(input);
|
||||
|
||||
let mut output = output_sig.write();
|
||||
let (new_output, needs_trigger) = match value.branch() {
|
||||
// If the value was ok, check whether we already had a value or not
|
||||
ControlFlow::Continue(value) => match output.take().map(Try::branch) {
|
||||
// If we had a signal already, write to it
|
||||
Some(ControlFlow::Continue(signal)) => {
|
||||
// If we have the set effect, run it suppressed,
|
||||
// to avoid writing the value of the output signal
|
||||
// back into the input.
|
||||
match set_weak_effect.get().and_then(WeakEffect::upgrade) {
|
||||
Some(set_effect) => set_effect.suppressed(|| signal.set(value)),
|
||||
None => signal.set(value),
|
||||
}
|
||||
|
||||
(SignalTry::<T>::from_output(signal), false)
|
||||
},
|
||||
|
||||
// Otherwise, we either had a failure, or nothing, so write a new signal
|
||||
// Note: If we're writing a new signal, we trigger if this isn't the first time running
|
||||
res => (SignalTry::<T>::from_output(Signal::new(value)), res.is_some()),
|
||||
},
|
||||
|
||||
// If the value was an error, wipe the signal
|
||||
ControlFlow::Break(err) => (
|
||||
SignalTry::<T>::from_residual(err),
|
||||
output.take().map(Try::branch).is_some(),
|
||||
),
|
||||
};
|
||||
|
||||
*output = Some(new_output);
|
||||
drop(output);
|
||||
if needs_trigger {
|
||||
trigger.exec();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// The set effect that writes the output back to the input
|
||||
let get_weak_effect = get_effect.downgrade();
|
||||
#[cloned(output_sig)]
|
||||
let set_effect = Effect::new_raw(move || {
|
||||
self::with_output_signal::<T, _>(&output_sig, |output| {
|
||||
let update = || input.update(|input| output.with(|output| set(input, output)));
|
||||
|
||||
// If we have the get effect, run it suppressed,
|
||||
// to avoid writing the value back into the output signal
|
||||
match get_weak_effect.upgrade() {
|
||||
Some(get_effect) => get_effect.suppressed(update),
|
||||
None => update(),
|
||||
}
|
||||
});
|
||||
});
|
||||
set_effect.gather_dependencies(|| self::with_output_signal::<T, _>(&output_sig, |sig| sig.with(|_| ())));
|
||||
|
||||
set_weak_effect
|
||||
.set(set_effect.downgrade())
|
||||
.expect("Set effect should be uninitialized");
|
||||
|
||||
Self {
|
||||
output: output_sig,
|
||||
_get_effect: get_effect,
|
||||
_set_effect: set_effect,
|
||||
trigger,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> SignalGetCloned for TryMappedSignal<T>
|
||||
where
|
||||
T: Try<Residual: Residual<Signal<T::Output>>>,
|
||||
SignalTry<T>: Clone,
|
||||
{
|
||||
type Value = SignalTry<T>;
|
||||
|
||||
fn get_cloned(&self) -> Self::Value {
|
||||
self.trigger.gather_subscribers();
|
||||
self.output.read().as_ref().expect("Output signal was missing").clone()
|
||||
}
|
||||
|
||||
fn get_cloned_raw(&self) -> Self::Value {
|
||||
self.output.read().as_ref().expect("Output signal was missing").clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// Output signal type
|
||||
type OutputSignal<T> = Rc<IMut<Option<SignalTry<T>>, WorldDefault>, WorldDefault>;
|
||||
|
||||
/// Signal try type
|
||||
type SignalTry<T: Try> = <T::Residual as Residual<Signal<T::Output>>>::TryType;
|
||||
|
||||
|
||||
/// Accesses the inner type of an `OutputSignal`.
|
||||
///
|
||||
/// Assumes the inner value is populated
|
||||
fn with_output_signal<T, F>(output_sig: &OutputSignal<T>, f: F)
|
||||
where
|
||||
T: Try<Residual: Residual<Signal<T::Output>>>,
|
||||
F: FnOnce(&Signal<T::Output>),
|
||||
{
|
||||
let mut output = output_sig.write();
|
||||
|
||||
// Take the existing type and branch on it
|
||||
let new_output = match output.take().expect("Output signal was missing").branch() {
|
||||
ControlFlow::Continue(sig) => {
|
||||
f(&sig);
|
||||
SignalTry::<T>::from_output(sig)
|
||||
},
|
||||
ControlFlow::Break(err) => SignalTry::<T>::from_residual(err),
|
||||
};
|
||||
|
||||
*output = Some(new_output);
|
||||
}
|
||||
|
||||
/// Mapped signal.
|
||||
///
|
||||
/// Maps a signal, infallibly.
|
||||
pub struct MappedSignal<T>(TryMappedSignal<Result<T, !>>);
|
||||
|
||||
impl<T> MappedSignal<T> {
|
||||
/// Creates a new mapped signal from a fallible getter
|
||||
pub fn new<S, Get, Set>(input: S, get: Get, set: Set) -> Self
|
||||
where
|
||||
T: 'static,
|
||||
S: for<'a> SignalWith<Value<'a>: Sized> + for<'a> SignalUpdate<Value<'a>: Sized> + Clone + 'static,
|
||||
Get: Fn(<S as SignalWith>::Value<'_>) -> T + 'static,
|
||||
Set: Fn(<S as SignalUpdate>::Value<'_>, &T) + 'static,
|
||||
{
|
||||
Self(TryMappedSignal::new(
|
||||
input,
|
||||
move |value| Ok(get(value)),
|
||||
move |value, new_value| set(value, new_value),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> SignalGetCloned for MappedSignal<T> {
|
||||
type Value = Signal<T>;
|
||||
|
||||
fn get_cloned(&self) -> Self::Value {
|
||||
self.0.get_cloned().into_ok()
|
||||
}
|
||||
|
||||
fn get_cloned_raw(&self) -> Self::Value {
|
||||
self.0.get_cloned_raw().into_ok()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use {
|
||||
super::*,
|
||||
crate::SignalGet,
|
||||
core::{assert_matches::assert_matches, cell::Cell},
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn basic() {
|
||||
let outer = Signal::new(Ok::<_, ()>(5));
|
||||
|
||||
// Counts the number of times that `outer` was written to
|
||||
#[thread_local]
|
||||
static TIMES_OUTER_CHANGED: Cell<usize> = Cell::new(0);
|
||||
#[cloned(outer)]
|
||||
let _effect = Effect::new(move || {
|
||||
_ = outer.get();
|
||||
TIMES_OUTER_CHANGED.set(TIMES_OUTER_CHANGED.get() + 1);
|
||||
});
|
||||
assert_eq!(TIMES_OUTER_CHANGED.get(), 1);
|
||||
|
||||
let mapped = TryMappedSignal::new(outer.clone(), |opt| opt.ok(), |opt, &value| *opt = Ok(value));
|
||||
assert_eq!(TIMES_OUTER_CHANGED.get(), 1);
|
||||
|
||||
{
|
||||
let inner = mapped.get_cloned().expect("Signal was missing");
|
||||
assert_eq!(TIMES_OUTER_CHANGED.get(), 1);
|
||||
assert_eq!(inner.get(), 5);
|
||||
|
||||
outer.set(Ok(6));
|
||||
assert_eq!(TIMES_OUTER_CHANGED.get(), 2);
|
||||
assert_eq!(inner.get(), 6);
|
||||
|
||||
inner.set(7);
|
||||
assert_eq!(TIMES_OUTER_CHANGED.get(), 3);
|
||||
assert_eq!(outer.get(), Ok(7));
|
||||
};
|
||||
|
||||
{
|
||||
outer.set(Err(()));
|
||||
assert_matches!(mapped.get_cloned(), None);
|
||||
};
|
||||
|
||||
{
|
||||
outer.set(Ok(1));
|
||||
let inner = mapped.get_cloned().expect("Signal was missing");
|
||||
assert_eq!(inner.get(), 1);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn effects() {
|
||||
let outer = Signal::new(Ok::<_, i32>(5));
|
||||
let mapped = TryMappedSignal::new(outer.clone(), |opt| *opt, |opt, &value| *opt = Ok(value));
|
||||
|
||||
// Counts the times that the mapped signal was run
|
||||
#[thread_local]
|
||||
static TIMES_RUN: Cell<usize> = Cell::new(0);
|
||||
let _effect = Effect::new(move || {
|
||||
_ = mapped.get_cloned();
|
||||
TIMES_RUN.set(TIMES_RUN.get() + 1);
|
||||
});
|
||||
|
||||
assert_eq!(TIMES_RUN.get(), 1);
|
||||
outer.set(Ok(6));
|
||||
assert_eq!(TIMES_RUN.get(), 1);
|
||||
outer.set(Err(1));
|
||||
assert_eq!(TIMES_RUN.get(), 2);
|
||||
outer.set(Err(2));
|
||||
assert_eq!(TIMES_RUN.get(), 3);
|
||||
outer.set(Ok(1));
|
||||
assert_eq!(TIMES_RUN.get(), 4);
|
||||
outer.set(Ok(2));
|
||||
assert_eq!(TIMES_RUN.get(), 4);
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user