1use std::collections::BTreeMap;
37use std::fs::File;
38use std::io::Read;
39use std::sync::Arc;
40use std::time::{Duration, Instant};
41
42use sim_codec_json::{JsonProjectionMode, project_expr_to_json, project_json_to_expr};
43use sim_kernel::{Cx, DefaultFactory, EagerPolicy, Expr, Result as SimResult, Symbol};
44use sim_lib_view::{LensRegistry, UNIVERSAL_SURFACE_CODEC_ID, register_universal_default, surface};
45use sim_lib_web_bridge::{FixtureTransport, SceneUpdate, Session};
46
47const INTENT_NAMESPACE: &str = "intent";
49
50pub const DEFAULT_PANE: &str = "pane-main";
53
54pub const DEFAULT_RESOURCE: &str = "demo";
56
57pub struct LiveSession {
65 session: Session<FixtureTransport>,
66 registry: LensRegistry,
67 cx: Cx,
68}
69
70impl LiveSession {
71 pub fn new() -> SimResult<Self> {
74 let mut transport = FixtureTransport::new();
75 transport.set(Symbol::new(DEFAULT_RESOURCE), demo_value());
76 let mut registry = LensRegistry::new();
77 register_universal_default(&mut registry, false);
78 let mut cx = Cx::new(Arc::new(EagerPolicy), Arc::new(DefaultFactory)); let mut session = Session::new(transport);
83 session.open_codec(
84 &mut cx,
85 ®istry,
86 Symbol::new(DEFAULT_PANE),
87 Symbol::new(DEFAULT_RESOURCE),
88 Symbol::new(UNIVERSAL_SURFACE_CODEC_ID),
89 surface::preset("webui").expect("webui is a known surface preset"),
90 )?;
91 Ok(Self {
92 session,
93 registry,
94 cx,
95 })
96 }
97
98 pub fn open(&mut self, resource: &str, pane: &str) -> SimResult<Expr> {
101 self.session.open_codec(
102 &mut self.cx,
103 &self.registry,
104 Symbol::new(pane),
105 Symbol::new(resource),
106 Symbol::new(UNIVERSAL_SURFACE_CODEC_ID),
107 surface::preset("webui").expect("webui is a known surface preset"),
108 )
109 }
110
111 pub fn submit(&mut self, pane: &str, intent: &Expr) -> SimResult<Vec<SceneUpdate>> {
114 self.session.submit_intent_at_rendered_revision(
115 &mut self.cx,
116 &self.registry,
117 &Symbol::new(pane),
118 intent,
119 )?;
120 self.session.pump(&mut self.cx, &self.registry)
121 }
122}
123
124pub trait LiveSurface {
129 fn open(&mut self, resource: &str, pane: &str) -> SimResult<Expr>;
131
132 fn submit(&mut self, pane: &str, intent: &Expr) -> SimResult<Vec<SceneUpdate>>;
134}
135
136impl LiveSurface for LiveSession {
137 fn open(&mut self, resource: &str, pane: &str) -> SimResult<Expr> {
138 Self::open(self, resource, pane)
139 }
140
141 fn submit(&mut self, pane: &str, intent: &Expr) -> SimResult<Vec<SceneUpdate>> {
142 Self::submit(self, pane, intent)
143 }
144}
145
146pub trait LiveSurfaceFactory {
148 fn create(&self) -> SimResult<Box<dyn LiveSurface>>;
151}
152
153#[derive(Debug, Default)]
155pub struct DefaultLiveSurfaceFactory;
156
157impl LiveSurfaceFactory for DefaultLiveSurfaceFactory {
158 fn create(&self) -> SimResult<Box<dyn LiveSurface>> {
159 LiveSession::new().map(|surface| Box::new(surface) as Box<dyn LiveSurface>)
160 }
161}
162
163#[derive(Debug, Clone)]
165pub struct LiveSessionTableConfig {
166 pub capacity: usize,
168 pub idle_ttl: Duration,
170}
171
172impl Default for LiveSessionTableConfig {
173 fn default() -> Self {
174 Self {
175 capacity: 64,
176 idle_ttl: Duration::from_secs(30 * 60),
177 }
178 }
179}
180
181struct LiveSessionEntry {
182 live: Box<dyn LiveSurface>,
183 last_used: Instant,
184 ordinal: u64,
185}
186
187pub struct LiveSessionTable {
189 factory: Box<dyn LiveSurfaceFactory + Send + Sync>,
190 config: LiveSessionTableConfig,
191 sessions: BTreeMap<String, LiveSessionEntry>,
192 next_ordinal: u64,
193}
194
195impl LiveSessionTable {
196 pub fn new(factory: Box<dyn LiveSurfaceFactory + Send + Sync>) -> Self {
198 Self::with_config(factory, LiveSessionTableConfig::default())
199 }
200
201 pub fn with_config(
203 factory: Box<dyn LiveSurfaceFactory + Send + Sync>,
204 config: LiveSessionTableConfig,
205 ) -> Self {
206 Self {
207 factory,
208 config,
209 sessions: BTreeMap::new(),
210 next_ordinal: 0,
211 }
212 }
213
214 #[cfg(test)]
215 fn len(&self) -> usize {
216 self.sessions.len()
217 }
218
219 pub fn open(
221 &mut self,
222 session_id: Option<&str>,
223 resource: &str,
224 pane: &str,
225 ) -> Result<(String, Expr), String> {
226 self.open_at(session_id, resource, pane, Instant::now())
227 }
228
229 pub fn submit(
231 &mut self,
232 session_id: &str,
233 pane: &str,
234 intent: &Expr,
235 ) -> Result<Vec<SceneUpdate>, String> {
236 self.submit_at(session_id, pane, intent, Instant::now())
237 }
238
239 pub fn close(&mut self, session_id: &str) -> Result<(), String> {
241 validate_session_id(session_id)?;
242 if self.sessions.remove(session_id).is_some() {
243 Ok(())
244 } else {
245 Err("unknown session id".to_owned())
246 }
247 }
248
249 pub fn open_at(
251 &mut self,
252 session_id: Option<&str>,
253 resource: &str,
254 pane: &str,
255 now: Instant,
256 ) -> Result<(String, Expr), String> {
257 self.evict_idle(now);
258 if let Some(session_id) = session_id {
259 let entry = self.entry_mut(session_id, now)?;
260 let scene = entry
261 .live
262 .open(resource, pane)
263 .map_err(|err| err.to_string())?;
264 return Ok((session_id.to_owned(), scene));
265 }
266 self.evict_for_capacity();
267 if self.config.capacity == 0 || self.sessions.len() >= self.config.capacity {
268 return Err("session capacity exhausted".to_owned());
269 }
270 let session_id = self.fresh_unused_session_id()?;
271 let mut live = self.factory.create().map_err(|err| err.to_string())?;
272 let scene = live.open(resource, pane).map_err(|err| err.to_string())?;
273 let ordinal = self.next_ordinal;
274 self.next_ordinal = self.next_ordinal.saturating_add(1);
275 self.sessions.insert(
276 session_id.clone(),
277 LiveSessionEntry {
278 live,
279 last_used: now,
280 ordinal,
281 },
282 );
283 Ok((session_id, scene))
284 }
285
286 pub fn submit_at(
288 &mut self,
289 session_id: &str,
290 pane: &str,
291 intent: &Expr,
292 now: Instant,
293 ) -> Result<Vec<SceneUpdate>, String> {
294 self.evict_idle(now);
295 let entry = self.entry_mut(session_id, now)?;
296 entry
297 .live
298 .submit(pane, intent)
299 .map_err(|err| err.to_string())
300 }
301
302 fn entry_mut(
303 &mut self,
304 session_id: &str,
305 now: Instant,
306 ) -> Result<&mut LiveSessionEntry, String> {
307 validate_session_id(session_id)?;
308 let entry = self
309 .sessions
310 .get_mut(session_id)
311 .ok_or_else(|| "unknown session id".to_owned())?;
312 entry.last_used = now;
313 Ok(entry)
314 }
315
316 fn evict_idle(&mut self, now: Instant) {
317 let ttl = self.config.idle_ttl;
318 self.sessions
319 .retain(|_, entry| now.duration_since(entry.last_used) <= ttl);
320 }
321
322 fn evict_for_capacity(&mut self) {
323 while self.config.capacity > 0 && self.sessions.len() >= self.config.capacity {
324 let Some(victim) = self
325 .sessions
326 .iter()
327 .min_by_key(|(_, entry)| (entry.last_used, entry.ordinal))
328 .map(|(id, _)| id.clone())
329 else {
330 return;
331 };
332 self.sessions.remove(&victim);
333 }
334 }
335
336 fn fresh_unused_session_id(&self) -> Result<String, String> {
337 for _ in 0..8 {
338 let session_id = fresh_session_id()?;
339 if !self.sessions.contains_key(&session_id) {
340 return Ok(session_id);
341 }
342 }
343 Err("could not allocate unique session id".to_owned())
344 }
345}
346
347fn validate_session_id(session_id: &str) -> Result<(), String> {
348 let valid = session_id.len() == 32 && session_id.bytes().all(|b| b.is_ascii_hexdigit());
349 if valid {
350 Ok(())
351 } else {
352 Err("malformed session id".to_owned())
353 }
354}
355
356fn fresh_session_id() -> Result<String, String> {
357 let mut bytes = [0u8; 16];
358 File::open("/dev/urandom")
359 .and_then(|mut file| file.read_exact(&mut bytes))
360 .map_err(|err| format!("could not allocate session id: {err}"))?;
361 Ok(bytes.iter().map(|byte| format!("{byte:02x}")).collect())
362}
363
364fn demo_value() -> Expr {
366 Expr::Map(vec![
367 (
368 Expr::Symbol(Symbol::new("title")),
369 Expr::String("SIM live session".to_owned()),
370 ),
371 (
372 Expr::Symbol(Symbol::new("note")),
373 Expr::String("edit me".to_owned()),
374 ),
375 ])
376}
377
378pub fn decode_intent_body(body: &str) -> Result<Expr, String> {
382 let value: serde_json::Value =
383 serde_json::from_str(body).map_err(|err| format!("invalid JSON intent body: {err}"))?;
384 let expr = project_json_to_expr(&value, JsonProjectionMode::UntaggedInterop);
385 lift_intent(expr)
386}
387
388pub fn encode_patches(updates: &[SceneUpdate]) -> String {
391 let patches: Vec<serde_json::Value> = updates
392 .iter()
393 .map(|update| project_expr_to_json(&update.diff, JsonProjectionMode::UntaggedInterop))
394 .collect();
395 serde_json::json!({ "patches": patches }).to_string()
396}
397
398pub fn encode_scene(scene: &Expr) -> String {
401 serde_json::json!({ "scene": project_expr_to_json(scene, JsonProjectionMode::UntaggedInterop) })
402 .to_string()
403}
404
405pub fn error_json(message: &str) -> String {
407 serde_json::json!({ "error": message }).to_string()
408}
409
410fn lift_intent(expr: Expr) -> Result<Expr, String> {
412 let Expr::Map(entries) = expr else {
413 return Err("intent body must be a JSON object".to_owned());
414 };
415 let mut lifted = Vec::with_capacity(entries.len());
416 for (key, value) in entries {
417 let name = key_name(&key)?;
418 let value = match name.as_str() {
419 "kind" => lift_kind(value)?,
420 "origin" => lift_origin(value),
421 "path" => lift_path(value),
422 _ => value,
423 };
424 lifted.push((Expr::Symbol(Symbol::new(name)), value));
425 }
426 Ok(Expr::Map(lifted))
427}
428
429fn key_name(key: &Expr) -> Result<String, String> {
431 match key {
432 Expr::Symbol(symbol) => Ok(symbol.name.to_string()),
433 Expr::String(text) => Ok(text.clone()),
434 other => Err(format!("intent key must be a string, found {other:?}")),
435 }
436}
437
438fn lift_kind(value: Expr) -> Result<Expr, String> {
441 match value {
442 Expr::Symbol(symbol) => Ok(Expr::Symbol(symbol)),
443 Expr::String(text) => {
444 let local = text.strip_prefix("intent/").unwrap_or(&text);
445 Ok(Expr::Symbol(Symbol::qualified(INTENT_NAMESPACE, local)))
446 }
447 other => Err(format!("intent 'kind' must be a string, found {other:?}")),
448 }
449}
450
451fn lift_origin(value: Expr) -> Expr {
453 let Expr::Map(entries) = value else {
454 return value;
455 };
456 let lifted = entries
457 .into_iter()
458 .map(|(key, value)| {
459 let is_operator = matches!(&key, Expr::Symbol(symbol) if &*symbol.name == "operator")
460 || matches!(&key, Expr::String(text) if text == "operator");
461 let value = match value {
462 Expr::String(text) if is_operator => Expr::Symbol(Symbol::new(text)),
463 other => other,
464 };
465 (key, value)
466 })
467 .collect();
468 Expr::Map(lifted)
469}
470
471fn lift_path(value: Expr) -> Expr {
476 let segments = match value {
477 Expr::List(segments) | Expr::Vector(segments) => segments,
478 other => return other,
479 };
480 Expr::List(segments.into_iter().map(lift_segment).collect())
481}
482
483fn lift_segment(segment: Expr) -> Expr {
485 let items = match segment {
486 Expr::List(items) | Expr::Vector(items) => items,
487 other => return other,
488 };
489 let lifted = items
490 .into_iter()
491 .enumerate()
492 .map(|(index, item)| match item {
493 Expr::String(text) if index == 0 => Expr::Symbol(Symbol::new(text)),
494 other => other,
495 })
496 .collect();
497 Expr::Vector(lifted)
498}
499
500#[cfg(test)]
501#[path = "live_tests.rs"]
502mod tests;