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

+97 -80
View File
@@ -12,7 +12,7 @@ use crate::{
pub type ReadOnlySignal<T> = ReadSignal<T>;
/// A boxed version of [Readable] that can be used to store any readable type.
pub struct ReadSignal<T: ?Sized + 'static> {
pub struct ReadSignal<T: ?Sized> {
value: CopyValue<Box<dyn Readable<Target = T, Storage = UnsyncStorage>>>,
}
@@ -25,45 +25,42 @@ impl<T: ?Sized + 'static> ReadSignal<T> {
}
/// Point to another [ReadSignal]. This will subscribe the other [ReadSignal] to all subscribers of this [ReadSignal].
pub fn point_to(&self, other: Self) -> BorrowResult
where
T: Sized + 'static,
{
#[allow(clippy::mutable_key_type)]
pub fn point_to(&self, other: Self) -> BorrowResult {
let this_subscribers = self.subscribers();
let mut this_subscribers_vec = Vec::new();
// Note we don't subscribe directly in the visit closure to avoid a deadlock when pointing to self
this_subscribers.visit(|subscriber| this_subscribers_vec.push(*subscriber));
let other_subscribers = other.subscribers();
if let (Some(this_subscribers), Some(other_subscribers)) =
(this_subscribers, other_subscribers)
{
this_subscribers.visit(|subscriber| {
subscriber.subscribe(other_subscribers.clone());
});
for subscriber in this_subscribers_vec {
subscriber.subscribe(other_subscribers.clone());
}
self.value.point_to(other.value)
self.value.point_to(other.value)?;
Ok(())
}
#[doc(hidden)]
/// This is only used by the `props` macro.
/// Mark any readers of the signal as dirty
pub fn mark_dirty(&mut self) {
let subscribers = self.value.subscribers();
if let Some(subscribers) = subscribers {
subscribers.visit(|subscriber| {
subscriber.mark_dirty();
});
let subscribers = self.subscribers();
let mut this_subscribers_vec = Vec::new();
subscribers.visit(|subscriber| this_subscribers_vec.push(*subscriber));
for subscriber in this_subscribers_vec {
subscribers.remove(&subscriber);
subscriber.mark_dirty();
}
}
}
impl<T: ?Sized + 'static> Clone for ReadSignal<T> {
impl<T: ?Sized> Clone for ReadSignal<T> {
fn clone(&self) -> Self {
*self
}
}
impl<T: ?Sized + 'static> Copy for ReadSignal<T> {}
impl<T: ?Sized> Copy for ReadSignal<T> {}
impl<T: ?Sized + 'static> PartialEq for ReadSignal<T> {
impl<T: ?Sized> PartialEq for ReadSignal<T> {
fn eq(&self, other: &Self) -> bool {
self.value == other.value
}
@@ -103,14 +100,17 @@ impl<T: Clone + 'static> Deref for ReadSignal<T> {
}
}
impl<T: 'static> Readable for ReadSignal<T> {
impl<T: ?Sized> Readable for ReadSignal<T> {
type Target = T;
type Storage = UnsyncStorage;
#[track_caller]
fn try_read_unchecked(
&self,
) -> Result<ReadableRef<'static, Self>, generational_box::BorrowError> {
) -> Result<ReadableRef<'static, Self>, generational_box::BorrowError>
where
T: 'static,
{
self.value
.try_peek_unchecked()
.unwrap()
@@ -118,39 +118,46 @@ impl<T: 'static> Readable for ReadSignal<T> {
}
#[track_caller]
fn try_peek_unchecked(&self) -> BorrowResult<ReadableRef<'static, Self>> {
fn try_peek_unchecked(&self) -> BorrowResult<ReadableRef<'static, Self>>
where
T: 'static,
{
self.value
.try_peek_unchecked()
.unwrap()
.try_peek_unchecked()
}
fn subscribers(&self) -> Option<Subscribers> {
self.value.subscribers()
fn subscribers(&self) -> Subscribers
where
T: 'static,
{
self.value.try_peek_unchecked().unwrap().subscribers()
}
}
// We can't implement From<impl Readable<Target = T, Storage = S> + 'static> for ReadSignal<T, S>
// We can't implement From<impl Readable<Target = T, Storage = S> > for ReadSignal<T, S>
// because it would conflict with the From<T> for T implementation, but we can implement it for
// all specific readable types
impl<T> From<Signal<T>> for ReadSignal<T> {
impl<T: 'static> From<Signal<T>> for ReadSignal<T> {
fn from(value: Signal<T>) -> Self {
Self::new(value)
}
}
impl<T: PartialEq> From<Memo<T>> for ReadSignal<T> {
impl<T: PartialEq + 'static> From<Memo<T>> for ReadSignal<T> {
fn from(value: Memo<T>) -> Self {
Self::new(value)
}
}
impl<T> From<CopyValue<T>> for ReadSignal<T> {
impl<T: 'static> From<CopyValue<T>> for ReadSignal<T> {
fn from(value: CopyValue<T>) -> Self {
Self::new(value)
}
}
impl<T: Clone + 'static, R: 'static> From<Global<T, R>> for ReadSignal<R>
impl<T, R> From<Global<T, R>> for ReadSignal<R>
where
T: Readable<Target = R, Storage = UnsyncStorage> + InitializeFromFunction<R>,
T: Readable<Target = R, Storage = UnsyncStorage> + InitializeFromFunction<R> + Clone + 'static,
R: 'static,
{
fn from(value: Global<T, R>) -> Self {
Self::new(value)
@@ -158,7 +165,7 @@ where
}
impl<V, O, F> From<MappedSignal<O, V, F>> for ReadSignal<O>
where
O: ?Sized,
O: ?Sized + 'static,
V: Readable<Storage = UnsyncStorage> + 'static,
F: Fn(&V::Target) -> &O + 'static,
{
@@ -168,7 +175,7 @@ where
}
impl<V, O, F, FMut> From<MappedMutSignal<O, V, F, FMut>> for ReadSignal<O>
where
O: ?Sized,
O: ?Sized + 'static,
V: Readable<Storage = UnsyncStorage> + 'static,
F: Fn(&V::Target) -> &O + 'static,
FMut: 'static,
@@ -177,18 +184,23 @@ where
Self::new(value)
}
}
impl<T: ?Sized + 'static> From<WriteSignal<T>> for ReadSignal<T> {
fn from(value: WriteSignal<T>) -> Self {
Self::new(value)
}
}
/// A boxed version of [Writable] that can be used to store any writable type.
pub struct WriteSignal<T: ?Sized + 'static> {
pub struct WriteSignal<T: ?Sized> {
value: CopyValue<
Box<dyn Writable<Target = T, Storage = UnsyncStorage, WriteMetadata = Box<dyn Any>>>,
>,
}
impl<T: ?Sized> WriteSignal<T> {
impl<T: ?Sized + 'static> WriteSignal<T> {
/// Create a new boxed writable value.
pub fn new<M>(
value: impl Writable<Target = T, Storage = UnsyncStorage, WriteMetadata = M> + 'static,
pub fn new(
value: impl Writable<Target = T, Storage = UnsyncStorage, WriteMetadata: 'static> + 'static,
) -> Self {
Self {
value: CopyValue::new(Box::new(BoxWriteMetadata::new(value))),
@@ -213,51 +225,47 @@ impl<W: Readable> Readable for BoxWriteMetadata<W> {
fn try_read_unchecked(
&self,
) -> Result<ReadableRef<'static, Self>, generational_box::BorrowError> {
) -> Result<ReadableRef<'static, Self>, generational_box::BorrowError>
where
W::Target: 'static,
{
self.value.try_read_unchecked()
}
fn try_peek_unchecked(
&self,
) -> Result<ReadableRef<'static, Self>, generational_box::BorrowError> {
) -> Result<ReadableRef<'static, Self>, generational_box::BorrowError>
where
W::Target: 'static,
{
self.value.try_peek_unchecked()
}
fn subscribers(&self) -> Option<Subscribers> {
fn subscribers(&self) -> Subscribers
where
W::Target: 'static,
{
self.value.subscribers()
}
}
impl<W: Writable> Writable for BoxWriteMetadata<W> {
impl<W> Writable for BoxWriteMetadata<W>
where
W: Writable,
W::WriteMetadata: 'static,
{
type WriteMetadata = Box<dyn Any>;
fn try_write_unchecked(
&self,
) -> Result<crate::WritableRef<'static, Self>, generational_box::BorrowMutError> {
) -> Result<crate::WritableRef<'static, Self>, generational_box::BorrowMutError>
where
W::Target: 'static,
{
self.value
.try_write_unchecked()
.map(|w| w.map_metadata(|data| Box::new(data) as Box<dyn Any>))
}
fn write(&mut self) -> crate::WritableRef<'_, Self> {
self.value
.write()
.map_metadata(|data| Box::new(data) as Box<dyn Any>)
}
fn try_write(
&mut self,
) -> Result<crate::WritableRef<'_, Self>, generational_box::BorrowMutError> {
self.value
.try_write()
.map(|w| w.map_metadata(|data| Box::new(data) as Box<dyn Any>))
}
fn write_unchecked(&self) -> crate::WritableRef<'static, Self> {
self.value
.write_unchecked()
.map_metadata(|data| Box::new(data) as Box<dyn Any>)
}
}
impl<T: ?Sized> Clone for WriteSignal<T> {
@@ -303,14 +311,17 @@ impl<T: Clone + 'static> Deref for WriteSignal<T> {
}
}
impl<T: 'static> Readable for WriteSignal<T> {
impl<T: ?Sized> Readable for WriteSignal<T> {
type Target = T;
type Storage = UnsyncStorage;
#[track_caller]
fn try_read_unchecked(
&self,
) -> Result<ReadableRef<'static, Self>, generational_box::BorrowError> {
) -> Result<ReadableRef<'static, Self>, generational_box::BorrowError>
where
T: 'static,
{
self.value
.try_peek_unchecked()
.unwrap()
@@ -318,28 +329,33 @@ impl<T: 'static> Readable for WriteSignal<T> {
}
#[track_caller]
fn try_peek_unchecked(&self) -> BorrowResult<ReadableRef<'static, Self>> {
fn try_peek_unchecked(&self) -> BorrowResult<ReadableRef<'static, Self>>
where
T: 'static,
{
self.value
.try_peek_unchecked()
.unwrap()
.try_peek_unchecked()
}
fn subscribers(&self) -> Option<Subscribers> {
self.value.subscribers()
fn subscribers(&self) -> Subscribers
where
T: 'static,
{
self.value.try_peek_unchecked().unwrap().subscribers()
}
}
impl<T: 'static> Writable for WriteSignal<T> {
impl<T: ?Sized> Writable for WriteSignal<T> {
type WriteMetadata = Box<dyn Any>;
fn write_unchecked(&self) -> crate::WritableRef<'static, Self> {
self.value.try_peek_unchecked().unwrap().write_unchecked()
}
fn try_write_unchecked(
&self,
) -> Result<crate::WritableRef<'static, Self>, generational_box::BorrowMutError> {
) -> Result<crate::WritableRef<'static, Self>, generational_box::BorrowMutError>
where
T: 'static,
{
self.value
.try_peek_unchecked()
.unwrap()
@@ -347,22 +363,23 @@ impl<T: 'static> Writable for WriteSignal<T> {
}
}
// We can't implement From<impl Writable<Target = T, Storage = S> + 'static> for Write<T, S>
// We can't implement From<impl Writable<Target = T, Storage = S>> for Write<T, S>
// because it would conflict with the From<T> for T implementation, but we can implement it for
// all specific readable types
impl<T> From<Signal<T>> for WriteSignal<T> {
impl<T: 'static> From<Signal<T>> for WriteSignal<T> {
fn from(value: Signal<T>) -> Self {
Self::new(value)
}
}
impl<T> From<CopyValue<T>> for WriteSignal<T> {
impl<T: 'static> From<CopyValue<T>> for WriteSignal<T> {
fn from(value: CopyValue<T>) -> Self {
Self::new(value)
}
}
impl<T: Clone + 'static, R: 'static> From<Global<T, R>> for WriteSignal<R>
impl<T, R> From<Global<T, R>> for WriteSignal<R>
where
T: Writable<Target = R, Storage = UnsyncStorage> + InitializeFromFunction<R>,
T: Writable<Target = R, Storage = UnsyncStorage> + InitializeFromFunction<R> + Clone + 'static,
R: 'static,
{
fn from(value: Global<T, R>) -> Self {
Self::new(value)
@@ -370,7 +387,7 @@ where
}
impl<V, O, F, FMut> From<MappedMutSignal<O, V, F, FMut>> for WriteSignal<O>
where
O: ?Sized,
O: ?Sized + 'static,
V: Writable<Storage = UnsyncStorage> + 'static,
F: Fn(&V::Target) -> &O + 'static,
FMut: Fn(&mut V::Target) -> &mut O + 'static,