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:
Evan AlmloffandJonathan Kelley authored and GitHub committed 2025-08-05 11:30:21 -07:00
1 parent 5591070e92
commit 58c41f8561
57 files changed
+4958 -436

No files matched your search

+34 -26
View File
@@ -117,11 +117,6 @@ impl<T, S: Storage<T>> GenerationalBox<T, S> {
*self.write() = value;
}
/// Returns true if the pointer is equal to the other pointer.
pub fn ptr_eq(&self, other: &Self) -> bool {
self.raw == other.raw
}
/// Drop the value out of the generational box and invalidate the generational box.
pub fn manually_drop(&self)
where
@@ -130,11 +125,6 @@ impl<T, S: Storage<T>> GenerationalBox<T, S> {
self.raw.recycle();
}
/// Try to get the location the generational box was created at. In release mode this will always return None.
pub fn created_at(&self) -> Option<&'static std::panic::Location<'static>> {
self.raw.location.created_at()
}
/// Get a reference to the value
#[track_caller]
pub fn leak_reference(&self) -> BorrowResult<GenerationalBox<T, S>> {
@@ -150,6 +140,21 @@ impl<T, S: Storage<T>> GenerationalBox<T, S> {
}
}
impl<T, S> GenerationalBox<T, S> {
/// Returns true if the pointer is equal to the other pointer.
pub fn ptr_eq(&self, other: &Self) -> bool
where
S: AnyStorage,
{
self.raw == other.raw
}
/// Try to get the location the generational box was created at. In release mode this will always return None.
pub fn created_at(&self) -> Option<&'static std::panic::Location<'static>> {
self.raw.location.created_at()
}
}
impl<T, S> Copy for GenerationalBox<T, S> {}
impl<T, S> Clone for GenerationalBox<T, S> {
@@ -159,7 +164,7 @@ impl<T, S> Clone for GenerationalBox<T, S> {
}
/// A trait for a storage backing type. (RefCell, RwLock, etc.)
pub trait Storage<Data = ()>: AnyStorage + 'static {
pub trait Storage<Data = ()>: AnyStorage {
/// Try to read the value. Returns None if the value is no longer valid.
fn try_read(pointer: GenerationalPointer<Self>) -> BorrowResult<Self::Ref<'static, Data>>;
@@ -193,34 +198,34 @@ pub trait Storage<Data = ()>: AnyStorage + 'static {
}
/// A trait for any storage backing type.
pub trait AnyStorage: Default + 'static {
pub trait AnyStorage: Default {
/// The reference this storage type returns.
type Ref<'a, T: ?Sized + 'static>: Deref<Target = T>;
type Ref<'a, T: ?Sized + 'a>: Deref<Target = T>;
/// The mutable reference this storage type returns.
type Mut<'a, T: ?Sized + 'static>: DerefMut<Target = T>;
type Mut<'a, T: ?Sized + 'a>: DerefMut<Target = T>;
/// Downcast a reference in a Ref to a more specific lifetime
///
/// This function enforces the variance of the lifetime parameter `'a` in Ref. Rust will typically infer this cast with a concrete type, but it cannot with a generic type.
fn downcast_lifetime_ref<'a: 'b, 'b, T: ?Sized + 'static>(
fn downcast_lifetime_ref<'a: 'b, 'b, T: ?Sized + 'a>(
ref_: Self::Ref<'a, T>,
) -> Self::Ref<'b, T>;
/// Downcast a mutable reference in a RefMut to a more specific lifetime
///
/// This function enforces the variance of the lifetime parameter `'a` in Mut. Rust will typically infer this cast with a concrete type, but it cannot with a generic type.
fn downcast_lifetime_mut<'a: 'b, 'b, T: ?Sized + 'static>(
fn downcast_lifetime_mut<'a: 'b, 'b, T: ?Sized + 'a>(
mut_: Self::Mut<'a, T>,
) -> Self::Mut<'b, T>;
/// Try to map the mutable ref.
fn try_map_mut<T: ?Sized + 'static, U: ?Sized + 'static>(
fn try_map_mut<T: ?Sized, U: ?Sized>(
mut_ref: Self::Mut<'_, T>,
f: impl FnOnce(&mut T) -> Option<&mut U>,
) -> Option<Self::Mut<'_, U>>;
/// Map the mutable ref.
fn map_mut<T: ?Sized + 'static, U: ?Sized + 'static>(
fn map_mut<T: ?Sized, U: ?Sized>(
mut_ref: Self::Mut<'_, T>,
f: impl FnOnce(&mut T) -> &mut U,
) -> Self::Mut<'_, U> {
@@ -228,13 +233,13 @@ pub trait AnyStorage: Default + 'static {
}
/// Try to map the ref.
fn try_map<T: ?Sized, U: ?Sized + 'static>(
fn try_map<T: ?Sized, U: ?Sized>(
ref_: Self::Ref<'_, T>,
f: impl FnOnce(&T) -> Option<&U>,
) -> Option<Self::Ref<'_, U>>;
/// Map the ref.
fn map<T: ?Sized, U: ?Sized + 'static>(
fn map<T: ?Sized, U: ?Sized>(
ref_: Self::Ref<'_, T>,
f: impl FnOnce(&T) -> &U,
) -> Self::Ref<'_, U> {
@@ -248,7 +253,10 @@ pub trait AnyStorage: Default + 'static {
fn recycle(location: GenerationalPointer<Self>);
/// Create a new owner. The owner will be responsible for dropping all of the generational boxes that it creates.
fn owner() -> Owner<Self> {
fn owner() -> Owner<Self>
where
Self: 'static,
{
Owner(Arc::new(Mutex::new(OwnerInner {
owned: Default::default(),
})))
@@ -374,7 +382,7 @@ impl<S: AnyStorage> Clone for Owner<S> {
impl<S: AnyStorage> Owner<S> {
/// Insert a value into the store. The value will be dropped when the owner is dropped.
#[track_caller]
pub fn insert<T: 'static>(&self, value: T) -> GenerationalBox<T, S>
pub fn insert<T>(&self, value: T) -> GenerationalBox<T, S>
where
S: Storage<T>,
{
@@ -383,7 +391,7 @@ impl<S: AnyStorage> Owner<S> {
/// Create a new reference counted box. The box will be dropped when all references are dropped.
#[track_caller]
pub fn insert_rc<T: 'static>(&self, value: T) -> GenerationalBox<T, S>
pub fn insert_rc<T>(&self, value: T) -> GenerationalBox<T, S>
where
S: Storage<T>,
{
@@ -391,7 +399,7 @@ impl<S: AnyStorage> Owner<S> {
}
/// Insert a value into the store with a specific location blamed for creating the value. The value will be dropped when the owner is dropped.
pub fn insert_rc_with_caller<T: 'static>(
pub fn insert_rc_with_caller<T>(
&self,
value: T,
caller: &'static std::panic::Location<'static>,
@@ -408,7 +416,7 @@ impl<S: AnyStorage> Owner<S> {
}
/// Insert a value into the store with a specific location blamed for creating the value. The value will be dropped when the owner is dropped.
pub fn insert_with_caller<T: 'static>(
pub fn insert_with_caller<T>(
&self,
value: T,
caller: &'static std::panic::Location<'static>,
@@ -428,7 +436,7 @@ impl<S: AnyStorage> Owner<S> {
///
/// This method may return an error if the other box is no longer valid or it is already borrowed mutably.
#[track_caller]
pub fn insert_reference<T: 'static>(
pub fn insert_reference<T>(
&self,
other: GenerationalBox<T, S>,
) -> BorrowResult<GenerationalBox<T, S>>
+3 -3
View File
@@ -84,7 +84,7 @@ pub struct GenerationalRef<R> {
guard: GenerationalRefBorrowGuard,
}
impl<T: ?Sized + 'static, R: Deref<Target = T>> GenerationalRef<R> {
impl<T: ?Sized, R: Deref<Target = T>> GenerationalRef<R> {
pub(crate) fn new(inner: R, guard: GenerationalRefBorrowGuard) -> Self {
Self { inner, guard }
}
@@ -118,7 +118,7 @@ impl<T: ?Sized + Display, R: Deref<Target = T>> Display for GenerationalRef<R> {
}
}
impl<T: ?Sized + 'static, R: Deref<Target = T>> Deref for GenerationalRef<R> {
impl<T: ?Sized, R: Deref<Target = T>> Deref for GenerationalRef<R> {
type Target = T;
fn deref(&self) -> &Self::Target {
@@ -218,7 +218,7 @@ pub struct GenerationalRefMut<W> {
pub(crate) borrow: GenerationalRefBorrowMutGuard,
}
impl<T: ?Sized + 'static, R: DerefMut<Target = T>> GenerationalRefMut<R> {
impl<T: ?Sized, R: DerefMut<Target = T>> GenerationalRefMut<R> {
pub(crate) fn new(inner: R, borrow: GenerationalRefBorrowMutGuard) -> Self {
Self { inner, borrow }
}
+8 -8
View File
@@ -178,43 +178,43 @@ fn sync_runtime() -> &'static Arc<Mutex<Vec<&'static SyncStorage>>> {
}
impl AnyStorage for SyncStorage {
type Ref<'a, R: ?Sized + 'static> = GenerationalRef<MappedRwLockReadGuard<'a, R>>;
type Mut<'a, W: ?Sized + 'static> = GenerationalRefMut<MappedRwLockWriteGuard<'a, W>>;
type Ref<'a, R: ?Sized + 'a> = GenerationalRef<MappedRwLockReadGuard<'a, R>>;
type Mut<'a, W: ?Sized + 'a> = GenerationalRefMut<MappedRwLockWriteGuard<'a, W>>;
fn downcast_lifetime_ref<'a: 'b, 'b, T: ?Sized + 'static>(
fn downcast_lifetime_ref<'a: 'b, 'b, T: ?Sized + 'b>(
ref_: Self::Ref<'a, T>,
) -> Self::Ref<'b, T> {
ref_
}
fn downcast_lifetime_mut<'a: 'b, 'b, T: ?Sized + 'static>(
fn downcast_lifetime_mut<'a: 'b, 'b, T: ?Sized + 'a>(
mut_: Self::Mut<'a, T>,
) -> Self::Mut<'b, T> {
mut_
}
fn map<T: ?Sized + 'static, U: ?Sized + 'static>(
fn map<T: ?Sized, U: ?Sized>(
ref_: Self::Ref<'_, T>,
f: impl FnOnce(&T) -> &U,
) -> Self::Ref<'_, U> {
ref_.map(|inner| MappedRwLockReadGuard::map(inner, f))
}
fn map_mut<T: ?Sized + 'static, U: ?Sized + 'static>(
fn map_mut<T: ?Sized, U: ?Sized>(
mut_ref: Self::Mut<'_, T>,
f: impl FnOnce(&mut T) -> &mut U,
) -> Self::Mut<'_, U> {
mut_ref.map(|inner| MappedRwLockWriteGuard::map(inner, f))
}
fn try_map<I: ?Sized + 'static, U: ?Sized + 'static>(
fn try_map<I: ?Sized, U: ?Sized>(
ref_: Self::Ref<'_, I>,
f: impl FnOnce(&I) -> Option<&U>,
) -> Option<Self::Ref<'_, U>> {
ref_.try_map(|inner| MappedRwLockReadGuard::try_map(inner, f).ok())
}
fn try_map_mut<I: ?Sized + 'static, U: ?Sized + 'static>(
fn try_map_mut<I: ?Sized, U: ?Sized>(
mut_ref: Self::Mut<'_, I>,
f: impl FnOnce(&mut I) -> Option<&mut U>,
) -> Option<Self::Mut<'_, U>> {
+8 -8
View File
@@ -174,43 +174,43 @@ impl UnsyncStorage {
}
impl AnyStorage for UnsyncStorage {
type Ref<'a, R: ?Sized + 'static> = GenerationalRef<Ref<'a, R>>;
type Mut<'a, W: ?Sized + 'static> = GenerationalRefMut<RefMut<'a, W>>;
type Ref<'a, R: ?Sized + 'a> = GenerationalRef<Ref<'a, R>>;
type Mut<'a, W: ?Sized + 'a> = GenerationalRefMut<RefMut<'a, W>>;
fn downcast_lifetime_ref<'a: 'b, 'b, T: ?Sized + 'static>(
fn downcast_lifetime_ref<'a: 'b, 'b, T: ?Sized + 'a>(
ref_: Self::Ref<'a, T>,
) -> Self::Ref<'b, T> {
ref_
}
fn downcast_lifetime_mut<'a: 'b, 'b, T: ?Sized + 'static>(
fn downcast_lifetime_mut<'a: 'b, 'b, T: ?Sized + 'a>(
mut_: Self::Mut<'a, T>,
) -> Self::Mut<'b, T> {
mut_
}
fn map<T: ?Sized + 'static, U: ?Sized + 'static>(
fn map<T: ?Sized, U: ?Sized>(
ref_: Self::Ref<'_, T>,
f: impl FnOnce(&T) -> &U,
) -> Self::Ref<'_, U> {
ref_.map(|inner| Ref::map(inner, f))
}
fn map_mut<T: ?Sized + 'static, U: ?Sized + 'static>(
fn map_mut<T: ?Sized, U: ?Sized>(
mut_ref: Self::Mut<'_, T>,
f: impl FnOnce(&mut T) -> &mut U,
) -> Self::Mut<'_, U> {
mut_ref.map(|inner| RefMut::map(inner, f))
}
fn try_map<I: ?Sized + 'static, U: ?Sized + 'static>(
fn try_map<I: ?Sized, U: ?Sized>(
_self: Self::Ref<'_, I>,
f: impl FnOnce(&I) -> Option<&U>,
) -> Option<Self::Ref<'_, U>> {
_self.try_map(|inner| Ref::filter_map(inner, f).ok())
}
fn try_map_mut<I: ?Sized + 'static, U: ?Sized + 'static>(
fn try_map_mut<I: ?Sized, U: ?Sized>(
mut_ref: Self::Mut<'_, I>,
f: impl FnOnce(&mut I) -> Option<&mut U>,
) -> Option<Self::Mut<'_, U>> {