Bladeren bron

simplify reactive context and expose less signal internals

Evan Almloff 1 jaar geleden
bovenliggende
commit
30bac267c9

+ 5 - 12
packages/signals/src/copy_value.rs

@@ -84,16 +84,9 @@ fn current_owner<S: Storage<T>, T>() -> Owner<S> {
         return owner;
     }
 
-    // If we are inside of an effect, we should use the owner of the effect as the owner of the value.
-    if let Some(scope) = ReactiveContext::current() {
-        return owner_in_scope(scope.origin_scope());
-    }
-
-    // Otherwise either get an owner from the current scope or create a new one.
-    match has_context() {
-        Some(rt) => rt,
-        None => provide_context(S::owner()),
-    }
+    // Otherwise get the owner from the current reactive context.
+    let current_reactive_context = ReactiveContext::current();
+    owner_in_scope(current_reactive_context.origin_scope())
 }
 
 fn owner_in_scope<S: Storage<T>, T>(scope: ScopeId) -> Owner<S> {
@@ -225,8 +218,8 @@ impl<T: 'static, S: Storage<T>> Readable<T> for CopyValue<T, S> {
         S::try_map(ref_, f)
     }
 
-    fn read(&self) -> Self::Ref<T> {
-        self.value.read()
+    fn try_read(&self) -> Result<S::Ref<T>, generational_box::BorrowError> {
+        self.value.try_read()
     }
 
     fn peek(&self) -> Self::Ref<T> {

+ 2 - 2
packages/signals/src/global/memo.rs

@@ -66,8 +66,8 @@ impl<T: PartialEq + 'static> Readable<T> for GlobalMemo<T> {
     }
 
     #[track_caller]
-    fn read(&self) -> Self::Ref<T> {
-        self.signal().read()
+    fn try_read(&self) -> Result<Self::Ref<T>, generational_box::BorrowError> {
+        self.signal().try_read()
     }
 
     #[track_caller]

+ 2 - 2
packages/signals/src/global/signal.rs

@@ -89,8 +89,8 @@ impl<T: 'static> Readable<T> for GlobalSignal<T> {
     }
 
     #[track_caller]
-    fn read(&self) -> Self::Ref<T> {
-        self.signal().read()
+    fn try_read(&self) -> Result<Self::Ref<T>, generational_box::BorrowError> {
+        self.signal().try_read()
     }
 
     #[track_caller]

+ 24 - 37
packages/signals/src/reactive_context.rs

@@ -1,11 +1,11 @@
 use dioxus_core::prelude::{
     current_scope_id, has_context, provide_context, schedule_update_any, ScopeId,
 };
-use generational_box::{GenerationalBoxId, SyncStorage};
-use rustc_hash::{FxHashMap, FxHashSet};
+use generational_box::SyncStorage;
+use rustc_hash::FxHashSet;
 use std::{cell::RefCell, hash::Hash, sync::Arc};
 
-use crate::{CopyValue, RcList, Readable, Writable};
+use crate::{CopyValue, Readable, Writable};
 
 /// A context for signal reads and writes to be directed to
 ///
@@ -33,8 +33,7 @@ impl ReactiveContext {
         }
 
         let inner = Inner {
-            signal_subscribers: FxHashMap::default(),
-            scope_subscribers,
+            scope_subscriber: scope,
             sender: tx,
             self_: None,
             update_any: schedule_update_any(),
@@ -58,21 +57,21 @@ impl ReactiveContext {
     /// If this was set manually, then that value will be returned.
     ///
     /// If there's no current reactive context, then a new one will be created at the current scope and returned.
-    pub fn current() -> Option<Self> {
+    pub fn current() -> Self {
         let cur = CURRENT.with(|current| current.borrow().last().cloned());
 
         // If we're already inside a reactive context, then return that
         if let Some(cur) = cur {
-            return Some(cur);
+            return cur;
         }
 
         // If we're rendering, then try and use the reactive context attached to this component
         if let Some(cx) = has_context() {
-            return Some(cx);
+            return cx;
         }
 
         // Otherwise, create a new context at the current scope
-        Some(provide_context(ReactiveContext::new(current_scope_id())))
+        provide_context(ReactiveContext::new(current_scope_id()))
     }
 
     /// Run this function in the context of this reactive context
@@ -89,23 +88,21 @@ impl ReactiveContext {
     /// Marks this reactive context as dirty
     ///
     /// If there's a scope associated with this context, then it will be marked as dirty too
-    pub fn mark_dirty(&self) {
-        for scope in self.inner.read().scope_subscribers.iter() {
-            (self.inner.read().update_any)(*scope);
+    ///
+    /// Returns true if the context was marked as dirty, or false if the context has been dropped
+    pub fn mark_dirty(&self) -> bool {
+        if let Ok(self_read) = self.inner.try_read() {
+            if let Some(scope) = self_read.scope_subscriber {
+                (self_read.update_any)(scope);
+            }
+
+            // mark the listeners as dirty
+            // If the channel is full it means that the receivers have already been marked as dirty
+            _ = self_read.sender.try_send(());
+            true
+        } else {
+            false
         }
-
-        // mark the listeners as dirty
-        // If the channel is full it means that the receivers have already been marked as dirty
-        _ = self.inner.read().sender.try_send(());
-    }
-
-    /// Create a two-way binding between this reactive context and a signal
-    pub fn link(&mut self, signal: GenerationalBoxId, rc_list: RcList) {
-        rc_list.write().insert(*self);
-        self.inner
-            .write()
-            .signal_subscribers
-            .insert(signal, rc_list);
     }
 
     /// Get the scope that inner CopyValue is associated with
@@ -127,9 +124,8 @@ impl Hash for ReactiveContext {
 }
 
 struct Inner {
-    // Set of signals bound to this context
-    signal_subscribers: FxHashMap<GenerationalBoxId, RcList>,
-    scope_subscribers: FxHashSet<ScopeId>,
+    // A scope we mark as dirty when this context is written to
+    scope_subscriber: Option<ScopeId>,
     self_: Option<ReactiveContext>,
     update_any: Arc<dyn Fn(ScopeId) + Send + Sync>,
 
@@ -137,12 +133,3 @@ struct Inner {
     sender: flume::Sender<()>,
     receiver: flume::Receiver<()>,
 }
-
-impl Drop for Inner {
-    // Remove this context from all the subscribers
-    fn drop(&mut self) {
-        self.signal_subscribers.values().for_each(|sub_list| {
-            sub_list.write().remove(&self.self_.unwrap());
-        });
-    }
-}

+ 7 - 1
packages/signals/src/read.rs

@@ -14,8 +14,14 @@ pub trait Readable<T: 'static = ()> {
         f: F,
     ) -> Option<Self::Ref<U>>;
 
+    /// Try to get the current value of the state. If this is a signal, this will subscribe the current scope to the signal. If the value has been dropped, this will panic.
+    fn try_read(&self) -> Result<Self::Ref<T>, generational_box::BorrowError>;
+
     /// Get the current value of the state. If this is a signal, this will subscribe the current scope to the signal. If the value has been dropped, this will panic.
-    fn read(&self) -> Self::Ref<T>;
+    #[track_caller]
+    fn read(&self) -> Self::Ref<T> {
+        self.try_read().unwrap()
+    }
 
     /// Get the current value of the state without subscribing to updates. If the value has been dropped, this will panic.
     fn peek(&self) -> Self::Ref<T>;

+ 3 - 5
packages/signals/src/read_only_signal.rs

@@ -68,17 +68,15 @@ impl<T, S: Storage<SignalData<T>>> Readable<T> for ReadOnlySignal<T, S> {
         S::try_map(ref_, f)
     }
 
-    /// Get the current value of the signal. This will subscribe the current scope to the signal.  If you would like to read the signal without subscribing to it, you can use [`Self::peek`] instead.
-    ///
-    /// If the signal has been dropped, this will panic.
     #[track_caller]
-    fn read(&self) -> S::Ref<T> {
-        self.inner.read()
+    fn try_read(&self) -> Result<Self::Ref<T>, generational_box::BorrowError> {
+        self.inner.try_read()
     }
 
     /// Get the current value of the signal. **Unlike read, this will not subscribe the current scope to the signal which can cause parts of your UI to not update.**
     ///
     /// If the signal has been dropped, this will panic.
+    #[track_caller]
     fn peek(&self) -> S::Ref<T> {
         self.inner.peek()
     }

+ 10 - 22
packages/signals/src/signal.rs

@@ -7,12 +7,11 @@ use dioxus_core::{
     ScopeId,
 };
 use generational_box::{AnyStorage, Storage, SyncStorage, UnsyncStorage};
-use parking_lot::RwLock;
 use std::{
     any::Any,
     collections::HashSet,
     ops::{Deref, DerefMut},
-    sync::Arc,
+    sync::Mutex,
 };
 
 /// Creates a new Signal. Signals are a Copy state management solution with automatic dependency tracking.
@@ -54,13 +53,9 @@ pub struct Signal<T: 'static, S: Storage<SignalData<T>> = UnsyncStorage> {
 /// A signal that can safely shared between threads.
 pub type SyncSignal<T> = Signal<T, SyncStorage>;
 
-/// The list of reactive contexts this signal is assocaited with
-/// Whenever we call .write() on the signal, these contexts will be notified of the change
-pub type RcList = Arc<RwLock<HashSet<ReactiveContext>>>;
-
 /// The data stored for tracking in a signal.
 pub struct SignalData<T> {
-    pub(crate) subscribers: RcList,
+    pub(crate) subscribers: Mutex<HashSet<ReactiveContext>>,
     pub(crate) value: T,
 }
 
@@ -107,8 +102,7 @@ impl<T: PartialEq + 'static> Signal<T> {
         mut f: impl FnMut() -> T + 'static,
     ) -> ReadOnlySignal<T, S> {
         // Get the current reactive context
-        let rc = ReactiveContext::current()
-            .expect("Cannot use a selector outside of a reactive context");
+        let rc = ReactiveContext::current();
 
         // Create a new signal in that context, wiring up its dependencies and subscribers
         let mut state: Signal<T, S> = rc.run_in(|| Signal::new_maybe_sync(f()));
@@ -188,9 +182,8 @@ impl<T: 'static, S: Storage<SignalData<T>>> Signal<T, S> {
         {
             let inner = self.inner.read();
 
-            for cx in inner.subscribers.read().iter() {
-                cx.mark_dirty();
-            }
+            let mut subscribers = inner.subscribers.lock().unwrap();
+            subscribers.retain(|reactive_context| reactive_context.mark_dirty())
         }
     }
 
@@ -219,19 +212,14 @@ impl<T, S: Storage<SignalData<T>>> Readable<T> for Signal<T, S> {
         S::try_map(ref_, f)
     }
 
-    /// Get the current value of the signal. This will subscribe the current scope to the signal.
-    /// If you would like to read the signal without subscribing to it, you can use [`Self::peek`] instead.
-    ///
-    /// If the signal has been dropped, this will panic.
     #[track_caller]
-    fn read(&self) -> S::Ref<T> {
-        let inner = self.inner.read();
+    fn try_read(&self) -> Result<S::Ref<T>, generational_box::BorrowError> {
+        let inner = self.inner.try_read()?;
 
-        if let Some(mut cx) = ReactiveContext::current() {
-            cx.link(self.id(), inner.subscribers.clone());
-        }
+        let reactive_context = ReactiveContext::current();
+        inner.subscribers.lock().unwrap().insert(reactive_context);
 
-        S::map(inner, |v| &v.value)
+        Ok(S::map(inner, |v| &v.value))
     }
 
     /// Get the current value of the signal. **Unlike read, this will not subscribe the current scope to the signal which can cause parts of your UI to not update.**