Skip to main content

sim_lib_web_bridge/
fabric.rs

1//! The fabric transport: a session as a `realize` target on an `EvalFabric`.
2//!
3//! [`FabricTransport`] implements the web-bridge [`Transport`] trait by
4//! delegating every commit to a kernel [`EvalFabric`](sim_kernel::EvalFabric).
5//! Reads and change events stay local to an in-memory store (exactly like the
6//! [`FixtureTransport`](crate::fixture::FixtureTransport)), but a realized
7//! operation is turned into an [`EvalRequest`], answered by the fabric's
8//! `realize`, and the reply value becomes the resource's new value. This proves
9//! "a surface session is a realize target on the fabric" using the existing
10//! Session/pump/diff machinery unchanged: the fabric interprets the checked
11//! operation and returns the new resource value.
12//!
13//! This transport does not provide streams; the stream methods fail closed.
14
15use std::collections::BTreeMap;
16use std::sync::Arc;
17
18use sim_kernel::{
19    Consistency, Cx, DefaultFactory, EagerPolicy, Error, EvalFabricRef, EvalMode, EvalRequest,
20    Expr, Result, Symbol,
21};
22use sim_lib_stream_core::{PushResult, StreamEnvelope, StreamItem, StreamStats};
23
24use crate::transport::{
25    ChangeEvent, SessionStatus, StreamInspectorRecord, Transport, TransportKind,
26};
27
28/// A transport that commits operations by delegating to an
29/// [`EvalFabric`](sim_kernel::EvalFabric).
30///
31/// Reads return locally stored values and change events accumulate locally;
32/// [`Transport::realize`] is forwarded to the fabric, whose reply value becomes
33/// the new value of the realized resource.
34pub struct FabricTransport {
35    fabric: EvalFabricRef,
36    cx: Cx,
37    store: BTreeMap<Symbol, Expr>,
38    events: Vec<ChangeEvent>,
39    status: SessionStatus,
40}
41
42impl FabricTransport {
43    /// A connected, empty transport over `fabric` (with its own kernel context).
44    pub fn new(fabric: EvalFabricRef) -> Self {
45        Self {
46            fabric,
47            cx: Cx::new(Arc::new(EagerPolicy), Arc::new(DefaultFactory)),
48            store: BTreeMap::new(),
49            events: Vec::new(),
50            status: SessionStatus::Connected,
51        }
52    }
53
54    /// Seed a resource value (builder form).
55    pub fn with(mut self, resource: Symbol, value: Expr) -> Self {
56        self.store.insert(resource, value);
57        self
58    }
59
60    /// Seed or replace a resource value.
61    pub fn set(&mut self, resource: Symbol, value: Expr) {
62        self.store.insert(resource, value);
63    }
64
65    fn no_streams(&self) -> Error {
66        Error::HostError("fabric transport does not provide streams".to_owned())
67    }
68}
69
70impl Transport for FabricTransport {
71    fn kind(&self) -> TransportKind {
72        TransportKind::Fabric
73    }
74
75    fn status(&self) -> SessionStatus {
76        self.status
77    }
78
79    fn read(&self, resource: &Symbol) -> Result<Expr> {
80        self.store
81            .get(resource)
82            .cloned()
83            .ok_or_else(|| Error::UnknownSymbol {
84                symbol: resource.clone(),
85            })
86    }
87
88    fn realize(&mut self, resource: &Symbol, operation: &Expr) -> Result<Expr> {
89        let request = operation_to_request(operation);
90        let reply = self.fabric.realize(&mut self.cx, request)?;
91        let new_value = reply.value.object().as_expr(&mut self.cx)?;
92        self.store.insert(resource.clone(), new_value.clone());
93        self.events.push(ChangeEvent {
94            resource: resource.clone(),
95        });
96        Ok(new_value)
97    }
98
99    fn drain_events(&mut self) -> Vec<ChangeEvent> {
100        std::mem::take(&mut self.events)
101    }
102
103    fn stream_subscribe(&mut self, _stream_id: &Symbol) -> Result<StreamInspectorRecord> {
104        Err(self.no_streams())
105    }
106
107    fn stream_read(&mut self, _stream_id: &Symbol, _limit: usize) -> Result<Vec<StreamItem>> {
108        Err(self.no_streams())
109    }
110
111    fn stream_push(
112        &mut self,
113        _stream_id: &Symbol,
114        _envelope: StreamEnvelope,
115    ) -> Result<PushResult> {
116        Err(self.no_streams())
117    }
118
119    fn stream_cancel(&mut self, _stream_id: &Symbol) -> Result<()> {
120        Err(self.no_streams())
121    }
122
123    fn stream_stats(&self, _stream_id: &Symbol) -> Result<StreamStats> {
124        Err(self.no_streams())
125    }
126
127    fn stream_inspector(&self, _stream_id: &Symbol) -> Result<StreamInspectorRecord> {
128        Err(self.no_streams())
129    }
130}
131
132/// Build a default [`EvalRequest`] carrying `operation` as its expression.
133///
134/// The request uses [`Consistency::LocalFirst`] and [`EvalMode::Eval`] with no
135/// shape, capabilities, deadline, streaming, or trace. The fabric interprets the
136/// operation and returns the resource's new value as the reply value.
137pub fn operation_to_request(operation: &Expr) -> EvalRequest {
138    EvalRequest {
139        expr: operation.clone(),
140        result_shape: None,
141        required_capabilities: Vec::new(),
142        deadline: None,
143        consistency: Consistency::LocalFirst,
144        mode: EvalMode::Eval,
145        answer_limit: None,
146        stream_buffer: None,
147        stream: false,
148        trace: false,
149    }
150}
151
152#[cfg(test)]
153mod tests {
154    use std::sync::Arc;
155
156    use sim_kernel::{Cx, Error, EvalFabric, EvalReply, EvalRequest, Expr, NumberLiteral, Result};
157    use sim_lib_intent::{Origin, intent};
158    use sim_lib_view::{
159        LensRegistry, UNIVERSAL_EDITOR_ID, UNIVERSAL_VIEW_ID, register_universal_default,
160    };
161
162    use super::FabricTransport;
163    use crate::session::Session;
164    use crate::transport::{Transport, TransportKind};
165
166    /// A fabric that interprets the universal editor's `set-value` operation by
167    /// returning its `value` field as the reply value.
168    struct SetValueFabric;
169
170    impl EvalFabric for SetValueFabric {
171        fn realize(&self, cx: &mut Cx, request: EvalRequest) -> Result<EvalReply> {
172            let Expr::Map(entries) = &request.expr else {
173                return Err(Error::HostError("operation is not a map".to_owned()));
174            };
175            let value_expr = entries
176                .iter()
177                .find_map(|(key, value)| {
178                    matches!(key, Expr::Symbol(symbol) if &*symbol.name == "value").then_some(value)
179                })
180                .ok_or_else(|| {
181                    Error::HostError("set-value operation is missing a 'value'".to_owned())
182                })?;
183            Ok(EvalReply {
184                value: cx.factory().expr(value_expr.clone())?,
185                diagnostics: Vec::new(),
186                trace: None,
187            })
188        }
189    }
190
191    use sim_kernel::testing::eager_cx as cx;
192
193    fn registry() -> LensRegistry {
194        let mut registry = LensRegistry::new();
195        register_universal_default(&mut registry, false);
196        registry
197    }
198
199    use sim_value::build::keyword as sym;
200
201    fn number(value: &str) -> Expr {
202        Expr::Number(NumberLiteral {
203            domain: sym("i64"),
204            canonical: value.to_owned(),
205        })
206    }
207
208    fn doc() -> Expr {
209        Expr::Map(vec![
210            (Expr::Symbol(sym("a")), number("1")),
211            (Expr::Symbol(sym("b")), number("2")),
212        ])
213    }
214
215    fn edit_a_to_9() -> Expr {
216        intent(
217            "edit-field",
218            Origin::human(1),
219            vec![
220                ("target", doc()),
221                (
222                    "path",
223                    Expr::List(vec![Expr::Vector(vec![
224                        Expr::Symbol(sym("k")),
225                        Expr::Symbol(sym("a")),
226                    ])]),
227                ),
228                ("value", number("9")),
229            ],
230        )
231    }
232
233    fn set_value_op(value: Expr) -> Expr {
234        Expr::Map(vec![
235            (Expr::Symbol(sym("op")), Expr::Symbol(sym("set-value"))),
236            (Expr::Symbol(sym("value")), value),
237        ])
238    }
239
240    fn field<'a>(value: &'a Expr, name: &str) -> Option<&'a Expr> {
241        let Expr::Map(entries) = value else {
242            return None;
243        };
244        entries
245            .iter()
246            .find(|(key, _)| matches!(key, Expr::Symbol(symbol) if &*symbol.name == name))
247            .map(|(_, value)| value)
248    }
249
250    #[test]
251    fn session_commits_an_edit_through_the_fabric_and_the_scene_diff_reconstructs() {
252        let mut cx = cx();
253        let registry = registry();
254        let transport = FabricTransport::new(Arc::new(SetValueFabric)).with(sym("doc"), doc());
255        let mut session = Session::new(transport);
256
257        let initial = session
258            .open(
259                &mut cx,
260                &registry,
261                sym("pane-1"),
262                sym("doc"),
263                sym(UNIVERSAL_VIEW_ID),
264                sym(UNIVERSAL_EDITOR_ID),
265            )
266            .unwrap();
267        sim_lib_scene::validate_scene(&initial).expect("initial scene is valid");
268
269        // The Intent decodes and commits through the fabric's realize.
270        session
271            .submit_intent(&mut cx, &registry, &sym("pane-1"), &edit_a_to_9())
272            .unwrap();
273
274        // The fabric-stored value changed.
275        let value = session.transport_mut().read(&sym("doc")).unwrap();
276        assert_eq!(field(&value, "a"), Some(&number("9")));
277
278        // Pumping yields a diff that reconstructs the new Scene from the old one.
279        let updates = session.pump(&mut cx, &registry).unwrap();
280        assert_eq!(updates.len(), 1, "exactly the subscribed pane updates");
281        let update = &updates[0];
282        assert_eq!(update.pane, sym("pane-1"));
283        assert_ne!(update.scene, initial, "the Scene changed");
284        let rebuilt = sim_lib_scene::apply(&initial, &update.diff).unwrap();
285        assert_eq!(rebuilt, update.scene, "the diff reconstructs the new Scene");
286    }
287
288    #[test]
289    fn direct_realize_returns_the_new_value_and_records_one_event() {
290        let mut transport = FabricTransport::new(Arc::new(SetValueFabric));
291        assert_eq!(transport.kind(), TransportKind::Fabric);
292
293        let new_value = transport
294            .realize(&sym("x"), &set_value_op(number("42")))
295            .unwrap();
296        assert_eq!(new_value, number("42"));
297        assert_eq!(transport.read(&sym("x")).unwrap(), number("42"));
298
299        let events = transport.drain_events();
300        assert_eq!(events.len(), 1);
301        assert_eq!(events[0].resource, sym("x"));
302        assert!(transport.drain_events().is_empty());
303    }
304}