diff --git a/Cargo.toml b/Cargo.toml index bbf8c1642..8c07b6254 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -68,7 +68,7 @@ dioxus-html-internal-macro = { path = "packages/html-internal-macro", version = dioxus-hooks = { path = "packages/hooks", version = "0.4.0" } dioxus-web = { path = "packages/web", version = "0.4.0", default-features = false } dioxus-ssr = { path = "packages/ssr", version = "0.4.0", default-features = false } -dioxus-desktop = { path = "packages/desktop", version = "0.4.0", default-features = false } +dioxus-desktop = { path = "packages/desktop", version = "0.4.0" } dioxus-mobile = { path = "packages/mobile", version = "0.4.0" } dioxus-interpreter-js = { path = "packages/interpreter", version = "0.4.0" } dioxus-liveview = { path = "packages/liveview", version = "0.4.0" } diff --git a/examples/eval.rs b/examples/eval.rs index c22d64932..7df54f7f7 100644 --- a/examples/eval.rs +++ b/examples/eval.rs @@ -22,7 +22,7 @@ fn app() -> Element { res }); - match future.value().read().as_ref() { + match future.value().as_ref() { Some(v) => rsx!( p { "{v}" } ), _ => rsx!( p { "waiting.." } ), } diff --git a/examples/readme.rs b/examples/readme.rs index ae4648cf7..ea0fc869f 100644 --- a/examples/readme.rs +++ b/examples/readme.rs @@ -7,8 +7,23 @@ fn main() { fn app() -> Element { let mut count = use_signal(|| 0); + use_effect(move || { + println!("The count is now: {}", count); + }); + rsx! { + h1 { "High-Five counter: {count}" } + Child { sig: count } button { onclick: move |_| count += 1, "Up high!" } button { onclick: move |_| count -= 1, "Down low!" } } } + +#[component] +fn Child(sig: Signal) -> Element { + let doubled = use_memo(move || sig() * 2); + + rsx! { + "The count is: {sig}, doubled: {doubled}" + } +} diff --git a/examples/server_future.rs b/examples/server_future.rs deleted file mode 100644 index fcfc91f85..000000000 --- a/examples/server_future.rs +++ /dev/null @@ -1,59 +0,0 @@ -use dioxus::prelude::*; - -fn main() { - launch(app); -} - -fn app() -> Element { - let val = use_server_future(fetch_users).suspend()?; - - rsx! { - h1 { "Users" } - - } -} - -#[component] -fn ClientComponent(name: Signal, id: i64) -> Element { - rsx! { - div { "Name: {name}, ID: {id}" } - button { - onclick: move |_| async move { - // Optimistically change the name on the client - name.set("new name".to_string()); - - // Change the name on the server - change_name(id, "new name".to_string()).await; - - // And then re-fetch the user list - revalidate(user_list); - }, - "Change name" - } - } -} - -#[derive(Table)] -struct Users { - name: String, - age: i32, -} - -#[server] -async fn fetch_users() -> Result { - let users = get_users().await?; - - Ok(rsx! { - for user in users { - ClientComponent { - name: user.name, - id: user.id, - } - } - }) -} - -#[server] -async fn change_name(id: i64, new_name: String) -> Result<()> { - // Send a request to the server to change the name -} diff --git a/examples/signals.rs b/examples/signals.rs index e666980ae..5cf15405e 100644 --- a/examples/signals.rs +++ b/examples/signals.rs @@ -77,6 +77,8 @@ fn app() -> Element { #[component] fn Child(mut count: ReadOnlySignal) -> Element { + println!("rendering child with count {count}"); + rsx! { h1 { "{count}" } } diff --git a/packages/core/src/global_context.rs b/packages/core/src/global_context.rs index c5257549a..4e2de7ba7 100644 --- a/packages/core/src/global_context.rs +++ b/packages/core/src/global_context.rs @@ -111,6 +111,11 @@ pub fn needs_update() { Runtime::with_current_scope(|cx| cx.needs_update()); } +/// Mark the current scope as dirty, causing it to re-render +pub fn needs_update_any(id: ScopeId) { + Runtime::with_current_scope(|cx| cx.needs_update_any(id)); +} + /// Schedule an update for the current component /// /// Note: Unlike [`needs_update`], the function returned by this method will work outside of the dioxus runtime. @@ -252,6 +257,10 @@ pub fn after_render(f: impl FnMut() + 'static) { /// Effects rely on this to ensure that they only run effects after the DOM has been updated. Without flush_sync effects /// are run immediately before diffing the DOM, which causes all sorts of out-of-sync weirdness. pub async fn flush_sync() { + // if flushing() { + // return; + // } + _ = Runtime::with(|rt| rt.flush.clone()) .unwrap() .recv_async() diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index b4f76fa9b..a55ce4be7 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -88,13 +88,13 @@ pub use crate::innerlude::{ pub mod prelude { pub use crate::innerlude::{ consume_context, consume_context_from_scope, current_scope_id, fc_to_builder, flush_sync, - generation, has_context, needs_update, parent_scope, provide_context, provide_root_context, - remove_future, schedule_update, schedule_update_any, spawn, spawn_forever, suspend, - try_consume_context, use_after_render, use_before_render, use_drop, use_error_boundary, - use_hook, use_hook_with_cleanup, AnyValue, Attribute, Component, ComponentFunction, - Element, ErrorBoundary, Event, EventHandler, Fragment, HasAttributes, IntoAttributeValue, - IntoDynNode, OptionStringFromMarker, Properties, Runtime, RuntimeGuard, ScopeId, - ScopeState, SuperFrom, SuperInto, Task, Template, TemplateAttribute, TemplateNode, Throw, - VNode, VNodeInner, VirtualDom, + generation, has_context, needs_update, needs_update_any, parent_scope, provide_context, + provide_root_context, remove_future, schedule_update, schedule_update_any, spawn, + spawn_forever, suspend, try_consume_context, use_after_render, use_before_render, use_drop, + use_error_boundary, use_hook, use_hook_with_cleanup, AnyValue, Attribute, Component, + ComponentFunction, Element, ErrorBoundary, Event, EventHandler, Fragment, HasAttributes, + IntoAttributeValue, IntoDynNode, OptionStringFromMarker, Properties, Runtime, RuntimeGuard, + ScopeId, ScopeState, SuperFrom, SuperInto, Task, Template, TemplateAttribute, TemplateNode, + Throw, VNode, VNodeInner, VirtualDom, }; } diff --git a/packages/core/src/scope_context.rs b/packages/core/src/scope_context.rs index 2efe83188..4514dcc3c 100644 --- a/packages/core/src/scope_context.rs +++ b/packages/core/src/scope_context.rs @@ -60,8 +60,13 @@ impl Scope { /// Mark this scope as dirty, and schedule a render for it. pub fn needs_update(&self) { + self.needs_update_any(self.id) + } + + /// Mark this scope as dirty, and schedule a render for it. + pub fn needs_update_any(&self, id: ScopeId) { self.sender() - .unbounded_send(SchedulerMsg::Immediate(self.id)) + .unbounded_send(SchedulerMsg::Immediate(id)) .expect("Scheduler to exist if scope exists"); } diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index a3e6b3a9a..ca7af404c 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -372,11 +372,15 @@ impl VirtualDom { /// /// Whenever the Runtime "works", it will re-render this scope pub fn mark_dirty(&mut self, id: ScopeId) { - if let Some(context) = self.runtime.get_state(id) { - let height = context.height(); - tracing::trace!("Marking scope {:?} ({}) as dirty", id, context.name); - self.dirty_scopes.insert(DirtyScope { height, id }); - } + let Some(scope) = self.runtime.get_state(id) else { + return; + }; + + tracing::trace!("Marking scope {:?} ({}) as dirty", id, scope.name); + self.dirty_scopes.insert(DirtyScope { + height: scope.height(), + id, + }); } /// Call a listener inside the VirtualDom with data from outside the VirtualDom. **The ElementId passed in must be the id of an element with a listener, not a static node or a text node.** @@ -422,9 +426,6 @@ impl VirtualDom { /// let dom = VirtualDom::new(app); /// ``` pub async fn wait_for_work(&mut self) { - // Send the flush signal to the runtime - _ = self.flush_tx.try_send(()); - // And then poll the futures self.poll_tasks().await; } @@ -432,6 +433,9 @@ impl VirtualDom { /// Poll futures without progressing any futures from the flush table async fn poll_tasks(&mut self) { loop { + // Send the flush signal to the runtime, waking up tasks + _ = self.flush_tx.try_send(()); + // Process all events - Scopes are marked dirty, etc // Sometimes when wakers fire we get a slew of updates at once, so its important that we drain this completely self.process_events(); diff --git a/packages/fullstack/examples/salvo-hello-world/src/main.rs b/packages/fullstack/examples/salvo-hello-world/src/main.rs index 3428578fe..b7cd8c48e 100644 --- a/packages/fullstack/examples/salvo-hello-world/src/main.rs +++ b/packages/fullstack/examples/salvo-hello-world/src/main.rs @@ -16,9 +16,7 @@ fn app() -> Element { let mut text = use_signal(|| "...".to_string()); rsx! { - div { - "Server state: {state.unwrap().value().clone()}" - } + div { "Server state: {state.value().unwrap()}" } h1 { "High-Five counter: {count}" } button { onclick: move |_| count += 1, "Up high!" } button { onclick: move |_| count -= 1, "Down low!" } @@ -51,6 +49,7 @@ async fn get_server_data() -> Result { fn main() { #[cfg(feature = "web")] tracing_wasm::set_as_global_default(); + #[cfg(feature = "ssr")] tracing_subscriber::fmt::init(); diff --git a/packages/fullstack/src/hooks/server_future.rs b/packages/fullstack/src/hooks/server_future.rs index bfee62433..8f7284956 100644 --- a/packages/fullstack/src/hooks/server_future.rs +++ b/packages/fullstack/src/hooks/server_future.rs @@ -62,37 +62,38 @@ pub struct UseServerFuture { value: Signal>>, } -// impl UseServerFuture { -// /// Restart the future with new dependencies. -// /// -// /// Will not cancel the previous future, but will ignore any values that it -// /// generates. -// pub fn restart(&self) { -// self.needs_regen.set(true); -// (self.update)(); -// } +impl UseServerFuture { + // /// Restart the future with new dependencies. + // /// + // /// Will not cancel the previous future, but will ignore any values that it + // /// generates. + // pub fn restart(&self) { + // self.needs_regen.set(true); + // (self.update)(); + // } -// /// Forcefully cancel a future -// pub fn cancel(&self) { -// if let Some(task) = self.task.take() { -// remove_future(task); -// } -// } + // /// Forcefully cancel a future + // pub fn cancel(&self) { + // if let Some(task) = self.task.take() { + // remove_future(task); + // } + // } -// /// Return any value, even old values if the future has not yet resolved. -// /// -// /// If the future has never completed, the returned value will be `None`. -// pub fn value(&self) -> Ref<'_, T> { -// Ref::map(self.value.borrow(), |v| v.as_deref().unwrap()) -// } + /// Return any value, even old values if the future has not yet resolved. + /// + /// If the future has never completed, the returned value will be `None`. + pub fn value(&self) -> Option> { + todo!() + // Ref::map(self.value.read(), |v: &Option| v.as_deref().unwrap()) + } -// /// Get the ID of the future in Dioxus' internal scheduler -// pub fn task(&self) -> Option { -// self.task.get() -// } + // /// Get the ID of the future in Dioxus' internal scheduler + // pub fn task(&self) -> Option { + // self.task.get() + // } -// /// Get the current state of the future. -// pub fn reloading(&self) -> bool { -// self.task.get().is_some() -// } -// } + // /// Get the current state of the future. + // pub fn reloading(&self) -> bool { + // self.task.get().is_some() + // } +} diff --git a/packages/hooks/src/use_effect.rs b/packages/hooks/src/use_effect.rs index a1f1b8b04..8e86ec692 100644 --- a/packages/hooks/src/use_effect.rs +++ b/packages/hooks/src/use_effect.rs @@ -1,24 +1,27 @@ use crate::use_hook_did_run; use dioxus_core::prelude::*; -use dioxus_signals::{CopyValue, Effect, Writable}; +use dioxus_signals::{CopyValue, Effect, ReactiveContext, Writable}; +use futures_util::select; /// Create a new effect. The effect will be run immediately and whenever any signal it reads changes. /// The signal will be owned by the current component and will be dropped when the component is dropped. /// /// If the use_effect call was skipped due to an early return, the effect will no longer activate. pub fn use_effect(mut callback: impl FnMut() + 'static) { - let mut run_effect = use_hook(|| CopyValue::new(true)); - - use_hook_did_run(move |did_run| match did_run { - true => run_effect.set(true), - false => run_effect.set(false), - }); + // let mut run_effect = use_hook(|| CopyValue::new(true)); + // use_hook_did_run(move |did_run| run_effect.set(did_run)); use_hook(|| { - Effect::new(move || { - if run_effect() { - callback(); + spawn(async move { + let rc = ReactiveContext::new(None); + + loop { + // Run the effect + rc.run_in(|| callback()); + + // Wait for context to change + rc.changed().await; } - }) + }); }); } diff --git a/packages/hooks/src/use_future.rs b/packages/hooks/src/use_future.rs index 92942b460..a30766500 100644 --- a/packages/hooks/src/use_future.rs +++ b/packages/hooks/src/use_future.rs @@ -21,11 +21,6 @@ where let mut callback = use_callback(move || { let fut = future(); spawn(async move { - // todo: not sure if we should flush_sync - // It's fine for most cases but means that the future will always be started in the next frame - // The point here is to not run use_future on the server... which like, shouldn't we? - flush_sync().await; - state.set(UseFutureState::Pending); fut.await; state.set(UseFutureState::Complete); diff --git a/packages/hooks/src/use_memo.rs b/packages/hooks/src/use_memo.rs index ad3acf8df..944d5504f 100644 --- a/packages/hooks/src/use_memo.rs +++ b/packages/hooks/src/use_memo.rs @@ -1,7 +1,7 @@ use crate::dependency::Dependency; use crate::use_signal; use dioxus_core::prelude::*; -use dioxus_signals::{ReadOnlySignal, Readable, Signal, SignalData}; +use dioxus_signals::{ReactiveContext, ReadOnlySignal, Readable, Signal, SignalData}; use dioxus_signals::{Storage, Writable}; /// Creates a new unsync Selector. The selector will be run immediately and whenever any signal it reads changes. @@ -45,9 +45,33 @@ pub fn use_memo(f: impl FnMut() -> R + 'static) -> ReadOnlySignal< /// ``` #[track_caller] pub fn use_maybe_sync_memo>>( - f: impl FnMut() -> R + 'static, + mut f: impl FnMut() -> R + 'static, ) -> ReadOnlySignal { - use_hook(|| Signal::maybe_sync_memo(f)) + use_hook(|| { + // Get the current reactive context + let rc = ReactiveContext::new(None); + + // Create a new signal in that context, wiring up its dependencies and subscribers + let mut state: Signal = rc.run_in(|| Signal::new_maybe_sync(f())); + + spawn(async move { + loop { + rc.changed().await; + + println!("change triggered in memo"); + + let new = rc.run_in(|| f()); + + if new != *state.peek() { + println!("change sett in memo"); + *state.write() = new; + } + } + }); + + // And just return the readonly variant of that signal + ReadOnlySignal::new_maybe_sync(state) + }) } /// Creates a new unsync Selector with some local dependencies. The selector will be run immediately and whenever any signal it reads or any dependencies it tracks changes diff --git a/packages/hooks/src/use_resource.rs b/packages/hooks/src/use_resource.rs index d64b7e210..cfa3799fe 100644 --- a/packages/hooks/src/use_resource.rs +++ b/packages/hooks/src/use_resource.rs @@ -27,20 +27,20 @@ where // Spawn a wrapper task that polls the innner future and watch its dependencies let task = spawn(async move { - // move the future here and pin it so we can poll it - let fut = fut; - pin_mut!(fut); + // // move the future here and pin it so we can poll it + // let fut = fut; + // pin_mut!(fut); - let res = future::poll_fn(|cx| { - // Set the effect stack properly - // add any dependencies to the effect stack that we need to watch when restarting the future - // Poll the inner future - fut.poll_unpin(cx) - }) - .await; + // let res = future::poll_fn(|cx| { + // // Set the effect stack properly + // // add any dependencies to the effect stack that we need to watch when restarting the future + // // Poll the inner future + // fut.poll_unpin(cx) + // }) + // .await; - // Set the value - value.set(Some(Signal::new(res))); + // // Set the value + // value.set(Some(Signal::new(res))); }); Some(task) diff --git a/packages/hooks/src/use_signal.rs b/packages/hooks/src/use_signal.rs index d7af63c4a..ce6087ea2 100644 --- a/packages/hooks/src/use_signal.rs +++ b/packages/hooks/src/use_signal.rs @@ -96,7 +96,7 @@ fn use_maybe_signal_sync>>( // By default, we want to unsubscribe the current component from the signal on every render // any calls to .read() in the body will re-subscribe the component to the signal - use_before_render(move || signal.unsubscribe(current_scope_id().unwrap())); + // use_before_render(move || signal.unsubscribe(current_scope_id().unwrap())); signal } diff --git a/packages/html/src/eval.rs b/packages/html/src/eval.rs index fe5d51383..7e86246b9 100644 --- a/packages/html/src/eval.rs +++ b/packages/html/src/eval.rs @@ -91,10 +91,15 @@ impl IntoFuture for UseEval { /// Represents an error when evaluating JavaScript #[derive(Debug)] pub enum EvalError { + /// The platform does not support evaluating JavaScript. + Unspported, + /// The provided JavaScript has already been ran. Finished, + /// The provided JavaScript is not valid and can't be ran. InvalidJs(String), + /// Represents an error communicating between JavaScript and Rust. Communication(String), } diff --git a/packages/signals/src/lib.rs b/packages/signals/src/lib.rs index 6c8dbd2bb..8b49a9afd 100644 --- a/packages/signals/src/lib.rs +++ b/packages/signals/src/lib.rs @@ -36,3 +36,6 @@ pub use write::*; mod props; pub use props::*; + +mod rc; +pub use rc::*; diff --git a/packages/signals/src/rc.rs b/packages/signals/src/rc.rs index dca6d6498..999bc42da 100644 --- a/packages/signals/src/rc.rs +++ b/packages/signals/src/rc.rs @@ -1,9 +1,13 @@ -use dioxus_core::prelude::ScopeId; +use dioxus_core::prelude::{ + consume_context, consume_context_from_scope, current_scope_id, has_context, needs_update_any, + provide_context, schedule_update, schedule_update_any, try_consume_context, ScopeId, +}; use generational_box::{GenerationalBoxId, SyncStorage}; -use rustc_hash::FxHashSet; +use rustc_hash::{FxHashMap, FxHashSet}; use std::{cell::RefCell, hash::Hash}; +use tokio::signal; -use crate::{CopyValue, Readable}; +use crate::{CopyValue, RcList, Readable, Writable}; /// A context for signal reads and writes to be directed to /// @@ -11,10 +15,9 @@ use crate::{CopyValue, Readable}; /// If it doesn't find it, then it will try and insert a context into the nearest component scope via context api. /// /// When the ReactiveContext drops, it will remove itself from the the associated contexts attached to signal -#[derive(Clone, Copy, PartialEq)] +#[derive(Clone, Copy, PartialEq, Eq)] pub struct ReactiveContext { - // todo: we dont need to use syncstorage per say - inner: CopyValue, + pub inner: CopyValue, } thread_local! { @@ -22,27 +25,94 @@ thread_local! { } impl ReactiveContext { + pub fn new(scope: Option) -> Self { + let (tx, rx) = flume::unbounded(); + + let mut scope_subscribers = FxHashSet::default(); + if let Some(scope) = scope { + scope_subscribers.insert(scope); + } + + let inner = Inner { + signal_subscribers: FxHashMap::default(), + scope_subscribers, + sender: tx, + _self: None, + receiver: rx, + }; + + let mut _self = Self { + inner: CopyValue::new_maybe_sync(inner), + }; + + _self.inner.write()._self = Some(_self); + + _self + } + /// Get the current reactive context /// /// 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() -> Self { - todo!() + pub fn current() -> Option { + 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 { + println!("Already found context!"); + return Some(cur); + } + + // Try and get the context out of the current scope + let scope = current_scope_id().unwrap(); + + // If we're rendering, then try and use the reactive context attached to this component + + if let Some(cx) = has_context() { + println!("found context at {scope:?}"); + return Some(cx); + } + + println!("creating new context at {scope:?}"); + + // Otherwise, create a new context at the current scope + Some(provide_context(ReactiveContext::new(Some(scope)))) } /// Run this function in the context of this reactive context /// /// This will set the current reactive context to this context for the duration of the function. /// You can then get information about the current subscriptions. - pub fn run_in(&self, f: impl FnOnce()) { - todo!() + pub fn run_in(&self, f: impl FnOnce() -> O) -> O { + CURRENT.with(|current| current.borrow_mut().push(*self)); + let out = f(); + CURRENT.with(|current| current.borrow_mut().pop()); + out } /// 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) {} + pub fn mark_dirty(&self) { + for scope in self.inner.read().scope_subscribers.iter() { + println!("marking dirty {scope:?}"); + needs_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.inner.read().sender.try_send(()); + } + + // Create a two-way binding between this reactive context and a signal + pub fn link(&self, signal: GenerationalBoxId, rc_list: RcList) { + rc_list.write().insert(*self); + self.inner + .write() + .signal_subscribers + .insert(signal, rc_list); + } /// Clear all subscribers from this reactive context pub fn clear_subscribers(&self) { @@ -51,9 +121,8 @@ impl ReactiveContext { /// Wait for this reactive context to change pub async fn changed(&self) { - let waiter = self.inner.read().waiters.1.clone(); - - _ = waiter.recv_async().await; + let rx = self.inner.read().receiver.clone(); + _ = rx.recv_async().await; } } @@ -63,22 +132,22 @@ impl Hash for ReactiveContext { } } -struct Inner { +pub struct Inner { // Set of signals bound to this context - subscribers: FxHashSet, - - // The scope that this context is associated with - // This is only relevant when RC is being used to call update on a signal - scope: Option, + pub signal_subscribers: FxHashMap, + scope_subscribers: FxHashSet, + _self: Option, // Futures will call .changed().await - waiters: (flume::Sender<()>, flume::Receiver<()>), + sender: flume::Sender<()>, + receiver: flume::Receiver<()>, } -impl Inner {} - impl Drop for Inner { fn drop(&mut self) { - todo!() + // Remove this context from all the subscribers + self.signal_subscribers.values().for_each(|sub_list| { + sub_list.write().remove(&self._self.unwrap()); + }); } } diff --git a/packages/signals/src/rt.rs b/packages/signals/src/rt.rs index 66c5c2a0f..1302b19fb 100644 --- a/packages/signals/src/rt.rs +++ b/packages/signals/src/rt.rs @@ -284,6 +284,7 @@ impl> PartialEq for CopyValue { self.value.ptr_eq(&other.value) } } +impl> Eq for CopyValue {} impl> Deref for CopyValue { type Target = dyn Fn() -> T; diff --git a/packages/signals/src/signal.rs b/packages/signals/src/signal.rs index 53a8cb04f..14f628c37 100644 --- a/packages/signals/src/signal.rs +++ b/packages/signals/src/signal.rs @@ -1,11 +1,12 @@ use crate::{ get_effect_ref, read::Readable, write::Writable, CopyValue, Effect, EffectInner, - EffectStackRef, GlobalMemo, GlobalSignal, MappedSignal, ReactiveContext, - ReactiveContextProvider, ReadSignal, EFFECT_STACK, + EffectStackRef, GlobalMemo, GlobalSignal, MappedSignal, ReactiveContext, ReadOnlySignal, + EFFECT_STACK, }; use dioxus_core::{ prelude::{ - current_scope_id, has_context, provide_context, schedule_update_any, IntoAttributeValue, + current_scope_id, has_context, provide_context, schedule_update_any, spawn, + IntoAttributeValue, }, ScopeId, }; @@ -59,21 +60,16 @@ pub struct Signal> = UnsyncStorage> { /// A signal that can safely shared between threads. pub type SyncSignal = Signal; +/// 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>>; + /// The data stored for tracking in a signal. pub struct SignalData { - pub(crate) subscribers: Arc>>, - pub(crate) update_any: Arc, - pub(crate) effect_ref: EffectStackRef, + pub(crate) subscribers: RcList, pub(crate) value: T, } -// #[derive(Default)] -// pub(crate) struct SignalSubscribers { -// // todo: use a linear map here for faster scans -// pub(crate) subscribers: FxHashSet, -// pub(crate) effect_subscribers: FxHashSet, -// } - impl Signal { /// Creates a new Signal. Signals are a Copy state management solution with automatic dependency tracking. #[track_caller] @@ -97,10 +93,7 @@ impl Signal { impl Signal { /// Creates a new global Signal that can be used in a global static. #[track_caller] - pub const fn global_memo(constructor: fn() -> T) -> GlobalMemo - where - T: PartialEq, - { + pub const fn global_memo(constructor: fn() -> T) -> GlobalMemo { GlobalMemo::new(constructor) } @@ -119,38 +112,26 @@ impl Signal { pub fn maybe_sync_memo>>( mut f: impl FnMut() -> T + 'static, ) -> ReadOnlySignal { - let effect = Effect { - source: current_scope_id().expect("in a virtual dom"), - inner: CopyValue::invalid(), - }; + // Get the current reactive context + let rc = ReactiveContext::current() + .expect("Cannot use a selector outside of a reactive context"); - { - EFFECT_STACK.with(|stack| stack.effects.write().push(effect)); - } - let mut state: Signal = Signal::new_maybe_sync(f()); - { - EFFECT_STACK.with(|stack| stack.effects.write().pop()); - } + // Create a new signal in that context, wiring up its dependencies and subscribers + let mut state: Signal = rc.run_in(|| Signal::new_maybe_sync(f())); - let invalid_id = effect.id(); - tracing::trace!("Creating effect: {:?}", invalid_id); - effect.inner.value.set(EffectInner { - callback: Box::new(move || { - let value = f(); - let changed = { - let old = state.inner.read(); - value != old.value - }; - if changed { - state.set(value) + spawn(async move { + loop { + rc.changed().await; + println!("changed"); + + let new = f(); + if new != *state.peek() { + *state.write() = new; } - }), - id: invalid_id, + } }); - { - EFFECT_STACK.with(|stack| stack.effect_mapping.write().insert(invalid_id, effect)); - } + // And just return the readonly variant of that signal ReadOnlySignal::new_maybe_sync(state) } } @@ -163,9 +144,7 @@ impl>> Signal { Self { inner: CopyValue::, S>::new_maybe_sync(SignalData { subscribers: Default::default(), - update_any: schedule_update_any(), value, - effect_ref: get_effect_ref(), }), } } @@ -179,9 +158,7 @@ impl>> Signal { inner: CopyValue::new_with_caller( SignalData { subscribers: Default::default(), - update_any: schedule_update_any(), value, - effect_ref: get_effect_ref(), }, #[cfg(debug_assertions)] caller, @@ -197,9 +174,7 @@ impl>> Signal { inner: CopyValue::, S>::new_maybe_sync_in_scope( SignalData { subscribers: Default::default(), - update_any: schedule_update_any(), value, - effect_ref: get_effect_ref(), }, owner, ), @@ -217,44 +192,13 @@ impl>> Signal { } fn update_subscribers(&self) { - todo!() - // { - // let inner = self.inner.read(); - // for &scope_id in inner.subscribers.read().subscribers.iter() { - // tracing::trace!( - // "Write on {:?} triggered update on {:?}", - // self.inner.value, - // scope_id - // ); - // (inner.update_any)(scope_id); - // } - // } + { + let inner = self.inner.read(); - // let self_read = &self.inner.read(); - // let subscribers = { - // let effects = &mut self_read.subscribers.write().effect_subscribers; - // std::mem::take(&mut *effects) - // }; - // let effect_ref = &self_read.effect_ref; - // for effect in subscribers { - // tracing::trace!( - // "Write on {:?} triggered effect {:?}", - // self.inner.value, - // effect - // ); - // effect_ref.queue_effect(effect); - // } - } - - /// Unsubscribe this scope from the signal's effect list - pub fn unsubscribe(&self, scope: ScopeId) { - todo!() - // self.inner - // .read() - // .subscribers - // .write() - // .subscribers - // .retain(|s| *s != scope); + for cx in inner.subscribers.read().iter() { + cx.mark_dirty(); + } + } } /// Map the signal to a new type. @@ -290,34 +234,9 @@ impl>> Readable for Signal { fn read(&self) -> S::Ref { let inner = self.inner.read(); - todo!(); - - // // if we're in a reactive context, attach the context to the signal via the mapping - // if let Some(effect) = ReactiveContextProvider::current() { - // inner - // .subscribers - // .write() - // .effect_subscribers - // .insert(effect.inner.id()); - // } else if let Some(current_scope_id) = current_scope_id() { - // // only subscribe if the vdom is rendering - // if dioxus_core::vdom_is_rendering() { - // tracing::trace!( - // "{:?} subscribed to {:?}", - // self.inner.value, - // current_scope_id - // ); - - // let mut subscribers = inner.subscribers.write(); - - // if !subscribers.subscribers.contains(¤t_scope_id) { - // subscribers.subscribers.insert(current_scope_id); - // subscribers - // .subscribers - // .insert(current_unsubscriber().borrow().scope); - // } - // } - // } + if let Some(mut cx) = ReactiveContext::current() { + cx.link(self.id(), inner.subscribers.clone()); + } S::map(inner, |v| &v.value) } @@ -404,44 +323,6 @@ impl<'de, T: serde::Deserialize<'de> + 'static, Store: Storage>> } } -struct Unsubscriber { - scope: ScopeId, - subscribers: UnsubscriberArray, -} - -type UnsubscriberArray = Vec>>>; - -impl Drop for Unsubscriber { - fn drop(&mut self) { - for subscribers in &self.subscribers { - subscribers.borrow_mut().retain(|s| *s != self.scope); - } - } -} - -fn current_unsubscriber() -> Rc> { - match has_context() { - Some(rt) => rt, - None => { - let owner = Unsubscriber { - scope: current_scope_id().expect("in a virtual dom"), - subscribers: Default::default(), - }; - provide_context(Rc::new(RefCell::new(owner))) - } - } -} - -struct SignalSubscriberDrop>> { - signal: Signal, -} - -impl>> Drop for SignalSubscriberDrop { - fn drop(&mut self) { - self.signal.update_subscribers(); - } -} - /// A mutable reference to a signal's value. /// /// T is the current type of the write @@ -489,3 +370,13 @@ impl DerefMut for Write { &mut self.write } } + +struct SignalSubscriberDrop>> { + signal: Signal, +} + +impl>> Drop for SignalSubscriberDrop { + fn drop(&mut self) { + self.signal.update_subscribers(); + } +}