feat: initial work on reactive stores

This commit is contained in:
Greg Johnston 2024-07-10 20:34:50 -04:00
parent 6ca3639c3e
commit db8f5e4899
16 changed files with 1062 additions and 41 deletions

View file

@ -19,6 +19,8 @@ members = [
"leptos_macro",
"leptos_server",
"reactive_graph",
"reactive_stores",
"reactive_stores_macro",
"server_fn",
"server_fn_macro",
"server_fn/server_fn_macro_default",
@ -60,6 +62,8 @@ next_tuple = { path = "./next_tuple", version = "0.1.0-alpha" }
oco_ref = { path = "./oco", version = "0.2" }
or_poisoned = { path = "./or_poisoned", version = "0.1" }
reactive_graph = { path = "./reactive_graph", version = "0.1.0-alpha" }
reactive_stores = { path = "./reactive_stores", version = "0.1.0-alpha" }
reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.1.0-alpha" }
server_fn = { path = "./server_fn", version = "0.7.0-alpha" }
server_fn_macro = { path = "./server_fn_macro", version = "0.7.0-alpha" }
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.7.0-alpha" }

View file

@ -0,0 +1,93 @@
use crate::{
effect::RenderEffect,
signal::ArcRwSignal,
traits::{Track, Update},
};
use or_poisoned::OrPoisoned;
use rustc_hash::FxHashMap;
use std::{
hash::Hash,
sync::{Arc, RwLock},
};
/// A conditional signal that only notifies subscribers when a change
/// in the source signals value changes whether the given function is true.
#[derive(Clone)]
pub struct Selector<T>
where
T: PartialEq + Eq + Clone + Hash + 'static,
{
subs: Arc<RwLock<FxHashMap<T, ArcRwSignal<bool>>>>,
v: Arc<RwLock<Option<T>>>,
#[allow(clippy::type_complexity)]
f: Arc<dyn Fn(&T, &T) -> bool + Send + Sync>,
// owning the effect keeps it alive, to keep updating the selector
#[allow(dead_code)]
effect: Arc<RenderEffect<T>>,
}
impl<T> Selector<T>
where
T: PartialEq + Eq + Clone + Hash + 'static,
{
pub fn new(source: impl Fn() -> T + Clone + 'static) -> Self {
Self::new_with_fn(source, PartialEq::eq)
}
pub fn new_with_fn(
source: impl Fn() -> T + Clone + 'static,
f: impl Fn(&T, &T) -> bool + Send + Sync + Clone + 'static,
) -> Self {
let subs: Arc<RwLock<FxHashMap<T, ArcRwSignal<bool>>>> =
Default::default();
let v: Arc<RwLock<Option<T>>> = Default::default();
let f = Arc::new(f) as Arc<dyn Fn(&T, &T) -> bool + Send + Sync>;
let effect = Arc::new(RenderEffect::new({
let subs = Arc::clone(&subs);
let f = Arc::clone(&f);
let v = Arc::clone(&v);
move |prev: Option<T>| {
let next_value = source();
*v.write().or_poisoned() = Some(next_value.clone());
if prev.as_ref() != Some(&next_value) {
for (key, signal) in &*subs.read().or_poisoned() {
if f(key, &next_value)
|| (prev.is_some()
&& f(key, prev.as_ref().unwrap()))
{
signal.update(|n| *n = true);
}
}
}
next_value
}
}));
Selector { subs, v, f, effect }
}
/// Reactively checks whether the given key is selected.
pub fn selected(&self, key: T) -> bool {
let read = {
let mut subs = self.subs.write().or_poisoned();
subs.entry(key.clone())
.or_insert_with(|| ArcRwSignal::new(false))
.clone()
};
read.track();
(self.f)(&key, self.v.read().or_poisoned().as_ref().unwrap())
}
/// Removes the listener for the given key.
pub fn remove(&self, key: &T) {
let mut subs = self.subs.write().or_poisoned();
subs.remove(key);
}
/// Clears the listeners for all keys.
pub fn clear(&self) {
let mut subs = self.subs.write().or_poisoned();
subs.clear();
}
}

View file

@ -6,7 +6,7 @@ use super::{
use crate::{
graph::{ReactiveNode, SubscriberSet},
prelude::{IsDisposed, Trigger},
traits::{DefinedAt, ReadUntracked, Writeable},
traits::{DefinedAt, ReadUntracked, UntrackableGuard, Writeable},
};
use core::fmt::{Debug, Formatter, Result};
use std::{
@ -156,13 +156,11 @@ impl<T> Trigger for ArcRwSignal<T> {
impl<T: 'static> Writeable for ArcRwSignal<T> {
type Value = T;
fn try_write(
&self,
) -> Option<WriteGuard<'_, Self, impl DerefMut<Target = Self::Value>>> {
fn try_write(&self) -> Option<impl UntrackableGuard<Target = Self::Value>> {
self.value
.write()
.ok()
.map(|guard| WriteGuard::new(self, guard))
.map(|guard| WriteGuard::new(self.clone(), guard))
}
fn try_write_untracked(&self) -> Option<UntrackedWriteGuard<Self::Value>> {

View file

@ -107,11 +107,11 @@ impl<T: 'static> Writeable for ArcWriteSignal<T> {
fn try_write(
&self,
) -> Option<WriteGuard<'_, Self, impl DerefMut<Target = Self::Value>>> {
) -> Option<WriteGuard<Self, impl DerefMut<Target = Self::Value>>> {
self.value
.write()
.ok()
.map(|guard| WriteGuard::new(self, guard))
.map(|guard| WriteGuard::new(self.clone(), guard))
}
fn try_write_untracked(&self) -> Option<UntrackedWriteGuard<Self::Value>> {

View file

@ -1,4 +1,7 @@
use crate::{computed::BlockingLock, traits::Trigger};
use crate::{
computed::BlockingLock,
traits::{Trigger, UntrackableGuard},
};
use core::fmt::Debug;
use guardian::{ArcRwLockReadGuardian, ArcRwLockWriteGuardian};
use std::{
@ -87,7 +90,7 @@ impl<T: 'static> Debug for Plain<T> {
}
impl<T: 'static> Plain<T> {
pub(crate) fn try_new(inner: Arc<RwLock<T>>) -> Option<Self> {
pub fn try_new(inner: Arc<RwLock<T>>) -> Option<Self> {
ArcRwLockReadGuardian::take(inner)
.ok()
.map(|guard| Plain { guard })
@ -131,7 +134,7 @@ impl<T: 'static> Debug for AsyncPlain<T> {
}
impl<T: 'static> AsyncPlain<T> {
pub(crate) fn try_new(inner: &Arc<async_lock::RwLock<T>>) -> Option<Self> {
pub fn try_new(inner: &Arc<async_lock::RwLock<T>>) -> Option<Self> {
Some(Self {
guard: inner.blocking_read_arc(),
})
@ -174,7 +177,7 @@ where
}
impl<T: 'static, U> Mapped<Plain<T>, U> {
pub(crate) fn try_new(
pub fn try_new(
inner: Arc<RwLock<T>>,
map_fn: fn(&T) -> &U,
) -> Option<Self> {
@ -187,7 +190,7 @@ impl<Inner, U> Mapped<Inner, U>
where
Inner: Deref,
{
pub(crate) fn new_with_guard(
pub fn new_with_guard(
inner: Inner,
map_fn: fn(&Inner::Target) -> &U,
) -> Self {
@ -234,31 +237,37 @@ where
}
#[derive(Debug)]
pub struct WriteGuard<'a, S, G>
pub struct WriteGuard<S, G>
where
S: Trigger,
{
pub(crate) triggerable: Option<&'a S>,
pub(crate) triggerable: Option<S>,
pub(crate) guard: Option<G>,
}
impl<'a, S, G> WriteGuard<'a, S, G>
impl<S, G> WriteGuard<S, G>
where
S: Trigger,
{
pub fn new(triggerable: &'a S, guard: G) -> Self {
pub fn new(triggerable: S, guard: G) -> Self {
Self {
triggerable: Some(triggerable),
guard: Some(guard),
}
}
}
pub fn untrack(&mut self) {
impl<S, G> UntrackableGuard for WriteGuard<S, G>
where
S: Trigger,
G: DerefMut,
{
fn untrack(&mut self) {
self.triggerable.take();
}
}
impl<'a, S, G> Deref for WriteGuard<'a, S, G>
impl<S, G> Deref for WriteGuard<S, G>
where
S: Trigger,
G: Deref,
@ -276,7 +285,7 @@ where
}
}
impl<'a, S, G> DerefMut for WriteGuard<'a, S, G>
impl<S, G> DerefMut for WriteGuard<S, G>
where
S: Trigger,
G: DerefMut,
@ -295,7 +304,7 @@ where
pub struct UntrackedWriteGuard<T: 'static>(ArcRwLockWriteGuardian<T>);
impl<T: 'static> UntrackedWriteGuard<T> {
pub(crate) fn try_new(inner: Arc<RwLock<T>>) -> Option<Self> {
pub fn try_new(inner: Arc<RwLock<T>>) -> Option<Self> {
ArcRwLockWriteGuardian::take(inner)
.ok()
.map(UntrackedWriteGuard)
@ -317,7 +326,7 @@ impl<T> DerefMut for UntrackedWriteGuard<T> {
}
// Dropping the write guard will notify dependencies.
impl<'a, S, T> Drop for WriteGuard<'a, S, T>
impl<S, T> Drop for WriteGuard<S, T>
where
S: Trigger,
{
@ -326,8 +335,82 @@ where
drop(self.guard.take());
// then, notify about a change
if let Some(triggerable) = self.triggerable {
if let Some(triggerable) = self.triggerable.as_ref() {
triggerable.trigger();
}
}
}
#[derive(Debug)]
pub struct MappedMut<Inner, U>
where
Inner: Deref,
{
inner: Inner,
map_fn: fn(&Inner::Target) -> &U,
map_fn_mut: fn(&mut Inner::Target) -> &mut U,
}
impl<Inner, U> UntrackableGuard for MappedMut<Inner, U>
where
Inner: UntrackableGuard,
{
fn untrack(&mut self) {
self.inner.untrack();
}
}
impl<Inner, U> MappedMut<Inner, U>
where
Inner: DerefMut,
{
pub fn new(
inner: Inner,
map_fn: fn(&Inner::Target) -> &U,
map_fn_mut: fn(&mut Inner::Target) -> &mut U,
) -> Self {
Self {
inner,
map_fn,
map_fn_mut,
}
}
}
impl<Inner, U> Deref for MappedMut<Inner, U>
where
Inner: Deref,
{
type Target = U;
fn deref(&self) -> &Self::Target {
(self.map_fn)(self.inner.deref())
}
}
impl<Inner, U> DerefMut for MappedMut<Inner, U>
where
Inner: DerefMut,
{
fn deref_mut(&mut self) -> &mut Self::Target {
(self.map_fn_mut)(self.inner.deref_mut())
}
}
impl<Inner, U: PartialEq> PartialEq for MappedMut<Inner, U>
where
Inner: Deref,
{
fn eq(&self, other: &Self) -> bool {
**self == **other
}
}
impl<Inner, U: Display> Display for MappedMut<Inner, U>
where
Inner: Deref,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
Display::fmt(&**self, f)
}
}

View file

@ -8,7 +8,8 @@ use crate::{
owner::StoredValue,
signal::guards::{UntrackedWriteGuard, WriteGuard},
traits::{
DefinedAt, Dispose, IsDisposed, ReadUntracked, Trigger, Writeable,
DefinedAt, Dispose, IsDisposed, ReadUntracked, Trigger,
UntrackableGuard, Writeable,
},
unwrap_signal,
};
@ -187,13 +188,11 @@ impl<T: 'static> Trigger for RwSignal<T> {
impl<T: 'static> Writeable for RwSignal<T> {
type Value = T;
fn try_write(
&self,
) -> Option<WriteGuard<'_, Self, impl DerefMut<Target = Self::Value>>> {
fn try_write(&self) -> Option<impl UntrackableGuard<Target = Self::Value>> {
let guard = self.inner.try_with_value(|n| {
ArcRwLockWriteGuardian::take(Arc::clone(&n.value)).ok()
})??;
Some(WriteGuard::new(self, guard))
Some(WriteGuard::new(*self, guard))
}
fn try_write_untracked(&self) -> Option<UntrackedWriteGuard<Self::Value>> {

View file

@ -4,7 +4,9 @@ use super::{
};
use crate::{
owner::StoredValue,
traits::{DefinedAt, Dispose, IsDisposed, Trigger, Writeable},
traits::{
DefinedAt, Dispose, IsDisposed, Trigger, UntrackableGuard, Writeable,
},
};
use core::fmt::Debug;
use guardian::ArcRwLockWriteGuardian;
@ -83,16 +85,16 @@ impl<T: 'static> Trigger for WriteSignal<T> {
impl<T: 'static> Writeable for WriteSignal<T> {
type Value = T;
fn try_write(
&self,
) -> Option<WriteGuard<'_, Self, impl DerefMut<Target = Self::Value>>> {
fn try_write(&self) -> Option<impl UntrackableGuard<Target = Self::Value>> {
let guard = self.inner.try_with_value(|n| {
ArcRwLockWriteGuardian::take(Arc::clone(&n.value)).ok()
})??;
Some(WriteGuard::new(self, guard))
Some(WriteGuard::new(*self, guard))
}
fn try_write_untracked(&self) -> Option<UntrackedWriteGuard<Self::Value>> {
fn try_write_untracked(
&self,
) -> Option<impl DerefMut<Target = Self::Value>> {
self.inner.with_value(|n| n.try_write_untracked())
}
}

View file

@ -172,22 +172,24 @@ where
}
}
pub trait UntrackableGuard: DerefMut {
fn untrack(&mut self);
}
pub trait Writeable: Sized + DefinedAt + Trigger {
type Value: Sized + 'static;
fn try_write(
&self,
) -> Option<WriteGuard<'_, Self, impl DerefMut<Target = Self::Value>>>;
fn try_write(&self) -> Option<impl UntrackableGuard<Target = Self::Value>>;
fn try_write_untracked(&self) -> Option<UntrackedWriteGuard<Self::Value>>;
fn write(
fn try_write_untracked(
&self,
) -> WriteGuard<'_, Self, impl DerefMut<Target = Self::Value>> {
) -> Option<impl DerefMut<Target = Self::Value>>;
fn write(&self) -> impl UntrackableGuard<Target = Self::Value> {
self.try_write().unwrap_or_else(unwrap_signal!(self))
}
fn write_untracked(&self) -> UntrackedWriteGuard<Self::Value> {
fn write_untracked(&self) -> impl DerefMut<Target = Self::Value> {
self.try_write_untracked()
.unwrap_or_else(unwrap_signal!(self))
}

View file

@ -0,0 +1,18 @@
[package]
name = "reactive_stores"
edition = "2021"
version = "0.1.0-alpha"
rust-version.workspace = true
[dependencies]
guardian = "1.1.0"
or_poisoned = { workspace = true }
reactive_graph = { workspace = true }
rustc-hash = "2"
[dev-dependencies]
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
tokio-test = { version = "0.4" }
any_spawner = { workspace = true, features = ["futures-executor", "tokio"] }
reactive_stores_macro = { workspace = true }
reactive_graph = { workspace = true, features = ["effects"] }

232
reactive_stores/src/lib.rs Normal file
View file

@ -0,0 +1,232 @@
use reactive_graph::{
signal::{
guards::{Mapped, Plain, ReadGuard},
ArcTrigger,
},
traits::{DefinedAt, IsDisposed, ReadUntracked, Track, Trigger},
};
use rustc_hash::FxHashMap;
use std::{
fmt::Debug,
panic::Location,
sync::{Arc, RwLock},
};
mod path;
mod read_store_field;
mod store_field;
mod subfield;
use path::StorePath;
use store_field::StoreField;
pub use subfield::Subfield;
pub struct ArcStore<T> {
#[cfg(debug_assertions)]
defined_at: &'static Location<'static>,
pub(crate) value: Arc<RwLock<T>>,
signals: Arc<RwLock<TriggerMap>>,
}
#[derive(Debug, Default)]
struct TriggerMap(FxHashMap<StorePath, ArcTrigger>);
impl TriggerMap {
fn get_or_insert(&mut self, key: StorePath) -> ArcTrigger {
if let Some(trigger) = self.0.get(&key) {
trigger.clone()
} else {
let new = ArcTrigger::new();
self.0.insert(key, new.clone());
new
}
}
fn remove(&mut self, key: &StorePath) -> Option<ArcTrigger> {
self.0.remove(key)
}
}
impl<T> ArcStore<T> {
pub fn new(value: T) -> Self {
Self {
#[cfg(debug_assertions)]
defined_at: Location::caller(),
value: Arc::new(RwLock::new(value)),
signals: Default::default(),
/* inner: Arc::new(RwLock::new(SubscriberSet::new())), */
}
}
}
impl<T: Debug> Debug for ArcStore<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut f = f.debug_struct("ArcStore");
#[cfg(debug_assertions)]
let f = f.field("defined_at", &self.defined_at);
f.field("value", &self.value)
.field("signals", &self.signals)
.finish()
}
}
impl<T> Clone for ArcStore<T> {
fn clone(&self) -> Self {
Self {
#[cfg(debug_assertions)]
defined_at: self.defined_at,
value: Arc::clone(&self.value),
signals: Arc::clone(&self.signals),
}
}
}
impl<T> DefinedAt for ArcStore<T> {
fn defined_at(&self) -> Option<&'static Location<'static>> {
#[cfg(debug_assertions)]
{
Some(self.defined_at)
}
#[cfg(not(debug_assertions))]
{
None
}
}
}
impl<T> IsDisposed for ArcStore<T> {
#[inline(always)]
fn is_disposed(&self) -> bool {
false
}
}
impl<T> ReadUntracked for ArcStore<T>
where
T: 'static,
{
type Value = ReadGuard<T, Plain<T>>;
fn try_read_untracked(&self) -> Option<Self::Value> {
Plain::try_new(Arc::clone(&self.value)).map(ReadGuard::new)
}
}
impl<T: 'static> Track for ArcStore<T> {
fn track(&self) {
self.get_trigger(Default::default()).trigger();
}
}
impl<T: 'static> Trigger for ArcStore<T> {
fn trigger(&self) {
self.get_trigger(self.path().collect()).trigger();
}
}
#[cfg(test)]
mod tests {
use super::ArcStore;
use crate as reactive_stores;
use reactive_graph::{
effect::Effect,
traits::{Read, ReadUntracked, Set, Update, Writeable},
};
use reactive_stores_macro::Store;
use std::sync::{
atomic::{AtomicUsize, Ordering},
Arc,
};
pub async fn tick() {
tokio::time::sleep(std::time::Duration::from_micros(1)).await;
}
#[derive(Debug, Store)]
struct Todos {
user: String,
todos: Vec<Todo>,
}
#[derive(Debug, Store)]
struct Todo {
label: String,
completed: bool,
}
impl Todo {
pub fn new(label: impl ToString) -> Self {
Self {
label: label.to_string(),
completed: false,
}
}
}
fn data() -> Todos {
Todos {
user: "Bob".to_string(),
todos: vec![
Todo {
label: "Create reactive store".to_string(),
completed: true,
},
Todo {
label: "???".to_string(),
completed: false,
},
Todo {
label: "Profit".to_string(),
completed: false,
},
],
}
}
#[tokio::test]
async fn mutating_field_triggers_effect() {
_ = any_spawner::Executor::init_tokio();
let combined_count = Arc::new(AtomicUsize::new(0));
let store = ArcStore::new(data());
assert_eq!(store.read_untracked().todos.len(), 3);
assert_eq!(store.clone().user().read_untracked().as_str(), "Bob");
Effect::new_sync({
let store = store.clone();
let combined_count = Arc::clone(&combined_count);
move |prev| {
if prev.is_none() {
println!("first run");
} else {
println!("next run");
}
println!("{:?}", *store.clone().user().read());
combined_count.fetch_add(1, Ordering::Relaxed);
}
});
tick().await;
tick().await;
store.clone().user().set("Greg".into());
tick().await;
store.clone().user().set("Carol".into());
tick().await;
store.clone().user().update(|name| name.push_str("!!!"));
tick().await;
// the effect reads from `user`, so it should trigger every time
assert_eq!(combined_count.load(Ordering::Relaxed), 4);
store
.clone()
.todos()
.write()
.push(Todo::new("Create reactive stores"));
tick().await;
store.clone().todos().write().push(Todo::new("???"));
tick().await;
store.clone().todos().write().push(Todo::new("Profit!"));
tick().await;
// the effect doesn't read from `todos`, so the count should not have changed
assert_eq!(combined_count.load(Ordering::Relaxed), 4);
}
}

View file

@ -0,0 +1,39 @@
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
pub struct StorePath(Vec<StorePathSegment>);
impl From<Vec<StorePathSegment>> for StorePath {
fn from(value: Vec<StorePathSegment>) -> Self {
Self(value)
}
}
impl StorePath {
pub fn push(&mut self, segment: impl Into<StorePathSegment>) {
self.0.push(segment.into());
}
pub fn pop(&mut self) -> Option<StorePathSegment> {
self.0.pop()
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct StorePathSegment(usize);
impl From<usize> for StorePathSegment {
fn from(value: usize) -> Self {
Self(value)
}
}
impl From<&usize> for StorePathSegment {
fn from(value: &usize) -> Self {
Self(*value)
}
}
impl FromIterator<StorePathSegment> for StorePath {
fn from_iter<T: IntoIterator<Item = StorePathSegment>>(iter: T) -> Self {
Self(Vec::from_iter(iter))
}
}

View file

@ -0,0 +1,64 @@
use reactive_graph::{
signal::{
guards::{Mapped, Plain, ReadGuard},
ArcTrigger,
},
traits::{DefinedAt, ReadUntracked, Track},
};
use std::{
panic::Location,
sync::{Arc, RwLock},
};
pub struct ArcReadStoreField<Orig, T>
where
T: 'static,
{
#[cfg(debug_assertions)]
defined_at: &'static Location<'static>,
data: Arc<RwLock<Orig>>,
trigger: ArcTrigger,
read: fn(&Orig) -> &T,
}
impl<Orig, T> Clone for ArcReadStoreField<Orig, T> {
fn clone(&self) -> Self {
Self {
#[cfg(debug_assertions)]
defined_at: self.defined_at,
data: Arc::clone(&self.data),
trigger: self.trigger.clone(),
read: self.read,
}
}
}
impl<Orig, T> DefinedAt for ArcReadStoreField<Orig, T> {
fn defined_at(&self) -> Option<&'static Location<'static>> {
#[cfg(debug_assertions)]
{
Some(self.defined_at)
}
#[cfg(not(debug_assertions))]
{
None
}
}
}
impl<Orig, T> Track for ArcReadStoreField<Orig, T> {
fn track(&self) {
self.trigger.track();
}
}
impl<Orig, T> ReadUntracked for ArcReadStoreField<Orig, T>
where
Orig: 'static,
{
type Value = ReadGuard<T, Mapped<Plain<Orig>, T>>;
fn try_read_untracked(&self) -> Option<Self::Value> {
Mapped::try_new(Arc::clone(&self.data), self.read).map(ReadGuard::new)
}
}

View file

@ -0,0 +1,68 @@
use crate::{
path::{StorePath, StorePathSegment},
ArcStore,
};
use guardian::ArcRwLockWriteGuardian;
use or_poisoned::OrPoisoned;
use reactive_graph::{
signal::{
guards::{Plain, WriteGuard},
ArcTrigger,
},
traits::UntrackableGuard,
};
use std::{
iter,
ops::Deref,
sync::{Arc, RwLock},
};
pub trait StoreField<T>: Sized {
type Orig;
type Reader: Deref<Target = T>;
type Writer: UntrackableGuard<Target = T>;
fn data(&self) -> Arc<RwLock<Self::Orig>>;
fn get_trigger(&self, path: StorePath) -> ArcTrigger;
fn path(&self) -> impl Iterator<Item = StorePathSegment>;
fn reader(&self) -> Option<Self::Reader>;
fn writer(&self) -> Option<Self::Writer>;
}
impl<T> StoreField<T> for ArcStore<T>
where
T: 'static,
{
type Orig = T;
type Reader = Plain<T>;
type Writer = WriteGuard<ArcTrigger, ArcRwLockWriteGuardian<T>>;
fn data(&self) -> Arc<RwLock<Self::Orig>> {
Arc::clone(&self.value)
}
fn get_trigger(&self, path: StorePath) -> ArcTrigger {
let triggers = &self.signals;
let trigger = triggers.write().or_poisoned().get_or_insert(path);
trigger
}
fn path(&self) -> impl Iterator<Item = StorePathSegment> {
iter::empty()
}
fn reader(&self) -> Option<Self::Reader> {
Plain::try_new(Arc::clone(&self.value))
}
fn writer(&self) -> Option<Self::Writer> {
let trigger = self.get_trigger(Default::default());
let guard =
ArcRwLockWriteGuardian::take(Arc::clone(&self.value)).ok()?;
Some(WriteGuard::new(trigger, guard))
}
}

View file

@ -0,0 +1,194 @@
use crate::{
path::{StorePath, StorePathSegment},
store_field::StoreField,
};
use reactive_graph::{
signal::{
guards::{
Mapped, MappedMut, Plain, ReadGuard, UntrackedWriteGuard,
WriteGuard,
},
ArcTrigger,
},
traits::{
DefinedAt, IsDisposed, ReadUntracked, Track, Trigger, UntrackableGuard,
Writeable,
},
};
use std::{
iter,
marker::PhantomData,
ops::DerefMut,
panic::Location,
sync::{Arc, RwLock},
};
#[derive(Debug)]
pub struct Subfield<Inner, Prev, T>
where
Inner: StoreField<Prev>,
{
#[cfg(debug_assertions)]
defined_at: &'static Location<'static>,
path_segment: StorePathSegment,
inner: Inner,
read: fn(&Prev) -> &T,
write: fn(&mut Prev) -> &mut T,
ty: PhantomData<T>,
}
impl<Inner, Prev, T> Clone for Subfield<Inner, Prev, T>
where
Inner: StoreField<Prev> + Clone,
{
fn clone(&self) -> Self {
Self {
#[cfg(debug_assertions)]
defined_at: self.defined_at,
path_segment: self.path_segment,
inner: self.inner.clone(),
read: self.read,
write: self.write,
ty: self.ty,
}
}
}
impl<Inner, Prev, T> Copy for Subfield<Inner, Prev, T> where
Inner: StoreField<Prev> + Copy
{
}
impl<Inner, Prev, T> Subfield<Inner, Prev, T>
where
Inner: StoreField<Prev>,
{
#[track_caller]
pub fn new(
inner: Inner,
path_segment: StorePathSegment,
read: fn(&Prev) -> &T,
write: fn(&mut Prev) -> &mut T,
) -> Self {
Self {
#[cfg(debug_assertions)]
defined_at: Location::caller(),
inner,
path_segment,
read,
write,
ty: PhantomData,
}
}
}
impl<Inner, Prev, T> StoreField<T> for Subfield<Inner, Prev, T>
where
Inner: StoreField<Prev>,
{
type Orig = Inner::Orig;
type Reader = Mapped<Inner::Reader, T>;
type Writer = MappedMut<WriteGuard<ArcTrigger, Inner::Writer>, T>;
fn path(&self) -> impl Iterator<Item = StorePathSegment> {
self.inner.path().chain(iter::once(self.path_segment))
}
fn data(&self) -> Arc<RwLock<Self::Orig>> {
self.inner.data()
}
fn get_trigger(&self, path: StorePath) -> ArcTrigger {
self.inner.get_trigger(path)
}
fn reader(&self) -> Option<Self::Reader> {
let inner = self.inner.reader()?;
Some(Mapped::new_with_guard(inner, self.read))
}
fn writer(&self) -> Option<Self::Writer> {
let trigger = self.get_trigger(self.path().collect());
let inner = WriteGuard::new(trigger, self.inner.writer()?);
Some(MappedMut::new(inner, self.read, self.write))
}
}
impl<Inner, Prev, T> DefinedAt for Subfield<Inner, Prev, T>
where
Inner: StoreField<Prev>,
{
fn defined_at(&self) -> Option<&'static Location<'static>> {
#[cfg(debug_assertions)]
{
Some(self.defined_at)
}
#[cfg(not(debug_assertions))]
{
None
}
}
}
impl<Inner, Prev, T> IsDisposed for Subfield<Inner, Prev, T>
where
Inner: StoreField<Prev> + IsDisposed,
{
fn is_disposed(&self) -> bool {
self.inner.is_disposed()
}
}
impl<Inner, Prev, T> Trigger for Subfield<Inner, Prev, T>
where
Inner: StoreField<Prev>,
{
fn trigger(&self) {
let trigger = self.get_trigger(self.path().collect());
trigger.trigger();
}
}
impl<Inner, Prev, T> Track for Subfield<Inner, Prev, T>
where
Inner: StoreField<Prev> + Send + Sync + Clone + 'static,
Prev: 'static,
T: 'static,
{
fn track(&self) {
let trigger = self.get_trigger(self.path().collect());
trigger.track();
}
}
impl<Inner, Prev, T> ReadUntracked for Subfield<Inner, Prev, T>
where
Inner: StoreField<Prev>,
{
type Value = <Self as StoreField<T>>::Reader;
fn try_read_untracked(&self) -> Option<Self::Value> {
self.reader()
}
}
impl<Inner, Prev, T> Writeable for Subfield<Inner, Prev, T>
where
T: 'static,
Inner: StoreField<Prev>,
{
type Value = T;
fn try_write(&self) -> Option<impl UntrackableGuard<Target = Self::Value>> {
self.writer()
}
fn try_write_untracked(
&self,
) -> Option<impl DerefMut<Target = Self::Value>> {
self.writer().map(|mut writer| {
writer.untrack();
writer
})
}
}

View file

@ -0,0 +1,14 @@
[package]
name = "reactive_stores_macro"
edition = "2021"
version = "0.1.0-alpha"
rust-version.workspace = true
[lib]
proc-macro = true
[dependencies]
proc-macro-error = "1"
proc-macro2 = "1"
quote = "1"
syn = "2"

View file

@ -0,0 +1,211 @@
use proc_macro2::{Span, TokenStream};
use proc_macro_error::{abort, abort_call_site, proc_macro_error};
use quote::{quote, ToTokens};
use syn::{
parse::{Parse, ParseStream, Parser},
punctuated::Punctuated,
token::Comma,
Data, Field, Fields, Generics, Ident, Meta, MetaList, Result, Visibility,
WhereClause, Type, Token, Index,
};
#[proc_macro_error]
#[proc_macro_derive(Store, attributes(store))]
pub fn derive_store(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
syn::parse_macro_input!(input as Model)
.into_token_stream()
.into()
}
struct Model {
pub vis: Visibility,
pub struct_name: Ident,
pub generics: Generics,
pub fields: Vec<Field>,
}
impl Parse for Model {
fn parse(input: ParseStream) -> Result<Self> {
let input = syn::DeriveInput::parse(input)?;
let syn::Data::Struct(s) = input.data else {
abort_call_site!("only structs can be used with `Store`");
};
let fields = match s.fields {
syn::Fields::Unit => {
abort!(s.semi_token, "unit structs are not supported");
}
syn::Fields::Named(fields) => {
fields.named.into_iter().collect::<Vec<_>>()
}
syn::Fields::Unnamed(fields) => fields
.unnamed
.into_iter()
.collect::<Vec<_>>(),
};
Ok(Self {
vis: input.vis,
struct_name: input.ident,
generics: input.generics,
fields,
})
}
}
#[derive(Clone)]
enum SubfieldMode {
Keyed(Ident, Type),
}
impl Parse for SubfieldMode {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let mode: Ident = input.parse()?;
if mode == "key" {
let _eq: Token!(=) = input.parse()?;
let ident: Ident = input.parse()?;
let _col: Token!(:) = input.parse()?;
let ty: Type = input.parse()?;
Ok(SubfieldMode::Keyed(ident, ty))
} else {
Err(input.error("expected `key = <ident>: <Type>`"))
}
}
}
#[allow(clippy::too_many_arguments)]
fn field_to_tokens(idx: usize, include_body: bool, modes: Option<&[SubfieldMode]>, library_path: &proc_macro2::TokenStream, orig_ident: Option<&Ident>, generics: &Generics, any_store_field: &Ident, struct_name: &Ident, ty: &Type) -> proc_macro2::TokenStream {
let ident = if orig_ident.is_none() {
let idx = Ident::new(&format!("field{idx}"), Span::call_site());
quote! { #idx }
} else {
quote! { #orig_ident }
};
let locator = if orig_ident.is_none() {
let idx = Index::from(idx);
quote! { #idx }
} else {
quote! { #ident }
};
if let Some(modes) = modes {
if modes.len() == 1 {
let mode = &modes[0];
// Can replace with a match if additional modes added
let SubfieldMode::Keyed(keyed_by, key_ty) = mode;
let signature = quote! {
fn #ident(self) -> #library_path::KeyedField<#any_store_field, #struct_name #generics, #ty, #key_ty>
};
return if include_body {
quote! {
#signature {
todo!()
}
}
} else {
quote! { #signature; }
}
} else {
abort!(orig_ident.map(|ident| ident.span()).unwrap_or_else(Span::call_site), "multiple modes not currently supported");
}
}
// default subfield
if include_body {
quote! {
fn #ident(self) -> #library_path::Subfield<#any_store_field, #struct_name #generics, #ty> {
#library_path::Subfield::new(
self,
#idx.into(),
|prev| &prev.#locator,
|prev| &mut prev.#locator,
)
}
}
} else {
quote! {
fn #ident(self) -> #library_path::Subfield<#any_store_field, #struct_name #generics, #ty>;
}
}
}
impl ToTokens for Model {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
let library_path = quote! { reactive_stores };
let Model {
vis,
struct_name,
generics,
fields,
} = &self;
let any_store_field = Ident::new("AnyStoreField", Span::call_site());
let trait_name = Ident::new(
&format!("{struct_name}StoreFields"),
struct_name.span(),
);
let generics_with_orig = {
let params = &generics.params;
quote! { <#any_store_field, #params> }
};
let where_with_orig = {
generics
.where_clause
.as_ref()
.map(|w| {
let WhereClause {
where_token,
predicates,
} = &w;
quote! {
#where_token
#any_store_field: #library_path::StoreField<#struct_name #generics>,
#predicates
}
})
.unwrap_or_else(|| quote! { where #any_store_field: #library_path::StoreField<#struct_name #generics> })
};
// define an extension trait that matches this struct
let all_field_data = fields.iter().enumerate().map(|(idx, field)| {
let Field { ident, ty, attrs, .. } = &field;
let modes = attrs.iter().find_map(|attr| {
attr.meta.path().is_ident("store").then(|| {
match &attr.meta {
Meta::List(list) => {
match Punctuated::<SubfieldMode, Comma>::parse_terminated.parse2(list.tokens.clone()) {
Ok(modes) => Some(modes.iter().cloned().collect::<Vec<_>>()),
Err(e) => abort!(list, e)
}
},
_ => None
}
})
}).flatten();
(
field_to_tokens(idx, false, modes.as_deref(), &library_path, ident.as_ref(), generics, &any_store_field, struct_name, ty),
field_to_tokens(idx, true, modes.as_deref(), &library_path, ident.as_ref(), generics, &any_store_field, struct_name, ty),
)
});
// implement that trait for all StoreFields
let (trait_fields, read_fields): (Vec<_>, Vec<_>) = all_field_data.unzip();
// read access
tokens.extend(quote! {
#vis trait #trait_name <AnyStoreField>
#where_with_orig
{
#(#trait_fields)*
}
impl #generics_with_orig #trait_name <AnyStoreField> for AnyStoreField
#where_with_orig
{
#(#read_fields)*
}
});
}
}