Stores: Externally tracked state and reactive collections (#4483)
* map signals mutably * boxed signals * remove mutable borrow type from boxed writable * improve map mut example * restore boxed_mut helper * make boxed readable/writable types copy * Move away from the ReadOnlySignal alias in examples * automatically convert Write * Fix some tests * ReadSignal and WriteSignal * move read only signal alias * make maps copy * store prototype * implement derive macro * Handle generics in the derive macro * Make Selector boxable * derive boxed conversion * Get rid of store type * Remove hardcoded type from derive macro * selector -> store * Refactor store implementation * more vec methods * todo mvc store example * Implement Storable for HashMap * fix write * restore sorting logic in todomvc * derive readable and writable * improve todomvc store example * Result store * Option store * collect dead subscribers * fix clippy and formatting * recognize some common foreign types automatically * fix foreign stores in props * slice and array selectors * clean up store example * writable is child owned * box selector * read and write to builtins * re-use the proc macro in the derive macro * Switch to a extension trait based approach * switch to extension traits in the derive macro * transposed method * derive enums * Expose Store::new * remove stores from the prelude * More consistent naming scheme for SelectorScope methods * document hashmap methods * document index extension * document option ext * document result ext * document slice extension * document the vec extension * make more methods borrow the store mutably * Move to impls to have higher priority over the builtin write extension traits * fix deadlock * finish stores documentation * Document the store macro * cleanup docs and organization * finish derive docs * fix typo * fix clippy * fix doc tests * fix derive macro with generics * fix clippy * test and fix empty structs * clean up todomvc store example * default to a boxed store writer to make it easier to type when providing to the context api * fix cargo doc * fix clippy in store example * relax generational box lifetime bounds * relax bounds * relax signal bounds * fix compilation * read store * relax bounds on default store impls * fix enum derive * Convert to readsignal and writesignal automatically * simplify type matching logic * add write store * unsized stores * shrink the path vec type * use more concrete types * remove opaque types * remove copy bounds * use lens as generic name everywhere * more consistent borrow semantics * use a concrete type for the signal metadata and mention references in the map docs * improve todomvc store docs * document store derive macro internals * refactor macro * improve selector node documentation * fix clippy * add btreemap * fix doc tests * more option methods * fix doc * fix clippy * add a bunch more result methods * add a bunch of bounds to the iterators * fix clippy * remove option from subscribers * add store macro to generate extension traits * fix store example * fix deadlock * fix docs * fix doc tests * fix clippy * add stores to public api * add store macro, and then update todomvc_store example --------- Co-authored-by: Jonathan Kelley <jkelleyrtp@gmail.com>
This commit is contained in:
1 parent
5591070e92
commit
58c41f8561
57 files changed
+4958
-436
No files matched your search
@@ -0,0 +1,366 @@
|
||||
use std::{borrow::Borrow, collections::BTreeMap, hash::Hash, iter::FusedIterator};
|
||||
|
||||
use crate::{store::Store, ReadStore};
|
||||
use dioxus_signals::{
|
||||
AnyStorage, BorrowError, BorrowMutError, ReadSignal, Readable, ReadableExt, UnsyncStorage,
|
||||
Writable, WriteLock, WriteSignal,
|
||||
};
|
||||
|
||||
impl<Lens: Readable<Target = BTreeMap<K, V>> + 'static, K: 'static, V: 'static>
|
||||
Store<BTreeMap<K, V>, Lens>
|
||||
{
|
||||
/// Get the length of the BTreeMap. This method will track the store shallowly and only cause
|
||||
/// re-runs when items are added or removed from the map, not when existing values are modified.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust, no_run
|
||||
/// use dioxus_stores::*;
|
||||
/// use dioxus::prelude::*;
|
||||
/// use std::collections::BTreeMap;
|
||||
/// let mut store = use_store(|| BTreeMap::new());
|
||||
/// assert_eq!(store.len(), 0);
|
||||
/// store.insert(0, "value".to_string());
|
||||
/// assert_eq!(store.len(), 1);
|
||||
/// ```
|
||||
pub fn len(&self) -> usize {
|
||||
self.selector().track_shallow();
|
||||
self.selector().peek().len()
|
||||
}
|
||||
|
||||
/// Check if the BTreeMap is empty. This method will track the store shallowly and only cause
|
||||
/// re-runs when items are added or removed from the map, not when existing values are modified.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust, no_run
|
||||
/// use dioxus_stores::*;
|
||||
/// use dioxus::prelude::*;
|
||||
/// use std::collections::BTreeMap;
|
||||
/// let mut store = use_store(|| BTreeMap::new());
|
||||
/// assert!(store.is_empty());
|
||||
/// store.insert(0, "value".to_string());
|
||||
/// assert!(!store.is_empty());
|
||||
/// ```
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.selector().track_shallow();
|
||||
self.selector().peek().is_empty()
|
||||
}
|
||||
|
||||
/// Iterate over the current entries in the BTreeMap, returning a tuple of the key and a store for the value. This method
|
||||
/// will track the store shallowly and only cause re-runs when items are added or removed from the map, not when existing
|
||||
/// values are modified.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust, no_run
|
||||
/// use dioxus_stores::*;
|
||||
/// use dioxus::prelude::*;
|
||||
/// use std::collections::BTreeMap;
|
||||
/// let mut store = use_store(|| BTreeMap::new());
|
||||
/// store.insert(0, "value1".to_string());
|
||||
/// store.insert(1, "value2".to_string());
|
||||
/// for (key, value_store) in store.iter() {
|
||||
/// println!("{}: {}", key, value_store.read());
|
||||
/// }
|
||||
/// ```
|
||||
pub fn iter(
|
||||
&self,
|
||||
) -> impl ExactSizeIterator<Item = (K, Store<V, GetWrite<K, Lens>>)>
|
||||
+ DoubleEndedIterator
|
||||
+ FusedIterator
|
||||
+ '_
|
||||
where
|
||||
K: Hash + Ord + Clone,
|
||||
Lens: Clone,
|
||||
{
|
||||
self.selector().track_shallow();
|
||||
let keys: Vec<_> = self.selector().peek_unchecked().keys().cloned().collect();
|
||||
keys.into_iter().map(move |key| {
|
||||
let value = self.clone().get(key.clone()).unwrap();
|
||||
(key, value)
|
||||
})
|
||||
}
|
||||
|
||||
/// Get an iterator over the values in the BTreeMap. This method will track the store shallowly and only cause
|
||||
/// re-runs when items are added or removed from the map, not when existing values are modified.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust, no_run
|
||||
/// use dioxus_stores::*;
|
||||
/// use dioxus::prelude::*;
|
||||
/// use std::collections::BTreeMap;
|
||||
/// let mut store = use_store(|| BTreeMap::new());
|
||||
/// store.insert(0, "value1".to_string());
|
||||
/// store.insert(1, "value2".to_string());
|
||||
/// for value_store in store.values() {
|
||||
/// println!("{}", value_store.read());
|
||||
/// }
|
||||
/// ```
|
||||
pub fn values(
|
||||
&self,
|
||||
) -> impl ExactSizeIterator<Item = Store<V, GetWrite<K, Lens>>>
|
||||
+ DoubleEndedIterator
|
||||
+ FusedIterator
|
||||
+ '_
|
||||
where
|
||||
K: Hash + Ord + Clone,
|
||||
Lens: Clone,
|
||||
{
|
||||
self.selector().track_shallow();
|
||||
let keys = self.selector().peek().keys().cloned().collect::<Vec<_>>();
|
||||
keys.into_iter()
|
||||
.map(move |key| self.clone().get(key).unwrap())
|
||||
}
|
||||
|
||||
/// Insert a new key-value pair into the BTreeMap. This method will mark the store as shallowly dirty, causing
|
||||
/// re-runs of any reactive scopes that depend on the shape of the map.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust, no_run
|
||||
/// use dioxus_stores::*;
|
||||
/// use dioxus::prelude::*;
|
||||
/// use std::collections::BTreeMap;
|
||||
/// let mut store = use_store(|| BTreeMap::new());
|
||||
/// assert!(store.get(0).is_none());
|
||||
/// store.insert(0, "value".to_string());
|
||||
/// assert_eq!(store.get(0).unwrap().cloned(), "value".to_string());
|
||||
/// ```
|
||||
pub fn insert(&mut self, key: K, value: V)
|
||||
where
|
||||
K: Ord,
|
||||
Lens: Writable,
|
||||
{
|
||||
self.selector().mark_dirty_shallow();
|
||||
self.selector().write_untracked().insert(key, value);
|
||||
}
|
||||
|
||||
/// Remove a key-value pair from the BTreeMap. This method will mark the store as shallowly dirty, causing
|
||||
/// re-runs of any reactive scopes that depend on the shape of the map or the value of the removed key.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust, no_run
|
||||
/// use dioxus_stores::*;
|
||||
/// use dioxus::prelude::*;
|
||||
/// use std::collections::BTreeMap;
|
||||
/// let mut store = use_store(|| BTreeMap::new());
|
||||
/// store.insert(0, "value".to_string());
|
||||
/// assert_eq!(store.get(0).unwrap().cloned(), "value".to_string());
|
||||
/// let removed_value = store.remove(&0);
|
||||
/// assert_eq!(removed_value, Some("value".to_string()));
|
||||
/// assert!(store.get(0).is_none());
|
||||
/// ```
|
||||
pub fn remove<Q>(&mut self, key: &Q) -> Option<V>
|
||||
where
|
||||
Q: ?Sized + Ord + 'static,
|
||||
K: Borrow<Q> + Ord,
|
||||
Lens: Writable,
|
||||
{
|
||||
self.selector().mark_dirty_shallow();
|
||||
self.selector().write_untracked().remove(key)
|
||||
}
|
||||
|
||||
/// Clear the BTreeMap, removing all key-value pairs. This method will mark the store as shallowly dirty,
|
||||
/// causing re-runs of any reactive scopes that depend on the shape of the map.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust, no_run
|
||||
/// use dioxus_stores::*;
|
||||
/// use dioxus::prelude::*;
|
||||
/// use std::collections::BTreeMap;
|
||||
/// let mut store = use_store(|| BTreeMap::new());
|
||||
/// store.insert(1, "value1".to_string());
|
||||
/// store.insert(2, "value2".to_string());
|
||||
/// assert_eq!(store.len(), 2);
|
||||
/// store.clear();
|
||||
/// assert!(store.is_empty());
|
||||
/// ```
|
||||
pub fn clear(&mut self)
|
||||
where
|
||||
Lens: Writable,
|
||||
{
|
||||
self.selector().mark_dirty_shallow();
|
||||
self.selector().write_untracked().clear();
|
||||
}
|
||||
|
||||
/// Retain only the key-value pairs that satisfy the given predicate. This method will mark the store as shallowly dirty,
|
||||
/// causing re-runs of any reactive scopes that depend on the shape of the map or the values retained.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust, no_run
|
||||
/// use dioxus_stores::*;
|
||||
/// use dioxus::prelude::*;
|
||||
/// use std::collections::BTreeMap;
|
||||
/// let mut store = use_store(|| BTreeMap::new());
|
||||
/// store.insert(1, "value1".to_string());
|
||||
/// store.insert(2, "value2".to_string());
|
||||
/// store.retain(|key, value| *key == 1);
|
||||
/// assert_eq!(store.len(), 1);
|
||||
/// assert!(store.get(1).is_some());
|
||||
/// assert!(store.get(2).is_none());
|
||||
/// ```
|
||||
pub fn retain(&mut self, mut f: impl FnMut(&K, &V) -> bool)
|
||||
where
|
||||
Lens: Writable,
|
||||
K: Ord,
|
||||
{
|
||||
self.selector().mark_dirty_shallow();
|
||||
self.selector().write_untracked().retain(|k, v| f(k, v));
|
||||
}
|
||||
|
||||
/// Check if the BTreeMap contains a key. This method will track the store shallowly and only cause
|
||||
/// re-runs when items are added or removed from the map, not when existing values are modified.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust, no_run
|
||||
/// use dioxus_stores::*;
|
||||
/// use dioxus::prelude::*;
|
||||
/// use std::collections::BTreeMap;
|
||||
/// let mut store = use_store(|| BTreeMap::new());
|
||||
/// assert!(!store.contains_key(&0));
|
||||
/// store.insert(0, "value".to_string());
|
||||
/// assert!(store.contains_key(&0));
|
||||
/// ```
|
||||
pub fn contains_key<Q>(&self, key: &Q) -> bool
|
||||
where
|
||||
Q: ?Sized + Ord + 'static,
|
||||
K: Borrow<Q> + Ord,
|
||||
{
|
||||
self.selector().track_shallow();
|
||||
self.selector().peek().contains_key(key)
|
||||
}
|
||||
|
||||
/// Get a store for the value associated with the given key. This method creates a new store scope
|
||||
/// that tracks just changes to the value associated with the key.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust, no_run
|
||||
/// use dioxus_stores::*;
|
||||
/// use dioxus::prelude::*;
|
||||
/// use std::collections::BTreeMap;
|
||||
/// let mut store = use_store(|| BTreeMap::new());
|
||||
/// assert!(store.get(0).is_none());
|
||||
/// store.insert(0, "value".to_string());
|
||||
/// assert_eq!(store.get(0).unwrap().cloned(), "value".to_string());
|
||||
/// ```
|
||||
pub fn get<Q>(self, key: Q) -> Option<Store<V, GetWrite<Q, Lens>>>
|
||||
where
|
||||
Q: Hash + Ord + 'static,
|
||||
K: Borrow<Q> + Ord,
|
||||
{
|
||||
self.contains_key(&key).then(|| {
|
||||
self.into_selector()
|
||||
.hash_child_unmapped(key.borrow())
|
||||
.map_writer(move |writer| GetWrite {
|
||||
index: key,
|
||||
write: writer,
|
||||
})
|
||||
.into()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// A specific index in a `Readable` / `Writable` BTreeMap
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct GetWrite<Index, Write> {
|
||||
index: Index,
|
||||
write: Write,
|
||||
}
|
||||
|
||||
impl<Index, Write, K, V> Readable for GetWrite<Index, Write>
|
||||
where
|
||||
Write: Readable<Target = BTreeMap<K, V>>,
|
||||
Index: Ord + 'static,
|
||||
K: Borrow<Index> + Ord + 'static,
|
||||
{
|
||||
type Target = V;
|
||||
|
||||
type Storage = Write::Storage;
|
||||
|
||||
fn try_read_unchecked(&self) -> Result<dioxus_signals::ReadableRef<'static, Self>, BorrowError>
|
||||
where
|
||||
Self::Target: 'static,
|
||||
{
|
||||
self.write.try_read_unchecked().map(|value| {
|
||||
Self::Storage::map(value, |value: &Write::Target| {
|
||||
value
|
||||
.get(&self.index)
|
||||
.expect("Tried to access a key that does not exist")
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn try_peek_unchecked(&self) -> Result<dioxus_signals::ReadableRef<'static, Self>, BorrowError>
|
||||
where
|
||||
Self::Target: 'static,
|
||||
{
|
||||
self.write.try_peek_unchecked().map(|value| {
|
||||
Self::Storage::map(value, |value: &Write::Target| {
|
||||
value
|
||||
.get(&self.index)
|
||||
.expect("Tried to access a key that does not exist")
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn subscribers(&self) -> dioxus_core::Subscribers
|
||||
where
|
||||
Self::Target: 'static,
|
||||
{
|
||||
self.write.subscribers()
|
||||
}
|
||||
}
|
||||
|
||||
impl<Index, Write, K, V> Writable for GetWrite<Index, Write>
|
||||
where
|
||||
Write: Writable<Target = BTreeMap<K, V>>,
|
||||
Index: Ord + 'static,
|
||||
K: Borrow<Index> + Ord + 'static,
|
||||
{
|
||||
type WriteMetadata = Write::WriteMetadata;
|
||||
|
||||
fn try_write_unchecked(
|
||||
&self,
|
||||
) -> Result<dioxus_signals::WritableRef<'static, Self>, BorrowMutError>
|
||||
where
|
||||
Self::Target: 'static,
|
||||
{
|
||||
self.write.try_write_unchecked().map(|value| {
|
||||
WriteLock::map(value, |value: &mut Write::Target| {
|
||||
value
|
||||
.get_mut(&self.index)
|
||||
.expect("Tried to access a key that does not exist")
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<Index, Write, K, V> ::std::convert::From<Store<V, GetWrite<Index, Write>>>
|
||||
for Store<V, WriteSignal<V>>
|
||||
where
|
||||
Write::WriteMetadata: 'static,
|
||||
Write: Writable<Target = BTreeMap<K, V>, Storage = UnsyncStorage> + 'static,
|
||||
Index: Ord + 'static,
|
||||
K: Borrow<Index> + Ord + 'static,
|
||||
V: 'static,
|
||||
{
|
||||
fn from(value: Store<V, GetWrite<Index, Write>>) -> Self {
|
||||
value
|
||||
.into_selector()
|
||||
.map_writer(|writer| WriteSignal::new(writer))
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl<Index, Write, K, V> ::std::convert::From<Store<V, GetWrite<Index, Write>>> for ReadStore<V>
|
||||
where
|
||||
Write: Readable<Target = BTreeMap<K, V>, Storage = UnsyncStorage> + 'static,
|
||||
Index: Ord + 'static,
|
||||
K: Borrow<Index> + Ord + 'static,
|
||||
V: 'static,
|
||||
{
|
||||
fn from(value: Store<V, GetWrite<Index, Write>>) -> Self {
|
||||
value
|
||||
.into_selector()
|
||||
.map_writer(|writer| ReadSignal::new(writer))
|
||||
.into()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
use std::ops::DerefMut;
|
||||
|
||||
use crate::{store::Store, MappedStore};
|
||||
use dioxus_signals::Readable;
|
||||
|
||||
impl<Lens, T> Store<T, Lens>
|
||||
where
|
||||
Lens: Readable<Target = T> + 'static,
|
||||
T: DerefMut + 'static,
|
||||
{
|
||||
/// Returns a store that dereferences the original value. The dereferenced store shares the same
|
||||
/// subscriptions and tracking as the original store, but allows you to access the methods of the underlying type.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust, no_run
|
||||
/// use dioxus_stores::*;
|
||||
/// let store = use_store(|| Box::new(vec![1, 2, 3]));
|
||||
/// let deref_store = store.deref();
|
||||
/// // The dereferenced store can access the store methods of the underlying type.
|
||||
/// assert_eq!(deref_store.len(), 3);
|
||||
/// ```
|
||||
pub fn deref(self) -> MappedStore<T::Target, Lens> {
|
||||
let map: fn(&T) -> &T::Target = |value| value.deref();
|
||||
let map_mut: fn(&mut T) -> &mut T::Target = |value| value.deref_mut();
|
||||
self.into_selector().map(map, map_mut).into()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,380 @@
|
||||
use std::{
|
||||
borrow::Borrow,
|
||||
collections::HashMap,
|
||||
hash::{BuildHasher, Hash},
|
||||
iter::FusedIterator,
|
||||
};
|
||||
|
||||
use crate::{store::Store, ReadStore};
|
||||
use dioxus_signals::{
|
||||
AnyStorage, BorrowError, BorrowMutError, ReadSignal, Readable, ReadableExt, UnsyncStorage,
|
||||
Writable, WriteLock, WriteSignal,
|
||||
};
|
||||
|
||||
impl<Lens: Readable<Target = HashMap<K, V, St>> + 'static, K: 'static, V: 'static, St: 'static>
|
||||
Store<HashMap<K, V, St>, Lens>
|
||||
{
|
||||
/// Get the length of the HashMap. This method will track the store shallowly and only cause
|
||||
/// re-runs when items are added or removed from the map, not when existing values are modified.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust, no_run
|
||||
/// use dioxus_stores::*;
|
||||
/// use dioxus::prelude::*;
|
||||
/// use std::collections::HashMap;
|
||||
/// let mut store = use_store(|| HashMap::new());
|
||||
/// assert_eq!(store.len(), 0);
|
||||
/// store.insert(0, "value".to_string());
|
||||
/// assert_eq!(store.len(), 1);
|
||||
/// ```
|
||||
pub fn len(&self) -> usize {
|
||||
self.selector().track_shallow();
|
||||
self.selector().peek().len()
|
||||
}
|
||||
|
||||
/// Check if the HashMap is empty. This method will track the store shallowly and only cause
|
||||
/// re-runs when items are added or removed from the map, not when existing values are modified.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust, no_run
|
||||
/// use dioxus_stores::*;
|
||||
/// use dioxus::prelude::*;
|
||||
/// use std::collections::HashMap;
|
||||
/// let mut store = use_store(|| HashMap::new());
|
||||
/// assert!(store.is_empty());
|
||||
/// store.insert(0, "value".to_string());
|
||||
/// assert!(!store.is_empty());
|
||||
/// ```
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.selector().track_shallow();
|
||||
self.selector().peek().is_empty()
|
||||
}
|
||||
|
||||
/// Iterate over the current entries in the HashMap, returning a tuple of the key and a store for the value. This method
|
||||
/// will track the store shallowly and only cause re-runs when items are added or removed from the map, not when existing
|
||||
/// values are modified.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust, no_run
|
||||
/// use dioxus_stores::*;
|
||||
/// use dioxus::prelude::*;
|
||||
/// use std::collections::HashMap;
|
||||
/// let mut store = use_store(|| HashMap::new());
|
||||
/// store.insert(0, "value1".to_string());
|
||||
/// store.insert(1, "value2".to_string());
|
||||
/// for (key, value_store) in store.iter() {
|
||||
/// println!("{}: {}", key, value_store.read());
|
||||
/// }
|
||||
/// ```
|
||||
pub fn iter(
|
||||
&self,
|
||||
) -> impl ExactSizeIterator<Item = (K, Store<V, GetWrite<K, Lens>>)>
|
||||
+ DoubleEndedIterator
|
||||
+ FusedIterator
|
||||
+ '_
|
||||
where
|
||||
K: Eq + Hash + Clone,
|
||||
St: BuildHasher,
|
||||
Lens: Clone,
|
||||
{
|
||||
self.selector().track_shallow();
|
||||
let keys: Vec<_> = self.selector().peek_unchecked().keys().cloned().collect();
|
||||
keys.into_iter().map(move |key| {
|
||||
let value = self.clone().get(key.clone()).unwrap();
|
||||
(key, value)
|
||||
})
|
||||
}
|
||||
|
||||
/// Get an iterator over the values in the HashMap. This method will track the store shallowly and only cause
|
||||
/// re-runs when items are added or removed from the map, not when existing values are modified.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust, no_run
|
||||
/// use dioxus_stores::*;
|
||||
/// use dioxus::prelude::*;
|
||||
/// use std::collections::HashMap;
|
||||
/// let mut store = use_store(|| HashMap::new());
|
||||
/// store.insert(0, "value1".to_string());
|
||||
/// store.insert(1, "value2".to_string());
|
||||
/// for value_store in store.values() {
|
||||
/// println!("{}", value_store.read());
|
||||
/// }
|
||||
/// ```
|
||||
pub fn values(
|
||||
&self,
|
||||
) -> impl ExactSizeIterator<Item = Store<V, GetWrite<K, Lens>>>
|
||||
+ DoubleEndedIterator
|
||||
+ FusedIterator
|
||||
+ '_
|
||||
where
|
||||
K: Eq + Hash + Clone,
|
||||
St: BuildHasher,
|
||||
Lens: Clone,
|
||||
{
|
||||
self.selector().track_shallow();
|
||||
let keys = self.selector().peek().keys().cloned().collect::<Vec<_>>();
|
||||
keys.into_iter()
|
||||
.map(move |key| self.clone().get(key).unwrap())
|
||||
}
|
||||
|
||||
/// Insert a new key-value pair into the HashMap. This method will mark the store as shallowly dirty, causing
|
||||
/// re-runs of any reactive scopes that depend on the shape of the map.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust, no_run
|
||||
/// use dioxus_stores::*;
|
||||
/// use dioxus::prelude::*;
|
||||
/// use std::collections::HashMap;
|
||||
/// let mut store = use_store(|| HashMap::new());
|
||||
/// assert!(store.get(0).is_none());
|
||||
/// store.insert(0, "value".to_string());
|
||||
/// assert_eq!(store.get(0).unwrap().cloned(), "value".to_string());
|
||||
/// ```
|
||||
pub fn insert(&mut self, key: K, value: V)
|
||||
where
|
||||
K: Eq + Hash,
|
||||
St: BuildHasher,
|
||||
Lens: Writable,
|
||||
{
|
||||
self.selector().mark_dirty_shallow();
|
||||
self.selector().write_untracked().insert(key, value);
|
||||
}
|
||||
|
||||
/// Remove a key-value pair from the HashMap. This method will mark the store as shallowly dirty, causing
|
||||
/// re-runs of any reactive scopes that depend on the shape of the map or the value of the removed key.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust, no_run
|
||||
/// use dioxus_stores::*;
|
||||
/// use dioxus::prelude::*;
|
||||
/// use std::collections::HashMap;
|
||||
/// let mut store = use_store(|| HashMap::new());
|
||||
/// store.insert(0, "value".to_string());
|
||||
/// assert_eq!(store.get(0).unwrap().cloned(), "value".to_string());
|
||||
/// let removed_value = store.remove(&0);
|
||||
/// assert_eq!(removed_value, Some("value".to_string()));
|
||||
/// assert!(store.get(0).is_none());
|
||||
/// ```
|
||||
pub fn remove<Q>(&mut self, key: &Q) -> Option<V>
|
||||
where
|
||||
Q: ?Sized + Hash + Eq + 'static,
|
||||
K: Borrow<Q> + Eq + Hash,
|
||||
St: BuildHasher,
|
||||
Lens: Writable,
|
||||
{
|
||||
self.selector().mark_dirty_shallow();
|
||||
self.selector().write_untracked().remove(key)
|
||||
}
|
||||
|
||||
/// Clear the HashMap, removing all key-value pairs. This method will mark the store as shallowly dirty,
|
||||
/// causing re-runs of any reactive scopes that depend on the shape of the map.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust, no_run
|
||||
/// use dioxus_stores::*;
|
||||
/// use dioxus::prelude::*;
|
||||
/// use std::collections::HashMap;
|
||||
/// let mut store = use_store(|| HashMap::new());
|
||||
/// store.insert(1, "value1".to_string());
|
||||
/// store.insert(2, "value2".to_string());
|
||||
/// assert_eq!(store.len(), 2);
|
||||
/// store.clear();
|
||||
/// assert!(store.is_empty());
|
||||
/// ```
|
||||
pub fn clear(&mut self)
|
||||
where
|
||||
Lens: Writable,
|
||||
{
|
||||
self.selector().mark_dirty_shallow();
|
||||
self.selector().write_untracked().clear();
|
||||
}
|
||||
|
||||
/// Retain only the key-value pairs that satisfy the given predicate. This method will mark the store as shallowly dirty,
|
||||
/// causing re-runs of any reactive scopes that depend on the shape of the map or the values retained.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust, no_run
|
||||
/// use dioxus_stores::*;
|
||||
/// use dioxus::prelude::*;
|
||||
/// use std::collections::HashMap;
|
||||
/// let mut store = use_store(|| HashMap::new());
|
||||
/// store.insert(1, "value1".to_string());
|
||||
/// store.insert(2, "value2".to_string());
|
||||
/// store.retain(|key, value| *key == 1);
|
||||
/// assert_eq!(store.len(), 1);
|
||||
/// assert!(store.get(1).is_some());
|
||||
/// assert!(store.get(2).is_none());
|
||||
/// ```
|
||||
pub fn retain(&mut self, mut f: impl FnMut(&K, &V) -> bool)
|
||||
where
|
||||
Lens: Writable,
|
||||
{
|
||||
self.selector().mark_dirty_shallow();
|
||||
self.selector().write_untracked().retain(|k, v| f(k, v));
|
||||
}
|
||||
|
||||
/// Check if the HashMap contains a key. This method will track the store shallowly and only cause
|
||||
/// re-runs when items are added or removed from the map, not when existing values are modified.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust, no_run
|
||||
/// use dioxus_stores::*;
|
||||
/// use dioxus::prelude::*;
|
||||
/// use std::collections::HashMap;
|
||||
/// let mut store = use_store(|| HashMap::new());
|
||||
/// assert!(!store.contains_key(&0));
|
||||
/// store.insert(0, "value".to_string());
|
||||
/// assert!(store.contains_key(&0));
|
||||
/// ```
|
||||
pub fn contains_key<Q>(&self, key: &Q) -> bool
|
||||
where
|
||||
Q: ?Sized + Hash + Eq + 'static,
|
||||
K: Borrow<Q> + Eq + Hash,
|
||||
St: BuildHasher,
|
||||
{
|
||||
self.selector().track_shallow();
|
||||
self.selector().peek().contains_key(key)
|
||||
}
|
||||
|
||||
/// Get a store for the value associated with the given key. This method creates a new store scope
|
||||
/// that tracks just changes to the value associated with the key.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust, no_run
|
||||
/// use dioxus_stores::*;
|
||||
/// use dioxus::prelude::*;
|
||||
/// use std::collections::HashMap;
|
||||
/// let mut store = use_store(|| HashMap::new());
|
||||
/// assert!(store.get(0).is_none());
|
||||
/// store.insert(0, "value".to_string());
|
||||
/// assert_eq!(store.get(0).unwrap().cloned(), "value".to_string());
|
||||
/// ```
|
||||
pub fn get<Q>(self, key: Q) -> Option<Store<V, GetWrite<Q, Lens>>>
|
||||
where
|
||||
Q: Hash + Eq + 'static,
|
||||
K: Borrow<Q> + Eq + Hash,
|
||||
St: BuildHasher,
|
||||
{
|
||||
self.contains_key(&key).then(|| {
|
||||
self.into_selector()
|
||||
.hash_child_unmapped(key.borrow())
|
||||
.map_writer(move |writer| GetWrite {
|
||||
index: key,
|
||||
write: writer,
|
||||
})
|
||||
.into()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// A specific index in a `Readable` / `Writable` hashmap
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct GetWrite<Index, Write> {
|
||||
index: Index,
|
||||
write: Write,
|
||||
}
|
||||
|
||||
impl<Index, Write, K, V, St> Readable for GetWrite<Index, Write>
|
||||
where
|
||||
Write: Readable<Target = HashMap<K, V, St>>,
|
||||
Index: Hash + Eq + 'static,
|
||||
K: Borrow<Index> + Eq + Hash + 'static,
|
||||
St: BuildHasher + 'static,
|
||||
{
|
||||
type Target = V;
|
||||
|
||||
type Storage = Write::Storage;
|
||||
|
||||
fn try_read_unchecked(&self) -> Result<dioxus_signals::ReadableRef<'static, Self>, BorrowError>
|
||||
where
|
||||
Self::Target: 'static,
|
||||
{
|
||||
self.write.try_read_unchecked().map(|value| {
|
||||
Self::Storage::map(value, |value: &Write::Target| {
|
||||
value
|
||||
.get(&self.index)
|
||||
.expect("Tried to access a key that does not exist")
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn try_peek_unchecked(&self) -> Result<dioxus_signals::ReadableRef<'static, Self>, BorrowError>
|
||||
where
|
||||
Self::Target: 'static,
|
||||
{
|
||||
self.write.try_peek_unchecked().map(|value| {
|
||||
Self::Storage::map(value, |value: &Write::Target| {
|
||||
value
|
||||
.get(&self.index)
|
||||
.expect("Tried to access a key that does not exist")
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn subscribers(&self) -> dioxus_core::Subscribers
|
||||
where
|
||||
Self::Target: 'static,
|
||||
{
|
||||
self.write.subscribers()
|
||||
}
|
||||
}
|
||||
|
||||
impl<Index, Write, K, V, St> Writable for GetWrite<Index, Write>
|
||||
where
|
||||
Write: Writable<Target = HashMap<K, V, St>>,
|
||||
Index: Hash + Eq + 'static,
|
||||
K: Borrow<Index> + Eq + Hash + 'static,
|
||||
St: BuildHasher + 'static,
|
||||
{
|
||||
type WriteMetadata = Write::WriteMetadata;
|
||||
|
||||
fn try_write_unchecked(
|
||||
&self,
|
||||
) -> Result<dioxus_signals::WritableRef<'static, Self>, BorrowMutError>
|
||||
where
|
||||
Self::Target: 'static,
|
||||
{
|
||||
self.write.try_write_unchecked().map(|value| {
|
||||
WriteLock::map(value, |value: &mut Write::Target| {
|
||||
value
|
||||
.get_mut(&self.index)
|
||||
.expect("Tried to access a key that does not exist")
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<Index, Write, K, V, St> ::std::convert::From<Store<V, GetWrite<Index, Write>>>
|
||||
for Store<V, WriteSignal<V>>
|
||||
where
|
||||
Write::WriteMetadata: 'static,
|
||||
Write: Writable<Target = HashMap<K, V, St>, Storage = UnsyncStorage> + 'static,
|
||||
Index: Hash + Eq + 'static,
|
||||
K: Borrow<Index> + Eq + Hash + 'static,
|
||||
St: BuildHasher + 'static,
|
||||
V: 'static,
|
||||
{
|
||||
fn from(value: Store<V, GetWrite<Index, Write>>) -> Self {
|
||||
value
|
||||
.into_selector()
|
||||
.map_writer(|writer| WriteSignal::new(writer))
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl<Index, Write, K, V, St> ::std::convert::From<Store<V, GetWrite<Index, Write>>> for ReadStore<V>
|
||||
where
|
||||
Write: Readable<Target = HashMap<K, V, St>, Storage = UnsyncStorage> + 'static,
|
||||
Index: Hash + Eq + 'static,
|
||||
K: Borrow<Index> + Eq + Hash + 'static,
|
||||
St: BuildHasher + 'static,
|
||||
V: 'static,
|
||||
{
|
||||
fn from(value: Store<V, GetWrite<Index, Write>>) -> Self {
|
||||
value
|
||||
.into_selector()
|
||||
.map_writer(|writer| ReadSignal::new(writer))
|
||||
.into()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
use std::{
|
||||
hash::Hash,
|
||||
ops::{self, Index, IndexMut},
|
||||
};
|
||||
|
||||
use crate::{store::Store, ReadStore};
|
||||
use dioxus_signals::{
|
||||
AnyStorage, BorrowError, BorrowMutError, ReadSignal, Readable, UnsyncStorage, Writable,
|
||||
WriteLock, WriteSignal,
|
||||
};
|
||||
|
||||
impl<Lens, T> Store<T, Lens> {
|
||||
/// Index into the store, returning a store that allows access to the item at the given index. The
|
||||
/// new store will only update when the item at the index changes.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust, no_run
|
||||
/// use dioxus_stores::*;
|
||||
/// let store = use_store(|| vec![1, 2, 3]);
|
||||
/// let indexed_store = store.index(1);
|
||||
/// // The indexed store can access the store methods of the indexed store.
|
||||
/// assert_eq!(indexed_store(), 2);
|
||||
/// ```
|
||||
pub fn index<Idx>(self, index: Idx) -> Store<T::Output, IndexWrite<Idx, Lens>>
|
||||
where
|
||||
T: IndexMut<Idx> + 'static,
|
||||
Idx: Hash + 'static,
|
||||
Lens: Readable<Target = T> + 'static,
|
||||
{
|
||||
self.into_selector()
|
||||
.hash_child_unmapped(&index)
|
||||
.map_writer(move |write| IndexWrite { index, write })
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
/// A specific index in a `Readable` / `Writable` type
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct IndexWrite<Index, Write> {
|
||||
index: Index,
|
||||
write: Write,
|
||||
}
|
||||
|
||||
impl<Index, Write> Readable for IndexWrite<Index, Write>
|
||||
where
|
||||
Write: Readable,
|
||||
Write::Target: ops::Index<Index> + 'static,
|
||||
Index: Clone,
|
||||
{
|
||||
type Target = <Write::Target as ops::Index<Index>>::Output;
|
||||
|
||||
type Storage = Write::Storage;
|
||||
|
||||
fn try_read_unchecked(&self) -> Result<dioxus_signals::ReadableRef<'static, Self>, BorrowError>
|
||||
where
|
||||
Self::Target: 'static,
|
||||
{
|
||||
self.write.try_read_unchecked().map(|value| {
|
||||
Self::Storage::map(value, |value: &Write::Target| {
|
||||
value.index(self.index.clone())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn try_peek_unchecked(&self) -> Result<dioxus_signals::ReadableRef<'static, Self>, BorrowError>
|
||||
where
|
||||
Self::Target: 'static,
|
||||
{
|
||||
self.write.try_peek_unchecked().map(|value| {
|
||||
Self::Storage::map(value, |value: &Write::Target| {
|
||||
value.index(self.index.clone())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn subscribers(&self) -> dioxus_core::Subscribers
|
||||
where
|
||||
Self::Target: 'static,
|
||||
{
|
||||
self.write.subscribers()
|
||||
}
|
||||
}
|
||||
|
||||
impl<Index, Write> Writable for IndexWrite<Index, Write>
|
||||
where
|
||||
Write: Writable,
|
||||
Write::Target: ops::IndexMut<Index> + 'static,
|
||||
Index: Clone,
|
||||
{
|
||||
type WriteMetadata = Write::WriteMetadata;
|
||||
|
||||
fn try_write_unchecked(
|
||||
&self,
|
||||
) -> Result<dioxus_signals::WritableRef<'static, Self>, BorrowMutError>
|
||||
where
|
||||
Self::Target: 'static,
|
||||
{
|
||||
self.write.try_write_unchecked().map(|value| {
|
||||
WriteLock::map(value, |value: &mut Write::Target| {
|
||||
value.index_mut(self.index.clone())
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<Idx, T, Write> ::std::convert::From<Store<T, IndexWrite<Idx, Write>>>
|
||||
for Store<T, WriteSignal<T>>
|
||||
where
|
||||
Write: Writable<Storage = UnsyncStorage> + 'static,
|
||||
Write::WriteMetadata: 'static,
|
||||
Write::Target: ops::IndexMut<Idx, Output = T> + 'static,
|
||||
Idx: Clone + 'static,
|
||||
T: 'static,
|
||||
{
|
||||
fn from(value: Store<T, IndexWrite<Idx, Write>>) -> Self {
|
||||
value
|
||||
.into_selector()
|
||||
.map_writer(|writer| WriteSignal::new(writer))
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl<Idx, T, Write> ::std::convert::From<Store<T, IndexWrite<Idx, Write>>> for ReadStore<T>
|
||||
where
|
||||
Write: Readable<Storage = UnsyncStorage> + 'static,
|
||||
Write::Target: ops::Index<Idx, Output = T> + 'static,
|
||||
Idx: Clone + 'static,
|
||||
T: 'static,
|
||||
{
|
||||
fn from(value: Store<T, IndexWrite<Idx, Write>>) -> Self {
|
||||
value
|
||||
.into_selector()
|
||||
.map_writer(|writer| ReadSignal::new(writer))
|
||||
.into()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
mod btreemap;
|
||||
mod deref;
|
||||
mod hashmap;
|
||||
mod index;
|
||||
mod option;
|
||||
mod result;
|
||||
mod slice;
|
||||
mod vec;
|
||||
@@ -0,0 +1,230 @@
|
||||
use std::ops::DerefMut;
|
||||
|
||||
use crate::{store::Store, MappedStore};
|
||||
use dioxus_signals::{Readable, ReadableExt};
|
||||
|
||||
impl<Lens: Readable<Target = Option<T>> + 'static, T: 'static> Store<Option<T>, Lens> {
|
||||
/// Checks if the `Option` is `Some`. This will only track the shallow state of the `Option`. It will
|
||||
/// only cause a re-run if the `Option` could change from `None` to `Some` or vice versa.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust, no_run
|
||||
/// use dioxus_stores::*;
|
||||
/// let store = use_store(|| Some(42));
|
||||
/// assert!(store.is_some());
|
||||
/// ```
|
||||
pub fn is_some(&self) -> bool {
|
||||
self.selector().track_shallow();
|
||||
self.selector().peek().is_some()
|
||||
}
|
||||
|
||||
/// Returns true if the option is Some and the closure returns true. This will always track the shallow
|
||||
/// state of the and will track the inner state of the enum if the enum is Some.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust, no_run
|
||||
/// use dioxus_stores::*;
|
||||
/// let store = use_store(|| Some(42));
|
||||
/// assert!(store.is_some_and(|v| *v == 42));
|
||||
/// ```
|
||||
pub fn is_some_and(&self, f: impl FnOnce(&T) -> bool) -> bool {
|
||||
self.selector().track_shallow();
|
||||
let value = self.selector().peek();
|
||||
if let Some(v) = &*value {
|
||||
self.selector().track();
|
||||
f(v)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if the `Option` is `None`. This will only track the shallow state of the `Option`. It will
|
||||
/// only cause a re-run if the `Option` could change from `Some` to `None` or vice versa.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust, no_run
|
||||
/// use dioxus_stores::*;
|
||||
/// let store = use_store(|| None::<i32>);
|
||||
/// assert!(store.is_none());
|
||||
/// ```
|
||||
pub fn is_none(&self) -> bool {
|
||||
self.selector().track_shallow();
|
||||
self.selector().peek().is_none()
|
||||
}
|
||||
|
||||
/// Returns true if the option is None or the closure returns true. This will always track the shallow
|
||||
/// state of the and will track the inner state of the enum if the enum is Some.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust, no_run
|
||||
/// use dioxus_stores::*;
|
||||
/// let store = use_store(|| Some(42));
|
||||
/// assert!(store.is_none_or(|v| *v == 42));
|
||||
/// ```
|
||||
pub fn is_none_or(&self, f: impl FnOnce(&T) -> bool) -> bool {
|
||||
self.selector().track_shallow();
|
||||
let value = self.selector().peek();
|
||||
if let Some(v) = &*value {
|
||||
self.selector().track();
|
||||
f(v)
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/// Transpose the `Store<Option<T>>` into a `Option<Store<T>>`. This will only track the shallow state of the `Option`. It will
|
||||
/// only cause a re-run if the `Option` could change from `None` to `Some` or vice versa.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust, no_run
|
||||
/// use dioxus_stores::*;
|
||||
/// let store = use_store(|| Some(42));
|
||||
/// let transposed = store.transpose();
|
||||
/// match transposed {
|
||||
/// Some(inner_store) => assert_eq!(inner_store(), 42),
|
||||
/// None => panic!("Expected Some"),
|
||||
/// }
|
||||
/// ```
|
||||
pub fn transpose(self) -> Option<MappedStore<T, Lens>> {
|
||||
self.is_some().then(move || {
|
||||
let map: fn(&Option<T>) -> &T = |value| {
|
||||
value.as_ref().unwrap_or_else(|| {
|
||||
panic!("Tried to access `Some` on an Option value");
|
||||
})
|
||||
};
|
||||
let map_mut: fn(&mut Option<T>) -> &mut T = |value| {
|
||||
value.as_mut().unwrap_or_else(|| {
|
||||
panic!("Tried to access `Some` on an Option value");
|
||||
})
|
||||
};
|
||||
self.into_selector().child(0, map, map_mut).into()
|
||||
})
|
||||
}
|
||||
|
||||
/// Unwraps the `Option` and returns a `Store<T>`. This will only track the shallow state of the `Option`. It will
|
||||
/// only cause a re-run if the `Option` could change from `None` to `Some` or vice versa.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust, no_run
|
||||
/// use dioxus_stores::*;
|
||||
/// let store = use_store(|| Some(42));
|
||||
/// let unwrapped = store.unwrap();
|
||||
/// assert_eq!(unwrapped(), 42);
|
||||
/// ```
|
||||
pub fn unwrap(self) -> MappedStore<T, Lens> {
|
||||
self.transpose().unwrap()
|
||||
}
|
||||
|
||||
/// Expects the `Option` to be `Some` and returns a `Store<T>`. If the value is `None`, this will panic with `msg`. This will
|
||||
/// only track the shallow state of the `Option`. It will only cause a re-run if the `Option` could change from `None`
|
||||
/// to `Some` or vice versa.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust, no_run
|
||||
/// use dioxus_stores::*;
|
||||
/// let store = use_store(|| Some(42));
|
||||
/// let unwrapped = store.expect("the answer to life the universe and everything");
|
||||
/// assert_eq!(unwrapped(), 42);
|
||||
/// ```
|
||||
pub fn expect(self, msg: &str) -> MappedStore<T, Lens> {
|
||||
self.transpose().expect(msg)
|
||||
}
|
||||
|
||||
/// Returns a slice of the contained value, or an empty slice. This will not subscribe to any part of the store.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust, no_run
|
||||
/// use dioxus::prelude::*;
|
||||
/// use dioxus_stores::*;
|
||||
/// let store = use_store(|| Some(42));
|
||||
/// let slice = store.as_slice();
|
||||
/// assert_eq!(&*slice.read(), [42]);
|
||||
/// ```
|
||||
pub fn as_slice(self) -> MappedStore<[T], Lens> {
|
||||
let map: fn(&Option<T>) -> &[T] = |value| value.as_slice();
|
||||
let map_mut: fn(&mut Option<T>) -> &mut [T] = |value| value.as_mut_slice();
|
||||
self.into_selector().map(map, map_mut).into()
|
||||
}
|
||||
|
||||
/// Transpose the store then coerce the contents of the Option with deref. This will only track the shallow state of the `Option`. It will
|
||||
/// only cause a re-run if the `Option` could change from `None` to `Some` or vice versa.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust, no_run
|
||||
/// use dioxus_stores::*;
|
||||
/// let store = use_store(|| Some(Box::new(42)));
|
||||
/// let derefed = store.as_deref().unwrap();
|
||||
/// assert_eq!(derefed(), 42);
|
||||
/// ```
|
||||
pub fn as_deref(self) -> Option<MappedStore<T::Target, Lens>>
|
||||
where
|
||||
T: DerefMut,
|
||||
{
|
||||
self.is_some().then(move || {
|
||||
let map: fn(&Option<T>) -> &T::Target = |value| {
|
||||
value
|
||||
.as_ref()
|
||||
.unwrap_or_else(|| {
|
||||
panic!("Tried to access `Some` on an Option value");
|
||||
})
|
||||
.deref()
|
||||
};
|
||||
let map_mut: fn(&mut Option<T>) -> &mut T::Target = |value| {
|
||||
value
|
||||
.as_mut()
|
||||
.unwrap_or_else(|| {
|
||||
panic!("Tried to access `Some` on an Option value");
|
||||
})
|
||||
.deref_mut()
|
||||
};
|
||||
self.into_selector().child(0, map, map_mut).into()
|
||||
})
|
||||
}
|
||||
|
||||
/// Transpose the store then filter the contents of the Option with a closure. This will always track the shallow
|
||||
/// state of the and will track the inner state of the enum if the enum is Some.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust, no_run
|
||||
/// use dioxus_stores::*;
|
||||
/// let store = use_store(|| Some(42));
|
||||
/// let option = store.filter(|&v| v > 40);
|
||||
/// let value = option.unwrap();
|
||||
/// assert_eq!(value(), 42);
|
||||
/// ```
|
||||
pub fn filter(self, f: impl FnOnce(&T) -> bool) -> Option<MappedStore<T, Lens>> {
|
||||
self.is_some_and(f).then(move || {
|
||||
let map: fn(&Option<T>) -> &T = |value| {
|
||||
value.as_ref().unwrap_or_else(|| {
|
||||
panic!("Tried to access `Some` on an Option value");
|
||||
})
|
||||
};
|
||||
let map_mut: fn(&mut Option<T>) -> &mut T = |value| {
|
||||
value.as_mut().unwrap_or_else(|| {
|
||||
panic!("Tried to access `Some` on an Option value");
|
||||
})
|
||||
};
|
||||
self.into_selector().child(0, map, map_mut).into()
|
||||
})
|
||||
}
|
||||
|
||||
/// Call the function with a reference to the inner value if it is Some. This will always track the shallow
|
||||
/// state of the and will track the inner state of the enum if the enum is Some.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust, no_run
|
||||
/// use dioxus_stores::*;
|
||||
/// let store = use_store(|| Some(42)).inspect(|v| println!("{v}"));
|
||||
/// ```
|
||||
pub fn inspect(self, f: impl FnOnce(&T)) -> Self {
|
||||
{
|
||||
self.selector().track_shallow();
|
||||
let value = self.selector().peek();
|
||||
if let Some(v) = &*value {
|
||||
self.selector().track();
|
||||
f(v);
|
||||
}
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,336 @@
|
||||
use std::fmt::Debug;
|
||||
|
||||
use crate::{store::Store, MappedStore};
|
||||
use dioxus_signals::{Readable, ReadableExt, Writable};
|
||||
|
||||
impl<Lens, T, E> Store<Result<T, E>, Lens>
|
||||
where
|
||||
Lens: Readable<Target = Result<T, E>> + 'static,
|
||||
T: 'static,
|
||||
E: 'static,
|
||||
{
|
||||
/// Checks if the `Result` is `Ok`. This will only track the shallow state of the `Result`. It will
|
||||
/// only cause a re-run if the `Result` could change from `Err` to `Ok` or vice versa.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust, no_run
|
||||
/// use dioxus_stores::*;
|
||||
/// let store = use_store(|| Ok::<u32, ()>(42));
|
||||
/// assert!(store.is_ok());
|
||||
/// ```
|
||||
pub fn is_ok(&self) -> bool {
|
||||
self.selector().track_shallow();
|
||||
self.selector().peek().is_ok()
|
||||
}
|
||||
|
||||
/// Returns true if the result is Ok and the closure returns true. This will always track the shallow
|
||||
/// state of the and will track the inner state of the enum if the enum is Ok.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust, no_run
|
||||
/// use dioxus_stores::*;
|
||||
/// let store = use_store(|| Ok::<u32, ()>(42));
|
||||
/// assert!(store.is_ok_and(|v| *v == 42));
|
||||
/// ```
|
||||
pub fn is_ok_and(&self, f: impl FnOnce(&T) -> bool) -> bool {
|
||||
self.selector().track_shallow();
|
||||
let value = self.selector().peek();
|
||||
if let Ok(v) = &*value {
|
||||
self.selector().track();
|
||||
f(v)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if the `Result` is `Err`. This will only track the shallow state of the `Result`. It will
|
||||
/// only cause a re-run if the `Result` could change from `Ok` to `Err` or vice versa.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust, no_run
|
||||
/// use dioxus_stores::*;
|
||||
/// let store = use_store(|| Err::<(), u32>(42));
|
||||
/// assert!(store.is_err());
|
||||
/// ```
|
||||
pub fn is_err(&self) -> bool {
|
||||
self.selector().track_shallow();
|
||||
self.selector().peek().is_err()
|
||||
}
|
||||
|
||||
/// Returns true if the result is Err and the closure returns true. This will always track the shallow
|
||||
/// state of the and will track the inner state of the enum if the enum is Err.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust, no_run
|
||||
/// use dioxus_stores::*;
|
||||
/// let store = use_store(|| Err::<(), u32>(42));
|
||||
/// assert!(store.is_err_and(|v| *v == 42));
|
||||
/// ```
|
||||
pub fn is_err_and(&self, f: impl FnOnce(&E) -> bool) -> bool {
|
||||
self.selector().track_shallow();
|
||||
let value = self.selector().peek();
|
||||
if let Err(e) = &*value {
|
||||
self.selector().track();
|
||||
f(e)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts `Store<Result<T, E>>` into `Option<Store<T>>`, discarding the error if present. This will
|
||||
/// only track the shallow state of the `Result`. It will only cause a re-run if the `Result` could
|
||||
/// change from `Err` to `Ok` or vice versa.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust, no_run
|
||||
/// use dioxus_stores::*;
|
||||
/// let store = use_store(|| Ok::<u32, ()>(42));
|
||||
/// match store.ok() {
|
||||
/// Some(ok_store) => assert_eq!(ok_store(), 42),
|
||||
/// None => panic!("Expected Ok"),
|
||||
/// }
|
||||
/// ```
|
||||
pub fn ok(self) -> Option<MappedStore<T, Lens>> {
|
||||
let map: fn(&Result<T, E>) -> &T = |value| {
|
||||
value.as_ref().unwrap_or_else(|_| {
|
||||
panic!("Tried to access `ok` on an Err value");
|
||||
})
|
||||
};
|
||||
let map_mut: fn(&mut Result<T, E>) -> &mut T = |value| {
|
||||
value.as_mut().unwrap_or_else(|_| {
|
||||
panic!("Tried to access `ok` on an Err value");
|
||||
})
|
||||
};
|
||||
self.is_ok()
|
||||
.then(|| self.into_selector().child(0, map, map_mut).into())
|
||||
}
|
||||
|
||||
/// Converts `Store<Result<T, E>>` into `Option<Store<E>>`, discarding the success if present. This will
|
||||
/// only track the shallow state of the `Result`. It will only cause a re-run if the `Result` could
|
||||
/// change from `Ok` to `Err` or vice versa.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust, no_run
|
||||
/// use dioxus_stores::*;
|
||||
/// let store = use_store(|| Err::<(), u32>(42));
|
||||
/// match store.err() {
|
||||
/// Some(err_store) => assert_eq!(err_store(), 42),
|
||||
/// None => panic!("Expected Err"),
|
||||
/// }
|
||||
/// ```
|
||||
pub fn err(self) -> Option<MappedStore<E, Lens>>
|
||||
where
|
||||
Lens: Writable<Target = Result<T, E>> + 'static,
|
||||
{
|
||||
self.is_err().then(|| {
|
||||
let map: fn(&Result<T, E>) -> &E = |value| match value {
|
||||
Ok(_) => panic!("Tried to access `err` on an Ok value"),
|
||||
Err(e) => e,
|
||||
};
|
||||
let map_mut: fn(&mut Result<T, E>) -> &mut E = |value| match value {
|
||||
Ok(_) => panic!("Tried to access `err` on an Ok value"),
|
||||
Err(e) => e,
|
||||
};
|
||||
self.into_selector().child(1, map, map_mut).into()
|
||||
})
|
||||
}
|
||||
|
||||
/// Transposes the `Store<Result<T, E>>` into a `Result<Store<T>, Store<E>>`. This will only track the
|
||||
/// shallow state of the `Result`. It will only cause a re-run if the `Result` could change from `Err` to
|
||||
/// `Ok` or vice versa.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust, no_run
|
||||
/// use dioxus_stores::*;
|
||||
/// let store = use_store(|| Ok::<u32, ()>(42));
|
||||
/// match store.transpose() {
|
||||
/// Ok(ok_store) => assert_eq!(ok_store(), 42),
|
||||
/// Err(err_store) => assert_eq!(err_store(), ()),
|
||||
/// }
|
||||
/// ```
|
||||
#[allow(clippy::result_large_err)]
|
||||
pub fn transpose(self) -> Result<MappedStore<T, Lens>, MappedStore<E, Lens>>
|
||||
where
|
||||
Lens: Writable<Target = Result<T, E>> + 'static,
|
||||
{
|
||||
if self.is_ok() {
|
||||
let map: fn(&Result<T, E>) -> &T = |value| match value {
|
||||
Ok(t) => t,
|
||||
Err(_) => panic!("Tried to access `ok` on an Err value"),
|
||||
};
|
||||
let map_mut: fn(&mut Result<T, E>) -> &mut T = |value| match value {
|
||||
Ok(t) => t,
|
||||
Err(_) => panic!("Tried to access `ok` on an Err value"),
|
||||
};
|
||||
Ok(self.into_selector().child(0, map, map_mut).into())
|
||||
} else {
|
||||
let map: fn(&Result<T, E>) -> &E = |value| match value {
|
||||
Ok(_) => panic!("Tried to access `err` on an Ok value"),
|
||||
Err(e) => e,
|
||||
};
|
||||
let map_mut: fn(&mut Result<T, E>) -> &mut E = |value| match value {
|
||||
Ok(_) => panic!("Tried to access `err` on an Ok value"),
|
||||
Err(e) => e,
|
||||
};
|
||||
Err(self.into_selector().child(1, map, map_mut).into())
|
||||
}
|
||||
}
|
||||
|
||||
/// Unwraps the `Result` and returns a `Store<T>`. This will only track the shallow state of the `Result`.
|
||||
/// It will only cause a re-run if the `Result` could change from `Err` to `Ok` or vice versa.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust, no_run
|
||||
/// use dioxus_stores::*;
|
||||
/// let store = use_store(|| Ok::<u32, ()>(42));
|
||||
/// let unwrapped = store.unwrap();
|
||||
/// assert_eq!(unwrapped(), 42);
|
||||
/// ```
|
||||
pub fn unwrap(self) -> MappedStore<T, Lens>
|
||||
where
|
||||
Lens: Writable<Target = Result<T, E>> + 'static,
|
||||
E: Debug,
|
||||
{
|
||||
self.transpose().unwrap()
|
||||
}
|
||||
|
||||
/// Expects the `Result` to be `Ok` and returns a `Store<T>`. If the value is `Err`, this will panic with `msg`.
|
||||
/// This will only track the shallow state of the `Result`. It will only cause a re-run if the `Result` could
|
||||
/// change from `Err` to `Ok` or vice versa.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust, no_run
|
||||
/// use dioxus_stores::*;
|
||||
/// let store = use_store(|| Ok::<u32, ()>(42));
|
||||
/// let unwrapped = store.expect("Expected Ok");
|
||||
/// assert_eq!(unwrapped(), 42);
|
||||
/// ```
|
||||
pub fn expect(self, msg: &str) -> MappedStore<T, Lens>
|
||||
where
|
||||
Lens: Writable<Target = Result<T, E>> + 'static,
|
||||
E: Debug,
|
||||
{
|
||||
self.transpose().expect(msg)
|
||||
}
|
||||
|
||||
/// Unwraps the error variant of the `Result`. This will only track the shallow state of the `Result`.
|
||||
/// It will only cause a re-run if the `Result` could change from `Ok` to `Err` or vice versa.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust, no_run
|
||||
/// use dioxus_stores::*;
|
||||
/// let store = use_store(|| Err::<(), u32>(42));
|
||||
/// let unwrapped_err = store.unwrap_err();
|
||||
/// assert_eq!(unwrapped_err(), 42);
|
||||
/// ```
|
||||
pub fn unwrap_err(self) -> MappedStore<E, Lens>
|
||||
where
|
||||
Lens: Writable<Target = Result<T, E>> + 'static,
|
||||
T: Debug,
|
||||
{
|
||||
self.transpose().unwrap_err()
|
||||
}
|
||||
|
||||
/// Expects the `Result` to be `Err` and returns a `Store<E>`. If the value is `Ok`, this will panic with `msg`.
|
||||
/// This will only track the shallow state of the `Result`. It will only cause a re-run if the `Result` could
|
||||
/// change from `Ok` to `Err` or vice versa.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust, no_run
|
||||
/// use dioxus_stores::*;
|
||||
/// let store = use_store(|| Err::<(), u32>(42));
|
||||
/// let unwrapped_err = store.expect_err("Expected Err");
|
||||
/// assert_eq!(unwrapped_err(), 42);
|
||||
/// ```
|
||||
pub fn expect_err(self, msg: &str) -> MappedStore<E, Lens>
|
||||
where
|
||||
Lens: Writable<Target = Result<T, E>> + 'static,
|
||||
T: Debug,
|
||||
{
|
||||
self.transpose().expect_err(msg)
|
||||
}
|
||||
|
||||
/// Call the function with a reference to the inner value if it is Ok. This will always track the shallow
|
||||
/// state of the and will track the inner state of the enum if the enum is Ok.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust, no_run
|
||||
/// use dioxus_stores::*;
|
||||
/// let store = use_store(|| Ok::<u32, ()>(42)).inspect(|v| println!("{v}"));
|
||||
/// ```
|
||||
pub fn inspect(self, f: impl FnOnce(&T)) -> Self
|
||||
where
|
||||
Lens: Writable<Target = Result<T, E>> + 'static,
|
||||
{
|
||||
{
|
||||
self.selector().track_shallow();
|
||||
let value = self.selector().peek();
|
||||
if let Ok(value) = &*value {
|
||||
self.selector().track();
|
||||
f(value);
|
||||
}
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Call the function with a mutable reference to the inner value if it is Err. This will always track the shallow
|
||||
/// state of the `Result` and will track the inner state of the enum if the enum is Err.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust, no_run
|
||||
/// use dioxus_stores::*;
|
||||
/// let store = use_store(|| Err::<(), u32>(42)).inspect_err(|v| println!("{v}"));
|
||||
/// ```
|
||||
pub fn inspect_err(self, f: impl FnOnce(&E)) -> Self
|
||||
where
|
||||
Lens: Writable<Target = Result<T, E>> + 'static,
|
||||
{
|
||||
{
|
||||
self.selector().track_shallow();
|
||||
let value = self.selector().peek();
|
||||
if let Err(value) = &*value {
|
||||
self.selector().track();
|
||||
f(value);
|
||||
}
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Transpose the store then coerce the contents of the Result with deref. This will only track the shallow state of the `Result`. It will
|
||||
/// only cause a re-run if the `Result` could change from `Err` to `Ok` or vice versa.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust, no_run
|
||||
/// use dioxus_stores::*;
|
||||
/// let store = use_store(|| Ok::<Box<u32>, ()>(Box::new(42)));
|
||||
/// let derefed = store.as_deref().unwrap();
|
||||
/// assert_eq!(derefed(), 42);
|
||||
/// ```
|
||||
pub fn as_deref(self) -> Result<MappedStore<T::Target, Lens>, MappedStore<E, Lens>>
|
||||
where
|
||||
Lens: Writable<Target = Result<T, E>> + 'static,
|
||||
T: std::ops::DerefMut,
|
||||
{
|
||||
if self.is_ok() {
|
||||
let map: fn(&Result<T, E>) -> &T::Target = |value| match value {
|
||||
Ok(t) => t.deref(),
|
||||
Err(_) => panic!("Tried to access `ok` on an Err value"),
|
||||
};
|
||||
let map_mut: fn(&mut Result<T, E>) -> &mut T::Target = |value| match value {
|
||||
Ok(t) => t.deref_mut(),
|
||||
Err(_) => panic!("Tried to access `ok` on an Err value"),
|
||||
};
|
||||
Ok(self.into_selector().child(0, map, map_mut).into())
|
||||
} else {
|
||||
let map: fn(&Result<T, E>) -> &E = |value| match value {
|
||||
Ok(_) => panic!("Tried to access `err` on an Ok value"),
|
||||
Err(e) => e,
|
||||
};
|
||||
let map_mut: fn(&mut Result<T, E>) -> &mut E = |value| match value {
|
||||
Ok(_) => panic!("Tried to access `err` on an Ok value"),
|
||||
Err(e) => e,
|
||||
};
|
||||
Err(self.into_selector().child(1, map, map_mut).into())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
use std::iter::FusedIterator;
|
||||
|
||||
use crate::{impls::index::IndexWrite, store::Store};
|
||||
use dioxus_signals::{Readable, ReadableExt};
|
||||
|
||||
impl<Lens, I> Store<Vec<I>, Lens>
|
||||
where
|
||||
Lens: Readable<Target = Vec<I>> + 'static,
|
||||
I: 'static,
|
||||
{
|
||||
/// Returns the length of the slice. This will only track the shallow state of the slice.
|
||||
/// It will only cause a re-run if the length of the slice could change.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust, no_run
|
||||
/// use dioxus_stores::*;
|
||||
/// let store = use_store(|| vec![1, 2, 3]);
|
||||
/// assert_eq!(store.len(), 3);
|
||||
/// ```
|
||||
pub fn len(&self) -> usize {
|
||||
self.selector().track_shallow();
|
||||
self.selector().peek().len()
|
||||
}
|
||||
|
||||
/// Checks if the slice is empty. This will only track the shallow state of the slice.
|
||||
/// It will only cause a re-run if the length of the slice could change.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust, no_run
|
||||
/// use dioxus_stores::*;
|
||||
/// let store = use_store(|| vec![1, 2, 3]);
|
||||
/// assert!(!store.is_empty());
|
||||
/// ```
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.selector().track_shallow();
|
||||
self.selector().peek().is_empty()
|
||||
}
|
||||
|
||||
/// Returns an iterator over the items in the slice. This will only track the shallow state of the slice.
|
||||
/// It will only cause a re-run if the length of the slice could change.
|
||||
/// # Example
|
||||
/// ```rust, no_run
|
||||
/// use dioxus_stores::*;
|
||||
/// let store = use_store(|| vec![1, 2, 3]);
|
||||
/// for item in store.iter() {
|
||||
/// println!("{}", item);
|
||||
/// }
|
||||
/// ```
|
||||
pub fn iter(
|
||||
&self,
|
||||
) -> impl ExactSizeIterator<Item = Store<I, IndexWrite<usize, Lens>>>
|
||||
+ DoubleEndedIterator
|
||||
+ FusedIterator
|
||||
+ '_
|
||||
where
|
||||
Lens: Clone,
|
||||
{
|
||||
(0..self.len()).map(move |i| self.clone().index(i))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
use crate::store::Store;
|
||||
use dioxus_signals::Writable;
|
||||
|
||||
impl<Lens: Writable<Target = Vec<T>> + 'static, T: 'static> Store<Vec<T>, Lens> {
|
||||
/// Pushes an item to the end of the vector. This will only mark the length of the vector as dirty.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust, no_run
|
||||
/// use dioxus_stores::*;
|
||||
/// let mut store = use_store(|| vec![1, 2, 3]);
|
||||
/// store.push(4);
|
||||
/// ```
|
||||
pub fn push(&mut self, value: T) {
|
||||
self.selector().mark_dirty_shallow();
|
||||
self.selector().write_untracked().push(value);
|
||||
}
|
||||
|
||||
/// Removes an item from the vector at the specified index and returns it. This will mark items after
|
||||
/// the index and the length of the vector as dirty.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust, no_run
|
||||
/// use dioxus_stores::*;
|
||||
/// let mut store = use_store(|| vec![1, 2, 3]);
|
||||
/// let removed = store.remove(1);
|
||||
/// assert_eq!(removed, 2);
|
||||
/// ```
|
||||
pub fn remove(&mut self, index: usize) -> T {
|
||||
self.selector().mark_dirty_shallow();
|
||||
self.selector().mark_dirty_at_and_after_index(index);
|
||||
self.selector().write_untracked().remove(index)
|
||||
}
|
||||
|
||||
/// Inserts an item at the specified index in the vector. This will mark items at and after the index
|
||||
/// and the length of the vector as dirty.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust, no_run
|
||||
/// use dioxus_stores::*;
|
||||
/// let mut store = use_store(|| vec![1, 2, 3]);
|
||||
/// store.insert(1, 4);
|
||||
/// ```
|
||||
pub fn insert(&mut self, index: usize, value: T) {
|
||||
self.selector().mark_dirty_shallow();
|
||||
self.selector().mark_dirty_at_and_after_index(index);
|
||||
self.selector().write_untracked().insert(index, value);
|
||||
}
|
||||
|
||||
/// Clears the vector, marking it as dirty.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust, no_run
|
||||
/// use dioxus_stores::*;
|
||||
/// let mut store = use_store(|| vec![1, 2, 3]);
|
||||
/// store.clear();
|
||||
/// ```
|
||||
pub fn clear(&mut self) {
|
||||
self.selector().mark_dirty();
|
||||
self.selector().write_untracked().clear();
|
||||
}
|
||||
|
||||
/// Retains only the elements specified by the predicate. This will only mark the length of the vector
|
||||
/// and items after the first removed item as dirty.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust, no_run
|
||||
/// use dioxus_stores::*;
|
||||
/// let mut store = use_store(|| vec![1, 2, 3, 4, 5]);
|
||||
/// store.retain(|&x| x % 2 == 0);
|
||||
/// assert_eq!(store.len(), 2);
|
||||
/// ```
|
||||
pub fn retain(&mut self, mut f: impl FnMut(&T) -> bool) {
|
||||
let mut index = 0;
|
||||
let mut first_removed_index = None;
|
||||
self.selector().write_untracked().retain(|item| {
|
||||
let keep = f(item);
|
||||
if !keep {
|
||||
first_removed_index = first_removed_index.or(Some(index));
|
||||
}
|
||||
index += 1;
|
||||
keep
|
||||
});
|
||||
if let Some(index) = first_removed_index {
|
||||
self.selector().mark_dirty_shallow();
|
||||
self.selector().mark_dirty_at_and_after_index(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
#![doc = include_str!("../README.md")]
|
||||
#![doc(html_logo_url = "https://avatars.githubusercontent.com/u/79236386")]
|
||||
#![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")]
|
||||
#![warn(missing_docs)]
|
||||
#![allow(clippy::type_complexity)]
|
||||
|
||||
mod impls;
|
||||
mod store;
|
||||
mod subscriptions;
|
||||
pub use store::*;
|
||||
pub mod scope;
|
||||
|
||||
#[cfg(feature = "macro")]
|
||||
pub use dioxus_stores_macro::{store, Store};
|
||||
|
||||
/// Re-exports for the store derive macro
|
||||
#[doc(hidden)]
|
||||
pub mod macro_helpers {
|
||||
pub use dioxus_core;
|
||||
pub use dioxus_signals;
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
//! This module contains the `SelectorScope` type with raw access to the underlying store system. Most applications should
|
||||
//! use the [`Store`](dioxus_stores_macro::Store) macro to derive stores for their data structures, which provides a more ergonomic API.
|
||||
|
||||
use std::{fmt::Debug, hash::Hash};
|
||||
|
||||
use crate::subscriptions::{PathKey, StoreSubscriptions, TinyVec};
|
||||
use dioxus_core::Subscribers;
|
||||
use dioxus_signals::{
|
||||
BorrowError, BorrowMutError, MappedMutSignal, Readable, ReadableRef, Writable, WritableExt,
|
||||
WritableRef,
|
||||
};
|
||||
|
||||
/// SelectorScope is the primitive that backs the store system.
|
||||
///
|
||||
/// Under the hood stores consist of two different parts:
|
||||
/// - The underlying lock that contains the data in the store.
|
||||
/// - A tree of subscriptions used to make the store reactive.
|
||||
///
|
||||
/// The `SelectorScope` contains a view into the lock (`Lens`) and a path into the subscription tree. When
|
||||
/// the selector is read to, it will track the current path in the subscription tree. When it it written to
|
||||
/// it marks itself and all its children as dirty.
|
||||
///
|
||||
/// When you derive the [`Store`](dioxus_stores_macro::Store) macro on your data structure,
|
||||
/// it generates methods that map the lock to a new type and scope the path to a specific part of the subscription structure.
|
||||
/// For example, a `Counter` store might look like this:
|
||||
///
|
||||
/// ```rust, ignore
|
||||
/// #[derive(Store)]
|
||||
/// struct Counter {
|
||||
/// count: i32,
|
||||
/// }
|
||||
///
|
||||
/// impl CounterStoreExt for Store<Counter> {
|
||||
/// fn count(
|
||||
/// self,
|
||||
/// ) -> dioxus_stores::Store<
|
||||
/// i32,
|
||||
/// dioxus_stores::macro_helpers::dioxus_signals::MappedMutSignal<i32, __W>,
|
||||
/// > {
|
||||
/// let __map_field: fn(&CounterTree) -> &i32 = |value| &value.count;
|
||||
/// let __map_mut_field: fn(&mut CounterTree) -> &mut i32 = |value| &mut value.count;
|
||||
/// let scope = self.selector().scope(0u32, __map_field, __map_mut_field);
|
||||
/// dioxus_stores::Store::new(scope)
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// The `count` method maps the lock to the `i32` type and creates a child `0` path in the subscription tree. Only writes
|
||||
/// to that `0` path or its parents will trigger a re-render of the components that read the `count` field.
|
||||
#[derive(PartialEq)]
|
||||
pub struct SelectorScope<Lens> {
|
||||
path: TinyVec,
|
||||
store: StoreSubscriptions,
|
||||
write: Lens,
|
||||
}
|
||||
|
||||
impl<Lens> Debug for SelectorScope<Lens> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
f.debug_struct("SelectorScope")
|
||||
.field("path", &self.path)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<Lens> Clone for SelectorScope<Lens>
|
||||
where
|
||||
Lens: Clone,
|
||||
{
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
path: self.path,
|
||||
store: self.store,
|
||||
write: self.write.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Lens> Copy for SelectorScope<Lens> where Lens: Copy {}
|
||||
|
||||
impl<Lens> SelectorScope<Lens> {
|
||||
pub(crate) fn new(path: TinyVec, store: StoreSubscriptions, write: Lens) -> Self {
|
||||
Self { path, store, write }
|
||||
}
|
||||
|
||||
/// Create a child selector scope for a hash key. The scope will only be marked as dirty when a
|
||||
/// write occurs to that key or its parents.
|
||||
///
|
||||
/// Note the hash is lossy, so there may rarely be collisions. If a collision does occur, it may
|
||||
/// cause reruns in a part of the app that has not changed. As long as derived data is pure,
|
||||
/// this should not cause issues.
|
||||
pub fn hash_child<U: ?Sized, T, F, FMut>(
|
||||
self,
|
||||
index: &impl Hash,
|
||||
map: F,
|
||||
map_mut: FMut,
|
||||
) -> SelectorScope<MappedMutSignal<U, Lens, F, FMut>>
|
||||
where
|
||||
F: Fn(&T) -> &U,
|
||||
FMut: Fn(&mut T) -> &mut U,
|
||||
{
|
||||
let hash = self.store.hash(index);
|
||||
self.child(hash, map, map_mut)
|
||||
}
|
||||
|
||||
/// Create a child selector scope for a specific index. The scope will only be marked as dirty when a
|
||||
/// write occurs to that index or its parents.
|
||||
pub fn child<U: ?Sized, T, F, FMut>(
|
||||
self,
|
||||
index: PathKey,
|
||||
map: F,
|
||||
map_mut: FMut,
|
||||
) -> SelectorScope<MappedMutSignal<U, Lens, F, FMut>>
|
||||
where
|
||||
F: Fn(&T) -> &U,
|
||||
FMut: Fn(&mut T) -> &mut U,
|
||||
{
|
||||
self.child_unmapped(index).map(map, map_mut)
|
||||
}
|
||||
|
||||
/// Create a hashed child selector scope for a specific index without mapping the writer. The scope will only
|
||||
/// be marked as dirty when a write occurs to that index or its parents.
|
||||
pub fn hash_child_unmapped(self, index: &impl Hash) -> SelectorScope<Lens> {
|
||||
let hash = self.store.hash(index);
|
||||
self.child_unmapped(hash)
|
||||
}
|
||||
|
||||
/// Create a child selector scope for a specific index without mapping the writer. The scope will only
|
||||
/// be marked as dirty when a write occurs to that index or its parents.
|
||||
pub fn child_unmapped(mut self, index: PathKey) -> SelectorScope<Lens> {
|
||||
self.path.push(index);
|
||||
self
|
||||
}
|
||||
|
||||
/// Map the view into the writable data without creating a child selector scope
|
||||
pub fn map<U: ?Sized, T, F, FMut>(
|
||||
self,
|
||||
map: F,
|
||||
map_mut: FMut,
|
||||
) -> SelectorScope<MappedMutSignal<U, Lens, F, FMut>>
|
||||
where
|
||||
F: Fn(&T) -> &U,
|
||||
FMut: Fn(&mut T) -> &mut U,
|
||||
{
|
||||
self.map_writer(move |write| MappedMutSignal::new(write, map, map_mut))
|
||||
}
|
||||
|
||||
/// Track this scope shallowly.
|
||||
pub fn track_shallow(&self) {
|
||||
self.store.track(&self.path);
|
||||
}
|
||||
|
||||
/// Track this scope recursively.
|
||||
pub fn track(&self) {
|
||||
self.store.track_recursive(&self.path);
|
||||
}
|
||||
|
||||
/// Mark this scope as dirty recursively.
|
||||
pub fn mark_dirty(&self) {
|
||||
self.store.mark_dirty(&self.path);
|
||||
}
|
||||
|
||||
/// Mark this scope as dirty shallowly.
|
||||
pub fn mark_dirty_shallow(&self) {
|
||||
self.store.mark_dirty_shallow(&self.path);
|
||||
}
|
||||
|
||||
/// Mark this scope as dirty at and after the given index.
|
||||
pub fn mark_dirty_at_and_after_index(&self, index: usize) {
|
||||
self.store.mark_dirty_at_and_after_index(&self.path, index);
|
||||
}
|
||||
|
||||
/// Map the writer to a new type.
|
||||
pub fn map_writer<W2>(self, map: impl FnOnce(Lens) -> W2) -> SelectorScope<W2> {
|
||||
SelectorScope {
|
||||
path: self.path,
|
||||
store: self.store,
|
||||
write: map(self.write),
|
||||
}
|
||||
}
|
||||
|
||||
/// Write without notifying subscribers.
|
||||
pub fn write_untracked(&self) -> WritableRef<'static, Lens>
|
||||
where
|
||||
Lens: Writable,
|
||||
{
|
||||
self.write.write_unchecked()
|
||||
}
|
||||
}
|
||||
|
||||
impl<Lens: Readable> Readable for SelectorScope<Lens> {
|
||||
type Target = Lens::Target;
|
||||
type Storage = Lens::Storage;
|
||||
|
||||
fn try_read_unchecked(&self) -> Result<ReadableRef<'static, Lens>, BorrowError> {
|
||||
self.track();
|
||||
self.write.try_read_unchecked()
|
||||
}
|
||||
|
||||
fn try_peek_unchecked(&self) -> Result<ReadableRef<'static, Lens>, BorrowError> {
|
||||
self.write.try_peek_unchecked()
|
||||
}
|
||||
|
||||
fn subscribers(&self) -> Subscribers {
|
||||
self.store.subscribers(&self.path)
|
||||
}
|
||||
}
|
||||
|
||||
impl<Lens: Writable> Writable for SelectorScope<Lens> {
|
||||
type WriteMetadata = Lens::WriteMetadata;
|
||||
|
||||
fn try_write_unchecked(&self) -> Result<WritableRef<'static, Lens>, BorrowMutError> {
|
||||
self.mark_dirty();
|
||||
self.write.try_write_unchecked()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,408 @@
|
||||
use crate::{
|
||||
scope::SelectorScope,
|
||||
subscriptions::{StoreSubscriptions, TinyVec},
|
||||
};
|
||||
use dioxus_core::{
|
||||
use_hook, AttributeValue, DynamicNode, IntoAttributeValue, IntoDynNode, Subscribers, SuperInto,
|
||||
};
|
||||
use dioxus_signals::{
|
||||
read_impls, write_impls, BorrowError, BorrowMutError, CopyValue, Global,
|
||||
InitializeFromFunction, MappedMutSignal, ReadSignal, Readable, ReadableExt, ReadableRef,
|
||||
Storage, UnsyncStorage, Writable, WritableExt, WritableRef, WriteSignal,
|
||||
};
|
||||
use std::marker::PhantomData;
|
||||
|
||||
/// A type alias for a store that has been mapped with a function
|
||||
pub(crate) type MappedStore<
|
||||
T,
|
||||
Lens,
|
||||
F = fn(&<Lens as Readable>::Target) -> &T,
|
||||
FMut = fn(&mut <Lens as Readable>::Target) -> &mut T,
|
||||
> = Store<T, MappedMutSignal<T, Lens, F, FMut>>;
|
||||
|
||||
/// A type alias for a boxed read-only store.
|
||||
pub type ReadStore<T> = Store<T, ReadSignal<T>>;
|
||||
|
||||
/// A type alias for a boxed writable-only store.
|
||||
pub type WriteStore<T> = Store<T, WriteSignal<T>>;
|
||||
|
||||
/// Stores are a reactive type built for nested data structures. Each store will lazily create signals
|
||||
/// for each field/member of the data structure as needed.
|
||||
///
|
||||
/// By default stores act a lot like [`dioxus_signals::Signal`]s, but they provide more granular
|
||||
/// subscriptions without requiring nested signals. You should derive [`Store`](dioxus_stores_macro::Store) on your data
|
||||
/// structures to generate selectors that let you scope the store to a specific part of your data.
|
||||
///
|
||||
/// You can also use the [`#[store]`](dioxus_stores_macro::store) macro on an impl block to add any additional methods to your store
|
||||
/// with an extension trait. This lets you add methods to the store even though the type is not defined in your crate.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust, no_run
|
||||
/// use dioxus::prelude::*;
|
||||
/// use dioxus_stores::*;
|
||||
///
|
||||
/// fn main() {
|
||||
/// dioxus::launch(app);
|
||||
/// }
|
||||
///
|
||||
/// // Deriving the store trait provides methods to scope the store to specific parts of your data structure.
|
||||
/// // The `Store` macro generates a `count` and `children` method for the `CounterTree` struct.
|
||||
/// #[derive(Store, Default)]
|
||||
/// struct CounterTree {
|
||||
/// count: i32,
|
||||
/// children: Vec<CounterTree>,
|
||||
/// }
|
||||
///
|
||||
/// // The store macro generates an extension trait with additional methods for the store based on the impl block.
|
||||
/// #[store]
|
||||
/// impl<Lens> Store<CounterTree, Lens> {
|
||||
/// // Methods that take &self automatically require the lens to implement `Readable` which lets you read the store.
|
||||
/// fn sum(&self) -> i32 {
|
||||
/// self.count().cloned() + self.children().iter().map(|c| c.sum()).sum::<i32>()
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// fn app() -> Element {
|
||||
/// let value = use_store(Default::default);
|
||||
///
|
||||
/// rsx! {
|
||||
/// Tree {
|
||||
/// value
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// #[component]
|
||||
/// fn Tree(value: Store<CounterTree>) -> Element {
|
||||
/// // Calling the generated `count` method returns a new store that can only
|
||||
/// // read and write the count field
|
||||
/// let mut count = value.count();
|
||||
/// let mut children = value.children();
|
||||
/// rsx! {
|
||||
/// button {
|
||||
/// // Incrementing the count will only rerun parts of the app that have read the count field
|
||||
/// onclick: move |_| count += 1,
|
||||
/// "Increment"
|
||||
/// }
|
||||
/// button {
|
||||
/// // Stores are aware of data structures like `Vec` and `Hashmap`. When we push an item to the vec
|
||||
/// // it will only rerun the parts of the app that depend on the length of the vec
|
||||
/// onclick: move |_| children.push(Default::default()),
|
||||
/// "Push child"
|
||||
/// }
|
||||
/// "sum: {value.sum()}"
|
||||
/// ul {
|
||||
/// // Iterating over the children gives us stores scoped to each child.
|
||||
/// for value in children.iter() {
|
||||
/// li {
|
||||
/// Tree { value }
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
pub struct Store<T: ?Sized, Lens = WriteSignal<T>> {
|
||||
selector: SelectorScope<Lens>,
|
||||
_phantom: PhantomData<Box<T>>,
|
||||
}
|
||||
|
||||
impl<T: 'static, S: Storage<T>> Store<T, CopyValue<T, S>> {
|
||||
/// Creates a new `Store` that might be sync. This allocates memory in the current scope, so this should only be called
|
||||
/// inside of an initialization closure like the closure passed to [`use_hook`].
|
||||
#[track_caller]
|
||||
pub fn new_maybe_sync(value: T) -> Self {
|
||||
let store = StoreSubscriptions::new();
|
||||
let value = CopyValue::new_maybe_sync(value);
|
||||
|
||||
let path = TinyVec::new();
|
||||
let selector = SelectorScope::new(path, store, value);
|
||||
selector.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: 'static> Store<T> {
|
||||
/// Creates a new `Store`. This allocates memory in the current scope, so this should only be called
|
||||
/// inside of an initialization closure like the closure passed to [`use_hook`].
|
||||
#[track_caller]
|
||||
pub fn new(value: T) -> Self {
|
||||
let store = StoreSubscriptions::new();
|
||||
let value = CopyValue::new_maybe_sync(value);
|
||||
let value = value.into();
|
||||
|
||||
let path = TinyVec::new();
|
||||
let selector = SelectorScope::new(path, store, value);
|
||||
selector.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ?Sized, Lens> Store<T, Lens> {
|
||||
/// Get the underlying selector for this store. The selector provides low level access to the lazy tracking system
|
||||
/// of the store. This can be useful to create selectors for custom data structures in libraries. For most applications
|
||||
/// the selectors generated by the [`Store`](dioxus_stores_macro::Store) macro provide all the functionality you need.
|
||||
pub fn selector(&self) -> &SelectorScope<Lens> {
|
||||
&self.selector
|
||||
}
|
||||
|
||||
/// Convert the store into the underlying selector
|
||||
pub fn into_selector(self) -> SelectorScope<Lens> {
|
||||
self.selector
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ?Sized, Lens> From<SelectorScope<Lens>> for Store<T, Lens> {
|
||||
fn from(selector: SelectorScope<Lens>) -> Self {
|
||||
Self {
|
||||
selector,
|
||||
_phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ?Sized, Lens> PartialEq for Store<T, Lens>
|
||||
where
|
||||
Lens: PartialEq,
|
||||
{
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.selector == other.selector
|
||||
}
|
||||
}
|
||||
impl<T: ?Sized, Lens> Clone for Store<T, Lens>
|
||||
where
|
||||
Lens: Clone,
|
||||
{
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
selector: self.selector.clone(),
|
||||
_phantom: ::std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<T: ?Sized, Lens> Copy for Store<T, Lens> where Lens: Copy {}
|
||||
|
||||
impl<__F, __FMut, T: ?Sized, Lens> ::std::convert::From<MappedStore<T, Lens, __F, __FMut>>
|
||||
for Store<T, WriteSignal<T>>
|
||||
where
|
||||
Lens: Writable<Storage = UnsyncStorage> + 'static,
|
||||
__F: Fn(&Lens::Target) -> &T + 'static,
|
||||
__FMut: Fn(&mut Lens::Target) -> &mut T + 'static,
|
||||
T: 'static,
|
||||
{
|
||||
fn from(value: MappedStore<T, Lens, __F, __FMut>) -> Self {
|
||||
Store {
|
||||
selector: value.selector.map_writer(::std::convert::Into::into),
|
||||
_phantom: ::std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<__F, __FMut, T: ?Sized, Lens> ::std::convert::From<MappedStore<T, Lens, __F, __FMut>>
|
||||
for Store<T, ReadSignal<T>>
|
||||
where
|
||||
Lens: Writable<Storage = UnsyncStorage> + 'static,
|
||||
__F: Fn(&Lens::Target) -> &T + 'static,
|
||||
__FMut: Fn(&mut Lens::Target) -> &mut T + 'static,
|
||||
T: 'static,
|
||||
{
|
||||
fn from(value: MappedStore<T, Lens, __F, __FMut>) -> Self {
|
||||
Store {
|
||||
selector: value.selector.map_writer(::std::convert::Into::into),
|
||||
_phantom: ::std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<T> ::std::convert::From<Store<T>> for ReadStore<T>
|
||||
where
|
||||
T: ?Sized + 'static,
|
||||
{
|
||||
fn from(value: Store<T>) -> Self {
|
||||
Self {
|
||||
selector: value.selector.map_writer(::std::convert::Into::into),
|
||||
_phantom: ::std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub struct SuperIntoReadSignalMarker;
|
||||
impl<T, Lens> SuperInto<ReadSignal<T>, SuperIntoReadSignalMarker> for Store<T, Lens>
|
||||
where
|
||||
T: ?Sized + 'static,
|
||||
Lens: Readable<Target = T, Storage = UnsyncStorage> + 'static,
|
||||
{
|
||||
fn super_into(self) -> ReadSignal<T> {
|
||||
ReadSignal::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub struct SuperIntoWriteSignalMarker;
|
||||
impl<T, Lens> SuperInto<WriteSignal<T>, SuperIntoWriteSignalMarker> for Store<T, Lens>
|
||||
where
|
||||
T: ?Sized + 'static,
|
||||
Lens: Writable<Target = T, Storage = UnsyncStorage> + 'static,
|
||||
{
|
||||
fn super_into(self) -> WriteSignal<T> {
|
||||
WriteSignal::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ?Sized, Lens> Readable for Store<T, Lens>
|
||||
where
|
||||
Lens: Readable<Target = T>,
|
||||
T: 'static,
|
||||
{
|
||||
type Storage = Lens::Storage;
|
||||
type Target = T;
|
||||
fn try_read_unchecked(&self) -> Result<ReadableRef<'static, Self>, BorrowError> {
|
||||
self.selector.try_read_unchecked()
|
||||
}
|
||||
fn try_peek_unchecked(&self) -> Result<ReadableRef<'static, Self>, BorrowError> {
|
||||
self.selector.try_peek_unchecked()
|
||||
}
|
||||
fn subscribers(&self) -> Subscribers {
|
||||
self.selector.subscribers()
|
||||
}
|
||||
}
|
||||
impl<T: ?Sized, Lens> Writable for Store<T, Lens>
|
||||
where
|
||||
Lens: Writable<Target = T>,
|
||||
T: 'static,
|
||||
{
|
||||
type WriteMetadata = Lens::WriteMetadata;
|
||||
fn try_write_unchecked(&self) -> Result<WritableRef<'static, Self>, BorrowMutError> {
|
||||
self.selector.try_write_unchecked()
|
||||
}
|
||||
}
|
||||
impl<T, Lens> IntoAttributeValue for Store<T, Lens>
|
||||
where
|
||||
Self: Readable<Target = T>,
|
||||
T: ::std::clone::Clone + IntoAttributeValue + 'static,
|
||||
{
|
||||
fn into_value(self) -> AttributeValue {
|
||||
ReadableExt::cloned(&self).into_value()
|
||||
}
|
||||
}
|
||||
impl<T, Lens> IntoDynNode for Store<T, Lens>
|
||||
where
|
||||
Self: Readable<Target = T>,
|
||||
T: ::std::clone::Clone + IntoDynNode + 'static,
|
||||
{
|
||||
fn into_dyn_node(self) -> DynamicNode {
|
||||
ReadableExt::cloned(&self).into_dyn_node()
|
||||
}
|
||||
}
|
||||
impl<T, Lens> ::std::ops::Deref for Store<T, Lens>
|
||||
where
|
||||
Self: Readable<Target = T> + 'static,
|
||||
T: ::std::clone::Clone + 'static,
|
||||
{
|
||||
type Target = dyn Fn() -> T;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
unsafe { ReadableExt::deref_impl(self) }
|
||||
}
|
||||
}
|
||||
|
||||
read_impls!(Store<T, Lens> where Lens: Readable<Target = T>);
|
||||
write_impls!(Store<T, Lens> where Lens: Writable<Target = T>);
|
||||
|
||||
/// Create a new [`Store`]. Stores are a reactive type built for nested data structures.
|
||||
///
|
||||
///
|
||||
/// By default stores act a lot like [`dioxus_signals::Signal`]s, but they provide more granular
|
||||
/// subscriptions without requiring nested signals. You should derive [`Store`](dioxus_stores_macro::Store) on your data
|
||||
/// structures to generate selectors that let you scope the store to a specific part of your data structure.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust, no_run
|
||||
/// use dioxus::prelude::*;
|
||||
/// use dioxus_stores::*;
|
||||
///
|
||||
/// fn main() {
|
||||
/// dioxus::launch(app);
|
||||
/// }
|
||||
///
|
||||
/// // Deriving the store trait provides methods to scope the store to specific parts of your data structure.
|
||||
/// // The `Store` macro generates a `count` and `children` method for `Store<CounterTree>`.
|
||||
/// #[derive(Store, Default)]
|
||||
/// struct CounterTree {
|
||||
/// count: i32,
|
||||
/// children: Vec<CounterTree>,
|
||||
/// }
|
||||
///
|
||||
/// fn app() -> Element {
|
||||
/// let value = use_store(Default::default);
|
||||
///
|
||||
/// rsx! {
|
||||
/// Tree {
|
||||
/// value
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// #[component]
|
||||
/// fn Tree(value: Store<CounterTree>) -> Element {
|
||||
/// // Calling the generated `count` method returns a new store that can only
|
||||
/// // read and write the count field
|
||||
/// let mut count = value.count();
|
||||
/// let mut children = value.children();
|
||||
/// rsx! {
|
||||
/// button {
|
||||
/// // Incrementing the count will only rerun parts of the app that have read the count field
|
||||
/// onclick: move |_| count += 1,
|
||||
/// "Increment"
|
||||
/// }
|
||||
/// button {
|
||||
/// // Stores are aware of data structures like `Vec` and `Hashmap`. When we push an item to the vec
|
||||
/// // it will only rerun the parts of the app that depend on the length of the vec
|
||||
/// onclick: move |_| children.push(Default::default()),
|
||||
/// "Push child"
|
||||
/// }
|
||||
/// ul {
|
||||
/// // Iterating over the children gives us stores scoped to each child.
|
||||
/// for value in children.iter() {
|
||||
/// li {
|
||||
/// Tree { value }
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
pub fn use_store<T: 'static>(init: impl FnOnce() -> T) -> Store<T> {
|
||||
use_hook(move || Store::new(init()))
|
||||
}
|
||||
|
||||
/// A type alias for global stores
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust, no_run
|
||||
/// use dioxus::prelude::*;
|
||||
/// use dioxus_stores::*;
|
||||
///
|
||||
/// #[derive(Store)]
|
||||
/// struct Counter {
|
||||
/// count: i32,
|
||||
/// }
|
||||
///
|
||||
/// static COUNTER: GlobalStore<Counter> = Global::new(|| Counter { count: 0 });
|
||||
///
|
||||
/// fn app() -> Element {
|
||||
/// let mut count = COUNTER.resolve().count();
|
||||
///
|
||||
/// rsx! {
|
||||
/// button {
|
||||
/// onclick: move |_| count += 1,
|
||||
/// "{count}"
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
pub type GlobalStore<T> = Global<Store<T>, T>;
|
||||
|
||||
impl<T: 'static> InitializeFromFunction<T> for Store<T> {
|
||||
fn initialize_from_function(f: fn() -> T) -> Self {
|
||||
Store::new(f())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,323 @@
|
||||
use dioxus_core::{ReactiveContext, SubscriberList, Subscribers};
|
||||
use dioxus_signals::{CopyValue, ReadableExt, SyncStorage, Writable, WritableExt};
|
||||
use std::fmt::Debug;
|
||||
use std::hash::BuildHasher;
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
hash::Hash,
|
||||
ops::Deref,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
/// A single node in the [`StoreSubscriptions`] tree. Each path is a specific view into the store
|
||||
/// and can be subscribed to and marked dirty separately. If the whole store is read or written to, all
|
||||
/// nodes in the subtree are subscribed to or marked as dirty.
|
||||
#[derive(Clone, Default)]
|
||||
pub(crate) struct SelectorNode {
|
||||
subscribers: HashSet<ReactiveContext>,
|
||||
root: HashMap<PathKey, SelectorNode>,
|
||||
}
|
||||
|
||||
impl SelectorNode {
|
||||
/// Get an existing selector node by its path.
|
||||
fn get(&self, path: &[PathKey]) -> Option<&SelectorNode> {
|
||||
let [first, rest @ ..] = path else {
|
||||
return Some(self);
|
||||
};
|
||||
self.root.get(first).and_then(|child| child.get(rest))
|
||||
}
|
||||
|
||||
/// Get an existing selector node by its path mutably.
|
||||
fn get_mut(&mut self, path: &[PathKey]) -> Option<&mut SelectorNode> {
|
||||
let [first, rest @ ..] = path else {
|
||||
return Some(self);
|
||||
};
|
||||
self.root
|
||||
.get_mut(first)
|
||||
.and_then(|child| child.get_mut(rest))
|
||||
}
|
||||
|
||||
/// Get a selector mutably or create one if it doesn't exist. This is used when subscribing to
|
||||
/// a path that may not exist yet.
|
||||
fn get_mut_or_default(&mut self, path: &[PathKey]) -> &mut SelectorNode {
|
||||
let [first, rest @ ..] = path else {
|
||||
return self;
|
||||
};
|
||||
self.root
|
||||
.entry(*first)
|
||||
.or_default()
|
||||
.get_mut_or_default(rest)
|
||||
}
|
||||
|
||||
/// Visit this node and all of its children in depth-first order, calling the provided function on each node.
|
||||
///
|
||||
/// This is used to mark nodes dirty recursively when a Store is written to.
|
||||
fn visit_depth_first_mut(&mut self, f: &mut dyn FnMut(&mut SelectorNode)) {
|
||||
f(self);
|
||||
for child in self.root.values_mut() {
|
||||
child.visit_depth_first_mut(f);
|
||||
}
|
||||
}
|
||||
|
||||
/// Mark this selector and all children as dirty. This should be called any time a raw mutable reference to a store
|
||||
/// is exposed to the user. They could write to any level of the store, so we need to mark all nodes as dirty.
|
||||
fn mark_children_dirty(&mut self, path: &[PathKey]) {
|
||||
let Some(node) = self.get_mut(path) else {
|
||||
return;
|
||||
};
|
||||
|
||||
// Mark the node and all its children as dirty
|
||||
node.visit_depth_first_mut(&mut |node| {
|
||||
node.mark_dirty();
|
||||
});
|
||||
}
|
||||
|
||||
/// Mark only children after a certain index as dirty. This is used when inserting a new item into a list.
|
||||
/// Items after the index that is inserted need to be marked dirty because the value that index points to may have changed.
|
||||
fn mark_dirty_at_and_after_index(&mut self, path: &[PathKey], index: usize) {
|
||||
let Some(node) = self.get_mut(path) else {
|
||||
return;
|
||||
};
|
||||
|
||||
// Mark the nodes before the index as dirty
|
||||
for (i, child) in node.root.iter_mut() {
|
||||
if *i as usize >= index {
|
||||
child.visit_depth_first_mut(&mut |node| {
|
||||
node.mark_dirty();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Mark a specific node as dirty without marking its children. This is used for data structures like HashMaps
|
||||
/// when inserting or removing items. Inserting an item into a HashMap only changes the length of the map and the
|
||||
/// specific value that was inserted or removed.
|
||||
fn mark_dirty_shallow(&mut self, path: &[PathKey]) {
|
||||
let Some(node) = self.get_mut(path) else {
|
||||
return;
|
||||
};
|
||||
|
||||
node.mark_dirty();
|
||||
}
|
||||
|
||||
/// Mark this node as dirty, which will notify all subscribers that the value has changed.
|
||||
fn mark_dirty(&mut self) {
|
||||
// We cannot hold the subscribers lock while calling mark_dirty, because mark_dirty can run user code which may cause a new subscriber to be added. If we hold the lock, we will deadlock.
|
||||
#[allow(clippy::mutable_key_type)]
|
||||
let mut subscribers = std::mem::take(&mut self.subscribers);
|
||||
subscribers.retain(|reactive_context| reactive_context.mark_dirty());
|
||||
// Extend the subscribers list instead of overwriting it in case a subscriber is added while reactive contexts are marked dirty
|
||||
self.subscribers.extend(subscribers);
|
||||
}
|
||||
|
||||
/// Remove a path from the subscription tree
|
||||
fn remove(&mut self, path: &[PathKey]) {
|
||||
let [first, rest @ ..] = path else {
|
||||
return;
|
||||
};
|
||||
if let Some(node) = self.root.get_mut(first) {
|
||||
if rest.is_empty() {
|
||||
self.root.remove(first);
|
||||
} else {
|
||||
node.remove(rest);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) type PathKey = u16;
|
||||
#[cfg(feature = "large-path")]
|
||||
const PATH_LENGTH: usize = 32;
|
||||
#[cfg(not(feature = "large-path"))]
|
||||
const PATH_LENGTH: usize = 16;
|
||||
|
||||
#[derive(Copy, Clone, PartialEq)]
|
||||
pub(crate) struct TinyVec {
|
||||
length: usize,
|
||||
path: [PathKey; PATH_LENGTH],
|
||||
}
|
||||
|
||||
impl Default for TinyVec {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for TinyVec {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("TinyVec")
|
||||
.field("path", &&self.path[..self.length])
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl TinyVec {
|
||||
pub(crate) const fn new() -> Self {
|
||||
Self {
|
||||
length: 0,
|
||||
path: [0; PATH_LENGTH],
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) const fn push(&mut self, index: u16) {
|
||||
if self.length < self.path.len() {
|
||||
self.path[self.length] = index;
|
||||
self.length += 1;
|
||||
} else {
|
||||
panic!("SelectorPath is full");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for TinyVec {
|
||||
type Target = [u16];
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.path[..self.length]
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct StoreSubscriptionsInner {
|
||||
root: SelectorNode,
|
||||
hasher: std::collections::hash_map::RandomState,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct StoreSubscriptions {
|
||||
inner: CopyValue<StoreSubscriptionsInner, SyncStorage>,
|
||||
}
|
||||
|
||||
impl Clone for StoreSubscriptions {
|
||||
fn clone(&self) -> Self {
|
||||
*self
|
||||
}
|
||||
}
|
||||
|
||||
impl Copy for StoreSubscriptions {}
|
||||
|
||||
impl PartialEq for StoreSubscriptions {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.inner == other.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl StoreSubscriptions {
|
||||
/// Create a new instance of StoreSubscriptions.
|
||||
pub(crate) fn new() -> Self {
|
||||
Self {
|
||||
inner: CopyValue::new_maybe_sync(StoreSubscriptionsInner {
|
||||
root: SelectorNode::default(),
|
||||
hasher: std::collections::hash_map::RandomState::new(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Hash an index into a PathKey using the hasher. The hash should be consistent
|
||||
/// across calls
|
||||
pub(crate) fn hash(&self, index: &impl Hash) -> PathKey {
|
||||
(self.inner.write_unchecked().hasher.hash_one(index) % PathKey::MAX as u64) as PathKey
|
||||
}
|
||||
|
||||
/// Subscribe to a specific path in the store.
|
||||
pub(crate) fn track(&self, key: &[PathKey]) {
|
||||
if let Some(rc) = ReactiveContext::current() {
|
||||
let subscribers = self.subscribers(key);
|
||||
rc.subscribe(subscribers);
|
||||
}
|
||||
}
|
||||
|
||||
/// Subscribe to a path and all of its children recursively. This should be called any time we give out
|
||||
/// a raw reference to a store, because the user could read any level of the store.
|
||||
pub(crate) fn track_recursive(&self, key: &[PathKey]) {
|
||||
if let Some(rc) = ReactiveContext::current() {
|
||||
let mut paths = Vec::new();
|
||||
{
|
||||
let mut write = self.inner.write_unchecked();
|
||||
|
||||
let root = write.root.get_mut_or_default(key);
|
||||
let mut nodes = vec![(key.to_vec(), &*root)];
|
||||
while let Some((path, node)) = nodes.pop() {
|
||||
for (child_key, child_node) in &node.root {
|
||||
let mut new_path = path.clone();
|
||||
new_path.push(*child_key);
|
||||
nodes.push((new_path, child_node));
|
||||
}
|
||||
paths.push(path);
|
||||
}
|
||||
}
|
||||
for path in paths {
|
||||
let subscribers = self.subscribers(&path);
|
||||
rc.subscribe(subscribers);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn mark_dirty(&self, key: &[PathKey]) {
|
||||
self.inner.write_unchecked().root.mark_children_dirty(key);
|
||||
}
|
||||
|
||||
pub(crate) fn mark_dirty_shallow(&self, key: &[PathKey]) {
|
||||
self.inner.write_unchecked().root.mark_dirty_shallow(key);
|
||||
}
|
||||
|
||||
pub(crate) fn mark_dirty_at_and_after_index(&self, key: &[PathKey], index: usize) {
|
||||
self.inner
|
||||
.write_unchecked()
|
||||
.root
|
||||
.mark_dirty_at_and_after_index(key, index);
|
||||
}
|
||||
|
||||
/// Get a subscriber list for a specific path in the store. This is used to subscribe to changes
|
||||
/// to a specific path in the store and remove the node from the subscription tree when it is no longer needed.
|
||||
pub(crate) fn subscribers(&self, key: &[PathKey]) -> Subscribers {
|
||||
Arc::new(StoreSubscribers {
|
||||
subscriptions: *self,
|
||||
path: key.to_vec().into_boxed_slice(),
|
||||
})
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
/// A subscriber list implementation that handles garbage collection of the subscription tree.
|
||||
struct StoreSubscribers {
|
||||
subscriptions: StoreSubscriptions,
|
||||
path: Box<[PathKey]>,
|
||||
}
|
||||
|
||||
impl SubscriberList for StoreSubscribers {
|
||||
/// Add a subscriber to the subscription list for this path in the store, creating the node if it doesn't exist.
|
||||
fn add(&self, subscriber: ReactiveContext) {
|
||||
let Ok(mut write) = self.subscriptions.inner.try_write_unchecked() else {
|
||||
return;
|
||||
};
|
||||
let node = write.root.get_mut_or_default(&self.path);
|
||||
node.subscribers.insert(subscriber);
|
||||
}
|
||||
|
||||
/// Remove a subscriber from the subscription list for this path in the store. If the node has no subscribers left
|
||||
/// remove that node from the subscription tree.
|
||||
fn remove(&self, subscriber: &ReactiveContext) {
|
||||
let Ok(mut write) = self.subscriptions.inner.try_write_unchecked() else {
|
||||
return;
|
||||
};
|
||||
let Some(node) = write.root.get_mut(&self.path) else {
|
||||
return;
|
||||
};
|
||||
node.subscribers.remove(subscriber);
|
||||
if node.subscribers.is_empty() && node.root.is_empty() {
|
||||
write.root.remove(&self.path);
|
||||
}
|
||||
}
|
||||
|
||||
/// Visit all subscribers for this path in the store, calling the provided function on each subscriber.
|
||||
fn visit(&self, f: &mut dyn FnMut(&ReactiveContext)) {
|
||||
let Ok(read) = self.subscriptions.inner.try_read() else {
|
||||
return;
|
||||
};
|
||||
let Some(node) = read.root.get(&self.path) else {
|
||||
return;
|
||||
};
|
||||
node.subscribers.iter().for_each(f);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user