mirror of
https://github.com/DioxusLabs/dioxus
synced 2024-11-10 06:34:20 +00:00
Implement effects using reactivecontext
This commit is contained in:
parent
968f24a7b3
commit
7c2947a131
21 changed files with 287 additions and 320 deletions
|
@ -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" }
|
||||
|
|
|
@ -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.." } ),
|
||||
}
|
||||
|
|
|
@ -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<i32>) -> Element {
|
||||
let doubled = use_memo(move || sig() * 2);
|
||||
|
||||
rsx! {
|
||||
"The count is: {sig}, doubled: {doubled}"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<i32>, 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<Element> {
|
||||
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
|
||||
}
|
|
@ -77,6 +77,8 @@ fn app() -> Element {
|
|||
|
||||
#[component]
|
||||
fn Child(mut count: ReadOnlySignal<i32>) -> Element {
|
||||
println!("rendering child with count {count}");
|
||||
|
||||
rsx! {
|
||||
h1 { "{count}" }
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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<String, ServerFnError> {
|
|||
fn main() {
|
||||
#[cfg(feature = "web")]
|
||||
tracing_wasm::set_as_global_default();
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
|
|
|
@ -62,37 +62,38 @@ pub struct UseServerFuture<T: 'static> {
|
|||
value: Signal<Option<Signal<T>>>,
|
||||
}
|
||||
|
||||
// impl<T> UseServerFuture<T> {
|
||||
// /// 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<T> UseServerFuture<T> {
|
||||
// /// 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<Signal<T>> {
|
||||
todo!()
|
||||
// Ref::map(self.value.read(), |v: &Option<T>| v.as_deref().unwrap())
|
||||
}
|
||||
|
||||
// /// Get the ID of the future in Dioxus' internal scheduler
|
||||
// pub fn task(&self) -> Option<Task> {
|
||||
// self.task.get()
|
||||
// }
|
||||
// /// Get the ID of the future in Dioxus' internal scheduler
|
||||
// pub fn task(&self) -> Option<Task> {
|
||||
// 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()
|
||||
// }
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
})
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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<R: PartialEq>(f: impl FnMut() -> R + 'static) -> ReadOnlySignal<
|
|||
/// ```
|
||||
#[track_caller]
|
||||
pub fn use_maybe_sync_memo<R: PartialEq, S: Storage<SignalData<R>>>(
|
||||
f: impl FnMut() -> R + 'static,
|
||||
mut f: impl FnMut() -> R + 'static,
|
||||
) -> ReadOnlySignal<R, S> {
|
||||
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<R, S> = 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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -96,7 +96,7 @@ fn use_maybe_signal_sync<T: 'static, U: Storage<SignalData<T>>>(
|
|||
|
||||
// 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
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
|
|
|
@ -36,3 +36,6 @@ pub use write::*;
|
|||
|
||||
mod props;
|
||||
pub use props::*;
|
||||
|
||||
mod rc;
|
||||
pub use rc::*;
|
||||
|
|
|
@ -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<Inner, SyncStorage>,
|
||||
pub inner: CopyValue<Inner, SyncStorage>,
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
|
@ -22,27 +25,94 @@ thread_local! {
|
|||
}
|
||||
|
||||
impl ReactiveContext {
|
||||
pub fn new(scope: Option<ScopeId>) -> 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<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 {
|
||||
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<O>(&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<GenerationalBoxId>,
|
||||
|
||||
// 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<ScopeId>,
|
||||
pub signal_subscribers: FxHashMap<GenerationalBoxId, RcList>,
|
||||
scope_subscribers: FxHashSet<ScopeId>,
|
||||
_self: Option<ReactiveContext>,
|
||||
|
||||
// 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());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -284,6 +284,7 @@ impl<T: 'static, S: Storage<T>> PartialEq for CopyValue<T, S> {
|
|||
self.value.ptr_eq(&other.value)
|
||||
}
|
||||
}
|
||||
impl<T: 'static, S: Storage<T>> Eq for CopyValue<T, S> {}
|
||||
|
||||
impl<T: Copy, S: Storage<T>> Deref for CopyValue<T, S> {
|
||||
type Target = dyn Fn() -> T;
|
||||
|
|
|
@ -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<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: Arc<RwLock<HashSet<ReactiveContext>>>,
|
||||
pub(crate) update_any: Arc<dyn Fn(ScopeId) + Sync + Send>,
|
||||
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<ScopeId>,
|
||||
// pub(crate) effect_subscribers: FxHashSet<GenerationalBoxId>,
|
||||
// }
|
||||
|
||||
impl<T: 'static> Signal<T> {
|
||||
/// Creates a new Signal. Signals are a Copy state management solution with automatic dependency tracking.
|
||||
#[track_caller]
|
||||
|
@ -97,10 +93,7 @@ impl<T: 'static> Signal<T> {
|
|||
impl<T: PartialEq + 'static> Signal<T> {
|
||||
/// Creates a new global Signal that can be used in a global static.
|
||||
#[track_caller]
|
||||
pub const fn global_memo(constructor: fn() -> T) -> GlobalMemo<T>
|
||||
where
|
||||
T: PartialEq,
|
||||
{
|
||||
pub const fn global_memo(constructor: fn() -> T) -> GlobalMemo<T> {
|
||||
GlobalMemo::new(constructor)
|
||||
}
|
||||
|
||||
|
@ -119,38 +112,26 @@ impl<T: PartialEq + 'static> Signal<T> {
|
|||
pub fn maybe_sync_memo<S: Storage<SignalData<T>>>(
|
||||
mut f: impl FnMut() -> T + 'static,
|
||||
) -> ReadOnlySignal<T, S> {
|
||||
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<T, S> = 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<T, S> = 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<T: 'static, S: Storage<SignalData<T>>> Signal<T, S> {
|
|||
Self {
|
||||
inner: CopyValue::<SignalData<T>, S>::new_maybe_sync(SignalData {
|
||||
subscribers: Default::default(),
|
||||
update_any: schedule_update_any(),
|
||||
value,
|
||||
effect_ref: get_effect_ref(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
@ -179,9 +158,7 @@ impl<T: 'static, S: Storage<SignalData<T>>> Signal<T, S> {
|
|||
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<T: 'static, S: Storage<SignalData<T>>> Signal<T, S> {
|
|||
inner: CopyValue::<SignalData<T>, 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<T: 'static, S: Storage<SignalData<T>>> Signal<T, S> {
|
|||
}
|
||||
|
||||
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<T, S: Storage<SignalData<T>>> Readable<T> for Signal<T, S> {
|
|||
fn read(&self) -> S::Ref<T> {
|
||||
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<SignalData<T>>>
|
|||
}
|
||||
}
|
||||
|
||||
struct Unsubscriber {
|
||||
scope: ScopeId,
|
||||
subscribers: UnsubscriberArray,
|
||||
}
|
||||
|
||||
type UnsubscriberArray = Vec<Rc<RefCell<Vec<ScopeId>>>>;
|
||||
|
||||
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<RefCell<Unsubscriber>> {
|
||||
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<T: 'static, S: Storage<SignalData<T>>> {
|
||||
signal: Signal<T, S>,
|
||||
}
|
||||
|
||||
impl<T: 'static, S: Storage<SignalData<T>>> Drop for SignalSubscriberDrop<T, S> {
|
||||
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<T: ?Sized, S: AnyStorage> DerefMut for Write<T, S> {
|
|||
&mut self.write
|
||||
}
|
||||
}
|
||||
|
||||
struct SignalSubscriberDrop<T: 'static, S: Storage<SignalData<T>>> {
|
||||
signal: Signal<T, S>,
|
||||
}
|
||||
|
||||
impl<T: 'static, S: Storage<SignalData<T>>> Drop for SignalSubscriberDrop<T, S> {
|
||||
fn drop(&mut self) {
|
||||
self.signal.update_subscribers();
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue