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,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()
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user