mirror of
https://github.com/Zenithsiz/dynatos.git
synced 2026-02-04 02:18:40 +00:00
603 lines
15 KiB
Rust
603 lines
15 KiB
Rust
//! Effect
|
|
//!
|
|
//! An effect is a function that is re-run whenever
|
|
//! one of it's dependencies changes.
|
|
|
|
// TODO: Downcasting? It isn't trivial due to the usages of `Rc<Inner<dyn EffectRun>>`,
|
|
// which doesn't allow casting to `Rc<dyn Any>`, required by `Rc::downcast`.
|
|
|
|
// Imports
|
|
#[cfg(debug_assertions)]
|
|
use core::panic::Location;
|
|
use {
|
|
crate::{world::EffectStack, ReactiveWorld, WeakTrigger},
|
|
core::{
|
|
fmt,
|
|
hash::Hash,
|
|
marker::{PhantomData, Unsize},
|
|
ops::CoerceUnsized,
|
|
sync::atomic::{self, AtomicBool},
|
|
},
|
|
dynatos_world::{IMut, IMutLike, IMutRefMut, Rc, RcLike, Weak, WeakLike, WorldDefault},
|
|
std::collections::HashSet,
|
|
};
|
|
|
|
/// Effect inner
|
|
pub(crate) struct Inner<F: ?Sized, W: ReactiveWorld> {
|
|
/// Whether this effect is currently suppressed
|
|
suppressed: AtomicBool,
|
|
|
|
#[cfg(debug_assertions)]
|
|
/// Where this effect was defined
|
|
defined_loc: &'static Location<'static>,
|
|
|
|
/// All dependencies of this effect
|
|
dependencies: IMut<HashSet<WeakTrigger<W>>, W>,
|
|
|
|
/// Lock for gathering dependencies
|
|
gather_deps_lock: IMut<(), W>,
|
|
|
|
/// Effect runner
|
|
run: F,
|
|
}
|
|
|
|
/// Effect
|
|
pub struct Effect<F: ?Sized, W: ReactiveWorld = WorldDefault> {
|
|
/// Inner
|
|
inner: Rc<Inner<F, W>, W>,
|
|
}
|
|
|
|
impl<F> Effect<F, WorldDefault> {
|
|
/// Creates a new computed effect.
|
|
///
|
|
/// Runs the effect once to gather dependencies.
|
|
#[track_caller]
|
|
pub fn new(run: F) -> Self
|
|
where
|
|
F: EffectRun<WorldDefault> + 'static,
|
|
{
|
|
Self::new_in(run, WorldDefault::default())
|
|
}
|
|
|
|
/// Crates a new raw computed effect.
|
|
///
|
|
/// The effect won't be run, and instead you must gather
|
|
/// dependencies manually.
|
|
#[track_caller]
|
|
pub fn new_raw(run: F) -> Self {
|
|
Self::new_raw_in(run, WorldDefault::default())
|
|
}
|
|
|
|
/// Tries to create a new effect.
|
|
///
|
|
/// If the effects ends up being inert, returns `None`
|
|
#[track_caller]
|
|
pub fn try_new(run: F) -> Option<Self>
|
|
where
|
|
F: EffectRun<WorldDefault> + 'static,
|
|
{
|
|
Self::try_new_in(run, WorldDefault::default())
|
|
}
|
|
}
|
|
|
|
impl<F, W: ReactiveWorld> Effect<F, W> {
|
|
/// Creates a new computed effect within a world.
|
|
///
|
|
/// Runs the effect once to gather dependencies.
|
|
#[track_caller]
|
|
pub fn new_in(run: F, world: W) -> Self
|
|
where
|
|
F: EffectRun<W> + Unsize<W::F> + 'static,
|
|
{
|
|
// Create the effect
|
|
let effect = Self::new_raw_in(run, world);
|
|
|
|
// And run it once to gather dependencies.
|
|
effect.run();
|
|
|
|
effect
|
|
}
|
|
|
|
/// Crates a new raw computed effect within a world.
|
|
///
|
|
/// The effect won't be run, and instead you must gather
|
|
/// dependencies manually.
|
|
#[track_caller]
|
|
pub fn new_raw_in(run: F, _world: W) -> Self {
|
|
let inner = Inner {
|
|
suppressed: AtomicBool::new(false),
|
|
#[cfg(debug_assertions)]
|
|
defined_loc: Location::caller(),
|
|
dependencies: IMut::<_, W>::new(HashSet::new()),
|
|
gather_deps_lock: IMut::<_, W>::new(()),
|
|
run,
|
|
};
|
|
|
|
Self {
|
|
inner: Rc::<_, W>::new(inner),
|
|
}
|
|
}
|
|
|
|
/// Tries to create a new effect within a world.
|
|
///
|
|
/// If the effects ends up being inert, returns `None`
|
|
#[track_caller]
|
|
pub fn try_new_in(run: F, world: W) -> Option<Self>
|
|
where
|
|
F: EffectRun<W> + Unsize<W::F> + 'static,
|
|
{
|
|
let effect = Self::new_in(run, world);
|
|
match effect.is_inert() {
|
|
true => None,
|
|
false => Some(effect),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<F: ?Sized, W: ReactiveWorld> Effect<F, W> {
|
|
/// Accesses the inner function
|
|
#[must_use]
|
|
pub fn inner_fn(&self) -> &F {
|
|
&self.inner.run
|
|
}
|
|
|
|
/// Returns where this effect was defined
|
|
#[cfg(debug_assertions)]
|
|
pub(crate) fn defined_loc(&self) -> &'static Location<'static> {
|
|
self.inner.defined_loc
|
|
}
|
|
|
|
/// Downgrades this effect
|
|
#[must_use]
|
|
pub fn downgrade(&self) -> WeakEffect<F, W> {
|
|
WeakEffect {
|
|
inner: Rc::<_, W>::downgrade(&self.inner),
|
|
}
|
|
}
|
|
|
|
/// Returns if this effect is inert.
|
|
///
|
|
/// An inert effect is one that will never be updated.
|
|
/// In detail, an effect is inert, if no other [`Effect`]s
|
|
/// or [`WeakEffect`]s exist that point to it.
|
|
#[must_use]
|
|
pub fn is_inert(&self) -> bool {
|
|
Rc::<_, W>::strong_count(&self.inner) == 1 && Rc::<_, W>::weak_count(&self.inner) == 0
|
|
}
|
|
|
|
/// Returns the pointer of this effect
|
|
///
|
|
/// This can be used for creating maps based on equality
|
|
#[must_use]
|
|
pub fn inner_ptr(&self) -> *const () {
|
|
Rc::<_, W>::as_ptr(&self.inner).cast()
|
|
}
|
|
|
|
/// Creates an effect dependency gatherer
|
|
///
|
|
/// While this type lives, all signals used will be gathered as dependencies
|
|
/// for this effect.
|
|
///
|
|
/// Removes any existing dependencies before returning.
|
|
#[must_use]
|
|
pub fn deps_gatherer(&self) -> EffectDepsGatherer<W>
|
|
where
|
|
F: Unsize<W::F> + 'static,
|
|
{
|
|
// Push the effect
|
|
W::EffectStack::push(self.downgrade());
|
|
|
|
// Lock the dependencies gatherer
|
|
let gather_deps_lock = self
|
|
.inner
|
|
.gather_deps_lock
|
|
.try_write()
|
|
.expect("Cannot gather effect dependencies recursively");
|
|
|
|
// Clear the dependencies
|
|
let mut deps = self.inner.dependencies.write();
|
|
#[expect(clippy::iter_over_hash_type, reason = "We don't care about the order here")]
|
|
for dep in deps.drain() {
|
|
let Some(trigger) = dep.upgrade() else { continue };
|
|
// TODO: Use `self.downgrade` once we fix issues around the world
|
|
trigger.remove_subscriber(W::EffectStack::top().expect("Just pushed"));
|
|
}
|
|
|
|
// Then return the gatherer, which will pop the effect from the stack on drop
|
|
EffectDepsGatherer(gather_deps_lock)
|
|
}
|
|
|
|
/// Gathers dependencies for this effect.
|
|
///
|
|
/// All signals used within `gather` will have this effect as a dependency.
|
|
///
|
|
/// Removes any existing dependencies before running.
|
|
pub fn gather_dependencies<G, O>(&self, gather: G) -> O
|
|
where
|
|
F: Unsize<W::F> + 'static,
|
|
G: FnOnce() -> O,
|
|
{
|
|
let _gatherer = self.deps_gatherer();
|
|
gather()
|
|
}
|
|
|
|
/// Runs the effect
|
|
pub fn run(&self)
|
|
where
|
|
F: EffectRun<W> + Unsize<W::F> + 'static,
|
|
{
|
|
// If we're suppressed, don't do anything
|
|
// TODO: Should we clear our dependencies in this case?
|
|
// TODO: Since triggers check if we're suppressed before adding
|
|
// us to the run queue, should we still need this check here?
|
|
if self.is_suppressed() {
|
|
return;
|
|
}
|
|
|
|
// Otherwise, run it
|
|
let ctx = EffectRunCtx {
|
|
// TODO: Not have to clone the effect each time we run?
|
|
effect: W::unsize_effect(self.clone()),
|
|
_phantom: PhantomData,
|
|
};
|
|
self.gather_dependencies(move || self.inner.run.run(ctx));
|
|
}
|
|
|
|
/// Suppresses this effect from running while calling this function
|
|
// TODO: Remove this and just add a wrapper around `EffectRun` with the check?
|
|
pub fn suppressed<F2, O>(&self, f: F2) -> O
|
|
where
|
|
F2: FnOnce() -> O,
|
|
{
|
|
// Set the suppress flag and run `f`
|
|
let last_suppressed = self.inner.suppressed.swap(true, atomic::Ordering::AcqRel);
|
|
let output = f();
|
|
|
|
// Then restore it
|
|
self.inner.suppressed.store(last_suppressed, atomic::Ordering::Release);
|
|
|
|
output
|
|
}
|
|
|
|
/// Returns whether the effect is suppressed
|
|
pub fn is_suppressed(&self) -> bool {
|
|
self.inner.suppressed.load(atomic::Ordering::Acquire)
|
|
}
|
|
|
|
/// Formats this effect into `s`
|
|
fn fmt_debug(&self, mut s: fmt::DebugStruct<'_, '_>) -> Result<(), fmt::Error> {
|
|
s.field("suppressed", &self.inner.suppressed.load(atomic::Ordering::Acquire));
|
|
|
|
#[cfg(debug_assertions)]
|
|
s.field_with("defined_loc", |f| fmt::Display::fmt(self.inner.defined_loc, f));
|
|
|
|
s.field_with("dependencies", |f| {
|
|
let mut s = f.debug_list();
|
|
|
|
let Some(deps) = self.inner.dependencies.try_read() else {
|
|
return s.finish_non_exhaustive();
|
|
};
|
|
#[expect(clippy::iter_over_hash_type, reason = "We don't care about the order")]
|
|
for dep in &*deps {
|
|
let Some(trigger) = dep.upgrade() else {
|
|
s.entry(&"<...>");
|
|
continue;
|
|
};
|
|
|
|
s.entry(&trigger);
|
|
}
|
|
|
|
s.finish()
|
|
});
|
|
|
|
s.finish_non_exhaustive()
|
|
}
|
|
}
|
|
|
|
impl<F1: ?Sized, F2: ?Sized, W: ReactiveWorld> PartialEq<Effect<F2, W>> for Effect<F1, W> {
|
|
fn eq(&self, other: &Effect<F2, W>) -> bool {
|
|
core::ptr::eq(self.inner_ptr(), other.inner_ptr())
|
|
}
|
|
}
|
|
|
|
impl<F: ?Sized, W: ReactiveWorld> Eq for Effect<F, W> {}
|
|
|
|
impl<F: ?Sized, W: ReactiveWorld> Clone for Effect<F, W> {
|
|
fn clone(&self) -> Self {
|
|
Self {
|
|
inner: Rc::<_, W>::clone(&self.inner),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<F: ?Sized, W: ReactiveWorld> Hash for Effect<F, W> {
|
|
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
|
|
Rc::<_, W>::as_ptr(&self.inner).hash(state);
|
|
}
|
|
}
|
|
|
|
impl<F: ?Sized, W: ReactiveWorld> fmt::Debug for Effect<F, W> {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
self.fmt_debug(f.debug_struct("Effect"))
|
|
}
|
|
}
|
|
|
|
impl<F1, F2, W> CoerceUnsized<Effect<F2, W>> for Effect<F1, W>
|
|
where
|
|
F1: ?Sized + Unsize<F2>,
|
|
F2: ?Sized,
|
|
W: ReactiveWorld,
|
|
Rc<Inner<F1, W>, W>: CoerceUnsized<Rc<Inner<F2, W>, W>>,
|
|
{
|
|
}
|
|
|
|
|
|
/// Weak effect
|
|
///
|
|
/// Used to break ownership between a signal and it's subscribers
|
|
pub struct WeakEffect<F: ?Sized, W: ReactiveWorld = WorldDefault> {
|
|
/// Inner
|
|
inner: Weak<Inner<F, W>, W>,
|
|
}
|
|
|
|
impl<F: ?Sized, W: ReactiveWorld> WeakEffect<F, W> {
|
|
/// Upgrades this effect
|
|
#[must_use]
|
|
pub fn upgrade(&self) -> Option<Effect<F, W>> {
|
|
self.inner.upgrade().map(|inner| Effect { inner })
|
|
}
|
|
|
|
/// Returns the pointer of this effect
|
|
///
|
|
/// This can be used for creating maps based on equality
|
|
#[must_use]
|
|
pub fn inner_ptr(&self) -> *const () {
|
|
Weak::<_, W>::as_ptr(&self.inner).cast()
|
|
}
|
|
|
|
/// Runs this effect, if it exists.
|
|
///
|
|
/// Returns if the effect still existed
|
|
pub fn try_run(&self) -> bool
|
|
where
|
|
F: EffectRun<W> + Unsize<W::F> + 'static,
|
|
{
|
|
// Try to upgrade, else return that it was missing
|
|
let Some(effect) = self.upgrade() else {
|
|
return false;
|
|
};
|
|
|
|
effect.run();
|
|
true
|
|
}
|
|
|
|
/// Adds a dependency to this effect
|
|
pub(crate) fn add_dependency(&self, trigger: WeakTrigger<W>) {
|
|
let Some(effect) = self.upgrade() else { return };
|
|
effect.inner.dependencies.write().insert(trigger);
|
|
}
|
|
}
|
|
|
|
impl<F1: ?Sized, F2: ?Sized, W: ReactiveWorld> PartialEq<WeakEffect<F2, W>> for WeakEffect<F1, W> {
|
|
fn eq(&self, other: &WeakEffect<F2, W>) -> bool {
|
|
core::ptr::eq(self.inner_ptr(), other.inner_ptr())
|
|
}
|
|
}
|
|
|
|
impl<F: ?Sized, W: ReactiveWorld> Eq for WeakEffect<F, W> {}
|
|
|
|
impl<F: ?Sized, W: ReactiveWorld> Clone for WeakEffect<F, W> {
|
|
fn clone(&self) -> Self {
|
|
Self {
|
|
inner: Weak::<_, W>::clone(&self.inner),
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
impl<F: ?Sized, W: ReactiveWorld> Hash for WeakEffect<F, W> {
|
|
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
|
|
self.inner_ptr().hash(state);
|
|
}
|
|
}
|
|
|
|
impl<F: ?Sized, W: ReactiveWorld> fmt::Debug for WeakEffect<F, W> {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
let mut s = f.debug_struct("WeakEffect");
|
|
|
|
match self.upgrade() {
|
|
Some(effect) => effect.fmt_debug(s),
|
|
None => s.finish_non_exhaustive(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<F1, F2, W> CoerceUnsized<WeakEffect<F2, W>> for WeakEffect<F1, W>
|
|
where
|
|
F1: ?Sized + Unsize<F2>,
|
|
F2: ?Sized,
|
|
W: ReactiveWorld,
|
|
Weak<Inner<F1, W>, W>: CoerceUnsized<Weak<Inner<F2, W>, W>>,
|
|
{
|
|
}
|
|
|
|
/// Effect dependency gatherer.
|
|
///
|
|
/// While this type is alive, any signals used will
|
|
/// be added as a dependency.
|
|
pub struct EffectDepsGatherer<'a, W: ReactiveWorld = WorldDefault>(IMutRefMut<'a, (), W>);
|
|
|
|
impl<W: ReactiveWorld> Drop for EffectDepsGatherer<'_, W> {
|
|
fn drop(&mut self) {
|
|
// Pop our effect from the stack
|
|
W::EffectStack::pop();
|
|
}
|
|
}
|
|
|
|
/// Returns the current running effect
|
|
// TODO: Move this elsewhere
|
|
#[must_use]
|
|
pub fn running<W: ReactiveWorld>() -> Option<WeakEffect<W::F, W>> {
|
|
<W>::EffectStack::top()
|
|
}
|
|
|
|
/// Effect run
|
|
pub trait EffectRun<W: ReactiveWorld = WorldDefault> {
|
|
/// Runs the effect
|
|
fn run(&self, ctx: EffectRunCtx<'_, W>);
|
|
}
|
|
|
|
/// Effect run context
|
|
pub struct EffectRunCtx<'a, W: ReactiveWorld> {
|
|
/// Effect
|
|
effect: Effect<W::F, W>,
|
|
|
|
_phantom: PhantomData<&'a ()>,
|
|
}
|
|
|
|
impl<'a, W: ReactiveWorld> EffectRunCtx<'a, W> {
|
|
/// Returns the effect running this
|
|
pub fn effect(&self) -> Effect<W::F, W> {
|
|
self.effect.clone()
|
|
}
|
|
}
|
|
|
|
impl<F, W> EffectRun<W> for F
|
|
where
|
|
F: Fn(),
|
|
W: ReactiveWorld,
|
|
{
|
|
fn run(&self, _ctx: EffectRunCtx<'_, W>) {
|
|
self();
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
// Imports
|
|
extern crate test;
|
|
use {
|
|
super::{super::effect, *},
|
|
core::{
|
|
cell::{Cell, OnceCell},
|
|
mem,
|
|
},
|
|
test::Bencher,
|
|
};
|
|
|
|
/// Ensures effects are executed
|
|
#[test]
|
|
fn run() {
|
|
#[thread_local]
|
|
static COUNT: Cell<usize> = Cell::new(0);
|
|
|
|
assert_eq!(COUNT.get(), 0);
|
|
let effect = Effect::new(|| COUNT.update(|x| x + 1));
|
|
assert_eq!(COUNT.get(), 1);
|
|
effect.run();
|
|
assert_eq!(COUNT.get(), 2);
|
|
}
|
|
|
|
/// Ensures the function returned by `Effect::running` is the same as the future being run.
|
|
#[test]
|
|
fn running() {
|
|
#[thread_local]
|
|
static RUNNING: OnceCell<WeakEffect<dyn EffectRun, WorldDefault>> = OnceCell::new();
|
|
|
|
// Create an effect, and save the running effect within it to `RUNNING`.
|
|
let effect = Effect::new(move || {
|
|
RUNNING
|
|
.set(effect::running().expect("Future wasn't running"))
|
|
.expect("Unable to set running effect");
|
|
});
|
|
|
|
// Then ensure the running effect is the same as the one created.
|
|
let running = RUNNING
|
|
.get()
|
|
.expect("Running effect missing")
|
|
.upgrade()
|
|
.expect("Running effect was dropped");
|
|
assert_eq!(effect, running);
|
|
}
|
|
|
|
/// Ensures the function returned by `Effect::running` is the same as the future being run,
|
|
/// while running stacked futures
|
|
#[test]
|
|
fn running_stacked() {
|
|
#[thread_local]
|
|
static RUNNING_TOP: OnceCell<WeakEffect<dyn EffectRun>> = OnceCell::new();
|
|
|
|
#[thread_local]
|
|
static RUNNING_BOTTOM: OnceCell<WeakEffect<dyn EffectRun>> = OnceCell::new();
|
|
|
|
// Create 2 stacked effects, saving the running within each to `running1` and `running2`.
|
|
// `running1` contains the top-level effect, while `running2` contains the inner one.
|
|
let effect = Effect::new(move || {
|
|
RUNNING_TOP
|
|
.set(effect::running().expect("Future wasn't running"))
|
|
.expect("Unable to set running effect");
|
|
|
|
let effect = Effect::new(move || {
|
|
RUNNING_BOTTOM
|
|
.set(effect::running().expect("Future wasn't running"))
|
|
.expect("Unable to set running effect");
|
|
});
|
|
|
|
// Then ensure the bottom-level running effect is the same as the one created.
|
|
let running_bottom = RUNNING_BOTTOM
|
|
.get()
|
|
.expect("Running effect missing")
|
|
.upgrade()
|
|
.expect("Running effect was dropped");
|
|
assert_eq!(effect, running_bottom);
|
|
});
|
|
|
|
// Then ensure the top-level running effect is the same as the one created.
|
|
let running_top = RUNNING_TOP
|
|
.get()
|
|
.expect("Running effect missing")
|
|
.upgrade()
|
|
.expect("Running effect was dropped");
|
|
assert_eq!(effect, running_top);
|
|
|
|
// And that the bottom-level running effect was already dropped
|
|
let running_bottom = RUNNING_BOTTOM.get().expect("Running effect missing").upgrade();
|
|
assert_eq!(running_bottom, None);
|
|
}
|
|
|
|
#[bench]
|
|
fn get_running_100_none(bencher: &mut Bencher) {
|
|
bencher.iter(|| {
|
|
for _ in 0..100 {
|
|
let effect = effect::running::<WorldDefault>();
|
|
test::black_box(effect);
|
|
}
|
|
});
|
|
}
|
|
|
|
#[bench]
|
|
fn get_running_100_some(bencher: &mut Bencher) {
|
|
let effect = Effect::new_raw(move || ());
|
|
|
|
effect.gather_dependencies(|| {
|
|
bencher.iter(|| {
|
|
for _ in 0..100 {
|
|
let effect = effect::running::<WorldDefault>();
|
|
test::black_box(effect);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
#[bench]
|
|
fn create_10(bencher: &mut Bencher) {
|
|
bencher.iter(|| {
|
|
for _ in 0..10 {
|
|
let effect = Effect::new(move || ());
|
|
test::black_box(&effect);
|
|
mem::forget(effect);
|
|
}
|
|
});
|
|
}
|
|
}
|