1use std::time::Duration;
5
6use vs_engine_webkit::WaitCondition as EngineWait;
7use vs_protocol::{Ref, StateToken, Warning, WarningCode};
8
9use super::audit::AuditCtx;
10use super::responses::{
11 ActCall, ActResponse, FindHit, FindResponse, ReadResponse, StatusResponse, ViewResponse,
12 WaitResponse,
13};
14use super::{render_subtree_text, Daemon};
15use crate::error::{DaemonError, Result};
16use crate::tokens;
17
18impl Daemon {
19 pub fn view(&self, session_id: &str, page_id: &str, force_full: bool) -> Result<ViewResponse> {
20 let ctx = AuditCtx::new("vs_view", session_id)
21 .with_page(page_id)
22 .with_args(String::new(), tokens::args_hash("vs_view", &[]));
23 self.audit_call(ctx, |ctx| {
24 let engine_handle = self.engine_handle_for(session_id, page_id)?;
25 let tree = self.inner.engine.snapshot(engine_handle)?;
26 let (token, form) = {
27 let mut sessions = self.inner.sessions.lock().expect("poisoned");
28 let page = sessions
29 .get_mut(session_id)
30 .ok_or_else(|| DaemonError::UnknownSession(session_id.to_string()))?
31 .pages
32 .get_mut(page_id)
33 .ok_or_else(|| DaemonError::UnknownPage(page_id.to_string()))?;
34 if force_full {
35 page.invalidate_baseline();
36 }
37 page.apply_snapshot(tree)
38 };
39 ctx.after_token = Some(token);
40 let mut store = self.inner.store.lock().expect("poisoned");
41 store.update_page_token(page_id, &token.to_string(), "engine", None)?;
42 drop(store);
43 Ok(ViewResponse {
44 token,
45 form,
46 warnings: Vec::new(),
47 })
48 })
49 }
50
51 pub fn read(&self, session_id: &str, page_id: &str, r: Ref) -> Result<ReadResponse> {
52 let ctx = AuditCtx::new("vs_read", session_id)
53 .with_page(page_id)
54 .with_args(
55 r.to_string(),
56 tokens::args_hash("vs_read", &[r.to_string()]),
57 );
58 self.audit_call(ctx, |ctx| {
59 let (token, body) = {
60 let sessions = self.inner.sessions.lock().expect("poisoned");
61 let page = sessions
62 .get(session_id)
63 .ok_or_else(|| DaemonError::UnknownSession(session_id.to_string()))?
64 .pages
65 .get(page_id)
66 .ok_or_else(|| DaemonError::UnknownPage(page_id.to_string()))?;
67 let node = page.find_node(r).ok_or(DaemonError::UnknownRef(r.0))?;
68 (
69 page.last_token.unwrap_or(StateToken::ZERO),
70 render_subtree_text(node),
71 )
72 };
73 ctx.before_token = Some(token);
74 ctx.after_token = Some(token);
75 Ok(ReadResponse { token, body })
76 })
77 }
78
79 pub fn act(&self, call: ActCall) -> Result<ActResponse> {
80 let ActCall {
81 session_id,
82 page_id,
83 target,
84 action,
85 before_token,
86 args_hash,
87 args_redacted,
88 group_label,
89 } = call;
90 let ctx = AuditCtx::new("vs_act", &session_id)
91 .with_page(&page_id)
92 .with_args(args_redacted, args_hash.clone())
93 .with_before(before_token)
94 .with_group(group_label);
95 self.audit_call(ctx, |ctx| {
96 let engine_handle = self.engine_handle_for(&session_id, &page_id)?;
97 let current_token = self.current_token(&session_id, &page_id)?;
98 if current_token != before_token {
99 return Err(DaemonError::StaleToken {
100 current: current_token,
101 reason: "mutate",
102 });
103 }
104 let now = vs_store::epoch_secs();
105 let store = self.inner.store.lock().expect("poisoned");
106 let cached = store.lookup_idempotent(
107 &page_id,
108 &before_token.to_string(),
109 &args_hash,
110 now,
111 vs_store::IDEMPOTENCY_TTL_SECS,
112 )?;
113 drop(store);
114
115 if let Some(cached) = cached {
116 let token = cached
117 .after_token
118 .as_deref()
119 .and_then(|s| s.parse::<StateToken>().ok())
120 .unwrap_or(StateToken::ZERO);
121 ctx.idempotency_hit = true;
122 ctx.after_token = Some(token);
123 ctx.result_summary = Some("idem".into());
124 return Ok(ActResponse {
125 token,
126 warnings: vec![Warning::new(WarningCode::IdempotentHit)],
127 });
128 }
129
130 self.inner.engine.act(engine_handle, target, action)?;
131 let tree = self.inner.engine.snapshot(engine_handle)?;
132 let (token, _form) = {
133 let mut sessions = self.inner.sessions.lock().expect("poisoned");
134 let page = sessions
135 .get_mut(&session_id)
136 .ok_or_else(|| DaemonError::UnknownSession(session_id.clone()))?
137 .pages
138 .get_mut(&page_id)
139 .ok_or_else(|| DaemonError::UnknownPage(page_id.clone()))?;
140 page.apply_snapshot(tree)
141 };
142 ctx.after_token = Some(token);
143
144 let mut store = self.inner.store.lock().expect("poisoned");
145 store.update_page_token(&page_id, &token.to_string(), "engine", None)?;
146 drop(store);
147
148 Ok(ActResponse {
149 token,
150 warnings: Vec::new(),
151 })
152 })
153 }
154
155 pub fn find(&self, session_id: &str, query: &str) -> Result<FindResponse> {
156 let ctx = AuditCtx::new("vs_find", session_id).with_args(
157 query.to_string(),
158 tokens::args_hash("vs_find", &[query.to_string()]),
159 );
160 self.audit_call(ctx, |_ctx| {
161 self.require_session(session_id)?;
162 let mut hits = Vec::new();
163 let sessions = self.inner.sessions.lock().expect("poisoned");
164 let s = sessions
165 .get(session_id)
166 .ok_or_else(|| DaemonError::UnknownSession(session_id.to_string()))?;
167 for (page_id, page) in &s.pages {
168 if let Some(tree) = &page.last_tree {
169 for node in tree {
170 if node.label.contains(query) {
171 hits.push(FindHit {
172 page_id: page_id.clone(),
173 r: node.r,
174 role: node.role.to_string(),
175 label: node.label.clone(),
176 });
177 }
178 }
179 }
180 }
181 Ok(FindResponse { hits })
182 })
183 }
184
185 pub fn wait(
186 &self,
187 session_id: &str,
188 page_id: &str,
189 cond: EngineWait,
190 budget: Duration,
191 ) -> Result<WaitResponse> {
192 let ctx = AuditCtx::new("vs_wait", session_id)
193 .with_page(page_id)
194 .with_args(String::new(), tokens::args_hash("vs_wait", &[]));
195 self.audit_call(ctx, |ctx| {
196 let engine_handle = self.engine_handle_for(session_id, page_id)?;
197 self.inner.engine.wait(engine_handle, cond, budget)?;
198 let token = self
199 .current_token(session_id, page_id)
200 .unwrap_or(StateToken::ZERO);
201 ctx.after_token = Some(token);
202 Ok(WaitResponse { token })
203 })
204 }
205
206 pub fn status(&self, session_id_opt: Option<&str>) -> Result<StatusResponse> {
209 let ctx = AuditCtx::new("vs_status", session_id_opt.unwrap_or("(none)"))
210 .with_args(String::new(), tokens::args_hash("vs_status", &[]));
211 self.audit_call(ctx, |_ctx| {
212 let body = self.render_status(session_id_opt)?;
213 Ok(StatusResponse { body })
214 })
215 }
216
217 fn render_status(&self, session_id_opt: Option<&str>) -> Result<String> {
218 use std::fmt::Write as _;
219 let mut out = String::new();
220 let sessions = self.inner.sessions.lock().expect("poisoned");
221 if let Some(sid) = session_id_opt {
222 let s = sessions
223 .get(sid)
224 .ok_or_else(|| DaemonError::UnknownSession(sid.to_string()))?;
225 writeln!(out, "session\t{sid}\tpages={}", s.pages.len()).ok();
226 for (page_id, page) in &s.pages {
227 let token = page.last_token.map(|t| t.to_string()).unwrap_or_default();
228 writeln!(out, "page\t{page_id}\turl={}\ttoken={token}", page.url).ok();
229 }
230 } else {
231 writeln!(out, "workspace\tsessions={}", sessions.len()).ok();
232 for (id, s) in sessions.iter() {
233 writeln!(out, "session\t{id}\tpages={}", s.pages.len()).ok();
234 }
235 }
236 Ok(out)
237 }
238}