Files
Glint-Runtime/src/interpreter/reactive.rs

172 lines
5.1 KiB
Rust

use std::collections::{HashMap, HashSet};
/// Unique identifier for an element in the VDOM tree.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ElementId(pub u32);
/// Tracks which variables affect which elements for incremental VDOM updates.
#[derive(Debug, Clone)]
pub struct ReactiveTracker {
/// For each variable name, which elements depend on it
subscribers: HashMap<String, HashSet<ElementId>>,
/// For each element, which variables it depends on
dependencies: HashMap<ElementId, HashSet<String>>,
/// Elements that need to be re-evaluated
dirty_set: HashSet<ElementId>,
next_id: u32,
}
impl ReactiveTracker {
pub fn new() -> Self {
Self {
subscribers: HashMap::new(),
dependencies: HashMap::new(),
dirty_set: HashSet::new(),
next_id: 0,
}
}
pub fn alloc_id(&mut self) -> ElementId {
let id = ElementId(self.next_id);
self.next_id += 1;
id
}
/// Register that `element` depends on `var_name`.
/// Call this during template parsing for each `$var` or `!rhee:{expr}` reference.
pub fn add_dependency(&mut self, element: ElementId, var_name: &str) {
self.subscribers.entry(var_name.to_string())
.or_default()
.insert(element);
self.dependencies.entry(element)
.or_default()
.insert(var_name.to_string());
}
/// Register multiple dependencies for an element from a property value.
pub fn scan_value(&mut self, element: ElementId, value: &str) {
// Scan for `$var` patterns
let mut chars = value.char_indices().peekable();
while let Some((_, c)) = chars.next() {
if c == '$' {
let mut var_name = String::new();
while let Some(&(_, next_c)) = chars.peek() {
if next_c.is_ascii_alphanumeric() || next_c == '_' {
var_name.push(chars.next().unwrap().1);
} else {
break;
}
}
if !var_name.is_empty() {
self.add_dependency(element, &var_name);
}
}
}
}
/// Called when a variable changes. Marks affected elements as dirty.
pub fn on_variable_changed(&mut self, name: &str) -> &HashSet<ElementId> {
if let Some(affected) = self.subscribers.get(name) {
self.dirty_set.extend(affected.iter());
}
&self.dirty_set
}
/// Returns the current dirty set and clears it.
pub fn take_dirty_set(&mut self) -> HashSet<ElementId> {
std::mem::take(&mut self.dirty_set)
}
/// Returns true if the element needs re-evaluation.
pub fn is_dirty(&self, id: ElementId) -> bool {
self.dirty_set.contains(&id)
}
/// Clear all tracking data (e.g., on template reload).
pub fn reset(&mut self) {
self.subscribers.clear();
self.dependencies.clear();
self.dirty_set.clear();
self.next_id = 0;
}
}
impl Default for ReactiveTracker {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_basic_dependency_tracking() {
let mut tracker = ReactiveTracker::new();
let e1 = tracker.alloc_id();
let e2 = tracker.alloc_id();
tracker.add_dependency(e1, "volume");
tracker.add_dependency(e2, "volume");
tracker.add_dependency(e2, "brightness");
let affected = tracker.on_variable_changed("volume");
assert!(affected.contains(&e1));
assert!(affected.contains(&e2));
let affected = tracker.on_variable_changed("brightness");
assert!(!affected.contains(&e1));
assert!(affected.contains(&e2));
}
#[test]
fn test_scan_value() {
let mut tracker = ReactiveTracker::new();
let e1 = tracker.alloc_id();
tracker.scan_value(e1, "Hello $name, you are $age years old");
let affected = tracker.on_variable_changed("name");
assert!(affected.contains(&e1));
let affected = tracker.on_variable_changed("age");
assert!(affected.contains(&e1));
}
#[test]
fn test_scan_no_vars() {
let mut tracker = ReactiveTracker::new();
let e1 = tracker.alloc_id();
tracker.scan_value(e1, "Hello world");
let affected = tracker.on_variable_changed("name");
assert!(!affected.contains(&e1));
}
#[test]
fn test_take_dirty_set() {
let mut tracker = ReactiveTracker::new();
let e1 = tracker.alloc_id();
tracker.add_dependency(e1, "x");
tracker.on_variable_changed("x");
let dirty = tracker.take_dirty_set();
assert_eq!(dirty.len(), 1);
assert!(tracker.dirty_set.is_empty());
}
#[test]
fn test_reset() {
let mut tracker = ReactiveTracker::new();
let e1 = tracker.alloc_id();
tracker.add_dependency(e1, "x");
tracker.reset();
assert!(tracker.subscribers.is_empty());
assert!(tracker.dependencies.is_empty());
assert!(tracker.dirty_set.is_empty());
}
}