Skip to main content

sim_lib_web_bridge/
sync.rs

1//! Multi-surface synchronized edit sessions: broadcast, handoff, and replay.
2//!
3//! One resource can be open in MANY surfaces at once. The [`SurfaceHub`] owns
4//! the single CANONICAL value for every resource and is the one coordination
5//! point: a committed edit on any surface is applied to the canonical store and
6//! then BROADCAST -- as a Scene plus a Scene diff -- to every surface/pane
7//! viewing that resource, including the surface that issued the edit. This
8//! avoids trying to make N independent transports share events; the hub is the
9//! shared state.
10//!
11//! Edits flow through the universal default lens: an Intent is proposed and
12//! committed through `edit:default`, yielding the universal `{op: set-value,
13//! value: <proposed>}` operation, which is applied to the canonical store and
14//! recorded in an append-only [`EditRow`] ledger carrying the issuing
15//! operator and logical tick. Two surfaces editing the same resource therefore
16//! apply in submit order (last write wins), and the ledger is replayable:
17//! [`replay`] re-applies it to a seed state and reproduces the same canonical
18//! state, proving the edit log is auditable.
19//!
20//! Handoff ([`SurfaceHub::handoff`]) opens an already-held resource on a second
21//! surface so subsequent edits broadcast to both.
22
23use std::collections::BTreeMap;
24use std::sync::Arc;
25
26use sim_kernel::{Cx, DefaultFactory, EagerPolicy, Error, Expr, Result, Symbol};
27use sim_lib_view::codec::reduce_for_caps;
28use sim_lib_view::{
29    LensRegistry, SurfaceCaps, UNIVERSAL_EDITOR_ID, UNIVERSAL_VIEW_ID, register_universal_default,
30};
31
32/// The role a surface holds inside a synchronized hub.
33#[derive(Clone, Copy, Debug, PartialEq, Eq)]
34pub enum SurfaceRole {
35    /// Primary focus surface for a shared session.
36    Main,
37    /// Secondary peer surface for the same canonical session.
38    Peer,
39}
40
41/// One re-rendered Scene pushed to a surface/pane after a canonical edit.
42///
43/// `diff` is the Scene patch from the pane's cached Scene to `scene`; applying
44/// it with [`sim_lib_scene::apply`] reconstructs `scene`.
45#[derive(Clone, Debug)]
46pub struct Broadcast {
47    /// The surface that receives this update.
48    pub surface: Symbol,
49    /// The pane on that surface.
50    pub pane: Symbol,
51    /// The full new Scene for the pane.
52    pub scene: Expr,
53    /// The Scene patch from the pane's prior Scene to `scene`.
54    pub diff: Expr,
55}
56
57/// One append-only ledger row: a committed edit, attributed and replayable.
58///
59/// Rows are appended in submit order. Replaying them in order through
60/// [`replay`] reproduces the final canonical state.
61#[derive(Clone, Debug)]
62pub struct EditRow {
63    /// The resource that was edited.
64    pub resource: Symbol,
65    /// The issuing operator (from the Intent origin, e.g. `human`/`agent`).
66    pub operator: Symbol,
67    /// The issuing logical tick (from the Intent origin `at-tick`).
68    pub tick: u64,
69    /// The committed `{op: set-value, value: <proposed>}` operation.
70    pub operation: Expr,
71}
72
73/// Public snapshot of one live `(surface, pane)` binding.
74#[derive(Clone, Debug, PartialEq, Eq)]
75pub struct SurfaceBinding {
76    /// The surface that owns the binding.
77    pub surface: Symbol,
78    /// The pane on that surface.
79    pub pane: Symbol,
80    /// The canonical resource shown by the binding.
81    pub resource: Symbol,
82}
83
84/// A live binding of a `(surface, pane)` to a resource, with the last Scene
85/// shown there so the next broadcast can be diffed against it.
86struct Binding {
87    surface: Symbol,
88    pane: Symbol,
89    resource: Symbol,
90    last_scene: Expr,
91}
92
93impl Binding {
94    fn snapshot(&self) -> SurfaceBinding {
95        SurfaceBinding {
96            surface: self.surface.clone(),
97            pane: self.pane.clone(),
98            resource: self.resource.clone(),
99        }
100    }
101}
102
103/// The canonical multi-surface coordination point.
104///
105/// Holds the single canonical value per resource, the universal [`LensRegistry`]
106/// that renders and edits, an owned [`Cx`], the registered surfaces and their
107/// [`SurfaceCaps`], the live `(surface, pane)` bindings, and the append-only
108/// [`EditRow`] ledger.
109pub struct SurfaceHub {
110    canonical: BTreeMap<Symbol, Expr>,
111    registry: LensRegistry,
112    cx: Cx,
113    surfaces: BTreeMap<Symbol, SurfaceCaps>,
114    roles: BTreeMap<Symbol, SurfaceRole>,
115    bindings: Vec<Binding>,
116    ledger: Vec<EditRow>,
117}
118
119impl Default for SurfaceHub {
120    fn default() -> Self {
121        Self::new()
122    }
123}
124
125impl SurfaceHub {
126    /// A new hub with the universal default lens registered (writable) and no
127    /// resources, surfaces, bindings, or ledger rows.
128    pub fn new() -> Self {
129        let mut registry = LensRegistry::new();
130        register_universal_default(&mut registry, false);
131        Self {
132            canonical: BTreeMap::new(),
133            registry,
134            cx: Cx::new(Arc::new(EagerPolicy), Arc::new(DefaultFactory)),
135            surfaces: BTreeMap::new(),
136            roles: BTreeMap::new(),
137            bindings: Vec::new(),
138            ledger: Vec::new(),
139        }
140    }
141
142    /// Set (or replace) the canonical value of `resource`.
143    pub fn seed(&mut self, resource: Symbol, value: Expr) {
144        self.canonical.insert(resource, value);
145    }
146
147    /// Register a surface (identified by `surface`) with its capabilities.
148    /// Re-registering replaces the stored caps.
149    pub fn register_surface(&mut self, surface: Symbol, caps: SurfaceCaps) {
150        self.register_surface_with_role(surface, caps, SurfaceRole::Main);
151    }
152
153    /// Register a surface with an explicit role inside the hub.
154    pub fn register_surface_with_role(
155        &mut self,
156        surface: Symbol,
157        caps: SurfaceCaps,
158        role: SurfaceRole,
159    ) {
160        self.roles.insert(surface.clone(), role);
161        self.surfaces.insert(surface, caps);
162    }
163
164    /// Returns the role recorded for `surface`.
165    pub fn surface_role(&self, surface: &Symbol) -> Option<SurfaceRole> {
166        self.roles.get(surface).copied()
167    }
168
169    /// Bind `(surface, pane)` to `resource`, render the canonical value through
170    /// the universal view (projected to the surface caps via
171    /// [`reduce_for_caps`]), cache that Scene for the pane, and return it.
172    ///
173    /// An existing binding for the same `(surface, pane)` is replaced. Fails if
174    /// the surface is not registered or the resource has no canonical value.
175    pub fn open(&mut self, surface: &Symbol, pane: Symbol, resource: Symbol) -> Result<Expr> {
176        let caps = self.caps_of(surface)?;
177        let value = self.value_of(&resource)?;
178        let scene = render_for_surface(&mut self.cx, &self.registry, &caps, &value)?;
179        self.bindings
180            .retain(|binding| !(binding.surface == *surface && binding.pane == pane));
181        self.bindings.push(Binding {
182            surface: surface.clone(),
183            pane,
184            resource,
185            last_scene: scene.clone(),
186        });
187        Ok(scene)
188    }
189
190    /// Submit an Intent against the resource shown in `(surface, pane)`.
191    ///
192    /// The Intent is proposed and committed through the universal editor against
193    /// the CURRENT canonical value; the resulting `set-value` operation is
194    /// applied to the canonical store and appended to the ledger (attributed to
195    /// the Intent origin's operator and tick). Then, for EVERY `(surface, pane)`
196    /// viewing that resource -- including other surfaces -- the new value is
197    /// re-rendered, diffed against the pane's cached Scene, the cache updated,
198    /// and a [`Broadcast`] emitted. Returns all broadcasts.
199    ///
200    /// Fails closed (returns an error, never panics) if the pane is not open,
201    /// the resource is missing, the Intent is invalid, or the draft is not
202    /// committable.
203    pub fn submit(
204        &mut self,
205        surface: &Symbol,
206        pane: &Symbol,
207        intent: &Expr,
208    ) -> Result<Vec<Broadcast>> {
209        let caps = self.caps_of(surface)?;
210        require_surface_input(&caps, intent)?;
211        let resource = self
212            .bindings
213            .iter()
214            .find(|binding| binding.surface == *surface && binding.pane == *pane)
215            .map(|binding| binding.resource.clone())
216            .ok_or_else(|| Error::HostError(format!("({surface}, {pane}) is not open")))?;
217        let value = self.value_of(&resource)?;
218
219        let editor = Symbol::new(UNIVERSAL_EDITOR_ID);
220        let draft = self
221            .registry
222            .propose(&mut self.cx, &editor, &value, intent)?;
223        let operation = self.registry.commit(&mut self.cx, &editor, &draft)?;
224        let new_value = apply_set_value(&operation.form)?;
225        self.commit_change(surface, pane, intent, new_value, operation.form)
226    }
227
228    /// Commit an already decoded value update from `surface`/`pane`.
229    ///
230    /// Device coordinators use this when a physical Intent has already been
231    /// reduced to a canonical value update. The hub still validates the Intent,
232    /// enforces the source surface input capability, records exactly one
233    /// append-only ledger row, and broadcasts through the same atomic path as
234    /// [`Self::submit`].
235    pub fn commit_value_from(
236        &mut self,
237        surface: &Symbol,
238        pane: &Symbol,
239        intent: &Expr,
240        new_value: Expr,
241    ) -> Result<Vec<Broadcast>> {
242        sim_lib_intent::validate_intent(intent)
243            .map_err(|error| Error::HostError(format!("invalid intent: {error}")))?;
244        let caps = self.caps_of(surface)?;
245        require_surface_input(&caps, intent)?;
246        let resource = self
247            .bindings
248            .iter()
249            .find(|binding| binding.surface == *surface && binding.pane == *pane)
250            .map(|binding| binding.resource.clone())
251            .ok_or_else(|| Error::HostError(format!("({surface}, {pane}) is not open")))?;
252        self.commit_resource_change(resource, intent, new_value)
253    }
254
255    /// Detach a surface from this hub without changing canonical resources or
256    /// ledger history.
257    pub fn detach_surface(&mut self, surface: &Symbol) -> Vec<SurfaceBinding> {
258        self.surfaces.remove(surface);
259        self.roles.remove(surface);
260        let mut removed = Vec::new();
261        self.bindings.retain(|binding| {
262            if binding.surface == *surface {
263                removed.push(binding.snapshot());
264                false
265            } else {
266                true
267            }
268        });
269        removed
270    }
271
272    /// Returns live bindings currently showing `resource`.
273    pub fn bindings_for_resource(&self, resource: &Symbol) -> Vec<SurfaceBinding> {
274        self.bindings
275            .iter()
276            .filter(|binding| binding.resource == *resource)
277            .map(Binding::snapshot)
278            .collect()
279    }
280
281    fn commit_change(
282        &mut self,
283        surface: &Symbol,
284        pane: &Symbol,
285        intent: &Expr,
286        new_value: Expr,
287        operation: Expr,
288    ) -> Result<Vec<Broadcast>> {
289        let resource = self
290            .bindings
291            .iter()
292            .find(|binding| binding.surface == *surface && binding.pane == *pane)
293            .map(|binding| binding.resource.clone())
294            .ok_or_else(|| Error::HostError(format!("({surface}, {pane}) is not open")))?;
295        self.commit_resource_change_with_operation(resource, intent, new_value, operation)
296    }
297
298    fn commit_resource_change(
299        &mut self,
300        resource: Symbol,
301        intent: &Expr,
302        new_value: Expr,
303    ) -> Result<Vec<Broadcast>> {
304        let operation = set_value_operation(new_value.clone());
305        self.commit_resource_change_with_operation(resource, intent, new_value, operation)
306    }
307
308    fn commit_resource_change_with_operation(
309        &mut self,
310        resource: Symbol,
311        intent: &Expr,
312        new_value: Expr,
313        operation: Expr,
314    ) -> Result<Vec<Broadcast>> {
315        // Render EVERY per-surface broadcast into a staging buffer FIRST. A
316        // render (or a surface that lost its capabilities) can fail mid-iteration;
317        // if it does we must mutate nothing -- otherwise canonical/ledger move
318        // forward while some caches advance and no broadcast is delivered, an
319        // unrecoverable replay divergence. We commit only after all succeed.
320        let mut staged: Vec<(usize, Broadcast)> = Vec::new();
321        {
322            let Self {
323                registry,
324                cx,
325                surfaces,
326                bindings,
327                ..
328            } = self;
329            for (index, binding) in bindings.iter().enumerate() {
330                if binding.resource != resource {
331                    continue;
332                }
333                let caps = surfaces.get(&binding.surface).ok_or_else(|| {
334                    Error::HostError(format!(
335                        "surface '{}' lost its capabilities",
336                        binding.surface
337                    ))
338                })?;
339                let scene = render_for_surface(cx, registry, caps, &new_value)?;
340                let diff = sim_lib_scene::diff(&binding.last_scene, &scene);
341                staged.push((
342                    index,
343                    Broadcast {
344                        surface: binding.surface.clone(),
345                        pane: binding.pane.clone(),
346                        scene,
347                        diff,
348                    },
349                ));
350            }
351        }
352
353        // All broadcasts rendered: commit atomically -- canonical, then ledger,
354        // then swap in each surface's advanced last_scene cache.
355        self.canonical.insert(resource.clone(), new_value);
356        let (operator, tick) = origin_of(intent);
357        self.ledger.push(EditRow {
358            resource,
359            operator,
360            tick,
361            operation,
362        });
363        let mut broadcasts = Vec::with_capacity(staged.len());
364        for (index, broadcast) in staged {
365            self.bindings[index].last_scene = broadcast.scene.clone();
366            broadcasts.push(broadcast);
367        }
368        Ok(broadcasts)
369    }
370
371    /// Hand `resource` off from `from` to `to`: open it on `to` in a new `pane`
372    /// and return its Scene. The `from` surface keeps its binding, so the
373    /// resource is now open on both and subsequent edits broadcast to both.
374    ///
375    /// Fails if `from` does not currently hold `resource`.
376    pub fn handoff(
377        &mut self,
378        from: &Symbol,
379        to: &Symbol,
380        resource: Symbol,
381        pane: Symbol,
382    ) -> Result<Expr> {
383        let held = self
384            .bindings
385            .iter()
386            .any(|binding| binding.surface == *from && binding.resource == resource);
387        if !held {
388            return Err(Error::HostError(format!(
389                "surface '{from}' does not hold resource '{resource}' to hand off"
390            )));
391        }
392        self.open(to, pane, resource)
393    }
394
395    /// The append-only edit ledger, in submit order.
396    pub fn ledger(&self) -> &[EditRow] {
397        &self.ledger
398    }
399
400    /// The current canonical value of `resource`, if any.
401    pub fn canonical(&self, resource: &Symbol) -> Option<&Expr> {
402        self.canonical.get(resource)
403    }
404
405    fn caps_of(&self, surface: &Symbol) -> Result<SurfaceCaps> {
406        self.surfaces
407            .get(surface)
408            .cloned()
409            .ok_or_else(|| Error::HostError(format!("surface '{surface}' is not registered")))
410    }
411
412    fn value_of(&self, resource: &Symbol) -> Result<Expr> {
413        self.canonical.get(resource).cloned().ok_or_else(|| {
414            Error::HostError(format!("resource '{resource}' has no canonical value"))
415        })
416    }
417}
418
419/// Re-apply a ledger to a seed canonical state, yielding the final state.
420///
421/// Rows are applied in order; for a resource, the last `set-value` wins. This is
422/// the replay surface that proves the ledger is auditable: feeding the rows
423/// produced by a run of edits back over the original seed reproduces the final
424/// canonical state of the hub.
425///
426/// Every committed row carries the universal `{op: set-value, ...}` operation,
427/// so replay fails closed if a row's operation is not a `set-value`: a foreign
428/// or corrupted ledger row is surfaced as an error rather than silently dropped,
429/// which would otherwise reproduce a state that never existed.
430pub fn replay(rows: &[EditRow], seed: BTreeMap<Symbol, Expr>) -> Result<BTreeMap<Symbol, Expr>> {
431    let mut state = seed;
432    for row in rows {
433        let value = apply_set_value(&row.operation)?;
434        state.insert(row.resource.clone(), value);
435    }
436    Ok(state)
437}
438
439/// Render `value` through the universal view, projected to `caps`.
440fn render_for_surface(
441    cx: &mut Cx,
442    registry: &LensRegistry,
443    caps: &SurfaceCaps,
444    value: &Expr,
445) -> Result<Expr> {
446    let scene = registry.render(cx, &Symbol::new(UNIVERSAL_VIEW_ID), value)?;
447    Ok(reduce_for_caps(&scene, caps))
448}
449
450fn require_surface_input(caps: &SurfaceCaps, intent: &Expr) -> Result<()> {
451    let required = input_capabilities_for_intent(intent)?;
452    if required
453        .iter()
454        .any(|capability| caps.input_flag(capability))
455    {
456        return Ok(());
457    }
458    Err(Error::HostError(format!(
459        "surface '{}' does not accept any required input for this Intent: {}",
460        caps.client_id,
461        required.join(", ")
462    )))
463}
464
465fn input_capabilities_for_intent(intent: &Expr) -> Result<&'static [&'static str]> {
466    let kind = match sim_value::access::field(intent, "kind") {
467        Some(Expr::Symbol(kind)) if kind.namespace.as_deref() == Some("intent") => {
468            kind.name.as_ref()
469        }
470        Some(Expr::Symbol(_)) => {
471            return Err(Error::HostError(
472                "Intent kind must be in the intent namespace".to_owned(),
473            ));
474        }
475        _ => return Err(Error::HostError("submit input is not an Intent".to_owned())),
476    };
477    match kind {
478        "tap" | "dismiss" | "commit" | "cancel" | "approve" | "reject" | "pause-agent"
479        | "rerun-validation" | "replay-cassette" => Ok(&["tap", "pointer", "touch", "keyboard"]),
480        "select" | "move" | "wire" | "unwire" | "create" | "delete" | "scrub"
481        | "piano-roll-edit" | "player-rack-edit" | "arranger-edit" => Ok(&["pointer", "touch"]),
482        "invoke" => Ok(&[
483            "pointer",
484            "touch",
485            "tap",
486            "button",
487            "gaze",
488            "head",
489            "hand",
490            "controller",
491            "voice",
492        ]),
493        "edit" | "edit-field" | "set-param" | "set-lens" | "set-mode" | "open" | "ask"
494        | "split-mission" | "open-source" => Ok(&["keyboard", "touch", "voice"]),
495        "performance-event" => Ok(&["keyboard", "touch", "camera"]),
496        other => Err(Error::HostError(format!(
497            "no surface input capability mapping for intent/{other}"
498        ))),
499    }
500}
501
502fn set_value_operation(value: Expr) -> Expr {
503    Expr::Map(vec![
504        (
505            Expr::Symbol(Symbol::new("op")),
506            Expr::Symbol(Symbol::new("set-value")),
507        ),
508        (Expr::Symbol(Symbol::new("value")), value),
509    ])
510}
511
512/// Interpret the universal `{op: set-value, value: <v>}` operation, returning
513/// `<v>`. Any other shape fails closed.
514fn apply_set_value(operation: &Expr) -> Result<Expr> {
515    let Expr::Map(entries) = operation else {
516        return Err(Error::HostError("operation is not a map".to_owned()));
517    };
518    let is_set_value = matches!(
519        sim_value::access::entry_field(entries, "op"),
520        Some(Expr::Symbol(symbol)) if &*symbol.name == "set-value"
521    );
522    if !is_set_value {
523        return Err(Error::HostError(
524            "operation is not a set-value op".to_owned(),
525        ));
526    }
527    sim_value::access::entry_field(entries, "value")
528        .cloned()
529        .ok_or_else(|| Error::HostError("set-value operation is missing a 'value'".to_owned()))
530}
531
532/// Read the operator symbol and logical tick from an Intent origin, defaulting
533/// to `unknown`/`0` if absent (the Intent is validated before this is called).
534fn origin_of(intent: &Expr) -> (Symbol, u64) {
535    let origin = sim_value::access::field(intent, "origin");
536    let operator = origin
537        .and_then(|origin| sim_value::access::field_sym(origin, "operator"))
538        .unwrap_or_else(|| Symbol::new("unknown"));
539    let tick = origin
540        .and_then(|origin| sim_value::access::field_any(origin, "at-tick"))
541        .and_then(|tick| match tick {
542            Expr::Number(number) => number.canonical.parse::<u64>().ok(),
543            _ => None,
544        })
545        .unwrap_or(0);
546    (operator, tick)
547}
548
549#[cfg(test)]
550mod tests;