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

+1 -1
View File
@@ -3,7 +3,7 @@ use crate::read::ReadableExt;
use crate::read_impls;
use crate::Memo;
impl<T: PartialEq> InitializeFromFunction<T> for Memo<T> {
impl<T: PartialEq + 'static> InitializeFromFunction<T> for Memo<T> {
fn initialize_from_function(f: fn() -> T) -> Self {
Memo::new(f)
}
+31 -15
View File
@@ -32,9 +32,10 @@ pub struct Global<T, R = T> {
/// Allow calling a signal with signal() syntax
///
/// Currently only limited to copy types, though could probably specialize for string/arc/rc
impl<T: Clone + 'static, R: Clone + 'static> Deref for Global<T, R>
impl<T: Clone, R: Clone> Deref for Global<T, R>
where
T: Readable<Target = R> + InitializeFromFunction<R>,
T: Readable<Target = R> + InitializeFromFunction<R> + 'static,
T::Target: 'static,
{
type Target = dyn Fn() -> R;
@@ -43,9 +44,9 @@ where
}
}
impl<T: Clone + 'static, R: 'static> Readable for Global<T, R>
impl<T, R> Readable for Global<T, R>
where
T: Readable<Target = R> + InitializeFromFunction<R>,
T: Readable<Target = R> + InitializeFromFunction<R> + Clone + 'static,
{
type Target = R;
type Storage = T::Storage;
@@ -53,23 +54,32 @@ where
#[track_caller]
fn try_read_unchecked(
&self,
) -> Result<ReadableRef<'static, Self>, generational_box::BorrowError> {
) -> Result<ReadableRef<'static, Self>, generational_box::BorrowError>
where
R: 'static,
{
self.resolve().try_read_unchecked()
}
#[track_caller]
fn try_peek_unchecked(&self) -> BorrowResult<ReadableRef<'static, Self>> {
fn try_peek_unchecked(&self) -> BorrowResult<ReadableRef<'static, Self>>
where
R: 'static,
{
self.resolve().try_peek_unchecked()
}
fn subscribers(&self) -> Option<Subscribers> {
fn subscribers(&self) -> Subscribers
where
R: 'static,
{
self.resolve().subscribers()
}
}
impl<T: Clone + 'static, R: 'static> Writable for Global<T, R>
impl<T: Clone, R> Writable for Global<T, R>
where
T: Writable<Target = R> + InitializeFromFunction<R>,
T: Writable<Target = R> + InitializeFromFunction<R> + 'static,
{
type WriteMetadata = T::WriteMetadata;
@@ -81,9 +91,9 @@ where
}
}
impl<T: Clone + 'static, R: 'static> Global<T, R>
impl<T: Clone, R> Global<T, R>
where
T: Writable<Target = R> + InitializeFromFunction<R>,
T: Writable<Target = R> + InitializeFromFunction<R> + 'static,
{
/// Write this value
pub fn write(&self) -> WritableRef<'static, T, R> {
@@ -93,12 +103,15 @@ where
/// Run a closure with a mutable reference to the signal's value.
/// If the signal has been dropped, this will panic.
#[track_caller]
pub fn with_mut<O>(&self, f: impl FnOnce(&mut R) -> O) -> O {
pub fn with_mut<O>(&self, f: impl FnOnce(&mut R) -> O) -> O
where
T::Target: 'static,
{
self.resolve().with_mut(f)
}
}
impl<T: Clone + 'static, R> Global<T, R>
impl<T: Clone, R> Global<T, R>
where
T: InitializeFromFunction<R>,
{
@@ -160,7 +173,10 @@ where
/// Resolve the global value. This will try to get the existing value from the current virtual dom, and if it doesn't exist, it will create a new one.
// NOTE: This is not called "get" or "value" because those methods overlap with Readable and Writable
pub fn resolve(&self) -> T {
pub fn resolve(&self) -> T
where
T: 'static,
{
let key = self.key();
let context = get_global_context();
@@ -237,7 +253,7 @@ impl From<&'static Location<'static>> for GlobalKey<'static> {
impl GlobalLazyContext {
/// Get a signal with the given string key
/// The key will be converted to a UUID with the appropriate internal namespace
pub fn get_signal_with_key<T>(&self, key: GlobalKey) -> Option<Signal<T>> {
pub fn get_signal_with_key<T: 'static>(&self, key: GlobalKey) -> Option<Signal<T>> {
self.map.borrow().get(&key).map(|f| {
*f.downcast_ref::<Signal<T>>().unwrap_or_else(|| {
panic!(
+1 -1
View File
@@ -3,7 +3,7 @@ use crate::read::ReadableExt;
use crate::read_impls;
use crate::Signal;
impl<T> InitializeFromFunction<T> for Signal<T> {
impl<T: 'static> InitializeFromFunction<T> for Signal<T> {
fn initialize_from_function(f: fn() -> T) -> Self {
Signal::new(f())
}