From 20d035831ceafb3414d9724b1348b0b3c700a7e4 Mon Sep 17 00:00:00 2001 From: szagi3891 Date: Wed, 19 Aug 2026 08:55:37 +0200 Subject: [PATCH 01/10] Add a prototype reactive graph with equality cutoff. * Dirty nodes are processed in topological order after a transaction; dependents are enqueued only when a node's value actually changed, so a stable Computed leaves its fan-out untouched. * Require PartialEq on Computed instead of a custom equality function. * Cutoff uses T's PartialEq; from_always is gone, and event callbacks are plain closures. * Drop always_notify. * Dependents are enqueued only when a value changes, so subscribe_all is no longer needed. --- Taskfile.yaml | 5 + crates/vertigo/src/{computed => }/auto_map.rs | 2 +- crates/vertigo/src/computed/mod.rs | 78 --- .../vertigo/src/computed/tests/app_state.rs | 100 --- .../src/computed/tests/box_value_version.rs | 61 -- crates/vertigo/src/computed/tests/computed.rs | 599 ------------------ crates/vertigo/src/computed/tests/mod.rs | 6 - .../src/computed/tests/nested_reactivity.rs | 83 --- crates/vertigo/src/css/css_manager.rs | 2 +- crates/vertigo/src/css/css_structs.rs | 22 - crates/vertigo/src/css/next_id.rs | 2 +- crates/vertigo/src/css/tailwind_class.rs | 22 +- crates/vertigo/src/dev/inspect.rs | 6 +- crates/vertigo/src/dev/mod.rs | 2 +- crates/vertigo/src/dom/attr_value.rs | 40 +- crates/vertigo/src/dom/callback.rs | 72 +-- crates/vertigo/src/dom/dom_comment.rs | 7 +- crates/vertigo/src/dom/dom_element.rs | 7 +- crates/vertigo/src/dom/dom_element_class.rs | 4 +- crates/vertigo/src/dom/dom_text.rs | 5 +- crates/vertigo/src/dom/events/click_event.rs | 2 +- crates/vertigo/src/dom_macro/dom.rs | 47 +- .../src/driver_module/api/api_arguments.rs | 4 +- .../src/driver_module/api/api_fetch_cache.rs | 2 +- .../src/driver_module/api/api_location.rs | 2 +- .../src/driver_module/api/api_timers.rs | 3 +- .../src/driver_module/api/api_websocket.rs | 3 +- .../src/driver_module/api/callbacks.rs | 6 +- .../src/driver_module/api/server_handler.rs | 2 +- .../src/driver_module/api/utils/callbacks.rs | 6 +- crates/vertigo/src/driver_module/dom.rs | 7 +- crates/vertigo/src/driver_module/driver.rs | 14 +- .../src/driver_module/event_emitter.rs | 4 +- .../src/driver_module/utils/futures_spawn.rs | 2 +- crates/vertigo/src/fetch/cache_value.rs | 4 +- crates/vertigo/src/fetch/lazy_cache.rs | 9 +- crates/vertigo/src/fetch/lazy_list_cache.rs | 9 +- crates/vertigo/src/fetch/resource.rs | 4 +- .../src/{computed => }/keyed_computed_list.rs | 2 +- crates/vertigo/src/lib.rs | 66 +- crates/vertigo/src/reactive/computed.rs | 222 +++++++ crates/vertigo/src/reactive/context.rs | 35 + .../{computed => reactive}/drop_resource.rs | 2 +- crates/vertigo/src/reactive/graph/dirty.rs | 132 ++++ crates/vertigo/src/reactive/graph/edges.rs | 158 +++++ crates/vertigo/src/reactive/graph/hooks.rs | 41 ++ crates/vertigo/src/reactive/graph/mod.rs | 189 ++++++ crates/vertigo/src/reactive/graph/nodes.rs | 32 + crates/vertigo/src/reactive/graph/watch.rs | 66 ++ crates/vertigo/src/reactive/mod.rs | 67 ++ crates/vertigo/src/reactive/tests.rs | 354 +++++++++++ crates/vertigo/src/reactive/to_computed.rs | 40 ++ crates/vertigo/src/reactive/value.rs | 186 ++++++ crates/vertigo/src/reactive_old/compare.rs | 265 ++++++++ .../computed.rs} | 20 +- .../src/{computed => reactive_old}/context.rs | 5 +- .../dependencies/external_connections.rs | 3 +- .../dependencies/graph.rs | 2 +- .../dependencies/graph_connections.rs | 3 +- .../dependencies/graph_one_to_many.rs | 2 +- .../dependencies/hook.rs | 2 +- .../dependencies/mod.rs | 2 +- .../dependencies/refresh.rs | 3 +- .../dependencies/transaction_state.rs | 3 +- .../{computed => reactive_old}/graph_id.rs | 0 .../{computed => reactive_old}/graph_value.rs | 5 +- crates/vertigo/src/reactive_old/mod.rs | 26 + .../{computed => reactive_old}/reactive.rs | 2 +- .../{computed => reactive_old}/to_computed.rs | 0 .../src/{computed => reactive_old}/value.rs | 60 +- .../{computed => reactive_old}/value_inner.rs | 3 +- crates/vertigo/src/render/mod.rs | 2 +- crates/vertigo/src/render/render_list.rs | 12 +- crates/vertigo/src/render/render_value.rs | 97 ++- crates/vertigo/src/router.rs | 6 +- .../struct_mut/btree_map_mut.rs | 0 .../{computed => }/struct_mut/counter_mut.rs | 0 .../{computed => }/struct_mut/hash_map_mut.rs | 0 .../{computed => }/struct_mut/inner_value.rs | 0 .../src/{computed => }/struct_mut/mod.rs | 0 .../{computed => }/struct_mut/value_mut.rs | 0 .../struct_mut/vec_deque_mut.rs | 0 .../src/{computed => }/struct_mut/vec_mut.rs | 0 crates/vertigo/src/tests/dom/component.rs | 1 + .../tests/keyed_computed_list.rs | 8 +- crates/vertigo/src/tests/mod.rs | 2 + .../src/{computed => }/tests/value_copies.rs | 0 .../sudoku/component/render_cell_possible.rs | 12 +- tests/basic/src/lib.rs | 8 +- 89 files changed, 2169 insertions(+), 1232 deletions(-) rename crates/vertigo/src/{computed => }/auto_map.rs (97%) delete mode 100644 crates/vertigo/src/computed/mod.rs delete mode 100644 crates/vertigo/src/computed/tests/app_state.rs delete mode 100644 crates/vertigo/src/computed/tests/box_value_version.rs delete mode 100644 crates/vertigo/src/computed/tests/computed.rs delete mode 100644 crates/vertigo/src/computed/tests/mod.rs delete mode 100644 crates/vertigo/src/computed/tests/nested_reactivity.rs rename crates/vertigo/src/{computed => }/keyed_computed_list.rs (99%) create mode 100644 crates/vertigo/src/reactive/computed.rs create mode 100644 crates/vertigo/src/reactive/context.rs rename crates/vertigo/src/{computed => reactive}/drop_resource.rs (86%) create mode 100644 crates/vertigo/src/reactive/graph/dirty.rs create mode 100644 crates/vertigo/src/reactive/graph/edges.rs create mode 100644 crates/vertigo/src/reactive/graph/hooks.rs create mode 100644 crates/vertigo/src/reactive/graph/mod.rs create mode 100644 crates/vertigo/src/reactive/graph/nodes.rs create mode 100644 crates/vertigo/src/reactive/graph/watch.rs create mode 100644 crates/vertigo/src/reactive/mod.rs create mode 100644 crates/vertigo/src/reactive/tests.rs create mode 100644 crates/vertigo/src/reactive/to_computed.rs create mode 100644 crates/vertigo/src/reactive/value.rs create mode 100644 crates/vertigo/src/reactive_old/compare.rs rename crates/vertigo/src/{computed/computed_box.rs => reactive_old/computed.rs} (92%) rename crates/vertigo/src/{computed => reactive_old}/context.rs (93%) rename crates/vertigo/src/{computed => reactive_old}/dependencies/external_connections.rs (92%) rename crates/vertigo/src/{computed => reactive_old}/dependencies/graph.rs (98%) rename crates/vertigo/src/{computed => reactive_old}/dependencies/graph_connections.rs (98%) rename crates/vertigo/src/{computed => reactive_old}/dependencies/graph_one_to_many.rs (98%) rename crates/vertigo/src/{computed => reactive_old}/dependencies/hook.rs (86%) rename crates/vertigo/src/{computed => reactive_old}/dependencies/mod.rs (98%) rename crates/vertigo/src/{computed => reactive_old}/dependencies/refresh.rs (94%) rename crates/vertigo/src/{computed => reactive_old}/dependencies/transaction_state.rs (97%) rename crates/vertigo/src/{computed => reactive_old}/graph_id.rs (100%) rename crates/vertigo/src/{computed => reactive_old}/graph_value.rs (96%) create mode 100644 crates/vertigo/src/reactive_old/mod.rs rename crates/vertigo/src/{computed => reactive_old}/reactive.rs (98%) rename crates/vertigo/src/{computed => reactive_old}/to_computed.rs (100%) rename crates/vertigo/src/{computed => reactive_old}/value.rs (65%) rename crates/vertigo/src/{computed => reactive_old}/value_inner.rs (94%) rename crates/vertigo/src/{computed => }/struct_mut/btree_map_mut.rs (100%) rename crates/vertigo/src/{computed => }/struct_mut/counter_mut.rs (100%) rename crates/vertigo/src/{computed => }/struct_mut/hash_map_mut.rs (100%) rename crates/vertigo/src/{computed => }/struct_mut/inner_value.rs (100%) rename crates/vertigo/src/{computed => }/struct_mut/mod.rs (100%) rename crates/vertigo/src/{computed => }/struct_mut/value_mut.rs (100%) rename crates/vertigo/src/{computed => }/struct_mut/vec_deque_mut.rs (100%) rename crates/vertigo/src/{computed => }/struct_mut/vec_mut.rs (100%) rename crates/vertigo/src/{computed => }/tests/keyed_computed_list.rs (99%) rename crates/vertigo/src/{computed => }/tests/value_copies.rs (100%) diff --git a/Taskfile.yaml b/Taskfile.yaml index 94bca0c83..73f2f4e55 100644 --- a/Taskfile.yaml +++ b/Taskfile.yaml @@ -11,6 +11,11 @@ tasks: cmds: - cargo test --all-features + reactive-compare: + desc: Compare old vs new reactive graph performance + cmds: + - cargo test -p vertigo --lib reactive_old::compare -- --nocapture + automated-tests: desc: "NOTE: WebDriver on localhost:9515 needs to be running" cmds: diff --git a/crates/vertigo/src/computed/auto_map.rs b/crates/vertigo/src/auto_map.rs similarity index 97% rename from crates/vertigo/src/computed/auto_map.rs rename to crates/vertigo/src/auto_map.rs index 225780e25..b35e2bf61 100644 --- a/crates/vertigo/src/computed/auto_map.rs +++ b/crates/vertigo/src/auto_map.rs @@ -1,7 +1,7 @@ use std::fmt::Debug; use std::{hash::Hash, rc::Rc}; -use crate::computed::struct_mut::HashMapMut; +use crate::struct_mut::HashMapMut; type CreateType = Box, &K) -> V>; diff --git a/crates/vertigo/src/computed/mod.rs b/crates/vertigo/src/computed/mod.rs deleted file mode 100644 index f60b209a8..000000000 --- a/crates/vertigo/src/computed/mod.rs +++ /dev/null @@ -1,78 +0,0 @@ -mod auto_map; -mod computed_box; -pub mod context; -mod dependencies; -mod keyed_computed_list; -pub use dependencies::{Dependencies, get_dependencies}; -mod drop_resource; -mod graph_id; -mod graph_value; -mod reactive; -pub mod struct_mut; -mod to_computed; -mod value; -mod value_inner; - -#[cfg(test)] -mod tests; - -pub use auto_map::AutoMap; -pub use computed_box::Computed; -pub use drop_resource::DropResource; -pub use graph_id::GraphId; -pub use graph_value::GraphValue; -pub use keyed_computed_list::{KeyedListItem, keyed_computed_list}; -pub use reactive::Reactive; -pub use to_computed::ToComputed; -pub use value::Value; - -/// Allows to create `Computed` out of `Value`, `Value`, ... -/// -/// # Examples -/// -/// ``` -/// use vertigo::{Value, computed_tuple}; -/// -/// let value1 = Value::new(true); -/// let value2 = Value::new(5); -/// let value3 = Value::new("Hello tuple!".to_string()); -/// -/// let my_tuple = computed_tuple!(value1, value2, value3); -/// -/// vertigo::transaction(|ctx| { -/// assert!(my_tuple.get(ctx).0); -/// assert_eq!(my_tuple.get(ctx).1, 5); -/// assert_eq!(&my_tuple.get(ctx).2, "Hello tuple!"); -/// }); -/// ``` -/// -/// ``` -/// use vertigo::{Value, computed_tuple}; -/// -/// let values = (Value::new(true), Value::new(5)); -/// let value3 = Value::new("Hello tuple!".to_string()); -/// -/// let my_tuple = computed_tuple!(a => values.0, b => values.1, c => value3); -/// -/// vertigo::transaction(|ctx| { -/// assert!(my_tuple.get(ctx).0); -/// assert_eq!(my_tuple.get(ctx).1, 5); -/// assert_eq!(&my_tuple.get(ctx).2, "Hello tuple!"); -/// }); -/// ``` -#[macro_export] -macro_rules! computed_tuple { - ($($arg: tt),*) => {{ - let ($($arg),*) = ($($arg.clone()),*); - $crate::Computed::from(move |ctx| { - ($($arg.get(ctx)),*) - }) - }}; - - ($($name: ident => $arg: expr),*) => {{ - let ($($name),*) = ($(($arg).clone()),*); - $crate::Computed::from(move |ctx| { - ($($name.get(ctx)),*) - }) - }}; -} diff --git a/crates/vertigo/src/computed/tests/app_state.rs b/crates/vertigo/src/computed/tests/app_state.rs deleted file mode 100644 index c21da5e3c..000000000 --- a/crates/vertigo/src/computed/tests/app_state.rs +++ /dev/null @@ -1,100 +0,0 @@ -use crate::computed::{Computed, Value, tests::box_value_version::SubscribeValueVer}; - -struct AppState { - value1: Value, - value2: Value, - value3: Value, - sum: Computed, -} - -impl AppState { - pub fn new() -> std::rc::Rc { - let value1 = Value::new(1); - let value2 = Value::new(2); - let value3 = Value::new(3); - - let sum = { - let com1 = value1.to_computed(); - let com2 = value2.to_computed(); - let com3 = value3.to_computed(); - - Computed::from(move |context| { - let val1 = com1.get(context); - let val2 = com2.get(context); - let val3 = com3.get(context); - - val1 + val2 + val3 - }) - }; - - std::rc::Rc::new(AppState { - value1, - value2, - value3, - sum, - }) - } -} - -#[test] -fn test_app_state() { - let app_state = AppState::new(); - - let sum3 = { - let app_state = app_state.clone(); - - Computed::from(move |context| -> i32 { - let val1 = app_state.value1.get(context); - let val3 = app_state.value3.get(context); - - val1 + val3 - }) - }; - - let mut sum3_box = SubscribeValueVer::new(sum3); - - assert_eq!(sum3_box.get(), (4, 1)); // 1 _ 3 - - app_state.value1.set(2); // 2 _ 3 - - assert_eq!(sum3_box.get(), (5, 2)); - - app_state.value1.set(3); // 3 _ 3 - assert_eq!(sum3_box.get(), (6, 3)); - - app_state.value2.set(4); // 3 _ 3 - assert_eq!(sum3_box.get(), (6, 3)); - - app_state.value2.set(5); // 3 _ 3 - assert_eq!(sum3_box.get(), (6, 3)); - - app_state.value3.set(6); // 3 _ 6 - assert_eq!(sum3_box.get(), (9, 4)); - - app_state.value3.set(7); // 3 _ 7 - assert_eq!(sum3_box.get(), (10, 5)); - - sum3_box.off(); - - app_state.value3.set(8); - - assert_eq!(sum3_box.get(), (10, 5)); - - let mut sum_total = SubscribeValueVer::new(app_state.sum.clone()); - - assert_eq!((sum3_box.get(), sum_total.get()), ((10, 5), (16, 1))); - - app_state.value1.set(2); - assert_eq!((sum3_box.get(), sum_total.get()), ((10, 5), (15, 2))); - - app_state.value2.set(3); - assert_eq!((sum3_box.get(), sum_total.get()), ((10, 5), (13, 3))); - - app_state.value3.set(4); - assert_eq!((sum3_box.get(), sum_total.get()), ((10, 5), (9, 4))); - - app_state.value3.set(4); - assert_eq!((sum3_box.get(), sum_total.get()), ((10, 5), (9, 4))); - - sum_total.off(); -} diff --git a/crates/vertigo/src/computed/tests/box_value_version.rs b/crates/vertigo/src/computed/tests/box_value_version.rs deleted file mode 100644 index b745bd4d0..000000000 --- a/crates/vertigo/src/computed/tests/box_value_version.rs +++ /dev/null @@ -1,61 +0,0 @@ -use std::rc::Rc; - -use crate::computed::{Computed, DropResource, struct_mut::ValueMut}; - -struct SubscribeValueVerInner { - version: ValueMut, - value: ValueMut>, -} - -impl SubscribeValueVerInner { - pub fn new() -> Rc> { - Rc::new(SubscribeValueVerInner { - version: ValueMut::new(0), - value: ValueMut::new(None), - }) - } -} - -pub struct SubscribeValueVer { - client: Option, - value: Rc>, -} - -impl SubscribeValueVer { - pub fn new(com: Computed) -> SubscribeValueVer { - let value = SubscribeValueVerInner::new(); - - let client = { - let value = value.clone(); - com.subscribe(move |new_value| { - value.value.set(Some(new_value)); - let current = value.version.get(); - value.version.set(current + 1); - }) - }; - - SubscribeValueVer { - client: Some(client), - value, - } - } - - pub fn get(&self) -> (T, u32) { - let value = self.value.value.get(); - - let value = match value { - Some(value) => value, - None => { - panic!("expected value"); - } - }; - - let version = self.value.version.get(); - - (value, version) - } - - pub fn off(&mut self) { - self.client = None; - } -} diff --git a/crates/vertigo/src/computed/tests/computed.rs b/crates/vertigo/src/computed/tests/computed.rs deleted file mode 100644 index beb4c597e..000000000 --- a/crates/vertigo/src/computed/tests/computed.rs +++ /dev/null @@ -1,599 +0,0 @@ -use std::rc::Rc; - -use crate::{ - computed::{ - Computed, DropResource, Value, get_dependencies, struct_mut::ValueMut, - tests::box_value_version::SubscribeValueVer, - }, - transaction, -}; - -#[test] -fn basic() { - let value1: Value = Value::new(1); - let value2: Value = Value::new(2); - - let sum: Computed = { - let com1 = value1.to_computed(); - let com2 = value2.to_computed(); - - Computed::from(move |context| -> i32 { - let value1 = com1.get(context); - let value2 = com2.get(context); - - value1 + value2 - }) - }; - - let mut sum_value = SubscribeValueVer::new(sum); - - assert_eq!(sum_value.get(), (3, 1)); - - value1.set(4); - assert_eq!(sum_value.get(), (6, 2)); - - value2.set(5); - assert_eq!(sum_value.get(), (9, 3)); - - sum_value.off(); - - value2.set(99); - assert_eq!(sum_value.get(), (9, 3)); -} - -#[test] -fn basic2() { - let val1 = Value::new(4); - let val2 = Value::new(5); - - let com1: Computed = val1.to_computed(); - let com2: Computed = val2.to_computed(); - - let sum = Computed::from(move |context| { - let a = com1.get(context); - let b = com2.get(context); - a + b - }); - - let sum2 = sum.map(|value: i32| -> i32 { 2 * (value) }); - - let mut sum_box1 = SubscribeValueVer::new(sum); - let mut sum_box2 = SubscribeValueVer::new(sum2); - - assert_eq!(sum_box1.get(), (9, 1)); - assert_eq!(sum_box2.get(), (18, 1)); - - val1.set(111); - - assert_eq!(sum_box1.get(), (116, 2)); - assert_eq!(sum_box2.get(), (232, 2)); - - val2.set(888); - - assert_eq!(sum_box1.get(), (999, 3)); - assert_eq!(sum_box2.get(), (1998, 3)); - - sum_box1.off(); - sum_box2.off(); - - val2.set(999); - - assert_eq!(sum_box1.get(), (999, 3)); - assert_eq!(sum_box2.get(), (1998, 3)); -} - -#[test] -fn pointers() { - // pointer conversion - - fn foo1() -> i32 { - 1 - } - - fn foo2() -> i32 { - 2 - } - - fn foo3(_yy: i32) -> i32 { - 3 - } - - let pointer1: u64 = foo1 as *const () as u64; - let pointer2: u64 = foo2 as *const () as u64; - let pointer11: u64 = foo1 as *const () as u64; - let pointer4: u64 = foo3 as *const () as u64; - - assert!(pointer1 != pointer2); - assert!(pointer1 == pointer11); - assert!(pointer1 != pointer4); -} - -#[test] -fn test_subscription() { - let val1 = Value::new(1); - let val2 = Value::new(2); - let val3 = Value::new(3); - - let com1: Computed = val1.to_computed(); - let com2: Computed = val2.to_computed(); - #[allow(unused_variables)] - let com3: Computed = val3.to_computed(); - - let sum = Computed::from(move |context| -> i32 { - let value1 = com1.get(context); - let value2 = com2.get(context); - - value1 + value2 - }); - - let mut sum_value = SubscribeValueVer::new(sum); - - assert_eq!(sum_value.get(), (3, 1)); - val1.set(2); - assert_eq!(sum_value.get(), (4, 2)); - val2.set(10); - assert_eq!(sum_value.get(), (12, 3)); - val3.set(10); - assert_eq!(sum_value.get(), (12, 3)); - val2.set(20); - assert_eq!(sum_value.get(), (22, 4)); - - sum_value.off(); - - val1.set(2); - assert_eq!(sum_value.get(), (22, 4)); - val1.set(2); - assert_eq!(sum_value.get(), (22, 4)); - val2.set(2); - assert_eq!(sum_value.get(), (22, 4)); - val3.set(2); - assert_eq!(sum_value.get(), (22, 4)); -} - -#[test] -fn test_computed_cache() { - let root = get_dependencies(); - - assert_eq!(root.graph.connections.all_connections_len(), 0); - - { - //a - //b - //c = a + b - //d = c % 2; - - let a = Value::new(1); - let b = Value::new(2); - - let c: Computed = { - let a = a.clone(); - - Computed::from(move |context| { - let a_val = a.get(context); - let b_val = b.get(context); - - a_val + b_val - }) - }; - - let d: Computed = { - //is even - let c = c.clone(); - Computed::from(move |context| -> bool { - let c_value = c.get(context); - - c_value.is_multiple_of(2) - }) - }; - - let mut c = SubscribeValueVer::new(c); - let mut d = SubscribeValueVer::new(d); - - assert_eq!(c.get(), (3, 1)); - assert_eq!(d.get(), (false, 1)); - - a.set(2); - - assert_eq!(c.get(), (4, 2)); - assert_eq!(d.get(), (true, 2)); - - a.set(2); - - assert_eq!(c.get(), (4, 2)); - assert_eq!(d.get(), (true, 2)); - - a.set(4); - - assert_eq!(c.get(), (6, 3)); - assert_eq!(d.get(), (true, 2)); - - assert_eq!(root.graph.connections.all_connections_len(), 5); - - c.off(); - d.off(); - - assert_eq!(root.graph.connections.all_connections_len(), 0); - } - - assert_eq!(root.graph.connections.all_connections_len(), 0); -} - -#[test] -fn test_computed_new_value() { - /* - a - b - c - d = a + b - e = d + c - */ - - #![allow(clippy::many_single_char_names)] - - let root = get_dependencies(); - - let a = Value::new(0); - let b = Value::new(0); - let c = Value::new(0); - - let d: Computed = { - let a = a.clone(); - - Computed::from(move |context| { - let a_val = a.get(context); - let b_val = b.get(context); - - a_val + b_val - }) - }; - - let e: Computed = { - //is even - let d = d.clone(); - let c = c.clone(); - Computed::from(move |context| -> u32 { - let d_val = d.get(context); - let c_val = c.get(context); - - d_val + c_val - }) - }; - - let mut d = SubscribeValueVer::new(d); - let mut e = SubscribeValueVer::new(e); - - assert_eq!(d.get(), (0, 1)); - assert_eq!(e.get(), (0, 1)); - - a.set(33); - assert_eq!(d.get(), (33, 2)); - assert_eq!(e.get(), (33, 2)); - - c.set(66); - assert_eq!(d.get(), (33, 2)); - assert_eq!(e.get(), (99, 3)); - - d.off(); - e.off(); - assert_eq!(root.graph.connections.all_connections_len(), 0); -} - -#[test] -fn test_computed_new_value2() { - #![allow(clippy::many_single_char_names)] - - let root = get_dependencies(); - - let a = Value::new(0); - let b = Value::new(0); - - let d: Computed = { - let a = a.clone(); - let b = b.clone(); - Computed::from(move |context| a.get(context) + b.get(context)) - }; - - let mut d = SubscribeValueVer::new(d); - - assert_eq!(d.get(), (0, 1)); - a.set(2); - assert_eq!(d.get(), (2, 2)); - b.set(9); - assert_eq!(d.get(), (11, 3)); - a.set(3); - assert_eq!(d.get(), (12, 4)); - a.set(4); - assert_eq!(d.get(), (13, 5)); - - d.off(); - assert_eq!(root.graph.connections.all_connections_len(), 0); -} - -#[test] -fn test_computed_switch_subscription() { - #[derive(Clone, PartialEq)] - enum Switch { - Ver1, - Ver2, - Ver3, - } - - //a, b, c - - let root = get_dependencies(); - - let switch = Value::new(Switch::Ver1); - let a = Value::new(0); - let b = Value::new(0); - let c = Value::new(0); - - let sum: Computed = { - let switch = switch.clone(); - let a = a.clone(); - let b = b.clone(); - let c = c.clone(); - - Computed::from(move |context| -> u32 { - let switch_value = switch.get(context); - - match switch_value { - Switch::Ver1 => a.get(context), - Switch::Ver2 => { - let a_value = a.get(context); - let b_value = b.get(context); - a_value + b_value - } - Switch::Ver3 => { - let a_value = a.get(context); - let b_value = b.get(context); - let c_value = c.get(context); - a_value + b_value + c_value - } - } - }) - }; - - assert_eq!(root.graph.connections.all_connections_len(), 0); - let mut sum = SubscribeValueVer::new(sum); - assert_eq!(root.graph.connections.all_connections_len(), 3); - - assert_eq!(sum.get(), (0, 1)); - assert_eq!(root.graph.connections.all_connections_len(), 3); - - a.set(1); - assert_eq!(sum.get(), (1, 2)); - assert_eq!(root.graph.connections.all_connections_len(), 3); - - b.set(1); - assert_eq!(sum.get(), (1, 2)); - c.set(1); - assert_eq!(sum.get(), (1, 2)); - - a.set(0); - b.set(0); - c.set(0); - assert_eq!(sum.get(), (0, 3)); - - assert_eq!(root.graph.connections.all_connections_len(), 3); - switch.set(Switch::Ver2); - assert_eq!(root.graph.connections.all_connections_len(), 4); - - assert_eq!(sum.get(), (0, 3)); //no rerender - - a.set(1); - - assert_eq!(root.graph.connections.all_connections_len(), 4); - - assert_eq!(sum.get(), (1, 4)); - b.set(1); - assert_eq!(sum.get(), (2, 5)); - c.set(1); - assert_eq!(sum.get(), (2, 5)); - - a.set(0); - b.set(0); - c.set(0); - assert_eq!(sum.get(), (0, 7)); - - switch.set(Switch::Ver3); - assert_eq!(sum.get(), (0, 7)); //no rerender - - assert_eq!(root.graph.connections.all_connections_len(), 5); - - a.set(1); - assert_eq!(sum.get(), (1, 8)); - b.set(1); - assert_eq!(sum.get(), (2, 9)); - c.set(1); - assert_eq!(sum.get(), (3, 10)); - - root.transaction(|_| { - a.set(0); - b.set(0); - c.set(0); - }); - - assert_eq!(sum.get(), (0, 11)); - - sum.off(); - assert_eq!(root.graph.connections.all_connections_len(), 0); -} - -#[test] -fn test_transaction() { - let root = get_dependencies(); - assert_eq!(root.graph.connections.all_connections_len(), 0); - - let val1 = Value::new(1); - let val2 = Value::new(2); - - let val3 = Computed::from({ - let val1 = val1.clone(); - let val2 = val2.clone(); - - move |context| val1.get(context) + val2.get(context) - }); - - let mut val2sub = SubscribeValueVer::new(val3); - - assert_eq!(val2sub.get(), (3, 1)); - - val1.set(444); - - assert_eq!(val2sub.get(), (446, 2)); - - root.transaction(|context| { - assert_eq!(val2sub.get(), (446, 2)); - val1.set(222); - assert_eq!(val1.get(context), 222); - assert_eq!(val2sub.get(), (446, 2)); - val2.set(333); - assert_eq!(val2.get(context), 333); - assert_eq!(val2sub.get(), (446, 2)); - }); - - root.transaction(|context| { - assert_eq!(val1.get(context), 222); - assert_eq!(val2.get(context), 333); - }); - - assert_eq!(val2sub.get(), (555, 3)); - - val2sub.off(); - assert_eq!(root.graph.connections.all_connections_len(), 0); -} - -#[test] -#[allow(clippy::bool_assert_comparison)] -fn test_connect() { - let is_subscribe = Rc::new(ValueMut::new(false)); - - let value = Value::with_connect(10, { - let is_subscribe = is_subscribe.clone(); - - move |_value| { - is_subscribe.set(true); - - DropResource::new({ - let is_subscribe = is_subscribe.clone(); - move || { - is_subscribe.set(false); - } - }) - } - }); - - assert_eq!(is_subscribe.get(), false); - - let current_value = Rc::new(ValueMut::new(0)); - - let client = value.clone().subscribe({ - let current_value = current_value.clone(); - move |val| { - current_value.set(val); - } - }); - - assert_eq!(is_subscribe.get(), true); - - drop(client); - - assert_eq!(is_subscribe.get(), false); - - let client = value.subscribe({ - move |val| { - current_value.set(val); - } - }); - - assert_eq!(is_subscribe.get(), true); - - drop(client); - - assert_eq!(is_subscribe.get(), false); -} - -#[test] -fn test_without_subscription() { - let value = Value::new(2); - - let comp_2 = { - let v = value.clone(); - Computed::from(move |context| v.get(context) * 2) - }; - - transaction(|context| { - assert_eq!(comp_2.get(context), 4); - }); - - value.set(6); - - transaction(|context| { - assert_eq!(comp_2.get(context), 12); - }); -} - -#[test] -fn test_set_if_changed() { - let value = Value::new(2); - - let value_com = value.to_computed(); - - let value_com = value_com.map(|item| item); - - fn build(value: &Computed) -> (Rc>, DropResource) { - let boxy = Rc::new(ValueMut::new(0)); - - let router = Computed::from({ - let value = value.clone(); - - move |context| value.get(context) - }); - - let router = Computed::from(move |context| router.get(context)); - - let router = router.map(|item| item); - - let router = router.map(|item| item); - - let router = router.map(|item| item); - - let client = router.subscribe({ - let boxy = boxy.clone(); - move |sub_value| { - println!("callback"); - boxy.set(sub_value); - } - }); - - (boxy, client) - } - - let (boxy, client) = build(&value_com); - let (boxy2, client2) = build(&value_com); - let (boxy3, client3) = build(&value_com); - - assert_eq!(boxy.get(), 2); - - value.set(3); - assert_eq!(boxy.get(), 3); - - value.set(4); - assert_eq!(boxy.get(), 4); - value.set(4); - assert_eq!(boxy.get(), 4); - - value.set(5); - assert_eq!(boxy.get(), 5); - value.set(5); - assert_eq!(boxy.get(), 5); - - value.set(6); - assert_eq!(boxy.get(), 6); - assert_eq!(boxy2.get(), 6); - assert_eq!(boxy3.get(), 6); - - drop(client); - drop(client2); - drop(client3); -} diff --git a/crates/vertigo/src/computed/tests/mod.rs b/crates/vertigo/src/computed/tests/mod.rs deleted file mode 100644 index 2c1bab3c0..000000000 --- a/crates/vertigo/src/computed/tests/mod.rs +++ /dev/null @@ -1,6 +0,0 @@ -pub mod app_state; -pub mod box_value_version; -pub mod computed; -pub mod keyed_computed_list; -pub mod nested_reactivity; -pub mod value_copies; diff --git a/crates/vertigo/src/computed/tests/nested_reactivity.rs b/crates/vertigo/src/computed/tests/nested_reactivity.rs deleted file mode 100644 index 888396f8e..000000000 --- a/crates/vertigo/src/computed/tests/nested_reactivity.rs +++ /dev/null @@ -1,83 +0,0 @@ -use crate::{Computed, Value, transaction}; - -#[test] -fn test_nested_reactivity() { - let val = Value::new(1); - - // Generate computed from computed - let computed = Computed::from({ - let val = val.clone(); - move |ctx| val.to_computed().get(ctx) - }); - - let mut result = 0; - transaction(|ctx| { - result = computed.get(ctx); - }); - assert_eq!(result, 1); - - val.set(2); - - transaction(|ctx| { - result = computed.get(ctx); - }); - 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>> - 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 is the same instance) -} diff --git a/crates/vertigo/src/css/css_manager.rs b/crates/vertigo/src/css/css_manager.rs index 8749f176a..ef7d9a8b8 100644 --- a/crates/vertigo/src/css/css_manager.rs +++ b/crates/vertigo/src/css/css_manager.rs @@ -3,8 +3,8 @@ use std::rc::Rc; use vertigo_macro::store; use crate::{ - computed::struct_mut::{HashMapMut, InnerValue}, driver_module::get_driver_dom, + struct_mut::{HashMapMut, InnerValue}, }; use super::{ diff --git a/crates/vertigo/src/css/css_structs.rs b/crates/vertigo/src/css/css_structs.rs index 677e35415..a7a15c2b0 100644 --- a/crates/vertigo/src/css/css_structs.rs +++ b/crates/vertigo/src/css/css_structs.rs @@ -1,7 +1,5 @@ use std::ops::{Add, AddAssign}; -use crate::Computed; - /// Css chunk, represented either as static or dynamic string. #[derive(Debug, Clone, PartialEq, Eq)] pub enum CssGroup { @@ -127,26 +125,6 @@ impl AddAssign<&Css> for &mut Css { } } -impl Add for Computed { - type Output = Computed; - - fn add(self, rhs: Css) -> Self::Output { - self.map(move |left| left.extend(rhs.clone())) - } -} - -impl Add> for Computed { - type Output = Computed; - - fn add(self, rhs: Computed) -> Self::Output { - Computed::from({ - let left = self.clone(); - let right = rhs.clone(); - move |ctx| left.get(ctx) + right.get(ctx) - }) - } -} - #[cfg(test)] mod tests { use super::{Css, CssGroup}; diff --git a/crates/vertigo/src/css/next_id.rs b/crates/vertigo/src/css/next_id.rs index 0f1371a94..437997ab7 100644 --- a/crates/vertigo/src/css/next_id.rs +++ b/crates/vertigo/src/css/next_id.rs @@ -1,4 +1,4 @@ -use crate::computed::struct_mut::ValueMut; +use crate::struct_mut::ValueMut; pub struct NextId { counter: ValueMut, diff --git a/crates/vertigo/src/css/tailwind_class.rs b/crates/vertigo/src/css/tailwind_class.rs index 6b7bf94f5..8d0cd7dc2 100644 --- a/crates/vertigo/src/css/tailwind_class.rs +++ b/crates/vertigo/src/css/tailwind_class.rs @@ -3,7 +3,7 @@ use std::{ ops::{Add, AddAssign}, }; -use crate::{AttrValue, Computed}; +use crate::AttrValue; /// This represents a tailwind class. Use [tw!](crate::tw!) macro to create one. #[derive(Clone, PartialEq, Eq)] @@ -63,26 +63,6 @@ impl AddAssign for TwClass { } } -impl Add for Computed { - type Output = Computed; - - fn add(self, rhs: TwClass) -> Self::Output { - self.map(move |left| left.join(&rhs)) - } -} - -impl Add> for Computed { - type Output = Computed; - - fn add(self, rhs: Computed) -> Self::Output { - Computed::from({ - let left = self.clone(); - let right = rhs.clone(); - move |ctx| left.get(ctx) + right.get(ctx) - }) - } -} - #[cfg(test)] mod tests { use super::TwClass; diff --git a/crates/vertigo/src/dev/inspect.rs b/crates/vertigo/src/dev/inspect.rs index da40ca712..9ea2cc369 100644 --- a/crates/vertigo/src/dev/inspect.rs +++ b/crates/vertigo/src/dev/inspect.rs @@ -13,11 +13,9 @@ mod logs { use vertigo_macro::store; use crate::{ - computed::{ - DropResource, - struct_mut::{ValueMut, VecMut}, - }, + DropResource, dev::inspect::DriverDomCommand, + struct_mut::{ValueMut, VecMut}, }; struct LogActive { diff --git a/crates/vertigo/src/dev/mod.rs b/crates/vertigo/src/dev/mod.rs index 1fe9f7893..f0dc2027c 100644 --- a/crates/vertigo/src/dev/mod.rs +++ b/crates/vertigo/src/dev/mod.rs @@ -13,10 +13,10 @@ pub use ssr_fetch_response::{ }; pub use super::{ - computed::struct_mut::{BTreeMapMut, HashMapMut, ValueMut, VecDequeMut, VecMut}, driver_module::{ driver::{VERTIGO_MOUNT_POINT_PLACEHOLDER, VERTIGO_PUBLIC_BUILD_PATH_PLACEHOLDER}, js_value::{JsJsonListDecoder, MemoryBlock, MemoryBlockRead, MemoryBlockWrite}, }, future_box::{FutureBox, FutureBoxSend}, + struct_mut::{BTreeMapMut, HashMapMut, ValueMut, VecDequeMut, VecMut}, }; diff --git a/crates/vertigo/src/dom/attr_value.rs b/crates/vertigo/src/dom/attr_value.rs index 1204a04c0..852444ee1 100644 --- a/crates/vertigo/src/dom/attr_value.rs +++ b/crates/vertigo/src/dom/attr_value.rs @@ -12,7 +12,7 @@ pub enum AttrValue { } impl AttrValue { - pub fn get(&self, ctx: &crate::computed::context::Context) -> Option> { + pub fn get(&self, ctx: &crate::Context) -> Option> { match self { AttrValue::String(s) => Some(s.clone()), AttrValue::Computed(c) => Some(Rc::new(c.get(ctx))), @@ -61,12 +61,46 @@ impl AttrValue { } } -impl From for AttrValue { - fn from(value: K) -> Self { +impl From for AttrValue { + fn from(value: String) -> Self { + AttrValue::String(Rc::new(value)) + } +} + +impl From<&&str> for AttrValue { + fn from(value: &&str) -> Self { + AttrValue::from(*value) + } +} + +impl From<&str> for AttrValue { + fn from(value: &str) -> Self { AttrValue::String(Rc::new(value.to_string())) } } +impl From<&String> for AttrValue { + fn from(value: &String) -> Self { + AttrValue::String(Rc::new(value.clone())) + } +} + +macro_rules! impl_from_display_for_attrvalue { + ($($typename:ty),* $(,)?) => { + $( + impl From<$typename> for AttrValue { + fn from(value: $typename) -> Self { + AttrValue::String(Rc::new(value.to_string())) + } + } + )* + }; +} + +impl_from_display_for_attrvalue!( + bool, char, u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize, f32, f64 +); + macro_rules! impl_from_computed_for_attrvalue { ($typename:ty, $variant:ident, |$var:ident| $body:expr) => { impl From<$typename> for AttrValue { diff --git a/crates/vertigo/src/dom/callback.rs b/crates/vertigo/src/dom/callback.rs index 98f050ad7..f3ac6981e 100644 --- a/crates/vertigo/src/dom/callback.rs +++ b/crates/vertigo/src/dom/callback.rs @@ -1,107 +1,51 @@ use std::rc::Rc; -use vertigo_macro::bind; - -use crate::computed::{Computed, DropResource, struct_mut::ValueMut}; - -pub enum Callback { +pub enum Callback { Basic(Rc R + 'static>), - Computed(Computed R + 'static>>), } -impl From R + 'static>> for Callback { +impl From R + 'static>> for Callback { fn from(value: Rc R + 'static>) -> Self { Callback::Basic(value) } } -impl R + 'static> From for Callback { +impl R + 'static> From for Callback { fn from(value: F) -> Self { Callback::Basic(Rc::new(value)) } } -impl From R + 'static>>> for Callback { - fn from(value: Computed R + 'static>>) -> Self { - Callback::Computed(value) - } -} - impl Callback { - pub fn subscribe(&self) -> (Rc R + 'static>, Option) { + pub fn subscribe(&self) -> (Rc R + 'static>, Option) { match self { Self::Basic(func) => (func.clone(), None), - Self::Computed(computed) => { - let current = Rc::new(ValueMut::new(None)); - - let drop = computed.clone().subscribe_all(bind!(current, |new_fn| { - current.set(Some(new_fn)); - })); - - let callback = Rc::new(move || -> R { - let callback = current.get(); - - let Some(callback) = callback else { - unreachable!(); - }; - - callback() - }); - - (callback, Some(drop)) - } } } } -pub enum Callback1 { +pub enum Callback1 { Basic(Rc R + 'static>), Rc(Rc R + 'static>), - Computed(Computed R + 'static>>), } -impl R + 'static> From for Callback1 { +impl R + 'static> From for Callback1 { fn from(value: F) -> Self { Callback1::Basic(Rc::new(value)) } } -impl From R + 'static>> for Callback1 { +impl From R + 'static>> for Callback1 { fn from(value: Rc R + 'static>) -> Self { Callback1::Rc(value) } } -impl From R + 'static>>> for Callback1 { - fn from(value: Computed R + 'static>>) -> Self { - Callback1::Computed(value) - } -} - impl Callback1 { - pub fn subscribe(&self) -> (Rc R + 'static>, Option) { + pub fn subscribe(&self) -> (Rc R + 'static>, Option) { match self { Self::Basic(func) => (func.clone(), None), Self::Rc(func) => (func.clone(), None), - Self::Computed(computed) => { - let current = Rc::new(ValueMut::new(None)); - - let drop = computed.clone().subscribe_all(bind!(current, |new_fn| { - current.set(Some(new_fn)); - })); - - let callback = Rc::new(move |param: T| -> R { - let callback = current.get(); - - let Some(callback) = callback else { - unreachable!(); - }; - - callback(param) - }); - - (callback, Some(drop)) - } } } } diff --git a/crates/vertigo/src/dom/dom_comment.rs b/crates/vertigo/src/dom/dom_comment.rs index 8d27a813b..9fc517f87 100644 --- a/crates/vertigo/src/dom/dom_comment.rs +++ b/crates/vertigo/src/dom/dom_comment.rs @@ -1,12 +1,9 @@ use std::rc::Rc; use crate::{ - DomNode, - computed::{ - DropResource, - struct_mut::{ValueMut, VecMut}, - }, + DomNode, DropResource, driver_module::get_driver_dom, + struct_mut::{ValueMut, VecMut}, }; use super::dom_id::DomId; diff --git a/crates/vertigo/src/dom/dom_element.rs b/crates/vertigo/src/dom/dom_element.rs index 9f15595df..586a4dbd8 100644 --- a/crates/vertigo/src/dom/dom_element.rs +++ b/crates/vertigo/src/dom/dom_element.rs @@ -1,13 +1,10 @@ use std::rc::Rc; use crate::{ - AttrGroupValue, Computed, DomText, DropFileItem, JsJson, - computed::{ - DropResource, - struct_mut::{VecDequeMut, VecMut}, - }, + AttrGroupValue, Computed, DomText, DropFileItem, DropResource, JsJson, dev::JsJsonListDecoder, driver_module::{StaticString, api::api_callbacks, get_driver_dom}, + struct_mut::{VecDequeMut, VecMut}, }; use super::{ diff --git a/crates/vertigo/src/dom/dom_element_class.rs b/crates/vertigo/src/dom/dom_element_class.rs index bbbff73fd..28438b9a6 100644 --- a/crates/vertigo/src/dom/dom_element_class.rs +++ b/crates/vertigo/src/dom/dom_element_class.rs @@ -1,10 +1,10 @@ use std::rc::Rc; use crate::{ - Css, DomId, - computed::{DropResource, struct_mut::ValueMut}, + Css, DomId, DropResource, css::get_css_manager, driver_module::{StaticString, get_driver_dom}, + struct_mut::ValueMut, }; struct DomElementClassMergeInner { diff --git a/crates/vertigo/src/dom/dom_text.rs b/crates/vertigo/src/dom/dom_text.rs index 3cc3b8781..e9bbc5d42 100644 --- a/crates/vertigo/src/dom/dom_text.rs +++ b/crates/vertigo/src/dom/dom_text.rs @@ -1,7 +1,4 @@ -use crate::{ - computed::{DropResource, ToComputed, struct_mut::VecMut}, - driver_module::get_driver_dom, -}; +use crate::{DropResource, ToComputed, driver_module::get_driver_dom, struct_mut::VecMut}; use super::dom_id::DomId; diff --git a/crates/vertigo/src/dom/events/click_event.rs b/crates/vertigo/src/dom/events/click_event.rs index 8da64e122..15ef46dfd 100644 --- a/crates/vertigo/src/dom/events/click_event.rs +++ b/crates/vertigo/src/dom/events/click_event.rs @@ -1,6 +1,6 @@ use std::rc::Rc; -use crate::{JsJson, computed::struct_mut::ValueMut}; +use crate::{JsJson, struct_mut::ValueMut}; /// Structure passed as a parameter to callback on on_key_down event. #[derive(Clone, Debug, Default)] diff --git a/crates/vertigo/src/dom_macro/dom.rs b/crates/vertigo/src/dom_macro/dom.rs index 4610046a3..b4da9eefa 100644 --- a/crates/vertigo/src/dom_macro/dom.rs +++ b/crates/vertigo/src/dom_macro/dom.rs @@ -8,6 +8,7 @@ use crate::{ dom_node::DomNode, events::{ClickEvent, IntersectionEvent}, }, + render::render_value, }; /// Type interpreted as component's dynamic attributes groups @@ -169,7 +170,15 @@ impl EmbedDom for DomNode { } } -impl EmbedDom for T { +impl EmbedDom for &mut String { + fn embed(self) -> DomNode { + DomNode::Text { + node: DomText::new(self.clone()), + } + } +} + +impl EmbedDom for &str { fn embed(self) -> DomNode { DomNode::Text { node: DomText::new(self.to_string()), @@ -177,9 +186,43 @@ impl EmbedDom for T { } } +impl EmbedDom for std::rc::Rc { + fn embed(self) -> DomNode { + DomNode::Text { + node: DomText::new((*self).to_string()), + } + } +} + +impl EmbedDom for &String { + fn embed(self) -> DomNode { + DomNode::Text { + node: DomText::new((*self).clone()), + } + } +} + +macro_rules! impl_embed_to_string { + ($($typename:ty),* $(,)?) => { + $( + impl EmbedDom for $typename { + fn embed(self) -> DomNode { + DomNode::Text { + node: DomText::new(self.to_string()), + } + } + } + )* + }; +} + +impl_embed_to_string!( + String, bool, char, u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize, f32, f64 +); + impl EmbedDom for &Computed { fn embed(self) -> DomNode { - self.render_value(|val| DomNode::Text { + render_value(self.clone(), |val| DomNode::Text { node: DomText::new(val.to_string()), }) } diff --git a/crates/vertigo/src/driver_module/api/api_arguments.rs b/crates/vertigo/src/driver_module/api/api_arguments.rs index fcb395f3d..097493c81 100644 --- a/crates/vertigo/src/driver_module/api/api_arguments.rs +++ b/crates/vertigo/src/driver_module/api/api_arguments.rs @@ -1,8 +1,6 @@ use std::rc::Rc; -use crate::{ - JsJson, computed::struct_mut::HashMapMut, dev::LongPtr, driver_module::js_value::MemoryBlock, -}; +use crate::{JsJson, dev::LongPtr, driver_module::js_value::MemoryBlock, struct_mut::HashMapMut}; #[derive(Clone)] pub struct Arguments { diff --git a/crates/vertigo/src/driver_module/api/api_fetch_cache.rs b/crates/vertigo/src/driver_module/api/api_fetch_cache.rs index c76565c22..bb7cdaa55 100644 --- a/crates/vertigo/src/driver_module/api/api_fetch_cache.rs +++ b/crates/vertigo/src/driver_module/api/api_fetch_cache.rs @@ -2,8 +2,8 @@ use std::rc::Rc; use vertigo_macro::store; use crate::{ - computed::struct_mut::ValueMut, dev::{SsrFetchCache, SsrFetchRequest, SsrFetchResponse}, + struct_mut::ValueMut, }; use super::api_browser_command; diff --git a/crates/vertigo/src/driver_module/api/api_location.rs b/crates/vertigo/src/driver_module/api/api_location.rs index f3dbf641a..25947dd86 100644 --- a/crates/vertigo/src/driver_module/api/api_location.rs +++ b/crates/vertigo/src/driver_module/api/api_location.rs @@ -2,7 +2,7 @@ use std::rc::Rc; use vertigo_macro::store; use crate::{ - computed::DropResource, + DropResource, dev::{ CallbackId, command::{LocationCallbackMode, LocationSetMode, LocationTarget}, diff --git a/crates/vertigo/src/driver_module/api/api_timers.rs b/crates/vertigo/src/driver_module/api/api_timers.rs index 1c584f4f6..78dc957ba 100644 --- a/crates/vertigo/src/driver_module/api/api_timers.rs +++ b/crates/vertigo/src/driver_module/api/api_timers.rs @@ -2,8 +2,9 @@ use std::rc::Rc; use vertigo_macro::store; use crate::{ - computed::{DropResource, struct_mut::ValueMut}, + DropResource, dev::{CallbackId, command::TimerKind}, + struct_mut::ValueMut, }; use super::{CallbackStore, api_browser_command}; diff --git a/crates/vertigo/src/driver_module/api/api_websocket.rs b/crates/vertigo/src/driver_module/api/api_websocket.rs index 9e16c309c..c0e4b1a1d 100644 --- a/crates/vertigo/src/driver_module/api/api_websocket.rs +++ b/crates/vertigo/src/driver_module/api/api_websocket.rs @@ -3,8 +3,7 @@ use std::rc::Rc; use vertigo_macro::store; use crate::{ - WebsocketConnection, WebsocketMessage, - computed::DropResource, + DropResource, WebsocketConnection, WebsocketMessage, dev::{CallbackId, command::WebsocketMessageFromBrowser}, }; diff --git a/crates/vertigo/src/driver_module/api/callbacks.rs b/crates/vertigo/src/driver_module/api/callbacks.rs index a2223483c..55b26c556 100644 --- a/crates/vertigo/src/driver_module/api/callbacks.rs +++ b/crates/vertigo/src/driver_module/api/callbacks.rs @@ -1,10 +1,6 @@ use std::rc::Rc; -use crate::{ - JsJson, - computed::{DropResource, struct_mut::HashMapMut}, - dev::CallbackId, -}; +use crate::{DropResource, JsJson, dev::CallbackId, struct_mut::HashMapMut}; type CallBackFn = dyn Fn(JsJson) -> JsJson + 'static; diff --git a/crates/vertigo/src/driver_module/api/server_handler.rs b/crates/vertigo/src/driver_module/api/server_handler.rs index 6afe95a07..c5e87533d 100644 --- a/crates/vertigo/src/driver_module/api/server_handler.rs +++ b/crates/vertigo/src/driver_module/api/server_handler.rs @@ -2,7 +2,7 @@ use std::{collections::HashMap, rc::Rc}; use vertigo::AutoJsJson; use vertigo_macro::store; -use crate::{JsJson, computed::struct_mut::ValueMut, driver_module::js_value::JsJsonSerialize}; +use crate::{JsJson, driver_module::js_value::JsJsonSerialize, struct_mut::ValueMut}; type PlainHandler = dyn Fn(&str) -> Option; diff --git a/crates/vertigo/src/driver_module/api/utils/callbacks.rs b/crates/vertigo/src/driver_module/api/utils/callbacks.rs index ebbd8aac3..ff69bfce3 100644 --- a/crates/vertigo/src/driver_module/api/utils/callbacks.rs +++ b/crates/vertigo/src/driver_module/api/utils/callbacks.rs @@ -1,11 +1,9 @@ use std::rc::Rc; use crate::{ - computed::{ - DropResource, - struct_mut::{HashMapMut, ValueMut}, - }, + DropResource, dev::CallbackId, + struct_mut::{HashMapMut, ValueMut}, }; type CallBackFn = dyn Fn(R) -> R2; diff --git a/crates/vertigo/src/driver_module/dom.rs b/crates/vertigo/src/driver_module/dom.rs index 37108fa20..c0d6ce925 100644 --- a/crates/vertigo/src/driver_module/dom.rs +++ b/crates/vertigo/src/driver_module/dom.rs @@ -2,13 +2,10 @@ use std::rc::Rc; use vertigo_macro::store; use crate::{ - DomId, - computed::{ - DropResource, - struct_mut::{HashMapMut, VecMut}, - }, + DomId, DropResource, dev::{CallbackId, command::DriverDomCommand}, driver_module::{api::api_browser_command, event_emitter::EventEmitter}, + struct_mut::{HashMapMut, VecMut}, }; use super::StaticString; diff --git a/crates/vertigo/src/driver_module/driver.rs b/crates/vertigo/src/driver_module/driver.rs index b18ffa7c3..d1d7397ad 100644 --- a/crates/vertigo/src/driver_module/driver.rs +++ b/crates/vertigo/src/driver_module/driver.rs @@ -2,8 +2,7 @@ use std::{future::Future, pin::Pin, rc::Rc}; use vertigo_macro::{AutoJsJson, store}; use crate::{ - Context, Css, DomNode, Instant, InstantType, JsJson, WebsocketMessage, - computed::{DropResource, get_dependencies, struct_mut::ValueMut}, + Context, Css, DomNode, DropResource, Instant, InstantType, JsJson, WebsocketMessage, css::get_css_manager, dev::{ FutureBox, @@ -15,6 +14,7 @@ use crate::{ utils::futures_spawn::spawn_local, }, fetch::request_builder::{RequestBody, RequestBuilder}, + struct_mut::ValueMut, }; use super::api::DomAccess; @@ -79,7 +79,7 @@ pub fn get_driver() -> Rc { }) }; - let subscribe = get_dependencies().hooks.on_after_transaction(move || { + let subscribe = crate::reactive::on_after_transaction(move || { get_driver_dom().flush_dom_changes(); }); @@ -237,10 +237,10 @@ impl Driver { spawn_executor(future); } - /// Fire provided function in a way that all changes in [dependency graph](struct.Dependencies.html) made by this function - /// will trigger only one run of updates, just like the changes were done all at once. + /// Fire provided function in a way that all reactive updates made by this function + /// run once, as if the changes were done all at once. pub fn transaction R>(&self, func: F) -> R { - get_dependencies().transaction(func) + crate::reactive::transaction(func) } /// Allows to access different objects in the browser (See [js!](crate::js) macro for convenient use). @@ -250,7 +250,7 @@ impl Driver { /// Function added for diagnostic purposes. It allows you to check whether a block with a transaction is missing somewhere. pub fn on_after_transaction(&self, callback: impl Fn() + 'static) -> DropResource { - get_dependencies().hooks.on_after_transaction(callback) + crate::reactive::on_after_transaction(callback) } /// Return true if the code is executed client-side (in the browser). diff --git a/crates/vertigo/src/driver_module/event_emitter.rs b/crates/vertigo/src/driver_module/event_emitter.rs index 23738d66a..fde316b43 100644 --- a/crates/vertigo/src/driver_module/event_emitter.rs +++ b/crates/vertigo/src/driver_module/event_emitter.rs @@ -1,6 +1,6 @@ use std::rc::Rc; -use crate::computed::{ +use crate::{ DropResource, struct_mut::{BTreeMapMut, CounterMut}, }; @@ -43,7 +43,7 @@ impl EventEmitter { pub fn trigger(&self, value: &T) { // Emitters on hot paths (every DOM command, every `Value` write) usually have no // listeners at all, so do not snapshot the callback list for them. - if self.list.is_empty() { + if self.is_empty() { return; } diff --git a/crates/vertigo/src/driver_module/utils/futures_spawn.rs b/crates/vertigo/src/driver_module/utils/futures_spawn.rs index ecb6730d0..3f9648717 100644 --- a/crates/vertigo/src/driver_module/utils/futures_spawn.rs +++ b/crates/vertigo/src/driver_module/utils/futures_spawn.rs @@ -6,7 +6,7 @@ use std::{ task::{Context, RawWaker, RawWakerVTable, Waker}, }; -use crate::{computed::struct_mut::ValueMut, driver_module::api::api_timers}; +use crate::{driver_module::api::api_timers, struct_mut::ValueMut}; #[inline] pub fn spawn_local(future: F) diff --git a/crates/vertigo/src/fetch/cache_value.rs b/crates/vertigo/src/fetch/cache_value.rs index e031c149d..8f8f2e571 100644 --- a/crates/vertigo/src/fetch/cache_value.rs +++ b/crates/vertigo/src/fetch/cache_value.rs @@ -1,7 +1,5 @@ use crate::{ - Computed, DropResource, - computed::{Value, context::Context}, - driver_module::api::api_timers, + Computed, Context, DropResource, Value, driver_module::api::api_timers, fetch::api_response::ApiResponse, }; diff --git a/crates/vertigo/src/fetch/lazy_cache.rs b/crates/vertigo/src/fetch/lazy_cache.rs index c0146ec5a..10a9bd367 100644 --- a/crates/vertigo/src/fetch/lazy_cache.rs +++ b/crates/vertigo/src/fetch/lazy_cache.rs @@ -2,11 +2,12 @@ use std::fmt::Debug; use std::rc::Rc; use crate::{ - Computed, DomNode, JsJsonDeserialize, RequestResponse, Resource, ToComputed, - computed::{context::Context, struct_mut::ValueMut}, + Computed, Context, DomNode, JsJsonDeserialize, RequestResponse, Resource, ToComputed, driver_module::api::{api_fetch, api_fetch_cache}, fetch::{api_response::ApiResponse, cache_value::CacheValue}, - get_driver, transaction, + get_driver, + struct_mut::ValueMut, + transaction, }; use super::request_builder::{RequestBody, RequestBuilder}; @@ -258,7 +259,7 @@ impl PartialEq for LazyCache { impl LazyCache { pub fn render(&self, render: impl Fn(Rc) -> DomNode + 'static) -> DomNode { - self.to_computed().render_value(move |value| match value { + crate::render::render_value(self.to_computed(), move |value| match value { Resource::Ready(value) => render(value), Resource::Loading => { use crate as vertigo; diff --git a/crates/vertigo/src/fetch/lazy_list_cache.rs b/crates/vertigo/src/fetch/lazy_list_cache.rs index 241bddf12..644277b89 100644 --- a/crates/vertigo/src/fetch/lazy_list_cache.rs +++ b/crates/vertigo/src/fetch/lazy_list_cache.rs @@ -1,15 +1,12 @@ use std::{collections::HashSet, rc::Rc}; use crate::{ - Computed, DomNode, JsJsonDeserialize, RequestResponse, Resource, Value, - computed::{ - context::Context, - struct_mut::{HashMapMut, ValueMut}, - }, + Computed, Context, DomNode, JsJsonDeserialize, RequestResponse, Resource, Value, driver_module::api::api_fetch, fetch::request_builder::{RequestBody, RequestBuilder}, get_driver, render::collection::CollectionKey, + struct_mut::{HashMapMut, ValueMut}, transaction, }; @@ -591,7 +588,7 @@ impl LazyListCache { /// For per-row reactivity, prefer rendering each row from its own [`Computed`] over /// [`get_by_key`](Self::get_by_key) instead. pub fn render(&self, render: impl Fn(Rc>) -> DomNode + 'static) -> DomNode { - self.to_computed().render_value(move |value| match value { + crate::render::render_value(self.to_computed(), move |value| match value { Resource::Ready(value) => render(value), Resource::Loading => { use crate as vertigo; diff --git a/crates/vertigo/src/fetch/resource.rs b/crates/vertigo/src/fetch/resource.rs index 6efef2893..4e519b74c 100644 --- a/crates/vertigo/src/fetch/resource.rs +++ b/crates/vertigo/src/fetch/resource.rs @@ -77,7 +77,7 @@ impl PartialEq for Resource { } } -impl ToComputed>> for Resource { +impl ToComputed>> for Resource { fn to_computed(&self) -> crate::Computed>> { Computed::from({ let myself = self.clone(); @@ -86,7 +86,7 @@ impl ToComputed>> for Resource { } } -impl ToComputed>> for Computed> { +impl ToComputed>> for Computed> { fn to_computed(&self) -> crate::Computed>> { self.map(|res| res.map(|item| Rc::new(item))) } diff --git a/crates/vertigo/src/computed/keyed_computed_list.rs b/crates/vertigo/src/keyed_computed_list.rs similarity index 99% rename from crates/vertigo/src/computed/keyed_computed_list.rs rename to crates/vertigo/src/keyed_computed_list.rs index 77f746aca..43a668096 100644 --- a/crates/vertigo/src/computed/keyed_computed_list.rs +++ b/crates/vertigo/src/keyed_computed_list.rs @@ -4,7 +4,7 @@ use std::{ rc::Rc, }; -use super::{Computed, ToComputed, struct_mut::ValueMut}; +use crate::{Computed, ToComputed, struct_mut::ValueMut}; /// One entry in a [`keyed_computed_list`]: a stable key plus a per-item value. /// diff --git a/crates/vertigo/src/lib.rs b/crates/vertigo/src/lib.rs index 97ef4a11f..53b9332ee 100644 --- a/crates/vertigo/src/lib.rs +++ b/crates/vertigo/src/lib.rs @@ -31,7 +31,7 @@ #![deny(rust_2018_idioms)] #![cfg_attr(test, allow(clippy::panic_in_result_fn))] -mod computed; +mod auto_map; mod css; pub mod dev; mod dom; @@ -43,8 +43,13 @@ mod fetch; mod future_box; pub mod html_entities; mod instant; +mod keyed_computed_list; +pub mod reactive; +#[cfg(test)] +mod reactive_old; pub mod render; pub mod router; +mod struct_mut; #[cfg(test)] mod tests; mod websocket; @@ -68,10 +73,7 @@ pub mod guides { // Exports from vertigo -pub use computed::{ - AutoMap, Computed, Dependencies, DropResource, KeyedListItem, Reactive, ToComputed, Value, - context::Context, keyed_computed_list, -}; +pub use auto_map::AutoMap; pub use css::{ css_structs::{Css, CssGroup}, tailwind_class::TwClass, @@ -101,6 +103,9 @@ pub use fetch::{ resource::Resource, }; pub use instant::{Instant, InstantType}; +pub use keyed_computed_list::{KeyedListItem, keyed_computed_list}; +pub use reactive::{Computed, Context, DropResource, GraphId, Reactive, ToComputed, Value}; +pub use render::RenderValue; pub use render::collection::CollectionKey; pub use websocket::{WebsocketConnection, WebsocketMessage}; pub use websocket_collection::{ @@ -113,6 +118,57 @@ pub mod prelude { pub use crate::{Computed, Css, DomNode, ToComputed, Value, bind, component, css, dom}; } +/// Allows to create `Computed<(T1, T2, ...)>` out of `Value`, `Value`, ... +/// +/// # Examples +/// +/// ``` +/// use vertigo::{Value, computed_tuple}; +/// +/// let value1 = Value::new(true); +/// let value2 = Value::new(5); +/// let value3 = Value::new("Hello tuple!".to_string()); +/// +/// let my_tuple = computed_tuple!(value1, value2, value3); +/// +/// vertigo::transaction(|ctx| { +/// assert!(my_tuple.get(ctx).0); +/// assert_eq!(my_tuple.get(ctx).1, 5); +/// assert_eq!(&my_tuple.get(ctx).2, "Hello tuple!"); +/// }); +/// ``` +/// +/// ``` +/// use vertigo::{Value, computed_tuple}; +/// +/// let values = (Value::new(true), Value::new(5)); +/// let value3 = Value::new("Hello tuple!".to_string()); +/// +/// let my_tuple = computed_tuple!(a => values.0, b => values.1, c => value3); +/// +/// vertigo::transaction(|ctx| { +/// assert!(my_tuple.get(ctx).0); +/// assert_eq!(my_tuple.get(ctx).1, 5); +/// assert_eq!(&my_tuple.get(ctx).2, "Hello tuple!"); +/// }); +/// ``` +#[macro_export] +macro_rules! computed_tuple { + ($($arg: tt),*) => {{ + let ($($arg),*) = ($($arg.clone()),*); + $crate::Computed::from(move |ctx| { + ($($arg.get(ctx)),*) + }) + }}; + + ($($name: ident => $arg: expr),*) => {{ + let ($($name),*) = ($(($arg).clone()),*); + $crate::Computed::from(move |ctx| { + ($($name.get(ctx)),*) + }) + }}; +} + // Re-export log module which can be used in vertigo plugins pub use log; diff --git a/crates/vertigo/src/reactive/computed.rs b/crates/vertigo/src/reactive/computed.rs new file mode 100644 index 000000000..d00928bbf --- /dev/null +++ b/crates/vertigo/src/reactive/computed.rs @@ -0,0 +1,222 @@ +use std::{cell::RefCell, ops::Add, rc::Rc}; + +use super::{ + Context, DropResource, GraphId, ToComputed, Value, + graph::{ErasedNode, GraphInner, NodeId}, +}; + +/// A read-only reactive cell, recomputed from other nodes. +pub struct Computed { + inner: Rc>, +} + +struct ComputedInner { + graph: Rc, + id: NodeId, + compute: Box T>, + value: RefCell>, +} + +struct SubscribeInner { + graph: Rc, + id: NodeId, + refresh: Box, +} + +impl Clone for Computed { + fn clone(&self) -> Self { + Computed { + inner: self.inner.clone(), + } + } +} + +impl PartialEq for Computed { + fn eq(&self, other: &Self) -> bool { + self.inner.id == other.inner.id + } +} + +impl ErasedNode for ComputedInner { + fn refresh(&self) -> bool { + self.recompute() + } +} + +impl ErasedNode for SubscribeInner { + fn refresh(&self) -> bool { + let ctx = Context::tracking(); + (self.refresh)(&ctx); + self.graph.set_parents(self.id, ctx.take_parents()); + false + } +} + +impl Drop for ComputedInner { + fn drop(&mut self) { + self.graph.unregister(self.id); + } +} + +impl Drop for SubscribeInner { + fn drop(&mut self) { + self.graph.unregister(self.id); + } +} + +impl ComputedInner { + fn recompute(&self) -> bool { + let ctx = Context::tracking(); + let new_value = (self.compute)(&ctx); + self.graph.set_parents(self.id, ctx.take_parents()); + + let mut slot = self.value.borrow_mut(); + match slot.as_ref() { + Some(old) if old == &new_value => false, + _ => { + *slot = Some(new_value); + true + } + } + } + + fn ensure(&self) -> T { + if self.value.borrow().is_none() { + self.recompute(); + } + match self.value.borrow().clone() { + Some(value) => value, + None => panic!("vertigo: computed has no value after refresh"), + } + } +} + +impl Computed { + pub(crate) fn create(graph: Rc, compute: impl Fn(&Context) -> T + 'static) -> Self { + let id = graph.alloc_id(); + let inner = Rc::new(ComputedInner { + graph: graph.clone(), + id, + compute: Box::new(compute), + value: RefCell::new(None), + }); + graph.register(id, inner.clone()); + Computed { inner } + } + + pub fn from(compute: impl Fn(&Context) -> T + 'static) -> Self { + super::default_graph().computed(compute) + } + + pub fn get(&self, ctx: &Context) -> T { + ctx.track(self.inner.id, self.inner.clone()); + self.inner.ensure() + } + + pub fn id(&self) -> GraphId { + GraphId::from_node(self.inner.id) + } + + /// Runs `create` when this value starts being observed; the returned + /// [`DropResource`] is dropped when it stops being observed. + pub fn when_connect DropResource + 'static>(&self, create: F) -> Computed { + let new_computed = Computed::create(self.inner.graph.clone(), { + let parent = self.clone(); + move |context| parent.get(context) + }); + new_computed + .inner + .graph + .register_connect(new_computed.inner.id, Rc::new(create)); + new_computed + } + + /// Subscribe; the callback runs only when the computed value *changes*. + pub fn subscribe R + 'static>(self, callback: F) -> DropResource { + let graph = self.inner.graph.clone(); + let parent = self.clone(); + let id = graph.alloc_id(); + let inner = Rc::new(SubscribeInner { + graph: graph.clone(), + id, + refresh: Box::new(move |ctx| { + let _ = callback(parent.get(ctx)); + }), + }); + graph.register(id, inner.clone()); + inner.refresh(); + DropResource::from_struct(inner) + } + + pub fn map K>( + &self, + fun: F, + ) -> Computed { + Computed::create(self.inner.graph.clone(), { + let myself = self.clone(); + move |context| fun(myself.get(context)) + }) + } +} + +impl ToComputed for Computed { + fn to_computed(&self) -> Computed { + self.clone() + } +} + +impl ToComputed for &Computed { + fn to_computed(&self) -> Computed { + (*self).clone() + } +} + +impl From> for Computed { + fn from(val: Value) -> Self { + val.to_computed() + } +} + +impl From for Computed { + fn from(value: T) -> Self { + Value::new(value).to_computed() + } +} + +impl From<&T> for Computed { + fn from(value: &T) -> Self { + Value::new(value.clone()).to_computed() + } +} + +impl From<&str> for Computed { + fn from(value: &str) -> Self { + Value::new(value.to_string()).to_computed() + } +} + +impl Add for Computed +where + T: Clone + PartialEq + Add + 'static, +{ + type Output = Computed; + + fn add(self, rhs: Self) -> Self::Output { + Computed::from({ + let left = self; + let right = rhs; + move |ctx| left.get(ctx) + right.get(ctx) + }) + } +} + +impl Add for Computed +where + T: Clone + PartialEq + Add + 'static, +{ + type Output = Computed; + + fn add(self, rhs: T) -> Self::Output { + self.map(move |left| left + rhs.clone()) + } +} diff --git a/crates/vertigo/src/reactive/context.rs b/crates/vertigo/src/reactive/context.rs new file mode 100644 index 000000000..bda947c21 --- /dev/null +++ b/crates/vertigo/src/reactive/context.rs @@ -0,0 +1,35 @@ +use std::{cell::RefCell, rc::Rc}; + +use super::graph::{ErasedNode, NodeId}; + +pub(crate) type ParentList = Vec<(NodeId, Rc)>; + +/// Tracking context passed into [`crate::Value::get`] / [`crate::Computed::get`]. +pub struct Context { + pub(crate) parents: Option>, +} + +impl Context { + pub(crate) fn read() -> Self { + Context { parents: None } + } + + pub(crate) fn tracking() -> Self { + Context { + parents: Some(RefCell::new(Vec::new())), + } + } + + pub(crate) fn track(&self, id: NodeId, slot: Rc) { + if let Some(parents) = &self.parents { + parents.borrow_mut().push((id, slot)); + } + } + + pub(crate) fn take_parents(&self) -> ParentList { + match &self.parents { + Some(parents) => parents.take(), + None => Vec::new(), + } + } +} diff --git a/crates/vertigo/src/computed/drop_resource.rs b/crates/vertigo/src/reactive/drop_resource.rs similarity index 86% rename from crates/vertigo/src/computed/drop_resource.rs rename to crates/vertigo/src/reactive/drop_resource.rs index b9e188d0e..6b618427c 100644 --- a/crates/vertigo/src/computed/drop_resource.rs +++ b/crates/vertigo/src/reactive/drop_resource.rs @@ -1,6 +1,6 @@ use std::any::Any; -/// A struct used by [driver](struct.Driver.html) to tidy things up on javascript side after a rust object goes out of scope. +/// Runs a destructor when dropped (or holds a value whose `Drop` cleans up). pub enum DropResource { Fun(Option>), Struct(Box), diff --git a/crates/vertigo/src/reactive/graph/dirty.rs b/crates/vertigo/src/reactive/graph/dirty.rs new file mode 100644 index 000000000..77c0d7287 --- /dev/null +++ b/crates/vertigo/src/reactive/graph/dirty.rs @@ -0,0 +1,132 @@ +use std::{ + cell::RefCell, + collections::{HashMap, HashSet, VecDeque}, +}; + +use super::{NodeId, edges::Edges}; + +/// Kahn-style worklist for one propagation wave. +/// +/// A dirty node is **ready** when none of its parents are still dirty. `dirty_parent_count` +/// stores that remaining count; `0` (or missing) means the node can be processed. +/// `scratch_children` is reused so `propagate` does not allocate a new `Vec` per node. +pub(super) struct Dirty { + in_dirty: RefCell>, + dirty_parent_count: RefCell>, + ready: RefCell>, + scratch_children: RefCell>, +} + +impl Dirty { + pub(super) fn new() -> Self { + Self { + in_dirty: RefCell::new(HashSet::new()), + dirty_parent_count: RefCell::new(HashMap::new()), + ready: RefCell::new(VecDeque::new()), + scratch_children: RefCell::new(Vec::new()), + } + } + + pub(super) fn contains(&self, id: NodeId) -> bool { + self.in_dirty.borrow().contains(&id) + } + + pub(super) fn enqueue(&self, id: NodeId, edges: &Edges) { + if !self.in_dirty.borrow_mut().insert(id) { + return; + } + let count = self.count_dirty_parents(id, edges); + if count == 0 { + self.ready.borrow_mut().push_back(id); + } else { + self.dirty_parent_count.borrow_mut().insert(id, count); + } + } + + pub(super) fn take_ready(&self) -> Option { + let mut ready = self.ready.borrow_mut(); + let in_dirty = self.in_dirty.borrow(); + while let Some(id) = ready.pop_front() { + if in_dirty.contains(&id) { + return Some(id); + } + } + None + } + + pub(super) fn dequeue(&self, id: NodeId) { + self.in_dirty.borrow_mut().remove(&id); + self.dirty_parent_count.borrow_mut().remove(&id); + } + + /// `Some` leftover ids when `ready` is empty but dirty nodes remain (a cycle). + pub(super) fn cycle_leftover(&self) -> Option> { + let in_dirty = self.in_dirty.borrow(); + if in_dirty.is_empty() { + None + } else { + Some(in_dirty.iter().copied().collect()) + } + } + + /// Parent left the dirty set: dependents waiting on it may become ready. + pub(super) fn release_parent(&self, parent: NodeId, edges: &Edges) { + if self.dirty_parent_count.borrow().is_empty() { + return; + } + self.fill_scratch(parent, edges); + self.release_from_scratch(); + } + + /// After `refresh`: release waiting children; enqueue dependents only on value change. + pub(super) fn after_refresh(&self, id: NodeId, changed: bool, edges: &Edges) { + let need_release = !self.dirty_parent_count.borrow().is_empty(); + if !need_release && !changed { + return; + } + + self.fill_scratch(id, edges); + if need_release { + self.release_from_scratch(); + } + if changed { + self.enqueue_from_scratch(edges); + } + } + + fn count_dirty_parents(&self, id: NodeId, edges: &Edges) -> u32 { + let in_dirty = self.in_dirty.borrow(); + edges.count_parents_if(id, |parent| in_dirty.contains(&parent)) + } + + fn fill_scratch(&self, id: NodeId, edges: &Edges) { + let mut buf = self.scratch_children.borrow_mut(); + edges.copy_children(id, &mut buf); + } + + fn release_from_scratch(&self) { + let mut newly_ready = Vec::new(); + { + let children = self.scratch_children.borrow(); + let mut counts = self.dirty_parent_count.borrow_mut(); + for child in children.iter() { + if let Some(count) = counts.get_mut(child) { + *count = count.saturating_sub(1); + if *count == 0 { + newly_ready.push(*child); + } + } + } + } + if !newly_ready.is_empty() { + self.ready.borrow_mut().extend(newly_ready); + } + } + + fn enqueue_from_scratch(&self, edges: &Edges) { + let children = self.scratch_children.borrow(); + for child in children.iter() { + self.enqueue(*child, edges); + } + } +} diff --git a/crates/vertigo/src/reactive/graph/edges.rs b/crates/vertigo/src/reactive/graph/edges.rs new file mode 100644 index 000000000..6981b4770 --- /dev/null +++ b/crates/vertigo/src/reactive/graph/edges.rs @@ -0,0 +1,158 @@ +use std::{ + cell::RefCell, + collections::{HashMap, HashSet}, + rc::Rc, +}; + +use super::super::context::ParentList; +use super::{ErasedNode, NodeId}; + +/// Result of replacing a child's parents: nodes that gained or lost their last child. +pub(super) struct ParentDiff { + pub became_watched: Vec, + pub became_unwatched: Vec, +} + +/// Bidirectional DAG plus strong parent refs (so parents outlive the child that lists them). +pub(super) struct Edges { + child_parents: RefCell>>, + parent_children: RefCell>>, + parent_refs: RefCell>>>, +} + +impl Edges { + pub(super) fn new() -> Self { + Self { + child_parents: RefCell::new(HashMap::new()), + parent_children: RefCell::new(HashMap::new()), + parent_refs: RefCell::new(HashMap::new()), + } + } + + pub(super) fn is_watched(&self, id: NodeId) -> bool { + self.parent_children + .borrow() + .get(&id) + .is_some_and(|c| !c.is_empty()) + } + + pub(super) fn count_parents_if(&self, id: NodeId, mut pred: impl FnMut(NodeId) -> bool) -> u32 { + match self.child_parents.borrow().get(&id) { + Some(parents) => parents + .iter() + .copied() + .filter(|&parent| pred(parent)) + .count() as u32, + None => 0, + } + } + + pub(super) fn copy_children(&self, id: NodeId, buf: &mut Vec) { + buf.clear(); + if let Some(set) = self.parent_children.borrow().get(&id) { + buf.extend(set.iter().copied()); + } + } + + /// Replace `child`'s parent set. `None` means the parent ids were already the same. + pub(super) fn replace(&self, child: NodeId, pairs: ParentList) -> Option { + { + let child_parents = self.child_parents.borrow(); + if let Some(old) = child_parents.get(&child) + && Self::same_parent_ids(old, &pairs) + { + return None; + } + } + + let new_parents: HashSet = pairs.iter().map(|(id, _)| *id).collect(); + let kept: Vec> = pairs.into_iter().map(|(_, slot)| slot).collect(); + + let old = self + .child_parents + .borrow_mut() + .insert(child, new_parents.clone()) + .unwrap_or_default(); + + let mut became_watched = Vec::new(); + let mut became_unwatched = Vec::new(); + + { + let mut parent_children = self.parent_children.borrow_mut(); + + for parent in old.difference(&new_parents) { + if let Some(children) = parent_children.get_mut(parent) { + children.remove(&child); + if children.is_empty() { + became_unwatched.push(*parent); + } + } + } + + for parent in new_parents.difference(&old) { + let children = parent_children.entry(*parent).or_default(); + let was_empty = children.is_empty(); + children.insert(child); + if was_empty { + became_watched.push(*parent); + } + } + } + + let old_kept = self.parent_refs.borrow_mut().insert(child, kept); + drop(old_kept); + + Some(ParentDiff { + became_watched, + became_unwatched, + }) + } + + /// Strip `id` from both adjacency maps. Returns parents that lost their last child. + pub(super) fn unregister(&self, id: NodeId) -> Vec { + let _kept = self.parent_refs.borrow_mut().remove(&id); + let parents = self + .child_parents + .borrow_mut() + .remove(&id) + .unwrap_or_default(); + let mut became_unwatched = Vec::new(); + { + let mut parent_children = self.parent_children.borrow_mut(); + for parent in &parents { + if let Some(children) = parent_children.get_mut(parent) { + children.remove(&id); + if children.is_empty() { + became_unwatched.push(*parent); + } + } + } + + if let Some(children) = parent_children.remove(&id) { + let mut child_parents = self.child_parents.borrow_mut(); + for child in children { + if let Some(ps) = child_parents.get_mut(&child) { + ps.remove(&id); + } + } + } + } + became_unwatched + } + + fn same_parent_ids(old: &HashSet, pairs: &ParentList) -> bool { + if old.is_empty() { + return pairs.is_empty(); + } + for (id, _) in pairs { + if !old.contains(id) { + return false; + } + } + if pairs.len() < old.len() { + return false; + } + old.iter() + .all(|id| pairs.iter().any(|(parent, _)| parent == id)) + } +} diff --git a/crates/vertigo/src/reactive/graph/hooks.rs b/crates/vertigo/src/reactive/graph/hooks.rs new file mode 100644 index 000000000..8fe091c90 --- /dev/null +++ b/crates/vertigo/src/reactive/graph/hooks.rs @@ -0,0 +1,41 @@ +use std::{ + cell::{Cell, RefCell}, + collections::BTreeMap, + rc::Rc, +}; + +/// Callbacks fired after a completed transaction (once `propagate` has finished). +pub(super) struct Hooks { + next_id: Cell, + hooks: RefCell>>, +} + +impl Hooks { + pub(super) fn new() -> Self { + Self { + next_id: Cell::new(1), + hooks: RefCell::new(BTreeMap::new()), + } + } + + pub(super) fn insert(&self, callback: impl Fn() + 'static) -> u64 { + let id = self.next_id.get(); + self.next_id.set(id + 1); + self.hooks.borrow_mut().insert(id, Rc::new(callback)); + id + } + + pub(super) fn remove(&self, id: u64) { + self.hooks.borrow_mut().remove(&id); + } + + pub(super) fn fire(&self) { + if self.hooks.borrow().is_empty() { + return; + } + let hooks: Vec<_> = self.hooks.borrow().values().cloned().collect(); + for hook in hooks { + hook(); + } + } +} diff --git a/crates/vertigo/src/reactive/graph/mod.rs b/crates/vertigo/src/reactive/graph/mod.rs new file mode 100644 index 000000000..da8874b01 --- /dev/null +++ b/crates/vertigo/src/reactive/graph/mod.rs @@ -0,0 +1,189 @@ +use std::{cell::Cell, rc::Rc}; + +use super::{Computed, Context, DropResource, Value, context::ParentList}; + +mod dirty; +mod edges; +mod hooks; +mod nodes; +mod watch; + +use dirty::Dirty; +use edges::Edges; +use hooks::Hooks; +use nodes::Nodes; +use watch::Watch; + +#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Debug)] +pub(crate) struct NodeId(pub u64); + +/// Identity of a [`Value`] or [`Computed`] node. +#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Debug)] +pub struct GraphId(u64); + +impl GraphId { + pub fn id(&self) -> u64 { + self.0 + } + + pub(crate) fn from_node(id: NodeId) -> Self { + GraphId(id.0) + } +} + +pub(crate) trait ErasedNode { + fn refresh(&self) -> bool; +} + +/// One reactive graph. Nodes created from different graphs do not see each other. +pub struct Graph { + pub(crate) inner: Rc, +} + +/// Orchestrator: transactions, `propagate`, `set_parents`, `unregister`. +/// +/// Persistent state lives in `Nodes`, `Edges`, `Dirty`, `Watch`, `Hooks`. +pub(crate) struct GraphInner { + next_id: Cell, + tx: Cell, + propagating: Cell, + nodes: Nodes, + edges: Edges, + dirty: Dirty, + watch: Watch, + hooks: Hooks, +} + +impl Clone for Graph { + fn clone(&self) -> Self { + Graph { + inner: self.inner.clone(), + } + } +} + +impl Default for Graph { + fn default() -> Self { + Self::new() + } +} + +impl Graph { + pub fn new() -> Self { + Graph { + inner: Rc::new(GraphInner { + next_id: Cell::new(1), + tx: Cell::new(0), + propagating: Cell::new(false), + nodes: Nodes::new(), + edges: Edges::new(), + dirty: Dirty::new(), + watch: Watch::new(), + hooks: Hooks::new(), + }), + } + } + + pub fn value(&self, value: T) -> Value { + Value::create(self.inner.clone(), value) + } + + pub fn computed( + &self, + compute: impl Fn(&Context) -> T + 'static, + ) -> Computed { + Computed::create(self.inner.clone(), compute) + } + + pub fn transaction(&self, f: impl FnOnce(&Context) -> R) -> R { + self.inner.tx.set(self.inner.tx.get() + 1); + let ctx = Context::read(); + let result = f(&ctx); + self.inner.tx.set(self.inner.tx.get() - 1); + if self.inner.tx.get() == 0 { + let nested_in_propagate = self.inner.propagating.get(); + self.inner.propagate(); + if !nested_in_propagate { + self.inner.hooks.fire(); + } + } + result + } + + pub fn on_after_transaction(&self, callback: impl Fn() + 'static) -> DropResource { + let id = self.inner.hooks.insert(callback); + let inner = self.inner.clone(); + DropResource::new(move || { + inner.hooks.remove(id); + }) + } +} + +impl GraphInner { + pub(crate) fn alloc_id(&self) -> NodeId { + let id = self.next_id.get(); + self.next_id.set(id + 1); + NodeId(id) + } + + pub(crate) fn register(&self, id: NodeId, slot: Rc) { + self.nodes.register(id, slot); + } + + pub(crate) fn enqueue(&self, id: NodeId) { + self.dirty.enqueue(id, &self.edges); + } + + pub(crate) fn register_connect(&self, id: NodeId, connect: Rc DropResource>) { + self.watch.register(id, connect, self.edges.is_watched(id)); + } + + pub(crate) fn set_parents(&self, child: NodeId, pairs: ParentList) { + if let Some(diff) = self.edges.replace(child, pairs) { + self.watch.apply(diff); + } + } + + pub(crate) fn unregister(&self, id: NodeId) { + let was_dirty = self.dirty.contains(id); + self.dirty.dequeue(id); + if was_dirty { + self.dirty.release_parent(id, &self.edges); + } + self.nodes.remove(id); + self.watch.unregister(id); + let became_unwatched = self.edges.unregister(id); + self.watch.on_unwatched_many(became_unwatched); + } + + /// Process dirty nodes in topological order. Dependents are enqueued only when + /// the node's value changed. + pub(crate) fn propagate(&self) { + if self.tx.get() != 0 || self.propagating.get() { + return; + } + self.propagating.set(true); + + loop { + let Some(id) = self.dirty.take_ready() else { + if let Some(leftover) = self.dirty.cycle_leftover() { + self.propagating.set(false); + panic!("vertigo: cycle in dirty graph ({leftover:?})"); + } + break; + }; + + self.dirty.dequeue(id); + + let Some(node) = self.nodes.upgrade(id) else { + self.dirty.release_parent(id, &self.edges); + continue; + }; + + let changed = node.refresh(); + self.dirty.after_refresh(id, changed, &self.edges); + } + + self.propagating.set(false); + } +} diff --git a/crates/vertigo/src/reactive/graph/nodes.rs b/crates/vertigo/src/reactive/graph/nodes.rs new file mode 100644 index 000000000..82952676f --- /dev/null +++ b/crates/vertigo/src/reactive/graph/nodes.rs @@ -0,0 +1,32 @@ +use std::{ + cell::RefCell, + collections::HashMap, + rc::{Rc, Weak}, +}; + +use super::{ErasedNode, NodeId}; + +/// Weak registry of live nodes. +pub(super) struct Nodes { + slots: RefCell>>, +} + +impl Nodes { + pub(super) fn new() -> Self { + Self { + slots: RefCell::new(HashMap::new()), + } + } + + pub(super) fn register(&self, id: NodeId, slot: Rc) { + self.slots.borrow_mut().insert(id, Rc::downgrade(&slot)); + } + + pub(super) fn upgrade(&self, id: NodeId) -> Option> { + self.slots.borrow().get(&id).and_then(Weak::upgrade) + } + + pub(super) fn remove(&self, id: NodeId) { + self.slots.borrow_mut().remove(&id); + } +} diff --git a/crates/vertigo/src/reactive/graph/watch.rs b/crates/vertigo/src/reactive/graph/watch.rs new file mode 100644 index 000000000..127d5467d --- /dev/null +++ b/crates/vertigo/src/reactive/graph/watch.rs @@ -0,0 +1,66 @@ +use std::{cell::RefCell, collections::HashMap, rc::Rc}; + +use super::super::DropResource; +use super::{NodeId, edges::ParentDiff}; + +/// `when_connect` lifecycle: run `connect` while a node has children, drop the resource when not. +pub(super) struct Watch { + connect: RefCell DropResource>>>, + connected: RefCell>, +} + +impl Watch { + pub(super) fn new() -> Self { + Self { + connect: RefCell::new(HashMap::new()), + connected: RefCell::new(HashMap::new()), + } + } + + pub(super) fn register( + &self, + id: NodeId, + connect: Rc DropResource>, + watched: bool, + ) { + self.connect.borrow_mut().insert(id, connect); + if watched { + self.on_watched(id); + } + } + + pub(super) fn unregister(&self, id: NodeId) { + self.connect.borrow_mut().remove(&id); + let _dropped = self.connected.borrow_mut().remove(&id); + } + + pub(super) fn apply(&self, diff: ParentDiff) { + for id in diff.became_watched { + self.on_watched(id); + } + for id in diff.became_unwatched { + self.on_unwatched(id); + } + } + + pub(super) fn on_unwatched_many(&self, ids: Vec) { + for id in ids { + self.on_unwatched(id); + } + } + + fn on_watched(&self, id: NodeId) { + if self.connected.borrow().contains_key(&id) { + return; + } + let Some(connect) = self.connect.borrow().get(&id).cloned() else { + return; + }; + let resource = connect(); + self.connected.borrow_mut().insert(id, resource); + } + + fn on_unwatched(&self, id: NodeId) { + let _dropped = self.connected.borrow_mut().remove(&id); + } +} diff --git a/crates/vertigo/src/reactive/mod.rs b/crates/vertigo/src/reactive/mod.rs new file mode 100644 index 000000000..e207ca322 --- /dev/null +++ b/crates/vertigo/src/reactive/mod.rs @@ -0,0 +1,67 @@ +//! Transactional reactive graph with equality cutoff. +//! +//! After a transaction, dirty nodes are processed in topological order (a node is +//! ready when none of its parents are dirty). After a node becomes clean, dependents +//! are enqueued **only if** the node's value changed ([`PartialEq`]). + +mod computed; +mod context; +mod drop_resource; +mod graph; +mod to_computed; +mod value; + +pub use computed::Computed; +pub use context::Context; +pub use drop_resource::DropResource; +pub use graph::{Graph, GraphId}; +pub use to_computed::ToComputed; +pub use value::Value; + +/// Types that behave like a [`Value`]. +pub trait Reactive: PartialEq { + fn set(&self, value: T); + fn get(&self, context: &Context) -> T; + fn change(&self, change_fn: impl FnOnce(&mut T)); +} + +impl Reactive for Value +where + T: Clone + PartialEq + 'static, +{ + fn set(&self, value: T) { + Value::set(self, value) + } + + fn get(&self, context: &Context) -> T { + Value::get(self, context) + } + + fn change(&self, change_fn: impl FnOnce(&mut T)) { + Value::change(self, change_fn) + } +} + +thread_local! { + static DEFAULT_GRAPH: Graph = Graph::new(); +} + +pub(crate) fn default_graph() -> Graph { + DEFAULT_GRAPH.with(Graph::clone) +} + +/// Run `f` as a transaction on the default graph. +/// +/// Nested calls are allowed. Propagation runs when the outermost transaction ends. +pub fn transaction(f: impl FnOnce(&Context) -> R) -> R { + default_graph().transaction(f) +} + +/// Register a callback on the default graph, fired after each completed transaction +/// (including a lone [`Value::set`]). +pub fn on_after_transaction(callback: impl Fn() + 'static) -> DropResource { + default_graph().on_after_transaction(callback) +} + +#[cfg(test)] +mod tests; diff --git a/crates/vertigo/src/reactive/tests.rs b/crates/vertigo/src/reactive/tests.rs new file mode 100644 index 000000000..635ead3ab --- /dev/null +++ b/crates/vertigo/src/reactive/tests.rs @@ -0,0 +1,354 @@ +use std::{cell::Cell, rc::Rc}; + +use super::{Computed, DropResource, Graph, Value}; + +#[test] +fn basic_sum() { + let g = Graph::new(); + let a = g.value(1); + let b = g.value(2); + let sum = g.computed({ + let a = a.clone(); + let b = b.clone(); + move |ctx| a.get(ctx) + b.get(ctx) + }); + + g.transaction(|ctx| { + assert_eq!(sum.get(ctx), 3); + }); + + a.set(4); + g.transaction(|ctx| { + assert_eq!(sum.get(ctx), 6); + }); +} + +#[test] +fn two_sets_in_one_transaction_compute_once() { + let g = Graph::new(); + let a = g.value(0); + let b = g.value(0); + let runs = Rc::new(Cell::new(0)); + let sum = g.computed({ + let a = a.clone(); + let b = b.clone(); + let runs = runs.clone(); + move |ctx| { + runs.set(runs.get() + 1); + a.get(ctx) + b.get(ctx) + } + }); + + g.transaction(|ctx| { + assert_eq!(sum.get(ctx), 0); + }); + runs.set(0); + + g.transaction(|_| { + a.set(10); + b.set(20); + }); + + g.transaction(|ctx| { + assert_eq!(sum.get(ctx), 30); + }); + assert_eq!(runs.get(), 1); +} + +#[test] +fn cutoff_leaves_fanout_untouched() { + let g = Graph::new(); + let a = g.value(1); + let even = g.computed({ + let a = a.clone(); + move |ctx| a.get(ctx) % 2 == 0 + }); + + let child_runs = Rc::new(Cell::new(0)); + let children: Vec<_> = (0..10_000) + .map(|i| { + let even = even.clone(); + let child_runs = child_runs.clone(); + g.computed(move |ctx| { + child_runs.set(child_runs.get() + 1); + (even.get(ctx), i) + }) + }) + .collect(); + + g.transaction(|ctx| { + assert!(!even.get(ctx)); + for child in &children { + let _ = child.get(ctx); + } + }); + assert_eq!(child_runs.get(), 10_000); + + child_runs.set(0); + a.set(3); + g.transaction(|ctx| { + assert!(!even.get(ctx)); + }); + assert_eq!( + child_runs.get(), + 0, + "odd→odd: even cut off, 10_000 must not run" + ); + + a.set(4); + g.transaction(|ctx| { + assert!(even.get(ctx)); + assert_eq!(children[0].get(ctx), (true, 0)); + }); + assert_eq!( + child_runs.get(), + 10_000, + "odd→even: fan-out must run once each" + ); +} + +#[test] +fn diamond_waits_for_both_parents() { + let g = Graph::new(); + let a = g.value(1); + let left = g.computed({ + let a = a.clone(); + move |ctx| a.get(ctx) + 1 + }); + let right = g.computed({ + let a = a.clone(); + move |ctx| a.get(ctx) * 10 + }); + let d_runs = Rc::new(Cell::new(0)); + let d = g.computed({ + let left = left.clone(); + let right = right.clone(); + let d_runs = d_runs.clone(); + move |ctx| { + d_runs.set(d_runs.get() + 1); + left.get(ctx) + right.get(ctx) + } + }); + + g.transaction(|ctx| { + assert_eq!(d.get(ctx), 12); + }); + d_runs.set(0); + + a.set(2); + g.transaction(|ctx| { + assert_eq!(left.get(ctx), 3); + assert_eq!(right.get(ctx), 20); + assert_eq!(d.get(ctx), 23); + }); + assert_eq!(d_runs.get(), 1); +} + +#[test] +fn subscribe_skips_when_parent_cuts_off() { + let g = Graph::new(); + let a = g.value(1); + let even = g.computed({ + let a = a.clone(); + move |ctx| a.get(ctx) % 2 == 0 + }); + + let fires = Rc::new(Cell::new(0)); + let last = Rc::new(Cell::new(false)); + let _sub: DropResource = even.subscribe({ + let fires = fires.clone(); + let last = last.clone(); + move |v| { + fires.set(fires.get() + 1); + last.set(v); + } + }); + + assert_eq!(fires.get(), 1); + assert!(!last.get()); + + a.set(3); + assert_eq!(fires.get(), 1); + + a.set(4); + assert_eq!(fires.get(), 2); + assert!(last.get()); +} + +#[test] +fn equal_set_does_not_enqueue() { + let g = Graph::new(); + let a = g.value(5); + let runs = Rc::new(Cell::new(0)); + let c = g.computed({ + let a = a.clone(); + let runs = runs.clone(); + move |ctx| { + runs.set(runs.get() + 1); + a.get(ctx) + } + }); + + g.transaction(|ctx| { + assert_eq!(c.get(ctx), 5); + }); + runs.set(0); + + a.set(5); + assert_eq!(runs.get(), 0); +} + +#[test] +fn default_graph_value_new() { + let a = Value::new(1); + let double = Computed::from({ + let a = a.clone(); + move |ctx| a.get(ctx) * 2 + }); + + super::transaction(|ctx| { + assert_eq!(double.get(ctx), 2); + }); + + a.set(5); + super::transaction(|ctx| { + assert_eq!(double.get(ctx), 10); + }); +} + +#[test] +fn when_connect_tracks_watchers() { + let connect_count = Rc::new(Cell::new(0)); + let disconnect_count = Rc::new(Cell::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.set(connect_count.get() + 1); + DropResource::new({ + let disconnect_count = disconnect_count.clone(); + move || { + disconnect_count.set(disconnect_count.get() + 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 when_connect_multiple_subscribers_share_one_connection() { + let connect_count = Rc::new(Cell::new(0)); + let disconnect_count = Rc::new(Cell::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.set(connect_count.get() + 1); + DropResource::new({ + let disconnect_count = disconnect_count.clone(); + move || { + disconnect_count.set(disconnect_count.get() + 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); +} + +#[test] +fn nested_computed_subscription() { + 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 = Rc::new(Cell::new(0)); + + 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); + + token_value.set("token2".to_string()); + assert_eq!(counter.get(), 2); +} + +#[test] +fn 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 = Rc::new(Cell::new(0)); + + let _drop = bearer_auth.subscribe({ + let counter = counter.clone(); + move |_| { + counter.set(counter.get() + 1); + } + }); + + assert_eq!(counter.get(), 1); + + token_value.set("token2".to_string()); + assert_eq!(counter.get(), 1); +} + +#[test] +fn on_after_transaction_fires_after_set() { + let fires = Rc::new(Cell::new(0)); + let _hook = super::on_after_transaction({ + let fires = fires.clone(); + move || { + fires.set(fires.get() + 1); + } + }); + + let a = Value::new(1); + a.set(2); + assert_eq!(fires.get(), 1); +} diff --git a/crates/vertigo/src/reactive/to_computed.rs b/crates/vertigo/src/reactive/to_computed.rs new file mode 100644 index 000000000..dd1345d71 --- /dev/null +++ b/crates/vertigo/src/reactive/to_computed.rs @@ -0,0 +1,40 @@ +use super::Computed; + +/// Convert the type into a [`Computed`]. +pub trait ToComputed { + fn to_computed(&self) -> Computed; +} + +macro_rules! impl_to_computed { + ($typename: ty) => { + impl ToComputed<$typename> for $typename { + fn to_computed(&self) -> Computed<$typename> { + let value = *self; + Computed::from(move |_| value) + } + } + }; +} + +impl_to_computed!(i8); +impl_to_computed!(i16); +impl_to_computed!(i32); +impl_to_computed!(i64); +impl_to_computed!(i128); +impl_to_computed!(isize); + +impl_to_computed!(u8); +impl_to_computed!(u16); +impl_to_computed!(u32); +impl_to_computed!(u64); +impl_to_computed!(u128); +impl_to_computed!(usize); + +impl_to_computed!(f32); +impl_to_computed!(f64); + +impl_to_computed!(char); + +impl_to_computed!(bool); + +impl_to_computed!(()); diff --git a/crates/vertigo/src/reactive/value.rs b/crates/vertigo/src/reactive/value.rs new file mode 100644 index 000000000..38ccc82be --- /dev/null +++ b/crates/vertigo/src/reactive/value.rs @@ -0,0 +1,186 @@ +use std::{cell::RefCell, collections::BTreeMap, rc::Rc}; + +use super::{ + Computed, Context, DropResource, Graph, GraphId, ToComputed, + graph::{ErasedNode, GraphInner, NodeId}, +}; + +type ValueEvents = BTreeMap>; + +/// A writable reactive cell. +/// +/// ``` +/// use vertigo::{Value, transaction}; +/// +/// let value = Value::new(5); +/// +/// transaction(|context| { +/// assert_eq!(value.get(context), 5); +/// }); +/// +/// value.set(10); +/// +/// transaction(|context| { +/// assert_eq!(value.get(context), 10); +/// }); +/// ``` +pub struct Value { + inner: Rc>, +} + +struct ValueInner { + graph: Rc, + id: NodeId, + value: RefCell, + next_event: RefCell, + events: RefCell>, +} + +impl Clone for Value { + fn clone(&self) -> Self { + Value { + inner: self.inner.clone(), + } + } +} + +impl PartialEq for Value { + fn eq(&self, other: &Self) -> bool { + self.inner.id == other.inner.id + } +} + +impl Default for Value { + fn default() -> Self { + Self::new(T::default()) + } +} + +impl ErasedNode for ValueInner { + fn refresh(&self) -> bool { + true + } +} + +impl Drop for ValueInner { + fn drop(&mut self) { + self.graph.unregister(self.id); + } +} + +impl Value { + pub(crate) fn create(graph: Rc, value: T) -> Self { + let id = graph.alloc_id(); + let inner = Rc::new(ValueInner { + graph: graph.clone(), + id, + value: RefCell::new(value), + next_event: RefCell::new(1), + events: RefCell::new(BTreeMap::new()), + }); + graph.register(id, inner.clone()); + Value { inner } + } + + /// Create a value on the default graph. + pub fn new(value: T) -> Self { + super::default_graph().value(value) + } + + /// Create a value that is connected to a generator. `value` is the starting + /// value; `create` is responsible for keeping it up to date. + pub fn with_connect(value: T, create: F) -> Computed + where + F: Fn(&Value) -> DropResource + 'static, + { + let value = Value::new(value); + let value_clone = value.clone(); + value + .to_computed() + .when_connect(move || create(&value_clone)) + } + + pub fn get(&self, ctx: &Context) -> T { + ctx.track(self.inner.id, self.inner.clone()); + self.inner.value.borrow().clone() + } + + pub fn set(&self, value: T) { + let graph = Graph { + inner: self.inner.graph.clone(), + }; + graph.transaction(|_| { + if *self.inner.value.borrow() == value { + return; + } + // The clone only exists to hand the new value to the listeners, so skip it + // when there are none — otherwise every write deep-copies whatever is stored. + if self.inner.events.borrow().is_empty() { + *self.inner.value.borrow_mut() = value; + } else { + *self.inner.value.borrow_mut() = value.clone(); + let events: Vec<_> = self.inner.events.borrow().values().cloned().collect(); + for event in events { + event(value.clone()); + } + } + self.inner.graph.enqueue(self.inner.id); + }); + } + + pub fn change(&self, change_fn: impl FnOnce(&mut T)) { + let graph = Graph { + inner: self.inner.graph.clone(), + }; + graph.transaction(|ctx| { + let mut value = self.get(ctx); + change_fn(&mut value); + self.set(value); + }); + } + + pub fn map K>( + &self, + fun: F, + ) -> Computed { + Computed::create(self.inner.graph.clone(), { + let myself = self.clone(); + move |context| fun(myself.get(context)) + }) + } + + pub fn to_computed(&self) -> Computed { + let myself = self.clone(); + Computed::create(self.inner.graph.clone(), move |context| myself.get(context)) + } + + pub fn id(&self) -> GraphId { + GraphId::from_node(self.inner.id) + } + + pub fn add_event(&self, callback: impl Fn(T) + 'static) -> DropResource { + let id = { + let mut next = self.inner.next_event.borrow_mut(); + let id = *next; + *next += 1; + id + }; + self.inner.events.borrow_mut().insert(id, Rc::new(callback)); + let inner = self.inner.clone(); + DropResource::new(move || { + inner.events.borrow_mut().remove(&id); + }) + } +} + +impl ToComputed for Value { + fn to_computed(&self) -> Computed { + self.to_computed() + } +} + +impl ToComputed for &Value { + fn to_computed(&self) -> Computed { + (*self).to_computed() + } +} diff --git a/crates/vertigo/src/reactive_old/compare.rs b/crates/vertigo/src/reactive_old/compare.rs new file mode 100644 index 000000000..728172a50 --- /dev/null +++ b/crates/vertigo/src/reactive_old/compare.rs @@ -0,0 +1,265 @@ +//! Wall-clock and work-count comparison: previous graph (`reactive_old`) vs current (`reactive`). +//! +//! Run with: `cargo test -p vertigo --lib reactive_old::compare -- --nocapture` + +use std::{ + cell::Cell, + rc::Rc, + time::{Duration, Instant}, +}; + +use crate::reactive::{self as new, Graph}; +use crate::reactive_old as old; + +const FANOUT: usize = 10_000; +const CHAIN: usize = 200; +const UPDATES: usize = 200; + +fn elapsed(f: impl FnOnce()) -> Duration { + let start = Instant::now(); + f(); + start.elapsed() +} + +fn report(name: &str, old_d: Duration, new_d: Duration) { + let old_ms = old_d.as_secs_f64() * 1000.0; + let new_ms = new_d.as_secs_f64() * 1000.0; + let speedup = if new_ms <= 0.0 { + f64::INFINITY + } else { + old_ms / new_ms + }; + eprintln!("compare {name:>28}: old={old_ms:8.3}ms new={new_ms:8.3}ms speedup={speedup:6.2}x"); +} + +struct FanoutOld { + source: old::Value, + _children: Vec>, + _subs: Vec, + runs: Rc>, +} + +fn setup_fanout_old() -> FanoutOld { + let source = old::Value::new(1); + let even = old::Computed::from({ + let source = source.clone(); + move |ctx| source.get(ctx) % 2 == 0 + }); + let runs = Rc::new(Cell::new(0)); + let children: Vec<_> = (0..FANOUT) + .map(|i| { + let even = even.clone(); + let runs = runs.clone(); + old::Computed::from(move |ctx| { + runs.set(runs.get() + 1); + (even.get(ctx), i) + }) + }) + .collect(); + let subs = children + .iter() + .cloned() + .map(|child| child.subscribe(|_| {})) + .collect(); + FanoutOld { + source, + _children: children, + _subs: subs, + runs, + } +} + +struct FanoutNew { + source: new::Value, + _children: Vec>, + _subs: Vec, + runs: Rc>, +} + +fn setup_fanout_new() -> FanoutNew { + let g = Graph::new(); + let source = g.value(1); + let even = g.computed({ + let source = source.clone(); + move |ctx| source.get(ctx) % 2 == 0 + }); + let runs = Rc::new(Cell::new(0)); + let children: Vec<_> = (0..FANOUT) + .map(|i| { + let even = even.clone(); + let runs = runs.clone(); + g.computed(move |ctx| { + runs.set(runs.get() + 1); + (even.get(ctx), i) + }) + }) + .collect(); + let subs = children + .iter() + .cloned() + .map(|child| child.subscribe(|_| {})) + .collect(); + FanoutNew { + source, + _children: children, + _subs: subs, + runs, + } +} + +#[test] +fn cutoff_fanout_skips_unchanged_children() { + let old_g = setup_fanout_old(); + let new_g = setup_fanout_new(); + + old_g.runs.set(0); + new_g.runs.set(0); + + let old_d = elapsed(|| old_g.source.set(3)); + let new_d = elapsed(|| new_g.source.set(3)); + + report("cutoff 10k (odd→odd)", old_d, new_d); + + assert_eq!( + new_g.runs.get(), + 0, + "new graph must cut off when even/odd is unchanged" + ); + assert_eq!( + old_g.runs.get(), + FANOUT, + "old graph invalidates the whole fan-out" + ); +} + +#[test] +fn fanout_runs_when_parity_changes() { + let old_g = setup_fanout_old(); + let new_g = setup_fanout_new(); + + old_g.runs.set(0); + new_g.runs.set(0); + + let old_d = elapsed(|| old_g.source.set(2)); + let new_d = elapsed(|| new_g.source.set(2)); + + report("fanout 10k (odd→even)", old_d, new_d); + + assert_eq!(old_g.runs.get(), FANOUT); + assert_eq!(new_g.runs.get(), FANOUT); +} + +fn setup_chain_old() -> (old::Value, Vec) { + let source = old::Value::new(0); + let mut prev: old::Computed = source.to_computed(); + let mut subs = Vec::new(); + for _ in 0..CHAIN { + let next = old::Computed::from({ + let prev = prev.clone(); + move |ctx| prev.get(ctx) + 1 + }); + subs.push(next.clone().subscribe(|_| {})); + prev = next; + } + (source, subs) +} + +fn setup_chain_new() -> (new::Value, Vec) { + let g = Graph::new(); + let source = g.value(0); + let mut prev: new::Computed = source.to_computed(); + let mut subs = Vec::new(); + for _ in 0..CHAIN { + let next = g.computed({ + let prev = prev.clone(); + move |ctx| prev.get(ctx) + 1 + }); + subs.push(next.clone().subscribe(|_| {})); + prev = next; + } + (source, subs) +} + +#[test] +fn deep_chain_updates() { + let (old_src, _old_subs) = setup_chain_old(); + let (new_src, _new_subs) = setup_chain_new(); + + // Warm the caches. + old_src.set(1); + new_src.set(1); + + let old_d = elapsed(|| { + for i in 2..(2 + UPDATES as i32) { + old_src.set(i); + } + }); + let new_d = elapsed(|| { + for i in 2..(2 + UPDATES as i32) { + new_src.set(i); + } + }); + + report(&format!("chain {CHAIN} x {UPDATES} sets"), old_d, new_d); +} + +fn setup_diamond_old() -> (old::Value, crate::DropResource) { + let a = old::Value::new(1); + let left = old::Computed::from({ + let a = a.clone(); + move |ctx| a.get(ctx) + 1 + }); + let right = old::Computed::from({ + let a = a.clone(); + move |ctx| a.get(ctx) * 10 + }); + let d = old::Computed::from({ + let left = left.clone(); + let right = right.clone(); + move |ctx| left.get(ctx) + right.get(ctx) + }); + let sub = d.subscribe(|_| {}); + (a, sub) +} + +fn setup_diamond_new() -> (new::Value, crate::DropResource) { + let g = Graph::new(); + let a = g.value(1); + let left = g.computed({ + let a = a.clone(); + move |ctx| a.get(ctx) + 1 + }); + let right = g.computed({ + let a = a.clone(); + move |ctx| a.get(ctx) * 10 + }); + let d = g.computed({ + let left = left.clone(); + let right = right.clone(); + move |ctx| left.get(ctx) + right.get(ctx) + }); + let sub = d.subscribe(|_| {}); + (a, sub) +} + +#[test] +fn diamond_repeated_updates() { + let (old_a, _old_sub) = setup_diamond_old(); + let (new_a, _new_sub) = setup_diamond_new(); + + old_a.set(2); + new_a.set(2); + + let old_d = elapsed(|| { + for i in 3..(3 + UPDATES as i32) { + old_a.set(i); + } + }); + let new_d = elapsed(|| { + for i in 3..(3 + UPDATES as i32) { + new_a.set(i); + } + }); + + report(&format!("diamond x {UPDATES} sets"), old_d, new_d); +} diff --git a/crates/vertigo/src/computed/computed_box.rs b/crates/vertigo/src/reactive_old/computed.rs similarity index 92% rename from crates/vertigo/src/computed/computed_box.rs rename to crates/vertigo/src/reactive_old/computed.rs index 6edac66ce..27bf881c0 100644 --- a/crates/vertigo/src/computed/computed_box.rs +++ b/crates/vertigo/src/reactive_old/computed.rs @@ -1,14 +1,8 @@ use std::rc::Rc; -use crate::{ - DomNode, - render::{render_value, render_value_option}, -}; +use crate::{DropResource, struct_mut::ValueMut}; -use super::{ - DropResource, GraphValue, Value, context::Context, get_dependencies, graph_id::GraphId, - struct_mut::ValueMut, -}; +use super::{GraphValue, Value, context::Context, get_dependencies, graph_id::GraphId}; /// A reactive value that is read-only and computed by dependency graph. /// @@ -167,16 +161,6 @@ impl Computed { DropResource::from_struct(graph_value) } - - /// Render value inside this [Computed]. See [Value::render_value()] for examples. - pub fn render_value(&self, render: impl Fn(T) -> DomNode + 'static) -> DomNode { - render_value(self.clone(), render) - } - - /// Render optional value inside this [Computed]. See [Value::render_value_option()] for examples. - pub fn render_value_option(&self, render: impl Fn(T) -> Option + 'static) -> DomNode { - render_value_option(self.clone(), render) - } } impl From> for Computed { diff --git a/crates/vertigo/src/computed/context.rs b/crates/vertigo/src/reactive_old/context.rs similarity index 93% rename from crates/vertigo/src/computed/context.rs rename to crates/vertigo/src/reactive_old/context.rs index d7c0df79b..c2e3881f1 100644 --- a/crates/vertigo/src/computed/context.rs +++ b/crates/vertigo/src/reactive_old/context.rs @@ -2,7 +2,8 @@ use std::any::Any; use std::collections::BTreeSet; use std::rc::Rc; -use super::{GraphId, struct_mut::VecMut}; +use crate::reactive_old::GraphId; +use crate::struct_mut::VecMut; pub enum Context { Computed { @@ -46,7 +47,7 @@ impl Context { #[test] fn test_context() { - use crate::computed::graph_id::GraphIdKind; + use crate::reactive_old::graph_id::GraphIdKind; let context = Context::computed(); diff --git a/crates/vertigo/src/computed/dependencies/external_connections.rs b/crates/vertigo/src/reactive_old/dependencies/external_connections.rs similarity index 92% rename from crates/vertigo/src/computed/dependencies/external_connections.rs rename to crates/vertigo/src/reactive_old/dependencies/external_connections.rs index c3a36d7f7..b85ed19ca 100644 --- a/crates/vertigo/src/computed/dependencies/external_connections.rs +++ b/crates/vertigo/src/reactive_old/dependencies/external_connections.rs @@ -1,6 +1,7 @@ use std::rc::Rc; -use crate::computed::{DropResource, GraphId, struct_mut::BTreeMapMut}; +use crate::reactive_old::GraphId; +use crate::{DropResource, struct_mut::BTreeMapMut}; pub type ConnectType = Rc DropResource>; diff --git a/crates/vertigo/src/computed/dependencies/graph.rs b/crates/vertigo/src/reactive_old/dependencies/graph.rs similarity index 98% rename from crates/vertigo/src/computed/dependencies/graph.rs rename to crates/vertigo/src/reactive_old/dependencies/graph.rs index 4dc807219..0ae29e7f1 100644 --- a/crates/vertigo/src/computed/dependencies/graph.rs +++ b/crates/vertigo/src/reactive_old/dependencies/graph.rs @@ -1,7 +1,7 @@ use super::external_connections::ExternalConnections; use super::graph_connections::GraphConnections; use super::refresh::Refresh; -use crate::computed::graph_id::GraphId; +use crate::reactive_old::GraphId; use std::collections::BTreeSet; pub struct Graph { diff --git a/crates/vertigo/src/computed/dependencies/graph_connections.rs b/crates/vertigo/src/reactive_old/dependencies/graph_connections.rs similarity index 98% rename from crates/vertigo/src/computed/dependencies/graph_connections.rs rename to crates/vertigo/src/reactive_old/dependencies/graph_connections.rs index 038bcdbe7..0e5f40c12 100644 --- a/crates/vertigo/src/computed/dependencies/graph_connections.rs +++ b/crates/vertigo/src/reactive_old/dependencies/graph_connections.rs @@ -1,6 +1,7 @@ use std::collections::{BTreeMap, BTreeSet}; -use crate::computed::{graph_id::GraphId, struct_mut::ValueMut}; +use crate::reactive_old::GraphId; +use crate::struct_mut::ValueMut; use super::graph_one_to_many::GraphOneToMany; diff --git a/crates/vertigo/src/computed/dependencies/graph_one_to_many.rs b/crates/vertigo/src/reactive_old/dependencies/graph_one_to_many.rs similarity index 98% rename from crates/vertigo/src/computed/dependencies/graph_one_to_many.rs rename to crates/vertigo/src/reactive_old/dependencies/graph_one_to_many.rs index 70b585c2c..20163cd17 100644 --- a/crates/vertigo/src/computed/dependencies/graph_one_to_many.rs +++ b/crates/vertigo/src/reactive_old/dependencies/graph_one_to_many.rs @@ -1,6 +1,6 @@ use std::collections::{BTreeMap, BTreeSet}; -use crate::computed::GraphId; +use crate::reactive_old::GraphId; pub struct GraphEdgeIter<'a> { data: Option<&'a BTreeSet>, diff --git a/crates/vertigo/src/computed/dependencies/hook.rs b/crates/vertigo/src/reactive_old/dependencies/hook.rs similarity index 86% rename from crates/vertigo/src/computed/dependencies/hook.rs rename to crates/vertigo/src/reactive_old/dependencies/hook.rs index c083182f0..fd217a81f 100644 --- a/crates/vertigo/src/computed/dependencies/hook.rs +++ b/crates/vertigo/src/reactive_old/dependencies/hook.rs @@ -1,4 +1,4 @@ -use crate::{computed::DropResource, driver_module::event_emitter::EventEmitter}; +use crate::{DropResource, driver_module::event_emitter::EventEmitter}; #[derive(Clone)] pub struct Hooks { diff --git a/crates/vertigo/src/computed/dependencies/mod.rs b/crates/vertigo/src/reactive_old/dependencies/mod.rs similarity index 98% rename from crates/vertigo/src/computed/dependencies/mod.rs rename to crates/vertigo/src/reactive_old/dependencies/mod.rs index 40c463243..6321d7706 100644 --- a/crates/vertigo/src/computed/dependencies/mod.rs +++ b/crates/vertigo/src/reactive_old/dependencies/mod.rs @@ -1,7 +1,7 @@ use std::{collections::BTreeSet, rc::Rc}; use vertigo_macro::store; -use crate::{Context, computed::GraphId}; +use crate::reactive_old::{Context, GraphId}; use super::graph_id::GraphIdKind; diff --git a/crates/vertigo/src/computed/dependencies/refresh.rs b/crates/vertigo/src/reactive_old/dependencies/refresh.rs similarity index 94% rename from crates/vertigo/src/computed/dependencies/refresh.rs rename to crates/vertigo/src/reactive_old/dependencies/refresh.rs index 7f3908255..807a163ff 100644 --- a/crates/vertigo/src/computed/dependencies/refresh.rs +++ b/crates/vertigo/src/reactive_old/dependencies/refresh.rs @@ -1,6 +1,7 @@ use std::rc::Rc; -use crate::computed::{graph_id::GraphId, struct_mut::BTreeMapMut}; +use crate::reactive_old::GraphId; +use crate::struct_mut::BTreeMapMut; pub struct Refresh { refresh: BTreeMapMut>, // Reference to GraphValue for refreshing if necessary diff --git a/crates/vertigo/src/computed/dependencies/transaction_state.rs b/crates/vertigo/src/reactive_old/dependencies/transaction_state.rs similarity index 97% rename from crates/vertigo/src/computed/dependencies/transaction_state.rs rename to crates/vertigo/src/reactive_old/dependencies/transaction_state.rs index 84e3b2748..80bdd1b2c 100644 --- a/crates/vertigo/src/computed/dependencies/transaction_state.rs +++ b/crates/vertigo/src/reactive_old/dependencies/transaction_state.rs @@ -1,6 +1,7 @@ use std::collections::BTreeSet; -use crate::computed::{graph_id::GraphId, struct_mut::ValueMut}; +use crate::reactive_old::GraphId; +use crate::struct_mut::ValueMut; #[derive(Default, PartialEq)] enum State { diff --git a/crates/vertigo/src/computed/graph_id.rs b/crates/vertigo/src/reactive_old/graph_id.rs similarity index 100% rename from crates/vertigo/src/computed/graph_id.rs rename to crates/vertigo/src/reactive_old/graph_id.rs diff --git a/crates/vertigo/src/computed/graph_value.rs b/crates/vertigo/src/reactive_old/graph_value.rs similarity index 96% rename from crates/vertigo/src/computed/graph_value.rs rename to crates/vertigo/src/reactive_old/graph_value.rs index 402f011ed..6bad1b86a 100644 --- a/crates/vertigo/src/computed/graph_value.rs +++ b/crates/vertigo/src/reactive_old/graph_value.rs @@ -1,9 +1,8 @@ use std::any::Any; use std::rc::Rc; -use crate::{Context, Dependencies}; - -use super::{GraphId, get_dependencies, struct_mut::ValueMut}; +use crate::reactive_old::{Context, Dependencies, GraphId, get_dependencies}; +use crate::struct_mut::ValueMut; pub struct GraphValue { deps: Rc, diff --git a/crates/vertigo/src/reactive_old/mod.rs b/crates/vertigo/src/reactive_old/mod.rs new file mode 100644 index 000000000..46ab6572b --- /dev/null +++ b/crates/vertigo/src/reactive_old/mod.rs @@ -0,0 +1,26 @@ +//! Previous reactive graph, compiled only in tests for comparison with [`crate::reactive`]. +#![allow(dead_code)] + +mod computed; +mod context; +mod dependencies; +mod graph_id; +mod graph_value; +mod reactive; +mod to_computed; +mod value; +mod value_inner; + +pub(crate) use computed::Computed; +pub(crate) use context::Context; +pub(crate) use dependencies::{Dependencies, get_dependencies}; +pub(crate) use graph_id::GraphId; +pub(crate) use graph_value::GraphValue; +pub(crate) use to_computed::ToComputed; +pub(crate) use value::Value; + +pub(crate) fn transaction(f: impl FnOnce(&Context) -> R) -> R { + get_dependencies().transaction(f) +} + +mod compare; diff --git a/crates/vertigo/src/computed/reactive.rs b/crates/vertigo/src/reactive_old/reactive.rs similarity index 98% rename from crates/vertigo/src/computed/reactive.rs rename to crates/vertigo/src/reactive_old/reactive.rs index b63e5c202..35f006686 100644 --- a/crates/vertigo/src/computed/reactive.rs +++ b/crates/vertigo/src/reactive_old/reactive.rs @@ -1,4 +1,4 @@ -use crate::{Context, Value}; +use crate::reactive_old::{Context, Value}; /// A trait that tells `Something` is behaving like a [`Value`]. /// diff --git a/crates/vertigo/src/computed/to_computed.rs b/crates/vertigo/src/reactive_old/to_computed.rs similarity index 100% rename from crates/vertigo/src/computed/to_computed.rs rename to crates/vertigo/src/reactive_old/to_computed.rs diff --git a/crates/vertigo/src/computed/value.rs b/crates/vertigo/src/reactive_old/value.rs similarity index 65% rename from crates/vertigo/src/computed/value.rs rename to crates/vertigo/src/reactive_old/value.rs index 895415d63..ae19b293b 100644 --- a/crates/vertigo/src/computed/value.rs +++ b/crates/vertigo/src/reactive_old/value.rs @@ -1,8 +1,10 @@ use std::rc::Rc; -use crate::{Context, DomNode, ToComputed, computed::value_inner::ValueInner}; +use crate::DropResource; -use super::{Computed, DropResource, GraphId, dependencies::get_dependencies}; +use super::{ + Computed, Context, GraphId, ToComputed, dependencies::get_dependencies, value_inner::ValueInner, +}; /// A reactive value. Basic building block of app state. /// @@ -122,60 +124,6 @@ impl Value { }); } - /// Render value (reactively transforms `T` into `DomNode`) - /// - /// See [computed_tuple](macro.computed_tuple.html) if you want to render multiple values in a handy way. - /// - /// ```rust - /// use vertigo::{dom, Value}; - /// - /// let my_value = Value::new(5); - /// - /// let element = my_value.render_value(|bare_value| dom! {
{bare_value}
}); - /// - /// dom! { - ///
- /// {element} - ///
- /// }; - /// ``` - /// - pub fn render_value(&self, render: impl Fn(T) -> DomNode + 'static) -> DomNode { - self.to_computed().render_value(render) - } - - /// Render optional value (reactively transforms `Option` into `Option`) - /// - /// See [computed_tuple](macro.computed_tuple.html) if you want to render multiple values in a handy way. - /// - /// ```rust - /// use vertigo::{dom, Value}; - /// - /// let value1 = Value::new(Some(5)); - /// let value2 = Value::new(None::); - /// - /// let element1 = value1.render_value_option(|bare_value| - /// bare_value.map(|value| dom! {
{value}
}) - /// ); - /// let element2 = value2.render_value_option(|bare_value| - /// match bare_value { - /// Some(value) => Some(dom! {
{value}
}), - /// None => Some(dom! {
"default"
}), - /// } - /// ); - /// - /// dom! { - ///
- /// {element1} - /// {element2} - ///
- /// }; - /// ``` - /// - pub fn render_value_option(&self, render: impl Fn(T) -> Option + 'static) -> DomNode { - self.to_computed().render_value_option(render) - } - pub fn add_event(&self, callback: impl Fn(T) + 'static) -> DropResource { self.inner.add_event(callback) } diff --git a/crates/vertigo/src/computed/value_inner.rs b/crates/vertigo/src/reactive_old/value_inner.rs similarity index 94% rename from crates/vertigo/src/computed/value_inner.rs rename to crates/vertigo/src/reactive_old/value_inner.rs index 50599b58e..6d2f731af 100644 --- a/crates/vertigo/src/computed/value_inner.rs +++ b/crates/vertigo/src/reactive_old/value_inner.rs @@ -1,4 +1,5 @@ -use super::{GraphId, struct_mut::ValueMut}; +use crate::reactive_old::GraphId; +use crate::struct_mut::ValueMut; use crate::{DropResource, driver_module::event_emitter::EventEmitter}; pub struct ValueInner { diff --git a/crates/vertigo/src/render/mod.rs b/crates/vertigo/src/render/mod.rs index 7f04f966b..ac2e106d7 100644 --- a/crates/vertigo/src/render/mod.rs +++ b/crates/vertigo/src/render/mod.rs @@ -5,4 +5,4 @@ mod render_value; pub use render_list::render_list; pub use render_list_memo::{render_list_memo, render_resource_list_memo}; -pub use render_value::{render_value, render_value_option}; +pub use render_value::{RenderValue, render_value, render_value_option}; diff --git a/crates/vertigo/src/render/render_list.rs b/crates/vertigo/src/render/render_list.rs index 4b6bb4e41..ec52ebcda 100644 --- a/crates/vertigo/src/render/render_list.rs +++ b/crates/vertigo/src/render/render_list.rs @@ -5,8 +5,8 @@ use std::{ }; use crate::{ - Computed, DomComment, DomNode, KeyedListItem, ToComputed, computed::struct_mut::ValueMut, - dom::dom_id::DomId, driver_module::get_driver_dom, keyed_computed_list, + Computed, DomComment, DomNode, KeyedListItem, ToComputed, dom::dom_id::DomId, + driver_module::get_driver_dom, keyed_computed_list, struct_mut::ValueMut, }; /// Render an iterable as a keyed list of DOM nodes. @@ -118,7 +118,7 @@ impl Row { } } -fn reorder_nodes( +fn reorder_nodes( parent_id: DomId, comment_id: DomId, mut real_child: VecDeque<(K, Row)>, @@ -141,7 +141,7 @@ fn reorder_nodes( pairs } -fn get_pairs_top( +fn get_pairs_top( current: &mut VecDeque<(K, Row)>, new_child: &mut VecDeque>>, ) -> VecDeque<(K, Row)> { @@ -167,7 +167,7 @@ fn get_pairs_top( } } -fn get_pairs_bottom( +fn get_pairs_bottom( current: &mut VecDeque<(K, Row)>, new_child: &mut VecDeque>>, ) -> VecDeque<(K, Row)> { @@ -193,7 +193,7 @@ fn get_pairs_bottom( } } -fn get_pairs_middle( +fn get_pairs_middle( parent_id: DomId, last_before: DomId, real_child: VecDeque<(K, Row)>, diff --git a/crates/vertigo/src/render/render_value.rs b/crates/vertigo/src/render/render_value.rs index 1201c9a70..06aeb9e64 100644 --- a/crates/vertigo/src/render/render_value.rs +++ b/crates/vertigo/src/render/render_value.rs @@ -1,7 +1,7 @@ use std::rc::Rc; use crate::{ - Computed, DomComment, DomNode, computed::struct_mut::ValueMut, driver_module::get_driver_dom, + Computed, DomComment, DomNode, Value, driver_module::get_driver_dom, struct_mut::ValueMut, }; /// Render a computed value as a DOM node. @@ -47,3 +47,98 @@ pub fn render_value_option( }) .into() } + +impl Computed { + /// Render value inside this [`Computed`]. See [`Value::render_value()`] for examples. + pub fn render_value(&self, render: impl Fn(T) -> DomNode + 'static) -> DomNode { + render_value(self.clone(), render) + } + + /// Render optional value inside this [`Computed`]. See [`Value::render_value_option()`] for examples. + pub fn render_value_option(&self, render: impl Fn(T) -> Option + 'static) -> DomNode { + render_value_option(self.clone(), render) + } +} + +impl Value { + /// Render value (reactively transforms `T` into `DomNode`) + /// + /// See [`computed_tuple`](crate::computed_tuple) if you want to render multiple values. + /// + /// ```rust + /// use vertigo::{dom, Value}; + /// + /// let my_value = Value::new(5); + /// + /// let element = my_value.render_value(|bare_value| dom! {
{bare_value}
}); + /// + /// dom! { + ///
+ /// {element} + ///
+ /// }; + /// ``` + pub fn render_value(&self, render: impl Fn(T) -> DomNode + 'static) -> DomNode { + self.to_computed().render_value(render) + } + + /// Render optional value (reactively transforms `T` into `Option`) + /// + /// See [`computed_tuple`](crate::computed_tuple) if you want to render multiple values. + /// + /// ```rust + /// use vertigo::{dom, Value}; + /// + /// let value1 = Value::new(Some(5)); + /// let value2 = Value::new(None::); + /// + /// let element1 = value1.render_value_option(|bare_value| + /// bare_value.map(|value| dom! {
{value}
}) + /// ); + /// let element2 = value2.render_value_option(|bare_value| + /// match bare_value { + /// Some(value) => Some(dom! {
{value}
}), + /// None => Some(dom! {
"default"
}), + /// } + /// ); + /// + /// dom! { + ///
+ /// {element1} + /// {element2} + ///
+ /// }; + /// ``` + pub fn render_value_option(&self, render: impl Fn(T) -> Option + 'static) -> DomNode { + self.to_computed().render_value_option(render) + } +} + +/// Render a [`Value`] or [`Computed`] as a [`DomNode`]. +/// +/// Prefer the inherent methods [`Value::render_value`] / [`Computed::render_value`]. +/// This trait is useful in generic code. +pub trait RenderValue { + fn render_value(&self, render: impl Fn(T) -> DomNode + 'static) -> DomNode; + fn render_value_option(&self, render: impl Fn(T) -> Option + 'static) -> DomNode; +} + +impl RenderValue for Computed { + fn render_value(&self, render: impl Fn(T) -> DomNode + 'static) -> DomNode { + Computed::render_value(self, render) + } + + fn render_value_option(&self, render: impl Fn(T) -> Option + 'static) -> DomNode { + Computed::render_value_option(self, render) + } +} + +impl RenderValue for Value { + fn render_value(&self, render: impl Fn(T) -> DomNode + 'static) -> DomNode { + Value::render_value(self, render) + } + + fn render_value_option(&self, render: impl Fn(T) -> Option + 'static) -> DomNode { + Value::render_value_option(self, render) + } +} diff --git a/crates/vertigo/src/router.rs b/crates/vertigo/src/router.rs index 47a876b2f..0a83ebfda 100644 --- a/crates/vertigo/src/router.rs +++ b/crates/vertigo/src/router.rs @@ -1,8 +1,8 @@ use crate::{ - Computed, DomNode, EmbedDom, Reactive, ToComputed, - computed::{Value, get_dependencies}, + Computed, DomNode, EmbedDom, Reactive, ToComputed, Value, dev::command::{LocationSetMode, LocationTarget}, driver_module::api::api_location, + transaction, }; /// Router based on path or hash part of current location. @@ -123,7 +123,7 @@ impl + PartialEq + 'static> Router { } fn change(&self, change_fn: impl FnOnce(&mut T)) { - get_dependencies().transaction(|ctx| { + transaction(|ctx| { let mut value = self.get(ctx); change_fn(&mut value); self.set(value); diff --git a/crates/vertigo/src/computed/struct_mut/btree_map_mut.rs b/crates/vertigo/src/struct_mut/btree_map_mut.rs similarity index 100% rename from crates/vertigo/src/computed/struct_mut/btree_map_mut.rs rename to crates/vertigo/src/struct_mut/btree_map_mut.rs diff --git a/crates/vertigo/src/computed/struct_mut/counter_mut.rs b/crates/vertigo/src/struct_mut/counter_mut.rs similarity index 100% rename from crates/vertigo/src/computed/struct_mut/counter_mut.rs rename to crates/vertigo/src/struct_mut/counter_mut.rs diff --git a/crates/vertigo/src/computed/struct_mut/hash_map_mut.rs b/crates/vertigo/src/struct_mut/hash_map_mut.rs similarity index 100% rename from crates/vertigo/src/computed/struct_mut/hash_map_mut.rs rename to crates/vertigo/src/struct_mut/hash_map_mut.rs diff --git a/crates/vertigo/src/computed/struct_mut/inner_value.rs b/crates/vertigo/src/struct_mut/inner_value.rs similarity index 100% rename from crates/vertigo/src/computed/struct_mut/inner_value.rs rename to crates/vertigo/src/struct_mut/inner_value.rs diff --git a/crates/vertigo/src/computed/struct_mut/mod.rs b/crates/vertigo/src/struct_mut/mod.rs similarity index 100% rename from crates/vertigo/src/computed/struct_mut/mod.rs rename to crates/vertigo/src/struct_mut/mod.rs diff --git a/crates/vertigo/src/computed/struct_mut/value_mut.rs b/crates/vertigo/src/struct_mut/value_mut.rs similarity index 100% rename from crates/vertigo/src/computed/struct_mut/value_mut.rs rename to crates/vertigo/src/struct_mut/value_mut.rs diff --git a/crates/vertigo/src/computed/struct_mut/vec_deque_mut.rs b/crates/vertigo/src/struct_mut/vec_deque_mut.rs similarity index 100% rename from crates/vertigo/src/computed/struct_mut/vec_deque_mut.rs rename to crates/vertigo/src/struct_mut/vec_deque_mut.rs diff --git a/crates/vertigo/src/computed/struct_mut/vec_mut.rs b/crates/vertigo/src/struct_mut/vec_mut.rs similarity index 100% rename from crates/vertigo/src/computed/struct_mut/vec_mut.rs rename to crates/vertigo/src/struct_mut/vec_mut.rs diff --git a/crates/vertigo/src/tests/dom/component.rs b/crates/vertigo/src/tests/dom/component.rs index 4ee492130..2d6842859 100644 --- a/crates/vertigo/src/tests/dom/component.rs +++ b/crates/vertigo/src/tests/dom/component.rs @@ -26,6 +26,7 @@ fn test_generics() { #[component] fn Hello(name: T) { + let name = name.to_string(); dom! { "Hello " {name} } diff --git a/crates/vertigo/src/computed/tests/keyed_computed_list.rs b/crates/vertigo/src/tests/keyed_computed_list.rs similarity index 99% rename from crates/vertigo/src/computed/tests/keyed_computed_list.rs rename to crates/vertigo/src/tests/keyed_computed_list.rs index 61de41ceb..17fb09c75 100644 --- a/crates/vertigo/src/computed/tests/keyed_computed_list.rs +++ b/crates/vertigo/src/tests/keyed_computed_list.rs @@ -5,8 +5,8 @@ use std::{ }; use crate::{ - Computed, DropResource, KeyedListItem, Value, computed::struct_mut::ValueMut, - keyed_computed_list, transaction, + Computed, DropResource, KeyedListItem, Value, keyed_computed_list, struct_mut::ValueMut, + transaction, }; #[derive(Clone, PartialEq, Debug)] @@ -546,8 +546,8 @@ fn map_keyed_list_state( create_state: impl Fn(Computed) -> S + 'static, ) -> Computed>> where - T: Clone + 'static, - S: Clone + 'static, + T: Clone + PartialEq + 'static, + S: Clone + PartialEq + 'static, K: Clone + Eq + std::hash::Hash + 'static, { let cache = Rc::new(ValueMut::new(HashMap::>::new())); diff --git a/crates/vertigo/src/tests/mod.rs b/crates/vertigo/src/tests/mod.rs index db6821a88..08d4e94e5 100644 --- a/crates/vertigo/src/tests/mod.rs +++ b/crates/vertigo/src/tests/mod.rs @@ -7,3 +7,5 @@ mod css; mod dom; mod js_macro; mod jsjson_bytes; +mod keyed_computed_list; +mod value_copies; diff --git a/crates/vertigo/src/computed/tests/value_copies.rs b/crates/vertigo/src/tests/value_copies.rs similarity index 100% rename from crates/vertigo/src/computed/tests/value_copies.rs rename to crates/vertigo/src/tests/value_copies.rs diff --git a/demo/app/src/app/sudoku/component/render_cell_possible.rs b/demo/app/src/app/sudoku/component/render_cell_possible.rs index 90efab49f..108f4524d 100644 --- a/demo/app/src/app/sudoku/component/render_cell_possible.rs +++ b/demo/app/src/app/sudoku/component/render_cell_possible.rs @@ -1,4 +1,4 @@ -use std::{collections::HashSet, rc::Rc}; +use std::collections::HashSet; use crate::app::sudoku::state::{Cell, number_item::SudokuValue}; use vertigo::{ClickEvent, Computed, Css, DomNode, bind, bind_rc, css, dom, dom_element}; @@ -75,13 +75,9 @@ fn view_last_value(cell_width: u32, cell: &Cell, possible_last_value: SudokuValu // cell.number.value.set(Some(possible_last_value)); // }); - let on_set = Computed::from(bind!(cell, possible_last_value, |_context| -> Rc< - dyn Fn(ClickEvent) + 'static, - > { - Rc::new(bind!(cell, possible_last_value, |_| { - cell.number.set(Some(possible_last_value)); - })) - })); + let on_set = bind!(cell, possible_last_value, |_| { + cell.number.set(Some(possible_last_value)); + }); dom! {
diff --git a/tests/basic/src/lib.rs b/tests/basic/src/lib.rs index 193876191..fdb1a9930 100644 --- a/tests/basic/src/lib.rs +++ b/tests/basic/src/lib.rs @@ -1,4 +1,4 @@ -use vertigo::{DomNode, Value, bind, dom, main, render::render_list}; +use vertigo::{DomNode, EmbedDom, Value, bind, dom, main, render::render_list}; mod row; use row::Row; @@ -18,6 +18,12 @@ impl std::fmt::Display for Mode { } } +impl EmbedDom for Mode { + fn embed(self) -> DomNode { + self.to_string().embed() + } +} + #[derive(Clone, PartialEq)] pub struct AppState { rows: Value>, From 776c4c10161acee5515a3daa18071ce9eac02b78 Mon Sep 17 00:00:00 2001 From: szagi3891 Date: Thu, 20 Aug 2026 07:18:28 +0200 Subject: [PATCH 02/10] Extract transaction nesting and propagate reentrancy into Transaction. * GraphInner no longer touches the depth and propagating cells directly; enter/leave and a Drop guard own that lifecycle. * Unit tests for reactive graph bookkeeping and when_connect disconnects. --- crates/vertigo/src/reactive/computed.rs | 42 +++++++ crates/vertigo/src/reactive/graph/dirty.rs | 103 ++++++++++++++++++ crates/vertigo/src/reactive/graph/edges.rs | 71 ++++++++++-- crates/vertigo/src/reactive/graph/hooks.rs | 37 +++++++ crates/vertigo/src/reactive/graph/mod.rs | 59 +++++++--- .../vertigo/src/reactive/graph/transaction.rs | 75 +++++++++++++ crates/vertigo/src/reactive/graph/watch.rs | 81 ++++++++++++++ crates/vertigo/src/reactive/tests.rs | 43 ++++++++ 8 files changed, 486 insertions(+), 25 deletions(-) create mode 100644 crates/vertigo/src/reactive/graph/transaction.rs diff --git a/crates/vertigo/src/reactive/computed.rs b/crates/vertigo/src/reactive/computed.rs index d00928bbf..aa17a824b 100644 --- a/crates/vertigo/src/reactive/computed.rs +++ b/crates/vertigo/src/reactive/computed.rs @@ -220,3 +220,45 @@ where self.map(move |left| left + rhs.clone()) } } + +#[cfg(test)] +mod tests { + use super::{super::Graph, *}; + use std::cell::Cell; + + #[test] + fn subscribe_does_not_notify_dependents() { + let g = Graph::new(); + let a = g.value(0); + let id = g.inner.alloc_id(); + let sink = Rc::new(SubscribeInner { + graph: g.inner.clone(), + id, + refresh: Box::new({ + let a = a.clone(); + move |ctx| { + let _ = a.get(ctx); + } + }), + }); + g.inner.register(id, sink.clone()); + sink.refresh(); + + let runs = Rc::new(Cell::new(0)); + let child = g.computed({ + let sink = sink.clone(); + let runs = runs.clone(); + move |ctx| { + runs.set(runs.get() + 1); + ctx.track(id, sink.clone()); + 0 + } + }); + g.transaction(|ctx| { + let _ = child.get(ctx); + }); + runs.set(0); + a.set(1); + assert_eq!(runs.get(), 0); + } +} diff --git a/crates/vertigo/src/reactive/graph/dirty.rs b/crates/vertigo/src/reactive/graph/dirty.rs index 77c0d7287..3b1d4c11f 100644 --- a/crates/vertigo/src/reactive/graph/dirty.rs +++ b/crates/vertigo/src/reactive/graph/dirty.rs @@ -70,6 +70,8 @@ impl Dirty { } /// Parent left the dirty set: dependents waiting on it may become ready. + /// + /// Empty `dirty_parent_count` means nobody is waiting, so skip copying children. pub(super) fn release_parent(&self, parent: NodeId, edges: &Edges) { if self.dirty_parent_count.borrow().is_empty() { return; @@ -79,6 +81,8 @@ impl Dirty { } /// After `refresh`: release waiting children; enqueue dependents only on value change. + /// + /// Cutoff with no waiters returns before `fill_scratch` so a large fan-out is not copied. pub(super) fn after_refresh(&self, id: NodeId, changed: bool, edges: &Edges) { let need_release = !self.dirty_parent_count.borrow().is_empty(); if !need_release && !changed { @@ -129,4 +133,103 @@ impl Dirty { self.enqueue(*child, edges); } } + + #[cfg(test)] + fn wait_count(&self, id: NodeId) -> Option { + self.dirty_parent_count.borrow().get(&id).copied() + } +} + +#[cfg(test)] +mod tests { + use super::super::{ErasedNode, NodeId, edges::Edges}; + use super::*; + use std::rc::Rc; + + struct N; + impl ErasedNode for N { + fn refresh(&self) -> bool { + false + } + } + + fn slot(id: u64) -> (NodeId, Rc) { + (NodeId(id), Rc::new(N)) + } + + #[test] + fn ready_is_fifo() { + let dirty = Dirty::new(); + let edges = Edges::new(); + dirty.enqueue(NodeId(1), &edges); + dirty.enqueue(NodeId(2), &edges); + assert_eq!(dirty.take_ready(), Some(NodeId(1))); + assert_eq!(dirty.take_ready(), Some(NodeId(2))); + } + + #[test] + fn leftover_when_waiting_child_never_released() { + let dirty = Dirty::new(); + let edges = Edges::new(); + edges.replace(NodeId(2), vec![slot(1)]); + dirty.enqueue(NodeId(1), &edges); + dirty.enqueue(NodeId(2), &edges); + dirty.dequeue(NodeId(1)); + assert_eq!(dirty.take_ready(), None); + assert!(dirty.cycle_leftover().is_some()); + } + + #[test] + fn enqueue_dedups_ready() { + let dirty = Dirty::new(); + let edges = Edges::new(); + dirty.enqueue(NodeId(1), &edges); + dirty.enqueue(NodeId(1), &edges); + assert_eq!(dirty.take_ready(), Some(NodeId(1))); + assert_eq!(dirty.take_ready(), None); + } + + #[test] + fn child_not_ready_while_parent_dirty() { + let dirty = Dirty::new(); + let edges = Edges::new(); + edges.replace(NodeId(2), vec![slot(1)]); + dirty.enqueue(NodeId(1), &edges); + dirty.enqueue(NodeId(2), &edges); + assert_eq!(dirty.take_ready(), Some(NodeId(1))); + assert_eq!(dirty.take_ready(), None); + } + + #[test] + fn take_ready_skips_dequeued() { + let dirty = Dirty::new(); + let edges = Edges::new(); + dirty.enqueue(NodeId(1), &edges); + dirty.dequeue(NodeId(1)); + assert_eq!(dirty.take_ready(), None); + } + + #[test] + fn dequeue_clears_wait_count() { + let dirty = Dirty::new(); + let edges = Edges::new(); + edges.replace(NodeId(2), vec![slot(1)]); + dirty.enqueue(NodeId(1), &edges); + dirty.enqueue(NodeId(2), &edges); + dirty.dequeue(NodeId(2)); + assert_eq!(dirty.wait_count(NodeId(2)), None); + } + + #[test] + fn cutoff_does_not_enqueue_waiting_sibling() { + let dirty = Dirty::new(); + let edges = Edges::new(); + edges.replace(NodeId(2), vec![slot(1)]); + edges.replace(NodeId(3), vec![slot(1)]); + dirty.enqueue(NodeId(1), &edges); + dirty.enqueue(NodeId(3), &edges); + dirty.dequeue(NodeId(1)); + dirty.after_refresh(NodeId(1), false, &edges); + assert!(!dirty.contains(NodeId(2))); + } } diff --git a/crates/vertigo/src/reactive/graph/edges.rs b/crates/vertigo/src/reactive/graph/edges.rs index 6981b4770..a543589ed 100644 --- a/crates/vertigo/src/reactive/graph/edges.rs +++ b/crates/vertigo/src/reactive/graph/edges.rs @@ -141,18 +141,65 @@ impl Edges { } fn same_parent_ids(old: &HashSet, pairs: &ParentList) -> bool { - if old.is_empty() { - return pairs.is_empty(); - } - for (id, _) in pairs { - if !old.contains(id) { - return false; - } - } - if pairs.len() < old.len() { - return false; + pairs.iter().all(|(id, _)| old.contains(id)) + && old + .iter() + .all(|id| pairs.iter().any(|(parent, _)| parent == id)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + struct N; + impl ErasedNode for N { + fn refresh(&self) -> bool { + false } - old.iter() - .all(|id| pairs.iter().any(|(parent, _)| parent == id)) + } + + fn slot(id: u64) -> (NodeId, Rc) { + (NodeId(id), Rc::new(N)) + } + + #[test] + fn is_watched_after_replace() { + let edges = Edges::new(); + edges.replace(NodeId(2), vec![slot(1)]); + assert!(edges.is_watched(NodeId(1))); + } + + #[test] + fn empty_children_is_not_watched() { + let edges = Edges::new(); + edges.replace(NodeId(2), vec![slot(1)]); + edges.replace(NodeId(2), vec![]); + assert!(!edges.is_watched(NodeId(1))); + } + + #[test] + fn replace_reports_unwatched() { + let edges = Edges::new(); + edges.replace(NodeId(2), vec![slot(1)]); + let became_unwatched = edges + .replace(NodeId(2), vec![]) + .map(|diff| diff.became_unwatched); + assert_eq!(became_unwatched, Some(vec![NodeId(1)])); + } + + #[test] + fn unregister_clears_parent_from_child() { + let edges = Edges::new(); + edges.replace(NodeId(2), vec![slot(1)]); + edges.unregister(NodeId(1)); + assert_eq!(edges.count_parents_if(NodeId(2), |_| true), 0); + } + + #[test] + fn unregister_reports_unwatched() { + let edges = Edges::new(); + edges.replace(NodeId(2), vec![slot(1)]); + assert_eq!(edges.unregister(NodeId(2)), vec![NodeId(1)]); } } diff --git a/crates/vertigo/src/reactive/graph/hooks.rs b/crates/vertigo/src/reactive/graph/hooks.rs index 8fe091c90..5e30585b7 100644 --- a/crates/vertigo/src/reactive/graph/hooks.rs +++ b/crates/vertigo/src/reactive/graph/hooks.rs @@ -39,3 +39,40 @@ impl Hooks { } } } + +#[cfg(test)] +mod tests { + use super::*; + use std::rc::Rc; + + #[test] + fn remove_does_not_fire() { + let hooks = Hooks::new(); + let n = Rc::new(Cell::new(0)); + let id = hooks.insert({ + let n = n.clone(); + move || n.set(1) + }); + hooks.remove(id); + hooks.fire(); + assert_eq!(n.get(), 0); + } + + #[test] + fn two_hooks_both_fire() { + let hooks = Hooks::new(); + let a = Rc::new(Cell::new(0)); + let b = Rc::new(Cell::new(0)); + hooks.insert({ + let a = a.clone(); + move || a.set(1) + }); + hooks.insert({ + let b = b.clone(); + move || b.set(1) + }); + hooks.fire(); + assert_eq!(a.get(), 1); + assert_eq!(b.get(), 1); + } +} diff --git a/crates/vertigo/src/reactive/graph/mod.rs b/crates/vertigo/src/reactive/graph/mod.rs index da8874b01..de926bca5 100644 --- a/crates/vertigo/src/reactive/graph/mod.rs +++ b/crates/vertigo/src/reactive/graph/mod.rs @@ -6,12 +6,14 @@ mod dirty; mod edges; mod hooks; mod nodes; +mod transaction; mod watch; use dirty::Dirty; use edges::Edges; use hooks::Hooks; use nodes::Nodes; +use transaction::Transaction; use watch::Watch; #[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Debug)] @@ -45,8 +47,7 @@ pub struct Graph { /// Persistent state lives in `Nodes`, `Edges`, `Dirty`, `Watch`, `Hooks`. pub(crate) struct GraphInner { next_id: Cell, - tx: Cell, - propagating: Cell, + tx: Transaction, nodes: Nodes, edges: Edges, dirty: Dirty, @@ -73,8 +74,7 @@ impl Graph { Graph { inner: Rc::new(GraphInner { next_id: Cell::new(1), - tx: Cell::new(0), - propagating: Cell::new(false), + tx: Transaction::new(), nodes: Nodes::new(), edges: Edges::new(), dirty: Dirty::new(), @@ -96,14 +96,12 @@ impl Graph { } pub fn transaction(&self, f: impl FnOnce(&Context) -> R) -> R { - self.inner.tx.set(self.inner.tx.get() + 1); + self.inner.tx.enter(); let ctx = Context::read(); let result = f(&ctx); - self.inner.tx.set(self.inner.tx.get() - 1); - if self.inner.tx.get() == 0 { - let nested_in_propagate = self.inner.propagating.get(); + if let Some(leave) = self.inner.tx.leave() { self.inner.propagate(); - if !nested_in_propagate { + if !leave.already_propagating { self.inner.hooks.fire(); } } @@ -159,15 +157,14 @@ impl GraphInner { /// Process dirty nodes in topological order. Dependents are enqueued only when /// the node's value changed. pub(crate) fn propagate(&self) { - if self.tx.get() != 0 || self.propagating.get() { + if !self.tx.can_propagate() { return; } - self.propagating.set(true); + let _guard = self.tx.start_propagate(); loop { let Some(id) = self.dirty.take_ready() else { if let Some(leftover) = self.dirty.cycle_leftover() { - self.propagating.set(false); panic!("vertigo: cycle in dirty graph ({leftover:?})"); } break; @@ -183,7 +180,43 @@ impl GraphInner { let changed = node.refresh(); self.dirty.after_refresh(id, changed, &self.edges); } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + struct N; + impl ErasedNode for N { + fn refresh(&self) -> bool { + false + } + } + + fn slot(id: u64) -> (NodeId, Rc) { + (NodeId(id), Rc::new(N)) + } + + #[test] + fn unregister_releases_waiting_child() { + let g = Graph::new(); + let inner = &*g.inner; + inner.edges.replace(NodeId(2), vec![slot(1)]); + inner.dirty.enqueue(NodeId(1), &inner.edges); + inner.dirty.enqueue(NodeId(2), &inner.edges); + inner.unregister(NodeId(1)); + assert_eq!(inner.dirty.take_ready(), Some(NodeId(2))); + } - self.propagating.set(false); + #[test] + fn dead_dirty_parent_releases_waiting_child() { + let g = Graph::new(); + let inner = &*g.inner; + inner.edges.replace(NodeId(2), vec![slot(1)]); + inner.dirty.enqueue(NodeId(1), &inner.edges); + inner.dirty.enqueue(NodeId(2), &inner.edges); + inner.propagate(); + assert!(!inner.dirty.contains(NodeId(2))); } } diff --git a/crates/vertigo/src/reactive/graph/transaction.rs b/crates/vertigo/src/reactive/graph/transaction.rs new file mode 100644 index 000000000..1b1777da6 --- /dev/null +++ b/crates/vertigo/src/reactive/graph/transaction.rs @@ -0,0 +1,75 @@ +use std::cell::Cell; + +/// Nesting depth of `Graph::transaction` and the reentrancy flag for `propagate`. +pub(super) struct Transaction { + depth: Cell, + propagating: Cell, +} + +/// The outermost transaction just closed. +pub(super) struct OuterLeave { + /// A write landed while `propagate` was already running (skip nested hooks). + pub already_propagating: bool, +} + +/// Clears `propagating` when the wave ends (including panic). +pub(super) struct Propagating<'a> { + tx: &'a Transaction, +} + +impl Drop for Propagating<'_> { + fn drop(&mut self) { + self.tx.propagating.set(false); + } +} + +impl Transaction { + pub(super) fn new() -> Self { + Self { + depth: Cell::new(0), + propagating: Cell::new(false), + } + } + + pub(super) fn enter(&self) { + self.depth.set(self.depth.get() + 1); + } + + /// Close one nesting level. `Some` when this was the outermost transaction. + pub(super) fn leave(&self) -> Option { + let depth = self.depth.get(); + debug_assert!(depth > 0); + self.depth.set(depth - 1); + if depth == 1 { + Some(OuterLeave { + already_propagating: self.propagating.get(), + }) + } else { + None + } + } + + /// `true` when no transaction is open and no propagate wave is running. + pub(super) fn can_propagate(&self) -> bool { + self.depth.get() == 0 && !self.propagating.get() + } + + /// Start a propagate wave. Call only when [`Self::can_propagate`] is `true`. + pub(super) fn start_propagate(&self) -> Propagating<'_> { + debug_assert!(self.can_propagate()); + self.propagating.set(true); + Propagating { tx: self } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn cannot_propagate_while_propagating() { + let tx = Transaction::new(); + let _wave = tx.start_propagate(); + assert!(!tx.can_propagate()); + } +} diff --git a/crates/vertigo/src/reactive/graph/watch.rs b/crates/vertigo/src/reactive/graph/watch.rs index 127d5467d..b73fd5511 100644 --- a/crates/vertigo/src/reactive/graph/watch.rs +++ b/crates/vertigo/src/reactive/graph/watch.rs @@ -64,3 +64,84 @@ impl Watch { let _dropped = self.connected.borrow_mut().remove(&id); } } + +#[cfg(test)] +mod tests { + use super::*; + use std::cell::Cell; + + #[test] + fn register_connects_when_watched() { + let watch = Watch::new(); + let n = Rc::new(Cell::new(0)); + watch.register( + NodeId(1), + Rc::new({ + let n = n.clone(); + move || { + n.set(1); + DropResource::new(|| {}) + } + }), + true, + ); + assert_eq!(n.get(), 1); + } + + fn connect_flag(flag: &Rc>) -> Rc DropResource> { + let flag = flag.clone(); + Rc::new(move || { + flag.set(flag.get() + 1); + DropResource::new({ + let flag = flag.clone(); + move || flag.set(flag.get() - 1) + }) + }) + } + + #[test] + fn watched_twice_connects_once() { + let watch = Watch::new(); + let connects = Rc::new(Cell::new(0)); + let connect = Rc::new({ + let connects = connects.clone(); + move || { + connects.set(connects.get() + 1); + DropResource::new(|| {}) + } + }); + watch.register(NodeId(1), connect.clone(), true); + watch.register(NodeId(1), connect, true); + assert_eq!(connects.get(), 1); + } + + #[test] + fn unregister_drops_connected() { + let watch = Watch::new(); + let live = Rc::new(Cell::new(0)); + watch.register(NodeId(1), connect_flag(&live), true); + watch.unregister(NodeId(1)); + assert_eq!(live.get(), 0); + } + + #[test] + fn apply_unwatched_drops_connected() { + let watch = Watch::new(); + let live = Rc::new(Cell::new(0)); + watch.register(NodeId(1), connect_flag(&live), true); + watch.apply(ParentDiff { + became_watched: Vec::new(), + became_unwatched: vec![NodeId(1)], + }); + assert_eq!(live.get(), 0); + } + + #[test] + fn unwatched_many_drops_connected() { + let watch = Watch::new(); + let live = Rc::new(Cell::new(0)); + watch.register(NodeId(1), connect_flag(&live), true); + watch.on_unwatched_many(vec![NodeId(1)]); + assert_eq!(live.get(), 0); + } +} diff --git a/crates/vertigo/src/reactive/tests.rs b/crates/vertigo/src/reactive/tests.rs index 635ead3ab..43f860289 100644 --- a/crates/vertigo/src/reactive/tests.rs +++ b/crates/vertigo/src/reactive/tests.rs @@ -283,6 +283,30 @@ fn when_connect_multiple_subscribers_share_one_connection() { assert_eq!(disconnect_count.get(), 1); } +#[test] +fn when_connect_disconnects_while_computed_lives() { + let connect = Rc::new(Cell::new(0)); + let disconnect = Rc::new(Cell::new(0)); + let value = Value::new(1); + let comp = value.to_computed().when_connect({ + let connect = connect.clone(); + let disconnect = disconnect.clone(); + move || { + connect.set(1); + DropResource::new({ + let disconnect = disconnect.clone(); + move || disconnect.set(1) + }) + } + }); + let keep = comp.clone(); + let sub = comp.subscribe(|_| {}); + assert_eq!(connect.get(), 1); + drop(sub); + assert_eq!(disconnect.get(), 1); + drop(keep); +} + #[test] fn nested_computed_subscription() { let token_value = Value::new("token1".to_string()); @@ -352,3 +376,22 @@ fn on_after_transaction_fires_after_set() { a.set(2); assert_eq!(fires.get(), 1); } + +#[test] +fn hook_once_when_set_during_propagate() { + let g = Graph::new(); + let fires = Rc::new(Cell::new(0)); + let _hook = g.on_after_transaction({ + let fires = fires.clone(); + move || fires.set(fires.get() + 1) + }); + let a = g.value(0); + let b = g.value(0); + let _sub = a.to_computed().subscribe({ + let b = b.clone(); + move |_| b.set(1) + }); + fires.set(0); + a.set(1); + assert_eq!(fires.get(), 1); +} From 05b535750773e74c7691a5c9a4235e2351bc9b84 Mon Sep 17 00:00:00 2001 From: Michal Pokrywka Date: Thu, 20 Aug 2026 15:08:04 +0200 Subject: [PATCH 03/10] fix: add refresh budget to reactive propagation to detect, log and ignore on callback loops refactor: optimize parent comparison by using HashSet equality and add verification tests --- crates/vertigo/src/reactive/graph/edges.rs | 44 +++- crates/vertigo/src/reactive/graph/mod.rs | 42 +++- crates/vertigo/src/reactive/mod.rs | 2 + .../vertigo/src/reactive/propagation_order.rs | 200 ++++++++++++++++++ crates/vertigo/src/reactive/tests.rs | 80 +++++++ 5 files changed, 358 insertions(+), 10 deletions(-) create mode 100644 crates/vertigo/src/reactive/propagation_order.rs diff --git a/crates/vertigo/src/reactive/graph/edges.rs b/crates/vertigo/src/reactive/graph/edges.rs index a543589ed..bc6a5f1e6 100644 --- a/crates/vertigo/src/reactive/graph/edges.rs +++ b/crates/vertigo/src/reactive/graph/edges.rs @@ -56,16 +56,20 @@ impl Edges { /// Replace `child`'s parent set. `None` means the parent ids were already the same. pub(super) fn replace(&self, child: NodeId, pairs: ParentList) -> Option { + // Collect the ids before comparing. Set-against-set is linear, while comparing the + // stored set against the raw `pairs` list is quadratic - and `pairs` carries one + // entry per `get` call, duplicates included, so it can be much longer than the set. + let new_parents: HashSet = pairs.iter().map(|(id, _)| *id).collect(); + { let child_parents = self.child_parents.borrow(); if let Some(old) = child_parents.get(&child) - && Self::same_parent_ids(old, &pairs) + && *old == new_parents { return None; } } - let new_parents: HashSet = pairs.iter().map(|(id, _)| *id).collect(); let kept: Vec> = pairs.into_iter().map(|(_, slot)| slot).collect(); let old = self @@ -139,13 +143,6 @@ impl Edges { } became_unwatched } - - fn same_parent_ids(old: &HashSet, pairs: &ParentList) -> bool { - pairs.iter().all(|(id, _)| old.contains(id)) - && old - .iter() - .all(|id| pairs.iter().any(|(parent, _)| parent == id)) - } } #[cfg(test)] @@ -188,6 +185,35 @@ mod tests { assert_eq!(became_unwatched, Some(vec![NodeId(1)])); } + /// Parent sets are compared as sets: order must not matter. + #[test] + fn replace_with_reordered_parents_is_noop() { + let edges = Edges::new(); + edges.replace(NodeId(3), vec![slot(1), slot(2)]); + assert!(edges.replace(NodeId(3), vec![slot(2), slot(1)]).is_none()); + } + + /// `ParentList` holds one entry per `get` call, so the same parent can repeat. + #[test] + fn replace_with_duplicate_parents_is_noop() { + let edges = Edges::new(); + edges.replace(NodeId(3), vec![slot(1)]); + assert!( + edges + .replace(NodeId(3), vec![slot(1), slot(1), slot(1)]) + .is_none() + ); + } + + #[test] + fn replace_with_different_parents_is_applied() { + let edges = Edges::new(); + edges.replace(NodeId(3), vec![slot(1)]); + assert!(edges.replace(NodeId(3), vec![slot(2)]).is_some()); + assert!(!edges.is_watched(NodeId(1))); + assert!(edges.is_watched(NodeId(2))); + } + #[test] fn unregister_clears_parent_from_child() { let edges = Edges::new(); diff --git a/crates/vertigo/src/reactive/graph/mod.rs b/crates/vertigo/src/reactive/graph/mod.rs index de926bca5..119876a43 100644 --- a/crates/vertigo/src/reactive/graph/mod.rs +++ b/crates/vertigo/src/reactive/graph/mod.rs @@ -1,4 +1,8 @@ -use std::{cell::Cell, rc::Rc}; +use std::{ + cell::Cell, + collections::{HashMap, HashSet}, + rc::Rc, +}; use super::{Computed, Context, DropResource, Value, context::ParentList}; @@ -33,6 +37,14 @@ impl GraphId { } } +/// How many times one node may refresh inside a single propagation. +/// +/// A write from a reactive callback re-dirties nodes in the wave that is already running, +/// so a feedback loop that settles is fine and is expected to take a handful of rounds. +/// One that never settles would spin forever, so past this budget the node is dropped from +/// the rest of the wave. +pub(crate) const MAX_REFRESH_PER_NODE: u32 = 100; + pub(crate) trait ErasedNode { fn refresh(&self) -> bool; } @@ -156,11 +168,22 @@ impl GraphInner { /// Process dirty nodes in topological order. Dependents are enqueued only when /// the node's value changed. + /// + /// A write performed from inside a reactive callback joins the wave that is already + /// running, which is what makes `Value::set` usable from a `subscribe` callback. It + /// also means a callback that keeps writing back into its own dependency never lets + /// the wave drain, so each node gets a refresh budget - see [`MAX_REFRESH_PER_NODE`]. + /// + /// A node that burns its budget is logged and dropped from the rest of the wave. + /// The rest of the graph still settles, and only that node and whatever depends on it + /// are left stale until something dirties them again. pub(crate) fn propagate(&self) { if !self.tx.can_propagate() { return; } let _guard = self.tx.start_propagate(); + let mut refreshes: HashMap = HashMap::new(); + let mut cut_off: HashSet = HashSet::new(); loop { let Some(id) = self.dirty.take_ready() else { @@ -172,11 +195,28 @@ impl GraphInner { self.dirty.dequeue(id); + if cut_off.contains(&id) { + // Already given up on this one; let anything waiting on it move along. + self.dirty.release_parent(id, &self.edges); + continue; + } + let Some(node) = self.nodes.upgrade(id) else { self.dirty.release_parent(id, &self.edges); continue; }; + let count = refreshes.entry(id).or_insert(0); + *count += 1; + if *count > MAX_REFRESH_PER_NODE { + log::error!( + "vertigo: node {id:?} refreshed {MAX_REFRESH_PER_NODE} times in one run - dropping (possible cycle)." + ); + cut_off.insert(id); + self.dirty.release_parent(id, &self.edges); + continue; + } + let changed = node.refresh(); self.dirty.after_refresh(id, changed, &self.edges); } diff --git a/crates/vertigo/src/reactive/mod.rs b/crates/vertigo/src/reactive/mod.rs index e207ca322..5c60195dc 100644 --- a/crates/vertigo/src/reactive/mod.rs +++ b/crates/vertigo/src/reactive/mod.rs @@ -63,5 +63,7 @@ pub fn on_after_transaction(callback: impl Fn() + 'static) -> DropResource { default_graph().on_after_transaction(callback) } +#[cfg(test)] +mod propagation_order; #[cfg(test)] mod tests; diff --git a/crates/vertigo/src/reactive/propagation_order.rs b/crates/vertigo/src/reactive/propagation_order.rs new file mode 100644 index 000000000..4cc16b52e --- /dev/null +++ b/crates/vertigo/src/reactive/propagation_order.rs @@ -0,0 +1,200 @@ +//! Ordering inside one propagation pass. +//! +//! These tests pin *current* behaviour, which is not the behaviour we want. A node can be +//! computed several times in a single pass, and a subscriber can observe intermediate +//! values that correspond to no consistent state of the inputs. +//! +//! Cause: [`Dirty::enqueue`](super::graph) counts how many of a node's parents are dirty +//! *at the moment it is enqueued*, but the dirty set grows while the pass runs - a +//! dependent is only enqueued once its parent's value really changed (the equality +//! cutoff). So a node can be declared ready while a parent that is about to become dirty +//! has not entered the queue yet. Nothing later corrects the order; the node simply runs +//! again when that parent finally changes. +//! +//! Each test carries the assertions that *should* hold, commented out. They describe the +//! outcome of refreshing a dirty node when it is read, instead of serving its cached +//! value - then ordering stops depending on the queue at all. + +use super::Graph; +use std::{cell::Cell, cell::RefCell, rc::Rc}; + +/// Static dependency set, no conditionals: `c` always reads both branches. The two paths +/// from the two roots have different lengths, and that alone is enough. +#[test] +fn static_unequal_path_lengths_recompute_twice() { + let g = Graph::new(); + let d1 = g.value(1i32); + let d2 = g.value(1i32); + + // short path: d1 -> b1 + let b1 = g.computed({ + let d1 = d1.clone(); + move |ctx| d1.get(ctx) + }); + + // long path: d2 -> e2 -> f2 -> b2 + let e2 = g.computed({ + let d2 = d2.clone(); + move |ctx| d2.get(ctx) + }); + let f2 = g.computed({ + let e2 = e2.clone(); + move |ctx| e2.get(ctx) + }); + let b2 = g.computed({ + let f2 = f2.clone(); + move |ctx| f2.get(ctx) + }); + + let runs = Rc::new(Cell::new(0)); + let c = g.computed({ + let b1 = b1.clone(); + let b2 = b2.clone(); + let runs = runs.clone(); + move |ctx| { + runs.set(runs.get() + 1); + b1.get(ctx) * 1000 + b2.get(ctx) + } + }); + + let seen = Rc::new(RefCell::new(Vec::new())); + let _sub = c.subscribe({ + let seen = seen.clone(); + move |value| seen.borrow_mut().push(value) + }); + + runs.set(0); + seen.borrow_mut().clear(); + + g.transaction(|_| { + d1.set(7); + d2.set(9); + }); + + // `b1` finishes first and enqueues `c`. At that point `f2` has not run, so `b2` is not + // dirty yet and `c` is treated as ready - it reads the stale `b2`. + assert_eq!(runs.get(), 2); + assert_eq!(*seen.borrow(), vec![7001, 7009]); + + // 7001 is a state that never existed: new `b1`, old `b2`. + // assert_eq!(runs.get(), 1); + // assert_eq!(*seen.borrow(), vec![7009]); +} + +/// A conditional read adds a second source of the same problem: a parent discovered +/// during this run cannot have been waited for, because the wait count was computed from +/// the parent set of the *previous* run. +/// +/// The later passes show this is not a first-computation effect. It repeats whenever the +/// dependency set is rebuilt, and pass 2 shows it happens even with the set already +/// complete. +#[test] +fn dynamic_branch_switch_recomputes_three_times() { + let g = Graph::new(); + let flag = g.value(true); + let d1 = g.value(1i32); + let d2 = g.value(1i32); + + // d1 -> e1 -> b1 + let e1 = g.computed({ + let d1 = d1.clone(); + move |ctx| d1.get(ctx) + }); + let b1 = g.computed({ + let e1 = e1.clone(); + move |ctx| e1.get(ctx) + }); + + // d2 -> e2 -> f2 -> b2, one hop longer so it settles later + let e2 = g.computed({ + let d2 = d2.clone(); + move |ctx| d2.get(ctx) + }); + let f2 = g.computed({ + let e2 = e2.clone(); + move |ctx| e2.get(ctx) + }); + let b2 = g.computed({ + let f2 = f2.clone(); + move |ctx| f2.get(ctx) + }); + + // Both branches observed, so each holds a cached value and has to refresh in the pass. + let _keep_b1 = b1.clone().subscribe(|_| {}); + let _keep_b2 = b2.clone().subscribe(|_| {}); + + let runs = Rc::new(Cell::new(0)); + let c = g.computed({ + let flag = flag.clone(); + let b1 = b1.clone(); + let b2 = b2.clone(); + let runs = runs.clone(); + move |ctx| { + runs.set(runs.get() + 1); + if flag.get(ctx) { + 0 + } else { + let first = b1.get(ctx); + if first >= 100 { + first + b2.get(ctx) + } else { + first + } + } + } + }); + + let seen = Rc::new(RefCell::new(Vec::new())); + let _sub = c.subscribe({ + let seen = seen.clone(); + move |value| seen.borrow_mut().push(value) + }); + + let mut pass = |label: &str, apply: &dyn Fn()| -> (usize, Vec) { + runs.set(0); + seen.borrow_mut().clear(); + g.transaction(|_| apply()); + let result = (runs.get(), seen.borrow().clone()); + println!("{label}: c ran {} times, saw {:?}", result.0, result.1); + result + }; + + // Pass 1: `c` only listed `flag` as a parent, so it runs before either branch, then + // once more for each branch it discovers. + let (runs_1, seen_1) = pass("pass 1", &|| { + d1.set(500); + d2.set(7); + flag.set(false); + }); + assert_eq!(runs_1, 3); + assert_eq!(seen_1, vec![1, 501, 507]); + // assert_eq!(runs_1, 1); + // assert_eq!(seen_1, vec![507]); + + // Pass 2: nothing is discovered now - `c` already lists both branches - and it still + // runs twice, because `b2` is not yet dirty when `b1` enqueues `c`. + let (runs_2, seen_2) = pass("pass 2", &|| { + d1.set(900); + d2.set(8); + }); + assert_eq!(runs_2, 2); + assert_eq!(seen_2, vec![907, 908]); + // assert_eq!(runs_2, 1); + // assert_eq!(seen_2, vec![908]); + + // Pass 3: back to the cheap branch, `c` drops `b1` and `b2` from its parents. + let (runs_3, seen_3) = pass("pass 3", &|| flag.set(true)); + assert_eq!(runs_3, 1); + assert_eq!(seen_3, vec![0]); + + // Pass 4: the shape of pass 1 again, and the same three runs. + let (runs_4, seen_4) = pass("pass 4", &|| { + d1.set(1500); + d2.set(9); + flag.set(false); + }); + assert_eq!(runs_4, 3); + assert_eq!(seen_4, vec![908, 1508, 1509]); + // assert_eq!(runs_4, 1); + // assert_eq!(seen_4, vec![1509]); +} diff --git a/crates/vertigo/src/reactive/tests.rs b/crates/vertigo/src/reactive/tests.rs index 43f860289..97f713e35 100644 --- a/crates/vertigo/src/reactive/tests.rs +++ b/crates/vertigo/src/reactive/tests.rs @@ -395,3 +395,83 @@ fn hook_once_when_set_during_propagate() { a.set(1); assert_eq!(fires.get(), 1); } + +/// A callback that writes back into its own dependency never lets the wave drain. +/// Without a budget this spins forever; the budget cuts the cycle instead. +#[test] +fn runaway_write_back_is_cut_off() { + let g = Graph::new(); + let a = g.value(0); + let runs = Rc::new(Cell::new(0)); + let _sub = a.to_computed().subscribe({ + let a = a.clone(); + let runs = runs.clone(); + move |value| { + runs.set(runs.get() + 1); + a.set(value + 1); + } + }); + + a.set(100); + + let budget = super::graph::MAX_REFRESH_PER_NODE as usize; + assert!( + runs.get() <= budget + 2, + "loop ran {} times, budget is {budget}", + runs.get() + ); +} + +/// Cutting a cycle must not take the rest of the graph with it: unrelated nodes still +/// settle in that same wave, and the graph keeps working afterwards. +#[test] +fn cut_off_cycle_leaves_the_rest_of_the_graph_working() { + let g = Graph::new(); + let looping = g.value(0); + let _cycle = looping.to_computed().subscribe({ + let looping = looping.clone(); + move |value| looping.set(value + 1) + }); + + let other = g.value(0); + let seen = Rc::new(Cell::new(0)); + let _sub = other.to_computed().subscribe({ + let seen = seen.clone(); + move |value| seen.set(value) + }); + + // Same wave as the runaway one. + g.transaction(|_| { + looping.set(100); + other.set(7); + }); + assert_eq!(seen.get(), 7, "unrelated node must still settle"); + + // And the graph is still usable once the wave is over. + other.set(9); + assert_eq!(seen.get(), 9); +} + +/// A feedback loop that settles is allowed - the budget must not fire on it. +#[test] +fn settling_write_back_is_allowed() { + let g = Graph::new(); + let a = g.value(0); + let runs = Rc::new(Cell::new(0)); + let _sub = a.to_computed().subscribe({ + let a = a.clone(); + let runs = runs.clone(); + move |value| { + runs.set(runs.get() + 1); + // Clamp: walks up to 10 and then stops writing. + if value < 10 { + a.set(value + 1); + } + } + }); + + a.set(5); + + assert_eq!(g.transaction(|ctx| a.get(ctx)), 10); + assert!(runs.get() >= 5, "callback ran {} times", runs.get()); +} From 4c8ef01fb87038f282757f870929f91e3378ebe4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pokrywka?= Date: Thu, 20 Aug 2026 21:07:15 +0200 Subject: [PATCH 04/10] perf: deduplicate consecutive parent node tracking and add unit tests --- crates/vertigo/src/reactive/context.rs | 79 +++++++++++++++++++++++++- 1 file changed, 78 insertions(+), 1 deletion(-) diff --git a/crates/vertigo/src/reactive/context.rs b/crates/vertigo/src/reactive/context.rs index bda947c21..b3da2cd64 100644 --- a/crates/vertigo/src/reactive/context.rs +++ b/crates/vertigo/src/reactive/context.rs @@ -20,9 +20,25 @@ impl Context { } } + /// Record `id` as a parent of the node currently computing. + /// + /// A run of reads of the *same* node collapses into one entry. That is the shape a + /// compute closure produces when it reads one value inside a loop, and it keeps the + /// list proportional to the edges rather than to the reads - everything downstream + /// (the strong ref kept per entry, building the id set in `Edges::replace`) is then + /// paid once per parent. + /// + /// Only *consecutive* repeats collapse. Reads interleaved between several nodes still + /// produce one entry each; `Edges::replace` folds those into a set anyway. A full + /// deduplication here was measured slower on the ordinary many-distinct-parents path, + /// which is why this stays a single comparison. pub(crate) fn track(&self, id: NodeId, slot: Rc) { if let Some(parents) = &self.parents { - parents.borrow_mut().push((id, slot)); + let mut parents = parents.borrow_mut(); + if parents.last().map(|(last, _)| *last) == Some(id) { + return; + } + parents.push((id, slot)); } } @@ -33,3 +49,64 @@ impl Context { } } } + +#[cfg(test)] +mod tests { + use super::*; + + struct N; + impl ErasedNode for N { + fn refresh(&self) -> bool { + false + } + } + + fn ids(parents: &ParentList) -> Vec { + parents.iter().map(|(id, _)| *id).collect() + } + + #[test] + fn repeated_reads_of_one_node_collapse() { + let ctx = Context::tracking(); + for _ in 0..100 { + ctx.track(NodeId(1), Rc::new(N)); + } + + assert_eq!(ids(&ctx.take_parents()), vec![NodeId(1)]); + } + + #[test] + fn distinct_reads_are_all_kept() { + let ctx = Context::tracking(); + ctx.track(NodeId(1), Rc::new(N)); + ctx.track(NodeId(2), Rc::new(N)); + ctx.track(NodeId(3), Rc::new(N)); + + assert_eq!( + ids(&ctx.take_parents()), + vec![NodeId(1), NodeId(2), NodeId(3)] + ); + } + + /// Interleaved repeats are left alone - `Edges::replace` folds them into a set. + #[test] + fn interleaved_repeats_are_left_for_the_edge_set() { + let ctx = Context::tracking(); + ctx.track(NodeId(1), Rc::new(N)); + ctx.track(NodeId(2), Rc::new(N)); + ctx.track(NodeId(1), Rc::new(N)); + + assert_eq!( + ids(&ctx.take_parents()), + vec![NodeId(1), NodeId(2), NodeId(1)] + ); + } + + #[test] + fn read_context_tracks_nothing() { + let ctx = Context::read(); + ctx.track(NodeId(1), Rc::new(N)); + + assert!(ctx.take_parents().is_empty()); + } +} From feaa95c812af8e1d1e17f6247dcd6272f9b57884 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pokrywka?= Date: Thu, 20 Aug 2026 21:36:49 +0200 Subject: [PATCH 05/10] refactor: update EmbedDom trait to support blanket reference types and specific owned types, added missing CHANGELOG entries, bumped to 0.13 --- Cargo.lock | 16 +- Cargo.toml | 6 +- crates/vertigo/docs/reactive-graph.md | 164 ++++++++++++++++++ crates/vertigo/src/dom_macro/dom.rs | 48 +++-- .../vertigo/src/driver_module/src_js/index.ts | 2 +- crates/vertigo/src/driver_module/wasm_run.js | 2 +- .../vertigo/src/driver_module/wasm_run.js.map | 2 +- crates/vertigo/src/lib.rs | 4 + crates/vertigo/src/tests/dom/embed.rs | 75 ++++++++ crates/vertigo/src/tests/dom/mod.rs | 1 + docs/CHANGELOG.md | 83 +++++---- 11 files changed, 345 insertions(+), 58 deletions(-) create mode 100644 crates/vertigo/docs/reactive-graph.md create mode 100644 crates/vertigo/src/tests/dom/embed.rs diff --git a/Cargo.lock b/Cargo.lock index a82153042..95b5195fc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3576,7 +3576,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "vertigo" -version = "0.12.0" +version = "0.13.0" dependencies = [ "base64", "chrono", @@ -3588,7 +3588,7 @@ dependencies = [ [[package]] name = "vertigo-cli" -version = "0.12.0" +version = "0.13.0" dependencies = [ "actix-cors", "actix-files", @@ -3621,7 +3621,7 @@ dependencies = [ [[package]] name = "vertigo-demo" -version = "0.12.0" +version = "0.13.0" dependencies = [ "log", "vertigo", @@ -3629,7 +3629,7 @@ dependencies = [ [[package]] name = "vertigo-demo-server" -version = "0.12.0" +version = "0.13.0" dependencies = [ "actix-web", "actix-ws", @@ -3643,28 +3643,28 @@ dependencies = [ [[package]] name = "vertigo-example-counter" -version = "0.12.0" +version = "0.13.0" dependencies = [ "vertigo", ] [[package]] name = "vertigo-example-router" -version = "0.12.0" +version = "0.13.0" dependencies = [ "vertigo", ] [[package]] name = "vertigo-example-trafficlights" -version = "0.12.0" +version = "0.13.0" dependencies = [ "vertigo", ] [[package]] name = "vertigo-macro" -version = "0.12.0" +version = "0.13.0" dependencies = [ "base64", "crc", diff --git a/Cargo.toml b/Cargo.toml index 8249c4b2c..af6bf93e9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,7 @@ exclude = ["build/vertigo-cli-test/some_app"] resolver = "2" [workspace.package] -version = "0.12.0" +version = "0.13.0" edition = "2024" repository = "https://github.com/vertigo-web/vertigo" authors = [ @@ -32,8 +32,8 @@ keywords = ["wasm", "web", "isomorphic", "reactive", "javascript"] homepage = "https://vertigo.znoj.pl" [workspace.dependencies] -vertigo = { path = "./crates/vertigo", version = "0.12.0" } -vertigo-macro = { path = "./crates/vertigo-macro", version = "0.12.0" } +vertigo = { path = "./crates/vertigo", version = "0.13.0" } +vertigo-macro = { path = "./crates/vertigo-macro", version = "0.13.0" } [workspace.lints.clippy] unwrap_used = "deny" diff --git a/crates/vertigo/docs/reactive-graph.md b/crates/vertigo/docs/reactive-graph.md new file mode 100644 index 000000000..5cbf3e09a --- /dev/null +++ b/crates/vertigo/docs/reactive-graph.md @@ -0,0 +1,164 @@ +# The reactive graph + +How `Value`, `Computed` and `subscribe` fit together, and what happens between a write and +the resulting recomputation. + +## The model + +Three kinds of node: + +| node | created by | holds | +| ---- | ---------- | ----- | +| value | [`Value::new`](crate::Value::new) | a value you write | +| computed | [`Computed::from`](crate::Computed::from), `map`, `to_computed` | a cached value derived from other nodes | +| subscription | [`Computed::subscribe`](crate::Computed::subscribe) | a callback, no value | + +Edges are never declared. They are recorded while a node computes: reading a node through +[`Value::get`](crate::Value::get) / [`Computed::get`](crate::Computed::get) registers the +node that was read as a parent of the node doing the reading. Each run replaces the whole +parent set, so a computed that stops reading something stops depending on it. + +A child holds a strong reference to its parents, so a parent cannot be dropped while +something still reads it. The graph itself only holds weak references, so dropping the last +`Value` / `Computed` handle removes the node. + +## Transactions + +A write is applied immediately, but nothing downstream runs until the outermost transaction +closes: + +```rust,ignore +transaction(|_| { + first_name.set("Ada".to_string()); + last_name.set("Lovelace".to_string()); +}); +// one propagation pass here, not two +``` + +Transactions nest; only the outermost one propagates. `Value::set` outside a transaction is +its own one-write transaction, which is why a lone `set` still works. + +After propagation finishes, the callbacks registered with +[`on_after_transaction`](crate::reactive::on_after_transaction) run. The DOM driver uses +one of those to flush its batched commands. + +## One propagation pass + +The writes leave a set of dirty nodes. The pass repeatedly takes a node whose parents are +all up to date, recomputes it, and then decides whether to carry on: + +```text +value written ──> node recomputed ──> value changed? ──yes──> dependents queued + │ + no + │ + └──> stop +``` + +That last step is the **equality cutoff**. A node's dependents are queued only when its new +value differs (`PartialEq`) from the old one. A change that computes back to the same value +therefore stops where it is instead of travelling through the graph. This is why `Computed` +requires `T: PartialEq`. + +The cutoff is what makes wide fan-out cheap: + +```rust,ignore +let is_even = number.map(|n| n % 2 == 0); +// thousands of nodes reading `is_even` + +number.set(3); // was 1: `is_even` recomputes, stays false, nothing downstream runs +number.set(4); // now it flips, and the fan-out runs +``` + +A subscription never has dependents, so its callback is a leaf of the pass. + +## Laziness + +A `Computed` does not compute when it is created. It computes on the first read, and after +that it is kept up to date by propagation for as long as something references it. A compute +closure therefore runs on a schedule you do not control - it must be a pure function of the +nodes it reads. + +## Writing from a callback + +Writing a `Value` from a `subscribe` callback is allowed. The write joins the pass that is +already running rather than starting a second one: + +```rust,ignore +selected_id.to_computed().subscribe(move |id| { + // fine: runs inside the pass, the update is picked up before it ends + form_dirty.set(false); +}); +``` + +A callback that writes back into a value it depends on can only work if it settles - each +round has to move towards a value that stops changing. One that never settles would spin +forever, so a node may be recomputed at most **100 times** in one pass. Past that the graph +logs + +```text +vertigo: node NodeId(12) refreshed 100 times in one run - dropping (possible cycle). +``` + +and drops that node from the rest of the pass. The rest of the graph still settles and the +application keeps running, with that one node left holding a stale value. It is a +diagnostic, not a supported mode: a loop that reaches it is a bug in the callback. + +Writing from a **compute** closure is a different matter and should be avoided. The closure +is not guaranteed to run once per change (see below), so the write can fire more than once, +and one of those firings may be based on a value that was still stale. + +## Connecting to the outside world + +[`when_connect`](crate::Computed::when_connect) runs a closure when a node gains its first +dependent and drops the returned [`DropResource`](crate::DropResource) when it loses its +last one. This is how a node backed by a fetch, a timer or a socket only does work while +something is actually looking at it. [`Value::with_connect`](crate::Value::with_connect) +packages the common shape of it. + +## Isolated graphs + +`Value::new` and `Computed::from` use one graph per thread. `Graph::new()` creates a +separate one; nodes belonging to different graphs never see each other. Tests use this to +avoid sharing state. + +## Known limitation: ordering within a pass + +A node can be recomputed more than once in a single pass, and a subscriber can observe an +intermediate value that corresponds to no consistent state of the inputs. + +Readiness is decided by counting how many of a node's parents are dirty *at the moment the +node is queued*. The dirty set is not known up front - it grows as the pass runs, because +the cutoff only queues a dependent once its parent's value really changed. So a node can be +declared ready while a parent that is about to become dirty has not been queued yet. When +that parent finally changes, the node runs again. + +Two paths of unequal length from two values written together are enough to show it: + +```text +d1 ─────────────> b1 ┐ + ├──> c +d2 ──> e2 ──> f2 ──> b2 ┘ +``` + +Writing `d1` and `d2` in one transaction computes `c` twice: once with the new `b1` and the +old `b2`, then again once `b2` catches up. A conditional read adds a second source of the +same problem, because a parent discovered during this run cannot have been waited for. + +The tests in `reactive::propagation_order` pin the current behaviour and carry the intended +behaviour alongside, commented out. The fix is to refresh a dirty node when it is read +instead of serving its cached value, which removes both the extra runs and the intermediate +values. + +## Coming from 0.12 + +* The old `Dependencies` type is gone; use [`transaction`](crate::transaction), + [`Driver::transaction`](crate::Driver::transaction) and + [`Driver::on_after_transaction`](crate::Driver::on_after_transaction). +* `Computed` now requires `T: PartialEq` everywhere, not only for `subscribe`. +* `subscribe_all` is gone. It reported every recomputation including the ones that produced + the same value, and those no longer notify anybody. +* Writing a `Value` from a callback used to panic with *"You cannot change the source value + while the dependency graph is being refreshed"*. It is now supported. +* Reading through `transaction(|ctx| ...)` serves the cached value instead of recomputing + the chain behind it. diff --git a/crates/vertigo/src/dom_macro/dom.rs b/crates/vertigo/src/dom_macro/dom.rs index b4da9eefa..3d8d99193 100644 --- a/crates/vertigo/src/dom_macro/dom.rs +++ b/crates/vertigo/src/dom_macro/dom.rs @@ -178,7 +178,14 @@ impl EmbedDom for &mut String { } } -impl EmbedDom for &str { +/// Anything printable, borrowed - `&str`, `&String`, `&u32`, `&MyDisplayType`. +/// +/// This is deliberately a blanket impl over references only. The by-value side is an +/// explicit list (the `impl_embed_to_string!` block below) so that a downstream type implementing +/// [`Display`](std::fmt::Display) can still provide its own `EmbedDom` and render real +/// DOM instead of a text node - a blanket `impl EmbedDom for T` would +/// collide with it. +impl EmbedDom for &T { fn embed(self) -> DomNode { DomNode::Text { node: DomText::new(self.to_string()), @@ -194,14 +201,6 @@ impl EmbedDom for std::rc::Rc { } } -impl EmbedDom for &String { - fn embed(self) -> DomNode { - DomNode::Text { - node: DomText::new((*self).clone()), - } - } -} - macro_rules! impl_embed_to_string { ($($typename:ty),* $(,)?) => { $( @@ -217,7 +216,36 @@ macro_rules! impl_embed_to_string { } impl_embed_to_string!( - String, bool, char, u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize, f32, f64 + String, + std::borrow::Cow<'_, str>, + bool, + char, + u8, + u16, + u32, + u64, + u128, + usize, + i8, + i16, + i32, + i64, + i128, + isize, + f32, + f64, + std::num::NonZeroU8, + std::num::NonZeroU16, + std::num::NonZeroU32, + std::num::NonZeroU64, + std::num::NonZeroU128, + std::num::NonZeroUsize, + std::num::NonZeroI8, + std::num::NonZeroI16, + std::num::NonZeroI32, + std::num::NonZeroI64, + std::num::NonZeroI128, + std::num::NonZeroIsize, ); impl EmbedDom for &Computed { diff --git a/crates/vertigo/src/driver_module/src_js/index.ts b/crates/vertigo/src/driver_module/src_js/index.ts index 91e808175..6c8a9e2a3 100644 --- a/crates/vertigo/src/driver_module/src_js/index.ts +++ b/crates/vertigo/src/driver_module/src_js/index.ts @@ -2,7 +2,7 @@ import { WasmModule } from "./wasm_module"; // vertigo-cli compatibility version, change together with package version. const VERTIGO_COMPAT_VERSION_MAJOR = 0; -const VERTIGO_COMPAT_VERSION_MINOR = 12; +const VERTIGO_COMPAT_VERSION_MINOR = 13; const moduleRun: Set = new Set(); diff --git a/crates/vertigo/src/driver_module/wasm_run.js b/crates/vertigo/src/driver_module/wasm_run.js index f417972ed..361f5e10f 100644 --- a/crates/vertigo/src/driver_module/wasm_run.js +++ b/crates/vertigo/src/driver_module/wasm_run.js @@ -1,2 +1,2 @@ -"use strict";const e=new TextDecoder("utf-8"),t=new TextEncoder;class o{constructor(e,t){this.getUint8Memory=e,this.pointer=0,this.ptr=Number(t>>32n),this.size=Number(t%2n**32n),this.dataView=new DataView(this.getUint8Memory().buffer,this.ptr,this.size)}getByte(){const e=this.dataView.getUint8(this.pointer);return this.pointer+=1,e}setByte(e){this.dataView.setUint8(this.pointer,e),this.pointer+=1}getU16(){const e=this.dataView.getUint16(this.pointer);return this.pointer+=2,e}setU16(e){this.dataView.setUint16(this.pointer,e),this.pointer+=2}getU32(){const e=this.dataView.getUint32(this.pointer);return this.pointer+=4,e}setU32(e){this.dataView.setUint32(this.pointer,e),this.pointer+=4}getI32(){const e=this.dataView.getInt32(this.pointer);return this.pointer+=4,e}setI32(e){this.dataView.setInt32(this.pointer,e),this.pointer+=4}getU64(){const e=this.dataView.getBigUint64(this.pointer);return this.pointer+=8,e}setU64(e){this.dataView.setBigUint64(this.pointer,e),this.pointer+=8}getI64(){const e=this.dataView.getBigInt64(this.pointer);return this.pointer+=8,e}setI64(e){this.dataView.setBigInt64(this.pointer,e),this.pointer+=8}getF64(){const e=this.dataView.getFloat64(this.pointer);return this.pointer+=8,e}setF64(e){this.dataView.setFloat64(this.pointer,e),this.pointer+=8}getBuffer(){const e=this.getU32(),t=this.getUint8Memory().subarray(this.ptr+this.pointer,this.ptr+this.pointer+e);return this.pointer+=e,t}setBuffer(e){const t=e.length;this.setU32(t);this.getUint8Memory().subarray(this.ptr+this.pointer,this.ptr+this.pointer+t).set(e),this.pointer+=t}getString(){return e.decode(this.getBuffer())}setString(e){const o=t.encode(e);this.setBuffer(o)}getSavedSize(){return this.pointer}}const n=1,s=2,r=3,i=4,a=5,l=6,c=7,d=8,h=9,u=e=>{if(!0===e||!1===e||null==e)return 1;if("string"==typeof e)return 5+(new TextEncoder).encode(e).length;if("number"==typeof e)return 9;if(e instanceof Uint8Array)return 5+e.length;if(Array.isArray(e)){let t=5;for(const o of e)t+=u(o);return t}if("object"==typeof e&&null!==e){let t=3;for(const[o,n]of Object.entries(e))t+=4+(new TextEncoder).encode(o).length,t+=u(n);return t}throw new Error("jsJsonGetSize: Unknown type "+typeof e)},m=e=>{const t=e.getByte();if(t===n)return!0;if(t===s)return!1;if(t===r)return null;if(t!==i){if(t===a)return e.getString();if(t===l)return e.getF64();if(t===c){const t=e.getU32(),o=[];for(let n=0;n{if(!0!==e)if(!1!==e)if(null!==e)if(void 0!==e){if("string"==typeof e)return t.setByte(a),void t.setString(e);if("number"==typeof e)return t.setByte(l),void t.setF64(e);if(e instanceof Uint8Array)return t.setByte(h),void t.setBuffer(e);if(!Array.isArray(e)){if("object"==typeof e&&null!==e){const o=Object.entries(e);t.setByte(d),t.setU16(o.length);for(const[e,n]of o)t.setString(e),f(n,t);return}throw new Error("saveJsJsonToBufferItem: Unknown type "+typeof e)}t.setByte(c),t.setU32(e.length);for(const o of e)f(o,t)}else t.setByte(i);else t.setByte(r);else t.setByte(s);else t.setByte(n)},g=async(e,t)=>{const n=await(async(e,t)=>{if("function"==typeof WebAssembly.instantiateStreaming){const o=fetch(e);try{return await WebAssembly.instantiateStreaming(o,t)}catch(e){console.warn("`WebAssembly.instantiateStreaming` failed. This could happen if your server does not serve wasm with `application/wasm` MIME type, but check the original error too. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n",e)}}console.info("fetchModule by WebAssembly.instantiate");const o=await fetch(e),n=await o.arrayBuffer();return await WebAssembly.instantiate(n,t)})(e,t);let s=new Uint8Array(1);const r=()=>{if(n.instance.exports.memory instanceof WebAssembly.Memory)return s.buffer!==n.instance.exports.memory.buffer&&(s=new Uint8Array(n.instance.exports.memory.buffer)),s;throw Error("Missing memory")},i=n.instance.exports;return{exports:i,getUint8Memory:r,wasmCommand:e=>{const t=u(e),n=i.vertigo_export_alloc_block(t),s=new o(r,n);f(e,s);let a=i.vertigo_export_wasm_command(n);if(0n===a)return null;const l=new o(r,a),c=m(l);return i.vertigo_export_free_block(a),c}}};class p{constructor(){this.events=new Set}on(e){let t=!0;const o=o=>{t&&e(o)};return this.events.add(o),()=>{t=!1,this.events.delete(o)}}trigger(e){const t=Array.from(this.events.values());for(const o of t)try{o(e)}catch(e){console.error(e)}}get size(){return this.events.size}}class v{constructor(){this.inner=null,this.resolve=e=>{const t=this.inner;this.inner=null,null!==t&&t.resolve(e)},this.reject=e=>{const t=this.inner;this.inner=null,null!==t&&t.reject(e)},this.isFulfilled=()=>null===this.inner;const[e,t]=(()=>{let e=null,t=null;const o=new Promise((o,n)=>{e=o,t=n});if(null===e)throw Error("createPromiseValue - resolve is null");if(null===t)throw Error("createPromiseValue - reject is null");return[{resolve:e,reject:t},o]})();this.inner=e,this.promise=t}}const w=async(e,t)=>{console.info(`${e} wait ${t}ms`),await(async e=>new Promise(t=>{setTimeout(t,e)}))(t),console.info(`${e} go forth`)};class b{constructor(e){this.host=e,this.formatLog=e=>`Socket ${this.host} ==> ${e}`}}class k{constructor(e,t){this.eventMessage=new p,this.close=e,this.send=t}static connect(e,t,o){const n=new v,s=new v,r=new WebSocket(t);let i=!1;console.info(e.formatLog("starting ..."));const a=()=>{i||(console.info(e.formatLog("close")),i=!0,n.resolve(null),s.resolve(),r.close())},l=new k(a,e=>{i||r.send(e)});setTimeout(()=>{!1===n.isFulfilled()&&(console.error(e.formatLog(`timeout (${o}ms)`)),a())},o);return r.addEventListener("open",()=>{console.info(e.formatLog("open")),n.resolve(l)}),r.addEventListener("error",t=>{console.error(e.formatLog("error"),t),a()}),r.addEventListener("close",a),r.addEventListener("message",t=>{if(i)return;const o=t.data;"string"!=typeof o?console.error(e.formatLog("onMessage - expected string"),o):l.eventMessage.trigger(o)}),{socket:n.promise,done:s.promise}}static startSocket(e,t,o,n){let s=!0,r=null;const i=new b(e);return(async()=>{for(;s;){const a=k.connect(i,e,t),l=await a.socket;if(null!==l){if(r=l,n({type:"socket",socket:l}),l.eventMessage.on(e=>{n({type:"message",message:e})}),await a.done,n({type:"close"}),!s)return void console.info(i.formatLog("disconnect (1)"));await w(i.formatLog("reconnect after close"),o)}else await w(i.formatLog("reconnect after error"),o)}console.info(i.formatLog("disconnect (2)"))})().catch(e=>{console.error(e)}),{send:e=>{null===r?console.error("send fail - missing connection",e):r.send(e)},dispose:()=>{s=!1,r?.close()}}}}const y=e=>{try{return JSON.parse(e)}catch{throw console.error("Failed to parse websocket message",e),Error(e)}},C=(e,t,o)=>{e.wasmCommand({Websocket:{callback:t,message:o}})};class E{constructor(e){this.websocket_register_callback=(e,t)=>{const o=this.getWasm();let n=k.startSocket(e,5e3,3e3,e=>{if(!1!==this.controllerList.has(t)){if("socket"===e.type)return this.socket.set(t,e.socket),void C(o,t,"Connected");if("message"!==e.type)return"close"===e.type?(this.socket.delete(t),void C(o,t,"Disconnected")):(e=>{throw console.error(e),Error("unknown message")})(e);C(o,t,{Message:{message:y(e.message)}})}});this.controllerList.set(t,n)},this.websocket_unregister_callback=e=>{const t=this.controllerList.get(e);void 0!==t?(t.dispose(),this.controllerList.delete(e)):console.error("Expected controller")},this.websocket_send_message=(e,t)=>{const o=this.socket.get(e);var n;void 0===o?console.error(`Missing socket connection for callback_id=${e}`):o.send((n=t,JSON.stringify(n)))},this.getWasm=e,this.controllerList=new Map,this.socket=new Map}}const T=e=>{const t={};for(const{k:o,v:n}of e)t[o]=n;return t},L=e=>{if("None"!==e)return JSON.stringify(e.Data.data)},x=async e=>{const t=e.status,o=e.headers.get("Content-Type");try{if(o?.startsWith("text/plain;"))return{Ok:{status:t,response:{Text:await e.text()}}};return{Ok:{status:t,response:{Json:0===(n=await e.text()).length?null:JSON.parse(n)}}}}catch(e){return{Err:{message:String(e)}}}var n};class A{constructor(e){this.timerSet=(e,t,o)=>{switch(o){case"Interval":{const o=setInterval(()=>{this.getWasm().wasmCommand({TimerCall:{callback:e}})},t);this.data.set(e,{kind:"Interval",timerId:o});break}case"Timeout":{const o=setTimeout(()=>{this.getWasm().wasmCommand({TimerCall:{callback:e}})},t);this.data.set(e,{kind:"Timeout",timerId:o});break}}},this.timerClear=e=>{const t=this.data.get(e);if(void 0===t)throw Error("panic");switch(t.kind){case"Interval":clearInterval(t.timerId);break;case"Timeout":clearTimeout(t.timerId)}},this.getWasm=e,this.data=new Map}}class S{constructor(e){this.trigger=()=>{for(const e of Array.from(this.callback.values()))e()},this.add=e=>{this.callback.set(e,()=>{this.getWasm().wasmCommand({LocationCall:{callback:e,value:this.get()}})})},this.remove=e=>{this.callback.delete(e)},this.push=e=>{this.get()!==e&&(location.hash=e,this.trigger())},this.replace=e=>{this.get()!==e&&history.replaceState(null,"",`#${e}`)},this.getWasm=e,this.callback=new Map,window.addEventListener("hashchange",this.trigger)}get(){return decodeURIComponent(location.hash.substr(1))}}class N{constructor(e){this.trigger=()=>{for(const e of Array.from(this.callback.values()))e()},this.add=e=>{this.callback.set(e,()=>{this.getWasm().wasmCommand({LocationCall:{callback:e,value:this.get()}})})},this.remove=e=>{this.callback.delete(e)},this.push=e=>{this.get()!==e&&(window.history.pushState(null,"",e),this.trigger())},this.replace=e=>{this.get()!==e&&(window.history.replaceState(null,"",e),this.trigger())},this.getWasm=e,this.callback=new Map,window.addEventListener("popstate",this.trigger)}get(){return window.location.pathname+window.location.search+window.location.hash}}class _{constructor(e){this.callback=(e,t,o)=>{switch(t){case"Add":return void this.locations[e].add(o);case"Remove":return void this.locations[e].remove(o)}},this.set=(e,t,o)=>{switch(t){case"Push":return void this.locations[e].push(o);case"Replace":return void this.locations[e].replace(o)}},this.get=e=>this.locations[e].get(),this.locations={Hash:new S(e),History:new N(e)}}}class M{constructor(){this.get=e=>{for(const t of document.cookie.split(";")){if(""===t)continue;const o=t.trim().split("=");if(2!==o.length){console.warn(`Cookies.get: Incorrect number of cookieChunk => ${o.length} in ${t}`);continue}const n=o[0],s=o[1];if(void 0!==n&&void 0!==s){if(n===e)return decodeURIComponent(s)}else console.warn(`Cookies.get: Broken cookie part => ${t}`)}return""},this.getJson=e=>{let t=this.get(e);if(0!==t.length)try{return JSON.parse(t)}catch(e){console.error("Error deserializing cookie",e)}return null},this.set=(e,t,o)=>{const n=null==t?"":encodeURIComponent(t),s=new Date;s.setTime(s.getTime()+1e3*o);let r="expires="+s.toUTCString();document.cookie=`${e}=${n};${r};path=/; samesite=Strict`},this.setJson=(e,t,o)=>{let n=JSON.stringify(t);this.set(e,n,o)}}}const I=(e,t)=>{const o=t-e+1;return e+Math.floor(Math.random()*o)};class B{constructor(e){this.getWasm=e,this.callbacks=new Map,this.observers=new Map}add(e,t,o,n){if("intersect"===o)return this.intersectAdd(e,t,n);const s=e=>"click"===o?this.click(e,n):"submit"===o?this.submit(e,n):"input"===o?this.input(e,n):"change"===o?this.change(e,n):"blur"===o?this.blur(e,n):"mousedown"===o?this.mousedown(e,n):"mouseup"===o?this.mouseup(e,n):"mouseenter"===o?this.mouseenter(e,n):"mouseleave"===o?this.mouseleave(e,n):"keydown"===o||"hook_keydown"===o?this.keydown(e,n):"drop"===o?this.drop(e,n):"load"===o?this.load(e,n):"change_file"===o?this.changeFile(e,n):void console.error(`No support for the event ${o}`);if(this.callbacks.has(n))console.error(`There was already a callback added with the callback_id=${n}`);else if(this.callbacks.set(n,s),"hook_keydown"===o)document.addEventListener("keydown",s,!1);else{const n=e.get("callback_add",t),r="change_file"===o?"change":o;n.addEventListener(r,s,!1)}}remove(e,t,o,n){if("intersect"===o)return this.intersectRemove(n);const s=this.callbacks.get(n);if(this.callbacks.delete(n),void 0!==s)if("hook_keydown"===o)document.removeEventListener("keydown",s);else{const n="change_file"===o?"change":o;e.get("callback_remove",t).removeEventListener(n,s)}else console.error(`The callback is missing with the id=${n}`)}wasmCallback(e,t){return this.getWasm().wasmCommand({CallbackCall:{callback_id:e,value:t}})}intersectAdd(e,t,o){if(this.observers.has(o))return void console.error(`There was already an intersect observer added with the callback_id=${o}`);const n=e.getNode("callback_add",t),s=new IntersectionObserver(e=>{for(const t of e)this.wasmCallback(o,[t.isIntersecting,t.intersectionRatio,t.boundingClientRect.top,t.boundingClientRect.bottom,t.boundingClientRect.height])});s.observe(n),this.observers.set(o,s)}intersectRemove(e){const t=this.observers.get(e);this.observers.delete(e),void 0!==t?t.disconnect():console.error(`The intersect observer is missing with the id=${e}`)}click(e,t){e.preventDefault();let o=this.wasmCallback(t,void 0);null===o||"object"!=typeof o||Array.isArray(o)||("stop_propagation"in o&&!0===o.stop_propagation&&e.stopPropagation(),"prevent_default"in o&&!0===o.prevent_default&&e.preventDefault())}submit(e,t){e.preventDefault(),this.wasmCallback(t,void 0)}input(e,t){const o=e.target;o instanceof HTMLInputElement||o instanceof HTMLTextAreaElement?this.wasmCallback(t,o.value):console.warn("event input ignore",o)}change(e,t){const o=e.target;o instanceof HTMLInputElement||o instanceof HTMLTextAreaElement||o instanceof HTMLSelectElement?this.wasmCallback(t,o.value):console.warn("event input ignore",o)}changeFile(e,t){const o=e.target;if(o instanceof HTMLInputElement&&null!==o.files&&o.files.length>0){const e=[];for(let t=0;t({name:n.name,data:new Uint8Array(e)})))}return e.length>0&&Promise.all(e).then(e=>{const o=[];for(const t of e)o.push([t.name,Array.from(t.data)]);this.wasmCallback(t,[o])}).catch(e=>console.error("changeFile ->",e)),void(o.value="")}console.warn("changeFile: not a file input or no files",o)}blur(e,t){this.wasmCallback(t,void 0)}mousedown(e,t){this.wasmCallback(t,void 0)&&e.preventDefault()}mouseup(e,t){this.wasmCallback(t,void 0)&&e.preventDefault()}mouseenter(e,t){this.wasmCallback(t,void 0)}mouseleave(e,t){this.wasmCallback(t,void 0)}drop(e,t){if(e.preventDefault(),e instanceof DragEvent)if(null===e.dataTransfer)console.error("dom -> drop -> dataTransfer null");else{const o=function(e){const t=[];for(let o=0;o drop -> item - undefined");else{const e=n.getAsFile();null===e?console.error(`dom -> drop -> index:${o} -> It's not a file`):t.push(e.arrayBuffer().then(t=>({name:e.name,data:new Uint8Array(t)})))}}return t}(e.dataTransfer.items);o.length?Promise.all(o).then(e=>{const o=[];for(const t of e){const e=Array.from(t.data);o.push([t.name,e])}this.wasmCallback(t,[o])}).catch(e=>{console.error("callback_drop -> promise.all -> ",e)}):console.error("No files to send")}else console.warn("event drop ignore",e)}keydown(e,t){if(e instanceof KeyboardEvent){return void(!0===this.wasmCallback(t,[e.key,e.code,e.altKey,e.ctrlKey,e.shiftKey,e.metaKey])&&(e.preventDefault(),e.stopPropagation()))}console.warn("keydown ignore",e)}load(e,t){e.preventDefault(),this.wasmCallback(t,void 0)}}function R(e,t){"a"===e.tagName.toLocaleLowerCase()&&function(e,t){e.addEventListener("click",o=>{let n=e.getAttribute("href");null!==n&&(n.startsWith("#")||n.startsWith("http://")||n.startsWith("https://")||n.startsWith("//")||(o.preventDefault(),t.set("History","Push",n),window.scrollTo(0,0)))})}(e,t)}class U{constructor(e,t,o){this.depth=-1,this.matched=0,this.nodes=t,this.appLocation=o,this.virtualNodes=this.createVirtualNodes(e)}hydrate(){this.virtualNodes.get(3)&&this.hydrateNode(3,document.body);this.virtualNodes.get(2)&&this.hydrateNode(2,document.head),console.log("Hydration complete,",(100*this.matched/this.virtualNodes.size).toFixed(2)," % vnodes matched.")}hydrateNode(e,t){const o=this.virtualNodes.get(e);if(!o)return;const n=Array.from(t.childNodes);let s=0;this.depth++;let r=!1;for(const e of o.children){const t=this.virtualNodes.get(e);if(t)if(r&&void 0!==t.value)this.matched++;else{r=!1;for(let o=s;o{let o=t.get(e);return o||(o={id:e,children:[]},t.set(e,o)),o};for(const t of e)if("CreateNode"in t){o(t.CreateNode.id).name=t.CreateNode.name.toUpperCase()}else if("CreateText"in t){o(t.CreateText.id).value=t.CreateText.value}else if("InsertBefore"in t){const e=o(t.InsertBefore.parent),n=t.InsertBefore.child,s=t.InsertBefore.ref_id;if(null==s)e.children.push(n);else{const o=e.children.indexOf(s);-1!==o?e.children.splice(o,0,n):(console.warn(`Hydration: ref_id ${s} not found in parent ${t.InsertBefore.parent}`),e.children.push(n))}}else if("SetAttr"in t){const e=o(t.SetAttr.id);e.attributes||(e.attributes=new Map),e.attributes.set(t.SetAttr.name,t.SetAttr.value)}return t}}class ${constructor(){this.data=new Map,this.initNodes=[...this.getRootHead().childNodes,...this.getRootBody().childNodes],this.style=document.createElement("style")}getRootHtml(){return document.documentElement}getRootHead(){return document.head}getRootBody(){return document.body}set(e,t){1===e||2===e||3===e||this.data.set(e,t)}getAnyOption(e){return 1===e?this.getRootHtml():2===e?this.getRootHead():3===e?this.getRootBody():this.data.get(e)}getAny(e,t){const o=this.getAnyOption(t);if(void 0===o)throw Error(`${e} -> item not found=${t}`);return o}get(e,t){const o=this.getAnyOption(t);if(void 0===o)throw new Error(`${e}->get: Item id not found = ${t}`);return o}getNodeElement(e,t){const o=this.get(e,t);if(o instanceof HTMLElement)return o;throw Error(`Expected id=${t} as HTMLElement`)}getNode(e,t){const o=this.get(e,t);if(o instanceof Element)return o;throw Error(`Expected id=${t} as Element`)}getText(e,t){const o=this.get(e,t);if(o instanceof Text)return o;throw Error(`Expected id=${t} as Text`)}getComment(e,t){const o=this.get(e,t);if(o instanceof Comment)return o;throw Error(`Expected id=${t} as Comment`)}delete(e,t){const o=this.getAnyOption(t);if(this.data.delete(t),void 0===o)throw new Error(`${e}->delete: Item id not found = ${t}`);return o}insertCss(e,t){if(null!==e){const o=document.createTextNode(`\n${e} { ${t} }`);this.style.appendChild(o)}else{const e=document.createTextNode(`\n${t}`);this.style.appendChild(e)}}removeInitNodes(){const e=this.initNodes;if(this.initNodes=null,null!==e)for(const t of e)t.remove()}insertBefore(e,t,o){const n=this.get("insert_before",e),s=this.getAny("insert_before child",t);if(null==o)n.insertBefore(s,null);else{const e=this.getAny("insert_before ref",o);n.insertBefore(s,e)}}addStyles(){this.getRootHead().appendChild(this.style)}hasInitNodes(){return null!==this.initNodes}claimNode(e,t){if(this.data.set(e,t),this.initNodes){const e=this.initNodes.indexOf(t);e>-1&&this.initNodes.splice(e,1)}}has(e){return 1===e||2===e||3===e||this.data.has(e)}}const W=new Set(["animate","animateMotion","animateTransform","circle","clipPath","defs","desc","discard","ellipse","feBlend","feColorMatrix","feComponentTransfer","feComposite","feConvolveMatrix","feDiffuseLighting","feDisplacementMap","feDistantLight","feDropShadow","feFlood","feFuncA","feFuncB","feFuncG","feFuncR","feGaussianBlur","feImage","feMerge","feMergeNode","feMorphology","feOffset","fePointLight","feSpecularLighting","feSpotLight","feTile","feTurbulence","filter","foreignObject","g","hatch","hatchpath","image","line","linearGradient","marker","mask","metadata","mpath","path","pattern","polygon","polyline","radialGradient","rect","set","stop","svg","switch","symbol","text","textPath","tspan","use","view","svg:a","svg:title","svg:desc","svg:script","svg:style"]);class F{constructor(e,t,o){this.metadata=e,this.update=e=>{this.nodes.hasInitNodes()&&this.metadata.getEnabledHydration()&&((e,t,o)=>{new U(e,t,o).hydrate()})(e,this.nodes,this.appLocation);const t=new Set;for(const o of e){try{this.runCommand(o)}catch(e){console.error("bulk_update - item",e,o)}"SetAttr"in o&&"autofocus"===o.SetAttr.name.toLocaleLowerCase()&&t.add(o.SetAttr.id)}t.size>0&&setTimeout(()=>{for(const e of t){this.nodes.getNodeElement(`set focus ${e}`,e).focus()}},0),this.nodes.removeInitNodes(),this.nodes.addStyles()},this.appLocation=t,this.nodes=new $,this.callbacks=new B(o),document.addEventListener("dragover",e=>{e.preventDefault()})}createNode(e,t){if(1===e||2===e||3===e)return;if(this.nodes.has(e))return;const o=(e=>W.has(e)?document.createElementNS("http://www.w3.org/2000/svg",e.replace("svg:","")):document.createElement(e))(t);this.nodes.set(e,o),R(o,this.appLocation)}setAttr(e,t,o){const n=this.nodes.getNode("set_attribute",e);if(n.setAttribute(t,o),"value"==t){if(n instanceof HTMLInputElement)return void(n.value=o);if(n instanceof HTMLTextAreaElement)return n.value=o,void(n.defaultValue=o)}}removeAttr(e,t){const o=this.nodes.getNode("remove_attribute",e);if(o.removeAttribute(t),"value"==t){if(o instanceof HTMLInputElement)return void(o.value="");if(o instanceof HTMLTextAreaElement)return o.value="",void(o.defaultValue="")}}removeNode(e){if(1===e||2===e||3===e)return;this.nodes.delete("remove_node",e).remove()}createText(e,t){if(this.nodes.has(e))return;const o=document.createTextNode(t);this.nodes.set(e,o)}removeText(e){this.nodes.delete("remove_node",e).remove()}updateText(e,t){this.nodes.getText("set_attribute",e).textContent=t}runCommand(e){if("RemoveNode"in e)this.removeNode(e.RemoveNode.id);else if("InsertBefore"in e)this.nodes.insertBefore(e.InsertBefore.parent,e.InsertBefore.child,null===e.InsertBefore.ref_id?null:e.InsertBefore.ref_id);else if("CreateNode"in e)this.createNode(e.CreateNode.id,e.CreateNode.name);else if("CreateText"in e)this.createText(e.CreateText.id,e.CreateText.value);else if("UpdateText"in e)this.updateText(e.UpdateText.id,e.UpdateText.value);else if("SetAttr"in e)this.setAttr(e.SetAttr.id,e.SetAttr.name,e.SetAttr.value);else if("RemoveAttr"in e)this.removeAttr(e.RemoveAttr.id,e.RemoveAttr.name);else if("RemoveText"in e)this.removeText(e.RemoveText.id);else if("InsertCss"in e)this.nodes.insertCss(e.InsertCss.selector,e.InsertCss.value);else{if("CreateComment"in e){const t=document.createComment(e.CreateComment.value);return void this.nodes.set(e.CreateComment.id,t)}if("RemoveComment"in e){return void this.nodes.delete("remove_comment",e.RemoveComment.id).remove()}if("CallbackAdd"in e)this.callbacks.add(this.nodes,e.CallbackAdd.id,e.CallbackAdd.event_name,e.CallbackAdd.callback_id);else{if(!("CallbackRemove"in e))return(e=>{throw console.error(e),Error("unknown command")})(e);this.callbacks.remove(this.nodes,e.CallbackRemove.id,e.CallbackRemove.event_name,e.CallbackRemove.callback_id)}}}}class D{constructor(e,t){this.metadata=e,this.getWasm=t;const o=new _(t);this.dom=new F(e,o,t),this.websocket=new E(t),this.interval=new A(t),this.location=o,this.cookie=new M}exec(e){const t=e;if("FetchCacheGet"===t)return{data:this.metadata.getFetchCache()};if("IsBrowser"===t)return{value:!0};if("GetDateNow"===t)return{value:Date.now()};if("TimezoneOffset"===t)return{value:(new Date).getTimezoneOffset()};if("HistoryBack"===t)return window.history.back(),null;if("FetchExec"in t)return(async(e,t,o)=>{const n=e();try{const e=await fetch(o.url,{method:o.method,headers:T(o.headers),body:L(o.body)}),s=await x(e);n.wasmCommand({FetchExecResponse:{response:s,callback:t}})}catch(e){console.error("fetch error (1)",e);const o={Err:{message:new String(e).toString()}};n.wasmCommand({FetchExecResponse:{response:o,callback:t}})}})(this.getWasm,t.FetchExec.callback,t.FetchExec.request),null;if("WebsocketRegister"in t)return this.websocket.websocket_register_callback(t.WebsocketRegister.host,t.WebsocketRegister.callback),null;if("WebsocketSendMessage"in t)return this.websocket.websocket_send_message(t.WebsocketSendMessage.callback,t.WebsocketSendMessage.message),null;if("WebsocketUnregister"in t)return this.websocket.websocket_unregister_callback(t.WebsocketUnregister.callback),null;if("TimerSet"in t)return this.interval.timerSet(t.TimerSet.callback,t.TimerSet.duration,t.TimerSet.kind),null;if("TimerClear"in t)return this.interval.timerClear(t.TimerClear.callback),null;if("LocationGet"in t)return{value:this.location.get(t.LocationGet.target)};if("LocationCallback"in t)return this.location.callback(t.LocationCallback.target,t.LocationCallback.mode,t.LocationCallback.callback),null;if("LocationSet"in t)return this.location.set(t.LocationSet.target,t.LocationSet.mode,t.LocationSet.value),null;if("CookieGet"in t)return{value:this.cookie.get(t.CookieGet.name)};if("CookieSet"in t)return this.cookie.set(t.CookieSet.name,t.CookieSet.value,t.CookieSet.expires_in),null;if("CookieJsonGet"in t)return{value:this.cookie.getJson(t.CookieJsonGet.name)};if("CookieJsonSet"in t)return this.cookie.setJson(t.CookieJsonSet.name,t.CookieJsonSet.value,t.CookieJsonSet.expires_in),null;if("GetEnv"in t){const e=t.GetEnv.name;return{value:this.metadata.getEnv(e)}}if("Log"in t)switch(t.Log.kind){case"Info":return console.info(t.Log.message,t.Log.arg2,t.Log.arg3,t.Log.arg4),null;case"Debug":return console.debug(t.Log.message,t.Log.arg2,t.Log.arg3,t.Log.arg4),null;case"Error":return console.error(t.Log.message,t.Log.arg2,t.Log.arg3,t.Log.arg4),null;case"Log":return console.log(t.Log.message,t.Log.arg2,t.Log.arg3,t.Log.arg4),null;case"Warn":return console.warn(t.Log.message,t.Log.arg2,t.Log.arg3,t.Log.arg4),null}return"GetRandom"in t?{value:I(t.GetRandom.min,t.GetRandom.max)}:"JsApiCall"in t?this.executeJsApiCall(t.JsApiCall.commands):"DomBulkUpdate"in t?(this.dom.update(t.DomBulkUpdate.list),null):(console.info("exec_command: Arg",t),(()=>{throw Error("assert never")})())}executeJsApiCall(e){let t=null;for(const o of e)if("Root"in o)if("window"===o.Root.name)t=window;else{if("document"!==o.Root.name)return console.error(`Unknown root: ${o.Root.name}`),null;t=document}else if("RootElement"in o){const e=o.RootElement.dom_id,n=this.dom.nodes.getAnyOption(e);if(void 0===n)return console.error(`Element not found: ${e}`),null;t=n}else if("Get"in o){if(null===t)return console.error("Get called on null"),null;t=t[o.Get.property]}else if("Set"in o){if(null===t)return console.error("Set called on null"),null;t[o.Set.property]=o.Set.value,t=void 0}else if("Call"in o){if(null===t)return console.error("Call called on null"),null;t=t[o.Call.method](...o.Call.args)}const o=e=>{if(null==e)return null;if("boolean"==typeof e)return e;if("string"==typeof e)return e;if("number"==typeof e)return e;if(e instanceof Uint8Array)return e;if(Array.isArray(e))return e.map(e=>o(e));if((e=>{if(null===e)return!1;if("object"!=typeof e)return!1;const t=Object.getPrototypeOf(e);return t===Object.prototype||null===t})(e)){const t={};for(const n of Object.keys(e))t[n]=o(e[n]);return t}return null};return o(t)}}class O{constructor(){this.get=e=>this.metadata.getAttribute(e)??null,this.getEnabledHydration=()=>"true"!==this.get("data-env-disable-hydration");const e=document.getElementById("v-metadata");if(null===e)throw Error("Expected v-metadata");this.metadata=e,e.remove()}getEnv(e){return this.get(`data-env-${e}`)}getFetchCache(){return this.get("data-fetch-cache")??null}}class H{constructor(e){this.wasm=e}vertigoEntryFunction(e,t){this.wasm.exports.vertigo_entry_function(e,t)}static async create(e){let t=null;const n=()=>{if(null===t)throw Error("Wasm is no initialized");return t},s=new O,r=new D(s,n);return window.$vertigoApi=r,t=await g(e,{mod:{panic_message:e=>{const t=Number(e%2n**32n),o=Number(e>>32n),s=new TextDecoder("utf-8"),r=n().getUint8Memory().subarray(o,o+t),i=s.decode(r);console.error("PANIC",i)},dom_access:e=>{if(0n===e)return console.error("dom_access - null pointer"),0n;const t=new o(()=>n().getUint8Memory(),e),s=m(t);n().exports.vertigo_export_free_block(e);const i=r.exec(s),a=u(i),l=n().exports.vertigo_export_alloc_block(a),c=new o(()=>n().getUint8Memory(),l);return f(i,c),l}}}),new H(t)}}const J=new Set,V=async()=>{document.querySelectorAll("*[data-vertigo-run-wasm]").forEach(e=>{const t=e.getAttribute("data-vertigo-run-wasm");"string"==typeof t?(async e=>{if(J.has(e))return;if(J.size>0)return void console.error("Only one wasm module can be run",{moduleRun:J,wasm:e});J.add(e),console.info(`Wasm module: "${e}" -> start`);const t=await H.create(e);console.info(`Wasm module: "${e}" -> initialized`),t.vertigoEntryFunction(0,12),console.info(`Wasm module: "${e}" -> launched vertigoEntryFunction with version 0.12`)})(t):console.error("Run error",e)})};window.addEventListener("load",V),setTimeout(V,3e3); +"use strict";const e=new TextDecoder("utf-8"),t=new TextEncoder;class o{constructor(e,t){this.getUint8Memory=e,this.pointer=0,this.ptr=Number(t>>32n),this.size=Number(t%2n**32n),this.dataView=new DataView(this.getUint8Memory().buffer,this.ptr,this.size)}getByte(){const e=this.dataView.getUint8(this.pointer);return this.pointer+=1,e}setByte(e){this.dataView.setUint8(this.pointer,e),this.pointer+=1}getU16(){const e=this.dataView.getUint16(this.pointer);return this.pointer+=2,e}setU16(e){this.dataView.setUint16(this.pointer,e),this.pointer+=2}getU32(){const e=this.dataView.getUint32(this.pointer);return this.pointer+=4,e}setU32(e){this.dataView.setUint32(this.pointer,e),this.pointer+=4}getI32(){const e=this.dataView.getInt32(this.pointer);return this.pointer+=4,e}setI32(e){this.dataView.setInt32(this.pointer,e),this.pointer+=4}getU64(){const e=this.dataView.getBigUint64(this.pointer);return this.pointer+=8,e}setU64(e){this.dataView.setBigUint64(this.pointer,e),this.pointer+=8}getI64(){const e=this.dataView.getBigInt64(this.pointer);return this.pointer+=8,e}setI64(e){this.dataView.setBigInt64(this.pointer,e),this.pointer+=8}getF64(){const e=this.dataView.getFloat64(this.pointer);return this.pointer+=8,e}setF64(e){this.dataView.setFloat64(this.pointer,e),this.pointer+=8}getBuffer(){const e=this.getU32(),t=this.getUint8Memory().subarray(this.ptr+this.pointer,this.ptr+this.pointer+e);return this.pointer+=e,t}setBuffer(e){const t=e.length;this.setU32(t);this.getUint8Memory().subarray(this.ptr+this.pointer,this.ptr+this.pointer+t).set(e),this.pointer+=t}getString(){return e.decode(this.getBuffer())}setString(e){const o=t.encode(e);this.setBuffer(o)}getSavedSize(){return this.pointer}}const n=1,s=2,r=3,i=4,a=5,l=6,c=7,d=8,h=9,u=e=>{if(!0===e||!1===e||null==e)return 1;if("string"==typeof e)return 5+(new TextEncoder).encode(e).length;if("number"==typeof e)return 9;if(e instanceof Uint8Array)return 5+e.length;if(Array.isArray(e)){let t=5;for(const o of e)t+=u(o);return t}if("object"==typeof e&&null!==e){let t=3;for(const[o,n]of Object.entries(e))t+=4+(new TextEncoder).encode(o).length,t+=u(n);return t}throw new Error("jsJsonGetSize: Unknown type "+typeof e)},m=e=>{const t=e.getByte();if(t===n)return!0;if(t===s)return!1;if(t===r)return null;if(t!==i){if(t===a)return e.getString();if(t===l)return e.getF64();if(t===c){const t=e.getU32(),o=[];for(let n=0;n{if(!0!==e)if(!1!==e)if(null!==e)if(void 0!==e){if("string"==typeof e)return t.setByte(a),void t.setString(e);if("number"==typeof e)return t.setByte(l),void t.setF64(e);if(e instanceof Uint8Array)return t.setByte(h),void t.setBuffer(e);if(!Array.isArray(e)){if("object"==typeof e&&null!==e){const o=Object.entries(e);t.setByte(d),t.setU16(o.length);for(const[e,n]of o)t.setString(e),f(n,t);return}throw new Error("saveJsJsonToBufferItem: Unknown type "+typeof e)}t.setByte(c),t.setU32(e.length);for(const o of e)f(o,t)}else t.setByte(i);else t.setByte(r);else t.setByte(s);else t.setByte(n)},g=async(e,t)=>{const n=await(async(e,t)=>{if("function"==typeof WebAssembly.instantiateStreaming){const o=fetch(e);try{return await WebAssembly.instantiateStreaming(o,t)}catch(e){console.warn("`WebAssembly.instantiateStreaming` failed. This could happen if your server does not serve wasm with `application/wasm` MIME type, but check the original error too. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n",e)}}console.info("fetchModule by WebAssembly.instantiate");const o=await fetch(e),n=await o.arrayBuffer();return await WebAssembly.instantiate(n,t)})(e,t);let s=new Uint8Array(1);const r=()=>{if(n.instance.exports.memory instanceof WebAssembly.Memory)return s.buffer!==n.instance.exports.memory.buffer&&(s=new Uint8Array(n.instance.exports.memory.buffer)),s;throw Error("Missing memory")},i=n.instance.exports;return{exports:i,getUint8Memory:r,wasmCommand:e=>{const t=u(e),n=i.vertigo_export_alloc_block(t),s=new o(r,n);f(e,s);let a=i.vertigo_export_wasm_command(n);if(0n===a)return null;const l=new o(r,a),c=m(l);return i.vertigo_export_free_block(a),c}}};class p{constructor(){this.events=new Set}on(e){let t=!0;const o=o=>{t&&e(o)};return this.events.add(o),()=>{t=!1,this.events.delete(o)}}trigger(e){const t=Array.from(this.events.values());for(const o of t)try{o(e)}catch(e){console.error(e)}}get size(){return this.events.size}}class v{constructor(){this.inner=null,this.resolve=e=>{const t=this.inner;this.inner=null,null!==t&&t.resolve(e)},this.reject=e=>{const t=this.inner;this.inner=null,null!==t&&t.reject(e)},this.isFulfilled=()=>null===this.inner;const[e,t]=(()=>{let e=null,t=null;const o=new Promise((o,n)=>{e=o,t=n});if(null===e)throw Error("createPromiseValue - resolve is null");if(null===t)throw Error("createPromiseValue - reject is null");return[{resolve:e,reject:t},o]})();this.inner=e,this.promise=t}}const w=async(e,t)=>{console.info(`${e} wait ${t}ms`),await(async e=>new Promise(t=>{setTimeout(t,e)}))(t),console.info(`${e} go forth`)};class b{constructor(e){this.host=e,this.formatLog=e=>`Socket ${this.host} ==> ${e}`}}class k{constructor(e,t){this.eventMessage=new p,this.close=e,this.send=t}static connect(e,t,o){const n=new v,s=new v,r=new WebSocket(t);let i=!1;console.info(e.formatLog("starting ..."));const a=()=>{i||(console.info(e.formatLog("close")),i=!0,n.resolve(null),s.resolve(),r.close())},l=new k(a,e=>{i||r.send(e)});setTimeout(()=>{!1===n.isFulfilled()&&(console.error(e.formatLog(`timeout (${o}ms)`)),a())},o);return r.addEventListener("open",()=>{console.info(e.formatLog("open")),n.resolve(l)}),r.addEventListener("error",t=>{console.error(e.formatLog("error"),t),a()}),r.addEventListener("close",a),r.addEventListener("message",t=>{if(i)return;const o=t.data;"string"!=typeof o?console.error(e.formatLog("onMessage - expected string"),o):l.eventMessage.trigger(o)}),{socket:n.promise,done:s.promise}}static startSocket(e,t,o,n){let s=!0,r=null;const i=new b(e);return(async()=>{for(;s;){const a=k.connect(i,e,t),l=await a.socket;if(null!==l){if(r=l,n({type:"socket",socket:l}),l.eventMessage.on(e=>{n({type:"message",message:e})}),await a.done,n({type:"close"}),!s)return void console.info(i.formatLog("disconnect (1)"));await w(i.formatLog("reconnect after close"),o)}else await w(i.formatLog("reconnect after error"),o)}console.info(i.formatLog("disconnect (2)"))})().catch(e=>{console.error(e)}),{send:e=>{null===r?console.error("send fail - missing connection",e):r.send(e)},dispose:()=>{s=!1,r?.close()}}}}const y=e=>{try{return JSON.parse(e)}catch{throw console.error("Failed to parse websocket message",e),Error(e)}},C=(e,t,o)=>{e.wasmCommand({Websocket:{callback:t,message:o}})};class E{constructor(e){this.websocket_register_callback=(e,t)=>{const o=this.getWasm();let n=k.startSocket(e,5e3,3e3,e=>{if(!1!==this.controllerList.has(t)){if("socket"===e.type)return this.socket.set(t,e.socket),void C(o,t,"Connected");if("message"!==e.type)return"close"===e.type?(this.socket.delete(t),void C(o,t,"Disconnected")):(e=>{throw console.error(e),Error("unknown message")})(e);C(o,t,{Message:{message:y(e.message)}})}});this.controllerList.set(t,n)},this.websocket_unregister_callback=e=>{const t=this.controllerList.get(e);void 0!==t?(t.dispose(),this.controllerList.delete(e)):console.error("Expected controller")},this.websocket_send_message=(e,t)=>{const o=this.socket.get(e);var n;void 0===o?console.error(`Missing socket connection for callback_id=${e}`):o.send((n=t,JSON.stringify(n)))},this.getWasm=e,this.controllerList=new Map,this.socket=new Map}}const T=e=>{const t={};for(const{k:o,v:n}of e)t[o]=n;return t},L=e=>{if("None"!==e)return JSON.stringify(e.Data.data)},x=async e=>{const t=e.status,o=e.headers.get("Content-Type");try{if(o?.startsWith("text/plain;"))return{Ok:{status:t,response:{Text:await e.text()}}};return{Ok:{status:t,response:{Json:0===(n=await e.text()).length?null:JSON.parse(n)}}}}catch(e){return{Err:{message:String(e)}}}var n};class A{constructor(e){this.timerSet=(e,t,o)=>{switch(o){case"Interval":{const o=setInterval(()=>{this.getWasm().wasmCommand({TimerCall:{callback:e}})},t);this.data.set(e,{kind:"Interval",timerId:o});break}case"Timeout":{const o=setTimeout(()=>{this.getWasm().wasmCommand({TimerCall:{callback:e}})},t);this.data.set(e,{kind:"Timeout",timerId:o});break}}},this.timerClear=e=>{const t=this.data.get(e);if(void 0===t)throw Error("panic");switch(t.kind){case"Interval":clearInterval(t.timerId);break;case"Timeout":clearTimeout(t.timerId)}},this.getWasm=e,this.data=new Map}}class S{constructor(e){this.trigger=()=>{for(const e of Array.from(this.callback.values()))e()},this.add=e=>{this.callback.set(e,()=>{this.getWasm().wasmCommand({LocationCall:{callback:e,value:this.get()}})})},this.remove=e=>{this.callback.delete(e)},this.push=e=>{this.get()!==e&&(location.hash=e,this.trigger())},this.replace=e=>{this.get()!==e&&history.replaceState(null,"",`#${e}`)},this.getWasm=e,this.callback=new Map,window.addEventListener("hashchange",this.trigger)}get(){return decodeURIComponent(location.hash.substr(1))}}class N{constructor(e){this.trigger=()=>{for(const e of Array.from(this.callback.values()))e()},this.add=e=>{this.callback.set(e,()=>{this.getWasm().wasmCommand({LocationCall:{callback:e,value:this.get()}})})},this.remove=e=>{this.callback.delete(e)},this.push=e=>{this.get()!==e&&(window.history.pushState(null,"",e),this.trigger())},this.replace=e=>{this.get()!==e&&(window.history.replaceState(null,"",e),this.trigger())},this.getWasm=e,this.callback=new Map,window.addEventListener("popstate",this.trigger)}get(){return window.location.pathname+window.location.search+window.location.hash}}class _{constructor(e){this.callback=(e,t,o)=>{switch(t){case"Add":return void this.locations[e].add(o);case"Remove":return void this.locations[e].remove(o)}},this.set=(e,t,o)=>{switch(t){case"Push":return void this.locations[e].push(o);case"Replace":return void this.locations[e].replace(o)}},this.get=e=>this.locations[e].get(),this.locations={Hash:new S(e),History:new N(e)}}}class M{constructor(){this.get=e=>{for(const t of document.cookie.split(";")){if(""===t)continue;const o=t.trim().split("=");if(2!==o.length){console.warn(`Cookies.get: Incorrect number of cookieChunk => ${o.length} in ${t}`);continue}const n=o[0],s=o[1];if(void 0!==n&&void 0!==s){if(n===e)return decodeURIComponent(s)}else console.warn(`Cookies.get: Broken cookie part => ${t}`)}return""},this.getJson=e=>{let t=this.get(e);if(0!==t.length)try{return JSON.parse(t)}catch(e){console.error("Error deserializing cookie",e)}return null},this.set=(e,t,o)=>{const n=null==t?"":encodeURIComponent(t),s=new Date;s.setTime(s.getTime()+1e3*o);let r="expires="+s.toUTCString();document.cookie=`${e}=${n};${r};path=/; samesite=Strict`},this.setJson=(e,t,o)=>{let n=JSON.stringify(t);this.set(e,n,o)}}}const I=(e,t)=>{const o=t-e+1;return e+Math.floor(Math.random()*o)};class B{constructor(e){this.getWasm=e,this.callbacks=new Map,this.observers=new Map}add(e,t,o,n){if("intersect"===o)return this.intersectAdd(e,t,n);const s=e=>"click"===o?this.click(e,n):"submit"===o?this.submit(e,n):"input"===o?this.input(e,n):"change"===o?this.change(e,n):"blur"===o?this.blur(e,n):"mousedown"===o?this.mousedown(e,n):"mouseup"===o?this.mouseup(e,n):"mouseenter"===o?this.mouseenter(e,n):"mouseleave"===o?this.mouseleave(e,n):"keydown"===o||"hook_keydown"===o?this.keydown(e,n):"drop"===o?this.drop(e,n):"load"===o?this.load(e,n):"change_file"===o?this.changeFile(e,n):void console.error(`No support for the event ${o}`);if(this.callbacks.has(n))console.error(`There was already a callback added with the callback_id=${n}`);else if(this.callbacks.set(n,s),"hook_keydown"===o)document.addEventListener("keydown",s,!1);else{const n=e.get("callback_add",t),r="change_file"===o?"change":o;n.addEventListener(r,s,!1)}}remove(e,t,o,n){if("intersect"===o)return this.intersectRemove(n);const s=this.callbacks.get(n);if(this.callbacks.delete(n),void 0!==s)if("hook_keydown"===o)document.removeEventListener("keydown",s);else{const n="change_file"===o?"change":o;e.get("callback_remove",t).removeEventListener(n,s)}else console.error(`The callback is missing with the id=${n}`)}wasmCallback(e,t){return this.getWasm().wasmCommand({CallbackCall:{callback_id:e,value:t}})}intersectAdd(e,t,o){if(this.observers.has(o))return void console.error(`There was already an intersect observer added with the callback_id=${o}`);const n=e.getNode("callback_add",t),s=new IntersectionObserver(e=>{for(const t of e)this.wasmCallback(o,[t.isIntersecting,t.intersectionRatio,t.boundingClientRect.top,t.boundingClientRect.bottom,t.boundingClientRect.height])});s.observe(n),this.observers.set(o,s)}intersectRemove(e){const t=this.observers.get(e);this.observers.delete(e),void 0!==t?t.disconnect():console.error(`The intersect observer is missing with the id=${e}`)}click(e,t){e.preventDefault();let o=this.wasmCallback(t,void 0);null===o||"object"!=typeof o||Array.isArray(o)||("stop_propagation"in o&&!0===o.stop_propagation&&e.stopPropagation(),"prevent_default"in o&&!0===o.prevent_default&&e.preventDefault())}submit(e,t){e.preventDefault(),this.wasmCallback(t,void 0)}input(e,t){const o=e.target;o instanceof HTMLInputElement||o instanceof HTMLTextAreaElement?this.wasmCallback(t,o.value):console.warn("event input ignore",o)}change(e,t){const o=e.target;o instanceof HTMLInputElement||o instanceof HTMLTextAreaElement||o instanceof HTMLSelectElement?this.wasmCallback(t,o.value):console.warn("event input ignore",o)}changeFile(e,t){const o=e.target;if(o instanceof HTMLInputElement&&null!==o.files&&o.files.length>0){const e=[];for(let t=0;t({name:n.name,data:new Uint8Array(e)})))}return e.length>0&&Promise.all(e).then(e=>{const o=[];for(const t of e)o.push([t.name,Array.from(t.data)]);this.wasmCallback(t,[o])}).catch(e=>console.error("changeFile ->",e)),void(o.value="")}console.warn("changeFile: not a file input or no files",o)}blur(e,t){this.wasmCallback(t,void 0)}mousedown(e,t){this.wasmCallback(t,void 0)&&e.preventDefault()}mouseup(e,t){this.wasmCallback(t,void 0)&&e.preventDefault()}mouseenter(e,t){this.wasmCallback(t,void 0)}mouseleave(e,t){this.wasmCallback(t,void 0)}drop(e,t){if(e.preventDefault(),e instanceof DragEvent)if(null===e.dataTransfer)console.error("dom -> drop -> dataTransfer null");else{const o=function(e){const t=[];for(let o=0;o drop -> item - undefined");else{const e=n.getAsFile();null===e?console.error(`dom -> drop -> index:${o} -> It's not a file`):t.push(e.arrayBuffer().then(t=>({name:e.name,data:new Uint8Array(t)})))}}return t}(e.dataTransfer.items);o.length?Promise.all(o).then(e=>{const o=[];for(const t of e){const e=Array.from(t.data);o.push([t.name,e])}this.wasmCallback(t,[o])}).catch(e=>{console.error("callback_drop -> promise.all -> ",e)}):console.error("No files to send")}else console.warn("event drop ignore",e)}keydown(e,t){if(e instanceof KeyboardEvent){return void(!0===this.wasmCallback(t,[e.key,e.code,e.altKey,e.ctrlKey,e.shiftKey,e.metaKey])&&(e.preventDefault(),e.stopPropagation()))}console.warn("keydown ignore",e)}load(e,t){e.preventDefault(),this.wasmCallback(t,void 0)}}function R(e,t){"a"===e.tagName.toLocaleLowerCase()&&function(e,t){e.addEventListener("click",o=>{let n=e.getAttribute("href");null!==n&&(n.startsWith("#")||n.startsWith("http://")||n.startsWith("https://")||n.startsWith("//")||(o.preventDefault(),t.set("History","Push",n),window.scrollTo(0,0)))})}(e,t)}class U{constructor(e,t,o){this.depth=-1,this.matched=0,this.nodes=t,this.appLocation=o,this.virtualNodes=this.createVirtualNodes(e)}hydrate(){this.virtualNodes.get(3)&&this.hydrateNode(3,document.body);this.virtualNodes.get(2)&&this.hydrateNode(2,document.head),console.log("Hydration complete,",(100*this.matched/this.virtualNodes.size).toFixed(2)," % vnodes matched.")}hydrateNode(e,t){const o=this.virtualNodes.get(e);if(!o)return;const n=Array.from(t.childNodes);let s=0;this.depth++;let r=!1;for(const e of o.children){const t=this.virtualNodes.get(e);if(t)if(r&&void 0!==t.value)this.matched++;else{r=!1;for(let o=s;o{let o=t.get(e);return o||(o={id:e,children:[]},t.set(e,o)),o};for(const t of e)if("CreateNode"in t){o(t.CreateNode.id).name=t.CreateNode.name.toUpperCase()}else if("CreateText"in t){o(t.CreateText.id).value=t.CreateText.value}else if("InsertBefore"in t){const e=o(t.InsertBefore.parent),n=t.InsertBefore.child,s=t.InsertBefore.ref_id;if(null==s)e.children.push(n);else{const o=e.children.indexOf(s);-1!==o?e.children.splice(o,0,n):(console.warn(`Hydration: ref_id ${s} not found in parent ${t.InsertBefore.parent}`),e.children.push(n))}}else if("SetAttr"in t){const e=o(t.SetAttr.id);e.attributes||(e.attributes=new Map),e.attributes.set(t.SetAttr.name,t.SetAttr.value)}return t}}class ${constructor(){this.data=new Map,this.initNodes=[...this.getRootHead().childNodes,...this.getRootBody().childNodes],this.style=document.createElement("style")}getRootHtml(){return document.documentElement}getRootHead(){return document.head}getRootBody(){return document.body}set(e,t){1===e||2===e||3===e||this.data.set(e,t)}getAnyOption(e){return 1===e?this.getRootHtml():2===e?this.getRootHead():3===e?this.getRootBody():this.data.get(e)}getAny(e,t){const o=this.getAnyOption(t);if(void 0===o)throw Error(`${e} -> item not found=${t}`);return o}get(e,t){const o=this.getAnyOption(t);if(void 0===o)throw new Error(`${e}->get: Item id not found = ${t}`);return o}getNodeElement(e,t){const o=this.get(e,t);if(o instanceof HTMLElement)return o;throw Error(`Expected id=${t} as HTMLElement`)}getNode(e,t){const o=this.get(e,t);if(o instanceof Element)return o;throw Error(`Expected id=${t} as Element`)}getText(e,t){const o=this.get(e,t);if(o instanceof Text)return o;throw Error(`Expected id=${t} as Text`)}getComment(e,t){const o=this.get(e,t);if(o instanceof Comment)return o;throw Error(`Expected id=${t} as Comment`)}delete(e,t){const o=this.getAnyOption(t);if(this.data.delete(t),void 0===o)throw new Error(`${e}->delete: Item id not found = ${t}`);return o}insertCss(e,t){if(null!==e){const o=document.createTextNode(`\n${e} { ${t} }`);this.style.appendChild(o)}else{const e=document.createTextNode(`\n${t}`);this.style.appendChild(e)}}removeInitNodes(){const e=this.initNodes;if(this.initNodes=null,null!==e)for(const t of e)t.remove()}insertBefore(e,t,o){const n=this.get("insert_before",e),s=this.getAny("insert_before child",t);if(null==o)n.insertBefore(s,null);else{const e=this.getAny("insert_before ref",o);n.insertBefore(s,e)}}addStyles(){this.getRootHead().appendChild(this.style)}hasInitNodes(){return null!==this.initNodes}claimNode(e,t){if(this.data.set(e,t),this.initNodes){const e=this.initNodes.indexOf(t);e>-1&&this.initNodes.splice(e,1)}}has(e){return 1===e||2===e||3===e||this.data.has(e)}}const W=new Set(["animate","animateMotion","animateTransform","circle","clipPath","defs","desc","discard","ellipse","feBlend","feColorMatrix","feComponentTransfer","feComposite","feConvolveMatrix","feDiffuseLighting","feDisplacementMap","feDistantLight","feDropShadow","feFlood","feFuncA","feFuncB","feFuncG","feFuncR","feGaussianBlur","feImage","feMerge","feMergeNode","feMorphology","feOffset","fePointLight","feSpecularLighting","feSpotLight","feTile","feTurbulence","filter","foreignObject","g","hatch","hatchpath","image","line","linearGradient","marker","mask","metadata","mpath","path","pattern","polygon","polyline","radialGradient","rect","set","stop","svg","switch","symbol","text","textPath","tspan","use","view","svg:a","svg:title","svg:desc","svg:script","svg:style"]);class F{constructor(e,t,o){this.metadata=e,this.update=e=>{this.nodes.hasInitNodes()&&this.metadata.getEnabledHydration()&&((e,t,o)=>{new U(e,t,o).hydrate()})(e,this.nodes,this.appLocation);const t=new Set;for(const o of e){try{this.runCommand(o)}catch(e){console.error("bulk_update - item",e,o)}"SetAttr"in o&&"autofocus"===o.SetAttr.name.toLocaleLowerCase()&&t.add(o.SetAttr.id)}t.size>0&&setTimeout(()=>{for(const e of t){this.nodes.getNodeElement(`set focus ${e}`,e).focus()}},0),this.nodes.removeInitNodes(),this.nodes.addStyles()},this.appLocation=t,this.nodes=new $,this.callbacks=new B(o),document.addEventListener("dragover",e=>{e.preventDefault()})}createNode(e,t){if(1===e||2===e||3===e)return;if(this.nodes.has(e))return;const o=(e=>W.has(e)?document.createElementNS("http://www.w3.org/2000/svg",e.replace("svg:","")):document.createElement(e))(t);this.nodes.set(e,o),R(o,this.appLocation)}setAttr(e,t,o){const n=this.nodes.getNode("set_attribute",e);if(n.setAttribute(t,o),"value"==t){if(n instanceof HTMLInputElement)return void(n.value=o);if(n instanceof HTMLTextAreaElement)return n.value=o,void(n.defaultValue=o)}}removeAttr(e,t){const o=this.nodes.getNode("remove_attribute",e);if(o.removeAttribute(t),"value"==t){if(o instanceof HTMLInputElement)return void(o.value="");if(o instanceof HTMLTextAreaElement)return o.value="",void(o.defaultValue="")}}removeNode(e){if(1===e||2===e||3===e)return;this.nodes.delete("remove_node",e).remove()}createText(e,t){if(this.nodes.has(e))return;const o=document.createTextNode(t);this.nodes.set(e,o)}removeText(e){this.nodes.delete("remove_node",e).remove()}updateText(e,t){this.nodes.getText("set_attribute",e).textContent=t}runCommand(e){if("RemoveNode"in e)this.removeNode(e.RemoveNode.id);else if("InsertBefore"in e)this.nodes.insertBefore(e.InsertBefore.parent,e.InsertBefore.child,null===e.InsertBefore.ref_id?null:e.InsertBefore.ref_id);else if("CreateNode"in e)this.createNode(e.CreateNode.id,e.CreateNode.name);else if("CreateText"in e)this.createText(e.CreateText.id,e.CreateText.value);else if("UpdateText"in e)this.updateText(e.UpdateText.id,e.UpdateText.value);else if("SetAttr"in e)this.setAttr(e.SetAttr.id,e.SetAttr.name,e.SetAttr.value);else if("RemoveAttr"in e)this.removeAttr(e.RemoveAttr.id,e.RemoveAttr.name);else if("RemoveText"in e)this.removeText(e.RemoveText.id);else if("InsertCss"in e)this.nodes.insertCss(e.InsertCss.selector,e.InsertCss.value);else{if("CreateComment"in e){const t=document.createComment(e.CreateComment.value);return void this.nodes.set(e.CreateComment.id,t)}if("RemoveComment"in e){return void this.nodes.delete("remove_comment",e.RemoveComment.id).remove()}if("CallbackAdd"in e)this.callbacks.add(this.nodes,e.CallbackAdd.id,e.CallbackAdd.event_name,e.CallbackAdd.callback_id);else{if(!("CallbackRemove"in e))return(e=>{throw console.error(e),Error("unknown command")})(e);this.callbacks.remove(this.nodes,e.CallbackRemove.id,e.CallbackRemove.event_name,e.CallbackRemove.callback_id)}}}}class D{constructor(e,t){this.metadata=e,this.getWasm=t;const o=new _(t);this.dom=new F(e,o,t),this.websocket=new E(t),this.interval=new A(t),this.location=o,this.cookie=new M}exec(e){const t=e;if("FetchCacheGet"===t)return{data:this.metadata.getFetchCache()};if("IsBrowser"===t)return{value:!0};if("GetDateNow"===t)return{value:Date.now()};if("TimezoneOffset"===t)return{value:(new Date).getTimezoneOffset()};if("HistoryBack"===t)return window.history.back(),null;if("FetchExec"in t)return(async(e,t,o)=>{const n=e();try{const e=await fetch(o.url,{method:o.method,headers:T(o.headers),body:L(o.body)}),s=await x(e);n.wasmCommand({FetchExecResponse:{response:s,callback:t}})}catch(e){console.error("fetch error (1)",e);const o={Err:{message:new String(e).toString()}};n.wasmCommand({FetchExecResponse:{response:o,callback:t}})}})(this.getWasm,t.FetchExec.callback,t.FetchExec.request),null;if("WebsocketRegister"in t)return this.websocket.websocket_register_callback(t.WebsocketRegister.host,t.WebsocketRegister.callback),null;if("WebsocketSendMessage"in t)return this.websocket.websocket_send_message(t.WebsocketSendMessage.callback,t.WebsocketSendMessage.message),null;if("WebsocketUnregister"in t)return this.websocket.websocket_unregister_callback(t.WebsocketUnregister.callback),null;if("TimerSet"in t)return this.interval.timerSet(t.TimerSet.callback,t.TimerSet.duration,t.TimerSet.kind),null;if("TimerClear"in t)return this.interval.timerClear(t.TimerClear.callback),null;if("LocationGet"in t)return{value:this.location.get(t.LocationGet.target)};if("LocationCallback"in t)return this.location.callback(t.LocationCallback.target,t.LocationCallback.mode,t.LocationCallback.callback),null;if("LocationSet"in t)return this.location.set(t.LocationSet.target,t.LocationSet.mode,t.LocationSet.value),null;if("CookieGet"in t)return{value:this.cookie.get(t.CookieGet.name)};if("CookieSet"in t)return this.cookie.set(t.CookieSet.name,t.CookieSet.value,t.CookieSet.expires_in),null;if("CookieJsonGet"in t)return{value:this.cookie.getJson(t.CookieJsonGet.name)};if("CookieJsonSet"in t)return this.cookie.setJson(t.CookieJsonSet.name,t.CookieJsonSet.value,t.CookieJsonSet.expires_in),null;if("GetEnv"in t){const e=t.GetEnv.name;return{value:this.metadata.getEnv(e)}}if("Log"in t)switch(t.Log.kind){case"Info":return console.info(t.Log.message,t.Log.arg2,t.Log.arg3,t.Log.arg4),null;case"Debug":return console.debug(t.Log.message,t.Log.arg2,t.Log.arg3,t.Log.arg4),null;case"Error":return console.error(t.Log.message,t.Log.arg2,t.Log.arg3,t.Log.arg4),null;case"Log":return console.log(t.Log.message,t.Log.arg2,t.Log.arg3,t.Log.arg4),null;case"Warn":return console.warn(t.Log.message,t.Log.arg2,t.Log.arg3,t.Log.arg4),null}return"GetRandom"in t?{value:I(t.GetRandom.min,t.GetRandom.max)}:"JsApiCall"in t?this.executeJsApiCall(t.JsApiCall.commands):"DomBulkUpdate"in t?(this.dom.update(t.DomBulkUpdate.list),null):(console.info("exec_command: Arg",t),(()=>{throw Error("assert never")})())}executeJsApiCall(e){let t=null;for(const o of e)if("Root"in o)if("window"===o.Root.name)t=window;else{if("document"!==o.Root.name)return console.error(`Unknown root: ${o.Root.name}`),null;t=document}else if("RootElement"in o){const e=o.RootElement.dom_id,n=this.dom.nodes.getAnyOption(e);if(void 0===n)return console.error(`Element not found: ${e}`),null;t=n}else if("Get"in o){if(null===t)return console.error("Get called on null"),null;t=t[o.Get.property]}else if("Set"in o){if(null===t)return console.error("Set called on null"),null;t[o.Set.property]=o.Set.value,t=void 0}else if("Call"in o){if(null===t)return console.error("Call called on null"),null;t=t[o.Call.method](...o.Call.args)}const o=e=>{if(null==e)return null;if("boolean"==typeof e)return e;if("string"==typeof e)return e;if("number"==typeof e)return e;if(e instanceof Uint8Array)return e;if(Array.isArray(e))return e.map(e=>o(e));if((e=>{if(null===e)return!1;if("object"!=typeof e)return!1;const t=Object.getPrototypeOf(e);return t===Object.prototype||null===t})(e)){const t={};for(const n of Object.keys(e))t[n]=o(e[n]);return t}return null};return o(t)}}class O{constructor(){this.get=e=>this.metadata.getAttribute(e)??null,this.getEnabledHydration=()=>"true"!==this.get("data-env-disable-hydration");const e=document.getElementById("v-metadata");if(null===e)throw Error("Expected v-metadata");this.metadata=e,e.remove()}getEnv(e){return this.get(`data-env-${e}`)}getFetchCache(){return this.get("data-fetch-cache")??null}}class H{constructor(e){this.wasm=e}vertigoEntryFunction(e,t){this.wasm.exports.vertigo_entry_function(e,t)}static async create(e){let t=null;const n=()=>{if(null===t)throw Error("Wasm is no initialized");return t},s=new O,r=new D(s,n);return window.$vertigoApi=r,t=await g(e,{mod:{panic_message:e=>{const t=Number(e%2n**32n),o=Number(e>>32n),s=new TextDecoder("utf-8"),r=n().getUint8Memory().subarray(o,o+t),i=s.decode(r);console.error("PANIC",i)},dom_access:e=>{if(0n===e)return console.error("dom_access - null pointer"),0n;const t=new o(()=>n().getUint8Memory(),e),s=m(t);n().exports.vertigo_export_free_block(e);const i=r.exec(s),a=u(i),l=n().exports.vertigo_export_alloc_block(a),c=new o(()=>n().getUint8Memory(),l);return f(i,c),l}}}),new H(t)}}const J=new Set,V=async()=>{document.querySelectorAll("*[data-vertigo-run-wasm]").forEach(e=>{const t=e.getAttribute("data-vertigo-run-wasm");"string"==typeof t?(async e=>{if(J.has(e))return;if(J.size>0)return void console.error("Only one wasm module can be run",{moduleRun:J,wasm:e});J.add(e),console.info(`Wasm module: "${e}" -> start`);const t=await H.create(e);console.info(`Wasm module: "${e}" -> initialized`),t.vertigoEntryFunction(0,13),console.info(`Wasm module: "${e}" -> launched vertigoEntryFunction with version 0.13`)})(t):console.error("Run error",e)})};window.addEventListener("load",V),setTimeout(V,3e3); //# sourceMappingURL=wasm_run.js.map diff --git a/crates/vertigo/src/driver_module/wasm_run.js.map b/crates/vertigo/src/driver_module/wasm_run.js.map index 2377b2cf9..68190a8ad 100644 --- a/crates/vertigo/src/driver_module/wasm_run.js.map +++ b/crates/vertigo/src/driver_module/wasm_run.js.map @@ -1 +1 @@ -{"version":3,"file":"wasm_run.js","sources":["src_js/buffer_cursor.ts","src_js/jsjson.ts","src_js/wasm_init.ts","src_js/api/websocket/event_emiter.ts","src_js/api/websocket/promise.ts","src_js/api/websocket/connection.ts","src_js/api/websocket/websocket.ts","src_js/assert_never.ts","src_js/api/command/fetchExec.ts","src_js/api/command/interval.ts","src_js/api/location/hashrouter.ts","src_js/api/location/historyLocation.ts","src_js/api/location/AppLocation.ts","src_js/api/command/cookies.ts","src_js/api/command/getRandom.ts","src_js/api/command/dom/callbackManager.ts","src_js/api/command/dom/dataTransfer.ts","src_js/api/command/dom/injects.ts","src_js/api/command/dom/hydration.ts","src_js/api/command/dom/map_nodes.ts","src_js/api/command/dom/dom.ts","src_js/api/api.ts","src_js/api/command/fetchCacheGet.ts","src_js/api/metadata.ts","src_js/wasm_module.ts","src_js/index.ts"],"sourcesContent":["///https://javascript.info/arraybuffer-binary-arrays#dataview\n\nconst decoder = new TextDecoder(\"utf-8\");\nconst encoder = new TextEncoder();\n\nexport class BufferCursor {\n private dataView: DataView;\n private pointer: number = 0;\n private ptr: number;\n private size: number;\n\n constructor(\n private getUint8Memory: () => Uint8Array,\n long_ptr: bigint,\n ) {\n this.ptr = Number(long_ptr >> 32n);\n this.size = Number(long_ptr % (2n ** 32n));\n\n this.dataView = new DataView(\n this.getUint8Memory().buffer,\n this.ptr,\n this.size\n );\n }\n\n public getByte(): number {\n const value = this.dataView.getUint8(this.pointer);\n this.pointer += 1;\n return value;\n }\n\n public setByte(byte: number) {\n this.dataView.setUint8(this.pointer, byte);\n this.pointer += 1;\n }\n\n public getU16(): number {\n const value = this.dataView.getUint16(this.pointer);\n this.pointer += 2;\n return value;\n }\n\n public setU16(value: number) {\n this.dataView.setUint16(this.pointer, value);\n this.pointer += 2;\n }\n\n public getU32(): number {\n const value = this.dataView.getUint32(this.pointer);\n this.pointer += 4;\n return value;\n }\n\n public setU32(value: number) {\n this.dataView.setUint32(this.pointer, value);\n this.pointer += 4;\n }\n\n public getI32(): number {\n const value = this.dataView.getInt32(this.pointer);\n this.pointer += 4;\n return value;\n }\n\n public setI32(value: number) {\n this.dataView.setInt32(this.pointer, value);\n this.pointer += 4;\n }\n\n public getU64(): bigint {\n const value = this.dataView.getBigUint64(this.pointer);\n this.pointer += 8;\n return value;\n }\n\n public setU64(value: bigint) {\n this.dataView.setBigUint64(this.pointer, value);\n this.pointer += 8;\n }\n\n public getI64(): bigint {\n const value = this.dataView.getBigInt64(this.pointer);\n this.pointer += 8;\n return value;\n }\n\n public setI64(value: bigint) {\n this.dataView.setBigInt64(this.pointer, value);\n this.pointer += 8;\n }\n\n public getF64(): number {\n const value = this.dataView.getFloat64(this.pointer);\n this.pointer += 8;\n return value;\n }\n\n public setF64(value: number) {\n this.dataView.setFloat64(this.pointer, value);\n this.pointer += 8;\n }\n\n public getBuffer(): Uint8Array {\n const size = this.getU32();\n const result = this\n .getUint8Memory()\n .subarray(\n this.ptr + this.pointer,\n this.ptr + this.pointer + size\n );\n\n this.pointer += size;\n return result;\n }\n\n public setBuffer(buffer: Uint8Array) {\n const size = buffer.length;\n this.setU32(size);\n\n const sub_buffer = this\n .getUint8Memory()\n .subarray(\n this.ptr + this.pointer,\n this.ptr + this.pointer + size\n );\n\n sub_buffer.set(buffer);\n\n this.pointer += size;\n }\n\n public getString(): string {\n return decoder.decode(this.getBuffer());\n }\n\n public setString(value: string) {\n const buffer = encoder.encode(value);\n this.setBuffer(buffer);\n }\n\n public getSavedSize(): number {\n return this.pointer;\n }\n}\n\nexport const getStringSize = (value: string): number => {\n return new TextEncoder().encode(value).length;\n};\n\n","import { BufferCursor } from \"./buffer_cursor\";\n\nconst JsJsonConst = {\n True: 1,\n False: 2,\n Null: 3,\n Undefined: 4,\n String: 5,\n Number: 6,\n List: 7,\n Object: 8,\n Vec: 9,\n} as const;\n\nexport type JsJsonType = boolean | null | undefined | string | number | Uint8Array | Array | { [key: string]: JsJsonType };\n\nexport const jsJsonGetSize = (value: JsJsonType): number => {\n if (value === true || value === false || value === null || value === undefined) {\n return 1;\n }\n\n if (typeof value === 'string') {\n return 1 + 4 + new TextEncoder().encode(value).length;\n }\n\n if (typeof value === 'number') {\n return 1 + 8;\n }\n\n if (value instanceof Uint8Array) {\n return 1 + 4 + value.length;\n }\n\n if (Array.isArray(value)) {\n let sum = 1 + 4;\n for (const item of value) {\n sum += jsJsonGetSize(item);\n }\n return sum;\n }\n\n if (typeof value === 'object' && value !== null) {\n let sum = 1 + 2;\n for (const [key, propertyValue] of Object.entries(value)) {\n sum += 4 + new TextEncoder().encode(key).length;\n sum += jsJsonGetSize(propertyValue);\n }\n return sum;\n }\n\n throw new Error(`jsJsonGetSize: Unknown type ${typeof value}`);\n};\n\nexport const jsJsonDecodeItem = (buffer: BufferCursor): JsJsonType => {\n const typeId = buffer.getByte();\n\n if (typeId === JsJsonConst.True) {\n return true;\n }\n\n if (typeId === JsJsonConst.False) {\n return false;\n }\n\n if (typeId === JsJsonConst.Null) {\n return null;\n }\n\n if (typeId === JsJsonConst.Undefined) {\n return undefined;\n }\n\n if (typeId === JsJsonConst.String) {\n return buffer.getString();\n }\n\n if (typeId === JsJsonConst.Number) {\n return buffer.getF64();\n }\n\n if (typeId === JsJsonConst.List) {\n const count = buffer.getU32();\n const list: Array = [];\n\n for (let i = 0; i < count; i++) {\n list.push(jsJsonDecodeItem(buffer));\n }\n\n return list;\n }\n\n if (typeId === JsJsonConst.Object) {\n const count = buffer.getU16();\n const obj: { [key: string]: JsJsonType } = {};\n\n for (let i = 0; i < count; i++) {\n const key = buffer.getString();\n const value = jsJsonDecodeItem(buffer);\n obj[key] = value;\n }\n\n return obj;\n }\n\n if (typeId === JsJsonConst.Vec) {\n return buffer.getBuffer();\n }\n\n throw new Error(`jsJsonDecodeItem: Unknown type id ${typeId}`);\n};\n\nexport const saveJsJsonToBufferItem = (value: JsJsonType, buffer: BufferCursor): void => {\n if (value === true) {\n buffer.setByte(JsJsonConst.True);\n return;\n }\n\n if (value === false) {\n buffer.setByte(JsJsonConst.False);\n return;\n }\n\n if (value === null) {\n buffer.setByte(JsJsonConst.Null);\n return;\n }\n\n if (value === undefined) {\n buffer.setByte(JsJsonConst.Undefined);\n return;\n }\n\n if (typeof value === 'string') {\n buffer.setByte(JsJsonConst.String);\n buffer.setString(value);\n return;\n }\n\n if (typeof value === 'number') {\n buffer.setByte(JsJsonConst.Number);\n buffer.setF64(value);\n return;\n }\n\n if (value instanceof Uint8Array) {\n buffer.setByte(JsJsonConst.Vec);\n buffer.setBuffer(value);\n return;\n }\n\n if (Array.isArray(value)) {\n buffer.setByte(JsJsonConst.List);\n buffer.setU32(value.length);\n\n for (const item of value) {\n saveJsJsonToBufferItem(item, buffer);\n }\n\n return;\n }\n\n if (typeof value === 'object' && value !== null) {\n const entries = Object.entries(value);\n\n buffer.setByte(JsJsonConst.Object);\n buffer.setU16(entries.length);\n\n for (const [key, propertyValue] of entries) {\n buffer.setString(key);\n saveJsJsonToBufferItem(propertyValue, buffer);\n }\n\n return;\n }\n\n throw new Error(`saveJsJsonToBufferItem: Unknown type ${typeof value}`);\n};\n","import { BufferCursor } from './buffer_cursor';\nimport { jsJsonGetSize, jsJsonDecodeItem, saveJsJsonToBufferItem, JsJsonType } from './jsjson';\n\nexport interface BaseExportType {\n vertigo_export_alloc_block: (size: number) => bigint,\n vertigo_export_free_block: (pointer: bigint) => void,\n vertigo_export_wasm_command: (value_ptr: bigint) => bigint,\n};\n\nexport interface ModuleControllerType {\n exports: ExportType,\n getUint8Memory: () => Uint8Array,\n wasmCommand: (params: JsJsonType) => JsJsonType,\n}\n\nconst fetchModule = async (wasmBinPath: string, imports: Record): Promise => {\n if (typeof WebAssembly.instantiateStreaming === 'function') {\n const stream = fetch(wasmBinPath);\n try {\n const module = await WebAssembly.instantiateStreaming(stream, imports);\n return module;\n } catch (err) {\n console.warn(\"`WebAssembly.instantiateStreaming` failed. This could happen if your server does not serve wasm with `application/wasm` MIME type, but check the original error too. Falling back to `WebAssembly.instantiate` which is slower. Original error:\\n\", err);\n }\n }\n\n console.info('fetchModule by WebAssembly.instantiate');\n\n const resp = await fetch(wasmBinPath);\n const binary = await resp.arrayBuffer();\n const module_instance = await WebAssembly.instantiate(binary, imports);\n return module_instance;\n};\n\nexport const wasmInit = async , ExportType extends BaseExportType>(\n wasmBinPath: string,\n imports: { mod: ImportType },\n): Promise> => {\n const module_instance = await fetchModule(wasmBinPath, imports);\n\n let cacheGetUint8Memory: Uint8Array = new Uint8Array(1);\n\n const getUint8Memory = () => {\n if (module_instance.instance.exports.memory instanceof WebAssembly.Memory) {\n if (cacheGetUint8Memory.buffer !== module_instance.instance.exports.memory.buffer) {\n cacheGetUint8Memory = new Uint8Array(module_instance.instance.exports.memory.buffer);\n }\n return cacheGetUint8Memory;\n } else {\n throw Error('Missing memory');\n }\n };\n\n //@ts-expect-error\n const exports: ExportType = module_instance.instance.exports;\n\n const wasmCommand = (value: JsJsonType): JsJsonType => {\n // Serialize JsJson\n const size = jsJsonGetSize(value);\n const long_ptr = exports.vertigo_export_alloc_block(size);\n const buffer = new BufferCursor(getUint8Memory, long_ptr);\n saveJsJsonToBufferItem(value, buffer);\n\n let result_long_ptr = exports.vertigo_export_wasm_command(long_ptr);\n\n // Decode JsJson\n if (result_long_ptr === 0n) {\n return null;\n }\n const resultBuffer = new BufferCursor(getUint8Memory, result_long_ptr);\n const result = jsJsonDecodeItem(resultBuffer);\n exports.vertigo_export_free_block(result_long_ptr);\n\n return result;\n };\n\n\n return {\n exports,\n getUint8Memory,\n wasmCommand: wasmCommand,\n };\n};\n","export class EventEmitter {\n private events: Set<(param: T) => void>;\n\n constructor() {\n this.events = new Set()\n }\n\n on(callback: (param: T) => void) {\n let isActive = true;\n\n const onExec = (param: T) => {\n if (isActive) {\n callback(param);\n }\n };\n\n this.events.add(onExec);\n\n return () => {\n isActive = false;\n this.events.delete(onExec);\n };\n }\n\n trigger(param: T) {\n const eventsCopy = Array.from(this.events.values())\n\n for (const itemCallbackToRun of eventsCopy) {\n try {\n itemCallbackToRun(param);\n } catch (err) {\n console.error(err);\n }\n }\n }\n\n get size(): number {\n return this.events.size;\n }\n}\n","type ResolveFn = (data: T) => void;\ntype RejectFn = (err: unknown) => void;\n\ninterface PromiseResolveReject {\n readonly resolve: (value: T) => void,\n readonly reject: (err: unknown) => void,\n};\n\nconst createPromiseValue = (): [PromiseResolveReject, Promise] => {\n let resolve: ResolveFn | null = null;\n let reject: RejectFn | null = null;\n\n const promise: Promise = new Promise((localResolve: ResolveFn, localReject: RejectFn) => {\n resolve = localResolve;\n reject = localReject;\n });\n\n if (resolve === null) {\n throw Error('createPromiseValue - resolve is null');\n }\n\n if (reject === null) {\n throw Error('createPromiseValue - reject is null');\n }\n\n const promiseValue = {\n resolve,\n reject,\n };\n\n return [promiseValue, promise];\n};\n\nexport class PromiseBoxRace {\n private inner: PromiseResolveReject | null = null;\n readonly promise: Promise;\n\n constructor() {\n const [promiseResolveReject, promise] = createPromiseValue();\n\n this.inner = promiseResolveReject;\n this.promise = promise;\n }\n\n resolve = (value: T) => {\n const promiseResolveReject = this.inner;\n this.inner = null;\n\n if (promiseResolveReject === null) {\n return;\n }\n\n promiseResolveReject.resolve(value);\n }\n\n reject = (err?: unknown) => {\n const promiseResolveReject = this.inner;\n this.inner = null;\n\n if (promiseResolveReject === null) {\n return;\n }\n\n promiseResolveReject.reject(err);\n }\n\n isFulfilled = (): boolean => {\n return this.inner === null;\n }\n}\n","import { EventEmitter } from \"./event_emiter\";\nimport { PromiseBoxRace } from \"./promise\";\n\nconst timeout = async (timeout: number): Promise => {\n return new Promise((resolve: (data: void) => void) => {\n setTimeout(resolve, timeout);\n });\n};\n\n\nconst reconnectDelay = async (label: string, timeout_retry: number): Promise => {\n console.info(`${label} wait ${timeout_retry}ms`);\n await timeout(timeout_retry);\n console.info(`${label} go forth`);\n};\n\nexport type SocketEventType = {\n type: 'message',\n message: string,\n} | {\n type: 'socket',\n socket: SocketConnection\n} | {\n type: 'close',\n};\n\nexport type OnMessageType = (message: SocketEventType) => void;\nexport type UnsubscribeFnType = () => void;\n\ninterface OpenSocketResult {\n socket: Promise,\n done: Promise,\n}\n\nexport interface SocketConnectionController {\n send: (message: string) => void,\n dispose: UnsubscribeFnType\n}\n\nclass LogContext {\n public constructor(private host: string) {}\n public formatLog = (message: string): string => `Socket ${this.host} ==> ${message}`;\n}\nexport class SocketConnection {\n private readonly eventMessage: EventEmitter;\n public readonly close: () => void;\n public readonly send: (message: string) => void;\n\n private constructor(\n close: () => void,\n send: (message: string) => void,\n ) {\n this.eventMessage = new EventEmitter();\n this.close = close;\n this.send = send;\n }\n\n private static connect(\n log: LogContext,\n host: string,\n timeout: number,\n ): OpenSocketResult {\n const result = new PromiseBoxRace();\n const done = new PromiseBoxRace();\n const socket = new WebSocket(host);\n let isClose: boolean = false;\n\n console.info(log.formatLog('starting ...'));\n\n const closeSocket = (): void => {\n if (isClose) {\n return;\n }\n\n console.info(log.formatLog('close'));\n\n isClose = true;\n result.resolve(null);\n done.resolve();\n socket.close();\n };\n\n\n const socketConnection = new SocketConnection(\n closeSocket,\n (message: string) => {\n if (isClose) {\n return;\n }\n socket.send(message);\n }\n );\n\n setTimeout(() => {\n if (result.isFulfilled() === false) {\n console.error(log.formatLog(`timeout (${timeout}ms)`));\n closeSocket();\n }\n }, timeout);\n\n const onOpen = (): void => {\n console.info(log.formatLog('open'));\n result.resolve(socketConnection);\n };\n\n const onError = (error: Event): void => {\n console.error(log.formatLog('error'), error);\n closeSocket();\n };\n\n const onMessage = (event: MessageEvent): void => {\n if (isClose) {\n return;\n }\n\n const dataRaw = event.data;\n\n if (typeof dataRaw === 'string') {\n socketConnection.eventMessage.trigger(dataRaw);\n return;\n }\n\n console.error(log.formatLog('onMessage - expected string'), dataRaw);\n };\n\n socket.addEventListener('open', onOpen);\n socket.addEventListener('error', onError);\n socket.addEventListener('close', closeSocket);\n socket.addEventListener('message', onMessage);\n\n return {\n socket: result.promise,\n done: done.promise\n };\n }\n\n public static startSocket(\n host: string,\n timeout_connection: number,\n timeout_retry: number,\n onMessage: OnMessageType,\n ): SocketConnectionController {\n let isConnect: boolean = true;\n let socketConnection: SocketConnection | null = null;\n\n const log = new LogContext(host);\n\n (async (): Promise => {\n while (isConnect) {\n const openSocketResult = SocketConnection.connect(log, host, timeout_connection);\n\n const socket = await openSocketResult.socket;\n\n if (socket === null) {\n await reconnectDelay(log.formatLog('reconnect after error'), timeout_retry);\n continue;\n }\n\n socketConnection = socket;\n onMessage({\n type: 'socket',\n socket\n });\n\n socket.eventMessage.on(message => {\n onMessage({\n type: 'message',\n message\n });\n });\n\n await openSocketResult.done;\n\n onMessage({\n type: 'close'\n });\n\n if (!isConnect) {\n console.info(log.formatLog('disconnect (1)'));\n return;\n }\n\n await reconnectDelay(log.formatLog('reconnect after close'), timeout_retry);\n }\n\n console.info(log.formatLog('disconnect (2)'));\n })().catch((error) => {\n console.error(error);\n });\n\n return {\n send: (message: string): void => {\n if (socketConnection === null) {\n console.error('send fail - missing connection', message);\n } else {\n socketConnection.send(message);\n }\n },\n dispose: (): void => {\n isConnect = false;\n socketConnection?.close();\n }\n };\n }\n}\n","import { JsJsonType } from \"../../jsjson\";\nimport { ModuleControllerType } from \"../../wasm_init\";\nimport { ExportType } from \"../../wasm_module\";\nimport { CallbackId } from \"../types\";\nimport { SocketConnection, SocketConnectionController } from \"./connection\";\n\nconst wireStringToJsJson = (raw: string): JsJsonType => {\n try {\n return JSON.parse(raw) as JsJsonType;\n } catch {\n console.error('Failed to parse websocket message', raw);\n throw Error(raw);\n }\n};\n\nconst jsJsonToWebSocketWire = (value: JsJsonType): string => {\n return JSON.stringify(value);\n};\n\nconst assertNeverMessage = (data: never): never => {\n console.error(data);\n throw Error('unknown message');\n};\n\ntype CommandType = 'Connected' | 'Disconnected' | {\n 'Message': {\n message: JsJsonType,\n }\n}\nconst wasmCallback = (wasm: ModuleControllerType, callbackId: CallbackId, command: CommandType) => {\n wasm.wasmCommand({\n 'Websocket': {\n callback: callbackId,\n message: command,\n }\n })\n};\n\n\nexport class DriverWebsocket {\n private getWasm: () => ModuleControllerType;\n private readonly controllerList: Map;\n private readonly socket: Map;\n\n constructor(getWasm: () => ModuleControllerType) {\n this.getWasm = getWasm;\n this.controllerList = new Map();\n this.socket = new Map();\n }\n\n public websocket_register_callback = (\n host: string,\n callback_id: CallbackId,\n ) => {\n const wasm = this.getWasm();\n\n let controller = SocketConnection.startSocket(\n host,\n 5000, //timeout connection\n 3000, //timeout reconnection\n (message) => {\n\n if (this.controllerList.has(callback_id) === false) {\n return;\n }\n\n if (message.type === 'socket') {\n this.socket.set(callback_id, message.socket);\n wasmCallback(wasm, callback_id, 'Connected');\n return;\n }\n\n if (message.type === 'message') {\n wasmCallback(wasm, callback_id, {\n 'Message': {\n message: wireStringToJsJson(message.message)\n }\n });\n return;\n }\n\n if (message.type === 'close') {\n this.socket.delete(callback_id);\n wasmCallback(wasm, callback_id, 'Disconnected');\n return;\n }\n\n return assertNeverMessage(message);\n }\n );\n\n this.controllerList.set(callback_id, controller);\n }\n\n public websocket_unregister_callback = (callback_id: CallbackId) => {\n const controller = this.controllerList.get(callback_id);\n\n if (controller === undefined) {\n console.error('Expected controller');\n return;\n }\n\n controller.dispose();\n this.controllerList.delete(callback_id);\n }\n\n public websocket_send_message = (\n callback_id: CallbackId,\n message: JsJsonType,\n ) => {\n const socket = this.socket.get(callback_id);\n\n if (socket === undefined) {\n console.error(`Missing socket connection for callback_id=${callback_id}`);\n } else {\n socket.send(jsJsonToWebSocketWire(message));\n }\n }\n}\n","\nexport const assertNever = (_value: never) => {\n throw Error(\"assert never\");\n}\n","import { JsJsonType } from \"../../jsjson\";\nimport { ModuleControllerType } from \"../../wasm_init\";\nimport { ExportType } from \"../../wasm_module\";\nimport { CallbackId } from \"../types\";\n\nexport interface FetchRequestType {\n method: string,\n url: string,\n headers: Array<{ k: string, v: string }>,\n body: 'None' | {\n Data: {\n data: JsJsonType\n }\n }\n}\n\ntype FetchResponseType = {\n Ok: {\n status: number,\n response: {\n Text: string\n } | {\n Json: JsJsonType,\n }\n }\n} | {\n Err: {\n message: string,\n }\n};\n\nconst getHeaders = (headers: Array<{ k: string, v: string }>): Record => {\n const result: Record = {};\n\n for (const { k, v } of headers) {\n result[k] = v;\n }\n\n return result;\n};\n\nconst getBodyString = (body: FetchRequestType['body']): string | undefined => {\n if (body === 'None') {\n return undefined;\n }\n\n return JSON.stringify(body.Data.data);\n};\n\n// 204/205 carry no body, and any other response with an empty body\n// would crash response.json(). Treat both as Json: null so the caller\n// sees a successful response with the real status code.\nexport const parseJsonBody = (bodyText: string): JsJsonType | null =>\n bodyText.length === 0 ? null : JSON.parse(bodyText);\n\nconst processResponse = async (response: Response): Promise => {\n const status = response.status;\n const contentType = response.headers.get(\"Content-Type\");\n\n try {\n if (contentType?.startsWith('text/plain;')) {\n return {\n Ok: {\n status,\n response: {\n Text: await response.text(),\n }\n }\n }\n }\n\n const json = parseJsonBody(await response.text());\n\n return {\n Ok: {\n status,\n response: {\n Json: json\n }\n }\n };\n } catch (error) {\n return {\n Err: {\n message: String(error),\n }\n };\n }\n};\n\n\nexport const fetchExec = async (\n getWasm: () => ModuleControllerType,\n callback_id: CallbackId,\n request: FetchRequestType\n): Promise => {\n const wasm = getWasm();\n\n try {\n const response = await fetch(request.url, {\n method: request.method,\n headers: getHeaders(request.headers),\n body: getBodyString(request.body),\n });\n\n const response2 = await processResponse(response);\n\n wasm.wasmCommand({\n 'FetchExecResponse': {\n response: response2,\n callback: callback_id,\n }\n });\n\n } catch (err) {\n console.error('fetch error (1)', err);\n const responseMessage = new String(err).toString();\n\n const responseToWasm: FetchResponseType = {\n 'Err': {\n message: responseMessage\n }\n };\n\n wasm.wasmCommand({\n 'FetchExecResponse': {\n response: responseToWasm,\n callback: callback_id,\n }\n });\n }\n};\n\n\n","import { CallbackId } from \"../types\";\nimport { ModuleControllerType } from \"../../wasm_init\";\nimport { ExportType } from \"../../wasm_module\";\n\ntype TimerResourceId = ReturnType;\n\ninterface TimerId {\n kind: 'Interval' | 'Timeout',\n timerId: TimerResourceId,\n}\n\nexport class Interval {\n private readonly getWasm: () => ModuleControllerType;\n private readonly data: Map;\n\n constructor(getWasm: () => ModuleControllerType) {\n this.getWasm = getWasm;\n this.data = new Map();\n }\n\n timerSet = (callback: CallbackId, duration: number, kind: 'Interval' | 'Timeout') => {\n switch (kind) {\n case 'Interval': {\n const timerId = setInterval(() => {\n this.getWasm().wasmCommand({\n 'TimerCall': {\n callback,\n },\n })\n }, duration);\n\n this.data.set(callback, {\n kind: 'Interval',\n timerId,\n });\n break;\n }\n case 'Timeout': {\n const timerId = setTimeout(() => {\n this.getWasm().wasmCommand({\n 'TimerCall': {\n callback,\n },\n })\n }, duration);\n\n this.data.set(callback, {\n kind: 'Timeout',\n timerId,\n });\n break;\n }\n }\n }\n\n timerClear = (callback: CallbackId) => {\n const timerResource = this.data.get(callback);\n\n if (timerResource === undefined) {\n throw Error('panic');\n }\n\n switch (timerResource.kind) {\n case 'Interval': {\n clearInterval(timerResource.timerId);\n break;\n }\n case 'Timeout': {\n clearTimeout(timerResource.timerId);\n break;\n }\n }\n }\n}\n","import { ModuleControllerType } from \"../../wasm_init\";\nimport { ExportType } from \"../../wasm_module\";\nimport { CallbackId } from \"../types\";\nimport { LocationCommonType } from \"./types\";\n\nexport class HashRouter implements LocationCommonType {\n private getWasm: () => ModuleControllerType;\n private callback: Map void>;\n\n constructor(getWasm: () => ModuleControllerType) {\n this.getWasm = getWasm;\n this.callback = new Map();\n\n window.addEventListener(\"hashchange\", this.trigger);\n }\n\n private trigger = () => {\n for (const callback of Array.from(this.callback.values())) {\n callback();\n }\n }\n\n public add = (callback_id: CallbackId) => {\n this.callback.set(callback_id, () => {\n this.getWasm().wasmCommand({\n LocationCall: {\n callback: callback_id,\n value: this.get(),\n }\n });\n });\n }\n\n public remove = (callback_id: CallbackId) => {\n this.callback.delete(callback_id);\n }\n\n public push = (new_hash: string) => {\n if (this.get() === new_hash) {\n return;\n }\n\n location.hash = new_hash;\n this.trigger();\n }\n\n public replace = (new_hash: string) => {\n if (this.get() === new_hash) {\n return;\n }\n\n history.replaceState(null, '', `#${new_hash}`);\n }\n\n public get(): string {\n return decodeURIComponent(location.hash.substr(1));\n }\n}\n","import { ModuleControllerType } from \"../../wasm_init\";\nimport { ExportType } from \"../../wasm_module\";\nimport { CallbackId } from \"../types\";\nimport { LocationCommonType } from \"./types\";\n\nexport class HistoryLocation implements LocationCommonType {\n private getWasm: () => ModuleControllerType;\n private callback: Map void>;\n\n constructor(getWasm: () => ModuleControllerType) {\n this.getWasm = getWasm;\n this.callback = new Map();\n\n window.addEventListener(\"popstate\", this.trigger);\n }\n\n private trigger = () => {\n for (const callback of Array.from(this.callback.values())) {\n callback();\n }\n }\n\n public add = (callback_id: CallbackId) => {\n this.callback.set(callback_id, () => {\n this.getWasm().wasmCommand({\n LocationCall: {\n callback: callback_id,\n value: this.get(),\n }\n });\n });\n }\n\n public remove = (callback_id: CallbackId) => {\n this.callback.delete(callback_id);\n }\n\n public push = (url: string) => {\n if (this.get() === url) {\n return;\n }\n\n window.history.pushState(null, '', url);\n this.trigger();\n }\n\n public replace = (url: string) => {\n if (this.get() === url) {\n return;\n }\n\n window.history.replaceState(null, '', url);\n this.trigger();\n }\n\n public get(): string {\n return window.location.pathname + window.location.search + window.location.hash;\n }\n}\n","import { ModuleControllerType } from \"../../wasm_init\";\nimport { ExportType } from \"../../wasm_module\";\nimport { CallbackId } from \"../types\";\nimport { HashRouter } from \"./hashrouter\";\nimport { HistoryLocation } from \"./historyLocation\";\nimport { LocationCommonType } from \"./types\";\n\ntype LocationTarget = 'Hash' | 'History';\n\nexport class AppLocation {\n private readonly locations: Record;\n\n constructor(getWasm: () => ModuleControllerType) {\n this.locations = {\n Hash: new HashRouter(getWasm),\n History: new HistoryLocation(getWasm),\n };\n }\n\n callback = (target: LocationTarget, mode: 'Add' | 'Remove', callbackId: CallbackId) => {\n switch (mode) {\n case 'Add': {\n this.locations[target].add(callbackId);\n return;\n }\n case 'Remove': {\n this.locations[target].remove(callbackId);\n return;\n }\n }\n }\n\n set = (target: LocationTarget, mode: 'Push' | 'Replace', newValue: string) => {\n switch (mode) {\n case 'Push': {\n this.locations[target].push(newValue);\n return;\n }\n case 'Replace': {\n this.locations[target].replace(newValue);\n return;\n }\n }\n }\n\n get = (target: LocationTarget): string => {\n return this.locations[target].get();\n }\n}","import { JsJsonType } from \"../../jsjson\";\n\nexport class Cookies {\n public get = (cname: string): string => {\n for (const cookie of document.cookie.split(';')) {\n if (cookie === \"\") continue;\n\n const cookieChunk = cookie.trim().split('=');\n\n if (cookieChunk.length !== 2) {\n console.warn(`Cookies.get: Incorrect number of cookieChunk => ${cookieChunk.length} in ${cookie}`);\n continue;\n }\n\n const cookieName = cookieChunk[0];\n const cookieValue = cookieChunk[1];\n\n if (cookieName === undefined || cookieValue === undefined) {\n console.warn(`Cookies.get: Broken cookie part => ${cookie}`);\n continue;\n }\n\n if (cookieName === cname) {\n return decodeURIComponent(cookieValue);\n }\n }\n\n return '';\n }\n\n public getJson = (cname: string): JsJsonType => {\n let cvalue_str = this.get(cname);\n\n if (cvalue_str.length !== 0) {\n try {\n let cookie_value = JSON.parse(cvalue_str);\n return cookie_value;\n } catch (e) {\n console.error!(\"Error deserializing cookie\", e);\n }\n }\n return null\n }\n\n public set = (\n cname: string,\n cvalue: string,\n expires_in: number,\n ) => {\n const cvalueEncoded = cvalue == null ? \"\" : encodeURIComponent(cvalue);\n\n const d = new Date();\n d.setTime(d.getTime() + (expires_in * 1000));\n let expires = \"expires=\" + d.toUTCString();\n\n document.cookie = `${cname}=${cvalueEncoded};${expires};path=/; samesite=Strict`;\n }\n\n public setJson = (\n cname: string,\n cvalue: JsJsonType,\n expires_in: number,\n ) => {\n let cvalue_str = JSON.stringify(cvalue);\n\n this.set(cname, cvalue_str, expires_in);\n }\n}\n","export const getRandom = (min: number, max: number): number => {\n const range = max - min + 1;\n let result = Math.floor(Math.random() * range);\n return min + result;\n};\n\n","import { ExportType } from \"../../../wasm_module\";\nimport { getFiles } from \"./dataTransfer\";\nimport { JsJsonType } from \"../../../jsjson\";\nimport { ModuleControllerType } from \"../../../wasm_init\";\nimport { MapNodes } from \"./map_nodes\";\nimport { CallbackId } from \"../../types\";\n\nexport class CallbackManager {\n private readonly getWasm: () => ModuleControllerType;\n private callbacks: Map void>;\n // IntersectionObserver does not use addEventListener, so its observers are\n // tracked separately (keyed by callback_id) for disconnect on remove.\n private observers: Map;\n\n public constructor(getWasm: () => ModuleControllerType) {\n this.getWasm = getWasm;\n this.callbacks = new Map();\n this.observers = new Map();\n }\n\n public add(nodes: MapNodes, id: number, event_name: string, callback_id: CallbackId) {\n if (event_name === 'intersect') {\n return this.intersectAdd(nodes, id, callback_id);\n }\n\n const callback = (event: Event) => {\n if (event_name === 'click') {\n return this.click(event, callback_id);\n }\n\n if (event_name === 'submit') {\n return this.submit(event, callback_id);\n }\n\n if (event_name === 'input') {\n return this.input(event, callback_id);\n }\n\n if (event_name === 'change') {\n return this.change(event, callback_id);\n }\n\n if (event_name === 'blur') {\n return this.blur(event, callback_id);\n }\n\n if (event_name === 'mousedown') {\n return this.mousedown(event, callback_id);\n }\n\n if (event_name === 'mouseup') {\n return this.mouseup(event, callback_id);\n }\n\n if (event_name === 'mouseenter') {\n return this.mouseenter(event, callback_id);\n }\n\n if (event_name === 'mouseleave') {\n return this.mouseleave(event, callback_id);\n }\n\n if (event_name === 'keydown') {\n return this.keydown(event, callback_id);\n }\n\n if (event_name === 'hook_keydown') {\n return this.keydown(event, callback_id);\n }\n\n if (event_name === 'drop') {\n return this.drop(event, callback_id);\n }\n\n if (event_name === 'load') {\n return this.load(event, callback_id);\n }\n\n if (event_name === 'change_file') {\n return this.changeFile(event, callback_id);\n }\n\n console.error(`No support for the event ${event_name}`);\n };\n\n if (this.callbacks.has(callback_id)) {\n console.error(`There was already a callback added with the callback_id=${callback_id}`);\n return;\n }\n\n this.callbacks.set(callback_id, callback);\n\n if (event_name === 'hook_keydown') {\n document.addEventListener('keydown', callback, false);\n } else {\n const node = nodes.get('callback_add', id);\n const domEventName = event_name === 'change_file' ? 'change' : event_name;\n node.addEventListener(domEventName, callback, false);\n }\n }\n\n public remove(nodes: MapNodes, id: number, event_name: string, callback_id: CallbackId) {\n if (event_name === 'intersect') {\n return this.intersectRemove(callback_id);\n }\n\n const callback = this.callbacks.get(callback_id);\n this.callbacks.delete(callback_id);\n\n if (callback === undefined) {\n console.error(`The callback is missing with the id=${callback_id}`);\n return;\n }\n\n if (event_name === 'hook_keydown') {\n document.removeEventListener('keydown', callback);\n } else {\n const node = nodes.get('callback_remove', id);\n const domEventName = event_name === 'change_file' ? 'change' : event_name;\n node.removeEventListener(domEventName, callback);\n }\n }\n\n private wasmCallback(callback_id: CallbackId, value: JsJsonType): JsJsonType {\n return this.getWasm().wasmCommand({\n CallbackCall: {\n callback_id,\n value: value\n }\n });\n }\n\n private intersectAdd(nodes: MapNodes, id: number, callback_id: CallbackId) {\n if (this.observers.has(callback_id)) {\n console.error(`There was already an intersect observer added with the callback_id=${callback_id}`);\n return;\n }\n\n const node = nodes.getNode('callback_add', id);\n\n const observer = new IntersectionObserver((entries) => {\n for (const entry of entries) {\n // Payload order MUST match the Rust decoder get_intersection_event.\n this.wasmCallback(callback_id, [\n entry.isIntersecting,\n entry.intersectionRatio,\n entry.boundingClientRect.top,\n entry.boundingClientRect.bottom,\n entry.boundingClientRect.height,\n ]);\n }\n });\n\n observer.observe(node);\n this.observers.set(callback_id, observer);\n }\n\n private intersectRemove(callback_id: CallbackId) {\n const observer = this.observers.get(callback_id);\n this.observers.delete(callback_id);\n\n if (observer === undefined) {\n console.error(`The intersect observer is missing with the id=${callback_id}`);\n return;\n }\n\n observer.disconnect();\n }\n\n private click(event: Event, callback_id: CallbackId) {\n event.preventDefault();\n let click_event = this.wasmCallback(callback_id, undefined);\n\n // Check if click_event is an object (JsJson Object type)\n if (click_event !== null && typeof click_event === 'object' && !Array.isArray(click_event)) {\n if ('stop_propagation' in click_event && click_event['stop_propagation'] === true) {\n event.stopPropagation();\n }\n if ('prevent_default' in click_event && click_event['prevent_default'] === true) {\n event.preventDefault();\n }\n }\n }\n\n private submit(event: Event, callback_id: CallbackId) {\n event.preventDefault();\n this.wasmCallback(callback_id, undefined);\n }\n\n private input(event: Event, callback_id: CallbackId) {\n const target = event.target;\n\n if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement) {\n this.wasmCallback(callback_id, target.value);\n return;\n }\n\n console.warn('event input ignore', target);\n }\n\n private change(event: Event, callback_id: CallbackId) {\n const target = event.target;\n\n if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement || target instanceof HTMLSelectElement) {\n this.wasmCallback(callback_id, target.value);\n return;\n }\n\n console.warn('event input ignore', target);\n }\n\n private changeFile(event: Event, callback_id: CallbackId) {\n const target = event.target;\n\n if (target instanceof HTMLInputElement && target.files !== null && target.files.length > 0) {\n const promises: Array> = [];\n\n for (let i = 0; i < target.files.length; i++) {\n const file = target.files[i];\n if (file !== undefined) {\n promises.push(\n file.arrayBuffer().then((buf) => ({\n name: file.name,\n data: new Uint8Array(buf),\n }))\n );\n }\n }\n\n if (promises.length > 0) {\n Promise.all(promises).then((files) => {\n const params = [];\n for (const f of files) {\n params.push([f.name, Array.from(f.data)]);\n }\n this.wasmCallback(callback_id, [params]);\n }).catch((err) => console.error('changeFile ->', err));\n }\n\n target.value = '';\n return;\n }\n\n console.warn('changeFile: not a file input or no files', target);\n }\n\n private blur(_event: Event, callback_id: CallbackId) {\n this.wasmCallback(callback_id, undefined);\n }\n\n private mousedown(event: Event, callback_id: CallbackId) {\n if (this.wasmCallback(callback_id, undefined)) {\n event.preventDefault()\n }\n }\n\n private mouseup(event: Event, callback_id: CallbackId) {\n if (this.wasmCallback(callback_id, undefined)) {\n event.preventDefault()\n }\n }\n\n private mouseenter(_event: Event, callback_id: CallbackId) {\n this.wasmCallback(callback_id, undefined);\n }\n\n private mouseleave(_event: Event, callback_id: CallbackId) {\n this.wasmCallback(callback_id, undefined);\n }\n\n private drop(event: Event, callback_id: CallbackId) {\n event.preventDefault();\n\n if (event instanceof DragEvent) {\n if (event.dataTransfer === null) {\n console.error('dom -> drop -> dataTransfer null');\n } else {\n const files = getFiles(event.dataTransfer.items);\n\n if (files.length) {\n Promise.all(files).then((files) => {\n const params = [];\n\n for (const file of files) {\n // Convert Uint8Array to array of numbers for JsJson\n const dataArray = Array.from(file.data);\n params.push([\n file.name,\n dataArray,\n ]);\n }\n\n this.wasmCallback(callback_id, [params]);\n }).catch((error) => {\n console.error('callback_drop -> promise.all -> ', error);\n });\n } else {\n console.error('No files to send');\n }\n }\n } else {\n console.warn('event drop ignore', event);\n }\n }\n\n private keydown(event: Event, callback_id: CallbackId) {\n if (event instanceof KeyboardEvent) {\n const result = this.wasmCallback(callback_id, [\n event.key,\n event.code,\n event.altKey,\n event.ctrlKey,\n event.shiftKey,\n event.metaKey\n ]);\n\n if (result === true) {\n event.preventDefault();\n event.stopPropagation();\n }\n\n return;\n }\n\n console.warn('keydown ignore', event);\n }\n\n private load(event: Event, callback_id: CallbackId) {\n event.preventDefault();\n this.wasmCallback(callback_id, undefined);\n }\n\n}\n","interface FileItemType {\n name: string,\n data: Uint8Array,\n}\n\nexport function getFiles(items: DataTransferItemList): Array> {\n const files: Array> = [];\n\n for (let i = 0; i < items.length; i++) {\n const item = items[i];\n\n if (item === undefined) {\n console.error('dom -> drop -> item - undefined');\n } else {\n const file = item.getAsFile();\n\n if (file === null) {\n console.error(`dom -> drop -> index:${i} -> It's not a file`);\n } else {\n files.push(file\n .arrayBuffer()\n .then((data): FileItemType => ({\n name: file.name,\n data: new Uint8Array(data),\n }))\n );\n }\n }\n }\n return files;\n}\n","import { AppLocation } from \"../../location/AppLocation\";\n\nexport function injects(node: Element, appLocation: AppLocation) {\n if (node.tagName.toLocaleLowerCase() === 'a') {\n hydrateLink(node, appLocation);\n }\n}\n\nfunction hydrateLink(node: Element, appLocation: AppLocation) {\n node.addEventListener('click', (e) => {\n let href = node.getAttribute('href');\n if (href === null) {\n return;\n }\n\n if (href.startsWith('#') || href.startsWith('http://') || href.startsWith('https://') || href.startsWith('//')) {\n return;\n }\n\n e.preventDefault();\n appLocation.set('History', 'Push', href);\n window.scrollTo(0, 0);\n })\n}\n","import { AppLocation } from \"../../location/AppLocation\";\nimport { CommandType } from \"./dom\";\nimport { injects } from \"./injects\";\nimport { MapNodes } from \"./map_nodes\";\n\ninterface VirtualNode {\n id: number;\n name?: string;\n value?: string;\n attributes?: Map;\n children: Array;\n}\n\nexport const hydrate = (commands: Array, nodes: MapNodes, appLocation: AppLocation) => {\n const engine = new HydrationEngine(commands, nodes, appLocation);\n engine.hydrate();\n};\n\nclass HydrationEngine {\n private nodes: MapNodes;\n private appLocation: AppLocation;\n private virtualNodes: Map;\n private depth: number = -1;\n private matched: number = 0;\n\n constructor(commands: Array, nodes: MapNodes, appLocation: AppLocation) {\n this.nodes = nodes;\n this.appLocation = appLocation;\n this.virtualNodes = this.createVirtualNodes(commands);\n }\n\n public hydrate() {\n // Start hydration from Body (id=3) and Head (id=2) if needed\n // Usually we care about Body.\n const bodyVNode = this.virtualNodes.get(3);\n if (bodyVNode) {\n this.hydrateNode(3, document.body);\n }\n\n const headVNode = this.virtualNodes.get(2);\n if (headVNode) {\n this.hydrateNode(2, document.head);\n }\n\n console.log(\n \"Hydration complete,\",\n (this.matched * 100 / this.virtualNodes.size).toFixed(2),\n \" % vnodes matched.\",\n );\n };\n\n // Traverse and Match\n private hydrateNode(vNodeId: number, realNode: Node) {\n const vNode = this.virtualNodes.get(vNodeId);\n if (!vNode) return;\n\n // console.log(`Hydration ${this.depth + 1}: Hydrate node`, vNode, realNode);\n\n // Match children\n const realChildren = Array.from(realNode.childNodes);\n let realIndex = 0;\n this.depth++;\n let skipTextVNodes = false;\n\n for (const childVId of vNode.children) {\n const childVNode = this.virtualNodes.get(childVId);\n if (!childVNode) continue;\n\n // If we are in group of text vnodes, skip them until we find a non-text vnode.\n if (skipTextVNodes && childVNode.value !== undefined) {\n // Deliberately skipped vNodes should be counted as matched\n this.matched++;\n continue;\n } else {\n skipTextVNodes = false;\n }\n\n // Find a matching real node starting from realIndex\n for (let i = realIndex; i < realChildren.length; i++) {\n const candidate = realChildren[i];\n if (!candidate) continue;\n\n let isMatch = false;\n if (childVNode.name) {\n // Element\n isMatch = this.checkElementMatch(candidate, childVNode);\n } else if (childVNode.value !== undefined) {\n // Text\n if (candidate.nodeType === Node.TEXT_NODE) {\n this.checkTextMatch(candidate, childVNode);\n isMatch = true;\n // Start skipping eventual group of text vnodes\n // as they were probably merged into one on SSR side.\n skipTextVNodes = true;\n } else {\n console.error(`Hydration ${this.depth}: Text node mismatch`, childVNode, candidate);\n }\n }\n\n if (isMatch) {\n this.removeSkippedNodes(realChildren, realIndex, i);\n this.claimNode(candidate, childVId);\n this.matched++;\n\n // Recurse if element\n if (childVNode.name) {\n this.hydrateNode(childVId, candidate);\n }\n\n // Advance realIndex to i + 1 (consume this node)\n realIndex = i + 1;\n break;\n }\n }\n }\n\n // Remove remaining real nodes\n this.removeSkippedNodes(realChildren, realIndex, realChildren.length);\n this.depth--;\n };\n\n private checkElementMatch(candidate: Node, childVNode: VirtualNode) {\n let isMatch = false;\n if (candidate.nodeType === Node.ELEMENT_NODE && (candidate as Element).tagName === childVNode.name) {\n isMatch = true;\n // Check attributes\n if (childVNode.attributes) {\n const element = candidate as Element;\n for (const [name, value] of childVNode.attributes) {\n if (element.getAttribute(name) !== value) {\n // console.info(`Hydration ${depth}: Reseting attribute`, element.getAttribute(name), \" !== \", value);\n element.setAttribute(name, value);\n }\n }\n }\n }\n return isMatch;\n };\n\n private checkTextMatch(candidate: Node, childVNode: VirtualNode) {\n // For text nodes, we might want to be lenient or exact.\n // Let's assume exact match or at least non-empty.\n // Often text nodes might have whitespace differences.\n // For now, let's just check if it's a text node.\n // Checking content might be safer.\n if (candidate.textContent?.replace('\\n', ' ').trim() !== childVNode.value?.replace('\\n', ' ').trim()) {\n // console.debug(`Hydration ${depth}: Joint text`, childVNode, candidate);\n candidate.textContent = childVNode.value || \"\";\n }\n };\n\n // Claim node and run injects\n private claimNode(candidate: Node, childVId: number) {\n if (candidate instanceof Element || candidate instanceof Comment || candidate instanceof Text) {\n this.nodes.claimNode(childVId, candidate);\n\n // Run injects\n if (candidate instanceof Element) {\n injects(candidate, this.appLocation);\n }\n }\n }\n\n // Remove nodes skipped during matching\n private removeSkippedNodes(realChildren: ChildNode[], realIndex: number, i: number) {\n for (let j = realIndex; j < i; j++) {\n const nodeToRemove = realChildren[j];\n if (nodeToRemove) {\n if (this.depth !== 0 && nodeToRemove.nodeType !== Node.TEXT_NODE) {\n console.warn(`Hydration ${this.depth}: Removing node`, nodeToRemove);\n }\n nodeToRemove.remove();\n }\n }\n }\n\n private createVirtualNodes(commands: Array): Map {\n const virtualNodes = new Map();\n\n // Helper to get or create a virtual node\n const getVNode = (id: number): VirtualNode => {\n let node = virtualNodes.get(id);\n if (!node) {\n node = { id, children: [] };\n virtualNodes.set(id, node);\n }\n return node;\n };\n\n // Build Virtual Tree from Commands\n for (const command of commands) {\n if ('CreateNode' in command) {\n const node = getVNode(command.CreateNode.id);\n node.name = command.CreateNode.name.toUpperCase();\n } else if ('CreateText' in command) {\n const node = getVNode(command.CreateText.id);\n node.value = command.CreateText.value;\n } else if ('InsertBefore' in command) {\n const parent = getVNode(command.InsertBefore.parent);\n const childId = command.InsertBefore.child;\n const refId = command.InsertBefore.ref_id;\n\n if (refId === null || refId === undefined) {\n parent.children.push(childId);\n } else {\n const index = parent.children.indexOf(refId);\n if (index !== -1) {\n parent.children.splice(index, 0, childId);\n } else {\n console.warn(`Hydration: ref_id ${refId} not found in parent ${command.InsertBefore.parent}`);\n parent.children.push(childId);\n }\n }\n } else if ('SetAttr' in command) {\n const node = getVNode(command.SetAttr.id);\n if (!node.attributes) {\n node.attributes = new Map();\n }\n node.attributes.set(command.SetAttr.name, command.SetAttr.value);\n }\n }\n\n return virtualNodes;\n };\n}\n","type NodeType = Element | Comment | Text;\nexport class MapNodes {\n private data: Map;\n private initNodes: Array | null;\n private style: HTMLStyleElement;\n\n constructor() {\n this.data = new Map();\n\n this.initNodes = [\n ...this.getRootHead().childNodes,\n ...this.getRootBody().childNodes,\n ];\n\n this.style = document.createElement('style');\n }\n\n private getRootHtml(): Element {\n return document.documentElement;\n }\n\n private getRootHead(): Element {\n return document.head;\n }\n\n private getRootBody(): Element {\n return document.body;\n }\n\n public set(id: number, value: NodeType) {\n if (id === 1 || id === 2 || id === 3) {\n //ignore\n } else {\n this.data.set(id, value);\n }\n }\n\n public getAnyOption(id: number): NodeType | undefined {\n if (id === 1) {\n return this.getRootHtml();\n }\n\n if (id === 2) {\n return this.getRootHead();\n }\n\n if (id === 3) {\n return this.getRootBody();\n }\n\n return this.data.get(id);\n }\n\n public getAny(label: string, id: number): NodeType {\n const item = this.getAnyOption(id);\n\n if (item === undefined) {\n throw Error(`${label} -> item not found=${id}`);\n }\n\n return item;\n }\n\n public get(label: string, id: number): NodeType {\n const item = this.getAnyOption(id);\n\n if (item === undefined) {\n throw new Error(`${label}->get: Item id not found = ${id}`);\n }\n return item;\n }\n\n public getNodeElement(label: string, id: number): HTMLElement {\n const node = this.get(label, id);\n if (node instanceof HTMLElement) {\n return node;\n } else {\n throw Error(`Expected id=${id} as HTMLElement`);\n }\n }\n\n public getNode(label: string, id: number): Element {\n const node = this.get(label, id);\n if (node instanceof Element) {\n return node;\n } else {\n throw Error(`Expected id=${id} as Element`);\n }\n }\n\n public getText(label: string, id: number): Text {\n const node = this.get(label, id);\n if (node instanceof Text) {\n return node;\n } else {\n throw Error(`Expected id=${id} as Text`);\n }\n }\n\n public getComment(label: string, id: number): Comment {\n const node = this.get(label, id);\n if (node instanceof Comment) {\n return node;\n } else {\n throw Error(`Expected id=${id} as Comment`);\n }\n }\n\n public delete(label: string, id: number): NodeType {\n const item = this.getAnyOption(id);\n this.data.delete(id);\n\n if (item === undefined) {\n throw new Error(`${label}->delete: Item id not found = ${id}`);\n }\n\n return item;\n }\n\n public insertCss(selector: string | null, value: string) {\n if (selector !== null) {\n // Add autocss styles\n const content = document.createTextNode(`\\n${selector} { ${value} }`);\n this.style.appendChild(content);\n } else {\n // Add bundle (i.e. a tailwind bundle)\n const content = document.createTextNode(`\\n${value}`);\n this.style.appendChild(content);\n }\n }\n\n public removeInitNodes() {\n const initNodes = this.initNodes;\n this.initNodes = null;\n\n if (initNodes === null) {\n return;\n }\n\n for (const node of initNodes) {\n node.remove();\n }\n }\n\n public insertBefore(parent: number, child: number, ref_id: number | null | undefined) {\n const parentNode = this.get(\"insert_before\", parent);\n const childNode = this.getAny(\"insert_before child\", child);\n\n if (ref_id === null || ref_id === undefined) {\n parentNode.insertBefore(childNode, null);\n } else {\n const ref_node = this.getAny('insert_before ref', ref_id);\n parentNode.insertBefore(childNode, ref_node);\n }\n }\n\n public addStyles() {\n this.getRootHead().appendChild(this.style);\n }\n\n public hasInitNodes(): boolean {\n return this.initNodes !== null;\n }\n\n public claimNode(id: number, node: NodeType) {\n this.data.set(id, node);\n\n if (this.initNodes) {\n const index = this.initNodes.indexOf(node as ChildNode);\n if (index > -1) {\n this.initNodes.splice(index, 1);\n }\n }\n }\n\n public has(id: number): boolean {\n // Root nodes always exist in real DOM\n if (id === 1 || id === 2 || id === 3) {\n return true;\n }\n\n return this.data.has(id);\n }\n}\n","import { AppLocation } from \"../../location/AppLocation\";\nimport { CallbackManager } from \"./callbackManager\";\nimport { ExportType } from \"../../../wasm_module\";\nimport { hydrate } from \"./hydration\";\nimport { injects } from \"./injects\";\nimport { MapNodes } from \"./map_nodes\";\nimport { ModuleControllerType } from \"../../../wasm_init\";\nimport { Metadata } from \"../../metadata\";\n\n// Workaround, remove when https://github.com/vertigo-web/vertigo/issues/539 is done.\nconst SVG_TAGS = new Set([\n \"animate\", \"animateMotion\", \"animateTransform\", \"circle\", \"clipPath\", \"defs\",\n \"desc\", \"discard\", \"ellipse\", \"feBlend\", \"feColorMatrix\", \"feComponentTransfer\",\n \"feComposite\", \"feConvolveMatrix\", \"feDiffuseLighting\", \"feDisplacementMap\",\n \"feDistantLight\", \"feDropShadow\", \"feFlood\", \"feFuncA\", \"feFuncB\", \"feFuncG\",\n \"feFuncR\", \"feGaussianBlur\", \"feImage\", \"feMerge\", \"feMergeNode\", \"feMorphology\",\n \"feOffset\", \"fePointLight\", \"feSpecularLighting\", \"feSpotLight\", \"feTile\",\n \"feTurbulence\", \"filter\", \"foreignObject\", \"g\", \"hatch\", \"hatchpath\", \"image\",\n \"line\", \"linearGradient\", \"marker\", \"mask\", \"metadata\", \"mpath\", \"path\", \"pattern\",\n \"polygon\", \"polyline\", \"radialGradient\", \"rect\", \"set\", \"stop\", \"svg\", \"switch\",\n \"symbol\", \"text\", \"textPath\", \"tspan\", \"use\", \"view\",\n \"svg:a\", \"svg:title\", \"svg:desc\", \"svg:script\", \"svg:style\"\n]);\n\nconst createElement = (name: string): Element => {\n if (SVG_TAGS.has(name)) {\n return document.createElementNS(\"http://www.w3.org/2000/svg\", name.replace(\"svg:\", \"\"));\n } else {\n return document.createElement(name);\n }\n}\n\nexport type CommandType = {\n CreateNode: {\n id: number,\n name: string,\n }\n} | {\n CreateText: {\n id: number,\n value: string\n }\n} | {\n UpdateText: {\n id: number,\n value: string\n }\n} | {\n SetAttr: {\n id: number,\n name: string,\n value: string\n }\n} | {\n RemoveAttr: {\n id: number,\n name: string\n }\n} | {\n RemoveNode: {\n id: number,\n }\n} | {\n RemoveText: {\n id: number,\n }\n} | {\n InsertBefore: {\n parent: number,\n child: number,\n ref_id: number | null,\n }\n} | {\n InsertCss: {\n selector: string | null,\n value: string\n }\n} | {\n CreateComment: {\n id: number,\n value: string\n }\n} | {\n RemoveComment: {\n id: number,\n }\n} | {\n CallbackAdd: {\n id: number,\n event_name: string,\n callback_id: number,\n }\n} | {\n CallbackRemove: {\n id: number,\n event_name: string,\n callback_id: number,\n }\n};\n\nconst assertNeverCommand = (data: never): never => {\n console.error(data);\n throw Error('unknown command');\n};\n\nexport class DriverDom {\n private appLocation: AppLocation;\n public readonly nodes: MapNodes;\n private readonly callbacks: CallbackManager;\n\n public constructor(private readonly metadata: Metadata, appLocation: AppLocation, getWasm: () => ModuleControllerType) {\n this.appLocation = appLocation;\n this.nodes = new MapNodes();\n this.callbacks = new CallbackManager(getWasm);\n\n document.addEventListener('dragover', (ev): void => {\n // console.log('File(s) in drop zone');\n ev.preventDefault();\n });\n }\n\n public update = (commands: Array) => {\n if (this.nodes.hasInitNodes() && this.metadata.getEnabledHydration()) {\n hydrate(commands, this.nodes, this.appLocation);\n }\n\n const setFocus: Set = new Set();\n\n for (const command of commands) {\n try {\n this.runCommand(command);\n } catch (error) {\n console.error('bulk_update - item', error, command);\n }\n\n if ('SetAttr' in command && command.SetAttr.name.toLocaleLowerCase() === 'autofocus') {\n setFocus.add(command.SetAttr.id);\n }\n }\n\n if (setFocus.size > 0) {\n setTimeout(() => {\n for (const id of setFocus) {\n const node = this.nodes.getNodeElement(`set focus ${id}`, id);\n node.focus();\n }\n }, 0);\n }\n\n this.nodes.removeInitNodes();\n\n // Make sure that the client-side generated styles are always the last element of the head\n this.nodes.addStyles();\n }\n\n private createNode(id: number, name: string) {\n // Root nodes (html/head/body) already exist in the real DOM\n if (id === 1 || id === 2 || id === 3) {\n return;\n }\n\n if (this.nodes.has(id)) {\n return;\n }\n\n const node = createElement(name);\n this.nodes.set(id, node);\n\n injects(node, this.appLocation);\n }\n\n private setAttr(id: number, name: string, value: string) {\n const node = this.nodes.getNode(\"set_attribute\", id);\n node.setAttribute(name, value);\n\n if (name == \"value\") {\n if (node instanceof HTMLInputElement) {\n node.value = value;\n return;\n }\n\n if (node instanceof HTMLTextAreaElement) {\n node.value = value;\n node.defaultValue = value;\n return;\n }\n }\n }\n\n private removeAttr(id: number, name: string) {\n const node = this.nodes.getNode(\"remove_attribute\", id);\n node.removeAttribute(name);\n\n if (name == \"value\") {\n if (node instanceof HTMLInputElement) {\n node.value = \"\";\n return;\n }\n\n if (node instanceof HTMLTextAreaElement) {\n node.value = \"\";\n node.defaultValue = \"\";\n return;\n }\n }\n }\n\n private removeNode(id: number) {\n // Never remove real document roots\n if (id === 1 || id === 2 || id === 3) {\n return;\n }\n\n const node = this.nodes.delete(\"remove_node\", id);\n node.remove();\n }\n\n private createText(id: number, value: string) {\n if (this.nodes.has(id)) {\n return;\n }\n\n const text = document.createTextNode(value);\n this.nodes.set(id, text);\n }\n\n private removeText(id: number) {\n const text = this.nodes.delete(\"remove_node\", id);\n text.remove();\n }\n\n private updateText(id: number, value: string) {\n const text = this.nodes.getText(\"set_attribute\", id);\n text.textContent = value;\n }\n\n private runCommand(command: CommandType) {\n if ('RemoveNode' in command) {\n this.removeNode(command.RemoveNode.id);\n return;\n }\n\n if ('InsertBefore' in command) {\n this.nodes.insertBefore(command.InsertBefore.parent, command.InsertBefore.child, command.InsertBefore.ref_id === null ? null : command.InsertBefore.ref_id);\n return;\n }\n\n if ('CreateNode' in command) {\n this.createNode(command.CreateNode.id, command.CreateNode.name);\n return;\n }\n\n if ('CreateText' in command) {\n this.createText(command.CreateText.id, command.CreateText.value);\n return;\n }\n\n if ('UpdateText' in command) {\n this.updateText(command.UpdateText.id, command.UpdateText.value);\n return;\n }\n\n if ('SetAttr' in command) {\n this.setAttr(command.SetAttr.id, command.SetAttr.name, command.SetAttr.value);\n return;\n }\n\n if ('RemoveAttr' in command) {\n this.removeAttr(command.RemoveAttr.id, command.RemoveAttr.name);\n return;\n }\n\n if ('RemoveText' in command) {\n this.removeText(command.RemoveText.id);\n return;\n }\n\n if ('InsertCss' in command) {\n this.nodes.insertCss(command.InsertCss.selector, command.InsertCss.value);\n return;\n }\n\n if ('CreateComment' in command) {\n const comment = document.createComment(command.CreateComment.value);\n this.nodes.set(command.CreateComment.id, comment);\n return;\n }\n\n if ('RemoveComment' in command) {\n const comment = this.nodes.delete(\"remove_comment\", command.RemoveComment.id);\n comment.remove();\n return;\n }\n\n if ('CallbackAdd' in command) {\n this.callbacks.add(this.nodes, command.CallbackAdd.id, command.CallbackAdd.event_name, command.CallbackAdd.callback_id);\n return;\n }\n\n if ('CallbackRemove' in command) {\n this.callbacks.remove(this.nodes, command.CallbackRemove.id, command.CallbackRemove.event_name, command.CallbackRemove.callback_id);\n return;\n }\n\n return assertNeverCommand(command);\n }\n}\n","import { DriverWebsocket } from \"./websocket/websocket\";\nimport { assertNever } from \"../assert_never\";\nimport { JsJsonType } from \"../jsjson\";\nimport { ModuleControllerType } from \"../wasm_init\";\nimport { ExportType } from \"../wasm_module\";\nimport { fetchCacheGet } from \"./command/fetchCacheGet\";\nimport { fetchExec, FetchRequestType } from \"./command/fetchExec\";\nimport { CallbackId } from \"./types\";\nimport { Interval } from \"./command/interval\";\nimport { AppLocation } from './location/AppLocation';\nimport { Cookies } from \"./command/cookies\";\nimport { getRandom } from \"./command/getRandom\";\nimport { CommandType, DriverDom } from \"./command/dom/dom\";\nimport { Metadata } from \"./metadata\";\n\ntype JsApiCommandType =\n | { Root: { name: string } }\n | { RootElement: { dom_id: number } }\n | { Get: { property: string } }\n | { Set: { property: string, value: JsJsonType } }\n | { Call: { method: string, args: JsJsonType[] } };\n\ntype ExecType\n = 'FetchCacheGet'\n | 'IsBrowser'\n | 'GetDateNow'\n | 'TimezoneOffset'\n | 'HistoryBack'\n | {\n FetchExec: {\n callback: CallbackId,\n request: FetchRequestType,\n }\n }\n | {\n WebsocketRegister: {\n callback: CallbackId,\n host: string\n }\n }\n | {\n WebsocketSendMessage: {\n callback: CallbackId,\n message: JsJsonType,\n }\n }\n | {\n WebsocketUnregister: {\n callback: CallbackId,\n }\n }\n | {\n TimerSet: {\n callback: CallbackId,\n duration: number,\n kind: 'Interval' | 'Timeout',\n }\n }\n | {\n TimerClear: {\n callback: CallbackId,\n }\n }\n | {\n LocationGet: {\n target: 'Hash' | 'History',\n }\n }\n | {\n LocationCallback: {\n callback: CallbackId,\n mode: 'Add' | 'Remove',\n target: 'Hash' | 'History'\n }\n }\n | {\n LocationSet: {\n mode: 'Push' | 'Replace',\n target: 'Hash' | 'History'\n value: string\n }\n }\n | {\n CookieSet: {\n name: string,\n value: string,\n expires_in: number,\n }\n }\n | {\n CookieGet: {\n name: string,\n }\n }\n | {\n CookieJsonSet: {\n name: string,\n value: JsJsonType,\n expires_in: number,\n }\n }\n | {\n CookieJsonGet: {\n name: string,\n }\n }\n | {\n GetEnv: {\n name: string\n }\n }\n | {\n Log: {\n arg2: string, //\"color: white; padding: 0 3px; background: green;\",\n arg3: string, //\"font-weight: bold; color: inherit\",\n arg4: string, //\"background: inherit; color: inherit\",\n kind: 'Debug' | 'Info' | 'Log' | 'Warn' | 'Error',\n message: string, //\"%cINFO%c crates/vertigo/src/driver_module/api/api_fetch_cache.rs:26%c FetchCache ready\"\n }\n }\n | {\n GetRandom: {\n min: number,\n max: number,\n }\n }\n | {\n JsApiCall: {\n commands: Array\n }\n }\n | {\n DomBulkUpdate: {\n list: Array\n }\n };\n\nexport class Api {\n public readonly dom: DriverDom;\n private readonly websocket: DriverWebsocket;\n private readonly interval: Interval;\n private readonly location: AppLocation;\n private readonly cookie: Cookies;\n\n\n constructor(private readonly metadata: Metadata, private readonly getWasm: () => ModuleControllerType) {\n const appLocation = new AppLocation(getWasm);\n\n this.dom = new DriverDom(metadata, appLocation, getWasm);\n this.websocket = new DriverWebsocket(getWasm);\n this.interval = new Interval(getWasm);\n this.location = appLocation;\n this.cookie = new Cookies();\n }\n\n exec(arg: JsJsonType): JsJsonType {\n\n //@ts-expect-error - //TODO Add safe type checking\n const safeArg: ExecType = arg;\n\n // console.info('exec arg', safeArg);\n\n if (safeArg === 'FetchCacheGet') {\n return fetchCacheGet(this.metadata);\n }\n\n if (safeArg === 'IsBrowser') {\n return {\n value: true\n };\n }\n\n if (safeArg === 'GetDateNow') {\n return {\n value: Date.now(),\n };\n }\n\n if (safeArg === 'TimezoneOffset') {\n return {\n value: new Date().getTimezoneOffset()\n };\n }\n\n if (safeArg === 'HistoryBack') {\n window.history.back();\n return null;\n }\n\n if ('FetchExec' in safeArg) {\n fetchExec(this.getWasm, safeArg.FetchExec.callback, safeArg.FetchExec.request);\n return null;\n }\n\n if ('WebsocketRegister' in safeArg) {\n this.websocket.websocket_register_callback(safeArg.WebsocketRegister.host, safeArg.WebsocketRegister.callback);\n return null;\n }\n\n if ('WebsocketSendMessage' in safeArg) {\n this.websocket.websocket_send_message(safeArg.WebsocketSendMessage.callback, safeArg.WebsocketSendMessage.message);\n return null;\n }\n\n if ('WebsocketUnregister' in safeArg) {\n this.websocket.websocket_unregister_callback(safeArg.WebsocketUnregister.callback);\n return null;\n }\n\n if ('TimerSet' in safeArg) {\n this.interval.timerSet(safeArg.TimerSet.callback, safeArg.TimerSet.duration, safeArg.TimerSet.kind);\n return null;\n }\n\n if ('TimerClear' in safeArg) {\n this.interval.timerClear(safeArg.TimerClear.callback);\n return null;\n }\n\n if ('LocationGet' in safeArg) {\n return {\n value: this.location.get(safeArg.LocationGet.target)\n };\n }\n\n if ('LocationCallback' in safeArg) {\n this.location.callback(safeArg.LocationCallback.target, safeArg.LocationCallback.mode, safeArg.LocationCallback.callback);\n return null;\n }\n\n if ('LocationSet' in safeArg) {\n this.location.set(safeArg.LocationSet.target, safeArg.LocationSet.mode, safeArg.LocationSet.value);\n return null;\n }\n\n if ('CookieGet' in safeArg) {\n return {\n value: this.cookie.get(safeArg.CookieGet.name)\n };\n }\n\n if ('CookieSet' in safeArg) {\n this.cookie.set(safeArg.CookieSet.name, safeArg.CookieSet.value, safeArg.CookieSet.expires_in);\n return null;\n }\n\n if ('CookieJsonGet' in safeArg) {\n return {\n value: this.cookie.getJson(safeArg.CookieJsonGet.name)\n };\n }\n\n if ('CookieJsonSet' in safeArg) {\n this.cookie.setJson(safeArg.CookieJsonSet.name, safeArg.CookieJsonSet.value, safeArg.CookieJsonSet.expires_in);\n return null;\n }\n\n if ('GetEnv' in safeArg) {\n const name = safeArg.GetEnv.name;\n\n return {\n value: this.metadata.getEnv(name),\n }\n }\n\n if ('Log' in safeArg) {\n switch (safeArg.Log.kind) {\n case 'Info': {\n console.info(safeArg.Log.message, safeArg.Log.arg2, safeArg.Log.arg3, safeArg.Log.arg4);\n return null;\n }\n case 'Debug': {\n console.debug(safeArg.Log.message, safeArg.Log.arg2, safeArg.Log.arg3, safeArg.Log.arg4);\n return null;\n }\n case 'Error': {\n console.error(safeArg.Log.message, safeArg.Log.arg2, safeArg.Log.arg3, safeArg.Log.arg4);\n return null;\n }\n case 'Log': {\n console.log(safeArg.Log.message, safeArg.Log.arg2, safeArg.Log.arg3, safeArg.Log.arg4);\n return null;\n }\n case 'Warn': {\n console.warn(safeArg.Log.message, safeArg.Log.arg2, safeArg.Log.arg3, safeArg.Log.arg4);\n return null;\n }\n }\n }\n\n if ('GetRandom' in safeArg) {\n return {\n value: getRandom(safeArg.GetRandom.min, safeArg.GetRandom.max)\n };\n }\n\n if ('JsApiCall' in safeArg) {\n return this.executeJsApiCall(safeArg.JsApiCall.commands);\n }\n\n if ('DomBulkUpdate' in safeArg) {\n this.dom.update(safeArg.DomBulkUpdate.list);\n return null;\n }\n\n console.info('exec_command: Arg', safeArg);\n return assertNever(safeArg);\n }\n\n private executeJsApiCall(commands: Array): JsJsonType {\n let current: any = null;\n\n for (const command of commands) {\n if ('Root' in command) {\n if (command.Root.name === 'window') {\n current = window;\n } else if (command.Root.name === 'document') {\n current = document;\n } else {\n console.error(`Unknown root: ${command.Root.name}`);\n return null;\n }\n } else if ('RootElement' in command) {\n const domId = command.RootElement.dom_id;\n const node = this.dom.nodes.getAnyOption(domId);\n if (node === undefined) {\n console.error(`Element not found: ${domId}`);\n return null;\n }\n current = node;\n } else if ('Get' in command) {\n if (current === null) {\n console.error('Get called on null');\n return null;\n }\n current = current[command.Get.property];\n } else if ('Set' in command) {\n if (current === null) {\n console.error('Set called on null');\n return null;\n }\n current[command.Set.property] = command.Set.value;\n current = undefined;\n } else if ('Call' in command) {\n if (current === null) {\n console.error('Call called on null');\n return null;\n }\n current = current[command.Call.method](...command.Call.args);\n }\n }\n\n // Convert result to JsJson - sanitize host objects (Window, Element, Function, etc.)\n const isPlainObject = (obj: any): boolean => {\n if (obj === null) return false;\n if (typeof obj !== 'object') return false;\n const proto = Object.getPrototypeOf(obj);\n return proto === Object.prototype || proto === null;\n };\n\n const sanitize = (value: any): JsJsonType => {\n if (value === null || value === undefined) {\n return null;\n }\n if (typeof value === 'boolean') {\n return value;\n }\n if (typeof value === 'string') {\n return value;\n }\n if (typeof value === 'number') {\n return value;\n }\n if (value instanceof Uint8Array) {\n return value;\n }\n if (Array.isArray(value)) {\n return value.map((v) => sanitize(v));\n }\n if (isPlainObject(value)) {\n const out: { [k: string]: JsJsonType } = {};\n for (const k of Object.keys(value)) {\n out[k] = sanitize(value[k]);\n }\n return out;\n }\n\n // Host objects (Window, Element, DOM nodes, functions, class instances, etc.)\n // are not serializable to JsJson. Return null for safety.\n return null;\n };\n\n return sanitize(current);\n }\n}\n","import { JsJsonType } from \"../../jsjson\";\nimport { Metadata } from \"../metadata\";\n\nexport const fetchCacheGet = (metadata: Metadata): JsJsonType => {\n const cache = metadata.getFetchCache();\n\n return {\n data: cache\n };\n};\n","export class Metadata {\n private readonly metadata: HTMLElement;\n\n constructor() {\n const metadata = document.getElementById('v-metadata');\n\n if (metadata === null) {\n throw Error('Expected v-metadata');\n }\n\n this.metadata = metadata;\n metadata.remove();\n }\n\n private get = (attr: string): string | null => {\n return this.metadata.getAttribute(attr) ?? null;\n }\n\n getEnv(name: string) {\n return this.get(`data-env-${name}`);\n }\n\n getFetchCache() {\n return this.get('data-fetch-cache') ?? null;\n }\n\n getEnabledHydration = (): boolean => {\n const value = this.get('data-env-disable-hydration');\n return value !== 'true';\n }\n}\n","import { wasmInit, ModuleControllerType } from './wasm_init';\nimport { BufferCursor } from './buffer_cursor';\nimport { jsJsonDecodeItem, jsJsonGetSize, saveJsJsonToBufferItem } from './jsjson';\nimport { Api } from './api/api';\nimport { Metadata } from './api/metadata';\n\n//Number -> u32 or i32\n//BigInt -> u64 or i64\n\nexport type ImportType = {\n panic_message: (long_ptr: bigint) => void,\n //call from rust\n dom_access: (long_ptr: bigint) => bigint,\n}\n\nexport type ExportType = {\n vertigo_export_alloc_block: (size: number) => bigint,\n vertigo_export_free_block: (pointer: bigint) => void,\n vertigo_export_wasm_command: (value_ptr: bigint) => bigint,\n vertigo_entry_function: (major: number, minor: number) => void,\n}\n\nexport class WasmModule {\n private readonly wasm: ModuleControllerType;\n\n private constructor(\n wasm: ModuleControllerType,\n ) {\n this.wasm = wasm;\n }\n\n public vertigoEntryFunction(major: number, minor: number) {\n this.wasm.exports.vertigo_entry_function(major, minor);\n }\n\n public static async create(wasmBinPath: string): Promise {\n let wasmModule: ModuleControllerType | null = null;\n\n const getWasm = (): ModuleControllerType => {\n if (wasmModule === null) {\n throw Error('Wasm is no initialized');\n }\n\n return wasmModule;\n };\n\n const metadata = new Metadata();\n const vertigo_api = new Api(metadata, getWasm);\n\n //@ts-expect-error\n window.$vertigoApi = vertigo_api;\n\n wasmModule = await wasmInit(wasmBinPath, {\n mod: {\n panic_message: (long_ptr: bigint) => {\n\n const size = Number(long_ptr % (2n ** 32n));\n const ptr = Number(long_ptr >> 32n);\n\n const decoder = new TextDecoder(\"utf-8\");\n const m = getWasm().getUint8Memory().subarray(ptr, ptr + size);\n const message = decoder.decode(m);\n console.error('PANIC', message);\n },\n dom_access: (long_ptr: bigint): bigint => {\n if (long_ptr === 0n) {\n console.error('dom_access - null pointer');\n return 0n;\n }\n\n // Decode JsJson\n const buffer = new BufferCursor(\n () => getWasm().getUint8Memory(),\n long_ptr\n );\n const args = jsJsonDecodeItem(buffer);\n getWasm().exports.vertigo_export_free_block(long_ptr);\n\n // Execute command (now using JsApiCall instead of array-of-arrays)\n const response = vertigo_api.exec(args);\n\n // Save JsJson response\n const responseSize = jsJsonGetSize(response);\n const responseLongPtr = getWasm().exports.vertigo_export_alloc_block(responseSize);\n const responseBuffer = new BufferCursor(\n () => getWasm().getUint8Memory(),\n responseLongPtr\n );\n saveJsJsonToBufferItem(response, responseBuffer);\n\n return responseLongPtr;\n }\n }\n });\n\n return new WasmModule(wasmModule);\n }\n}\n","import { WasmModule } from \"./wasm_module\";\n\n// vertigo-cli compatibility version, change together with package version.\nconst VERTIGO_COMPAT_VERSION_MAJOR = 0;\nconst VERTIGO_COMPAT_VERSION_MINOR = 12;\n\nconst moduleRun: Set = new Set();\n\nconst runModule = async (wasm: string) => {\n if (moduleRun.has(wasm)) {\n //ok, module is run\n return;\n }\n\n if (moduleRun.size > 0) {\n console.error('Only one wasm module can be run', { moduleRun, wasm });\n return;\n }\n\n moduleRun.add(wasm);\n\n console.info(`Wasm module: \"${wasm}\" -> start`);\n const wasmModule = await WasmModule.create(wasm);\n console.info(`Wasm module: \"${wasm}\" -> initialized`);\n wasmModule.vertigoEntryFunction(VERTIGO_COMPAT_VERSION_MAJOR, VERTIGO_COMPAT_VERSION_MINOR);\n console.info(`Wasm module: \"${wasm}\" -> launched vertigoEntryFunction with version ${VERTIGO_COMPAT_VERSION_MAJOR}.${VERTIGO_COMPAT_VERSION_MINOR}`);\n};\n\nconst findAndRunModule = async () => {\n document.querySelectorAll('*[data-vertigo-run-wasm]').forEach((node) => {\n const wasm = node.getAttribute('data-vertigo-run-wasm');\n\n if (typeof wasm === 'string') {\n runModule(wasm);\n } else {\n console.error('Run error', node);\n }\n });\n};\n\n(() => {\n window.addEventListener('load', findAndRunModule);\n setTimeout(findAndRunModule, 3000);\n})();\n"],"names":["decoder","TextDecoder","encoder","TextEncoder","BufferCursor","constructor","getUint8Memory","long_ptr","this","pointer","ptr","Number","size","dataView","DataView","buffer","getByte","value","getUint8","setByte","byte","setUint8","getU16","getUint16","setU16","setUint16","getU32","getUint32","setU32","setUint32","getI32","getInt32","setI32","setInt32","getU64","getBigUint64","setU64","setBigUint64","getI64","getBigInt64","setI64","setBigInt64","getF64","getFloat64","setF64","setFloat64","getBuffer","result","subarray","setBuffer","length","set","getString","decode","setString","encode","getSavedSize","JsJsonConst","jsJsonGetSize","Uint8Array","Array","isArray","sum","item","key","propertyValue","Object","entries","Error","jsJsonDecodeItem","typeId","count","list","i","push","obj","saveJsJsonToBufferItem","undefined","wasmInit","async","wasmBinPath","imports","module_instance","WebAssembly","instantiateStreaming","stream","fetch","err","console","warn","info","resp","binary","arrayBuffer","instantiate","fetchModule","cacheGetUint8Memory","instance","exports","memory","Memory","wasmCommand","vertigo_export_alloc_block","result_long_ptr","vertigo_export_wasm_command","resultBuffer","vertigo_export_free_block","EventEmitter","events","Set","on","callback","isActive","onExec","param","add","delete","trigger","eventsCopy","from","values","itemCallbackToRun","error","PromiseBoxRace","inner","resolve","promiseResolveReject","reject","isFulfilled","promise","Promise","localResolve","localReject","createPromiseValue","reconnectDelay","label","timeout_retry","timeout","setTimeout","LogContext","host","formatLog","message","SocketConnection","close","send","eventMessage","connect","log","done","socket","WebSocket","isClose","closeSocket","socketConnection","addEventListener","event","dataRaw","data","startSocket","timeout_connection","onMessage","isConnect","openSocketResult","type","catch","dispose","wireStringToJsJson","raw","JSON","parse","wasmCallback","wasm","callbackId","command","Websocket","DriverWebsocket","getWasm","websocket_register_callback","callback_id","controller","controllerList","has","assertNeverMessage","Message","websocket_unregister_callback","get","websocket_send_message","stringify","Map","getHeaders","headers","k","v","getBodyString","body","Data","processResponse","response","status","contentType","startsWith","Ok","Text","text","Json","bodyText","Err","String","Interval","timerSet","duration","kind","timerId","setInterval","TimerCall","timerClear","timerResource","clearInterval","clearTimeout","HashRouter","LocationCall","remove","new_hash","location","hash","replace","history","replaceState","window","decodeURIComponent","substr","HistoryLocation","url","pushState","pathname","search","AppLocation","target","mode","locations","newValue","Hash","History","Cookies","cname","cookie","document","split","cookieChunk","trim","cookieName","cookieValue","getJson","cvalue_str","e","cvalue","expires_in","cvalueEncoded","encodeURIComponent","d","Date","setTime","getTime","expires","toUTCString","setJson","getRandom","min","max","range","Math","floor","random","CallbackManager","callbacks","observers","nodes","id","event_name","intersectAdd","click","submit","input","change","blur","mousedown","mouseup","mouseenter","mouseleave","keydown","drop","load","changeFile","node","domEventName","intersectRemove","removeEventListener","CallbackCall","getNode","observer","IntersectionObserver","entry","isIntersecting","intersectionRatio","boundingClientRect","top","bottom","height","observe","disconnect","preventDefault","click_event","stopPropagation","HTMLInputElement","HTMLTextAreaElement","HTMLSelectElement","files","promises","file","then","buf","name","all","params","f","_event","DragEvent","dataTransfer","items","getAsFile","getFiles","dataArray","KeyboardEvent","code","altKey","ctrlKey","shiftKey","metaKey","injects","appLocation","tagName","toLocaleLowerCase","href","getAttribute","scrollTo","hydrateLink","HydrationEngine","commands","depth","matched","virtualNodes","createVirtualNodes","hydrate","hydrateNode","head","toFixed","vNodeId","realNode","vNode","realChildren","childNodes","realIndex","skipTextVNodes","childVId","children","childVNode","candidate","isMatch","checkElementMatch","nodeType","Node","TEXT_NODE","checkTextMatch","removeSkippedNodes","claimNode","ELEMENT_NODE","attributes","element","setAttribute","textContent","Element","Comment","j","nodeToRemove","getVNode","CreateNode","toUpperCase","CreateText","parent","InsertBefore","childId","child","refId","ref_id","index","indexOf","splice","SetAttr","MapNodes","initNodes","getRootHead","getRootBody","style","createElement","getRootHtml","documentElement","getAnyOption","getAny","getNodeElement","HTMLElement","getText","getComment","insertCss","selector","content","createTextNode","appendChild","removeInitNodes","insertBefore","parentNode","childNode","ref_node","addStyles","hasInitNodes","SVG_TAGS","DriverDom","metadata","update","getEnabledHydration","setFocus","runCommand","focus","ev","createNode","createElementNS","setAttr","defaultValue","removeAttr","removeAttribute","removeNode","createText","removeText","updateText","RemoveNode","UpdateText","RemoveAttr","RemoveText","InsertCss","comment","createComment","CreateComment","RemoveComment","CallbackAdd","assertNeverCommand","CallbackRemove","Api","dom","websocket","interval","exec","arg","safeArg","getFetchCache","now","getTimezoneOffset","back","request","method","response2","FetchExecResponse","responseToWasm","toString","fetchExec","FetchExec","WebsocketRegister","WebsocketSendMessage","WebsocketUnregister","TimerSet","TimerClear","LocationGet","LocationCallback","LocationSet","CookieGet","CookieSet","CookieJsonGet","CookieJsonSet","GetEnv","getEnv","Log","arg2","arg3","arg4","debug","GetRandom","executeJsApiCall","JsApiCall","DomBulkUpdate","assertNever","current","Root","domId","RootElement","dom_id","Get","property","Call","args","sanitize","map","proto","getPrototypeOf","prototype","isPlainObject","out","keys","Metadata","attr","getElementById","WasmModule","vertigoEntryFunction","major","minor","vertigo_entry_function","create","wasmModule","vertigo_api","$vertigoApi","mod","panic_message","m","dom_access","responseSize","responseLongPtr","responseBuffer","moduleRun","findAndRunModule","querySelectorAll","forEach","runModule"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"wasm_run.js","sources":["src_js/buffer_cursor.ts","src_js/jsjson.ts","src_js/wasm_init.ts","src_js/api/websocket/event_emiter.ts","src_js/api/websocket/promise.ts","src_js/api/websocket/connection.ts","src_js/api/websocket/websocket.ts","src_js/assert_never.ts","src_js/api/command/fetchExec.ts","src_js/api/command/interval.ts","src_js/api/location/hashrouter.ts","src_js/api/location/historyLocation.ts","src_js/api/location/AppLocation.ts","src_js/api/command/cookies.ts","src_js/api/command/getRandom.ts","src_js/api/command/dom/callbackManager.ts","src_js/api/command/dom/dataTransfer.ts","src_js/api/command/dom/injects.ts","src_js/api/command/dom/hydration.ts","src_js/api/command/dom/map_nodes.ts","src_js/api/command/dom/dom.ts","src_js/api/api.ts","src_js/api/command/fetchCacheGet.ts","src_js/api/metadata.ts","src_js/wasm_module.ts","src_js/index.ts"],"sourcesContent":["///https://javascript.info/arraybuffer-binary-arrays#dataview\n\nconst decoder = new TextDecoder(\"utf-8\");\nconst encoder = new TextEncoder();\n\nexport class BufferCursor {\n private dataView: DataView;\n private pointer: number = 0;\n private ptr: number;\n private size: number;\n\n constructor(\n private getUint8Memory: () => Uint8Array,\n long_ptr: bigint,\n ) {\n this.ptr = Number(long_ptr >> 32n);\n this.size = Number(long_ptr % (2n ** 32n));\n\n this.dataView = new DataView(\n this.getUint8Memory().buffer,\n this.ptr,\n this.size\n );\n }\n\n public getByte(): number {\n const value = this.dataView.getUint8(this.pointer);\n this.pointer += 1;\n return value;\n }\n\n public setByte(byte: number) {\n this.dataView.setUint8(this.pointer, byte);\n this.pointer += 1;\n }\n\n public getU16(): number {\n const value = this.dataView.getUint16(this.pointer);\n this.pointer += 2;\n return value;\n }\n\n public setU16(value: number) {\n this.dataView.setUint16(this.pointer, value);\n this.pointer += 2;\n }\n\n public getU32(): number {\n const value = this.dataView.getUint32(this.pointer);\n this.pointer += 4;\n return value;\n }\n\n public setU32(value: number) {\n this.dataView.setUint32(this.pointer, value);\n this.pointer += 4;\n }\n\n public getI32(): number {\n const value = this.dataView.getInt32(this.pointer);\n this.pointer += 4;\n return value;\n }\n\n public setI32(value: number) {\n this.dataView.setInt32(this.pointer, value);\n this.pointer += 4;\n }\n\n public getU64(): bigint {\n const value = this.dataView.getBigUint64(this.pointer);\n this.pointer += 8;\n return value;\n }\n\n public setU64(value: bigint) {\n this.dataView.setBigUint64(this.pointer, value);\n this.pointer += 8;\n }\n\n public getI64(): bigint {\n const value = this.dataView.getBigInt64(this.pointer);\n this.pointer += 8;\n return value;\n }\n\n public setI64(value: bigint) {\n this.dataView.setBigInt64(this.pointer, value);\n this.pointer += 8;\n }\n\n public getF64(): number {\n const value = this.dataView.getFloat64(this.pointer);\n this.pointer += 8;\n return value;\n }\n\n public setF64(value: number) {\n this.dataView.setFloat64(this.pointer, value);\n this.pointer += 8;\n }\n\n public getBuffer(): Uint8Array {\n const size = this.getU32();\n const result = this\n .getUint8Memory()\n .subarray(\n this.ptr + this.pointer,\n this.ptr + this.pointer + size\n );\n\n this.pointer += size;\n return result;\n }\n\n public setBuffer(buffer: Uint8Array) {\n const size = buffer.length;\n this.setU32(size);\n\n const sub_buffer = this\n .getUint8Memory()\n .subarray(\n this.ptr + this.pointer,\n this.ptr + this.pointer + size\n );\n\n sub_buffer.set(buffer);\n\n this.pointer += size;\n }\n\n public getString(): string {\n return decoder.decode(this.getBuffer());\n }\n\n public setString(value: string) {\n const buffer = encoder.encode(value);\n this.setBuffer(buffer);\n }\n\n public getSavedSize(): number {\n return this.pointer;\n }\n}\n\nexport const getStringSize = (value: string): number => {\n return new TextEncoder().encode(value).length;\n};\n\n","import { BufferCursor } from \"./buffer_cursor\";\n\nconst JsJsonConst = {\n True: 1,\n False: 2,\n Null: 3,\n Undefined: 4,\n String: 5,\n Number: 6,\n List: 7,\n Object: 8,\n Vec: 9,\n} as const;\n\nexport type JsJsonType = boolean | null | undefined | string | number | Uint8Array | Array | { [key: string]: JsJsonType };\n\nexport const jsJsonGetSize = (value: JsJsonType): number => {\n if (value === true || value === false || value === null || value === undefined) {\n return 1;\n }\n\n if (typeof value === 'string') {\n return 1 + 4 + new TextEncoder().encode(value).length;\n }\n\n if (typeof value === 'number') {\n return 1 + 8;\n }\n\n if (value instanceof Uint8Array) {\n return 1 + 4 + value.length;\n }\n\n if (Array.isArray(value)) {\n let sum = 1 + 4;\n for (const item of value) {\n sum += jsJsonGetSize(item);\n }\n return sum;\n }\n\n if (typeof value === 'object' && value !== null) {\n let sum = 1 + 2;\n for (const [key, propertyValue] of Object.entries(value)) {\n sum += 4 + new TextEncoder().encode(key).length;\n sum += jsJsonGetSize(propertyValue);\n }\n return sum;\n }\n\n throw new Error(`jsJsonGetSize: Unknown type ${typeof value}`);\n};\n\nexport const jsJsonDecodeItem = (buffer: BufferCursor): JsJsonType => {\n const typeId = buffer.getByte();\n\n if (typeId === JsJsonConst.True) {\n return true;\n }\n\n if (typeId === JsJsonConst.False) {\n return false;\n }\n\n if (typeId === JsJsonConst.Null) {\n return null;\n }\n\n if (typeId === JsJsonConst.Undefined) {\n return undefined;\n }\n\n if (typeId === JsJsonConst.String) {\n return buffer.getString();\n }\n\n if (typeId === JsJsonConst.Number) {\n return buffer.getF64();\n }\n\n if (typeId === JsJsonConst.List) {\n const count = buffer.getU32();\n const list: Array = [];\n\n for (let i = 0; i < count; i++) {\n list.push(jsJsonDecodeItem(buffer));\n }\n\n return list;\n }\n\n if (typeId === JsJsonConst.Object) {\n const count = buffer.getU16();\n const obj: { [key: string]: JsJsonType } = {};\n\n for (let i = 0; i < count; i++) {\n const key = buffer.getString();\n const value = jsJsonDecodeItem(buffer);\n obj[key] = value;\n }\n\n return obj;\n }\n\n if (typeId === JsJsonConst.Vec) {\n return buffer.getBuffer();\n }\n\n throw new Error(`jsJsonDecodeItem: Unknown type id ${typeId}`);\n};\n\nexport const saveJsJsonToBufferItem = (value: JsJsonType, buffer: BufferCursor): void => {\n if (value === true) {\n buffer.setByte(JsJsonConst.True);\n return;\n }\n\n if (value === false) {\n buffer.setByte(JsJsonConst.False);\n return;\n }\n\n if (value === null) {\n buffer.setByte(JsJsonConst.Null);\n return;\n }\n\n if (value === undefined) {\n buffer.setByte(JsJsonConst.Undefined);\n return;\n }\n\n if (typeof value === 'string') {\n buffer.setByte(JsJsonConst.String);\n buffer.setString(value);\n return;\n }\n\n if (typeof value === 'number') {\n buffer.setByte(JsJsonConst.Number);\n buffer.setF64(value);\n return;\n }\n\n if (value instanceof Uint8Array) {\n buffer.setByte(JsJsonConst.Vec);\n buffer.setBuffer(value);\n return;\n }\n\n if (Array.isArray(value)) {\n buffer.setByte(JsJsonConst.List);\n buffer.setU32(value.length);\n\n for (const item of value) {\n saveJsJsonToBufferItem(item, buffer);\n }\n\n return;\n }\n\n if (typeof value === 'object' && value !== null) {\n const entries = Object.entries(value);\n\n buffer.setByte(JsJsonConst.Object);\n buffer.setU16(entries.length);\n\n for (const [key, propertyValue] of entries) {\n buffer.setString(key);\n saveJsJsonToBufferItem(propertyValue, buffer);\n }\n\n return;\n }\n\n throw new Error(`saveJsJsonToBufferItem: Unknown type ${typeof value}`);\n};\n","import { BufferCursor } from './buffer_cursor';\nimport { jsJsonGetSize, jsJsonDecodeItem, saveJsJsonToBufferItem, JsJsonType } from './jsjson';\n\nexport interface BaseExportType {\n vertigo_export_alloc_block: (size: number) => bigint,\n vertigo_export_free_block: (pointer: bigint) => void,\n vertigo_export_wasm_command: (value_ptr: bigint) => bigint,\n};\n\nexport interface ModuleControllerType {\n exports: ExportType,\n getUint8Memory: () => Uint8Array,\n wasmCommand: (params: JsJsonType) => JsJsonType,\n}\n\nconst fetchModule = async (wasmBinPath: string, imports: Record): Promise => {\n if (typeof WebAssembly.instantiateStreaming === 'function') {\n const stream = fetch(wasmBinPath);\n try {\n const module = await WebAssembly.instantiateStreaming(stream, imports);\n return module;\n } catch (err) {\n console.warn(\"`WebAssembly.instantiateStreaming` failed. This could happen if your server does not serve wasm with `application/wasm` MIME type, but check the original error too. Falling back to `WebAssembly.instantiate` which is slower. Original error:\\n\", err);\n }\n }\n\n console.info('fetchModule by WebAssembly.instantiate');\n\n const resp = await fetch(wasmBinPath);\n const binary = await resp.arrayBuffer();\n const module_instance = await WebAssembly.instantiate(binary, imports);\n return module_instance;\n};\n\nexport const wasmInit = async , ExportType extends BaseExportType>(\n wasmBinPath: string,\n imports: { mod: ImportType },\n): Promise> => {\n const module_instance = await fetchModule(wasmBinPath, imports);\n\n let cacheGetUint8Memory: Uint8Array = new Uint8Array(1);\n\n const getUint8Memory = () => {\n if (module_instance.instance.exports.memory instanceof WebAssembly.Memory) {\n if (cacheGetUint8Memory.buffer !== module_instance.instance.exports.memory.buffer) {\n cacheGetUint8Memory = new Uint8Array(module_instance.instance.exports.memory.buffer);\n }\n return cacheGetUint8Memory;\n } else {\n throw Error('Missing memory');\n }\n };\n\n //@ts-expect-error\n const exports: ExportType = module_instance.instance.exports;\n\n const wasmCommand = (value: JsJsonType): JsJsonType => {\n // Serialize JsJson\n const size = jsJsonGetSize(value);\n const long_ptr = exports.vertigo_export_alloc_block(size);\n const buffer = new BufferCursor(getUint8Memory, long_ptr);\n saveJsJsonToBufferItem(value, buffer);\n\n let result_long_ptr = exports.vertigo_export_wasm_command(long_ptr);\n\n // Decode JsJson\n if (result_long_ptr === 0n) {\n return null;\n }\n const resultBuffer = new BufferCursor(getUint8Memory, result_long_ptr);\n const result = jsJsonDecodeItem(resultBuffer);\n exports.vertigo_export_free_block(result_long_ptr);\n\n return result;\n };\n\n\n return {\n exports,\n getUint8Memory,\n wasmCommand: wasmCommand,\n };\n};\n","export class EventEmitter {\n private events: Set<(param: T) => void>;\n\n constructor() {\n this.events = new Set()\n }\n\n on(callback: (param: T) => void) {\n let isActive = true;\n\n const onExec = (param: T) => {\n if (isActive) {\n callback(param);\n }\n };\n\n this.events.add(onExec);\n\n return () => {\n isActive = false;\n this.events.delete(onExec);\n };\n }\n\n trigger(param: T) {\n const eventsCopy = Array.from(this.events.values())\n\n for (const itemCallbackToRun of eventsCopy) {\n try {\n itemCallbackToRun(param);\n } catch (err) {\n console.error(err);\n }\n }\n }\n\n get size(): number {\n return this.events.size;\n }\n}\n","type ResolveFn = (data: T) => void;\ntype RejectFn = (err: unknown) => void;\n\ninterface PromiseResolveReject {\n readonly resolve: (value: T) => void,\n readonly reject: (err: unknown) => void,\n};\n\nconst createPromiseValue = (): [PromiseResolveReject, Promise] => {\n let resolve: ResolveFn | null = null;\n let reject: RejectFn | null = null;\n\n const promise: Promise = new Promise((localResolve: ResolveFn, localReject: RejectFn) => {\n resolve = localResolve;\n reject = localReject;\n });\n\n if (resolve === null) {\n throw Error('createPromiseValue - resolve is null');\n }\n\n if (reject === null) {\n throw Error('createPromiseValue - reject is null');\n }\n\n const promiseValue = {\n resolve,\n reject,\n };\n\n return [promiseValue, promise];\n};\n\nexport class PromiseBoxRace {\n private inner: PromiseResolveReject | null = null;\n readonly promise: Promise;\n\n constructor() {\n const [promiseResolveReject, promise] = createPromiseValue();\n\n this.inner = promiseResolveReject;\n this.promise = promise;\n }\n\n resolve = (value: T) => {\n const promiseResolveReject = this.inner;\n this.inner = null;\n\n if (promiseResolveReject === null) {\n return;\n }\n\n promiseResolveReject.resolve(value);\n }\n\n reject = (err?: unknown) => {\n const promiseResolveReject = this.inner;\n this.inner = null;\n\n if (promiseResolveReject === null) {\n return;\n }\n\n promiseResolveReject.reject(err);\n }\n\n isFulfilled = (): boolean => {\n return this.inner === null;\n }\n}\n","import { EventEmitter } from \"./event_emiter\";\nimport { PromiseBoxRace } from \"./promise\";\n\nconst timeout = async (timeout: number): Promise => {\n return new Promise((resolve: (data: void) => void) => {\n setTimeout(resolve, timeout);\n });\n};\n\n\nconst reconnectDelay = async (label: string, timeout_retry: number): Promise => {\n console.info(`${label} wait ${timeout_retry}ms`);\n await timeout(timeout_retry);\n console.info(`${label} go forth`);\n};\n\nexport type SocketEventType = {\n type: 'message',\n message: string,\n} | {\n type: 'socket',\n socket: SocketConnection\n} | {\n type: 'close',\n};\n\nexport type OnMessageType = (message: SocketEventType) => void;\nexport type UnsubscribeFnType = () => void;\n\ninterface OpenSocketResult {\n socket: Promise,\n done: Promise,\n}\n\nexport interface SocketConnectionController {\n send: (message: string) => void,\n dispose: UnsubscribeFnType\n}\n\nclass LogContext {\n public constructor(private host: string) {}\n public formatLog = (message: string): string => `Socket ${this.host} ==> ${message}`;\n}\nexport class SocketConnection {\n private readonly eventMessage: EventEmitter;\n public readonly close: () => void;\n public readonly send: (message: string) => void;\n\n private constructor(\n close: () => void,\n send: (message: string) => void,\n ) {\n this.eventMessage = new EventEmitter();\n this.close = close;\n this.send = send;\n }\n\n private static connect(\n log: LogContext,\n host: string,\n timeout: number,\n ): OpenSocketResult {\n const result = new PromiseBoxRace();\n const done = new PromiseBoxRace();\n const socket = new WebSocket(host);\n let isClose: boolean = false;\n\n console.info(log.formatLog('starting ...'));\n\n const closeSocket = (): void => {\n if (isClose) {\n return;\n }\n\n console.info(log.formatLog('close'));\n\n isClose = true;\n result.resolve(null);\n done.resolve();\n socket.close();\n };\n\n\n const socketConnection = new SocketConnection(\n closeSocket,\n (message: string) => {\n if (isClose) {\n return;\n }\n socket.send(message);\n }\n );\n\n setTimeout(() => {\n if (result.isFulfilled() === false) {\n console.error(log.formatLog(`timeout (${timeout}ms)`));\n closeSocket();\n }\n }, timeout);\n\n const onOpen = (): void => {\n console.info(log.formatLog('open'));\n result.resolve(socketConnection);\n };\n\n const onError = (error: Event): void => {\n console.error(log.formatLog('error'), error);\n closeSocket();\n };\n\n const onMessage = (event: MessageEvent): void => {\n if (isClose) {\n return;\n }\n\n const dataRaw = event.data;\n\n if (typeof dataRaw === 'string') {\n socketConnection.eventMessage.trigger(dataRaw);\n return;\n }\n\n console.error(log.formatLog('onMessage - expected string'), dataRaw);\n };\n\n socket.addEventListener('open', onOpen);\n socket.addEventListener('error', onError);\n socket.addEventListener('close', closeSocket);\n socket.addEventListener('message', onMessage);\n\n return {\n socket: result.promise,\n done: done.promise\n };\n }\n\n public static startSocket(\n host: string,\n timeout_connection: number,\n timeout_retry: number,\n onMessage: OnMessageType,\n ): SocketConnectionController {\n let isConnect: boolean = true;\n let socketConnection: SocketConnection | null = null;\n\n const log = new LogContext(host);\n\n (async (): Promise => {\n while (isConnect) {\n const openSocketResult = SocketConnection.connect(log, host, timeout_connection);\n\n const socket = await openSocketResult.socket;\n\n if (socket === null) {\n await reconnectDelay(log.formatLog('reconnect after error'), timeout_retry);\n continue;\n }\n\n socketConnection = socket;\n onMessage({\n type: 'socket',\n socket\n });\n\n socket.eventMessage.on(message => {\n onMessage({\n type: 'message',\n message\n });\n });\n\n await openSocketResult.done;\n\n onMessage({\n type: 'close'\n });\n\n if (!isConnect) {\n console.info(log.formatLog('disconnect (1)'));\n return;\n }\n\n await reconnectDelay(log.formatLog('reconnect after close'), timeout_retry);\n }\n\n console.info(log.formatLog('disconnect (2)'));\n })().catch((error) => {\n console.error(error);\n });\n\n return {\n send: (message: string): void => {\n if (socketConnection === null) {\n console.error('send fail - missing connection', message);\n } else {\n socketConnection.send(message);\n }\n },\n dispose: (): void => {\n isConnect = false;\n socketConnection?.close();\n }\n };\n }\n}\n","import { JsJsonType } from \"../../jsjson\";\nimport { ModuleControllerType } from \"../../wasm_init\";\nimport { ExportType } from \"../../wasm_module\";\nimport { CallbackId } from \"../types\";\nimport { SocketConnection, SocketConnectionController } from \"./connection\";\n\nconst wireStringToJsJson = (raw: string): JsJsonType => {\n try {\n return JSON.parse(raw) as JsJsonType;\n } catch {\n console.error('Failed to parse websocket message', raw);\n throw Error(raw);\n }\n};\n\nconst jsJsonToWebSocketWire = (value: JsJsonType): string => {\n return JSON.stringify(value);\n};\n\nconst assertNeverMessage = (data: never): never => {\n console.error(data);\n throw Error('unknown message');\n};\n\ntype CommandType = 'Connected' | 'Disconnected' | {\n 'Message': {\n message: JsJsonType,\n }\n}\nconst wasmCallback = (wasm: ModuleControllerType, callbackId: CallbackId, command: CommandType) => {\n wasm.wasmCommand({\n 'Websocket': {\n callback: callbackId,\n message: command,\n }\n })\n};\n\n\nexport class DriverWebsocket {\n private getWasm: () => ModuleControllerType;\n private readonly controllerList: Map;\n private readonly socket: Map;\n\n constructor(getWasm: () => ModuleControllerType) {\n this.getWasm = getWasm;\n this.controllerList = new Map();\n this.socket = new Map();\n }\n\n public websocket_register_callback = (\n host: string,\n callback_id: CallbackId,\n ) => {\n const wasm = this.getWasm();\n\n let controller = SocketConnection.startSocket(\n host,\n 5000, //timeout connection\n 3000, //timeout reconnection\n (message) => {\n\n if (this.controllerList.has(callback_id) === false) {\n return;\n }\n\n if (message.type === 'socket') {\n this.socket.set(callback_id, message.socket);\n wasmCallback(wasm, callback_id, 'Connected');\n return;\n }\n\n if (message.type === 'message') {\n wasmCallback(wasm, callback_id, {\n 'Message': {\n message: wireStringToJsJson(message.message)\n }\n });\n return;\n }\n\n if (message.type === 'close') {\n this.socket.delete(callback_id);\n wasmCallback(wasm, callback_id, 'Disconnected');\n return;\n }\n\n return assertNeverMessage(message);\n }\n );\n\n this.controllerList.set(callback_id, controller);\n }\n\n public websocket_unregister_callback = (callback_id: CallbackId) => {\n const controller = this.controllerList.get(callback_id);\n\n if (controller === undefined) {\n console.error('Expected controller');\n return;\n }\n\n controller.dispose();\n this.controllerList.delete(callback_id);\n }\n\n public websocket_send_message = (\n callback_id: CallbackId,\n message: JsJsonType,\n ) => {\n const socket = this.socket.get(callback_id);\n\n if (socket === undefined) {\n console.error(`Missing socket connection for callback_id=${callback_id}`);\n } else {\n socket.send(jsJsonToWebSocketWire(message));\n }\n }\n}\n","\nexport const assertNever = (_value: never) => {\n throw Error(\"assert never\");\n}\n","import { JsJsonType } from \"../../jsjson\";\nimport { ModuleControllerType } from \"../../wasm_init\";\nimport { ExportType } from \"../../wasm_module\";\nimport { CallbackId } from \"../types\";\n\nexport interface FetchRequestType {\n method: string,\n url: string,\n headers: Array<{ k: string, v: string }>,\n body: 'None' | {\n Data: {\n data: JsJsonType\n }\n }\n}\n\ntype FetchResponseType = {\n Ok: {\n status: number,\n response: {\n Text: string\n } | {\n Json: JsJsonType,\n }\n }\n} | {\n Err: {\n message: string,\n }\n};\n\nconst getHeaders = (headers: Array<{ k: string, v: string }>): Record => {\n const result: Record = {};\n\n for (const { k, v } of headers) {\n result[k] = v;\n }\n\n return result;\n};\n\nconst getBodyString = (body: FetchRequestType['body']): string | undefined => {\n if (body === 'None') {\n return undefined;\n }\n\n return JSON.stringify(body.Data.data);\n};\n\n// 204/205 carry no body, and any other response with an empty body\n// would crash response.json(). Treat both as Json: null so the caller\n// sees a successful response with the real status code.\nexport const parseJsonBody = (bodyText: string): JsJsonType | null =>\n bodyText.length === 0 ? null : JSON.parse(bodyText);\n\nconst processResponse = async (response: Response): Promise => {\n const status = response.status;\n const contentType = response.headers.get(\"Content-Type\");\n\n try {\n if (contentType?.startsWith('text/plain;')) {\n return {\n Ok: {\n status,\n response: {\n Text: await response.text(),\n }\n }\n }\n }\n\n const json = parseJsonBody(await response.text());\n\n return {\n Ok: {\n status,\n response: {\n Json: json\n }\n }\n };\n } catch (error) {\n return {\n Err: {\n message: String(error),\n }\n };\n }\n};\n\n\nexport const fetchExec = async (\n getWasm: () => ModuleControllerType,\n callback_id: CallbackId,\n request: FetchRequestType\n): Promise => {\n const wasm = getWasm();\n\n try {\n const response = await fetch(request.url, {\n method: request.method,\n headers: getHeaders(request.headers),\n body: getBodyString(request.body),\n });\n\n const response2 = await processResponse(response);\n\n wasm.wasmCommand({\n 'FetchExecResponse': {\n response: response2,\n callback: callback_id,\n }\n });\n\n } catch (err) {\n console.error('fetch error (1)', err);\n const responseMessage = new String(err).toString();\n\n const responseToWasm: FetchResponseType = {\n 'Err': {\n message: responseMessage\n }\n };\n\n wasm.wasmCommand({\n 'FetchExecResponse': {\n response: responseToWasm,\n callback: callback_id,\n }\n });\n }\n};\n\n\n","import { CallbackId } from \"../types\";\nimport { ModuleControllerType } from \"../../wasm_init\";\nimport { ExportType } from \"../../wasm_module\";\n\ntype TimerResourceId = ReturnType;\n\ninterface TimerId {\n kind: 'Interval' | 'Timeout',\n timerId: TimerResourceId,\n}\n\nexport class Interval {\n private readonly getWasm: () => ModuleControllerType;\n private readonly data: Map;\n\n constructor(getWasm: () => ModuleControllerType) {\n this.getWasm = getWasm;\n this.data = new Map();\n }\n\n timerSet = (callback: CallbackId, duration: number, kind: 'Interval' | 'Timeout') => {\n switch (kind) {\n case 'Interval': {\n const timerId = setInterval(() => {\n this.getWasm().wasmCommand({\n 'TimerCall': {\n callback,\n },\n })\n }, duration);\n\n this.data.set(callback, {\n kind: 'Interval',\n timerId,\n });\n break;\n }\n case 'Timeout': {\n const timerId = setTimeout(() => {\n this.getWasm().wasmCommand({\n 'TimerCall': {\n callback,\n },\n })\n }, duration);\n\n this.data.set(callback, {\n kind: 'Timeout',\n timerId,\n });\n break;\n }\n }\n }\n\n timerClear = (callback: CallbackId) => {\n const timerResource = this.data.get(callback);\n\n if (timerResource === undefined) {\n throw Error('panic');\n }\n\n switch (timerResource.kind) {\n case 'Interval': {\n clearInterval(timerResource.timerId);\n break;\n }\n case 'Timeout': {\n clearTimeout(timerResource.timerId);\n break;\n }\n }\n }\n}\n","import { ModuleControllerType } from \"../../wasm_init\";\nimport { ExportType } from \"../../wasm_module\";\nimport { CallbackId } from \"../types\";\nimport { LocationCommonType } from \"./types\";\n\nexport class HashRouter implements LocationCommonType {\n private getWasm: () => ModuleControllerType;\n private callback: Map void>;\n\n constructor(getWasm: () => ModuleControllerType) {\n this.getWasm = getWasm;\n this.callback = new Map();\n\n window.addEventListener(\"hashchange\", this.trigger);\n }\n\n private trigger = () => {\n for (const callback of Array.from(this.callback.values())) {\n callback();\n }\n }\n\n public add = (callback_id: CallbackId) => {\n this.callback.set(callback_id, () => {\n this.getWasm().wasmCommand({\n LocationCall: {\n callback: callback_id,\n value: this.get(),\n }\n });\n });\n }\n\n public remove = (callback_id: CallbackId) => {\n this.callback.delete(callback_id);\n }\n\n public push = (new_hash: string) => {\n if (this.get() === new_hash) {\n return;\n }\n\n location.hash = new_hash;\n this.trigger();\n }\n\n public replace = (new_hash: string) => {\n if (this.get() === new_hash) {\n return;\n }\n\n history.replaceState(null, '', `#${new_hash}`);\n }\n\n public get(): string {\n return decodeURIComponent(location.hash.substr(1));\n }\n}\n","import { ModuleControllerType } from \"../../wasm_init\";\nimport { ExportType } from \"../../wasm_module\";\nimport { CallbackId } from \"../types\";\nimport { LocationCommonType } from \"./types\";\n\nexport class HistoryLocation implements LocationCommonType {\n private getWasm: () => ModuleControllerType;\n private callback: Map void>;\n\n constructor(getWasm: () => ModuleControllerType) {\n this.getWasm = getWasm;\n this.callback = new Map();\n\n window.addEventListener(\"popstate\", this.trigger);\n }\n\n private trigger = () => {\n for (const callback of Array.from(this.callback.values())) {\n callback();\n }\n }\n\n public add = (callback_id: CallbackId) => {\n this.callback.set(callback_id, () => {\n this.getWasm().wasmCommand({\n LocationCall: {\n callback: callback_id,\n value: this.get(),\n }\n });\n });\n }\n\n public remove = (callback_id: CallbackId) => {\n this.callback.delete(callback_id);\n }\n\n public push = (url: string) => {\n if (this.get() === url) {\n return;\n }\n\n window.history.pushState(null, '', url);\n this.trigger();\n }\n\n public replace = (url: string) => {\n if (this.get() === url) {\n return;\n }\n\n window.history.replaceState(null, '', url);\n this.trigger();\n }\n\n public get(): string {\n return window.location.pathname + window.location.search + window.location.hash;\n }\n}\n","import { ModuleControllerType } from \"../../wasm_init\";\nimport { ExportType } from \"../../wasm_module\";\nimport { CallbackId } from \"../types\";\nimport { HashRouter } from \"./hashrouter\";\nimport { HistoryLocation } from \"./historyLocation\";\nimport { LocationCommonType } from \"./types\";\n\ntype LocationTarget = 'Hash' | 'History';\n\nexport class AppLocation {\n private readonly locations: Record;\n\n constructor(getWasm: () => ModuleControllerType) {\n this.locations = {\n Hash: new HashRouter(getWasm),\n History: new HistoryLocation(getWasm),\n };\n }\n\n callback = (target: LocationTarget, mode: 'Add' | 'Remove', callbackId: CallbackId) => {\n switch (mode) {\n case 'Add': {\n this.locations[target].add(callbackId);\n return;\n }\n case 'Remove': {\n this.locations[target].remove(callbackId);\n return;\n }\n }\n }\n\n set = (target: LocationTarget, mode: 'Push' | 'Replace', newValue: string) => {\n switch (mode) {\n case 'Push': {\n this.locations[target].push(newValue);\n return;\n }\n case 'Replace': {\n this.locations[target].replace(newValue);\n return;\n }\n }\n }\n\n get = (target: LocationTarget): string => {\n return this.locations[target].get();\n }\n}","import { JsJsonType } from \"../../jsjson\";\n\nexport class Cookies {\n public get = (cname: string): string => {\n for (const cookie of document.cookie.split(';')) {\n if (cookie === \"\") continue;\n\n const cookieChunk = cookie.trim().split('=');\n\n if (cookieChunk.length !== 2) {\n console.warn(`Cookies.get: Incorrect number of cookieChunk => ${cookieChunk.length} in ${cookie}`);\n continue;\n }\n\n const cookieName = cookieChunk[0];\n const cookieValue = cookieChunk[1];\n\n if (cookieName === undefined || cookieValue === undefined) {\n console.warn(`Cookies.get: Broken cookie part => ${cookie}`);\n continue;\n }\n\n if (cookieName === cname) {\n return decodeURIComponent(cookieValue);\n }\n }\n\n return '';\n }\n\n public getJson = (cname: string): JsJsonType => {\n let cvalue_str = this.get(cname);\n\n if (cvalue_str.length !== 0) {\n try {\n let cookie_value = JSON.parse(cvalue_str);\n return cookie_value;\n } catch (e) {\n console.error!(\"Error deserializing cookie\", e);\n }\n }\n return null\n }\n\n public set = (\n cname: string,\n cvalue: string,\n expires_in: number,\n ) => {\n const cvalueEncoded = cvalue == null ? \"\" : encodeURIComponent(cvalue);\n\n const d = new Date();\n d.setTime(d.getTime() + (expires_in * 1000));\n let expires = \"expires=\" + d.toUTCString();\n\n document.cookie = `${cname}=${cvalueEncoded};${expires};path=/; samesite=Strict`;\n }\n\n public setJson = (\n cname: string,\n cvalue: JsJsonType,\n expires_in: number,\n ) => {\n let cvalue_str = JSON.stringify(cvalue);\n\n this.set(cname, cvalue_str, expires_in);\n }\n}\n","export const getRandom = (min: number, max: number): number => {\n const range = max - min + 1;\n let result = Math.floor(Math.random() * range);\n return min + result;\n};\n\n","import { ExportType } from \"../../../wasm_module\";\nimport { getFiles } from \"./dataTransfer\";\nimport { JsJsonType } from \"../../../jsjson\";\nimport { ModuleControllerType } from \"../../../wasm_init\";\nimport { MapNodes } from \"./map_nodes\";\nimport { CallbackId } from \"../../types\";\n\nexport class CallbackManager {\n private readonly getWasm: () => ModuleControllerType;\n private callbacks: Map void>;\n // IntersectionObserver does not use addEventListener, so its observers are\n // tracked separately (keyed by callback_id) for disconnect on remove.\n private observers: Map;\n\n public constructor(getWasm: () => ModuleControllerType) {\n this.getWasm = getWasm;\n this.callbacks = new Map();\n this.observers = new Map();\n }\n\n public add(nodes: MapNodes, id: number, event_name: string, callback_id: CallbackId) {\n if (event_name === 'intersect') {\n return this.intersectAdd(nodes, id, callback_id);\n }\n\n const callback = (event: Event) => {\n if (event_name === 'click') {\n return this.click(event, callback_id);\n }\n\n if (event_name === 'submit') {\n return this.submit(event, callback_id);\n }\n\n if (event_name === 'input') {\n return this.input(event, callback_id);\n }\n\n if (event_name === 'change') {\n return this.change(event, callback_id);\n }\n\n if (event_name === 'blur') {\n return this.blur(event, callback_id);\n }\n\n if (event_name === 'mousedown') {\n return this.mousedown(event, callback_id);\n }\n\n if (event_name === 'mouseup') {\n return this.mouseup(event, callback_id);\n }\n\n if (event_name === 'mouseenter') {\n return this.mouseenter(event, callback_id);\n }\n\n if (event_name === 'mouseleave') {\n return this.mouseleave(event, callback_id);\n }\n\n if (event_name === 'keydown') {\n return this.keydown(event, callback_id);\n }\n\n if (event_name === 'hook_keydown') {\n return this.keydown(event, callback_id);\n }\n\n if (event_name === 'drop') {\n return this.drop(event, callback_id);\n }\n\n if (event_name === 'load') {\n return this.load(event, callback_id);\n }\n\n if (event_name === 'change_file') {\n return this.changeFile(event, callback_id);\n }\n\n console.error(`No support for the event ${event_name}`);\n };\n\n if (this.callbacks.has(callback_id)) {\n console.error(`There was already a callback added with the callback_id=${callback_id}`);\n return;\n }\n\n this.callbacks.set(callback_id, callback);\n\n if (event_name === 'hook_keydown') {\n document.addEventListener('keydown', callback, false);\n } else {\n const node = nodes.get('callback_add', id);\n const domEventName = event_name === 'change_file' ? 'change' : event_name;\n node.addEventListener(domEventName, callback, false);\n }\n }\n\n public remove(nodes: MapNodes, id: number, event_name: string, callback_id: CallbackId) {\n if (event_name === 'intersect') {\n return this.intersectRemove(callback_id);\n }\n\n const callback = this.callbacks.get(callback_id);\n this.callbacks.delete(callback_id);\n\n if (callback === undefined) {\n console.error(`The callback is missing with the id=${callback_id}`);\n return;\n }\n\n if (event_name === 'hook_keydown') {\n document.removeEventListener('keydown', callback);\n } else {\n const node = nodes.get('callback_remove', id);\n const domEventName = event_name === 'change_file' ? 'change' : event_name;\n node.removeEventListener(domEventName, callback);\n }\n }\n\n private wasmCallback(callback_id: CallbackId, value: JsJsonType): JsJsonType {\n return this.getWasm().wasmCommand({\n CallbackCall: {\n callback_id,\n value: value\n }\n });\n }\n\n private intersectAdd(nodes: MapNodes, id: number, callback_id: CallbackId) {\n if (this.observers.has(callback_id)) {\n console.error(`There was already an intersect observer added with the callback_id=${callback_id}`);\n return;\n }\n\n const node = nodes.getNode('callback_add', id);\n\n const observer = new IntersectionObserver((entries) => {\n for (const entry of entries) {\n // Payload order MUST match the Rust decoder get_intersection_event.\n this.wasmCallback(callback_id, [\n entry.isIntersecting,\n entry.intersectionRatio,\n entry.boundingClientRect.top,\n entry.boundingClientRect.bottom,\n entry.boundingClientRect.height,\n ]);\n }\n });\n\n observer.observe(node);\n this.observers.set(callback_id, observer);\n }\n\n private intersectRemove(callback_id: CallbackId) {\n const observer = this.observers.get(callback_id);\n this.observers.delete(callback_id);\n\n if (observer === undefined) {\n console.error(`The intersect observer is missing with the id=${callback_id}`);\n return;\n }\n\n observer.disconnect();\n }\n\n private click(event: Event, callback_id: CallbackId) {\n event.preventDefault();\n let click_event = this.wasmCallback(callback_id, undefined);\n\n // Check if click_event is an object (JsJson Object type)\n if (click_event !== null && typeof click_event === 'object' && !Array.isArray(click_event)) {\n if ('stop_propagation' in click_event && click_event['stop_propagation'] === true) {\n event.stopPropagation();\n }\n if ('prevent_default' in click_event && click_event['prevent_default'] === true) {\n event.preventDefault();\n }\n }\n }\n\n private submit(event: Event, callback_id: CallbackId) {\n event.preventDefault();\n this.wasmCallback(callback_id, undefined);\n }\n\n private input(event: Event, callback_id: CallbackId) {\n const target = event.target;\n\n if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement) {\n this.wasmCallback(callback_id, target.value);\n return;\n }\n\n console.warn('event input ignore', target);\n }\n\n private change(event: Event, callback_id: CallbackId) {\n const target = event.target;\n\n if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement || target instanceof HTMLSelectElement) {\n this.wasmCallback(callback_id, target.value);\n return;\n }\n\n console.warn('event input ignore', target);\n }\n\n private changeFile(event: Event, callback_id: CallbackId) {\n const target = event.target;\n\n if (target instanceof HTMLInputElement && target.files !== null && target.files.length > 0) {\n const promises: Array> = [];\n\n for (let i = 0; i < target.files.length; i++) {\n const file = target.files[i];\n if (file !== undefined) {\n promises.push(\n file.arrayBuffer().then((buf) => ({\n name: file.name,\n data: new Uint8Array(buf),\n }))\n );\n }\n }\n\n if (promises.length > 0) {\n Promise.all(promises).then((files) => {\n const params = [];\n for (const f of files) {\n params.push([f.name, Array.from(f.data)]);\n }\n this.wasmCallback(callback_id, [params]);\n }).catch((err) => console.error('changeFile ->', err));\n }\n\n target.value = '';\n return;\n }\n\n console.warn('changeFile: not a file input or no files', target);\n }\n\n private blur(_event: Event, callback_id: CallbackId) {\n this.wasmCallback(callback_id, undefined);\n }\n\n private mousedown(event: Event, callback_id: CallbackId) {\n if (this.wasmCallback(callback_id, undefined)) {\n event.preventDefault()\n }\n }\n\n private mouseup(event: Event, callback_id: CallbackId) {\n if (this.wasmCallback(callback_id, undefined)) {\n event.preventDefault()\n }\n }\n\n private mouseenter(_event: Event, callback_id: CallbackId) {\n this.wasmCallback(callback_id, undefined);\n }\n\n private mouseleave(_event: Event, callback_id: CallbackId) {\n this.wasmCallback(callback_id, undefined);\n }\n\n private drop(event: Event, callback_id: CallbackId) {\n event.preventDefault();\n\n if (event instanceof DragEvent) {\n if (event.dataTransfer === null) {\n console.error('dom -> drop -> dataTransfer null');\n } else {\n const files = getFiles(event.dataTransfer.items);\n\n if (files.length) {\n Promise.all(files).then((files) => {\n const params = [];\n\n for (const file of files) {\n // Convert Uint8Array to array of numbers for JsJson\n const dataArray = Array.from(file.data);\n params.push([\n file.name,\n dataArray,\n ]);\n }\n\n this.wasmCallback(callback_id, [params]);\n }).catch((error) => {\n console.error('callback_drop -> promise.all -> ', error);\n });\n } else {\n console.error('No files to send');\n }\n }\n } else {\n console.warn('event drop ignore', event);\n }\n }\n\n private keydown(event: Event, callback_id: CallbackId) {\n if (event instanceof KeyboardEvent) {\n const result = this.wasmCallback(callback_id, [\n event.key,\n event.code,\n event.altKey,\n event.ctrlKey,\n event.shiftKey,\n event.metaKey\n ]);\n\n if (result === true) {\n event.preventDefault();\n event.stopPropagation();\n }\n\n return;\n }\n\n console.warn('keydown ignore', event);\n }\n\n private load(event: Event, callback_id: CallbackId) {\n event.preventDefault();\n this.wasmCallback(callback_id, undefined);\n }\n\n}\n","interface FileItemType {\n name: string,\n data: Uint8Array,\n}\n\nexport function getFiles(items: DataTransferItemList): Array> {\n const files: Array> = [];\n\n for (let i = 0; i < items.length; i++) {\n const item = items[i];\n\n if (item === undefined) {\n console.error('dom -> drop -> item - undefined');\n } else {\n const file = item.getAsFile();\n\n if (file === null) {\n console.error(`dom -> drop -> index:${i} -> It's not a file`);\n } else {\n files.push(file\n .arrayBuffer()\n .then((data): FileItemType => ({\n name: file.name,\n data: new Uint8Array(data),\n }))\n );\n }\n }\n }\n return files;\n}\n","import { AppLocation } from \"../../location/AppLocation\";\n\nexport function injects(node: Element, appLocation: AppLocation) {\n if (node.tagName.toLocaleLowerCase() === 'a') {\n hydrateLink(node, appLocation);\n }\n}\n\nfunction hydrateLink(node: Element, appLocation: AppLocation) {\n node.addEventListener('click', (e) => {\n let href = node.getAttribute('href');\n if (href === null) {\n return;\n }\n\n if (href.startsWith('#') || href.startsWith('http://') || href.startsWith('https://') || href.startsWith('//')) {\n return;\n }\n\n e.preventDefault();\n appLocation.set('History', 'Push', href);\n window.scrollTo(0, 0);\n })\n}\n","import { AppLocation } from \"../../location/AppLocation\";\nimport { CommandType } from \"./dom\";\nimport { injects } from \"./injects\";\nimport { MapNodes } from \"./map_nodes\";\n\ninterface VirtualNode {\n id: number;\n name?: string;\n value?: string;\n attributes?: Map;\n children: Array;\n}\n\nexport const hydrate = (commands: Array, nodes: MapNodes, appLocation: AppLocation) => {\n const engine = new HydrationEngine(commands, nodes, appLocation);\n engine.hydrate();\n};\n\nclass HydrationEngine {\n private nodes: MapNodes;\n private appLocation: AppLocation;\n private virtualNodes: Map;\n private depth: number = -1;\n private matched: number = 0;\n\n constructor(commands: Array, nodes: MapNodes, appLocation: AppLocation) {\n this.nodes = nodes;\n this.appLocation = appLocation;\n this.virtualNodes = this.createVirtualNodes(commands);\n }\n\n public hydrate() {\n // Start hydration from Body (id=3) and Head (id=2) if needed\n // Usually we care about Body.\n const bodyVNode = this.virtualNodes.get(3);\n if (bodyVNode) {\n this.hydrateNode(3, document.body);\n }\n\n const headVNode = this.virtualNodes.get(2);\n if (headVNode) {\n this.hydrateNode(2, document.head);\n }\n\n console.log(\n \"Hydration complete,\",\n (this.matched * 100 / this.virtualNodes.size).toFixed(2),\n \" % vnodes matched.\",\n );\n };\n\n // Traverse and Match\n private hydrateNode(vNodeId: number, realNode: Node) {\n const vNode = this.virtualNodes.get(vNodeId);\n if (!vNode) return;\n\n // console.log(`Hydration ${this.depth + 1}: Hydrate node`, vNode, realNode);\n\n // Match children\n const realChildren = Array.from(realNode.childNodes);\n let realIndex = 0;\n this.depth++;\n let skipTextVNodes = false;\n\n for (const childVId of vNode.children) {\n const childVNode = this.virtualNodes.get(childVId);\n if (!childVNode) continue;\n\n // If we are in group of text vnodes, skip them until we find a non-text vnode.\n if (skipTextVNodes && childVNode.value !== undefined) {\n // Deliberately skipped vNodes should be counted as matched\n this.matched++;\n continue;\n } else {\n skipTextVNodes = false;\n }\n\n // Find a matching real node starting from realIndex\n for (let i = realIndex; i < realChildren.length; i++) {\n const candidate = realChildren[i];\n if (!candidate) continue;\n\n let isMatch = false;\n if (childVNode.name) {\n // Element\n isMatch = this.checkElementMatch(candidate, childVNode);\n } else if (childVNode.value !== undefined) {\n // Text\n if (candidate.nodeType === Node.TEXT_NODE) {\n this.checkTextMatch(candidate, childVNode);\n isMatch = true;\n // Start skipping eventual group of text vnodes\n // as they were probably merged into one on SSR side.\n skipTextVNodes = true;\n } else {\n console.error(`Hydration ${this.depth}: Text node mismatch`, childVNode, candidate);\n }\n }\n\n if (isMatch) {\n this.removeSkippedNodes(realChildren, realIndex, i);\n this.claimNode(candidate, childVId);\n this.matched++;\n\n // Recurse if element\n if (childVNode.name) {\n this.hydrateNode(childVId, candidate);\n }\n\n // Advance realIndex to i + 1 (consume this node)\n realIndex = i + 1;\n break;\n }\n }\n }\n\n // Remove remaining real nodes\n this.removeSkippedNodes(realChildren, realIndex, realChildren.length);\n this.depth--;\n };\n\n private checkElementMatch(candidate: Node, childVNode: VirtualNode) {\n let isMatch = false;\n if (candidate.nodeType === Node.ELEMENT_NODE && (candidate as Element).tagName === childVNode.name) {\n isMatch = true;\n // Check attributes\n if (childVNode.attributes) {\n const element = candidate as Element;\n for (const [name, value] of childVNode.attributes) {\n if (element.getAttribute(name) !== value) {\n // console.info(`Hydration ${depth}: Reseting attribute`, element.getAttribute(name), \" !== \", value);\n element.setAttribute(name, value);\n }\n }\n }\n }\n return isMatch;\n };\n\n private checkTextMatch(candidate: Node, childVNode: VirtualNode) {\n // For text nodes, we might want to be lenient or exact.\n // Let's assume exact match or at least non-empty.\n // Often text nodes might have whitespace differences.\n // For now, let's just check if it's a text node.\n // Checking content might be safer.\n if (candidate.textContent?.replace('\\n', ' ').trim() !== childVNode.value?.replace('\\n', ' ').trim()) {\n // console.debug(`Hydration ${depth}: Joint text`, childVNode, candidate);\n candidate.textContent = childVNode.value || \"\";\n }\n };\n\n // Claim node and run injects\n private claimNode(candidate: Node, childVId: number) {\n if (candidate instanceof Element || candidate instanceof Comment || candidate instanceof Text) {\n this.nodes.claimNode(childVId, candidate);\n\n // Run injects\n if (candidate instanceof Element) {\n injects(candidate, this.appLocation);\n }\n }\n }\n\n // Remove nodes skipped during matching\n private removeSkippedNodes(realChildren: ChildNode[], realIndex: number, i: number) {\n for (let j = realIndex; j < i; j++) {\n const nodeToRemove = realChildren[j];\n if (nodeToRemove) {\n if (this.depth !== 0 && nodeToRemove.nodeType !== Node.TEXT_NODE) {\n console.warn(`Hydration ${this.depth}: Removing node`, nodeToRemove);\n }\n nodeToRemove.remove();\n }\n }\n }\n\n private createVirtualNodes(commands: Array): Map {\n const virtualNodes = new Map();\n\n // Helper to get or create a virtual node\n const getVNode = (id: number): VirtualNode => {\n let node = virtualNodes.get(id);\n if (!node) {\n node = { id, children: [] };\n virtualNodes.set(id, node);\n }\n return node;\n };\n\n // Build Virtual Tree from Commands\n for (const command of commands) {\n if ('CreateNode' in command) {\n const node = getVNode(command.CreateNode.id);\n node.name = command.CreateNode.name.toUpperCase();\n } else if ('CreateText' in command) {\n const node = getVNode(command.CreateText.id);\n node.value = command.CreateText.value;\n } else if ('InsertBefore' in command) {\n const parent = getVNode(command.InsertBefore.parent);\n const childId = command.InsertBefore.child;\n const refId = command.InsertBefore.ref_id;\n\n if (refId === null || refId === undefined) {\n parent.children.push(childId);\n } else {\n const index = parent.children.indexOf(refId);\n if (index !== -1) {\n parent.children.splice(index, 0, childId);\n } else {\n console.warn(`Hydration: ref_id ${refId} not found in parent ${command.InsertBefore.parent}`);\n parent.children.push(childId);\n }\n }\n } else if ('SetAttr' in command) {\n const node = getVNode(command.SetAttr.id);\n if (!node.attributes) {\n node.attributes = new Map();\n }\n node.attributes.set(command.SetAttr.name, command.SetAttr.value);\n }\n }\n\n return virtualNodes;\n };\n}\n","type NodeType = Element | Comment | Text;\nexport class MapNodes {\n private data: Map;\n private initNodes: Array | null;\n private style: HTMLStyleElement;\n\n constructor() {\n this.data = new Map();\n\n this.initNodes = [\n ...this.getRootHead().childNodes,\n ...this.getRootBody().childNodes,\n ];\n\n this.style = document.createElement('style');\n }\n\n private getRootHtml(): Element {\n return document.documentElement;\n }\n\n private getRootHead(): Element {\n return document.head;\n }\n\n private getRootBody(): Element {\n return document.body;\n }\n\n public set(id: number, value: NodeType) {\n if (id === 1 || id === 2 || id === 3) {\n //ignore\n } else {\n this.data.set(id, value);\n }\n }\n\n public getAnyOption(id: number): NodeType | undefined {\n if (id === 1) {\n return this.getRootHtml();\n }\n\n if (id === 2) {\n return this.getRootHead();\n }\n\n if (id === 3) {\n return this.getRootBody();\n }\n\n return this.data.get(id);\n }\n\n public getAny(label: string, id: number): NodeType {\n const item = this.getAnyOption(id);\n\n if (item === undefined) {\n throw Error(`${label} -> item not found=${id}`);\n }\n\n return item;\n }\n\n public get(label: string, id: number): NodeType {\n const item = this.getAnyOption(id);\n\n if (item === undefined) {\n throw new Error(`${label}->get: Item id not found = ${id}`);\n }\n return item;\n }\n\n public getNodeElement(label: string, id: number): HTMLElement {\n const node = this.get(label, id);\n if (node instanceof HTMLElement) {\n return node;\n } else {\n throw Error(`Expected id=${id} as HTMLElement`);\n }\n }\n\n public getNode(label: string, id: number): Element {\n const node = this.get(label, id);\n if (node instanceof Element) {\n return node;\n } else {\n throw Error(`Expected id=${id} as Element`);\n }\n }\n\n public getText(label: string, id: number): Text {\n const node = this.get(label, id);\n if (node instanceof Text) {\n return node;\n } else {\n throw Error(`Expected id=${id} as Text`);\n }\n }\n\n public getComment(label: string, id: number): Comment {\n const node = this.get(label, id);\n if (node instanceof Comment) {\n return node;\n } else {\n throw Error(`Expected id=${id} as Comment`);\n }\n }\n\n public delete(label: string, id: number): NodeType {\n const item = this.getAnyOption(id);\n this.data.delete(id);\n\n if (item === undefined) {\n throw new Error(`${label}->delete: Item id not found = ${id}`);\n }\n\n return item;\n }\n\n public insertCss(selector: string | null, value: string) {\n if (selector !== null) {\n // Add autocss styles\n const content = document.createTextNode(`\\n${selector} { ${value} }`);\n this.style.appendChild(content);\n } else {\n // Add bundle (i.e. a tailwind bundle)\n const content = document.createTextNode(`\\n${value}`);\n this.style.appendChild(content);\n }\n }\n\n public removeInitNodes() {\n const initNodes = this.initNodes;\n this.initNodes = null;\n\n if (initNodes === null) {\n return;\n }\n\n for (const node of initNodes) {\n node.remove();\n }\n }\n\n public insertBefore(parent: number, child: number, ref_id: number | null | undefined) {\n const parentNode = this.get(\"insert_before\", parent);\n const childNode = this.getAny(\"insert_before child\", child);\n\n if (ref_id === null || ref_id === undefined) {\n parentNode.insertBefore(childNode, null);\n } else {\n const ref_node = this.getAny('insert_before ref', ref_id);\n parentNode.insertBefore(childNode, ref_node);\n }\n }\n\n public addStyles() {\n this.getRootHead().appendChild(this.style);\n }\n\n public hasInitNodes(): boolean {\n return this.initNodes !== null;\n }\n\n public claimNode(id: number, node: NodeType) {\n this.data.set(id, node);\n\n if (this.initNodes) {\n const index = this.initNodes.indexOf(node as ChildNode);\n if (index > -1) {\n this.initNodes.splice(index, 1);\n }\n }\n }\n\n public has(id: number): boolean {\n // Root nodes always exist in real DOM\n if (id === 1 || id === 2 || id === 3) {\n return true;\n }\n\n return this.data.has(id);\n }\n}\n","import { AppLocation } from \"../../location/AppLocation\";\nimport { CallbackManager } from \"./callbackManager\";\nimport { ExportType } from \"../../../wasm_module\";\nimport { hydrate } from \"./hydration\";\nimport { injects } from \"./injects\";\nimport { MapNodes } from \"./map_nodes\";\nimport { ModuleControllerType } from \"../../../wasm_init\";\nimport { Metadata } from \"../../metadata\";\n\n// Workaround, remove when https://github.com/vertigo-web/vertigo/issues/539 is done.\nconst SVG_TAGS = new Set([\n \"animate\", \"animateMotion\", \"animateTransform\", \"circle\", \"clipPath\", \"defs\",\n \"desc\", \"discard\", \"ellipse\", \"feBlend\", \"feColorMatrix\", \"feComponentTransfer\",\n \"feComposite\", \"feConvolveMatrix\", \"feDiffuseLighting\", \"feDisplacementMap\",\n \"feDistantLight\", \"feDropShadow\", \"feFlood\", \"feFuncA\", \"feFuncB\", \"feFuncG\",\n \"feFuncR\", \"feGaussianBlur\", \"feImage\", \"feMerge\", \"feMergeNode\", \"feMorphology\",\n \"feOffset\", \"fePointLight\", \"feSpecularLighting\", \"feSpotLight\", \"feTile\",\n \"feTurbulence\", \"filter\", \"foreignObject\", \"g\", \"hatch\", \"hatchpath\", \"image\",\n \"line\", \"linearGradient\", \"marker\", \"mask\", \"metadata\", \"mpath\", \"path\", \"pattern\",\n \"polygon\", \"polyline\", \"radialGradient\", \"rect\", \"set\", \"stop\", \"svg\", \"switch\",\n \"symbol\", \"text\", \"textPath\", \"tspan\", \"use\", \"view\",\n \"svg:a\", \"svg:title\", \"svg:desc\", \"svg:script\", \"svg:style\"\n]);\n\nconst createElement = (name: string): Element => {\n if (SVG_TAGS.has(name)) {\n return document.createElementNS(\"http://www.w3.org/2000/svg\", name.replace(\"svg:\", \"\"));\n } else {\n return document.createElement(name);\n }\n}\n\nexport type CommandType = {\n CreateNode: {\n id: number,\n name: string,\n }\n} | {\n CreateText: {\n id: number,\n value: string\n }\n} | {\n UpdateText: {\n id: number,\n value: string\n }\n} | {\n SetAttr: {\n id: number,\n name: string,\n value: string\n }\n} | {\n RemoveAttr: {\n id: number,\n name: string\n }\n} | {\n RemoveNode: {\n id: number,\n }\n} | {\n RemoveText: {\n id: number,\n }\n} | {\n InsertBefore: {\n parent: number,\n child: number,\n ref_id: number | null,\n }\n} | {\n InsertCss: {\n selector: string | null,\n value: string\n }\n} | {\n CreateComment: {\n id: number,\n value: string\n }\n} | {\n RemoveComment: {\n id: number,\n }\n} | {\n CallbackAdd: {\n id: number,\n event_name: string,\n callback_id: number,\n }\n} | {\n CallbackRemove: {\n id: number,\n event_name: string,\n callback_id: number,\n }\n};\n\nconst assertNeverCommand = (data: never): never => {\n console.error(data);\n throw Error('unknown command');\n};\n\nexport class DriverDom {\n private appLocation: AppLocation;\n public readonly nodes: MapNodes;\n private readonly callbacks: CallbackManager;\n\n public constructor(private readonly metadata: Metadata, appLocation: AppLocation, getWasm: () => ModuleControllerType) {\n this.appLocation = appLocation;\n this.nodes = new MapNodes();\n this.callbacks = new CallbackManager(getWasm);\n\n document.addEventListener('dragover', (ev): void => {\n // console.log('File(s) in drop zone');\n ev.preventDefault();\n });\n }\n\n public update = (commands: Array) => {\n if (this.nodes.hasInitNodes() && this.metadata.getEnabledHydration()) {\n hydrate(commands, this.nodes, this.appLocation);\n }\n\n const setFocus: Set = new Set();\n\n for (const command of commands) {\n try {\n this.runCommand(command);\n } catch (error) {\n console.error('bulk_update - item', error, command);\n }\n\n if ('SetAttr' in command && command.SetAttr.name.toLocaleLowerCase() === 'autofocus') {\n setFocus.add(command.SetAttr.id);\n }\n }\n\n if (setFocus.size > 0) {\n setTimeout(() => {\n for (const id of setFocus) {\n const node = this.nodes.getNodeElement(`set focus ${id}`, id);\n node.focus();\n }\n }, 0);\n }\n\n this.nodes.removeInitNodes();\n\n // Make sure that the client-side generated styles are always the last element of the head\n this.nodes.addStyles();\n }\n\n private createNode(id: number, name: string) {\n // Root nodes (html/head/body) already exist in the real DOM\n if (id === 1 || id === 2 || id === 3) {\n return;\n }\n\n if (this.nodes.has(id)) {\n return;\n }\n\n const node = createElement(name);\n this.nodes.set(id, node);\n\n injects(node, this.appLocation);\n }\n\n private setAttr(id: number, name: string, value: string) {\n const node = this.nodes.getNode(\"set_attribute\", id);\n node.setAttribute(name, value);\n\n if (name == \"value\") {\n if (node instanceof HTMLInputElement) {\n node.value = value;\n return;\n }\n\n if (node instanceof HTMLTextAreaElement) {\n node.value = value;\n node.defaultValue = value;\n return;\n }\n }\n }\n\n private removeAttr(id: number, name: string) {\n const node = this.nodes.getNode(\"remove_attribute\", id);\n node.removeAttribute(name);\n\n if (name == \"value\") {\n if (node instanceof HTMLInputElement) {\n node.value = \"\";\n return;\n }\n\n if (node instanceof HTMLTextAreaElement) {\n node.value = \"\";\n node.defaultValue = \"\";\n return;\n }\n }\n }\n\n private removeNode(id: number) {\n // Never remove real document roots\n if (id === 1 || id === 2 || id === 3) {\n return;\n }\n\n const node = this.nodes.delete(\"remove_node\", id);\n node.remove();\n }\n\n private createText(id: number, value: string) {\n if (this.nodes.has(id)) {\n return;\n }\n\n const text = document.createTextNode(value);\n this.nodes.set(id, text);\n }\n\n private removeText(id: number) {\n const text = this.nodes.delete(\"remove_node\", id);\n text.remove();\n }\n\n private updateText(id: number, value: string) {\n const text = this.nodes.getText(\"set_attribute\", id);\n text.textContent = value;\n }\n\n private runCommand(command: CommandType) {\n if ('RemoveNode' in command) {\n this.removeNode(command.RemoveNode.id);\n return;\n }\n\n if ('InsertBefore' in command) {\n this.nodes.insertBefore(command.InsertBefore.parent, command.InsertBefore.child, command.InsertBefore.ref_id === null ? null : command.InsertBefore.ref_id);\n return;\n }\n\n if ('CreateNode' in command) {\n this.createNode(command.CreateNode.id, command.CreateNode.name);\n return;\n }\n\n if ('CreateText' in command) {\n this.createText(command.CreateText.id, command.CreateText.value);\n return;\n }\n\n if ('UpdateText' in command) {\n this.updateText(command.UpdateText.id, command.UpdateText.value);\n return;\n }\n\n if ('SetAttr' in command) {\n this.setAttr(command.SetAttr.id, command.SetAttr.name, command.SetAttr.value);\n return;\n }\n\n if ('RemoveAttr' in command) {\n this.removeAttr(command.RemoveAttr.id, command.RemoveAttr.name);\n return;\n }\n\n if ('RemoveText' in command) {\n this.removeText(command.RemoveText.id);\n return;\n }\n\n if ('InsertCss' in command) {\n this.nodes.insertCss(command.InsertCss.selector, command.InsertCss.value);\n return;\n }\n\n if ('CreateComment' in command) {\n const comment = document.createComment(command.CreateComment.value);\n this.nodes.set(command.CreateComment.id, comment);\n return;\n }\n\n if ('RemoveComment' in command) {\n const comment = this.nodes.delete(\"remove_comment\", command.RemoveComment.id);\n comment.remove();\n return;\n }\n\n if ('CallbackAdd' in command) {\n this.callbacks.add(this.nodes, command.CallbackAdd.id, command.CallbackAdd.event_name, command.CallbackAdd.callback_id);\n return;\n }\n\n if ('CallbackRemove' in command) {\n this.callbacks.remove(this.nodes, command.CallbackRemove.id, command.CallbackRemove.event_name, command.CallbackRemove.callback_id);\n return;\n }\n\n return assertNeverCommand(command);\n }\n}\n","import { DriverWebsocket } from \"./websocket/websocket\";\nimport { assertNever } from \"../assert_never\";\nimport { JsJsonType } from \"../jsjson\";\nimport { ModuleControllerType } from \"../wasm_init\";\nimport { ExportType } from \"../wasm_module\";\nimport { fetchCacheGet } from \"./command/fetchCacheGet\";\nimport { fetchExec, FetchRequestType } from \"./command/fetchExec\";\nimport { CallbackId } from \"./types\";\nimport { Interval } from \"./command/interval\";\nimport { AppLocation } from './location/AppLocation';\nimport { Cookies } from \"./command/cookies\";\nimport { getRandom } from \"./command/getRandom\";\nimport { CommandType, DriverDom } from \"./command/dom/dom\";\nimport { Metadata } from \"./metadata\";\n\ntype JsApiCommandType =\n | { Root: { name: string } }\n | { RootElement: { dom_id: number } }\n | { Get: { property: string } }\n | { Set: { property: string, value: JsJsonType } }\n | { Call: { method: string, args: JsJsonType[] } };\n\ntype ExecType\n = 'FetchCacheGet'\n | 'IsBrowser'\n | 'GetDateNow'\n | 'TimezoneOffset'\n | 'HistoryBack'\n | {\n FetchExec: {\n callback: CallbackId,\n request: FetchRequestType,\n }\n }\n | {\n WebsocketRegister: {\n callback: CallbackId,\n host: string\n }\n }\n | {\n WebsocketSendMessage: {\n callback: CallbackId,\n message: JsJsonType,\n }\n }\n | {\n WebsocketUnregister: {\n callback: CallbackId,\n }\n }\n | {\n TimerSet: {\n callback: CallbackId,\n duration: number,\n kind: 'Interval' | 'Timeout',\n }\n }\n | {\n TimerClear: {\n callback: CallbackId,\n }\n }\n | {\n LocationGet: {\n target: 'Hash' | 'History',\n }\n }\n | {\n LocationCallback: {\n callback: CallbackId,\n mode: 'Add' | 'Remove',\n target: 'Hash' | 'History'\n }\n }\n | {\n LocationSet: {\n mode: 'Push' | 'Replace',\n target: 'Hash' | 'History'\n value: string\n }\n }\n | {\n CookieSet: {\n name: string,\n value: string,\n expires_in: number,\n }\n }\n | {\n CookieGet: {\n name: string,\n }\n }\n | {\n CookieJsonSet: {\n name: string,\n value: JsJsonType,\n expires_in: number,\n }\n }\n | {\n CookieJsonGet: {\n name: string,\n }\n }\n | {\n GetEnv: {\n name: string\n }\n }\n | {\n Log: {\n arg2: string, //\"color: white; padding: 0 3px; background: green;\",\n arg3: string, //\"font-weight: bold; color: inherit\",\n arg4: string, //\"background: inherit; color: inherit\",\n kind: 'Debug' | 'Info' | 'Log' | 'Warn' | 'Error',\n message: string, //\"%cINFO%c crates/vertigo/src/driver_module/api/api_fetch_cache.rs:26%c FetchCache ready\"\n }\n }\n | {\n GetRandom: {\n min: number,\n max: number,\n }\n }\n | {\n JsApiCall: {\n commands: Array\n }\n }\n | {\n DomBulkUpdate: {\n list: Array\n }\n };\n\nexport class Api {\n public readonly dom: DriverDom;\n private readonly websocket: DriverWebsocket;\n private readonly interval: Interval;\n private readonly location: AppLocation;\n private readonly cookie: Cookies;\n\n\n constructor(private readonly metadata: Metadata, private readonly getWasm: () => ModuleControllerType) {\n const appLocation = new AppLocation(getWasm);\n\n this.dom = new DriverDom(metadata, appLocation, getWasm);\n this.websocket = new DriverWebsocket(getWasm);\n this.interval = new Interval(getWasm);\n this.location = appLocation;\n this.cookie = new Cookies();\n }\n\n exec(arg: JsJsonType): JsJsonType {\n\n //@ts-expect-error - //TODO Add safe type checking\n const safeArg: ExecType = arg;\n\n // console.info('exec arg', safeArg);\n\n if (safeArg === 'FetchCacheGet') {\n return fetchCacheGet(this.metadata);\n }\n\n if (safeArg === 'IsBrowser') {\n return {\n value: true\n };\n }\n\n if (safeArg === 'GetDateNow') {\n return {\n value: Date.now(),\n };\n }\n\n if (safeArg === 'TimezoneOffset') {\n return {\n value: new Date().getTimezoneOffset()\n };\n }\n\n if (safeArg === 'HistoryBack') {\n window.history.back();\n return null;\n }\n\n if ('FetchExec' in safeArg) {\n fetchExec(this.getWasm, safeArg.FetchExec.callback, safeArg.FetchExec.request);\n return null;\n }\n\n if ('WebsocketRegister' in safeArg) {\n this.websocket.websocket_register_callback(safeArg.WebsocketRegister.host, safeArg.WebsocketRegister.callback);\n return null;\n }\n\n if ('WebsocketSendMessage' in safeArg) {\n this.websocket.websocket_send_message(safeArg.WebsocketSendMessage.callback, safeArg.WebsocketSendMessage.message);\n return null;\n }\n\n if ('WebsocketUnregister' in safeArg) {\n this.websocket.websocket_unregister_callback(safeArg.WebsocketUnregister.callback);\n return null;\n }\n\n if ('TimerSet' in safeArg) {\n this.interval.timerSet(safeArg.TimerSet.callback, safeArg.TimerSet.duration, safeArg.TimerSet.kind);\n return null;\n }\n\n if ('TimerClear' in safeArg) {\n this.interval.timerClear(safeArg.TimerClear.callback);\n return null;\n }\n\n if ('LocationGet' in safeArg) {\n return {\n value: this.location.get(safeArg.LocationGet.target)\n };\n }\n\n if ('LocationCallback' in safeArg) {\n this.location.callback(safeArg.LocationCallback.target, safeArg.LocationCallback.mode, safeArg.LocationCallback.callback);\n return null;\n }\n\n if ('LocationSet' in safeArg) {\n this.location.set(safeArg.LocationSet.target, safeArg.LocationSet.mode, safeArg.LocationSet.value);\n return null;\n }\n\n if ('CookieGet' in safeArg) {\n return {\n value: this.cookie.get(safeArg.CookieGet.name)\n };\n }\n\n if ('CookieSet' in safeArg) {\n this.cookie.set(safeArg.CookieSet.name, safeArg.CookieSet.value, safeArg.CookieSet.expires_in);\n return null;\n }\n\n if ('CookieJsonGet' in safeArg) {\n return {\n value: this.cookie.getJson(safeArg.CookieJsonGet.name)\n };\n }\n\n if ('CookieJsonSet' in safeArg) {\n this.cookie.setJson(safeArg.CookieJsonSet.name, safeArg.CookieJsonSet.value, safeArg.CookieJsonSet.expires_in);\n return null;\n }\n\n if ('GetEnv' in safeArg) {\n const name = safeArg.GetEnv.name;\n\n return {\n value: this.metadata.getEnv(name),\n }\n }\n\n if ('Log' in safeArg) {\n switch (safeArg.Log.kind) {\n case 'Info': {\n console.info(safeArg.Log.message, safeArg.Log.arg2, safeArg.Log.arg3, safeArg.Log.arg4);\n return null;\n }\n case 'Debug': {\n console.debug(safeArg.Log.message, safeArg.Log.arg2, safeArg.Log.arg3, safeArg.Log.arg4);\n return null;\n }\n case 'Error': {\n console.error(safeArg.Log.message, safeArg.Log.arg2, safeArg.Log.arg3, safeArg.Log.arg4);\n return null;\n }\n case 'Log': {\n console.log(safeArg.Log.message, safeArg.Log.arg2, safeArg.Log.arg3, safeArg.Log.arg4);\n return null;\n }\n case 'Warn': {\n console.warn(safeArg.Log.message, safeArg.Log.arg2, safeArg.Log.arg3, safeArg.Log.arg4);\n return null;\n }\n }\n }\n\n if ('GetRandom' in safeArg) {\n return {\n value: getRandom(safeArg.GetRandom.min, safeArg.GetRandom.max)\n };\n }\n\n if ('JsApiCall' in safeArg) {\n return this.executeJsApiCall(safeArg.JsApiCall.commands);\n }\n\n if ('DomBulkUpdate' in safeArg) {\n this.dom.update(safeArg.DomBulkUpdate.list);\n return null;\n }\n\n console.info('exec_command: Arg', safeArg);\n return assertNever(safeArg);\n }\n\n private executeJsApiCall(commands: Array): JsJsonType {\n let current: any = null;\n\n for (const command of commands) {\n if ('Root' in command) {\n if (command.Root.name === 'window') {\n current = window;\n } else if (command.Root.name === 'document') {\n current = document;\n } else {\n console.error(`Unknown root: ${command.Root.name}`);\n return null;\n }\n } else if ('RootElement' in command) {\n const domId = command.RootElement.dom_id;\n const node = this.dom.nodes.getAnyOption(domId);\n if (node === undefined) {\n console.error(`Element not found: ${domId}`);\n return null;\n }\n current = node;\n } else if ('Get' in command) {\n if (current === null) {\n console.error('Get called on null');\n return null;\n }\n current = current[command.Get.property];\n } else if ('Set' in command) {\n if (current === null) {\n console.error('Set called on null');\n return null;\n }\n current[command.Set.property] = command.Set.value;\n current = undefined;\n } else if ('Call' in command) {\n if (current === null) {\n console.error('Call called on null');\n return null;\n }\n current = current[command.Call.method](...command.Call.args);\n }\n }\n\n // Convert result to JsJson - sanitize host objects (Window, Element, Function, etc.)\n const isPlainObject = (obj: any): boolean => {\n if (obj === null) return false;\n if (typeof obj !== 'object') return false;\n const proto = Object.getPrototypeOf(obj);\n return proto === Object.prototype || proto === null;\n };\n\n const sanitize = (value: any): JsJsonType => {\n if (value === null || value === undefined) {\n return null;\n }\n if (typeof value === 'boolean') {\n return value;\n }\n if (typeof value === 'string') {\n return value;\n }\n if (typeof value === 'number') {\n return value;\n }\n if (value instanceof Uint8Array) {\n return value;\n }\n if (Array.isArray(value)) {\n return value.map((v) => sanitize(v));\n }\n if (isPlainObject(value)) {\n const out: { [k: string]: JsJsonType } = {};\n for (const k of Object.keys(value)) {\n out[k] = sanitize(value[k]);\n }\n return out;\n }\n\n // Host objects (Window, Element, DOM nodes, functions, class instances, etc.)\n // are not serializable to JsJson. Return null for safety.\n return null;\n };\n\n return sanitize(current);\n }\n}\n","import { JsJsonType } from \"../../jsjson\";\nimport { Metadata } from \"../metadata\";\n\nexport const fetchCacheGet = (metadata: Metadata): JsJsonType => {\n const cache = metadata.getFetchCache();\n\n return {\n data: cache\n };\n};\n","export class Metadata {\n private readonly metadata: HTMLElement;\n\n constructor() {\n const metadata = document.getElementById('v-metadata');\n\n if (metadata === null) {\n throw Error('Expected v-metadata');\n }\n\n this.metadata = metadata;\n metadata.remove();\n }\n\n private get = (attr: string): string | null => {\n return this.metadata.getAttribute(attr) ?? null;\n }\n\n getEnv(name: string) {\n return this.get(`data-env-${name}`);\n }\n\n getFetchCache() {\n return this.get('data-fetch-cache') ?? null;\n }\n\n getEnabledHydration = (): boolean => {\n const value = this.get('data-env-disable-hydration');\n return value !== 'true';\n }\n}\n","import { wasmInit, ModuleControllerType } from './wasm_init';\nimport { BufferCursor } from './buffer_cursor';\nimport { jsJsonDecodeItem, jsJsonGetSize, saveJsJsonToBufferItem } from './jsjson';\nimport { Api } from './api/api';\nimport { Metadata } from './api/metadata';\n\n//Number -> u32 or i32\n//BigInt -> u64 or i64\n\nexport type ImportType = {\n panic_message: (long_ptr: bigint) => void,\n //call from rust\n dom_access: (long_ptr: bigint) => bigint,\n}\n\nexport type ExportType = {\n vertigo_export_alloc_block: (size: number) => bigint,\n vertigo_export_free_block: (pointer: bigint) => void,\n vertigo_export_wasm_command: (value_ptr: bigint) => bigint,\n vertigo_entry_function: (major: number, minor: number) => void,\n}\n\nexport class WasmModule {\n private readonly wasm: ModuleControllerType;\n\n private constructor(\n wasm: ModuleControllerType,\n ) {\n this.wasm = wasm;\n }\n\n public vertigoEntryFunction(major: number, minor: number) {\n this.wasm.exports.vertigo_entry_function(major, minor);\n }\n\n public static async create(wasmBinPath: string): Promise {\n let wasmModule: ModuleControllerType | null = null;\n\n const getWasm = (): ModuleControllerType => {\n if (wasmModule === null) {\n throw Error('Wasm is no initialized');\n }\n\n return wasmModule;\n };\n\n const metadata = new Metadata();\n const vertigo_api = new Api(metadata, getWasm);\n\n //@ts-expect-error\n window.$vertigoApi = vertigo_api;\n\n wasmModule = await wasmInit(wasmBinPath, {\n mod: {\n panic_message: (long_ptr: bigint) => {\n\n const size = Number(long_ptr % (2n ** 32n));\n const ptr = Number(long_ptr >> 32n);\n\n const decoder = new TextDecoder(\"utf-8\");\n const m = getWasm().getUint8Memory().subarray(ptr, ptr + size);\n const message = decoder.decode(m);\n console.error('PANIC', message);\n },\n dom_access: (long_ptr: bigint): bigint => {\n if (long_ptr === 0n) {\n console.error('dom_access - null pointer');\n return 0n;\n }\n\n // Decode JsJson\n const buffer = new BufferCursor(\n () => getWasm().getUint8Memory(),\n long_ptr\n );\n const args = jsJsonDecodeItem(buffer);\n getWasm().exports.vertigo_export_free_block(long_ptr);\n\n // Execute command (now using JsApiCall instead of array-of-arrays)\n const response = vertigo_api.exec(args);\n\n // Save JsJson response\n const responseSize = jsJsonGetSize(response);\n const responseLongPtr = getWasm().exports.vertigo_export_alloc_block(responseSize);\n const responseBuffer = new BufferCursor(\n () => getWasm().getUint8Memory(),\n responseLongPtr\n );\n saveJsJsonToBufferItem(response, responseBuffer);\n\n return responseLongPtr;\n }\n }\n });\n\n return new WasmModule(wasmModule);\n }\n}\n","import { WasmModule } from \"./wasm_module\";\n\n// vertigo-cli compatibility version, change together with package version.\nconst VERTIGO_COMPAT_VERSION_MAJOR = 0;\nconst VERTIGO_COMPAT_VERSION_MINOR = 13;\n\nconst moduleRun: Set = new Set();\n\nconst runModule = async (wasm: string) => {\n if (moduleRun.has(wasm)) {\n //ok, module is run\n return;\n }\n\n if (moduleRun.size > 0) {\n console.error('Only one wasm module can be run', { moduleRun, wasm });\n return;\n }\n\n moduleRun.add(wasm);\n\n console.info(`Wasm module: \"${wasm}\" -> start`);\n const wasmModule = await WasmModule.create(wasm);\n console.info(`Wasm module: \"${wasm}\" -> initialized`);\n wasmModule.vertigoEntryFunction(VERTIGO_COMPAT_VERSION_MAJOR, VERTIGO_COMPAT_VERSION_MINOR);\n console.info(`Wasm module: \"${wasm}\" -> launched vertigoEntryFunction with version ${VERTIGO_COMPAT_VERSION_MAJOR}.${VERTIGO_COMPAT_VERSION_MINOR}`);\n};\n\nconst findAndRunModule = async () => {\n document.querySelectorAll('*[data-vertigo-run-wasm]').forEach((node) => {\n const wasm = node.getAttribute('data-vertigo-run-wasm');\n\n if (typeof wasm === 'string') {\n runModule(wasm);\n } else {\n console.error('Run error', node);\n }\n });\n};\n\n(() => {\n window.addEventListener('load', findAndRunModule);\n setTimeout(findAndRunModule, 3000);\n})();\n"],"names":["decoder","TextDecoder","encoder","TextEncoder","BufferCursor","constructor","getUint8Memory","long_ptr","this","pointer","ptr","Number","size","dataView","DataView","buffer","getByte","value","getUint8","setByte","byte","setUint8","getU16","getUint16","setU16","setUint16","getU32","getUint32","setU32","setUint32","getI32","getInt32","setI32","setInt32","getU64","getBigUint64","setU64","setBigUint64","getI64","getBigInt64","setI64","setBigInt64","getF64","getFloat64","setF64","setFloat64","getBuffer","result","subarray","setBuffer","length","set","getString","decode","setString","encode","getSavedSize","JsJsonConst","jsJsonGetSize","Uint8Array","Array","isArray","sum","item","key","propertyValue","Object","entries","Error","jsJsonDecodeItem","typeId","count","list","i","push","obj","saveJsJsonToBufferItem","undefined","wasmInit","async","wasmBinPath","imports","module_instance","WebAssembly","instantiateStreaming","stream","fetch","err","console","warn","info","resp","binary","arrayBuffer","instantiate","fetchModule","cacheGetUint8Memory","instance","exports","memory","Memory","wasmCommand","vertigo_export_alloc_block","result_long_ptr","vertigo_export_wasm_command","resultBuffer","vertigo_export_free_block","EventEmitter","events","Set","on","callback","isActive","onExec","param","add","delete","trigger","eventsCopy","from","values","itemCallbackToRun","error","PromiseBoxRace","inner","resolve","promiseResolveReject","reject","isFulfilled","promise","Promise","localResolve","localReject","createPromiseValue","reconnectDelay","label","timeout_retry","timeout","setTimeout","LogContext","host","formatLog","message","SocketConnection","close","send","eventMessage","connect","log","done","socket","WebSocket","isClose","closeSocket","socketConnection","addEventListener","event","dataRaw","data","startSocket","timeout_connection","onMessage","isConnect","openSocketResult","type","catch","dispose","wireStringToJsJson","raw","JSON","parse","wasmCallback","wasm","callbackId","command","Websocket","DriverWebsocket","getWasm","websocket_register_callback","callback_id","controller","controllerList","has","assertNeverMessage","Message","websocket_unregister_callback","get","websocket_send_message","stringify","Map","getHeaders","headers","k","v","getBodyString","body","Data","processResponse","response","status","contentType","startsWith","Ok","Text","text","Json","bodyText","Err","String","Interval","timerSet","duration","kind","timerId","setInterval","TimerCall","timerClear","timerResource","clearInterval","clearTimeout","HashRouter","LocationCall","remove","new_hash","location","hash","replace","history","replaceState","window","decodeURIComponent","substr","HistoryLocation","url","pushState","pathname","search","AppLocation","target","mode","locations","newValue","Hash","History","Cookies","cname","cookie","document","split","cookieChunk","trim","cookieName","cookieValue","getJson","cvalue_str","e","cvalue","expires_in","cvalueEncoded","encodeURIComponent","d","Date","setTime","getTime","expires","toUTCString","setJson","getRandom","min","max","range","Math","floor","random","CallbackManager","callbacks","observers","nodes","id","event_name","intersectAdd","click","submit","input","change","blur","mousedown","mouseup","mouseenter","mouseleave","keydown","drop","load","changeFile","node","domEventName","intersectRemove","removeEventListener","CallbackCall","getNode","observer","IntersectionObserver","entry","isIntersecting","intersectionRatio","boundingClientRect","top","bottom","height","observe","disconnect","preventDefault","click_event","stopPropagation","HTMLInputElement","HTMLTextAreaElement","HTMLSelectElement","files","promises","file","then","buf","name","all","params","f","_event","DragEvent","dataTransfer","items","getAsFile","getFiles","dataArray","KeyboardEvent","code","altKey","ctrlKey","shiftKey","metaKey","injects","appLocation","tagName","toLocaleLowerCase","href","getAttribute","scrollTo","hydrateLink","HydrationEngine","commands","depth","matched","virtualNodes","createVirtualNodes","hydrate","hydrateNode","head","toFixed","vNodeId","realNode","vNode","realChildren","childNodes","realIndex","skipTextVNodes","childVId","children","childVNode","candidate","isMatch","checkElementMatch","nodeType","Node","TEXT_NODE","checkTextMatch","removeSkippedNodes","claimNode","ELEMENT_NODE","attributes","element","setAttribute","textContent","Element","Comment","j","nodeToRemove","getVNode","CreateNode","toUpperCase","CreateText","parent","InsertBefore","childId","child","refId","ref_id","index","indexOf","splice","SetAttr","MapNodes","initNodes","getRootHead","getRootBody","style","createElement","getRootHtml","documentElement","getAnyOption","getAny","getNodeElement","HTMLElement","getText","getComment","insertCss","selector","content","createTextNode","appendChild","removeInitNodes","insertBefore","parentNode","childNode","ref_node","addStyles","hasInitNodes","SVG_TAGS","DriverDom","metadata","update","getEnabledHydration","setFocus","runCommand","focus","ev","createNode","createElementNS","setAttr","defaultValue","removeAttr","removeAttribute","removeNode","createText","removeText","updateText","RemoveNode","UpdateText","RemoveAttr","RemoveText","InsertCss","comment","createComment","CreateComment","RemoveComment","CallbackAdd","assertNeverCommand","CallbackRemove","Api","dom","websocket","interval","exec","arg","safeArg","getFetchCache","now","getTimezoneOffset","back","request","method","response2","FetchExecResponse","responseToWasm","toString","fetchExec","FetchExec","WebsocketRegister","WebsocketSendMessage","WebsocketUnregister","TimerSet","TimerClear","LocationGet","LocationCallback","LocationSet","CookieGet","CookieSet","CookieJsonGet","CookieJsonSet","GetEnv","getEnv","Log","arg2","arg3","arg4","debug","GetRandom","executeJsApiCall","JsApiCall","DomBulkUpdate","assertNever","current","Root","domId","RootElement","dom_id","Get","property","Call","args","sanitize","map","proto","getPrototypeOf","prototype","isPlainObject","out","keys","Metadata","attr","getElementById","WasmModule","vertigoEntryFunction","major","minor","vertigo_entry_function","create","wasmModule","vertigo_api","$vertigoApi","mod","panic_message","m","dom_access","responseSize","responseLongPtr","responseBuffer","moduleRun","findAndRunModule","querySelectorAll","forEach","runModule"],"mappings":"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"} \ No newline at end of file diff --git a/crates/vertigo/src/lib.rs b/crates/vertigo/src/lib.rs index 53b9332ee..c5d09361c 100644 --- a/crates/vertigo/src/lib.rs +++ b/crates/vertigo/src/lib.rs @@ -24,6 +24,7 @@ //! //! # Guides //! +//! * [guides::reactive_graph] - how `Value`, `Computed` and `subscribe` are wired together //! * [guides::collection_key_and_list_renderers] - `CollectionKey` and the memoized list renderers //! * [guides::lazy_list_cache] - `LazyListCache`: optimistic, per-item reactive list cache //! * [guides::websocket_collection] - `WsCollection`: server-pushed reactive collections over a WebSocket @@ -61,6 +62,9 @@ mod websocket_collection; /// directory so it gets its own rustdoc page instead of being inlined into the /// crate root. pub mod guides { + #[doc = include_str!("../docs/reactive-graph.md")] + pub mod reactive_graph {} + #[doc = include_str!("../docs/collection-key-and-list-renderers.md")] pub mod collection_key_and_list_renderers {} diff --git a/crates/vertigo/src/tests/dom/embed.rs b/crates/vertigo/src/tests/dom/embed.rs new file mode 100644 index 000000000..42b17286c --- /dev/null +++ b/crates/vertigo/src/tests/dom/embed.rs @@ -0,0 +1,75 @@ +//! What `dom!` accepts as an embedded value. + +use std::{borrow::Cow, num::NonZeroU32, rc::Rc}; + +use crate::{ + self as vertigo, DomNode, EmbedDom, + dev::inspect::{DomDebugFragment, log_start}, + dom, +}; + +/// A downstream type: printable, but rendering is its own business. +struct Money(u32); + +impl std::fmt::Display for Money { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "${}", self.0) + } +} + +fn html(node: impl FnOnce() -> DomNode) -> String { + log_start(); + let _root = node(); + DomDebugFragment::from_log().to_pseudo_html() +} + +#[test] +fn owned_values_embed() { + assert_eq!(html(|| dom! {
{5u32}
}), "
5
"); + assert_eq!(html(|| dom! {
{true}
}), "
true
"); + assert_eq!( + html(|| dom! {
{String::from("hi")}
}), + "
hi
" + ); + assert_eq!( + html(|| dom! {
{NonZeroU32::MIN}
}), + "
1
" + ); + assert_eq!( + html(|| dom! {
{Cow::Borrowed("cow")}
}), + "
cow
" + ); + assert_eq!(html(|| dom! {
{Rc::new(9u32)}
}), "
9
"); +} + +/// Anything printable can be embedded by reference, including a downstream type. +#[test] +fn borrowed_values_embed() { + let number = 5u32; + let text = String::from("hi"); + let money = Money(3); + + assert_eq!(html(|| dom! {
{&number}
}), "
5
"); + assert_eq!(html(|| dom! {
{&text}
}), "
hi
"); + assert_eq!( + html(|| dom! {
{"literal"}
}), + "
literal
" + ); + assert_eq!(html(|| dom! {
{&money}
}), "
$3
"); +} + +/// The by-value side is an explicit list precisely so this stays possible: a printable +/// downstream type can render its own DOM rather than being forced into a text node. +impl EmbedDom for Money { + fn embed(self) -> DomNode { + dom! { {self.to_string()} } + } +} + +#[test] +fn owned_type_can_define_its_own_embedding() { + assert_eq!( + html(|| dom! {
{Money(3)}
}), + "
$3
" + ); +} diff --git a/crates/vertigo/src/tests/dom/mod.rs b/crates/vertigo/src/tests/dom/mod.rs index bbdccbfba..e101f3cdd 100644 --- a/crates/vertigo/src/tests/dom/mod.rs +++ b/crates/vertigo/src/tests/dom/mod.rs @@ -2,6 +2,7 @@ mod children; mod component; mod component_dynamic; mod component_namespaces; +mod embed; mod html_attrs; mod list_spread; mod params; diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 122b0f5c4..a18b96adf 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -3,56 +3,71 @@ ## 0.13.0 - unreleased -Keyed list rendering was rebuilt around per-key `Computed`s. `render_list` and the memoized -list renderers changed shape, and the `Value::synchronize` machinery they used to rely on is -gone. See [`guides::collection_key_and_list_renderers`](https://docs.rs/vertigo/latest/vertigo/guides/collection_key_and_list_renderers/index.html). +The reactive graph was rewritten and keyed list rendering was rebuilt around per-key +`Computed`s. See the [reactive graph guide][reactive-graph] and the +[collection guide][collections]. ### Added -* `keyed_computed_list` - maps a reactive list into a list of per-item `Computed`s, reusing the - same `Computed` for a given key across updates (Solid ``-style), together with `KeyedListItem` -* `MarkerContent` - lets a marker comment report the nodes it keeps in front of itself, so the - subtree travels with the marker instead of being rebuilt when it moves +* A new reactive graph in the `vertigo::reactive` module: transactional, with an equality + cutoff. See the [guide][reactive-graph] +* `reactive::Graph` - an isolated graph instance, mostly useful in tests +* `reactive::transaction` and `reactive::on_after_transaction` +* `GraphId` - identity of a `Value` or `Computed` +* `RenderValue` - trait form of `render_value` / `render_value_option`, for generic code +* `keyed_computed_list` and `KeyedListItem` - per-key `Computed`s from a reactive list, + reusing the same `Computed` for a given key across updates (Solid ``-style) +* `MarkerContent` - lets a marker comment report the nodes it keeps in front of itself ### Changed -* **Breaking**: `render_list` now takes a `Vec` source (previously any - `IntoIterator + Clone + PartialEq`), its render closure receives `&Computed` instead of `&T`, - and the key type must implement `Debug`. The closure runs once per key *appearance*; item - updates flow through the per-key `Computed`, so embed it in `dom!` or wrap it with - `Computed::render_value` -* **Breaking**: the render closures of `render_list_memo` and `render_resource_list_memo` receive - `&Computed`. `render_resource_list_memo` renders `Loading` and `Error` as an empty list +* **Breaking**: `Computed` requires `T: PartialEq` everywhere, not only for `subscribe` +* **Breaking**: writing a `Value` from a reactive callback is supported; it used to panic +* **Breaking**: a `Computed` recomputes when its dependencies change, not on the next read +* **Breaking**: reading through `transaction(|ctx| ...)` serves the cached value +* **Breaking**: `EmbedDom` is no longer implemented for every owned `T: ToString`. Owned + support is an explicit list; references stay blanket, so `&MyType` still embeds. For an + owned value: `impl EmbedDom for MyType { fn embed(self) -> DomNode { self.to_string().embed() } }` +* **Breaking**: `render_list` takes a `Vec` source, its render closure receives + `&Computed` instead of `&T`, and the key type must implement `Debug` +* **Breaking**: the render closures of `render_list_memo` and `render_resource_list_memo` + receive `&Computed`; `Loading` and `Error` render as an empty list * **Breaking**: the mount closure of `DomComment::new_marker` takes a third argument, `&MarkerContent`. A marker moved within the same parent no longer re-runs its mount -* Every `render_list` row is preceded by an anchor comment node, which marks where the row begins - regardless of the shape the row renders to -* Guide `guides::value_synchronize_and_collections` replaced by - `guides::collection_key_and_list_renderers` +* Every `render_list` row is preceded by an anchor comment node +* Guide `guides::value_synchronize_and_collections` replaced by [`collections`][collections] ### Removed +* **Breaking**: `Dependencies`. Use `transaction`, `Driver::transaction` and + `Driver::on_after_transaction` +* **Breaking**: `Computed::subscribe_all` - unchanged values no longer notify anybody, so + it has nothing to report. Use `subscribe` * **Breaking**: `ValueSynchronize`, `Value::synchronize`, `LazyCache::synchronize` and - `CacheValue::synchronize`. `render_list_memo` no longer mirrors its source into a side - structure, so there is nothing left to synchronize; use `keyed_computed_list` to derive - per-item `Computed`s + `CacheValue::synchronize`. Use `keyed_computed_list` * **Breaking**: `Collection` and `CollectionModel`, superseded by `keyed_computed_list`. - `CollectionKey` stays and still describes how list items are identified + `CollectionKey` stays ### Fixed -* `render_list` corrupted sibling order when reordering or inserting rows whose root is a plain - element rather than a `render_value` marker -* Moving a row no longer destroys and rebuilds its DOM; the existing nodes are repositioned, so - their state (input values, listeners, children) survives a reorder -* Updating one row of a keyed list cost work proportional to the *square* of the list length, - because every row copied the whole shared key-to-value map on each update -* `Value::new` and `Value::set` no longer deep-copy the payload when nothing is listening for - `Value::add_event` -* `vertigo build` no longer fails wasm optimization with *"memory.copy operations require bulk - memory operations"* - the WASM features enabled by default for `wasm32-unknown-unknown` are now - passed to `wasm-opt` explicitly, because `strip = true` in the cargo profile removes the - `target_features` section that `wasm-opt` would otherwise read them from +* `render_list` corrupted sibling order when reordering or inserting rows whose root is a + plain element rather than a `render_value` marker +* Moving a row repositions its DOM instead of rebuilding it, so input values, listeners and + children survive a reorder +* Updating one row of a keyed list no longer costs work proportional to the *square* of the + list length +* `Value::new` and `Value::set` no longer deep-copy the payload when nothing is listening + for `Value::add_event` +* An equal-length diamond is computed once per change instead of once per path +* Recomputing a node with many dependencies is no longer quadratic in their number +* A compute closure reading the same value repeatedly records it once +* `vertigo build` no longer fails wasm optimization with *"memory.copy operations require + bulk memory operations"* - the WASM features enabled by default for + `wasm32-unknown-unknown` are now passed to `wasm-opt` explicitly, because `strip = true` + removes the `target_features` section it would otherwise read them from + +[reactive-graph]: https://docs.rs/vertigo/latest/vertigo/guides/reactive_graph/index.html +[collections]: https://docs.rs/vertigo/latest/vertigo/guides/collection_key_and_list_renderers/index.html ## 0.12.0 - 2026-07-01 From a02ac5a2ff4c91f9ad643de50d008f77c93f8a50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pokrywka?= Date: Thu, 20 Aug 2026 22:12:39 +0200 Subject: [PATCH 06/10] fix: ensure writes during first subscription run are captured by wrapping refresh in a transaction --- crates/vertigo/src/reactive/computed.rs | 15 +++++- .../vertigo/src/reactive/propagation_order.rs | 2 +- crates/vertigo/src/reactive/tests.rs | 51 +++++++++++++++++++ 3 files changed, 65 insertions(+), 3 deletions(-) diff --git a/crates/vertigo/src/reactive/computed.rs b/crates/vertigo/src/reactive/computed.rs index aa17a824b..5cf83e74c 100644 --- a/crates/vertigo/src/reactive/computed.rs +++ b/crates/vertigo/src/reactive/computed.rs @@ -1,7 +1,7 @@ use std::{cell::RefCell, ops::Add, rc::Rc}; use super::{ - Context, DropResource, GraphId, ToComputed, Value, + Context, DropResource, Graph, GraphId, ToComputed, Value, graph::{ErasedNode, GraphInner, NodeId}, }; @@ -144,7 +144,18 @@ impl Computed { }), }); graph.register(id, inner.clone()); - inner.refresh(); + + // Inside a transaction, so that a write performed by the callback's first run is + // held until `refresh` has registered the parents it read. Propagating it right + // away would walk a graph this node has no edges in yet, and the update it just + // caused would never come back to it. + Graph { + inner: graph.clone(), + } + .transaction(|_| { + inner.refresh(); + }); + DropResource::from_struct(inner) } diff --git a/crates/vertigo/src/reactive/propagation_order.rs b/crates/vertigo/src/reactive/propagation_order.rs index 4cc16b52e..a359472e5 100644 --- a/crates/vertigo/src/reactive/propagation_order.rs +++ b/crates/vertigo/src/reactive/propagation_order.rs @@ -150,7 +150,7 @@ fn dynamic_branch_switch_recomputes_three_times() { move |value| seen.borrow_mut().push(value) }); - let mut pass = |label: &str, apply: &dyn Fn()| -> (usize, Vec) { + let pass = |label: &str, apply: &dyn Fn()| -> (usize, Vec) { runs.set(0); seen.borrow_mut().clear(); g.transaction(|_| apply()); diff --git a/crates/vertigo/src/reactive/tests.rs b/crates/vertigo/src/reactive/tests.rs index 97f713e35..385cf85ac 100644 --- a/crates/vertigo/src/reactive/tests.rs +++ b/crates/vertigo/src/reactive/tests.rs @@ -475,3 +475,54 @@ fn settling_write_back_is_allowed() { assert_eq!(g.transaction(|ctx| a.get(ctx)), 10); assert!(runs.get() >= 5, "callback ran {} times", runs.get()); } + +/// A write from a callback's very first run must not be lost. The callback runs before its +/// parents are registered, so the wave that write starts cannot reach this node yet. +#[test] +fn write_from_first_subscribe_run_is_not_lost() { + let g = Graph::new(); + let a = g.value(0); + let seen = Rc::new(std::cell::RefCell::new(Vec::new())); + + let _sub = a.to_computed().subscribe({ + let a = a.clone(); + let seen = seen.clone(); + move |value| { + seen.borrow_mut().push(value); + if value == 0 { + a.set(1); + } + } + }); + + assert_eq!(*seen.borrow(), vec![0, 1]); + assert_eq!(g.transaction(|ctx| a.get(ctx)), 1); +} + +/// `when_connect` fires while the node it belongs to is being computed. A write from there +/// must reach the subscriber too, not just land in the value. +#[test] +fn write_from_when_connect_reaches_the_subscriber() { + let g = Graph::new(); + let source = g.value(0); + let observed = g + .computed({ + let source = source.clone(); + move |ctx| source.get(ctx) + }) + .when_connect({ + let source = source.clone(); + move || { + source.set(7); + DropResource::new(|| {}) + } + }); + + let seen = Rc::new(std::cell::RefCell::new(Vec::new())); + let _sub = observed.subscribe({ + let seen = seen.clone(); + move |value| seen.borrow_mut().push(value) + }); + + assert_eq!(*seen.borrow(), vec![0, 7]); +} From 9b8360069c20e9f2049392b25b4e5eaf714dc10b Mon Sep 17 00:00:00 2001 From: Michal Pokrywka Date: Fri, 21 Aug 2026 15:28:23 +0200 Subject: [PATCH 07/10] Document that edge budget is reachable with no cycle at all. Added unit tests. --- crates/vertigo/docs/reactive-graph.md | 8 ++ crates/vertigo/src/reactive/graph/mod.rs | 5 + .../vertigo/src/reactive/propagation_order.rs | 95 +++++++++++++++++++ 3 files changed, 108 insertions(+) diff --git a/crates/vertigo/docs/reactive-graph.md b/crates/vertigo/docs/reactive-graph.md index 5cbf3e09a..9e94f40ed 100644 --- a/crates/vertigo/docs/reactive-graph.md +++ b/crates/vertigo/docs/reactive-graph.md @@ -104,6 +104,10 @@ and drops that node from the rest of the pass. The rest of the graph still settl application keeps running, with that one node left holding a stale value. It is a diagnostic, not a supported mode: a loop that reaches it is a bug in the callback. +The count is per node per pass, and the ordering limitation below can raise it on its own, +so a node fed by more than 100 paths of differing lengths can burn the budget with no cycle +anywhere. That is a bug in the graph, not in the code that hit it. + Writing from a **compute** closure is a different matter and should be avoided. The closure is not guaranteed to run once per change (see below), so the write can fire more than once, and one of those firings may be based on a value that was still stale. @@ -145,6 +149,10 @@ Writing `d1` and `d2` in one transaction computes `c` twice: once with the new ` old `b2`, then again once `b2` catches up. A conditional read adds a second source of the same problem, because a parent discovered during this run cannot have been waited for. +The count is one run per incoming path, so it scales with the fan-in. Past 100 paths the +refresh budget fires and `c` is dropped from the rest of the pass, leaving it wrong rather +than merely recomputed too often. + The tests in `reactive::propagation_order` pin the current behaviour and carry the intended behaviour alongside, commented out. The fix is to refresh a dirty node when it is read instead of serving its cached value, which removes both the extra runs and the intermediate diff --git a/crates/vertigo/src/reactive/graph/mod.rs b/crates/vertigo/src/reactive/graph/mod.rs index 119876a43..cb6a1560b 100644 --- a/crates/vertigo/src/reactive/graph/mod.rs +++ b/crates/vertigo/src/reactive/graph/mod.rs @@ -174,6 +174,11 @@ impl GraphInner { /// also means a callback that keeps writing back into its own dependency never lets /// the wave drain, so each node gets a refresh budget - see [`MAX_REFRESH_PER_NODE`]. /// + /// A write-back loop is not the only way to spend that budget. Ordering inside a wave is + /// imperfect (see [`propagation_order`](super::super::propagation_order)), so a node runs + /// once per incoming path of differing length - more than [`MAX_REFRESH_PER_NODE`] such + /// paths burn the budget with no cycle present. + /// /// A node that burns its budget is logged and dropped from the rest of the wave. /// The rest of the graph still settles, and only that node and whatever depends on it /// are left stale until something dirties them again. diff --git a/crates/vertigo/src/reactive/propagation_order.rs b/crates/vertigo/src/reactive/propagation_order.rs index a359472e5..5e4305e30 100644 --- a/crates/vertigo/src/reactive/propagation_order.rs +++ b/crates/vertigo/src/reactive/propagation_order.rs @@ -198,3 +198,98 @@ fn dynamic_branch_switch_recomputes_three_times() { // assert_eq!(runs_4, 1); // assert_eq!(seen_4, vec![1509]); } + +/// Build a fan-in: `paths` chains of *different* lengths, all rooted in values written in +/// one transaction, all read by one node `c`. Chain `k` is `k + 1` hops long, so each one +/// reaches `c` at a different point of the pass. +/// +/// No node here depends on itself, nothing writes from a callback: the graph is a plain +/// acyclic fan-in. +/// +/// Returns how many times `c` computed, and the last value its subscriber saw. +fn fan_in(paths: usize) -> (u32, Option) { + let g = Graph::new(); + let roots = (0..paths).map(|_| g.value(0i32)).collect::>(); + + let mut tails = Vec::new(); + for (k, root) in roots.iter().enumerate() { + let mut node = g.computed({ + let root = root.clone(); + move |ctx| root.get(ctx) + }); + for _ in 0..k { + node = g.computed({ + let prev = node.clone(); + move |ctx| prev.get(ctx) + }); + } + tails.push(node); + } + + let runs = Rc::new(Cell::new(0u32)); + let c = g.computed({ + let tails = tails.clone(); + let runs = runs.clone(); + move |ctx| { + runs.set(runs.get() + 1); + tails.iter().map(|tail| tail.get(ctx)).sum::() + } + }); + + let last = Rc::new(Cell::new(None)); + let _sub = c.subscribe({ + let last = last.clone(); + move |value| last.set(Some(value)) + }); + + runs.set(0); + g.transaction(|_| { + for (i, root) in roots.iter().enumerate() { + root.set(i as i32 + 1); + } + }); + + (runs.get(), last.get()) +} + +/// The extra runs are not a constant overhead - the count grows with the number of paths +/// of differing length that reach the node. This is the ramp that the next test drives +/// past the budget. +#[test] +fn one_extra_run_per_incoming_path() { + for paths in [2usize, 3, 5, 10] { + let expected_sum = (paths * (paths + 1) / 2) as i32; + let (runs, last) = fan_in(paths); + + assert_eq!(runs as usize, paths, "{paths} paths"); + assert_eq!(last, Some(expected_sum), "{paths} paths"); + // assert_eq!(runs, 1, "{paths} paths"); + } +} + +/// Push that ramp past [`MAX_REFRESH_PER_NODE`] and the budget - meant as a cycle +/// diagnostic - fires on a graph that has no cycle at all. +/// +/// `c` is refreshed once per path, so with `MAX_REFRESH_PER_NODE + 1` paths the last +/// refresh is refused, `vertigo: node NodeId(..) refreshed 100 times in one run - +/// dropping (possible cycle).` is logged, and `c` is left holding a value that is missing +/// the contribution of the longest path. +/// +/// This is the ordering defect at its worst: not just a wasted recomputation and a glitch, +/// but a permanently wrong value in a correct program. The pull-on-read fix removes it - +/// with one refresh per pass the budget is untouchable by acyclic graphs. +#[test] +fn enough_paths_burn_the_refresh_budget() { + let paths = super::graph::MAX_REFRESH_PER_NODE as usize + 1; + let correct_sum = (paths * (paths + 1) / 2) as i32; + + let (runs, last) = fan_in(paths); + + assert_eq!(runs, super::graph::MAX_REFRESH_PER_NODE); + // The subscriber did see values, just never the right one: the longest path is missing. + assert_ne!(last, Some(correct_sum), "the last path never reached `c`"); + assert_eq!(last, Some(correct_sum - paths as i32)); + + // assert_eq!(runs, 1); + // assert_eq!(last, Some(correct_sum)); +} From 9db1ae31366a3b231f9a15770ca79af0b86bab87 Mon Sep 17 00:00:00 2001 From: szagi3891 Date: Fri, 21 Aug 2026 17:33:57 +0200 Subject: [PATCH 08/10] Forbid Value::set from compute and subscribe; finish every wave. Graph callbacks log and ignore writes instead of joining the current pass. when_connect runs after propagation so with_connect may still write. Drop the per-node refresh budget so a settled graph is never left with a stale value. --- crates/vertigo/docs/reactive-graph.md | 62 +++--- crates/vertigo/src/reactive/computed.rs | 13 +- crates/vertigo/src/reactive/graph/logger.rs | 108 ++++++++++ crates/vertigo/src/reactive/graph/mod.rs | 94 ++++---- .../vertigo/src/reactive/graph/transaction.rs | 35 ++- crates/vertigo/src/reactive/graph/watch.rs | 88 ++++++-- crates/vertigo/src/reactive/invariants.md | 102 +++++++++ crates/vertigo/src/reactive/invariants.pl.md | 104 +++++++++ crates/vertigo/src/reactive/mod.rs | 6 +- .../vertigo/src/reactive/propagation_order.rs | 33 ++- crates/vertigo/src/reactive/tests.rs | 203 +++++++++--------- crates/vertigo/src/reactive/value.rs | 6 + 12 files changed, 626 insertions(+), 228 deletions(-) create mode 100644 crates/vertigo/src/reactive/graph/logger.rs create mode 100644 crates/vertigo/src/reactive/invariants.md create mode 100644 crates/vertigo/src/reactive/invariants.pl.md diff --git a/crates/vertigo/docs/reactive-graph.md b/crates/vertigo/docs/reactive-graph.md index 9e94f40ed..a173cb4ac 100644 --- a/crates/vertigo/docs/reactive-graph.md +++ b/crates/vertigo/docs/reactive-graph.md @@ -81,44 +81,34 @@ nodes it reads. ## Writing from a callback -Writing a `Value` from a `subscribe` callback is allowed. The write joins the pass that is -already running rather than starting a second one: +`Value::set` from a compute closure or a `subscribe` callback is forbidden. The write is +ignored and logged. Those closures run during a wave (or the first refresh of a +subscription); feeding values back into the graph would re-enter the wave. Domain rules: +[`invariants`](crate::reactive::invariants). ```rust,ignore selected_id.to_computed().subscribe(move |id| { - // fine: runs inside the pass, the update is picked up before it ends + // ignored: subscribe must not write form_dirty.set(false); }); ``` -A callback that writes back into a value it depends on can only work if it settles - each -round has to move towards a value that stops changing. One that never settles would spin -forever, so a node may be recomputed at most **100 times** in one pass. Past that the graph -logs +Legal places to write: DOM/event handlers, timers, fetch/socket callbacks, +`on_after_transaction`, and `when_connect` / `Value::with_connect`. -```text -vertigo: node NodeId(12) refreshed 100 times in one run - dropping (possible cycle). -``` - -and drops that node from the rest of the pass. The rest of the graph still settles and the -application keeps running, with that one node left holding a stale value. It is a -diagnostic, not a supported mode: a loop that reaches it is a bug in the callback. - -The count is per node per pass, and the ordering limitation below can raise it on its own, -so a node fed by more than 100 paths of differing lengths can burn the budget with no cycle -anywhere. That is a bug in the graph, not in the code that hit it. - -Writing from a **compute** closure is a different matter and should be avoided. The closure -is not guaranteed to run once per change (see below), so the write can fire more than once, -and one of those firings may be based on a value that was still stale. +A node may still be recomputed more than once in one pass because of the ordering +limitation below. The wave always finishes; the last value of each node is the one that +matches the sources. ## Connecting to the outside world -[`when_connect`](crate::Computed::when_connect) runs a closure when a node gains its first -dependent and drops the returned [`DropResource`](crate::DropResource) when it loses its -last one. This is how a node backed by a fetch, a timer or a socket only does work while -something is actually looking at it. [`Value::with_connect`](crate::Value::with_connect) -packages the common shape of it. +[`when_connect`](crate::Computed::when_connect) runs a closure **after the wave** in which +a node gains its first dependent, and drops the returned +[`DropResource`](crate::DropResource) after the wave in which it loses its last one. +Watched-then-unwatched in the same wave is a no-op. This is how a node backed by a fetch, +a timer or a socket only does work while something is actually looking at it. +[`Value::with_connect`](crate::Value::with_connect) packages the common shape of it; +because `create` runs after the wave, it may write the `Value`. ## Isolated graphs @@ -149,14 +139,11 @@ Writing `d1` and `d2` in one transaction computes `c` twice: once with the new ` old `b2`, then again once `b2` catches up. A conditional read adds a second source of the same problem, because a parent discovered during this run cannot have been waited for. -The count is one run per incoming path, so it scales with the fan-in. Past 100 paths the -refresh budget fires and `c` is dropped from the rest of the pass, leaving it wrong rather -than merely recomputed too often. - -The tests in `reactive::propagation_order` pin the current behaviour and carry the intended -behaviour alongside, commented out. The fix is to refresh a dirty node when it is read -instead of serving its cached value, which removes both the extra runs and the intermediate -values. +The count is one run per incoming path, so it scales with the fan-in. The extra runs are +wasted work and can glitch a subscriber, but the last refresh is the correct value. The +tests in `reactive::propagation_order` pin that. The intended fix is to refresh a dirty +node when it is read instead of serving its cached value, which removes both the extra +runs and the intermediate values. ## Coming from 0.12 @@ -166,7 +153,8 @@ values. * `Computed` now requires `T: PartialEq` everywhere, not only for `subscribe`. * `subscribe_all` is gone. It reported every recomputation including the ones that produced the same value, and those no longer notify anybody. -* Writing a `Value` from a callback used to panic with *"You cannot change the source value - while the dependency graph is being refreshed"*. It is now supported. +* Writing a `Value` from a compute or subscribe callback is ignored and logged (same idea as + 0.12's *"You cannot change the source value while the dependency graph is being refreshed"*). + `when_connect` / `Value::with_connect` run after the wave and may write. * Reading through `transaction(|ctx| ...)` serves the cached value instead of recomputing the chain behind it. diff --git a/crates/vertigo/src/reactive/computed.rs b/crates/vertigo/src/reactive/computed.rs index 5cf83e74c..21c5884a3 100644 --- a/crates/vertigo/src/reactive/computed.rs +++ b/crates/vertigo/src/reactive/computed.rs @@ -45,6 +45,7 @@ impl ErasedNode for ComputedInner { impl ErasedNode for SubscribeInner { fn refresh(&self) -> bool { + let _guard = self.graph.enter_callback(); let ctx = Context::tracking(); (self.refresh)(&ctx); self.graph.set_parents(self.id, ctx.take_parents()); @@ -66,6 +67,7 @@ impl Drop for SubscribeInner { impl ComputedInner { fn recompute(&self) -> bool { + let _guard = self.graph.enter_callback(); let ctx = Context::tracking(); let new_value = (self.compute)(&ctx); self.graph.set_parents(self.id, ctx.take_parents()); @@ -117,8 +119,9 @@ impl Computed { GraphId::from_node(self.inner.id) } - /// Runs `create` when this value starts being observed; the returned - /// [`DropResource`] is dropped when it stops being observed. + /// Runs `create` after the wave in which this value starts being observed; the + /// returned [`DropResource`] is dropped after the wave in which it stops being + /// observed. `create` may write `Value`s. A `set` from compute or subscribe is ignored. pub fn when_connect DropResource + 'static>(&self, create: F) -> Computed { let new_computed = Computed::create(self.inner.graph.clone(), { let parent = self.clone(); @@ -145,10 +148,6 @@ impl Computed { }); graph.register(id, inner.clone()); - // Inside a transaction, so that a write performed by the callback's first run is - // held until `refresh` has registered the parents it read. Propagating it right - // away would walk a graph this node has no edges in yet, and the update it just - // caused would never come back to it. Graph { inner: graph.clone(), } @@ -240,6 +239,7 @@ mod tests { #[test] fn subscribe_does_not_notify_dependents() { let g = Graph::new(); + let logs = g.logger().listen(); let a = g.value(0); let id = g.inner.alloc_id(); let sink = Rc::new(SubscribeInner { @@ -271,5 +271,6 @@ mod tests { runs.set(0); a.set(1); assert_eq!(runs.get(), 0); + logs.assert_eq(&[]); } } diff --git a/crates/vertigo/src/reactive/graph/logger.rs b/crates/vertigo/src/reactive/graph/logger.rs new file mode 100644 index 000000000..3d8a310d4 --- /dev/null +++ b/crates/vertigo/src/reactive/graph/logger.rs @@ -0,0 +1,108 @@ +use std::{ + cell::{Cell, RefCell}, + collections::BTreeMap, + rc::Rc, +}; + +struct LoggerInner { + next_id: Cell, + buffers: RefCell>>>>, +} + +/// Collects `log::error!` messages for one graph. +#[derive(Clone)] +pub struct Logger { + inner: Rc, +} + +/// One subscription to a [`Logger`]. [`Drop`] unregisters it. +pub struct LoggerListener { + logger: Rc, + id: u64, + buffer: Rc>>, +} + +impl Logger { + pub(super) fn new() -> Self { + Logger { + inner: Rc::new(LoggerInner { + next_id: Cell::new(1), + buffers: RefCell::new(BTreeMap::new()), + }), + } + } + + pub fn listen(&self) -> LoggerListener { + let id = self.inner.next_id.get(); + self.inner.next_id.set(id + 1); + let buffer = Rc::new(RefCell::new(Vec::new())); + self.inner.buffers.borrow_mut().insert(id, buffer.clone()); + LoggerListener { + logger: self.inner.clone(), + id, + buffer, + } + } + + pub(super) fn error(&self, message: &str) { + log::error!("{message}"); + for buffer in self.inner.buffers.borrow().values() { + buffer.borrow_mut().push(message.to_string()); + } + } +} + +impl LoggerListener { + /// Current messages, then empty the buffer. + pub fn take(&self) -> Vec { + std::mem::take(&mut *self.buffer.borrow_mut()) + } + + /// [`take`](Self::take) and compare with `expected`. Panic points at the caller. + #[track_caller] + pub fn assert_eq(&self, expected: &[&str]) { + assert_eq!(self.take(), expected); + } +} + +impl Drop for LoggerListener { + fn drop(&mut self) { + self.logger.buffers.borrow_mut().remove(&self.id); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn take_returns_messages_and_clears() { + let logger = Logger::new(); + let listener = logger.listen(); + logger.error("a"); + logger.error("b"); + assert_eq!(listener.take(), ["a", "b"]); + listener.assert_eq(&[]); + logger.error("c"); + listener.assert_eq(&["c"]); + } + + #[test] + fn drop_unregisters() { + let logger = Logger::new(); + let listener = logger.listen(); + drop(listener); + logger.error("x"); + assert!(logger.inner.buffers.borrow().is_empty()); + } + + #[test] + fn two_listeners_both_receive() { + let logger = Logger::new(); + let a = logger.listen(); + let b = logger.listen(); + logger.error("x"); + assert_eq!(a.take(), ["x"]); + assert_eq!(b.take(), ["x"]); + } +} diff --git a/crates/vertigo/src/reactive/graph/mod.rs b/crates/vertigo/src/reactive/graph/mod.rs index cb6a1560b..85d56bc70 100644 --- a/crates/vertigo/src/reactive/graph/mod.rs +++ b/crates/vertigo/src/reactive/graph/mod.rs @@ -1,14 +1,11 @@ -use std::{ - cell::Cell, - collections::{HashMap, HashSet}, - rc::Rc, -}; +use std::{cell::Cell, rc::Rc}; use super::{Computed, Context, DropResource, Value, context::ParentList}; mod dirty; mod edges; mod hooks; +mod logger; mod nodes; mod transaction; mod watch; @@ -17,9 +14,12 @@ use dirty::Dirty; use edges::Edges; use hooks::Hooks; use nodes::Nodes; +pub(crate) use transaction::CallbackGuard; use transaction::Transaction; use watch::Watch; +pub use logger::{Logger, LoggerListener}; + #[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Debug)] pub(crate) struct NodeId(pub u64); @@ -37,18 +37,12 @@ impl GraphId { } } -/// How many times one node may refresh inside a single propagation. -/// -/// A write from a reactive callback re-dirties nodes in the wave that is already running, -/// so a feedback loop that settles is fine and is expected to take a handful of rounds. -/// One that never settles would spin forever, so past this budget the node is dropped from -/// the rest of the wave. -pub(crate) const MAX_REFRESH_PER_NODE: u32 = 100; - pub(crate) trait ErasedNode { fn refresh(&self) -> bool; } +pub(crate) const BLOCKED_WRITE: &str = "vertigo: Value::set is not allowed from a computed, a subscribe callback, or during propagation"; + /// One reactive graph. Nodes created from different graphs do not see each other. pub struct Graph { pub(crate) inner: Rc, @@ -56,7 +50,7 @@ pub struct Graph { /// Orchestrator: transactions, `propagate`, `set_parents`, `unregister`. /// -/// Persistent state lives in `Nodes`, `Edges`, `Dirty`, `Watch`, `Hooks`. +/// Persistent state lives in `Nodes`, `Edges`, `Dirty`, `Watch`, `Hooks`, `Logger`. pub(crate) struct GraphInner { next_id: Cell, tx: Transaction, @@ -65,6 +59,7 @@ pub(crate) struct GraphInner { dirty: Dirty, watch: Watch, hooks: Hooks, + logger: Logger, } impl Clone for Graph { @@ -92,6 +87,7 @@ impl Graph { dirty: Dirty::new(), watch: Watch::new(), hooks: Hooks::new(), + logger: Logger::new(), }), } } @@ -114,6 +110,7 @@ impl Graph { if let Some(leave) = self.inner.tx.leave() { self.inner.propagate(); if !leave.already_propagating { + self.inner.flush_watch(); self.inner.hooks.fire(); } } @@ -127,6 +124,10 @@ impl Graph { inner.hooks.remove(id); }) } + + pub fn logger(&self) -> Logger { + self.inner.logger.clone() + } } impl GraphInner { @@ -146,6 +147,29 @@ impl GraphInner { pub(crate) fn register_connect(&self, id: NodeId, connect: Rc DropResource>) { self.watch.register(id, connect, self.edges.is_watched(id)); + self.flush_watch_if_idle(); + } + + pub(crate) fn enter_callback(&self) -> CallbackGuard<'_> { + self.tx.enter_callback() + } + + pub(crate) fn writes_allowed(&self) -> bool { + if self.tx.writes_blocked() { + self.logger.error(BLOCKED_WRITE); + return false; + } + true + } + + fn flush_watch(&self) { + self.watch.flush(|id| self.edges.is_watched(id)); + } + + fn flush_watch_if_idle(&self) { + if self.tx.can_propagate() { + self.flush_watch(); + } } pub(crate) fn set_parents(&self, child: NodeId, pairs: ParentList) { @@ -164,31 +188,22 @@ impl GraphInner { self.watch.unregister(id); let became_unwatched = self.edges.unregister(id); self.watch.on_unwatched_many(became_unwatched); + self.flush_watch_if_idle(); } /// Process dirty nodes in topological order. Dependents are enqueued only when /// the node's value changed. /// - /// A write performed from inside a reactive callback joins the wave that is already - /// running, which is what makes `Value::set` usable from a `subscribe` callback. It - /// also means a callback that keeps writing back into its own dependency never lets - /// the wave drain, so each node gets a refresh budget - see [`MAX_REFRESH_PER_NODE`]. - /// - /// A write-back loop is not the only way to spend that budget. Ordering inside a wave is - /// imperfect (see [`propagation_order`](super::super::propagation_order)), so a node runs - /// once per incoming path of differing length - more than [`MAX_REFRESH_PER_NODE`] such - /// paths burn the budget with no cycle present. - /// - /// A node that burns its budget is logged and dropped from the rest of the wave. - /// The rest of the graph still settles, and only that node and whatever depends on it - /// are left stale until something dirties them again. + /// `Value::set` from compute/subscribe is ignored, so a wave cannot be re-entered + /// by a write. A node may still refresh more than once because ordering inside a + /// wave is imperfect (see `reactive::propagation_order`): it runs once per incoming + /// path of differing length. The wave still drains, and the last refresh of each + /// node is the value that matches the sources. pub(crate) fn propagate(&self) { if !self.tx.can_propagate() { return; } let _guard = self.tx.start_propagate(); - let mut refreshes: HashMap = HashMap::new(); - let mut cut_off: HashSet = HashSet::new(); loop { let Some(id) = self.dirty.take_ready() else { @@ -200,28 +215,11 @@ impl GraphInner { self.dirty.dequeue(id); - if cut_off.contains(&id) { - // Already given up on this one; let anything waiting on it move along. - self.dirty.release_parent(id, &self.edges); - continue; - } - let Some(node) = self.nodes.upgrade(id) else { self.dirty.release_parent(id, &self.edges); continue; }; - let count = refreshes.entry(id).or_insert(0); - *count += 1; - if *count > MAX_REFRESH_PER_NODE { - log::error!( - "vertigo: node {id:?} refreshed {MAX_REFRESH_PER_NODE} times in one run - dropping (possible cycle)." - ); - cut_off.insert(id); - self.dirty.release_parent(id, &self.edges); - continue; - } - let changed = node.refresh(); self.dirty.after_refresh(id, changed, &self.edges); } @@ -246,22 +244,26 @@ mod tests { #[test] fn unregister_releases_waiting_child() { let g = Graph::new(); + let logs = g.logger().listen(); let inner = &*g.inner; inner.edges.replace(NodeId(2), vec![slot(1)]); inner.dirty.enqueue(NodeId(1), &inner.edges); inner.dirty.enqueue(NodeId(2), &inner.edges); inner.unregister(NodeId(1)); assert_eq!(inner.dirty.take_ready(), Some(NodeId(2))); + logs.assert_eq(&[]); } #[test] fn dead_dirty_parent_releases_waiting_child() { let g = Graph::new(); + let logs = g.logger().listen(); let inner = &*g.inner; inner.edges.replace(NodeId(2), vec![slot(1)]); inner.dirty.enqueue(NodeId(1), &inner.edges); inner.dirty.enqueue(NodeId(2), &inner.edges); inner.propagate(); assert!(!inner.dirty.contains(NodeId(2))); + logs.assert_eq(&[]); } } diff --git a/crates/vertigo/src/reactive/graph/transaction.rs b/crates/vertigo/src/reactive/graph/transaction.rs index 1b1777da6..46ab65ef4 100644 --- a/crates/vertigo/src/reactive/graph/transaction.rs +++ b/crates/vertigo/src/reactive/graph/transaction.rs @@ -1,9 +1,11 @@ use std::cell::Cell; -/// Nesting depth of `Graph::transaction` and the reentrancy flag for `propagate`. +/// Nesting depth of `Graph::transaction`, the reentrancy flag for `propagate`, +/// and the depth of graph callbacks (`compute` / `subscribe`) that must not write. pub(super) struct Transaction { depth: Cell, propagating: Cell, + callback_depth: Cell, } /// The outermost transaction just closed. @@ -23,11 +25,23 @@ impl Drop for Propagating<'_> { } } +/// Decrements callback depth when a `compute` / `subscribe` closure returns (including panic). +pub(crate) struct CallbackGuard<'a> { + tx: &'a Transaction, +} + +impl Drop for CallbackGuard<'_> { + fn drop(&mut self) { + self.tx.callback_depth.set(self.tx.callback_depth.get() - 1); + } +} + impl Transaction { pub(super) fn new() -> Self { Self { depth: Cell::new(0), propagating: Cell::new(false), + callback_depth: Cell::new(0), } } @@ -60,6 +74,16 @@ impl Transaction { self.propagating.set(true); Propagating { tx: self } } + + pub(super) fn enter_callback(&self) -> CallbackGuard<'_> { + self.callback_depth.set(self.callback_depth.get() + 1); + CallbackGuard { tx: self } + } + + /// `Value::set` is forbidden from `compute` / `subscribe` and while a wave is running. + pub(super) fn writes_blocked(&self) -> bool { + self.callback_depth.get() > 0 || self.propagating.get() + } } #[cfg(test)] @@ -71,5 +95,14 @@ mod tests { let tx = Transaction::new(); let _wave = tx.start_propagate(); assert!(!tx.can_propagate()); + assert!(tx.writes_blocked()); + } + + #[test] + fn writes_blocked_inside_callback() { + let tx = Transaction::new(); + assert!(!tx.writes_blocked()); + let _guard = tx.enter_callback(); + assert!(tx.writes_blocked()); } } diff --git a/crates/vertigo/src/reactive/graph/watch.rs b/crates/vertigo/src/reactive/graph/watch.rs index b73fd5511..b4aadaa46 100644 --- a/crates/vertigo/src/reactive/graph/watch.rs +++ b/crates/vertigo/src/reactive/graph/watch.rs @@ -1,12 +1,20 @@ -use std::{cell::RefCell, collections::HashMap, rc::Rc}; +use std::{ + cell::RefCell, + collections::{HashMap, HashSet}, + rc::Rc, +}; use super::super::DropResource; use super::{NodeId, edges::ParentDiff}; -/// `when_connect` lifecycle: run `connect` while a node has children, drop the resource when not. +/// `when_connect` lifecycle: connect while a node has children, drop the resource when not. +/// +/// `apply` / `register` / `unregister` only mark the node pending. [`Self::flush`] runs +/// after the propagation wave so connect/disconnect never run in the middle of a refresh. pub(super) struct Watch { connect: RefCell DropResource>>>, connected: RefCell>, + pending: RefCell>, } impl Watch { @@ -14,6 +22,7 @@ impl Watch { Self { connect: RefCell::new(HashMap::new()), connected: RefCell::new(HashMap::new()), + pending: RefCell::new(HashSet::new()), } } @@ -25,43 +34,60 @@ impl Watch { ) { self.connect.borrow_mut().insert(id, connect); if watched { - self.on_watched(id); + self.schedule(id); } } pub(super) fn unregister(&self, id: NodeId) { self.connect.borrow_mut().remove(&id); - let _dropped = self.connected.borrow_mut().remove(&id); + self.schedule(id); } pub(super) fn apply(&self, diff: ParentDiff) { for id in diff.became_watched { - self.on_watched(id); + self.schedule(id); } for id in diff.became_unwatched { - self.on_unwatched(id); + self.schedule(id); } } pub(super) fn on_unwatched_many(&self, ids: Vec) { for id in ids { - self.on_unwatched(id); + self.schedule(id); } } - fn on_watched(&self, id: NodeId) { - if self.connected.borrow().contains_key(&id) { - return; - } - let Some(connect) = self.connect.borrow().get(&id).cloned() else { - return; - }; - let resource = connect(); - self.connected.borrow_mut().insert(id, resource); + fn schedule(&self, id: NodeId) { + self.pending.borrow_mut().insert(id); } - fn on_unwatched(&self, id: NodeId) { - let _dropped = self.connected.borrow_mut().remove(&id); + /// Match connectedness to `is_watched`. No-op when a node was watched and unwatched + /// before this flush (net unchanged, never connected). + pub(super) fn flush(&self, is_watched: impl Fn(NodeId) -> bool) { + loop { + let pending: Vec = self.pending.borrow_mut().drain().collect(); + if pending.is_empty() { + return; + } + for id in pending { + let watched = is_watched(id); + let has_connect = self.connect.borrow().contains_key(&id); + let is_connected = self.connected.borrow().contains_key(&id); + + if is_connected && (!watched || !has_connect) { + let _dropped = self.connected.borrow_mut().remove(&id); + } + + if watched && has_connect && !self.connected.borrow().contains_key(&id) { + let Some(connect) = self.connect.borrow().get(&id).cloned() else { + continue; + }; + let resource = connect(); + self.connected.borrow_mut().insert(id, resource); + } + } + } } } @@ -70,6 +96,10 @@ mod tests { use super::*; use std::cell::Cell; + fn flush_if(watch: &Watch, watched: impl Fn(NodeId) -> bool) { + watch.flush(watched); + } + #[test] fn register_connects_when_watched() { let watch = Watch::new(); @@ -85,6 +115,8 @@ mod tests { }), true, ); + assert_eq!(n.get(), 0, "connect waits for flush"); + flush_if(&watch, |id| id == NodeId(1)); assert_eq!(n.get(), 1); } @@ -112,6 +144,7 @@ mod tests { }); watch.register(NodeId(1), connect.clone(), true); watch.register(NodeId(1), connect, true); + flush_if(&watch, |id| id == NodeId(1)); assert_eq!(connects.get(), 1); } @@ -120,7 +153,9 @@ mod tests { let watch = Watch::new(); let live = Rc::new(Cell::new(0)); watch.register(NodeId(1), connect_flag(&live), true); + flush_if(&watch, |id| id == NodeId(1)); watch.unregister(NodeId(1)); + flush_if(&watch, |_| false); assert_eq!(live.get(), 0); } @@ -129,10 +164,12 @@ mod tests { let watch = Watch::new(); let live = Rc::new(Cell::new(0)); watch.register(NodeId(1), connect_flag(&live), true); + flush_if(&watch, |id| id == NodeId(1)); watch.apply(ParentDiff { became_watched: Vec::new(), became_unwatched: vec![NodeId(1)], }); + flush_if(&watch, |_| false); assert_eq!(live.get(), 0); } @@ -141,7 +178,22 @@ mod tests { let watch = Watch::new(); let live = Rc::new(Cell::new(0)); watch.register(NodeId(1), connect_flag(&live), true); + flush_if(&watch, |id| id == NodeId(1)); watch.on_unwatched_many(vec![NodeId(1)]); + flush_if(&watch, |_| false); + assert_eq!(live.get(), 0); + } + + #[test] + fn watch_and_unwatch_before_flush_does_not_connect() { + let watch = Watch::new(); + let live = Rc::new(Cell::new(0)); + watch.register(NodeId(1), connect_flag(&live), true); + watch.apply(ParentDiff { + became_watched: Vec::new(), + became_unwatched: vec![NodeId(1)], + }); + flush_if(&watch, |_| false); assert_eq!(live.get(), 0); } } diff --git a/crates/vertigo/src/reactive/invariants.md b/crates/vertigo/src/reactive/invariants.md new file mode 100644 index 000000000..e30e85e99 --- /dev/null +++ b/crates/vertigo/src/reactive/invariants.md @@ -0,0 +1,102 @@ +# Reactive graph — invariants + +One `Graph` owns a set of `Value`, `Computed`, and subscription nodes. +Nodes from different graphs do not see each other. + +A **wave** is one run of `propagate`. A **transaction** batches writes. +**Cutoff** means we skip dependents when a value did not change. +**Connect** / **disconnect** start and stop external work (`when_connect`). + +## How a write runs + +1. `set` or `transaction` writes values and marks them dirty. +2. The outermost transaction starts a wave. +3. The wave refreshes ready nodes. Unchanged nodes do not dirty their children. +4. After the wave: connect and disconnect. +5. Then `on_after_transaction` hooks. + +A `set` from `when_connect` starts this again. +A `set` from compute or subscribe is logged and ignored. It never writes. + +## Invariants + +### 1. Compute and subscribe must not write + +Do not call `Value::set` (or `change`) from: + +- a compute closure +- a subscribe callback +- a wave that is already running + +Compute only reads. Subscribe only talks to the outside world (DOM, logs). +Neither may write back into the graph. + +If they do, the write is ignored and the console gets: + +```text +vertigo: Value::set is not allowed from a computed, a subscribe callback, or during propagation +``` + +You may write from click/input handlers, timers, fetch, sockets, +`on_after_transaction`, and `when_connect` / `Value::with_connect`. + +### 2. Connect and disconnect wait until the wave is done + +`when_connect` does not run the moment a node gets a child. +Disconnect does not run the moment it loses the last child. +Both wait until the wave ends. +If the graph is idle, they run at once. + +If a node is watched and then unwatched in the same wave, nothing happens. +If it is unwatched and then watched, it connects once. + +`when_connect` runs after the wave, so `Value::with_connect` may call `set`. +That `set` is a new transaction and a new wave. + +### 3. Only the outermost transaction starts a wave + +`Value::set` is a transaction. +A `transaction` inside another `transaction` only writes and marks dirty. +The wave starts when the outer call returns. + +### 4. Unchanged values stop the update + +After a refresh, children run only if the new value is different (`PartialEq`). +That is why `Computed` needs `T: PartialEq`. + +A subscription has no children. It does not pass the change on. + +### 5. Dependencies come from `get`, not from a declaration + +Calling `get` records that node as a parent of the one that is computing. +The next run replaces the whole parent list. +If a computed stops reading a node, it no longer depends on it. + +A child keeps its parents alive (strong refs). +The graph does not (weak refs). +Drop the last handle of a node, and the node is removed. + +### 6. A wave walks dirty nodes whose parents are done + +A dirty node is ready when none of its parents are still dirty. +If dirty nodes remain and none are ready, there is a cycle, and the program panics. + +A node may refresh more than once in one wave. +That happens when a parent becomes dirty after the node was already queued. +Extra refreshes are fine. Stopping early is not. See invariant 7. + +### 7. After a wave, every value is correct + +When the wave ends, every `Value` and `Computed` matches the current sources. +A subscriber may see a half-updated value *during* the wave. +It must not still see that value when the wave is over. + +The wave runs until nothing is dirty. +More work is better than a wrong node. +A cycle panics. Nodes are not dropped to "save" the wave. + +### 8. Graphs do not mix + +`Value::new` and `Computed::from` use one graph per thread. +`Graph::new()` makes a separate graph. +A write on graph A never sees nodes of graph B. diff --git a/crates/vertigo/src/reactive/invariants.pl.md b/crates/vertigo/src/reactive/invariants.pl.md new file mode 100644 index 000000000..3211c0897 --- /dev/null +++ b/crates/vertigo/src/reactive/invariants.pl.md @@ -0,0 +1,104 @@ +# Graf reaktywny — niezmienniki + +(wersja robocza) + +Jeden `Graph` trzyma węzły `Value`, `Computed` i subskrypcje. +Węzły z różnych grafów nie widzą się nawzajem. + +**Fala** to jedno odpalenie `propagate`. **Transakcja** zbiera zapisy. +**Cutoff** znaczy: pomijamy dzieci, gdy wartość się nie zmieniła. +**Connect** / **disconnect** włączają i wyłączają pracę na zewnątrz (`when_connect`). + +## Jak przebiega zapis + +1. `set` albo `transaction` zapisuje wartości i oznacza je jako brudne. +2. Najbardziej zewnętrzna transakcja startuje falę. +3. Fala odświeża gotowe węzły. Niezmienione węzły nie brudzą dzieci. +4. Po fali: connect i disconnect. +5. Potem hooki `on_after_transaction`. + +`set` z `when_connect` odpala to od nowa. +`set` z compute albo subscribe leci na konsolę jako błąd i jest ignorowany. Nic nie zapisuje. + +## Niezmienniki + +### 1. Compute i subscribe nie mogą pisać + +Nie wołaj `Value::set` (ani `change`) z: + +- funkcji compute +- callbacku subscribe +- fali, która już trwa + +Compute tylko czyta. Subscribe tylko gada ze światem (DOM, logi). +Żadne z nich nie może zapisać z powrotem do grafu. + +Jeśli taka próba się odbędzie, zapis jest ignorowany i na konsolę leci: + +```text +vertigo: Value::set is not allowed from a computed, a subscribe callback, or during propagation +``` + +Pisać wolno z handlerów click/input, timerów, fetcha, socketów, +`on_after_transaction` oraz `when_connect` / `Value::with_connect`. + +### 2. Connect i disconnect czekają na koniec fali + +`when_connect` nie leci w chwili, gdy węzeł dostaje dziecko. +Disconnect nie leci w chwili, gdy traci ostatnie dziecko. +Oba czekają, aż fala się skończy. +Gdy graf nic nie liczy, lecą od razu. + +Jeśli węzeł w jednej fali był obserwowany i przestał być — nic się nie dzieje. +Jeśli nie był, a potem zaczął być — connect leci raz. + +`when_connect` leci po fali, więc `Value::with_connect` może wołać `set`. +Ten `set` to nowa transakcja i nowa fala. + +### 3. Falę startuje tylko zewnętrzna transakcja + +`Value::set` sam jest transakcją. +`transaction` wewnątrz innej `transaction` tylko zapisuje i oznacza brudne. +Fala startuje, gdy wraca zewnętrzne wywołanie. + +### 4. Niezmieniona wartość zatrzymuje aktualizację + +Po odświeżeniu dzieci lecą tylko wtedy, gdy nowa wartość jest inna (`PartialEq`). +Dlatego `Computed` wymaga `T: PartialEq`. + +Subskrypcja nie ma dzieci. Nie przekazuje zmiany dalej. + +### 5. Zależności biorą się z `get`, nie z deklaracji + +Wywołanie `get` zapisuje ten węzeł jako rodzica tego, który właśnie się liczy. +Następne odpalenie podmienia całą listę rodziców. +Jeśli computed przestaje czytać węzeł, przestaje od niego zależeć. + +Dziecko trzyma rodziców przy życiu (silne referencje). +Graf nie (słabe referencje). +Upuść ostatni uchwyt węzła — węzeł znika. + +### 6. Fala bierze brudne węzły, których rodzice są już gotowi + +Brudny węzeł jest gotowy, gdy żaden z jego rodziców nie jest już brudny. +Jeśli zostają brudne węzły i żaden nie jest gotowy — jest cykl, program panikuje. + +W jednej fali węzeł może odświeżyć się więcej niż raz. +Tak jest, gdy rodzic staje się brudny już po wstawieniu węzła do kolejki. +Dodatkowe odświeżenia są w porządku. Przerwanie w pół drogi — nie. Patrz niezmiennik 7. + +### 7. Po fali każda wartość jest poprawna + +Gdy fala się kończy, każdy `Value` i `Computed` zgadza się z bieżącymi źródłami. +Subskrybent może w *trakcie* fali zobaczyć wartość w połowie drogi. +Nie może jej dalej widzieć, gdy fala już się skończyła. + +Fala trwa, aż nic nie jest brudne. +Lepiej policzyć za dużo, niż zostawić zły węzeł. +Cykl panikuje. Węzłów nie odrzucamy, żeby „uratować” falę. + +### 8. Grafy się nie mieszają + +`Value::new` i `Computed::from` używają jednego grafu na wątek. +`Graph::new()` robi osobny graf. +Zapis w grafie A nigdy nie widzi węzłów grafu B. diff --git a/crates/vertigo/src/reactive/mod.rs b/crates/vertigo/src/reactive/mod.rs index 5c60195dc..8647cea73 100644 --- a/crates/vertigo/src/reactive/mod.rs +++ b/crates/vertigo/src/reactive/mod.rs @@ -3,18 +3,22 @@ //! After a transaction, dirty nodes are processed in topological order (a node is //! ready when none of its parents are dirty). After a node becomes clean, dependents //! are enqueued **only if** the node's value changed ([`PartialEq`]). +//! +//! Domain invariants for this bounded context: [`invariants`]. mod computed; mod context; mod drop_resource; mod graph; +#[doc = include_str!("invariants.md")] +pub mod invariants {} mod to_computed; mod value; pub use computed::Computed; pub use context::Context; pub use drop_resource::DropResource; -pub use graph::{Graph, GraphId}; +pub use graph::{Graph, GraphId, Logger, LoggerListener}; pub use to_computed::ToComputed; pub use value::Value; diff --git a/crates/vertigo/src/reactive/propagation_order.rs b/crates/vertigo/src/reactive/propagation_order.rs index 5e4305e30..57e3cbde0 100644 --- a/crates/vertigo/src/reactive/propagation_order.rs +++ b/crates/vertigo/src/reactive/propagation_order.rs @@ -23,6 +23,7 @@ use std::{cell::Cell, cell::RefCell, rc::Rc}; #[test] fn static_unequal_path_lengths_recompute_twice() { let g = Graph::new(); + let logs = g.logger().listen(); let d1 = g.value(1i32); let d2 = g.value(1i32); @@ -79,6 +80,7 @@ fn static_unequal_path_lengths_recompute_twice() { // 7001 is a state that never existed: new `b1`, old `b2`. // assert_eq!(runs.get(), 1); // assert_eq!(*seen.borrow(), vec![7009]); + logs.assert_eq(&[]); } /// A conditional read adds a second source of the same problem: a parent discovered @@ -91,6 +93,7 @@ fn static_unequal_path_lengths_recompute_twice() { #[test] fn dynamic_branch_switch_recomputes_three_times() { let g = Graph::new(); + let logs = g.logger().listen(); let flag = g.value(true); let d1 = g.value(1i32); let d2 = g.value(1i32); @@ -197,6 +200,7 @@ fn dynamic_branch_switch_recomputes_three_times() { assert_eq!(seen_4, vec![908, 1508, 1509]); // assert_eq!(runs_4, 1); // assert_eq!(seen_4, vec![1509]); + logs.assert_eq(&[]); } /// Build a fan-in: `paths` chains of *different* lengths, all rooted in values written in @@ -209,6 +213,7 @@ fn dynamic_branch_switch_recomputes_three_times() { /// Returns how many times `c` computed, and the last value its subscriber saw. fn fan_in(paths: usize) -> (u32, Option) { let g = Graph::new(); + let logs = g.logger().listen(); let roots = (0..paths).map(|_| g.value(0i32)).collect::>(); let mut tails = Vec::new(); @@ -249,12 +254,12 @@ fn fan_in(paths: usize) -> (u32, Option) { } }); + logs.assert_eq(&[]); (runs.get(), last.get()) } /// The extra runs are not a constant overhead - the count grows with the number of paths -/// of differing length that reach the node. This is the ramp that the next test drives -/// past the budget. +/// of differing length that reach the node. #[test] fn one_extra_run_per_incoming_path() { for paths in [2usize, 3, 5, 10] { @@ -267,29 +272,17 @@ fn one_extra_run_per_incoming_path() { } } -/// Push that ramp past [`MAX_REFRESH_PER_NODE`] and the budget - meant as a cycle -/// diagnostic - fires on a graph that has no cycle at all. -/// -/// `c` is refreshed once per path, so with `MAX_REFRESH_PER_NODE + 1` paths the last -/// refresh is refused, `vertigo: node NodeId(..) refreshed 100 times in one run - -/// dropping (possible cycle).` is logged, and `c` is left holding a value that is missing -/// the contribution of the longest path. -/// -/// This is the ordering defect at its worst: not just a wasted recomputation and a glitch, -/// but a permanently wrong value in a correct program. The pull-on-read fix removes it - -/// with one refresh per pass the budget is untouchable by acyclic graphs. +/// Many unequal paths still recompute `c` once per path, but the wave is not cut off: +/// the last value includes every path. #[test] -fn enough_paths_burn_the_refresh_budget() { - let paths = super::graph::MAX_REFRESH_PER_NODE as usize + 1; +fn many_unequal_paths_still_settle_to_the_correct_value() { + let paths = 101usize; let correct_sum = (paths * (paths + 1) / 2) as i32; let (runs, last) = fan_in(paths); - assert_eq!(runs, super::graph::MAX_REFRESH_PER_NODE); - // The subscriber did see values, just never the right one: the longest path is missing. - assert_ne!(last, Some(correct_sum), "the last path never reached `c`"); - assert_eq!(last, Some(correct_sum - paths as i32)); + assert_eq!(runs as usize, paths); + assert_eq!(last, Some(correct_sum)); // assert_eq!(runs, 1); - // assert_eq!(last, Some(correct_sum)); } diff --git a/crates/vertigo/src/reactive/tests.rs b/crates/vertigo/src/reactive/tests.rs index 385cf85ac..f9284fee9 100644 --- a/crates/vertigo/src/reactive/tests.rs +++ b/crates/vertigo/src/reactive/tests.rs @@ -5,6 +5,7 @@ use super::{Computed, DropResource, Graph, Value}; #[test] fn basic_sum() { let g = Graph::new(); + let logs = g.logger().listen(); let a = g.value(1); let b = g.value(2); let sum = g.computed({ @@ -21,11 +22,13 @@ fn basic_sum() { g.transaction(|ctx| { assert_eq!(sum.get(ctx), 6); }); + logs.assert_eq(&[]); } #[test] fn two_sets_in_one_transaction_compute_once() { let g = Graph::new(); + let logs = g.logger().listen(); let a = g.value(0); let b = g.value(0); let runs = Rc::new(Cell::new(0)); @@ -53,11 +56,13 @@ fn two_sets_in_one_transaction_compute_once() { assert_eq!(sum.get(ctx), 30); }); assert_eq!(runs.get(), 1); + logs.assert_eq(&[]); } #[test] fn cutoff_leaves_fanout_untouched() { let g = Graph::new(); + let logs = g.logger().listen(); let a = g.value(1); let even = g.computed({ let a = a.clone(); @@ -105,11 +110,13 @@ fn cutoff_leaves_fanout_untouched() { 10_000, "odd→even: fan-out must run once each" ); + logs.assert_eq(&[]); } #[test] fn diamond_waits_for_both_parents() { let g = Graph::new(); + let logs = g.logger().listen(); let a = g.value(1); let left = g.computed({ let a = a.clone(); @@ -142,11 +149,13 @@ fn diamond_waits_for_both_parents() { assert_eq!(d.get(ctx), 23); }); assert_eq!(d_runs.get(), 1); + logs.assert_eq(&[]); } #[test] fn subscribe_skips_when_parent_cuts_off() { let g = Graph::new(); + let logs = g.logger().listen(); let a = g.value(1); let even = g.computed({ let a = a.clone(); @@ -173,11 +182,13 @@ fn subscribe_skips_when_parent_cuts_off() { a.set(4); assert_eq!(fires.get(), 2); assert!(last.get()); + logs.assert_eq(&[]); } #[test] fn equal_set_does_not_enqueue() { let g = Graph::new(); + let logs = g.logger().listen(); let a = g.value(5); let runs = Rc::new(Cell::new(0)); let c = g.computed({ @@ -196,10 +207,12 @@ fn equal_set_does_not_enqueue() { a.set(5); assert_eq!(runs.get(), 0); + logs.assert_eq(&[]); } #[test] fn default_graph_value_new() { + let logs = super::default_graph().logger().listen(); let a = Value::new(1); let double = Computed::from({ let a = a.clone(); @@ -214,10 +227,12 @@ fn default_graph_value_new() { super::transaction(|ctx| { assert_eq!(double.get(ctx), 10); }); + logs.assert_eq(&[]); } #[test] fn when_connect_tracks_watchers() { + let logs = super::default_graph().logger().listen(); let connect_count = Rc::new(Cell::new(0)); let disconnect_count = Rc::new(Cell::new(0)); @@ -248,10 +263,12 @@ fn when_connect_tracks_watchers() { assert_eq!(connect_count.get(), 1); assert_eq!(disconnect_count.get(), 1); + logs.assert_eq(&[]); } #[test] fn when_connect_multiple_subscribers_share_one_connection() { + let logs = super::default_graph().logger().listen(); let connect_count = Rc::new(Cell::new(0)); let disconnect_count = Rc::new(Cell::new(0)); @@ -281,10 +298,12 @@ fn when_connect_multiple_subscribers_share_one_connection() { drop(drop2); assert_eq!(disconnect_count.get(), 1); + logs.assert_eq(&[]); } #[test] fn when_connect_disconnects_while_computed_lives() { + let logs = super::default_graph().logger().listen(); let connect = Rc::new(Cell::new(0)); let disconnect = Rc::new(Cell::new(0)); let value = Value::new(1); @@ -305,10 +324,60 @@ fn when_connect_disconnects_while_computed_lives() { drop(sub); assert_eq!(disconnect.get(), 1); drop(keep); + logs.assert_eq(&[]); +} + +#[test] +fn when_connect_runs_after_subscribe_callback() { + let g = Graph::new(); + let logs = g.logger().listen(); + let order = Rc::new(std::cell::RefCell::new(Vec::new())); + let value = g.value(1); + let comp = value.to_computed().when_connect({ + let order = order.clone(); + move || { + order.borrow_mut().push("connect"); + DropResource::new(|| {}) + } + }); + let _sub = comp.subscribe({ + let order = order.clone(); + move |_| order.borrow_mut().push("subscribe") + }); + assert_eq!(*order.borrow(), ["subscribe", "connect"]); + logs.assert_eq(&[]); +} + +#[test] +fn watch_and_unwatch_in_one_transaction_does_not_connect() { + let g = Graph::new(); + let logs = g.logger().listen(); + let connects = Rc::new(Cell::new(0)); + let disconnects = Rc::new(Cell::new(0)); + let value = g.value(1); + let comp = value.to_computed().when_connect({ + let connects = connects.clone(); + let disconnects = disconnects.clone(); + move || { + connects.set(connects.get() + 1); + DropResource::new({ + let disconnects = disconnects.clone(); + move || disconnects.set(disconnects.get() + 1) + }) + } + }); + g.transaction(|_| { + let sub = comp.subscribe(|_| {}); + drop(sub); + }); + assert_eq!(connects.get(), 0); + assert_eq!(disconnects.get(), 0); + logs.assert_eq(&[]); } #[test] fn nested_computed_subscription() { + let logs = super::default_graph().logger().listen(); let token_value = Value::new("token1".to_string()); let token_computed = token_value.to_computed(); @@ -335,10 +404,12 @@ fn nested_computed_subscription() { token_value.set("token2".to_string()); assert_eq!(counter.get(), 2); + logs.assert_eq(&[]); } #[test] fn nested_computed_subscription_no_flattening() { + let logs = super::default_graph().logger().listen(); let token_value = Value::new("token1".to_string()); let token_computed = token_value.to_computed(); @@ -360,10 +431,12 @@ fn nested_computed_subscription_no_flattening() { token_value.set("token2".to_string()); assert_eq!(counter.get(), 1); + logs.assert_eq(&[]); } #[test] fn on_after_transaction_fires_after_set() { + let logs = super::default_graph().logger().listen(); let fires = Rc::new(Cell::new(0)); let _hook = super::on_after_transaction({ let fires = fires.clone(); @@ -375,135 +448,66 @@ fn on_after_transaction_fires_after_set() { let a = Value::new(1); a.set(2); assert_eq!(fires.get(), 1); + logs.assert_eq(&[]); } #[test] -fn hook_once_when_set_during_propagate() { +fn set_from_subscribe_is_ignored() { let g = Graph::new(); - let fires = Rc::new(Cell::new(0)); - let _hook = g.on_after_transaction({ - let fires = fires.clone(); - move || fires.set(fires.get() + 1) - }); + let logs = g.logger().listen(); let a = g.value(0); let b = g.value(0); let _sub = a.to_computed().subscribe({ let b = b.clone(); move |_| b.set(1) }); - fires.set(0); - a.set(1); - assert_eq!(fires.get(), 1); -} + logs.assert_eq(&[super::graph::BLOCKED_WRITE]); -/// A callback that writes back into its own dependency never lets the wave drain. -/// Without a budget this spins forever; the budget cuts the cycle instead. -#[test] -fn runaway_write_back_is_cut_off() { - let g = Graph::new(); - let a = g.value(0); - let runs = Rc::new(Cell::new(0)); - let _sub = a.to_computed().subscribe({ - let a = a.clone(); - let runs = runs.clone(); - move |value| { - runs.set(runs.get() + 1); - a.set(value + 1); - } - }); - - a.set(100); - - let budget = super::graph::MAX_REFRESH_PER_NODE as usize; - assert!( - runs.get() <= budget + 2, - "loop ran {} times, budget is {budget}", - runs.get() - ); -} - -/// Cutting a cycle must not take the rest of the graph with it: unrelated nodes still -/// settle in that same wave, and the graph keeps working afterwards. -#[test] -fn cut_off_cycle_leaves_the_rest_of_the_graph_working() { - let g = Graph::new(); - let looping = g.value(0); - let _cycle = looping.to_computed().subscribe({ - let looping = looping.clone(); - move |value| looping.set(value + 1) + g.transaction(|ctx| { + assert_eq!(b.get(ctx), 0); }); + logs.assert_eq(&[]); - let other = g.value(0); - let seen = Rc::new(Cell::new(0)); - let _sub = other.to_computed().subscribe({ - let seen = seen.clone(); - move |value| seen.set(value) - }); + a.set(2); + logs.assert_eq(&[super::graph::BLOCKED_WRITE]); - // Same wave as the runaway one. - g.transaction(|_| { - looping.set(100); - other.set(7); + g.transaction(|ctx| { + assert_eq!(b.get(ctx), 0); }); - assert_eq!(seen.get(), 7, "unrelated node must still settle"); - - // And the graph is still usable once the wave is over. - other.set(9); - assert_eq!(seen.get(), 9); + logs.assert_eq(&[]); } -/// A feedback loop that settles is allowed - the budget must not fire on it. #[test] -fn settling_write_back_is_allowed() { +fn set_from_compute_is_ignored() { let g = Graph::new(); + let logs = g.logger().listen(); let a = g.value(0); - let runs = Rc::new(Cell::new(0)); - let _sub = a.to_computed().subscribe({ + let b = g.value(0); + let c = g.computed({ let a = a.clone(); - let runs = runs.clone(); - move |value| { - runs.set(runs.get() + 1); - // Clamp: walks up to 10 and then stops writing. - if value < 10 { - a.set(value + 1); - } + let b = b.clone(); + move |ctx| { + b.set(a.get(ctx)); + a.get(ctx) } }); + logs.assert_eq(&[]); - a.set(5); - - assert_eq!(g.transaction(|ctx| a.get(ctx)), 10); - assert!(runs.get() >= 5, "callback ran {} times", runs.get()); -} - -/// A write from a callback's very first run must not be lost. The callback runs before its -/// parents are registered, so the wave that write starts cannot reach this node yet. -#[test] -fn write_from_first_subscribe_run_is_not_lost() { - let g = Graph::new(); - let a = g.value(0); - let seen = Rc::new(std::cell::RefCell::new(Vec::new())); - - let _sub = a.to_computed().subscribe({ - let a = a.clone(); - let seen = seen.clone(); - move |value| { - seen.borrow_mut().push(value); - if value == 0 { - a.set(1); - } - } + g.transaction(|ctx| { + assert_eq!(c.get(ctx), 0); + assert_eq!(b.get(ctx), 0); }); - - assert_eq!(*seen.borrow(), vec![0, 1]); - assert_eq!(g.transaction(|ctx| a.get(ctx)), 1); + logs.assert_eq(&[super::graph::BLOCKED_WRITE]); + logs.assert_eq(&[]); } -/// `when_connect` fires while the node it belongs to is being computed. A write from there -/// must reach the subscriber too, not just land in the value. +/// `when_connect` runs after the wave, so a write from there is a new transaction +/// and must reach the subscriber too, not just land in the value. #[test] fn write_from_when_connect_reaches_the_subscriber() { let g = Graph::new(); + let logs = g.logger().listen(); + let source = g.value(0); let observed = g .computed({ @@ -525,4 +529,5 @@ fn write_from_when_connect_reaches_the_subscriber() { }); assert_eq!(*seen.borrow(), vec![0, 7]); + logs.assert_eq(&[]); } diff --git a/crates/vertigo/src/reactive/value.rs b/crates/vertigo/src/reactive/value.rs index 38ccc82be..47c9bebcf 100644 --- a/crates/vertigo/src/reactive/value.rs +++ b/crates/vertigo/src/reactive/value.rs @@ -89,6 +89,9 @@ impl Value { /// Create a value that is connected to a generator. `value` is the starting /// value; `create` is responsible for keeping it up to date. + /// + /// `create` runs after the wave in which this value starts being observed, so it + /// may call [`Value::set`](Self::set). A `set` from compute or subscribe is ignored. pub fn with_connect(value: T, create: F) -> Computed where F: Fn(&Value) -> DropResource + 'static, @@ -106,6 +109,9 @@ impl Value { } pub fn set(&self, value: T) { + if !self.inner.graph.writes_allowed() { + return; + } let graph = Graph { inner: self.inner.graph.clone(), }; From 7370b047e888b528c29982d3c90ca7ae799212e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pokrywka?= Date: Fri, 21 Aug 2026 18:47:34 +0200 Subject: [PATCH 09/10] fix: prevent reentrant flush recursion and ensure nodes disconnected during connect are properly cleaned up --- crates/vertigo/src/reactive/graph/watch.rs | 89 +++++++++++++++++++- crates/vertigo/src/reactive/invariants.md | 4 + crates/vertigo/src/reactive/invariants.pl.md | 4 + crates/vertigo/src/reactive/tests.rs | 52 ++++++++++++ 4 files changed, 148 insertions(+), 1 deletion(-) diff --git a/crates/vertigo/src/reactive/graph/watch.rs b/crates/vertigo/src/reactive/graph/watch.rs index b4aadaa46..bb34a4be6 100644 --- a/crates/vertigo/src/reactive/graph/watch.rs +++ b/crates/vertigo/src/reactive/graph/watch.rs @@ -1,5 +1,5 @@ use std::{ - cell::RefCell, + cell::{Cell, RefCell}, collections::{HashMap, HashSet}, rc::Rc, }; @@ -15,6 +15,18 @@ pub(super) struct Watch { connect: RefCell DropResource>>>, connected: RefCell>, pending: RefCell>, + flushing: Cell, +} + +/// Clears `flushing` when the flush ends (including panic). +struct Flushing<'a> { + watch: &'a Watch, +} + +impl Drop for Flushing<'_> { + fn drop(&mut self) { + self.watch.flushing.set(false); + } } impl Watch { @@ -23,6 +35,7 @@ impl Watch { connect: RefCell::new(HashMap::new()), connected: RefCell::new(HashMap::new()), pending: RefCell::new(HashSet::new()), + flushing: Cell::new(false), } } @@ -64,7 +77,21 @@ impl Watch { /// Match connectedness to `is_watched`. No-op when a node was watched and unwatched /// before this flush (net unchanged, never connected). + /// + /// Reentrant calls return at once and leave their work in `pending`, for the loop + /// below to pick up. A `connect` closure may write, and a write runs a whole wave - + /// transaction, propagation, and another flush - before the closure returns. Letting + /// that inner flush run would judge this node while its resource is not in + /// `connected` yet: a node unwatched by its own connect would keep the resource + /// forever, and one re-watched there would connect twice, the second resource + /// silently replacing the first. pub(super) fn flush(&self, is_watched: impl Fn(NodeId) -> bool) { + if self.flushing.get() { + return; + } + self.flushing.set(true); + let _guard = Flushing { watch: self }; + loop { let pending: Vec = self.pending.borrow_mut().drain().collect(); if pending.is_empty() { @@ -184,6 +211,66 @@ mod tests { assert_eq!(live.get(), 0); } + /// A `connect` closure that writes runs a whole wave before it returns, and that wave + /// ends in another flush. Here the wave costs the node its last child: the resource + /// the closure is about to return is already stale, and must not stay alive. + #[test] + fn connect_that_unwatches_itself_is_disconnected() { + let watch = Rc::new(Watch::new()); + let live = Rc::new(Cell::new(0)); + let watched = Rc::new(Cell::new(true)); + + let connect: Rc DropResource> = Rc::new({ + let watch = Rc::downgrade(&watch); + let live = live.clone(); + let watched = watched.clone(); + move || { + let inner = connect_flag(&live)(); + // What a write from `connect` amounts to: the node loses its last child, + // and the wave ends by flushing again. + watched.set(false); + if let Some(watch) = watch.upgrade() { + watch.schedule(NodeId(1)); + let watched = watched.clone(); + watch.flush(move |_| watched.get()); + } + inner + } + }); + + watch.register(NodeId(1), connect, true); + let is_watched = watched.clone(); + watch.flush(move |_| is_watched.get()); + + assert_eq!(live.get(), 0, "unwatched, so it must not stay connected"); + } + + /// Same reentrancy, but the node is still watched afterwards: the inner flush must not + /// connect a second time. Without the guard this recurses until the stack runs out. + #[test] + fn reentrant_flush_does_not_connect_twice() { + let watch = Rc::new(Watch::new()); + let connects = Rc::new(Cell::new(0)); + + let connect: Rc DropResource> = Rc::new({ + let watch = Rc::downgrade(&watch); + let connects = connects.clone(); + move || { + connects.set(connects.get() + 1); + if let Some(watch) = watch.upgrade() { + watch.schedule(NodeId(1)); + watch.flush(|_| true); + } + DropResource::new(|| {}) + } + }); + + watch.register(NodeId(1), connect, true); + watch.flush(|_| true); + + assert_eq!(connects.get(), 1); + } + #[test] fn watch_and_unwatch_before_flush_does_not_connect() { let watch = Watch::new(); diff --git a/crates/vertigo/src/reactive/invariants.md b/crates/vertigo/src/reactive/invariants.md index e30e85e99..0d25262be 100644 --- a/crates/vertigo/src/reactive/invariants.md +++ b/crates/vertigo/src/reactive/invariants.md @@ -53,6 +53,10 @@ If it is unwatched and then watched, it connects once. `when_connect` runs after the wave, so `Value::with_connect` may call `set`. That `set` is a new transaction and a new wave. +That wave can change who is watched, including the node that is connecting right now. +The connect state is matched to the graph again once the closure returns. +A node unwatched by its own connect is disconnected. It never stays connected. + ### 3. Only the outermost transaction starts a wave `Value::set` is a transaction. diff --git a/crates/vertigo/src/reactive/invariants.pl.md b/crates/vertigo/src/reactive/invariants.pl.md index 3211c0897..1993a8ef2 100644 --- a/crates/vertigo/src/reactive/invariants.pl.md +++ b/crates/vertigo/src/reactive/invariants.pl.md @@ -55,6 +55,10 @@ Jeśli nie był, a potem zaczął być — connect leci raz. `when_connect` leci po fali, więc `Value::with_connect` może wołać `set`. Ten `set` to nowa transakcja i nowa fala. +Ta fala może zmienić to, kto jest obserwowany — także węzeł, który właśnie się łączy. +Po powrocie z domknięcia stan połączeń jest ponownie dopasowywany do grafu. +Węzeł, który sam siebie przestał obserwować, dostaje disconnect. Nie zostaje połączony. + ### 3. Falę startuje tylko zewnętrzna transakcja `Value::set` sam jest transakcją. diff --git a/crates/vertigo/src/reactive/tests.rs b/crates/vertigo/src/reactive/tests.rs index f9284fee9..c785ba6d8 100644 --- a/crates/vertigo/src/reactive/tests.rs +++ b/crates/vertigo/src/reactive/tests.rs @@ -348,6 +348,58 @@ fn when_connect_runs_after_subscribe_callback() { logs.assert_eq(&[]); } +/// A `when_connect` closure may write, and that write runs a whole wave before the +/// closure returns. If the wave takes the last child away from the node that is +/// connecting, the resource it returns must be dropped - the node is no longer watched, +/// so nothing is left to keep the external work alive. +#[test] +fn connect_that_unwatches_itself_disconnects() { + let g = Graph::new(); + let logs = g.logger().listen(); + let connects = Rc::new(Cell::new(0)); + let disconnects = Rc::new(Cell::new(0)); + + let flag = g.value(true); + let source = g.value(1); + + let connected = source.to_computed().when_connect({ + let connects = connects.clone(); + let disconnects = disconnects.clone(); + let flag = flag.clone(); + move || { + connects.set(connects.get() + 1); + // Legal from `when_connect`, and it costs `connected` its only child. + flag.set(false); + DropResource::new({ + let disconnects = disconnects.clone(); + move || disconnects.set(disconnects.get() + 1) + }) + } + }); + + let reader = g.computed({ + let flag = flag.clone(); + let connected = connected.clone(); + move |ctx| { + if flag.get(ctx) { connected.get(ctx) } else { 0 } + } + }); + let _sub = reader.subscribe(|_| {}); + + assert_eq!(connects.get(), 1); + assert_eq!( + disconnects.get(), + 1, + "unwatched, so it must not stay connected" + ); + + // And it stays disconnected: no later wave revives it. + source.set(2); + assert_eq!(connects.get(), 1); + assert_eq!(disconnects.get(), 1); + logs.assert_eq(&[]); +} + #[test] fn watch_and_unwatch_in_one_transaction_does_not_connect() { let g = Graph::new(); From 10569e08b277f4dd09434d7a9b4417c19a8e55a0 Mon Sep 17 00:00:00 2001 From: szagi3891 Date: Fri, 21 Aug 2026 20:03:21 +0200 Subject: [PATCH 10/10] Refresh each node at most once per wave without marking the whole fan-out. get pulls a stale ancestor before returning the cache, so a join sees every branch. Unchanged nodes still skip their children, so cutoff stays cheap. --- crates/vertigo/docs/reactive-graph.md | 36 +---- crates/vertigo/src/reactive/computed.rs | 1 + crates/vertigo/src/reactive/graph/dirty.rs | 137 +++++++++++++++++- crates/vertigo/src/reactive/graph/edges.rs | 7 + crates/vertigo/src/reactive/graph/mod.rs | 83 ++++++++--- .../vertigo/src/reactive/graph/transaction.rs | 11 +- crates/vertigo/src/reactive/invariants.md | 15 +- crates/vertigo/src/reactive/invariants.pl.md | 15 +- crates/vertigo/src/reactive/mod.rs | 5 +- .../vertigo/src/reactive/propagation_order.rs | 125 ++++++---------- crates/vertigo/src/reactive/value.rs | 1 + 11 files changed, 280 insertions(+), 156 deletions(-) diff --git a/crates/vertigo/docs/reactive-graph.md b/crates/vertigo/docs/reactive-graph.md index a173cb4ac..43b2ea50d 100644 --- a/crates/vertigo/docs/reactive-graph.md +++ b/crates/vertigo/docs/reactive-graph.md @@ -55,6 +55,9 @@ value written ──> node recomputed ──> value changed? ──yes──> de └──> stop ``` +If a compute reads a parent that is still stale, that parent is refreshed before `get` +returns. A node therefore sees only fresh parents, and it refreshes at most once. + That last step is the **equality cutoff**. A node's dependents are queued only when its new value differs (`PartialEq`) from the old one. A change that computes back to the same value therefore stops where it is instead of travelling through the graph. This is why `Computed` @@ -96,10 +99,6 @@ selected_id.to_computed().subscribe(move |id| { Legal places to write: DOM/event handlers, timers, fetch/socket callbacks, `on_after_transaction`, and `when_connect` / `Value::with_connect`. -A node may still be recomputed more than once in one pass because of the ordering -limitation below. The wave always finishes; the last value of each node is the one that -matches the sources. - ## Connecting to the outside world [`when_connect`](crate::Computed::when_connect) runs a closure **after the wave** in which @@ -116,35 +115,6 @@ because `create` runs after the wave, it may write the `Value`. separate one; nodes belonging to different graphs never see each other. Tests use this to avoid sharing state. -## Known limitation: ordering within a pass - -A node can be recomputed more than once in a single pass, and a subscriber can observe an -intermediate value that corresponds to no consistent state of the inputs. - -Readiness is decided by counting how many of a node's parents are dirty *at the moment the -node is queued*. The dirty set is not known up front - it grows as the pass runs, because -the cutoff only queues a dependent once its parent's value really changed. So a node can be -declared ready while a parent that is about to become dirty has not been queued yet. When -that parent finally changes, the node runs again. - -Two paths of unequal length from two values written together are enough to show it: - -```text -d1 ─────────────> b1 ┐ - ├──> c -d2 ──> e2 ──> f2 ──> b2 ┘ -``` - -Writing `d1` and `d2` in one transaction computes `c` twice: once with the new `b1` and the -old `b2`, then again once `b2` catches up. A conditional read adds a second source of the -same problem, because a parent discovered during this run cannot have been waited for. - -The count is one run per incoming path, so it scales with the fan-in. The extra runs are -wasted work and can glitch a subscriber, but the last refresh is the correct value. The -tests in `reactive::propagation_order` pin that. The intended fix is to refresh a dirty -node when it is read instead of serving its cached value, which removes both the extra -runs and the intermediate values. - ## Coming from 0.12 * The old `Dependencies` type is gone; use [`transaction`](crate::transaction), diff --git a/crates/vertigo/src/reactive/computed.rs b/crates/vertigo/src/reactive/computed.rs index 21c5884a3..5a2634291 100644 --- a/crates/vertigo/src/reactive/computed.rs +++ b/crates/vertigo/src/reactive/computed.rs @@ -112,6 +112,7 @@ impl Computed { pub fn get(&self, ctx: &Context) -> T { ctx.track(self.inner.id, self.inner.clone()); + self.inner.graph.ensure_fresh(self.inner.id); self.inner.ensure() } diff --git a/crates/vertigo/src/reactive/graph/dirty.rs b/crates/vertigo/src/reactive/graph/dirty.rs index 3b1d4c11f..35c0ed27b 100644 --- a/crates/vertigo/src/reactive/graph/dirty.rs +++ b/crates/vertigo/src/reactive/graph/dirty.rs @@ -7,6 +7,10 @@ use super::{NodeId, edges::Edges}; /// Kahn-style worklist for one propagation wave. /// +/// Children are enqueued only when a parent’s value changed (equality cutoff). A join +/// node that runs before a later parent is therefore pulled to freshness in `get`, not +/// by marking the whole descendant set up front. +/// /// A dirty node is **ready** when none of its parents are still dirty. `dirty_parent_count` /// stores that remaining count; `0` (or missing) means the node can be processed. /// `scratch_children` is reused so `propagate` does not allocate a new `Vec` per node. @@ -15,6 +19,24 @@ pub(super) struct Dirty { dirty_parent_count: RefCell>, ready: RefCell>, scratch_children: RefCell>, + /// Already refreshed (or confirmed fresh) in this wave. At most one `refresh` per id. + done: RefCell>, + /// Subset of `done` whose value changed. + changed: RefCell>, + /// Nodes whose `refresh` is on the stack (gray). Re-entering one is a cycle. + refreshing: RefCell>, +} + +/// Clears the gray mark when `refresh` returns, including panic. +pub(super) struct Refreshing<'a> { + dirty: &'a Dirty, + id: NodeId, +} + +impl Drop for Refreshing<'_> { + fn drop(&mut self) { + self.dirty.refreshing.borrow_mut().remove(&self.id); + } } impl Dirty { @@ -24,14 +46,52 @@ impl Dirty { dirty_parent_count: RefCell::new(HashMap::new()), ready: RefCell::new(VecDeque::new()), scratch_children: RefCell::new(Vec::new()), + done: RefCell::new(HashSet::new()), + changed: RefCell::new(HashSet::new()), + refreshing: RefCell::new(HashSet::new()), } } + pub(super) fn begin_wave(&self) { + self.done.borrow_mut().clear(); + self.changed.borrow_mut().clear(); + } + pub(super) fn contains(&self, id: NodeId) -> bool { self.in_dirty.borrow().contains(&id) } + pub(super) fn is_done(&self, id: NodeId) -> bool { + self.done.borrow().contains(&id) + } + + pub(super) fn changed_this_wave(&self, id: NodeId) -> bool { + self.changed.borrow().contains(&id) + } + + pub(super) fn is_refreshing(&self, id: NodeId) -> bool { + self.refreshing.borrow().contains(&id) + } + + /// Mark `id` gray for the duration of `refresh`. Panic if it is already gray. + pub(super) fn enter_refresh(&self, id: NodeId) -> Refreshing<'_> { + if !self.refreshing.borrow_mut().insert(id) { + panic!("vertigo: cycle in dirty graph ({id:?})"); + } + Refreshing { dirty: self, id } + } + + pub(super) fn finish(&self, id: NodeId, changed: bool) { + self.done.borrow_mut().insert(id); + if changed { + self.changed.borrow_mut().insert(id); + } + } + pub(super) fn enqueue(&self, id: NodeId, edges: &Edges) { + if self.done.borrow().contains(&id) || self.refreshing.borrow().contains(&id) { + return; + } if !self.in_dirty.borrow_mut().insert(id) { return; } @@ -46,8 +106,10 @@ impl Dirty { pub(super) fn take_ready(&self) -> Option { let mut ready = self.ready.borrow_mut(); let in_dirty = self.in_dirty.borrow(); + let refreshing = self.refreshing.borrow(); + let done = self.done.borrow(); while let Some(id) = ready.pop_front() { - if in_dirty.contains(&id) { + if in_dirty.contains(&id) && !refreshing.contains(&id) && !done.contains(&id) { return Some(id); } } @@ -110,17 +172,26 @@ impl Dirty { fn release_from_scratch(&self) { let mut newly_ready = Vec::new(); + let mut zeroed = Vec::new(); { let children = self.scratch_children.borrow(); let mut counts = self.dirty_parent_count.borrow_mut(); + let refreshing = self.refreshing.borrow(); + let done = self.done.borrow(); for child in children.iter() { if let Some(count) = counts.get_mut(child) { *count = count.saturating_sub(1); if *count == 0 { - newly_ready.push(*child); + zeroed.push(*child); + if !refreshing.contains(child) && !done.contains(child) { + newly_ready.push(*child); + } } } } + for id in zeroed { + counts.remove(&id); + } } if !newly_ready.is_empty() { self.ready.borrow_mut().extend(newly_ready); @@ -232,4 +303,66 @@ mod tests { dirty.after_refresh(NodeId(1), false, &edges); assert!(!dirty.contains(NodeId(2))); } + + #[test] + fn cutoff_does_not_copy_or_enqueue_children() { + let dirty = Dirty::new(); + let edges = Edges::new(); + edges.replace(NodeId(2), vec![slot(1)]); + dirty.enqueue(NodeId(1), &edges); + dirty.dequeue(NodeId(1)); + dirty.after_refresh(NodeId(1), false, &edges); + assert!(!dirty.contains(NodeId(2))); + assert_eq!(dirty.take_ready(), None); + } + + #[test] + fn child_waits_for_both_dirty_parents() { + let dirty = Dirty::new(); + let edges = Edges::new(); + edges.replace(NodeId(3), vec![slot(1), slot(2)]); + dirty.enqueue(NodeId(1), &edges); + dirty.enqueue(NodeId(2), &edges); + dirty.enqueue(NodeId(3), &edges); + assert_eq!(dirty.wait_count(NodeId(3)), Some(2)); + + let first = dirty.take_ready().unwrap(); + assert!(first == NodeId(1) || first == NodeId(2)); + dirty.dequeue(first); + dirty.after_refresh(first, true, &edges); + + let second = dirty.take_ready().unwrap(); + assert!(second == NodeId(1) || second == NodeId(2)); + assert_ne!(second, first); + dirty.dequeue(second); + dirty.after_refresh(second, true, &edges); + assert_eq!(dirty.take_ready(), Some(NodeId(3))); + } + + #[test] + fn begin_wave_allows_enqueue_of_previously_done_node() { + let dirty = Dirty::new(); + let edges = Edges::new(); + dirty.finish(NodeId(1), true); + dirty.enqueue(NodeId(1), &edges); + assert!(!dirty.contains(NodeId(1))); + dirty.begin_wave(); + dirty.enqueue(NodeId(1), &edges); + assert!(dirty.contains(NodeId(1))); + } + + #[test] + fn enter_refresh_is_gray() { + let dirty = Dirty::new(); + let _guard = dirty.enter_refresh(NodeId(1)); + assert!(dirty.is_refreshing(NodeId(1))); + } + + #[test] + #[should_panic(expected = "cycle in dirty graph")] + fn reentering_refresh_is_a_cycle() { + let dirty = Dirty::new(); + let _guard = dirty.enter_refresh(NodeId(1)); + let _again = dirty.enter_refresh(NodeId(1)); + } } diff --git a/crates/vertigo/src/reactive/graph/edges.rs b/crates/vertigo/src/reactive/graph/edges.rs index bc6a5f1e6..5c7dc5b9b 100644 --- a/crates/vertigo/src/reactive/graph/edges.rs +++ b/crates/vertigo/src/reactive/graph/edges.rs @@ -54,6 +54,13 @@ impl Edges { } } + pub(super) fn copy_parents(&self, id: NodeId, buf: &mut Vec) { + buf.clear(); + if let Some(set) = self.child_parents.borrow().get(&id) { + buf.extend(set.iter().copied()); + } + } + /// Replace `child`'s parent set. `None` means the parent ids were already the same. pub(super) fn replace(&self, child: NodeId, pairs: ParentList) -> Option { // Collect the ids before comparing. Set-against-set is linear, while comparing the diff --git a/crates/vertigo/src/reactive/graph/mod.rs b/crates/vertigo/src/reactive/graph/mod.rs index 85d56bc70..7c82359aa 100644 --- a/crates/vertigo/src/reactive/graph/mod.rs +++ b/crates/vertigo/src/reactive/graph/mod.rs @@ -104,7 +104,9 @@ impl Graph { } pub fn transaction(&self, f: impl FnOnce(&Context) -> R) -> R { - self.inner.tx.enter(); + if self.inner.tx.enter() { + self.inner.dirty.begin_wave(); + } let ctx = Context::read(); let result = f(&ctx); if let Some(leave) = self.inner.tx.leave() { @@ -162,6 +164,63 @@ impl GraphInner { true } + /// During a wave, make `id` current before a `get` returns its cache. + /// + /// Dirty nodes are refreshed now. A node that is not dirty may still be stale when + /// an ancestor changed; parents are pulled first, and this node refreshes only if + /// one of them changed. Unchanged fan-out is never marked dirty (equality cutoff). + pub(crate) fn ensure_fresh(&self, id: NodeId) { + if !self.tx.is_propagating() || self.dirty.is_done(id) { + return; + } + if self.dirty.is_refreshing(id) { + panic!("vertigo: cycle in dirty graph ({id:?})"); + } + if self.dirty.contains(id) { + self.refresh_now(id); + return; + } + if self.parents_changed(id) { + self.refresh_now(id); + } + } + + fn parents_changed(&self, id: NodeId) -> bool { + let mut parents = Vec::new(); + self.edges.copy_parents(id, &mut parents); + let mut changed = false; + for parent in parents { + self.ensure_fresh(parent); + if self.dirty.changed_this_wave(parent) { + changed = true; + } + } + changed + } + + fn refresh_now(&self, id: NodeId) { + if self.dirty.is_done(id) { + return; + } + let _guard = self.dirty.enter_refresh(id); + + let Some(node) = self.nodes.upgrade(id) else { + if self.dirty.contains(id) { + self.dirty.dequeue(id); + self.dirty.after_refresh(id, false, &self.edges); + } + self.dirty.finish(id, false); + return; + }; + + let changed = node.refresh(); + if self.dirty.contains(id) { + self.dirty.dequeue(id); + } + self.dirty.after_refresh(id, changed, &self.edges); + self.dirty.finish(id, changed); + } + fn flush_watch(&self) { self.watch.flush(|id| self.edges.is_watched(id)); } @@ -191,19 +250,17 @@ impl GraphInner { self.flush_watch_if_idle(); } - /// Process dirty nodes in topological order. Dependents are enqueued only when - /// the node's value changed. + /// Process dirty nodes in topological order. A node refreshes at most once per wave. /// - /// `Value::set` from compute/subscribe is ignored, so a wave cannot be re-entered - /// by a write. A node may still refresh more than once because ordering inside a - /// wave is imperfect (see `reactive::propagation_order`): it runs once per incoming - /// path of differing length. The wave still drains, and the last refresh of each - /// node is the value that matches the sources. + /// Children are enqueued only when a parent’s value changed. `get` pulls a stale + /// ancestor (dirty, or a parent that changed this wave) before returning the cache, + /// so a join still sees every branch. Dependents of an unchanged node are not marked. pub(crate) fn propagate(&self) { if !self.tx.can_propagate() { return; } let _guard = self.tx.start_propagate(); + self.dirty.begin_wave(); loop { let Some(id) = self.dirty.take_ready() else { @@ -213,15 +270,7 @@ impl GraphInner { break; }; - self.dirty.dequeue(id); - - let Some(node) = self.nodes.upgrade(id) else { - self.dirty.release_parent(id, &self.edges); - continue; - }; - - let changed = node.refresh(); - self.dirty.after_refresh(id, changed, &self.edges); + self.refresh_now(id); } } } diff --git a/crates/vertigo/src/reactive/graph/transaction.rs b/crates/vertigo/src/reactive/graph/transaction.rs index 46ab65ef4..65960521a 100644 --- a/crates/vertigo/src/reactive/graph/transaction.rs +++ b/crates/vertigo/src/reactive/graph/transaction.rs @@ -45,8 +45,11 @@ impl Transaction { } } - pub(super) fn enter(&self) { - self.depth.set(self.depth.get() + 1); + /// Increment nesting. `true` when this opened the outermost transaction. + pub(super) fn enter(&self) -> bool { + let depth = self.depth.get(); + self.depth.set(depth + 1); + depth == 0 } /// Close one nesting level. `Some` when this was the outermost transaction. @@ -68,6 +71,10 @@ impl Transaction { self.depth.get() == 0 && !self.propagating.get() } + pub(super) fn is_propagating(&self) -> bool { + self.propagating.get() + } + /// Start a propagate wave. Call only when [`Self::can_propagate`] is `true`. pub(super) fn start_propagate(&self) -> Propagating<'_> { debug_assert!(self.can_propagate()); diff --git a/crates/vertigo/src/reactive/invariants.md b/crates/vertigo/src/reactive/invariants.md index 0d25262be..6c5f6029a 100644 --- a/crates/vertigo/src/reactive/invariants.md +++ b/crates/vertigo/src/reactive/invariants.md @@ -80,23 +80,22 @@ A child keeps its parents alive (strong refs). The graph does not (weak refs). Drop the last handle of a node, and the node is removed. -### 6. A wave walks dirty nodes whose parents are done +### 6. A wave refreshes each node at most once A dirty node is ready when none of its parents are still dirty. -If dirty nodes remain and none are ready, there is a cycle, and the program panics. +Children are marked dirty only when a parent’s value changed. +If a compute reads a parent that is still stale, that parent is refreshed first. +If dirty nodes remain and none are ready, or a node is refreshed while it is already +refreshing, there is a cycle, and the program panics. -A node may refresh more than once in one wave. -That happens when a parent becomes dirty after the node was already queued. -Extra refreshes are fine. Stopping early is not. See invariant 7. +A node does not refresh a second time in the same wave. ### 7. After a wave, every value is correct When the wave ends, every `Value` and `Computed` matches the current sources. -A subscriber may see a half-updated value *during* the wave. -It must not still see that value when the wave is over. +A subscriber sees one value for the wave: the one that matches the sources. The wave runs until nothing is dirty. -More work is better than a wrong node. A cycle panics. Nodes are not dropped to "save" the wave. ### 8. Graphs do not mix diff --git a/crates/vertigo/src/reactive/invariants.pl.md b/crates/vertigo/src/reactive/invariants.pl.md index 1993a8ef2..484d4f3ed 100644 --- a/crates/vertigo/src/reactive/invariants.pl.md +++ b/crates/vertigo/src/reactive/invariants.pl.md @@ -82,23 +82,22 @@ Dziecko trzyma rodziców przy życiu (silne referencje). Graf nie (słabe referencje). Upuść ostatni uchwyt węzła — węzeł znika. -### 6. Fala bierze brudne węzły, których rodzice są już gotowi +### 6. W jednej fali węzeł odświeża się najwyżej raz Brudny węzeł jest gotowy, gdy żaden z jego rodziców nie jest już brudny. -Jeśli zostają brudne węzły i żaden nie jest gotowy — jest cykl, program panikuje. +Dzieci są oznaczane jako brudne tylko wtedy, gdy wartość rodzica się zmieniła. +Jeśli compute czyta rodzica, który jeszcze jest nieaktualny, ten rodzic jest odświeżany najpierw. +Jeśli zostają brudne węzły i żaden nie jest gotowy, albo węzeł jest odświeżany w trakcie +własnego odświeżania — jest cykl, program panikuje. -W jednej fali węzeł może odświeżyć się więcej niż raz. -Tak jest, gdy rodzic staje się brudny już po wstawieniu węzła do kolejki. -Dodatkowe odświeżenia są w porządku. Przerwanie w pół drogi — nie. Patrz niezmiennik 7. +W jednej fali węzeł nie odświeża się drugi raz. ### 7. Po fali każda wartość jest poprawna Gdy fala się kończy, każdy `Value` i `Computed` zgadza się z bieżącymi źródłami. -Subskrybent może w *trakcie* fali zobaczyć wartość w połowie drogi. -Nie może jej dalej widzieć, gdy fala już się skończyła. +Subskrybent widzi jedną wartość z tej fali: tę, która zgadza się ze źródłami. Fala trwa, aż nic nie jest brudne. -Lepiej policzyć za dużo, niż zostawić zły węzeł. Cykl panikuje. Węzłów nie odrzucamy, żeby „uratować” falę. ### 8. Grafy się nie mieszają diff --git a/crates/vertigo/src/reactive/mod.rs b/crates/vertigo/src/reactive/mod.rs index 8647cea73..8f39d5cb8 100644 --- a/crates/vertigo/src/reactive/mod.rs +++ b/crates/vertigo/src/reactive/mod.rs @@ -1,8 +1,9 @@ //! Transactional reactive graph with equality cutoff. //! //! After a transaction, dirty nodes are processed in topological order (a node is -//! ready when none of its parents are dirty). After a node becomes clean, dependents -//! are enqueued **only if** the node's value changed ([`PartialEq`]). +//! ready when none of its parents are dirty). `get` during the wave pulls a stale +//! parent before returning its cache. After a node becomes clean, dependents are +//! enqueued **only if** the node's value changed ([`PartialEq`]). //! //! Domain invariants for this bounded context: [`invariants`]. diff --git a/crates/vertigo/src/reactive/propagation_order.rs b/crates/vertigo/src/reactive/propagation_order.rs index 57e3cbde0..2b399a23a 100644 --- a/crates/vertigo/src/reactive/propagation_order.rs +++ b/crates/vertigo/src/reactive/propagation_order.rs @@ -1,27 +1,17 @@ //! Ordering inside one propagation pass. //! -//! These tests pin *current* behaviour, which is not the behaviour we want. A node can be -//! computed several times in a single pass, and a subscriber can observe intermediate -//! values that correspond to no consistent state of the inputs. -//! -//! Cause: [`Dirty::enqueue`](super::graph) counts how many of a node's parents are dirty -//! *at the moment it is enqueued*, but the dirty set grows while the pass runs - a -//! dependent is only enqueued once its parent's value really changed (the equality -//! cutoff). So a node can be declared ready while a parent that is about to become dirty -//! has not entered the queue yet. Nothing later corrects the order; the node simply runs -//! again when that parent finally changes. -//! -//! Each test carries the assertions that *should* hold, commented out. They describe the -//! outcome of refreshing a dirty node when it is read, instead of serving its cached -//! value - then ordering stops depending on the queue at all. +//! A node refreshes at most once per wave. Children are queued only when a parent +//! changed (equality cutoff). `get` pulls a stale ancestor so a join still sees every +//! branch, and a subscriber sees one value: the one that matches the sources. use super::Graph; -use std::{cell::Cell, cell::RefCell, rc::Rc}; +use crate::struct_mut::ValueMut; +use std::rc::Rc; /// Static dependency set, no conditionals: `c` always reads both branches. The two paths -/// from the two roots have different lengths, and that alone is enough. +/// from the two roots have different lengths. #[test] -fn static_unequal_path_lengths_recompute_twice() { +fn static_unequal_path_lengths_compute_once() { let g = Graph::new(); let logs = g.logger().listen(); let d1 = g.value(1i32); @@ -47,51 +37,40 @@ fn static_unequal_path_lengths_recompute_twice() { move |ctx| f2.get(ctx) }); - let runs = Rc::new(Cell::new(0)); + let runs = Rc::new(ValueMut::new(0)); let c = g.computed({ let b1 = b1.clone(); let b2 = b2.clone(); let runs = runs.clone(); move |ctx| { - runs.set(runs.get() + 1); + runs.change(|n| *n += 1); b1.get(ctx) * 1000 + b2.get(ctx) } }); - let seen = Rc::new(RefCell::new(Vec::new())); + let seen = Rc::new(ValueMut::new(Vec::new())); let _sub = c.subscribe({ let seen = seen.clone(); - move |value| seen.borrow_mut().push(value) + move |value| seen.change(|seen| seen.push(value)) }); runs.set(0); - seen.borrow_mut().clear(); + seen.set(Vec::new()); g.transaction(|_| { d1.set(7); d2.set(9); }); - // `b1` finishes first and enqueues `c`. At that point `f2` has not run, so `b2` is not - // dirty yet and `c` is treated as ready - it reads the stale `b2`. - assert_eq!(runs.get(), 2); - assert_eq!(*seen.borrow(), vec![7001, 7009]); - - // 7001 is a state that never existed: new `b1`, old `b2`. - // assert_eq!(runs.get(), 1); - // assert_eq!(*seen.borrow(), vec![7009]); + assert_eq!(runs.get(), 1); + assert_eq!(seen.get(), vec![7009]); logs.assert_eq(&[]); } -/// A conditional read adds a second source of the same problem: a parent discovered -/// during this run cannot have been waited for, because the wait count was computed from -/// the parent set of the *previous* run. -/// -/// The later passes show this is not a first-computation effect. It repeats whenever the -/// dependency set is rebuilt, and pass 2 shows it happens even with the set already -/// complete. +/// A conditional read discovers parents that were not in the previous parent set. +/// `get` still pulls them, so `c` runs once with both branches already fresh. #[test] -fn dynamic_branch_switch_recomputes_three_times() { +fn dynamic_branch_switch_computes_once() { let g = Graph::new(); let logs = g.logger().listen(); let flag = g.value(true); @@ -126,14 +105,14 @@ fn dynamic_branch_switch_recomputes_three_times() { let _keep_b1 = b1.clone().subscribe(|_| {}); let _keep_b2 = b2.clone().subscribe(|_| {}); - let runs = Rc::new(Cell::new(0)); + let runs = Rc::new(ValueMut::new(0)); let c = g.computed({ let flag = flag.clone(); let b1 = b1.clone(); let b2 = b2.clone(); let runs = runs.clone(); move |ctx| { - runs.set(runs.get() + 1); + runs.change(|n| *n += 1); if flag.get(ctx) { 0 } else { @@ -147,65 +126,50 @@ fn dynamic_branch_switch_recomputes_three_times() { } }); - let seen = Rc::new(RefCell::new(Vec::new())); + let seen = Rc::new(ValueMut::new(Vec::new())); let _sub = c.subscribe({ let seen = seen.clone(); - move |value| seen.borrow_mut().push(value) + move |value| seen.change(|seen| seen.push(value)) }); - let pass = |label: &str, apply: &dyn Fn()| -> (usize, Vec) { + let pass = |apply: &dyn Fn()| -> (usize, Vec) { runs.set(0); - seen.borrow_mut().clear(); + seen.set(Vec::new()); g.transaction(|_| apply()); - let result = (runs.get(), seen.borrow().clone()); - println!("{label}: c ran {} times, saw {:?}", result.0, result.1); - result + (runs.get(), seen.get()) }; - // Pass 1: `c` only listed `flag` as a parent, so it runs before either branch, then - // once more for each branch it discovers. - let (runs_1, seen_1) = pass("pass 1", &|| { + let (runs_1, seen_1) = pass(&|| { d1.set(500); d2.set(7); flag.set(false); }); - assert_eq!(runs_1, 3); - assert_eq!(seen_1, vec![1, 501, 507]); - // assert_eq!(runs_1, 1); - // assert_eq!(seen_1, vec![507]); + assert_eq!(runs_1, 1); + assert_eq!(seen_1, vec![507]); - // Pass 2: nothing is discovered now - `c` already lists both branches - and it still - // runs twice, because `b2` is not yet dirty when `b1` enqueues `c`. - let (runs_2, seen_2) = pass("pass 2", &|| { + let (runs_2, seen_2) = pass(&|| { d1.set(900); d2.set(8); }); - assert_eq!(runs_2, 2); - assert_eq!(seen_2, vec![907, 908]); - // assert_eq!(runs_2, 1); - // assert_eq!(seen_2, vec![908]); + assert_eq!(runs_2, 1); + assert_eq!(seen_2, vec![908]); - // Pass 3: back to the cheap branch, `c` drops `b1` and `b2` from its parents. - let (runs_3, seen_3) = pass("pass 3", &|| flag.set(true)); + let (runs_3, seen_3) = pass(&|| flag.set(true)); assert_eq!(runs_3, 1); assert_eq!(seen_3, vec![0]); - // Pass 4: the shape of pass 1 again, and the same three runs. - let (runs_4, seen_4) = pass("pass 4", &|| { + let (runs_4, seen_4) = pass(&|| { d1.set(1500); d2.set(9); flag.set(false); }); - assert_eq!(runs_4, 3); - assert_eq!(seen_4, vec![908, 1508, 1509]); - // assert_eq!(runs_4, 1); - // assert_eq!(seen_4, vec![1509]); + assert_eq!(runs_4, 1); + assert_eq!(seen_4, vec![1509]); logs.assert_eq(&[]); } /// Build a fan-in: `paths` chains of *different* lengths, all rooted in values written in -/// one transaction, all read by one node `c`. Chain `k` is `k + 1` hops long, so each one -/// reaches `c` at a different point of the pass. +/// one transaction, all read by one node `c`. Chain `k` is `k + 1` hops long. /// /// No node here depends on itself, nothing writes from a callback: the graph is a plain /// acyclic fan-in. @@ -231,17 +195,17 @@ fn fan_in(paths: usize) -> (u32, Option) { tails.push(node); } - let runs = Rc::new(Cell::new(0u32)); + let runs = Rc::new(ValueMut::new(0u32)); let c = g.computed({ let tails = tails.clone(); let runs = runs.clone(); move |ctx| { - runs.set(runs.get() + 1); + runs.change(|n| *n += 1); tails.iter().map(|tail| tail.get(ctx)).sum::() } }); - let last = Rc::new(Cell::new(None)); + let last = Rc::new(ValueMut::new(None)); let _sub = c.subscribe({ let last = last.clone(); move |value| last.set(Some(value)) @@ -258,22 +222,17 @@ fn fan_in(paths: usize) -> (u32, Option) { (runs.get(), last.get()) } -/// The extra runs are not a constant overhead - the count grows with the number of paths -/// of differing length that reach the node. #[test] -fn one_extra_run_per_incoming_path() { +fn one_run_regardless_of_incoming_paths() { for paths in [2usize, 3, 5, 10] { let expected_sum = (paths * (paths + 1) / 2) as i32; let (runs, last) = fan_in(paths); - assert_eq!(runs as usize, paths, "{paths} paths"); + assert_eq!(runs, 1, "{paths} paths"); assert_eq!(last, Some(expected_sum), "{paths} paths"); - // assert_eq!(runs, 1, "{paths} paths"); } } -/// Many unequal paths still recompute `c` once per path, but the wave is not cut off: -/// the last value includes every path. #[test] fn many_unequal_paths_still_settle_to_the_correct_value() { let paths = 101usize; @@ -281,8 +240,6 @@ fn many_unequal_paths_still_settle_to_the_correct_value() { let (runs, last) = fan_in(paths); - assert_eq!(runs as usize, paths); + assert_eq!(runs, 1); assert_eq!(last, Some(correct_sum)); - - // assert_eq!(runs, 1); } diff --git a/crates/vertigo/src/reactive/value.rs b/crates/vertigo/src/reactive/value.rs index 47c9bebcf..f9ec27536 100644 --- a/crates/vertigo/src/reactive/value.rs +++ b/crates/vertigo/src/reactive/value.rs @@ -105,6 +105,7 @@ impl Value { pub fn get(&self, ctx: &Context) -> T { ctx.track(self.inner.id, self.inner.clone()); + self.inner.graph.ensure_fresh(self.inner.id); self.inner.value.borrow().clone() }