Added Signal, Effect and WeakEffect primitives to dynatos-reactive.

This commit is contained in:
Filipe Rodrigues 2024-02-03 23:35:43 +00:00
parent ecd5254f6f
commit b1967cddef
Signed by: zenithsiz
SSH Key Fingerprint: SHA256:Mb5ppb3Sh7IarBO/sBTXLHbYEOz37hJAlslLQPPAPaU
6 changed files with 365 additions and 0 deletions

83
Cargo.lock generated
View File

@ -2,6 +2,16 @@
# It is not intended for manual editing.
version = 3
[[package]]
name = "duplicate"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de78e66ac9061e030587b2a2e75cc88f22304913c907b11307bca737141230cb"
dependencies = [
"heck",
"proc-macro-error",
]
[[package]]
name = "dynatos"
version = "0.1.0"
@ -9,3 +19,76 @@ version = "0.1.0"
[[package]]
name = "dynatos-reactive"
version = "0.1.0"
dependencies = [
"duplicate",
]
[[package]]
name = "heck"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
[[package]]
name = "proc-macro-error"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
dependencies = [
"proc-macro-error-attr",
"proc-macro2",
"quote",
"syn",
"version_check",
]
[[package]]
name = "proc-macro-error-attr"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
dependencies = [
"proc-macro2",
"quote",
"version_check",
]
[[package]]
name = "proc-macro2"
version = "1.0.78"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
dependencies = [
"proc-macro2",
]
[[package]]
name = "syn"
version = "1.0.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
dependencies = [
"proc-macro2",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[package]]
name = "version_check"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"

View File

@ -8,3 +8,5 @@ resolver = "2"
# Workspace members
dynatos = { path = "dynatos" }
dynatos-reactive = { path = "dynatos-reactive" }
duplicate = "1.0.0"

View File

@ -4,3 +4,5 @@ version = "0.1.0"
edition = "2021"
[dependencies]
duplicate = { workspace = true }

View File

@ -0,0 +1,121 @@
//! Effect
//!
//! An effect is a function that is re-run whenever
//! one of it's dependencies changes.
// Imports
use std::{
cell::RefCell,
hash::Hash,
rc::{Rc, Weak},
};
thread_local! {
/// Effect stack
static EFFECT_STACK: RefCell<Vec<Effect>> = RefCell::new(vec![]);
}
/// Effect inner
struct Inner {
/// Effect runner
run: Box<dyn Fn()>,
}
/// Effect
#[derive(Clone)]
pub struct Effect {
/// Inner
inner: Rc<RefCell<Inner>>,
}
impl Effect {
/// Creates a new computed effect.
///
/// Runs the effect once to gather dependencies.
pub fn new<F>(run: F) -> Self
where
F: Fn() + 'static,
{
// Create the effect
let inner = Inner { run: Box::new(run) };
let effect = Self {
inner: Rc::new(RefCell::new(inner)),
};
// And run it once to gather dependencies.
effect.run();
effect
}
/// Downgrades this effect
pub fn downgrade(&self) -> WeakEffect {
WeakEffect {
inner: Rc::downgrade(&self.inner),
}
}
/// Returns the current running effect
pub fn running() -> Option<Self> {
EFFECT_STACK.with_borrow(|effects| effects.last().cloned())
}
/// Runs the effect
pub fn run(&self) {
// Push the effect, run the closure and pop it
EFFECT_STACK.with_borrow_mut(|effects| effects.push(self.clone()));
// Then run it
let inner = self.inner.borrow();
let run = inner.run.as_ref();
run();
// And finally pop the effect from the stack
EFFECT_STACK
.with_borrow_mut(|effects| effects.pop())
.expect("Missing added effect");
}
}
/// Weak effect
///
/// Used to break ownership between a signal and it's subscribers
#[derive(Clone)]
pub struct WeakEffect {
/// Inner
inner: Weak<RefCell<Inner>>,
}
impl WeakEffect {
/// Upgrades this effect
pub fn upgrade(&self) -> Option<Effect> {
self.inner.upgrade().map(|inner| Effect { inner })
}
/// Runs this effect, if it exists.
///
/// Returns if the effect still existed
pub fn try_run(&self) -> bool {
// Try to upgrade, else return that it was missing
let Some(effect) = self.upgrade() else {
return false;
};
effect.run();
true
}
}
impl PartialEq for WeakEffect {
fn eq(&self, other: &Self) -> bool {
Weak::ptr_eq(&self.inner, &other.inner)
}
}
impl Eq for WeakEffect {}
impl Hash for WeakEffect {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.inner.as_ptr().hash(state);
}
}

View File

@ -1 +1,11 @@
//! Reactivity for [`dynatos`]
// Modules
pub mod effect;
pub mod signal;
// Exports
pub use self::{
effect::{Effect, WeakEffect},
signal::Signal,
};

View File

@ -0,0 +1,147 @@
//! Signal
//!
//! A read-write value that automatically updates
//! any subscribers when changed.
// Imports
use {
crate::{Effect, WeakEffect},
std::{cell::RefCell, collections::HashSet, mem, rc::Rc},
};
/// Signal inner
struct Inner<T> {
/// Value
value: T,
/// Subscribers
subscribers: HashSet<WeakEffect>,
}
/// Signal
pub struct Signal<T> {
/// Inner
inner: Rc<RefCell<Inner<T>>>,
}
impl<T> Signal<T> {
/// Creates a new signal
pub fn new(value: T) -> Self {
let inner = Inner {
value,
subscribers: HashSet::new(),
};
Self {
inner: Rc::new(RefCell::new(inner)),
}
}
/// Gets the inner value
#[track_caller]
pub fn get(&self) -> T
where
T: Copy,
{
if let Some(effect) = Effect::running() {
self.add_subscriber(effect);
}
self.inner
.try_borrow()
.expect("Cannot get signal value while updating")
.value
}
/// Calls `f` with the inner value
#[track_caller]
pub fn with<O, F>(&self, f: F) -> O
where
F: FnOnce(&T) -> O,
{
let inner = self.inner.try_borrow().expect("Cannot use signal value while updating");
f(&inner.value)
}
/// Sets the inner value.
///
/// Updates all subscribers.
///
/// Returns the previous value
pub fn set(&self, new_value: T) -> T {
self.update(|value| mem::replace(value, new_value))
}
/// Updates the value in-place.
///
/// Updates all subscribers
#[track_caller]
pub fn update<O, F>(&self, f: F) -> O
where
F: FnOnce(&mut T) -> O,
{
// Update the value and get the output
let output = {
let mut inner = self
.inner
.try_borrow_mut()
.expect("Cannot update signal value while using it");
f(&mut inner.value)
};
// Then update all subscribers, removing any stale ones.
// Note: Since running the effect will add subscribers, we can't keep
// the inner borrow active, so we gather all dependencies before-hand.
// However, we can remove subscribers in between running effects, so we
// don't need to wait for that.
let subscribers = self.inner.borrow().subscribers.iter().cloned().collect::<Vec<_>>();
for subscriber in subscribers {
if !subscriber.try_run() {
self.remove_subscriber(subscriber);
}
}
output
}
/// Explicitly adds a subscriber to this signal.
///
/// Returns if the subscriber already existed.
pub fn add_subscriber<S: IntoSubscriber>(&self, subscriber: S) -> bool {
let mut inner = self.inner.borrow_mut();
let new_effect = inner.subscribers.insert(subscriber.into_subscriber());
!new_effect
}
/// Removes a subscriber from this signal.
///
/// Returns if the subscriber existed
pub fn remove_subscriber<S: IntoSubscriber>(&self, subscriber: S) -> bool {
let mut inner = self.inner.borrow_mut();
inner.subscribers.remove(&subscriber.into_subscriber())
}
}
impl<T> Clone for Signal<T> {
fn clone(&self) -> Self {
Self {
inner: Rc::clone(&self.inner),
}
}
}
/// Types that may be converted into a subscriber
pub trait IntoSubscriber {
fn into_subscriber(self) -> WeakEffect;
}
#[duplicate::duplicate_item(
T body;
[ Effect ] [ self.downgrade() ];
[ &'_ Effect ] [ self.downgrade() ];
[ WeakEffect ] [ self ];
)]
impl IntoSubscriber for T {
fn into_subscriber(self) -> WeakEffect {
body
}
}