1use std::collections::HashSet;
8
9use vs_engine_webkit::PageHandle;
10use vs_protocol::{DeltaOp, Ref, StateToken, Tree};
11
12use crate::tokens;
13
14#[derive(Debug)]
16pub struct PageState {
17 pub id: String,
18 pub url: String,
19 pub engine_handle: PageHandle,
20 pub last_tree: Option<Tree>,
23 pub last_token: Option<StateToken>,
26 pub force_full: bool,
29 pub seen_refs: HashSet<Ref>,
31}
32
33impl PageState {
34 #[must_use]
35 pub fn new(id: String, url: String, engine_handle: PageHandle) -> Self {
36 Self {
37 id,
38 url,
39 engine_handle,
40 last_tree: None,
41 last_token: None,
42 force_full: true,
43 seen_refs: HashSet::new(),
44 }
45 }
46
47 pub fn invalidate_baseline(&mut self) {
49 self.force_full = true;
50 }
51
52 pub fn apply_snapshot(&mut self, new_tree: Tree) -> (StateToken, ViewForm) {
55 let token = tokens::compute(&new_tree, &self.url, &self.id);
56
57 if !self.force_full && self.last_token.as_ref().is_some_and(|t| *t == token) {
61 return (token, ViewForm::NoChange);
62 }
63
64 let form = match (&self.last_tree, self.force_full) {
65 (None, _) | (Some(_), true) => ViewForm::Full(new_tree.clone()),
66 (Some(prev), false) => {
67 let ops = vs_protocol::diff(prev, &new_tree);
68 ViewForm::Delta(ops)
69 }
70 };
71
72 for node in &new_tree {
74 self.seen_refs.insert(node.r);
75 }
76
77 self.last_tree = Some(new_tree);
78 self.last_token = Some(token);
79 self.force_full = false;
80 (token, form)
81 }
82
83 #[must_use]
85 pub fn find_node(&self, r: Ref) -> Option<&vs_protocol::Node> {
86 self.last_tree
87 .as_ref()
88 .and_then(|t| t.iter().find(|n| n.r == r))
89 }
90}
91
92#[derive(Debug, Clone, PartialEq, Eq)]
94pub enum ViewForm {
95 Full(Tree),
98 Delta(Vec<DeltaOp>),
100 NoChange,
102}
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107 use vs_protocol::{Node, Role};
108
109 fn sample_tree(label: &str) -> Tree {
110 Tree::from_root(Node::leaf(Ref(1), Role::Doc, label))
111 }
112
113 fn make_state() -> PageState {
114 PageState::new("page-1".into(), "https://x".into(), PageHandle(1))
115 }
116
117 #[test]
118 fn first_snapshot_is_full() {
119 let mut p = make_state();
120 let (_token, form) = p.apply_snapshot(sample_tree("Hello"));
121 match form {
122 ViewForm::Full(tree) => assert_eq!(tree.roots[0].label, "Hello"),
123 other => panic!("unexpected: {other:?}"),
124 }
125 }
126
127 #[test]
128 fn second_snapshot_with_change_is_delta() {
129 let mut p = make_state();
130 p.apply_snapshot(sample_tree("Hello"));
131 let (_token, form) = p.apply_snapshot(sample_tree("World"));
132 assert!(matches!(form, ViewForm::Delta(_)));
133 }
134
135 #[test]
136 fn unchanged_snapshot_is_no_change() {
137 let mut p = make_state();
138 p.apply_snapshot(sample_tree("Hello"));
139 let (_token, form) = p.apply_snapshot(sample_tree("Hello"));
140 assert!(matches!(form, ViewForm::NoChange));
141 }
142
143 #[test]
144 fn baseline_reset_forces_full() {
145 let mut p = make_state();
146 p.apply_snapshot(sample_tree("Hello"));
147 p.invalidate_baseline();
148 let (_token, form) = p.apply_snapshot(sample_tree("Hello"));
149 assert!(matches!(form, ViewForm::Full(_)));
150 }
151
152 #[test]
153 fn token_changes_when_tree_changes() {
154 let mut p = make_state();
155 let (t1, _) = p.apply_snapshot(sample_tree("A"));
156 let (t2, _) = p.apply_snapshot(sample_tree("B"));
157 assert_ne!(t1, t2);
158 }
159
160 #[test]
161 fn refs_accumulate_in_seen_set() {
162 let mut p = make_state();
163 let mut t = Tree::from_root(Node::leaf(Ref(1), Role::Doc, "X"));
164 t.roots[0].children.push(Node::leaf(Ref(2), Role::P, "p1"));
165 p.apply_snapshot(t);
166 assert!(p.seen_refs.contains(&Ref(1)));
167 assert!(p.seen_refs.contains(&Ref(2)));
168 }
169
170 #[test]
171 fn find_node_returns_match() {
172 let mut p = make_state();
173 let mut t = Tree::from_root(Node::leaf(Ref(1), Role::Doc, "X"));
174 t.roots[0].children.push(Node::leaf(Ref(2), Role::P, "kid"));
175 p.apply_snapshot(t);
176 let node = p.find_node(Ref(2)).unwrap();
177 assert_eq!(node.label, "kid");
178 }
179}