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