1use std::collections::BTreeMap;
19
20use sim_kernel::{Cx, 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
34fn universal_view() -> Symbol {
35 Symbol::new(UNIVERSAL_VIEW_ID)
36}
37
38fn universal_editor() -> Symbol {
39 Symbol::new(UNIVERSAL_EDITOR_ID)
40}
41
42pub struct PhoneHost<T: Transport> {
51 session: Session<T>,
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 {
61 session: Session::new(transport),
62 caps: surface::preset("phone").expect("phone is a known surface preset"),
63 queue: Vec::new(),
64 scenes: BTreeMap::new(),
65 }
66 }
67
68 fn pane() -> Symbol {
69 Symbol::new(PHONE_PANE)
70 }
71
72 pub fn open(&mut self, cx: &mut Cx, registry: &LensRegistry, resource: Symbol) -> Result<Expr> {
75 let pane = Self::pane();
76 let scene = self.session.open(
77 cx,
78 registry,
79 pane.clone(),
80 resource,
81 universal_view(),
82 universal_editor(),
83 )?;
84 self.scenes.insert(pane, scene.clone());
85 Ok(scene)
86 }
87
88 pub fn submit(
95 &mut self,
96 cx: &mut Cx,
97 registry: &LensRegistry,
98 intent: Expr,
99 ) -> Result<Vec<SceneUpdate>> {
100 match self.session.status() {
101 SessionStatus::Connected => {
102 self.session
103 .submit_intent(cx, registry, &Self::pane(), &intent)?;
104 let updates = self.session.pump(cx, registry)?;
105 self.cache(&updates);
106 Ok(updates)
107 }
108 _ => {
109 self.queue.push(intent);
110 Ok(Vec::new())
111 }
112 }
113 }
114
115 pub fn resume(&mut self, cx: &mut Cx, registry: &LensRegistry) -> Result<Vec<SceneUpdate>> {
129 let pane = Self::pane();
130 let mut applied = 0usize;
131 let mut failure = None;
132 while let Some(intent) = self.queue.first().cloned() {
133 match self.session.submit_intent(cx, registry, &pane, &intent) {
134 Ok(()) => {
135 self.queue.remove(0);
136 applied += 1;
137 }
138 Err(err) => {
139 failure = Some(err);
142 break;
143 }
144 }
145 }
146 if applied == 0
147 && let Some(err) = failure
148 {
149 return Err(err);
150 }
151 let updates = self.session.pump(cx, registry)?;
152 self.cache(&updates);
153 Ok(updates)
154 }
155
156 fn cache(&mut self, updates: &[SceneUpdate]) {
157 for update in updates {
158 self.scenes
159 .insert(update.pane.clone(), update.scene.clone());
160 }
161 }
162
163 pub fn caps(&self) -> &SurfaceCaps {
165 &self.caps
166 }
167
168 pub fn queued(&self) -> usize {
170 self.queue.len()
171 }
172
173 pub fn last_scene(&self, pane: &Symbol) -> Option<&Expr> {
175 self.scenes.get(pane)
176 }
177
178 pub fn transport_mut(&mut self) -> &mut T {
180 self.session.transport_mut()
181 }
182}
183
184pub struct DesktopHost<T: Transport> {
190 session: Session<T>,
191 caps: SurfaceCaps,
192 panes: Vec<Symbol>,
193}
194
195impl<T: Transport> DesktopHost<T> {
196 pub fn new(transport: T) -> Self {
198 Self {
199 session: Session::new(transport),
200 caps: surface::preset("desktop").expect("desktop is a known surface preset"),
201 panes: Vec::new(),
202 }
203 }
204
205 pub fn open_pane(
212 &mut self,
213 cx: &mut Cx,
214 registry: &LensRegistry,
215 pane: Symbol,
216 resource: Symbol,
217 ) -> Result<Expr> {
218 let scene = self.session.open(
219 cx,
220 registry,
221 pane.clone(),
222 resource,
223 universal_view(),
224 universal_editor(),
225 )?;
226 if !self.panes.contains(&pane) {
227 self.panes.push(pane);
228 }
229 Ok(scene)
230 }
231
232 pub fn submit(
237 &mut self,
238 cx: &mut Cx,
239 registry: &LensRegistry,
240 pane: &Symbol,
241 intent: Expr,
242 ) -> Result<Vec<SceneUpdate>> {
243 self.session.submit_intent(cx, registry, pane, &intent)?;
244 self.session.pump(cx, registry)
245 }
246
247 pub fn panes(&self) -> Vec<Symbol> {
249 self.panes.clone()
250 }
251
252 pub fn caps(&self) -> &SurfaceCaps {
254 &self.caps
255 }
256
257 pub fn transport_mut(&mut self) -> &mut T {
259 self.session.transport_mut()
260 }
261}
262
263#[cfg(test)]
264mod tests {
265
266 use sim_kernel::{Expr, NumberLiteral, Symbol};
267 use sim_lib_intent::{Origin, intent};
268 use sim_lib_view::{LensRegistry, register_universal_default};
269
270 use super::{DesktopHost, PHONE_PANE, PhoneHost};
271 use crate::fixture::FixtureTransport;
272 use crate::transport::Transport;
273
274 use sim_kernel::testing::eager_cx as cx;
275
276 fn registry() -> LensRegistry {
277 let mut registry = LensRegistry::new();
278 register_universal_default(&mut registry, false);
279 registry
280 }
281
282 use sim_value::build::keyword as sym;
283
284 fn number(value: &str) -> Expr {
285 Expr::Number(NumberLiteral {
286 domain: sym("i64"),
287 canonical: value.to_owned(),
288 })
289 }
290
291 fn doc() -> Expr {
292 Expr::Map(vec![
293 (Expr::Symbol(sym("a")), number("1")),
294 (Expr::Symbol(sym("b")), number("2")),
295 ])
296 }
297
298 fn edit(name: &str, value: &str) -> Expr {
300 intent(
301 "edit-field",
302 Origin::human(1),
303 vec![
304 ("target", doc()),
305 (
306 "path",
307 Expr::List(vec![Expr::Vector(vec![
308 Expr::Symbol(sym("k")),
309 Expr::Symbol(sym(name)),
310 ])]),
311 ),
312 ("value", number(value)),
313 ],
314 )
315 }
316
317 fn broken_edit() -> Expr {
321 intent(
322 "edit-field",
323 Origin::human(1),
324 vec![
325 ("target", doc()),
326 (
327 "path",
328 Expr::List(vec![Expr::Vector(vec![
329 Expr::Symbol(sym("i")),
330 Expr::String("0".to_owned()),
331 ])]),
332 ),
333 ("value", number("99")),
334 ],
335 )
336 }
337
338 fn field_of(value: &Expr, name: &str) -> Option<Expr> {
339 let Expr::Map(entries) = value else {
340 return None;
341 };
342 entries
343 .iter()
344 .find(|(k, _)| matches!(k, Expr::Symbol(s) if &*s.name == name))
345 .map(|(_, v)| v.clone())
346 }
347
348 #[test]
349 fn phone_caches_online_edits_and_queues_offline_ones_until_resume() {
350 let mut cx = cx();
351 let registry = registry();
352 let mut phone = PhoneHost::new(FixtureTransport::new().with(sym("doc"), doc()));
353 let pane = sym(PHONE_PANE);
354
355 let initial = phone.open(&mut cx, ®istry, sym("doc")).unwrap();
357 sim_lib_scene::validate_scene(&initial).expect("initial scene is valid");
358 assert_eq!(phone.last_scene(&pane), Some(&initial));
359
360 let online = phone.submit(&mut cx, ®istry, edit("a", "9")).unwrap();
362 assert_eq!(online.len(), 1, "the open pane updates");
363 assert_eq!(phone.queued(), 0, "nothing is queued while connected");
364 assert_eq!(phone.last_scene(&pane), Some(&online[0].scene));
365 assert_ne!(online[0].scene, initial, "the frame changed");
366
367 phone.transport_mut().disconnect();
369 let q1 = phone.submit(&mut cx, ®istry, edit("b", "8")).unwrap();
370 let q2 = phone.submit(&mut cx, ®istry, edit("a", "30")).unwrap();
371 assert!(
372 q1.is_empty() && q2.is_empty(),
373 "offline edits return no frames"
374 );
375 assert_eq!(phone.queued(), 2, "both offline edits are queued");
376
377 phone.transport_mut().begin_reconnect();
379 phone.transport_mut().reconnect();
380 let resumed = phone.resume(&mut cx, ®istry).unwrap();
381 assert_eq!(phone.queued(), 0, "the queue drained");
382 assert_eq!(resumed.len(), 2, "one frame per replayed edit, in order");
383
384 let value = phone.transport_mut().read(&sym("doc")).unwrap();
386 assert_eq!(field_of(&value, "a"), Some(number("30")));
387 assert_eq!(field_of(&value, "b"), Some(number("8")));
388
389 let latest = resumed.last().expect("resume produced frames");
391 assert_eq!(phone.last_scene(&pane), Some(&latest.scene));
392 }
393
394 #[test]
395 fn resume_stops_at_a_failing_intent_and_keeps_the_tail() {
396 let mut cx = cx();
397 let registry = registry();
398 let mut phone = PhoneHost::new(FixtureTransport::new().with(sym("doc"), doc()));
399 phone.open(&mut cx, ®istry, sym("doc")).unwrap();
400
401 phone.transport_mut().disconnect();
403 phone.submit(&mut cx, ®istry, edit("b", "8")).unwrap();
404 phone.submit(&mut cx, ®istry, broken_edit()).unwrap();
405 phone.submit(&mut cx, ®istry, edit("a", "30")).unwrap();
406 assert_eq!(phone.queued(), 3, "all three edits are queued offline");
407
408 phone.transport_mut().begin_reconnect();
411 phone.transport_mut().reconnect();
412 let updates = phone.resume(&mut cx, ®istry).unwrap();
413 assert!(!updates.is_empty(), "the committed edit produced a frame");
414 assert_eq!(
415 phone.queued(),
416 2,
417 "the failed Intent and its tail are NOT dropped"
418 );
419
420 let value = phone.transport_mut().read(&sym("doc")).unwrap();
422 assert_eq!(field_of(&value, "b"), Some(number("8")), "b := 8 applied");
423 assert_eq!(
424 field_of(&value, "a"),
425 Some(number("1")),
426 "a is untouched -- the post-failure edit did not apply"
427 );
428 }
429
430 #[test]
431 fn desktop_fans_a_shared_resource_edit_out_to_every_pane() {
432 let mut cx = cx();
433 let registry = registry();
434 let mut desktop = DesktopHost::new(FixtureTransport::new().with(sym("doc"), doc()));
435
436 let scene_a = desktop
438 .open_pane(&mut cx, ®istry, sym("pane-a"), sym("doc"))
439 .unwrap();
440 let scene_b = desktop
441 .open_pane(&mut cx, ®istry, sym("pane-b"), sym("doc"))
442 .unwrap();
443 assert_eq!(desktop.panes(), vec![sym("pane-a"), sym("pane-b")]);
444
445 let updates = desktop
447 .submit(&mut cx, ®istry, &sym("pane-a"), edit("a", "9"))
448 .unwrap();
449 assert_eq!(updates.len(), 2, "both panes share the resource");
450 let panes: Vec<Symbol> = updates.iter().map(|u| u.pane.clone()).collect();
451 assert!(panes.contains(&sym("pane-a")) && panes.contains(&sym("pane-b")));
452
453 for update in &updates {
455 let initial = if update.pane == sym("pane-a") {
456 &scene_a
457 } else {
458 &scene_b
459 };
460 let rebuilt = sim_lib_scene::apply(initial, &update.diff).unwrap();
461 assert_eq!(rebuilt, update.scene, "the diff reconstructs the new Scene");
462 }
463 }
464}