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_EDITOR_ID, UNIVERSAL_VIEW_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_view() -> Symbol {
38 Symbol::new(UNIVERSAL_VIEW_ID)
39}
40
41fn universal_editor() -> Symbol {
42 Symbol::new(UNIVERSAL_EDITOR_ID)
43}
44
45pub struct PhoneHost<T: Transport> {
54 session: Session<T>,
55 caps: SurfaceCaps,
56 queue: Vec<Expr>,
57 scenes: BTreeMap<Symbol, Expr>,
58}
59
60impl<T: Transport> PhoneHost<T> {
61 pub fn new(transport: T) -> Self {
63 Self {
64 session: Session::new(transport),
65 caps: surface::preset("phone").expect("phone is a known surface preset"),
66 queue: Vec::new(),
67 scenes: BTreeMap::new(),
68 }
69 }
70
71 fn pane() -> Symbol {
72 Symbol::new(PHONE_PANE)
73 }
74
75 pub fn open(&mut self, cx: &mut Cx, registry: &LensRegistry, resource: Symbol) -> Result<Expr> {
78 let pane = Self::pane();
79 let scene = self.session.open(
80 cx,
81 registry,
82 pane.clone(),
83 resource,
84 universal_view(),
85 universal_editor(),
86 )?;
87 self.scenes.insert(pane, scene.clone());
88 Ok(scene)
89 }
90
91 pub fn submit(
98 &mut self,
99 cx: &mut Cx,
100 registry: &LensRegistry,
101 intent: Expr,
102 ) -> Result<Vec<SceneUpdate>> {
103 match self.session.status() {
104 SessionStatus::Connected => {
105 self.session
106 .submit_intent(cx, registry, &Self::pane(), &intent)?;
107 let updates = self.session.pump(cx, registry)?;
108 self.cache(&updates);
109 Ok(updates)
110 }
111 _ => {
112 if self.queue.len() >= MAX_PHONE_OFFLINE_QUEUE {
113 return Err(Error::HostError(format!(
114 "phone offline queue is full ({MAX_PHONE_OFFLINE_QUEUE}); resume before submitting another intent"
115 )));
116 }
117 self.queue.push(intent);
118 Ok(Vec::new())
119 }
120 }
121 }
122
123 pub fn resume(&mut self, cx: &mut Cx, registry: &LensRegistry) -> Result<Vec<SceneUpdate>> {
137 let pane = Self::pane();
138 let mut applied = 0usize;
139 let mut failure = None;
140 while let Some(intent) = self.queue.first().cloned() {
141 match self.session.submit_intent(cx, registry, &pane, &intent) {
142 Ok(()) => {
143 self.queue.remove(0);
144 applied += 1;
145 }
146 Err(err) => {
147 failure = Some(err);
150 break;
151 }
152 }
153 }
154 if applied == 0
155 && let Some(err) = failure
156 {
157 return Err(err);
158 }
159 let updates = self.session.pump(cx, registry)?;
160 self.cache(&updates);
161 Ok(updates)
162 }
163
164 fn cache(&mut self, updates: &[SceneUpdate]) {
165 for update in updates {
166 self.scenes
167 .insert(update.pane.clone(), update.scene.clone());
168 }
169 }
170
171 pub fn caps(&self) -> &SurfaceCaps {
173 &self.caps
174 }
175
176 pub fn queued(&self) -> usize {
178 self.queue.len()
179 }
180
181 pub fn last_scene(&self, pane: &Symbol) -> Option<&Expr> {
183 self.scenes.get(pane)
184 }
185
186 pub fn transport_mut(&mut self) -> &mut T {
188 self.session.transport_mut()
189 }
190}
191
192pub struct DesktopHost<T: Transport> {
198 session: Session<T>,
199 caps: SurfaceCaps,
200 panes: Vec<Symbol>,
201}
202
203impl<T: Transport> DesktopHost<T> {
204 pub fn new(transport: T) -> Self {
206 Self {
207 session: Session::new(transport),
208 caps: surface::preset("desktop").expect("desktop is a known surface preset"),
209 panes: Vec::new(),
210 }
211 }
212
213 pub fn open_pane(
220 &mut self,
221 cx: &mut Cx,
222 registry: &LensRegistry,
223 pane: Symbol,
224 resource: Symbol,
225 ) -> Result<Expr> {
226 let scene = self.session.open(
227 cx,
228 registry,
229 pane.clone(),
230 resource,
231 universal_view(),
232 universal_editor(),
233 )?;
234 if !self.panes.contains(&pane) {
235 self.panes.push(pane);
236 }
237 Ok(scene)
238 }
239
240 pub fn submit(
245 &mut self,
246 cx: &mut Cx,
247 registry: &LensRegistry,
248 pane: &Symbol,
249 intent: Expr,
250 ) -> Result<Vec<SceneUpdate>> {
251 self.session.submit_intent(cx, registry, pane, &intent)?;
252 self.session.pump(cx, registry)
253 }
254
255 pub fn panes(&self) -> Vec<Symbol> {
257 self.panes.clone()
258 }
259
260 pub fn caps(&self) -> &SurfaceCaps {
262 &self.caps
263 }
264
265 pub fn transport_mut(&mut self) -> &mut T {
267 self.session.transport_mut()
268 }
269}
270
271#[cfg(test)]
272mod tests {
273
274 use sim_kernel::{Expr, NumberLiteral, Symbol};
275 use sim_lib_intent::{Origin, intent};
276 use sim_lib_view::{LensRegistry, register_universal_default};
277
278 use super::{DesktopHost, MAX_PHONE_OFFLINE_QUEUE, PHONE_PANE, PhoneHost};
279 use crate::fixture::FixtureTransport;
280 use crate::transport::Transport;
281
282 use sim_kernel::testing::eager_cx as cx;
283
284 fn registry() -> LensRegistry {
285 let mut registry = LensRegistry::new();
286 register_universal_default(&mut registry, false);
287 registry
288 }
289
290 use sim_value::build::keyword as sym;
291
292 fn number(value: &str) -> Expr {
293 Expr::Number(NumberLiteral {
294 domain: sym("i64"),
295 canonical: value.to_owned(),
296 })
297 }
298
299 fn doc() -> Expr {
300 Expr::Map(vec![
301 (Expr::Symbol(sym("a")), number("1")),
302 (Expr::Symbol(sym("b")), number("2")),
303 ])
304 }
305
306 fn edit(name: &str, value: &str) -> Expr {
308 intent(
309 "edit-field",
310 Origin::human(1),
311 vec![
312 ("target", doc()),
313 (
314 "path",
315 Expr::List(vec![Expr::Vector(vec![
316 Expr::Symbol(sym("k")),
317 Expr::Symbol(sym(name)),
318 ])]),
319 ),
320 ("value", number(value)),
321 ],
322 )
323 }
324
325 fn broken_edit() -> Expr {
329 intent(
330 "edit-field",
331 Origin::human(1),
332 vec![
333 ("target", doc()),
334 (
335 "path",
336 Expr::List(vec![Expr::Vector(vec![
337 Expr::Symbol(sym("i")),
338 Expr::String("0".to_owned()),
339 ])]),
340 ),
341 ("value", number("99")),
342 ],
343 )
344 }
345
346 fn field_of(value: &Expr, name: &str) -> Option<Expr> {
347 let Expr::Map(entries) = value else {
348 return None;
349 };
350 entries
351 .iter()
352 .find(|(k, _)| matches!(k, Expr::Symbol(s) if &*s.name == name))
353 .map(|(_, v)| v.clone())
354 }
355
356 #[test]
357 fn phone_caches_online_edits_and_queues_offline_ones_until_resume() {
358 let mut cx = cx();
359 let registry = registry();
360 let mut phone = PhoneHost::new(FixtureTransport::new().with(sym("doc"), doc()));
361 let pane = sym(PHONE_PANE);
362
363 let initial = phone.open(&mut cx, ®istry, sym("doc")).unwrap();
365 sim_lib_scene::validate_scene(&initial).expect("initial scene is valid");
366 assert_eq!(phone.last_scene(&pane), Some(&initial));
367
368 let online = phone.submit(&mut cx, ®istry, edit("a", "9")).unwrap();
370 assert_eq!(online.len(), 1, "the open pane updates");
371 assert_eq!(phone.queued(), 0, "nothing is queued while connected");
372 assert_eq!(phone.last_scene(&pane), Some(&online[0].scene));
373 assert_ne!(online[0].scene, initial, "the frame changed");
374
375 phone.transport_mut().disconnect();
377 let q1 = phone.submit(&mut cx, ®istry, edit("b", "8")).unwrap();
378 let q2 = phone.submit(&mut cx, ®istry, edit("a", "30")).unwrap();
379 assert!(
380 q1.is_empty() && q2.is_empty(),
381 "offline edits return no frames"
382 );
383 assert_eq!(phone.queued(), 2, "both offline edits are queued");
384
385 phone.transport_mut().begin_reconnect();
387 phone.transport_mut().reconnect();
388 let resumed = phone.resume(&mut cx, ®istry).unwrap();
389 assert_eq!(phone.queued(), 0, "the queue drained");
390 assert_eq!(resumed.len(), 2, "one frame per replayed edit, in order");
391
392 let value = phone.transport_mut().read(&sym("doc")).unwrap();
394 assert_eq!(field_of(&value, "a"), Some(number("30")));
395 assert_eq!(field_of(&value, "b"), Some(number("8")));
396
397 let latest = resumed.last().expect("resume produced frames");
399 assert_eq!(phone.last_scene(&pane), Some(&latest.scene));
400 }
401
402 #[test]
403 fn resume_stops_at_a_failing_intent_and_keeps_the_tail() {
404 let mut cx = cx();
405 let registry = registry();
406 let mut phone = PhoneHost::new(FixtureTransport::new().with(sym("doc"), doc()));
407 phone.open(&mut cx, ®istry, sym("doc")).unwrap();
408
409 phone.transport_mut().disconnect();
411 phone.submit(&mut cx, ®istry, edit("b", "8")).unwrap();
412 phone.submit(&mut cx, ®istry, broken_edit()).unwrap();
413 phone.submit(&mut cx, ®istry, edit("a", "30")).unwrap();
414 assert_eq!(phone.queued(), 3, "all three edits are queued offline");
415
416 phone.transport_mut().begin_reconnect();
419 phone.transport_mut().reconnect();
420 let updates = phone.resume(&mut cx, ®istry).unwrap();
421 assert!(!updates.is_empty(), "the committed edit produced a frame");
422 assert_eq!(
423 phone.queued(),
424 2,
425 "the failed Intent and its tail are NOT dropped"
426 );
427
428 let value = phone.transport_mut().read(&sym("doc")).unwrap();
430 assert_eq!(field_of(&value, "b"), Some(number("8")), "b := 8 applied");
431 assert_eq!(
432 field_of(&value, "a"),
433 Some(number("1")),
434 "a is untouched -- the post-failure edit did not apply"
435 );
436 }
437
438 #[test]
439 fn phone_offline_queue_has_a_backpressure_cap() {
440 let mut cx = cx();
441 let registry = registry();
442 let mut phone = PhoneHost::new(FixtureTransport::new().with(sym("doc"), doc()));
443 phone.open(&mut cx, ®istry, sym("doc")).unwrap();
444 phone.transport_mut().disconnect();
445
446 for index in 0..MAX_PHONE_OFFLINE_QUEUE {
447 phone
448 .submit(&mut cx, ®istry, edit("a", &index.to_string()))
449 .unwrap();
450 }
451
452 let err = phone
453 .submit(&mut cx, ®istry, edit("a", "999"))
454 .unwrap_err();
455
456 assert!(
457 err.to_string().contains("phone offline queue is full"),
458 "unexpected error: {err}"
459 );
460 assert_eq!(phone.queued(), MAX_PHONE_OFFLINE_QUEUE);
461 }
462
463 #[test]
464 fn desktop_fans_a_shared_resource_edit_out_to_every_pane() {
465 let mut cx = cx();
466 let registry = registry();
467 let mut desktop = DesktopHost::new(FixtureTransport::new().with(sym("doc"), doc()));
468
469 let scene_a = desktop
471 .open_pane(&mut cx, ®istry, sym("pane-a"), sym("doc"))
472 .unwrap();
473 let scene_b = desktop
474 .open_pane(&mut cx, ®istry, sym("pane-b"), sym("doc"))
475 .unwrap();
476 assert_eq!(desktop.panes(), vec![sym("pane-a"), sym("pane-b")]);
477
478 let updates = desktop
480 .submit(&mut cx, ®istry, &sym("pane-a"), edit("a", "9"))
481 .unwrap();
482 assert_eq!(updates.len(), 2, "both panes share the resource");
483 let panes: Vec<Symbol> = updates.iter().map(|u| u.pane.clone()).collect();
484 assert!(panes.contains(&sym("pane-a")) && panes.contains(&sym("pane-b")));
485
486 for update in &updates {
488 let initial = if update.pane == sym("pane-a") {
489 &scene_a
490 } else {
491 &scene_b
492 };
493 let rebuilt = sim_lib_scene::apply(initial, &update.diff).unwrap();
494 assert_eq!(rebuilt, update.scene, "the diff reconstructs the new Scene");
495 }
496 }
497}