logicaffeine_compile/page_host/
snapshot.rs1use crate::semantics::{dom, storage, world};
10use serde::Serialize;
11
12#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
16#[serde(rename_all = "snake_case")]
17pub enum Axis {
18 Document,
20 Head,
23 Title,
25 Url,
27 RouteOps,
29 Focus,
31 Scroll,
33 Selection,
35 Clipboard,
37 Downloads,
39 Notifications,
41 Published,
43 Storage,
45 Fetches,
47 Journal,
49 Failure,
51 Stopped,
53}
54
55impl Axis {
56 pub const SNAPSHOT: [Axis; 16] = [
58 Axis::Document,
59 Axis::Head,
60 Axis::Title,
61 Axis::Url,
62 Axis::RouteOps,
63 Axis::Focus,
64 Axis::Scroll,
65 Axis::Selection,
66 Axis::Clipboard,
67 Axis::Downloads,
68 Axis::Notifications,
69 Axis::Published,
70 Axis::Storage,
71 Axis::Fetches,
72 Axis::Journal,
73 Axis::Failure,
74 ];
75
76 pub fn name(self) -> &'static str {
78 match self {
79 Axis::Document => "document",
80 Axis::Head => "head",
81 Axis::Title => "title",
82 Axis::Url => "url",
83 Axis::RouteOps => "route_ops",
84 Axis::Focus => "focus",
85 Axis::Scroll => "scroll",
86 Axis::Selection => "selection",
87 Axis::Clipboard => "clipboard",
88 Axis::Downloads => "downloads",
89 Axis::Notifications => "notifications",
90 Axis::Published => "published",
91 Axis::Storage => "storage",
92 Axis::Fetches => "fetches",
93 Axis::Journal => "journal",
94 Axis::Failure => "failure",
95 Axis::Stopped => "stopped",
96 }
97 }
98
99 pub fn from_name(name: &str) -> Option<Axis> {
101 Axis::SNAPSHOT
102 .iter()
103 .copied()
104 .chain([Axis::Stopped])
105 .find(|a| a.name() == name)
106 }
107}
108
109#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
111pub struct DocSnapshot {
112 pub document: String,
113 pub head: Option<String>,
114 pub title: String,
115 pub url: String,
116 pub route_ops: Vec<String>,
117 pub focus: Option<String>,
118 pub scroll: Option<String>,
119 pub selection: Option<String>,
120 pub clipboard: String,
121 pub downloads: Vec<(String, String)>,
122 pub notifications: Vec<(String, String)>,
123 pub published: Vec<(String, String)>,
124 pub storage: Vec<(String, String, String)>,
126 pub fetches: Vec<String>,
127 pub journal: Vec<String>,
128 pub failure: Option<String>,
129}
130
131impl DocSnapshot {
132 pub fn capture(failure: Option<String>) -> DocSnapshot {
135 let mut storage_rows = Vec::new();
136 for area in [storage::LOCAL, storage::SESSION] {
137 for key in storage::keys(area).unwrap_or_default() {
138 let value = storage::get(area, &key).ok().flatten().unwrap_or_default();
139 storage_rows.push((area.to_string(), key, value));
140 }
141 }
142 DocSnapshot {
143 document: dom::render(),
144 head: dom::document_head(),
145 title: dom::title(),
146 url: dom::route_path(),
147 route_ops: dom::route_ops()
148 .into_iter()
149 .map(|op| match op {
150 dom::RouteOp::Navigate {
151 path,
152 replace: false,
153 } => format!("push {path}"),
154 dom::RouteOp::Navigate {
155 path,
156 replace: true,
157 } => format!("replace {path}"),
158 dom::RouteOp::Load { path } => format!("load {path}"),
159 })
160 .collect(),
161 focus: element_identity(dom::focused()),
162 scroll: element_identity(dom::scrolled_to()),
163 selection: element_identity(dom::selected()),
164 clipboard: dom::clipboard(),
165 downloads: dom::downloads(),
166 notifications: dom::notifications(),
167 published: dom::published(),
168 storage: storage_rows,
169 fetches: dom::fetch_requests()
170 .into_iter()
171 .map(|r| {
172 fetch_record(
173 &r.method,
174 &r.url,
175 &r.body,
176 &r.headers,
177 r.timeout_ms,
178 r.aborted,
179 )
180 })
181 .collect(),
182 journal: world::journal().into_iter().map(|e| e.action).collect(),
183 failure,
184 }
185 }
186
187 pub fn axis(&self, axis: Axis) -> String {
189 fn pairs(v: &[(String, String)]) -> String {
190 v.iter()
191 .map(|(a, b)| format!("({a:?}, {b:?})"))
192 .collect::<Vec<_>>()
193 .join(", ")
194 }
195 match axis {
196 Axis::Document => self.document.clone(),
197 Axis::Head => self.head.clone().unwrap_or_default(),
198 Axis::Title => self.title.clone(),
199 Axis::Url => self.url.clone(),
200 Axis::RouteOps => self.route_ops.join(", "),
201 Axis::Focus => self.focus.clone().unwrap_or_default(),
202 Axis::Scroll => self.scroll.clone().unwrap_or_default(),
203 Axis::Selection => self.selection.clone().unwrap_or_default(),
204 Axis::Clipboard => self.clipboard.clone(),
205 Axis::Downloads => pairs(&self.downloads),
206 Axis::Notifications => pairs(&self.notifications),
207 Axis::Published => pairs(&self.published),
208 Axis::Storage => self
209 .storage
210 .iter()
211 .map(|(a, k, v)| format!("{a}:{k}={v:?}"))
212 .collect::<Vec<_>>()
213 .join(", "),
214 Axis::Fetches => self.fetches.join("; "),
215 Axis::Journal => self.journal.join(", "),
216 Axis::Failure => self.failure.clone().unwrap_or_default(),
217 Axis::Stopped => String::new(),
218 }
219 }
220}
221
222pub fn fetch_record(
225 method: &str,
226 url: &str,
227 body: &str,
228 headers: &[(String, String)],
229 timeout_ms: i64,
230 aborted: bool,
231) -> String {
232 let headers: Vec<String> = headers.iter().map(|(n, v)| format!("{n}: {v}")).collect();
233 format!(
234 "{method} {url} body={body:?} headers=[{}] timeout={timeout_ms} aborted={aborted}",
235 headers.join(", ")
236 )
237}
238
239fn element_identity(node: i64) -> Option<String> {
242 if node < 0 {
243 return None;
244 }
245 let tag = dom::tag_of(node)?;
246 if let Some(id) = dom::attribute(node, "id").filter(|id| !id.is_empty()) {
247 return Some(format!("{tag}#{id}"));
248 }
249 match dom::query_selector_all(&tag)
250 .iter()
251 .position(|n| *n == node)
252 {
253 Some(i) => Some(format!("{tag}[{i}]")),
254 None => Some(format!("{tag} (detached)")),
255 }
256}