1use std::collections::BTreeMap;
19
20use sim_kernel::{Cx, Error, Expr, Result, Symbol};
21use sim_lib_view::surface::SurfaceCaps;
22use sim_lib_view::{LensRegistry, UNIVERSAL_SURFACE_CODEC_ID, surface};
23
24use crate::session::{SceneUpdate, Session};
25use crate::transport::{SessionStatus, Transport};
26
27pub const PHONE_PANE: &str = "phone:main";
33
34pub const MAX_PHONE_OFFLINE_QUEUE: usize = 128;
36
37fn universal_surface_codec() -> Symbol {
38 Symbol::new(UNIVERSAL_SURFACE_CODEC_ID)
39}
40
41pub struct PhoneHost<T: Transport> {
50 session: Session<T>,
51 surface_codec: Symbol,
52 caps: SurfaceCaps,
53 queue: Vec<Expr>,
54 scenes: BTreeMap<Symbol, Expr>,
55}
56
57impl<T: Transport> PhoneHost<T> {
58 pub fn new(transport: T) -> Self {
60 Self::with_surface_codec(transport, universal_surface_codec())
61 }
62
63 pub fn with_surface_codec(transport: T, surface_codec: Symbol) -> Self {
68 Self {
69 session: Session::new(transport),
70 surface_codec,
71 caps: surface::preset("phone").expect("phone is a known surface preset"),
72 queue: Vec::new(),
73 scenes: BTreeMap::new(),
74 }
75 }
76
77 fn pane() -> Symbol {
78 Symbol::new(PHONE_PANE)
79 }
80
81 pub fn open(&mut self, cx: &mut Cx, registry: &LensRegistry, resource: Symbol) -> Result<Expr> {
84 let pane = Self::pane();
85 let scene = self.session.open_codec(
86 cx,
87 registry,
88 pane.clone(),
89 resource,
90 self.surface_codec.clone(),
91 self.caps.clone(),
92 )?;
93 self.scenes.insert(pane, scene.clone());
94 Ok(scene)
95 }
96
97 pub fn submit(
104 &mut self,
105 cx: &mut Cx,
106 registry: &LensRegistry,
107 intent: Expr,
108 ) -> Result<Vec<SceneUpdate>> {
109 match self.session.status() {
110 SessionStatus::Connected => {
111 self.session.submit_intent_at_rendered_revision(
112 cx,
113 registry,
114 &Self::pane(),
115 &intent,
116 )?;
117 let updates = self.session.pump(cx, registry)?;
118 self.cache(&updates);
119 Ok(updates)
120 }
121 _ => {
122 if self.queue.len() >= MAX_PHONE_OFFLINE_QUEUE {
123 return Err(Error::HostError(format!(
124 "phone offline queue is full ({MAX_PHONE_OFFLINE_QUEUE}); resume before submitting another intent"
125 )));
126 }
127 self.queue.push(intent);
128 Ok(Vec::new())
129 }
130 }
131 }
132
133 pub fn resume(&mut self, cx: &mut Cx, registry: &LensRegistry) -> Result<Vec<SceneUpdate>> {
147 let pane = Self::pane();
148 let mut applied = 0usize;
149 let mut updates = Vec::new();
150 let mut failure = None;
151 while let Some(intent) = self.queue.first().cloned() {
152 match self
153 .session
154 .submit_intent_at_rendered_revision(cx, registry, &pane, &intent)
155 {
156 Ok(()) => {
157 self.queue.remove(0);
158 applied += 1;
159 let next = self.session.pump(cx, registry)?;
160 self.cache(&next);
161 updates.extend(next);
162 }
163 Err(err) => {
164 failure = Some(err);
167 break;
168 }
169 }
170 }
171 if applied == 0
172 && let Some(err) = failure
173 {
174 return Err(err);
175 }
176 Ok(updates)
177 }
178
179 fn cache(&mut self, updates: &[SceneUpdate]) {
180 for update in updates {
181 self.scenes
182 .insert(update.pane.clone(), update.scene.clone());
183 }
184 }
185
186 pub fn caps(&self) -> &SurfaceCaps {
188 &self.caps
189 }
190
191 pub fn surface_codec(&self) -> &Symbol {
193 &self.surface_codec
194 }
195
196 pub fn queued(&self) -> usize {
198 self.queue.len()
199 }
200
201 pub fn last_scene(&self, pane: &Symbol) -> Option<&Expr> {
203 self.scenes.get(pane)
204 }
205
206 pub fn transport_mut(&mut self) -> &mut T {
208 self.session.transport_mut()
209 }
210}
211
212pub struct DesktopHost<T: Transport> {
218 session: Session<T>,
219 surface_codec: Symbol,
220 caps: SurfaceCaps,
221 panes: Vec<Symbol>,
222}
223
224impl<T: Transport> DesktopHost<T> {
225 pub fn new(transport: T) -> Self {
227 Self::with_surface_codec(transport, universal_surface_codec())
228 }
229
230 pub fn with_surface_codec(transport: T, surface_codec: Symbol) -> Self {
232 Self {
233 session: Session::new(transport),
234 surface_codec,
235 caps: surface::preset("desktop").expect("desktop is a known surface preset"),
236 panes: Vec::new(),
237 }
238 }
239
240 pub fn open_pane(
247 &mut self,
248 cx: &mut Cx,
249 registry: &LensRegistry,
250 pane: Symbol,
251 resource: Symbol,
252 ) -> Result<Expr> {
253 let scene = self.session.open_codec(
254 cx,
255 registry,
256 pane.clone(),
257 resource,
258 self.surface_codec.clone(),
259 self.caps.clone(),
260 )?;
261 if !self.panes.contains(&pane) {
262 self.panes.push(pane);
263 }
264 Ok(scene)
265 }
266
267 pub fn submit(
272 &mut self,
273 cx: &mut Cx,
274 registry: &LensRegistry,
275 pane: &Symbol,
276 intent: Expr,
277 ) -> Result<Vec<SceneUpdate>> {
278 self.session
279 .submit_intent_at_rendered_revision(cx, registry, pane, &intent)?;
280 self.session.pump(cx, registry)
281 }
282
283 pub fn panes(&self) -> Vec<Symbol> {
285 self.panes.clone()
286 }
287
288 pub fn caps(&self) -> &SurfaceCaps {
290 &self.caps
291 }
292
293 pub fn surface_codec(&self) -> &Symbol {
295 &self.surface_codec
296 }
297
298 pub fn transport_mut(&mut self) -> &mut T {
300 self.session.transport_mut()
301 }
302}
303
304#[cfg(test)]
305mod tests {
306
307 use sim_kernel::{Expr, NumberLiteral, Symbol};
308 use sim_lib_intent::{Origin, intent};
309 use sim_lib_view::{LensRegistry, register_universal_default};
310
311 use super::{DesktopHost, MAX_PHONE_OFFLINE_QUEUE, PHONE_PANE, PhoneHost};
312 use crate::fixture::FixtureTransport;
313 use crate::transport::Transport;
314
315 use sim_kernel::testing::eager_cx as cx;
316
317 fn registry() -> LensRegistry {
318 let mut registry = LensRegistry::new();
319 register_universal_default(&mut registry, false);
320 registry
321 }
322
323 use sim_value::build::keyword as sym;
324
325 fn number(value: &str) -> Expr {
326 Expr::Number(NumberLiteral {
327 domain: sym("i64"),
328 canonical: value.to_owned(),
329 })
330 }
331
332 fn doc() -> Expr {
333 Expr::Map(vec![
334 (Expr::Symbol(sym("a")), number("1")),
335 (Expr::Symbol(sym("b")), number("2")),
336 ])
337 }
338
339 fn edit(name: &str, value: &str) -> Expr {
341 intent(
342 "edit-field",
343 Origin::human(1),
344 vec![
345 ("target", doc()),
346 (
347 "path",
348 Expr::List(vec![Expr::Vector(vec![
349 Expr::Symbol(sym("k")),
350 Expr::Symbol(sym(name)),
351 ])]),
352 ),
353 ("value", number(value)),
354 ],
355 )
356 }
357
358 fn broken_edit() -> Expr {
362 intent(
363 "edit-field",
364 Origin::human(1),
365 vec![
366 ("target", doc()),
367 (
368 "path",
369 Expr::List(vec![Expr::Vector(vec![
370 Expr::Symbol(sym("i")),
371 Expr::String("0".to_owned()),
372 ])]),
373 ),
374 ("value", number("99")),
375 ],
376 )
377 }
378
379 fn field_of(value: &Expr, name: &str) -> Option<Expr> {
380 let Expr::Map(entries) = value else {
381 return None;
382 };
383 entries
384 .iter()
385 .find(|(k, _)| matches!(k, Expr::Symbol(s) if &*s.name == name))
386 .map(|(_, v)| v.clone())
387 }
388
389 #[test]
390 fn phone_caches_online_edits_and_queues_offline_ones_until_resume() {
391 let mut cx = cx();
392 let registry = registry();
393 let mut phone = PhoneHost::new(FixtureTransport::new().with(sym("doc"), doc()));
394 let pane = sym(PHONE_PANE);
395
396 let initial = phone.open(&mut cx, ®istry, sym("doc")).unwrap();
398 sim_lib_scene::validate_scene(&initial).expect("initial scene is valid");
399 assert_eq!(phone.last_scene(&pane), Some(&initial));
400
401 let online = phone.submit(&mut cx, ®istry, edit("a", "9")).unwrap();
403 assert_eq!(online.len(), 1, "the open pane updates");
404 assert_eq!(phone.queued(), 0, "nothing is queued while connected");
405 assert_eq!(phone.last_scene(&pane), Some(&online[0].scene));
406 assert_ne!(online[0].scene, initial, "the frame changed");
407
408 phone.transport_mut().disconnect();
410 let q1 = phone.submit(&mut cx, ®istry, edit("b", "8")).unwrap();
411 let q2 = phone.submit(&mut cx, ®istry, edit("a", "30")).unwrap();
412 assert!(
413 q1.is_empty() && q2.is_empty(),
414 "offline edits return no frames"
415 );
416 assert_eq!(phone.queued(), 2, "both offline edits are queued");
417
418 phone.transport_mut().begin_reconnect();
420 phone.transport_mut().reconnect();
421 let resumed = phone.resume(&mut cx, ®istry).unwrap();
422 assert_eq!(phone.queued(), 0, "the queue drained");
423 assert_eq!(resumed.len(), 2, "one frame per replayed edit, in order");
424
425 let value = phone.transport_mut().read(&mut cx, &sym("doc")).unwrap();
427 assert_eq!(field_of(&value, "a"), Some(number("30")));
428 assert_eq!(field_of(&value, "b"), Some(number("8")));
429
430 let latest = resumed.last().expect("resume produced frames");
432 assert_eq!(phone.last_scene(&pane), Some(&latest.scene));
433 }
434
435 #[test]
436 fn resume_stops_at_a_failing_intent_and_keeps_the_tail() {
437 let mut cx = cx();
438 let registry = registry();
439 let mut phone = PhoneHost::new(FixtureTransport::new().with(sym("doc"), doc()));
440 phone.open(&mut cx, ®istry, sym("doc")).unwrap();
441
442 phone.transport_mut().disconnect();
444 phone.submit(&mut cx, ®istry, edit("b", "8")).unwrap();
445 phone.submit(&mut cx, ®istry, broken_edit()).unwrap();
446 phone.submit(&mut cx, ®istry, edit("a", "30")).unwrap();
447 assert_eq!(phone.queued(), 3, "all three edits are queued offline");
448
449 phone.transport_mut().begin_reconnect();
452 phone.transport_mut().reconnect();
453 let updates = phone.resume(&mut cx, ®istry).unwrap();
454 assert!(!updates.is_empty(), "the committed edit produced a frame");
455 assert_eq!(
456 phone.queued(),
457 2,
458 "the failed Intent and its tail are NOT dropped"
459 );
460
461 let value = phone.transport_mut().read(&mut cx, &sym("doc")).unwrap();
463 assert_eq!(field_of(&value, "b"), Some(number("8")), "b := 8 applied");
464 assert_eq!(
465 field_of(&value, "a"),
466 Some(number("1")),
467 "a is untouched -- the post-failure edit did not apply"
468 );
469 }
470
471 #[test]
472 fn phone_offline_queue_has_a_backpressure_cap() {
473 let mut cx = cx();
474 let registry = registry();
475 let mut phone = PhoneHost::new(FixtureTransport::new().with(sym("doc"), doc()));
476 phone.open(&mut cx, ®istry, sym("doc")).unwrap();
477 phone.transport_mut().disconnect();
478
479 for index in 0..MAX_PHONE_OFFLINE_QUEUE {
480 phone
481 .submit(&mut cx, ®istry, edit("a", &index.to_string()))
482 .unwrap();
483 }
484
485 let err = phone
486 .submit(&mut cx, ®istry, edit("a", "999"))
487 .unwrap_err();
488
489 assert!(
490 err.to_string().contains("phone offline queue is full"),
491 "unexpected error: {err}"
492 );
493 assert_eq!(phone.queued(), MAX_PHONE_OFFLINE_QUEUE);
494 }
495
496 #[test]
497 fn desktop_fans_a_shared_resource_edit_out_to_every_pane() {
498 let mut cx = cx();
499 let registry = registry();
500 let mut desktop = DesktopHost::new(FixtureTransport::new().with(sym("doc"), doc()));
501
502 let scene_a = desktop
504 .open_pane(&mut cx, ®istry, sym("pane-a"), sym("doc"))
505 .unwrap();
506 let scene_b = desktop
507 .open_pane(&mut cx, ®istry, sym("pane-b"), sym("doc"))
508 .unwrap();
509 assert_eq!(desktop.panes(), vec![sym("pane-a"), sym("pane-b")]);
510
511 let updates = desktop
513 .submit(&mut cx, ®istry, &sym("pane-a"), edit("a", "9"))
514 .unwrap();
515 assert_eq!(updates.len(), 2, "both panes share the resource");
516 let panes: Vec<Symbol> = updates.iter().map(|u| u.pane.clone()).collect();
517 assert!(panes.contains(&sym("pane-a")) && panes.contains(&sym("pane-b")));
518
519 for update in &updates {
521 let initial = if update.pane == sym("pane-a") {
522 &scene_a
523 } else {
524 &scene_b
525 };
526 let rebuilt = sim_lib_scene::apply(initial, &update.diff).unwrap();
527 assert_eq!(rebuilt, update.scene, "the diff reconstructs the new Scene");
528 }
529 }
530}