use std::collections::BTreeMap;
use sim_kernel::{Cx, Expr, Result, Symbol};
use sim_lib_view::surface::SurfaceCaps;
use sim_lib_view::{LensRegistry, UNIVERSAL_EDITOR_ID, UNIVERSAL_VIEW_ID, surface};
use crate::session::{SceneUpdate, Session};
use crate::transport::{SessionStatus, Transport};
pub const PHONE_PANE: &str = "phone:main";
fn universal_view() -> Symbol {
Symbol::new(UNIVERSAL_VIEW_ID)
}
fn universal_editor() -> Symbol {
Symbol::new(UNIVERSAL_EDITOR_ID)
}
pub struct PhoneHost<T: Transport> {
session: Session<T>,
caps: SurfaceCaps,
queue: Vec<Expr>,
scenes: BTreeMap<Symbol, Expr>,
}
impl<T: Transport> PhoneHost<T> {
pub fn new(transport: T) -> Self {
Self {
session: Session::new(transport),
caps: surface::preset("phone").expect("phone is a known surface preset"),
queue: Vec::new(),
scenes: BTreeMap::new(),
}
}
fn pane() -> Symbol {
Symbol::new(PHONE_PANE)
}
pub fn open(&mut self, cx: &mut Cx, registry: &LensRegistry, resource: Symbol) -> Result<Expr> {
let pane = Self::pane();
let scene = self.session.open(
cx,
registry,
pane.clone(),
resource,
universal_view(),
universal_editor(),
)?;
self.scenes.insert(pane, scene.clone());
Ok(scene)
}
pub fn submit(
&mut self,
cx: &mut Cx,
registry: &LensRegistry,
intent: Expr,
) -> Result<Vec<SceneUpdate>> {
match self.session.status() {
SessionStatus::Connected => {
self.session
.submit_intent(cx, registry, &Self::pane(), &intent)?;
let updates = self.session.pump(cx, registry)?;
self.cache(&updates);
Ok(updates)
}
_ => {
self.queue.push(intent);
Ok(Vec::new())
}
}
}
pub fn resume(&mut self, cx: &mut Cx, registry: &LensRegistry) -> Result<Vec<SceneUpdate>> {
let pane = Self::pane();
let mut applied = 0usize;
let mut failure = None;
while let Some(intent) = self.queue.first().cloned() {
match self.session.submit_intent(cx, registry, &pane, &intent) {
Ok(()) => {
self.queue.remove(0);
applied += 1;
}
Err(err) => {
failure = Some(err);
break;
}
}
}
if applied == 0
&& let Some(err) = failure
{
return Err(err);
}
let updates = self.session.pump(cx, registry)?;
self.cache(&updates);
Ok(updates)
}
fn cache(&mut self, updates: &[SceneUpdate]) {
for update in updates {
self.scenes
.insert(update.pane.clone(), update.scene.clone());
}
}
pub fn caps(&self) -> &SurfaceCaps {
&self.caps
}
pub fn queued(&self) -> usize {
self.queue.len()
}
pub fn last_scene(&self, pane: &Symbol) -> Option<&Expr> {
self.scenes.get(pane)
}
pub fn transport_mut(&mut self) -> &mut T {
self.session.transport_mut()
}
}
pub struct DesktopHost<T: Transport> {
session: Session<T>,
caps: SurfaceCaps,
panes: Vec<Symbol>,
}
impl<T: Transport> DesktopHost<T> {
pub fn new(transport: T) -> Self {
Self {
session: Session::new(transport),
caps: surface::preset("desktop").expect("desktop is a known surface preset"),
panes: Vec::new(),
}
}
pub fn open_pane(
&mut self,
cx: &mut Cx,
registry: &LensRegistry,
pane: Symbol,
resource: Symbol,
) -> Result<Expr> {
let scene = self.session.open(
cx,
registry,
pane.clone(),
resource,
universal_view(),
universal_editor(),
)?;
if !self.panes.contains(&pane) {
self.panes.push(pane);
}
Ok(scene)
}
pub fn submit(
&mut self,
cx: &mut Cx,
registry: &LensRegistry,
pane: &Symbol,
intent: Expr,
) -> Result<Vec<SceneUpdate>> {
self.session.submit_intent(cx, registry, pane, &intent)?;
self.session.pump(cx, registry)
}
pub fn panes(&self) -> Vec<Symbol> {
self.panes.clone()
}
pub fn caps(&self) -> &SurfaceCaps {
&self.caps
}
pub fn transport_mut(&mut self) -> &mut T {
self.session.transport_mut()
}
}
#[cfg(test)]
mod tests {
use sim_kernel::{Expr, NumberLiteral, Symbol};
use sim_lib_intent::{Origin, intent};
use sim_lib_view::{LensRegistry, register_universal_default};
use super::{DesktopHost, PHONE_PANE, PhoneHost};
use crate::fixture::FixtureTransport;
use crate::transport::Transport;
use sim_kernel::testing::eager_cx as cx;
fn registry() -> LensRegistry {
let mut registry = LensRegistry::new();
register_universal_default(&mut registry, false);
registry
}
use sim_value::build::keyword as sym;
fn number(value: &str) -> Expr {
Expr::Number(NumberLiteral {
domain: sym("i64"),
canonical: value.to_owned(),
})
}
fn doc() -> Expr {
Expr::Map(vec![
(Expr::Symbol(sym("a")), number("1")),
(Expr::Symbol(sym("b")), number("2")),
])
}
fn edit(name: &str, value: &str) -> Expr {
intent(
"edit-field",
Origin::human(1),
vec![
("target", doc()),
(
"path",
Expr::List(vec![Expr::Vector(vec![
Expr::Symbol(sym("k")),
Expr::Symbol(sym(name)),
])]),
),
("value", number(value)),
],
)
}
fn broken_edit() -> Expr {
intent(
"edit-field",
Origin::human(1),
vec![
("target", doc()),
(
"path",
Expr::List(vec![Expr::Vector(vec![
Expr::Symbol(sym("i")),
Expr::String("0".to_owned()),
])]),
),
("value", number("99")),
],
)
}
fn field_of(value: &Expr, name: &str) -> Option<Expr> {
let Expr::Map(entries) = value else {
return None;
};
entries
.iter()
.find(|(k, _)| matches!(k, Expr::Symbol(s) if &*s.name == name))
.map(|(_, v)| v.clone())
}
#[test]
fn phone_caches_online_edits_and_queues_offline_ones_until_resume() {
let mut cx = cx();
let registry = registry();
let mut phone = PhoneHost::new(FixtureTransport::new().with(sym("doc"), doc()));
let pane = sym(PHONE_PANE);
let initial = phone.open(&mut cx, ®istry, sym("doc")).unwrap();
sim_lib_scene::validate_scene(&initial).expect("initial scene is valid");
assert_eq!(phone.last_scene(&pane), Some(&initial));
let online = phone.submit(&mut cx, ®istry, edit("a", "9")).unwrap();
assert_eq!(online.len(), 1, "the open pane updates");
assert_eq!(phone.queued(), 0, "nothing is queued while connected");
assert_eq!(phone.last_scene(&pane), Some(&online[0].scene));
assert_ne!(online[0].scene, initial, "the frame changed");
phone.transport_mut().disconnect();
let q1 = phone.submit(&mut cx, ®istry, edit("b", "8")).unwrap();
let q2 = phone.submit(&mut cx, ®istry, edit("a", "30")).unwrap();
assert!(
q1.is_empty() && q2.is_empty(),
"offline edits return no frames"
);
assert_eq!(phone.queued(), 2, "both offline edits are queued");
phone.transport_mut().begin_reconnect();
phone.transport_mut().reconnect();
let resumed = phone.resume(&mut cx, ®istry).unwrap();
assert_eq!(phone.queued(), 0, "the queue drained");
assert_eq!(resumed.len(), 2, "one frame per replayed edit, in order");
let value = phone.transport_mut().read(&sym("doc")).unwrap();
assert_eq!(field_of(&value, "a"), Some(number("30")));
assert_eq!(field_of(&value, "b"), Some(number("8")));
let latest = resumed.last().expect("resume produced frames");
assert_eq!(phone.last_scene(&pane), Some(&latest.scene));
}
#[test]
fn resume_stops_at_a_failing_intent_and_keeps_the_tail() {
let mut cx = cx();
let registry = registry();
let mut phone = PhoneHost::new(FixtureTransport::new().with(sym("doc"), doc()));
phone.open(&mut cx, ®istry, sym("doc")).unwrap();
phone.transport_mut().disconnect();
phone.submit(&mut cx, ®istry, edit("b", "8")).unwrap();
phone.submit(&mut cx, ®istry, broken_edit()).unwrap();
phone.submit(&mut cx, ®istry, edit("a", "30")).unwrap();
assert_eq!(phone.queued(), 3, "all three edits are queued offline");
phone.transport_mut().begin_reconnect();
phone.transport_mut().reconnect();
let updates = phone.resume(&mut cx, ®istry).unwrap();
assert!(!updates.is_empty(), "the committed edit produced a frame");
assert_eq!(
phone.queued(),
2,
"the failed Intent and its tail are NOT dropped"
);
let value = phone.transport_mut().read(&sym("doc")).unwrap();
assert_eq!(field_of(&value, "b"), Some(number("8")), "b := 8 applied");
assert_eq!(
field_of(&value, "a"),
Some(number("1")),
"a is untouched -- the post-failure edit did not apply"
);
}
#[test]
fn desktop_fans_a_shared_resource_edit_out_to_every_pane() {
let mut cx = cx();
let registry = registry();
let mut desktop = DesktopHost::new(FixtureTransport::new().with(sym("doc"), doc()));
let scene_a = desktop
.open_pane(&mut cx, ®istry, sym("pane-a"), sym("doc"))
.unwrap();
let scene_b = desktop
.open_pane(&mut cx, ®istry, sym("pane-b"), sym("doc"))
.unwrap();
assert_eq!(desktop.panes(), vec![sym("pane-a"), sym("pane-b")]);
let updates = desktop
.submit(&mut cx, ®istry, &sym("pane-a"), edit("a", "9"))
.unwrap();
assert_eq!(updates.len(), 2, "both panes share the resource");
let panes: Vec<Symbol> = updates.iter().map(|u| u.pane.clone()).collect();
assert!(panes.contains(&sym("pane-a")) && panes.contains(&sym("pane-b")));
for update in &updates {
let initial = if update.pane == sym("pane-a") {
&scene_a
} else {
&scene_b
};
let rebuilt = sim_lib_scene::apply(initial, &update.diff).unwrap();
assert_eq!(rebuilt, update.scene, "the diff reconstructs the new Scene");
}
}
}