Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions crates/vertigo/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,8 @@ default-features = false
features = ["std"]
optional = true

[dev-dependencies]
tokio = { version = "1", features = ["macros", "rt"] }

[lints]
workspace = true
90 changes: 89 additions & 1 deletion crates/vertigo/src/computed/computed_box.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ use crate::{
};

use super::{
DropResource, GraphValue, Value, context::Context, graph_id::GraphId, struct_mut::ValueMut,
DropResource, GraphValue, Value, context::Context, get_dependencies, graph_id::GraphId,
struct_mut::ValueMut,
};

/// A reactive value that is read-only and computed by dependency graph.
Expand Down Expand Up @@ -95,6 +96,26 @@ impl<T: Clone + 'static> Computed<T> {
self.inner.id()
}

/// Executes a closure when the `Computed` value starts being observed.
///
/// The provided closure should return a [`DropResource`] which will be dropped
/// when the value stops being observed. This is especially useful for integrating
/// with sources that are not reactive (like external data fetching or socket connections),
/// allowing side effects to start only when they are actually needed by the UI.
pub fn when_connect<F: Fn() -> DropResource + 'static>(&self, create: F) -> Computed<T> {
let new_computed = Computed::from({
let parent = self.clone();
move |context| parent.get(context)
});

get_dependencies()
.graph
.external_connections
.register_connect(new_computed.id(), Rc::new(create));

new_computed
}

/// Do something every time the value inside [Computed] is triggered.
///
/// Note that the `callback` is fired every time the value in [Computed] is computed, even if the outcome value is not changed.
Expand Down Expand Up @@ -225,3 +246,70 @@ fn drop_computed() {

drop(drop_resource);
}

#[test]
fn test_when_connect() {
let connect_count = Rc::new(ValueMut::new(0));
let disconnect_count = Rc::new(ValueMut::new(0));

let value = Value::new(1);
let comp = value.to_computed().when_connect({
let connect_count = connect_count.clone();
let disconnect_count = disconnect_count.clone();
move || {
connect_count.change(|v| *v += 1);
DropResource::new({
let disconnect_count = disconnect_count.clone();
move || {
disconnect_count.change(|v| *v += 1);
}
})
}
});

assert_eq!(connect_count.get(), 0);
assert_eq!(disconnect_count.get(), 0);

let drop_resource = comp.subscribe(|_| {});

assert_eq!(connect_count.get(), 1);
assert_eq!(disconnect_count.get(), 0);

drop(drop_resource);

assert_eq!(connect_count.get(), 1);
assert_eq!(disconnect_count.get(), 1);
}

#[test]
fn test_when_connect_multiple() {
let connect_count = Rc::new(ValueMut::new(0));
let disconnect_count = Rc::new(ValueMut::new(0));

let value = Value::new(1);
let comp = value.to_computed().when_connect({
let connect_count = connect_count.clone();
let disconnect_count = disconnect_count.clone();
move || {
connect_count.change(|v| *v += 1);
DropResource::new({
let disconnect_count = disconnect_count.clone();
move || {
disconnect_count.change(|v| *v += 1);
}
})
}
});

let drop1 = comp.clone().subscribe(|_| {});
assert_eq!(connect_count.get(), 1);

let drop2 = comp.subscribe(|_| {});
assert_eq!(connect_count.get(), 1);

drop(drop1);
assert_eq!(disconnect_count.get(), 0);

drop(drop2);
assert_eq!(disconnect_count.get(), 1);
}
42 changes: 25 additions & 17 deletions crates/vertigo/src/computed/dependencies/graph.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use super::external_connections::ExternalConnections;
use super::graph_connections::GraphConnections;
use super::refresh::Refresh;
use crate::computed::graph_id::{GraphId, GraphIdKind};
use crate::computed::graph_id::GraphId;
use std::collections::BTreeSet;

pub struct Graph {
Expand Down Expand Up @@ -40,23 +40,31 @@ impl Graph {
.set_parent_for_client(client_id, parents_list);

for (id, active) in edge_list {
match id.get_type() {
GraphIdKind::Value => {
self.external_connections.set_connection(id, active);
}
GraphIdKind::Computed => {
if active {
} else {
self.refresh.clear_cache(&id);
}
}
GraphIdKind::Client => {
if active {
} else {
self.refresh.clear_cache(&id);
}
}
self.external_connections.set_connection(id, active);

if !active {
self.refresh.clear_cache(&id);
}

// TODO - Keeping this comment here for a while
// match id.get_type() {
// GraphIdKind::Value => {
// self.external_connections.set_connection(id, active);
// }
// GraphIdKind::Computed => {
// self.external_connections.set_connection(id, active);
// if active {
// } else {
// self.refresh.clear_cache(&id);
// }
// }
// GraphIdKind::Client => {
// if active {
// } else {
// self.refresh.clear_cache(&id);
// }
// }
// }
}
}
}
13 changes: 9 additions & 4 deletions crates/vertigo/src/computed/graph_value.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
use std::any::Any;
use std::rc::Rc;

use crate::Context;
use crate::{Context, Dependencies};

use super::{GraphId, get_dependencies, struct_mut::ValueMut};

pub struct GraphValue<T> {
deps: Rc<Dependencies>,
id: GraphId,
get_value: Box<dyn Fn(&Context) -> T>,
state: ValueMut<Option<T>>,
Expand All @@ -23,6 +24,7 @@ impl<T: Clone + 'static> GraphValue<T> {
};

let graph_value = Rc::new(GraphValue {
deps: get_dependencies(),
id,
get_value: Box::new(get_value),
state: ValueMut::new(None),
Expand Down Expand Up @@ -98,8 +100,11 @@ impl<T: Clone + 'static> GraphValue<T> {

impl<T> Drop for GraphValue<T> {
fn drop(&mut self) {
let deps = get_dependencies();
deps.graph.refresh.refresh_token_drop(self.id);
deps.graph.remove_client(self.id);
self.deps.graph.refresh.refresh_token_drop(self.id);
self.deps.graph.remove_client(self.id);
self.deps
.graph
.external_connections
.unregister_connect(self.id);
}
}
58 changes: 58 additions & 0 deletions crates/vertigo/src/computed/tests/nested_reactivity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,61 @@ fn test_nested_reactivity() {
});
assert_eq!(result, 2);
}

#[test]
fn test_nested_computed_subscription() {
let token_value = Value::new("token1".to_string());
let token_computed = token_value.to_computed();

// bearer_auth equivalent: Computed<Option<Computed<String>>>
let bearer_auth = Computed::from({
let token_computed = token_computed.clone();
move |_ctx| Some(token_computed.clone())
});

let counter = std::rc::Rc::new(std::cell::Cell::new(0));

// The "flattened" revalidate_trigger equivalent
let revalidate_trigger = Computed::from({
let bearer_auth = bearer_auth.clone();
move |ctx| bearer_auth.get(ctx).map(|c| c.get(ctx))
});

let _drop = revalidate_trigger.subscribe({
let counter = counter.clone();
move |_| {
counter.set(counter.get() + 1);
}
});

assert_eq!(counter.get(), 1); // Initial subscription fire

token_value.set("token2".to_string());
assert_eq!(counter.get(), 2); // Should fire because of flattening
}

#[test]
fn test_nested_computed_subscription_no_flattening() {
let token_value = Value::new("token1".to_string());
let token_computed = token_value.to_computed();

let bearer_auth = Computed::from({
let token_computed = token_computed.clone();
move |_ctx| Some(token_computed.clone())
});

let counter = std::rc::Rc::new(std::cell::Cell::new(0));

// NO flattening: subcribing directly to bearer_auth
let _drop = bearer_auth.subscribe({
let counter = counter.clone();
move |_| {
counter.set(counter.get() + 1);
}
});

assert_eq!(counter.get(), 1); // Initial fire

token_value.set("token2".to_string());
assert_eq!(counter.get(), 1); // Does NOT fire because bearer_auth didn't change (the Option<Computed> is the same instance)
}
36 changes: 6 additions & 30 deletions crates/vertigo/src/computed/value.rs
Original file line number Diff line number Diff line change
@@ -1,27 +1,17 @@
use std::hash::Hash;
use std::rc::Rc;

use crate::{Context, Dependencies, DomNode, ToComputed};
use crate::{Context, DomNode, ToComputed};

use super::{
Computed, DropResource, GraphId, dependencies::get_dependencies, struct_mut::ValueMut,
};

struct ValueInner<T> {
dependencies: Rc<Dependencies>,
id: GraphId,
value: ValueMut<T>,
}

impl<T> Drop for ValueInner<T> {
fn drop(&mut self) {
self.dependencies
.graph
.external_connections
.unregister_connect(self.id);
}
}

/// A reactive value. Basic building block of app state.
///
/// Can be read or written.
Expand Down Expand Up @@ -63,7 +53,6 @@ impl<T: Clone + 'static> Value<T> {
pub fn new(value: T) -> Self {
Value {
inner: Rc::new(ValueInner {
dependencies: get_dependencies(),
id: GraphId::new_value(),
value: ValueMut::new(value),
}),
Expand All @@ -78,24 +67,11 @@ impl<T: Clone + 'static> Value<T> {
where
F: Fn(&Value<T>) -> DropResource + 'static,
{
let id = GraphId::new_value();

let value = Value {
inner: Rc::new(ValueInner {
dependencies: get_dependencies(),
id,
value: ValueMut::new(value),
}),
};

let computed = value.to_computed();

get_dependencies()
.graph
.external_connections
.register_connect(id, Rc::new(move || create(&value)));

computed
let value = Value::new(value);
let value_clone = value.clone();
value
.to_computed()
.when_connect(move || create(&value_clone))
}

/// Allows to set a new value if `T` doesn't implement [PartialEq].
Expand Down
Loading
Loading