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/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/docs/reactive-graph.md b/crates/vertigo/docs/reactive-graph.md new file mode 100644 index 000000000..43b2ea50d --- /dev/null +++ b/crates/vertigo/docs/reactive-graph.md @@ -0,0 +1,130 @@ +# 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 +``` + +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` +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 + +`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| { + // ignored: subscribe must not write + form_dirty.set(false); +}); +``` + +Legal places to write: DOM/event handlers, timers, fetch/socket callbacks, +`on_after_transaction`, and `when_connect` / `Value::with_connect`. + +## Connecting to the outside world + +[`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 + +`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. + +## 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 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/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..3d8d99193 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,22 @@ impl EmbedDom for DomNode { } } -impl EmbedDom for T { +impl EmbedDom for &mut String { + fn embed(self) -> DomNode { + DomNode::Text { + node: DomText::new(self.clone()), + } + } +} + +/// 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()), @@ -177,9 +193,64 @@ impl EmbedDom for T { } } +impl EmbedDom for std::rc::Rc { + fn embed(self) -> DomNode { + DomNode::Text { + node: DomText::new((*self).to_string()), + } + } +} + +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, + 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 { 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/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/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/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/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..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 @@ -31,7 +32,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 +44,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; @@ -56,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 {} @@ -68,10 +77,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 +107,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 +122,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..5a2634291 --- /dev/null +++ b/crates/vertigo/src/reactive/computed.rs @@ -0,0 +1,277 @@ +use std::{cell::RefCell, ops::Add, rc::Rc}; + +use super::{ + Context, DropResource, Graph, 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 _guard = self.graph.enter_callback(); + 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 _guard = self.graph.enter_callback(); + 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.graph.ensure_fresh(self.inner.id); + self.inner.ensure() + } + + pub fn id(&self) -> GraphId { + GraphId::from_node(self.inner.id) + } + + /// 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(); + 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()); + + Graph { + inner: graph.clone(), + } + .transaction(|_| { + 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()) + } +} + +#[cfg(test)] +mod tests { + use super::{super::Graph, *}; + use std::cell::Cell; + + #[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 { + 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); + logs.assert_eq(&[]); + } +} diff --git a/crates/vertigo/src/reactive/context.rs b/crates/vertigo/src/reactive/context.rs new file mode 100644 index 000000000..b3da2cd64 --- /dev/null +++ b/crates/vertigo/src/reactive/context.rs @@ -0,0 +1,112 @@ +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())), + } + } + + /// 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 { + let mut parents = parents.borrow_mut(); + if parents.last().map(|(last, _)| *last) == Some(id) { + return; + } + parents.push((id, slot)); + } + } + + pub(crate) fn take_parents(&self) -> ParentList { + match &self.parents { + Some(parents) => parents.take(), + None => Vec::new(), + } + } +} + +#[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()); + } +} 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..35c0ed27b --- /dev/null +++ b/crates/vertigo/src/reactive/graph/dirty.rs @@ -0,0 +1,368 @@ +use std::{ + cell::RefCell, + collections::{HashMap, HashSet, VecDeque}, +}; + +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. +pub(super) struct Dirty { + in_dirty: RefCell>, + 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 { + 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()), + 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; + } + 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(); + let refreshing = self.refreshing.borrow(); + let done = self.done.borrow(); + while let Some(id) = ready.pop_front() { + if in_dirty.contains(&id) && !refreshing.contains(&id) && !done.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. + /// + /// 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; + } + self.fill_scratch(parent, edges); + self.release_from_scratch(); + } + + /// 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 { + 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 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 { + 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); + } + } + + fn enqueue_from_scratch(&self, edges: &Edges) { + let children = self.scratch_children.borrow(); + for child in children.iter() { + 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))); + } + + #[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 new file mode 100644 index 000000000..5c7dc5b9b --- /dev/null +++ b/crates/vertigo/src/reactive/graph/edges.rs @@ -0,0 +1,238 @@ +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()); + } + } + + 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 + // 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) + && *old == new_parents + { + return None; + } + } + + 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 + } +} + +#[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 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)])); + } + + /// 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(); + 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 new file mode 100644 index 000000000..5e30585b7 --- /dev/null +++ b/crates/vertigo/src/reactive/graph/hooks.rs @@ -0,0 +1,78 @@ +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(); + } + } +} + +#[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/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 new file mode 100644 index 000000000..7c82359aa --- /dev/null +++ b/crates/vertigo/src/reactive/graph/mod.rs @@ -0,0 +1,318 @@ +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; + +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); + +/// 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; +} + +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, +} + +/// Orchestrator: transactions, `propagate`, `set_parents`, `unregister`. +/// +/// Persistent state lives in `Nodes`, `Edges`, `Dirty`, `Watch`, `Hooks`, `Logger`. +pub(crate) struct GraphInner { + next_id: Cell, + tx: Transaction, + nodes: Nodes, + edges: Edges, + dirty: Dirty, + watch: Watch, + hooks: Hooks, + logger: Logger, +} + +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: Transaction::new(), + nodes: Nodes::new(), + edges: Edges::new(), + dirty: Dirty::new(), + watch: Watch::new(), + hooks: Hooks::new(), + logger: Logger::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 { + 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() { + self.inner.propagate(); + if !leave.already_propagating { + self.inner.flush_watch(); + 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); + }) + } + + pub fn logger(&self) -> Logger { + self.inner.logger.clone() + } +} + +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)); + 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 + } + + /// 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)); + } + + fn flush_watch_if_idle(&self) { + if self.tx.can_propagate() { + self.flush_watch(); + } + } + + 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); + self.flush_watch_if_idle(); + } + + /// Process dirty nodes in topological order. A node refreshes at most once per wave. + /// + /// 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 { + if let Some(leftover) = self.dirty.cycle_leftover() { + panic!("vertigo: cycle in dirty graph ({leftover:?})"); + } + break; + }; + + self.refresh_now(id); + } + } +} + +#[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 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/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/transaction.rs b/crates/vertigo/src/reactive/graph/transaction.rs new file mode 100644 index 000000000..65960521a --- /dev/null +++ b/crates/vertigo/src/reactive/graph/transaction.rs @@ -0,0 +1,115 @@ +use std::cell::Cell; + +/// 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. +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); + } +} + +/// 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), + } + } + + /// 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. + 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() + } + + 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()); + 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)] +mod tests { + use super::*; + + #[test] + fn cannot_propagate_while_propagating() { + 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 new file mode 100644 index 000000000..bb34a4be6 --- /dev/null +++ b/crates/vertigo/src/reactive/graph/watch.rs @@ -0,0 +1,286 @@ +use std::{ + cell::{Cell, RefCell}, + collections::{HashMap, HashSet}, + rc::Rc, +}; + +use super::super::DropResource; +use super::{NodeId, edges::ParentDiff}; + +/// `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>, + 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 { + pub(super) fn new() -> Self { + Self { + connect: RefCell::new(HashMap::new()), + connected: RefCell::new(HashMap::new()), + pending: RefCell::new(HashSet::new()), + flushing: Cell::new(false), + } + } + + pub(super) fn register( + &self, + id: NodeId, + connect: Rc DropResource>, + watched: bool, + ) { + self.connect.borrow_mut().insert(id, connect); + if watched { + self.schedule(id); + } + } + + pub(super) fn unregister(&self, id: NodeId) { + self.connect.borrow_mut().remove(&id); + self.schedule(id); + } + + pub(super) fn apply(&self, diff: ParentDiff) { + for id in diff.became_watched { + self.schedule(id); + } + for id in diff.became_unwatched { + self.schedule(id); + } + } + + pub(super) fn on_unwatched_many(&self, ids: Vec) { + for id in ids { + self.schedule(id); + } + } + + fn schedule(&self, id: NodeId) { + self.pending.borrow_mut().insert(id); + } + + /// 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() { + 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); + } + } + } + } +} + +#[cfg(test)] +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(); + 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(), 0, "connect waits for flush"); + flush_if(&watch, |id| id == NodeId(1)); + 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); + flush_if(&watch, |id| id == NodeId(1)); + 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); + flush_if(&watch, |id| id == NodeId(1)); + watch.unregister(NodeId(1)); + flush_if(&watch, |_| false); + 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); + 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); + } + + #[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); + flush_if(&watch, |id| id == NodeId(1)); + watch.on_unwatched_many(vec![NodeId(1)]); + flush_if(&watch, |_| false); + 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(); + 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..6c5f6029a --- /dev/null +++ b/crates/vertigo/src/reactive/invariants.md @@ -0,0 +1,105 @@ +# 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. + +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. +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 refreshes each node at most once + +A dirty node is ready when none of its parents are still dirty. +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 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 sees one value for the wave: the one that matches the sources. + +The wave runs until nothing is dirty. +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..484d4f3ed --- /dev/null +++ b/crates/vertigo/src/reactive/invariants.pl.md @@ -0,0 +1,107 @@ +# 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. + +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ą. +`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. 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. +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ł 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 widzi jedną wartość z tej fali: tę, która zgadza się ze źródłami. + +Fala trwa, aż nic nie jest brudne. +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 new file mode 100644 index 000000000..8f39d5cb8 --- /dev/null +++ b/crates/vertigo/src/reactive/mod.rs @@ -0,0 +1,74 @@ +//! 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). `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`]. + +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, Logger, LoggerListener}; +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 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..2b399a23a --- /dev/null +++ b/crates/vertigo/src/reactive/propagation_order.rs @@ -0,0 +1,245 @@ +//! Ordering inside one propagation pass. +//! +//! 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 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. +#[test] +fn static_unequal_path_lengths_compute_once() { + let g = Graph::new(); + let logs = g.logger().listen(); + 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(ValueMut::new(0)); + let c = g.computed({ + let b1 = b1.clone(); + let b2 = b2.clone(); + let runs = runs.clone(); + move |ctx| { + runs.change(|n| *n += 1); + b1.get(ctx) * 1000 + b2.get(ctx) + } + }); + + let seen = Rc::new(ValueMut::new(Vec::new())); + let _sub = c.subscribe({ + let seen = seen.clone(); + move |value| seen.change(|seen| seen.push(value)) + }); + + runs.set(0); + seen.set(Vec::new()); + + g.transaction(|_| { + d1.set(7); + d2.set(9); + }); + + assert_eq!(runs.get(), 1); + assert_eq!(seen.get(), vec![7009]); + logs.assert_eq(&[]); +} + +/// 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_computes_once() { + let g = Graph::new(); + let logs = g.logger().listen(); + 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(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.change(|n| *n += 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(ValueMut::new(Vec::new())); + let _sub = c.subscribe({ + let seen = seen.clone(); + move |value| seen.change(|seen| seen.push(value)) + }); + + let pass = |apply: &dyn Fn()| -> (usize, Vec) { + runs.set(0); + seen.set(Vec::new()); + g.transaction(|_| apply()); + (runs.get(), seen.get()) + }; + + let (runs_1, seen_1) = pass(&|| { + d1.set(500); + d2.set(7); + flag.set(false); + }); + assert_eq!(runs_1, 1); + assert_eq!(seen_1, vec![507]); + + let (runs_2, seen_2) = pass(&|| { + d1.set(900); + d2.set(8); + }); + assert_eq!(runs_2, 1); + assert_eq!(seen_2, vec![908]); + + let (runs_3, seen_3) = pass(&|| flag.set(true)); + assert_eq!(runs_3, 1); + assert_eq!(seen_3, vec![0]); + + let (runs_4, seen_4) = pass(&|| { + d1.set(1500); + d2.set(9); + flag.set(false); + }); + 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. +/// +/// 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 logs = g.logger().listen(); + 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(ValueMut::new(0u32)); + let c = g.computed({ + let tails = tails.clone(); + let runs = runs.clone(); + move |ctx| { + runs.change(|n| *n += 1); + tails.iter().map(|tail| tail.get(ctx)).sum::() + } + }); + + let last = Rc::new(ValueMut::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); + } + }); + + logs.assert_eq(&[]); + (runs.get(), last.get()) +} + +#[test] +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, 1, "{paths} paths"); + assert_eq!(last, Some(expected_sum), "{paths} paths"); + } +} + +#[test] +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, 1); + assert_eq!(last, Some(correct_sum)); +} diff --git a/crates/vertigo/src/reactive/tests.rs b/crates/vertigo/src/reactive/tests.rs new file mode 100644 index 000000000..c785ba6d8 --- /dev/null +++ b/crates/vertigo/src/reactive/tests.rs @@ -0,0 +1,585 @@ +use std::{cell::Cell, rc::Rc}; + +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({ + 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); + }); + 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)); + 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); + 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(); + 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" + ); + 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(); + 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); + 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(); + 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()); + 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({ + 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); + 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(); + 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); + }); + 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)); + + 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); + 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)); + + 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); + 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); + 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); + 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(&[]); +} + +/// 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(); + 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(); + + 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); + 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(); + + 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); + 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(); + move || { + fires.set(fires.get() + 1); + } + }); + + let a = Value::new(1); + a.set(2); + assert_eq!(fires.get(), 1); + logs.assert_eq(&[]); +} + +#[test] +fn set_from_subscribe_is_ignored() { + let g = Graph::new(); + 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) + }); + logs.assert_eq(&[super::graph::BLOCKED_WRITE]); + + g.transaction(|ctx| { + assert_eq!(b.get(ctx), 0); + }); + logs.assert_eq(&[]); + + a.set(2); + logs.assert_eq(&[super::graph::BLOCKED_WRITE]); + + g.transaction(|ctx| { + assert_eq!(b.get(ctx), 0); + }); + logs.assert_eq(&[]); +} + +#[test] +fn set_from_compute_is_ignored() { + let g = Graph::new(); + let logs = g.logger().listen(); + let a = g.value(0); + let b = g.value(0); + let c = g.computed({ + let a = a.clone(); + let b = b.clone(); + move |ctx| { + b.set(a.get(ctx)); + a.get(ctx) + } + }); + logs.assert_eq(&[]); + + g.transaction(|ctx| { + assert_eq!(c.get(ctx), 0); + assert_eq!(b.get(ctx), 0); + }); + logs.assert_eq(&[super::graph::BLOCKED_WRITE]); + logs.assert_eq(&[]); +} + +/// `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({ + 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]); + logs.assert_eq(&[]); +} 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..f9ec27536 --- /dev/null +++ b/crates/vertigo/src/reactive/value.rs @@ -0,0 +1,193 @@ +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. + /// + /// `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, + { + 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.graph.ensure_fresh(self.inner.id); + self.inner.value.borrow().clone() + } + + pub fn set(&self, value: T) { + if !self.inner.graph.writes_allowed() { + return; + } + 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/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/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/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 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>,