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};
23use sim_lib_view::Operation;
24
25use crate::transport::{
26    ChangeEvent, SessionStatus, StreamInspectorRecord, Transport, TransportKind,
27};
28
29/// A transport that commits operations by delegating to an
30/// [`EvalFabric`](sim_kernel::EvalFabric).
31///
32/// Reads return locally stored values and change events accumulate locally;
33/// [`Transport::realize`] is forwarded to the fabric, whose reply value becomes
34/// the new value of the realized resource.
35pub struct FabricTransport {
36    fabric: EvalFabricRef,
37    cx: Cx,
38    store: BTreeMap<Symbol, Expr>,
39    events: Vec<ChangeEvent>,
40    status: SessionStatus,
41}
42
43impl FabricTransport {
44    /// A connected, empty transport over `fabric` (with its own kernel context).
45    pub fn new(fabric: EvalFabricRef) -> Self {
46        Self {
47            fabric,
48            cx: Cx::new(Arc::new(EagerPolicy), Arc::new(DefaultFactory)),
49            store: BTreeMap::new(),
50            events: Vec::new(),
51            status: SessionStatus::Connected,
52        }
53    }
54
55    /// Seed a resource value (builder form).
56    pub fn with(mut self, resource: Symbol, value: Expr) -> Self {
57        self.store.insert(resource, value);
58        self
59    }
60
61    /// Seed or replace a resource value.
62    pub fn set(&mut self, resource: Symbol, value: Expr) {
63        self.store.insert(resource, value);
64    }
65
66    fn no_streams(&self) -> Error {
67        Error::HostError("fabric transport does not provide streams".to_owned())
68    }
69}
70
71impl Transport for FabricTransport {
72    fn kind(&self) -> TransportKind {
73        TransportKind::Fabric
74    }
75
76    fn status(&self) -> SessionStatus {
77        self.status
78    }
79
80    fn read(&self, resource: &Symbol) -> Result<Expr> {
81        self.store
82            .get(resource)
83            .cloned()
84            .ok_or_else(|| Error::UnknownSymbol {
85                symbol: resource.clone(),
86            })
87    }
88
89    fn realize_operation(&mut self, resource: &Symbol, operation: &Operation) -> Result<Expr> {
90        let request = operation_to_request(operation);
91        let reply = self.fabric.realize(&mut self.cx, request)?;
92        let new_value = reply.value.object().as_expr(&mut self.cx)?;
93        validate_reply_shape(&mut self.cx, operation, &new_value)?;
94        self.store.insert(resource.clone(), new_value.clone());
95        self.events.push(ChangeEvent {
96            resource: resource.clone(),
97        });
98        Ok(new_value)
99    }
100
101    fn drain_events(&mut self) -> Vec<ChangeEvent> {
102        std::mem::take(&mut self.events)
103    }
104
105    fn stream_subscribe(&mut self, _stream_id: &Symbol) -> Result<StreamInspectorRecord> {
106        Err(self.no_streams())
107    }
108
109    fn stream_read(&mut self, _stream_id: &Symbol, _limit: usize) -> Result<Vec<StreamItem>> {
110        Err(self.no_streams())
111    }
112
113    fn stream_push(
114        &mut self,
115        _stream_id: &Symbol,
116        _envelope: StreamEnvelope,
117    ) -> Result<PushResult> {
118        Err(self.no_streams())
119    }
120
121    fn stream_cancel(&mut self, _stream_id: &Symbol) -> Result<()> {
122        Err(self.no_streams())
123    }
124
125    fn stream_stats(&self, _stream_id: &Symbol) -> Result<StreamStats> {
126        Err(self.no_streams())
127    }
128
129    fn stream_inspector(&self, _stream_id: &Symbol) -> Result<StreamInspectorRecord> {
130        Err(self.no_streams())
131    }
132}
133
134/// Build a default [`EvalRequest`] carrying `operation` and its authority
135/// metadata.
136///
137/// The request uses [`Consistency::LocalFirst`] and [`EvalMode::Eval`], carries
138/// the operation's expected result shape and required capabilities, and leaves
139/// deadline, streaming, and trace unset. The fabric interprets the operation and
140/// returns the resource's new value as the reply value.
141pub fn operation_to_request(operation: &Operation) -> EvalRequest {
142    EvalRequest {
143        expr: operation.form.clone(),
144        result_shape: operation.result_shape.clone(),
145        required_capabilities: operation.required_capabilities.clone(),
146        deadline: None,
147        consistency: Consistency::LocalFirst,
148        mode: EvalMode::Eval,
149        answer_limit: None,
150        stream_buffer: None,
151        stream: false,
152        trace: false,
153    }
154}
155
156fn validate_reply_shape(cx: &mut Cx, operation: &Operation, value: &Expr) -> Result<()> {
157    let Some(shape_value) = &operation.result_shape else {
158        return Ok(());
159    };
160    let Some(shape) = shape_value.object().as_shape() else {
161        return Err(Error::HostError(
162            "operation result_shape is not a Shape".to_owned(),
163        ));
164    };
165    let matched = shape.check_expr(cx, value)?;
166    if matched.accepted {
167        Ok(())
168    } else {
169        Err(Error::HostError(
170            "fabric reply failed operation result_shape".to_owned(),
171        ))
172    }
173}
174
175#[cfg(test)]
176mod tests {
177    use std::sync::Arc;
178
179    use sim_kernel::{
180        CapabilityName, Cx, Error, EvalFabric, EvalReply, EvalRequest, Expr, ExprKind,
181        NumberLiteral, Result, Symbol,
182    };
183    use sim_lib_intent::{Origin, intent};
184    use sim_lib_view::{
185        LensRegistry, Operation, UNIVERSAL_EDITOR_ID, UNIVERSAL_VIEW_ID, register_universal_default,
186    };
187    use sim_shape::{ExprKindShape, shape_value};
188
189    use super::{FabricTransport, operation_to_request};
190    use crate::session::Session;
191    use crate::transport::{Transport, TransportKind};
192
193    /// A fabric that interprets the universal editor's `set-value` operation by
194    /// returning its `value` field as the reply value.
195    struct SetValueFabric;
196
197    impl EvalFabric for SetValueFabric {
198        fn realize(&self, cx: &mut Cx, request: EvalRequest) -> Result<EvalReply> {
199            let Expr::Map(entries) = &request.expr else {
200                return Err(Error::HostError("operation is not a map".to_owned()));
201            };
202            let value_expr = sim_value::access::entry_field(entries, "value").ok_or_else(|| {
203                Error::HostError("set-value operation is missing a 'value'".to_owned())
204            })?;
205            Ok(EvalReply {
206                value: cx.factory().expr(value_expr.clone())?,
207                diagnostics: Vec::new(),
208                trace: None,
209            })
210        }
211    }
212
213    struct StringReplyFabric;
214
215    impl EvalFabric for StringReplyFabric {
216        fn realize(&self, cx: &mut Cx, _request: EvalRequest) -> Result<EvalReply> {
217            Ok(EvalReply {
218                value: cx.factory().expr(Expr::String("wrong shape".to_owned()))?,
219                diagnostics: Vec::new(),
220                trace: None,
221            })
222        }
223    }
224
225    use sim_kernel::testing::eager_cx as cx;
226
227    fn registry() -> LensRegistry {
228        let mut registry = LensRegistry::new();
229        register_universal_default(&mut registry, false);
230        registry
231    }
232
233    use sim_value::build::keyword as sym;
234
235    fn number(value: &str) -> Expr {
236        Expr::Number(NumberLiteral {
237            domain: sym("i64"),
238            canonical: value.to_owned(),
239        })
240    }
241
242    fn doc() -> Expr {
243        Expr::Map(vec![
244            (Expr::Symbol(sym("a")), number("1")),
245            (Expr::Symbol(sym("b")), number("2")),
246        ])
247    }
248
249    fn edit_a_to_9() -> Expr {
250        intent(
251            "edit-field",
252            Origin::human(1),
253            vec![
254                ("target", doc()),
255                (
256                    "path",
257                    Expr::List(vec![Expr::Vector(vec![
258                        Expr::Symbol(sym("k")),
259                        Expr::Symbol(sym("a")),
260                    ])]),
261                ),
262                ("value", number("9")),
263            ],
264        )
265    }
266
267    fn set_value_op(value: Expr) -> Expr {
268        Expr::Map(vec![
269            (Expr::Symbol(sym("op")), Expr::Symbol(sym("set-value"))),
270            (Expr::Symbol(sym("value")), value),
271        ])
272    }
273
274    fn number_shape() -> sim_kernel::ShapeRef {
275        shape_value(
276            Symbol::qualified("core", "Number"),
277            Arc::new(ExprKindShape::new(ExprKind::Number)),
278        )
279    }
280
281    #[test]
282    fn session_commits_an_edit_through_the_fabric_and_the_scene_diff_reconstructs() {
283        let mut cx = cx();
284        let registry = registry();
285        let transport = FabricTransport::new(Arc::new(SetValueFabric)).with(sym("doc"), doc());
286        let mut session = Session::new(transport);
287
288        let initial = session
289            .open(
290                &mut cx,
291                &registry,
292                sym("pane-1"),
293                sym("doc"),
294                sym(UNIVERSAL_VIEW_ID),
295                sym(UNIVERSAL_EDITOR_ID),
296            )
297            .unwrap();
298        sim_lib_scene::validate_scene(&initial).expect("initial scene is valid");
299
300        // The Intent decodes and commits through the fabric's realize.
301        session
302            .submit_intent(&mut cx, &registry, &sym("pane-1"), &edit_a_to_9())
303            .unwrap();
304
305        // The fabric-stored value changed.
306        let value = session.transport_mut().read(&sym("doc")).unwrap();
307        assert_eq!(sim_value::access::field(&value, "a"), Some(&number("9")));
308
309        // Pumping yields a diff that reconstructs the new Scene from the old one.
310        let updates = session.pump(&mut cx, &registry).unwrap();
311        assert_eq!(updates.len(), 1, "exactly the subscribed pane updates");
312        let update = &updates[0];
313        assert_eq!(update.pane, sym("pane-1"));
314        assert_ne!(update.scene, initial, "the Scene changed");
315        let rebuilt = sim_lib_scene::apply(&initial, &update.diff).unwrap();
316        assert_eq!(rebuilt, update.scene, "the diff reconstructs the new Scene");
317    }
318
319    #[test]
320    fn direct_realize_returns_the_new_value_and_records_one_event() {
321        let mut transport = FabricTransport::new(Arc::new(SetValueFabric));
322        assert_eq!(transport.kind(), TransportKind::Fabric);
323
324        let new_value = transport
325            .realize(&sym("x"), &set_value_op(number("42")))
326            .unwrap();
327        assert_eq!(new_value, number("42"));
328        assert_eq!(transport.read(&sym("x")).unwrap(), number("42"));
329
330        let events = transport.drain_events();
331        assert_eq!(events.len(), 1);
332        assert_eq!(events[0].resource, sym("x"));
333        assert!(transport.drain_events().is_empty());
334    }
335
336    #[test]
337    fn operation_to_request_preserves_shape_and_capability_requirements() {
338        let operation = Operation::new(set_value_op(number("42")))
339            .with_result_shape(number_shape())
340            .requiring(CapabilityName::new("web.write"));
341
342        let request = operation_to_request(&operation);
343
344        assert_eq!(request.expr, operation.form);
345        assert!(request.result_shape.is_some());
346        assert_eq!(
347            request
348                .required_capabilities
349                .iter()
350                .map(|capability| capability.as_str())
351                .collect::<Vec<_>>(),
352            vec!["web.write"]
353        );
354    }
355
356    #[test]
357    fn fabric_reply_must_match_the_operation_result_shape_before_storage_changes() {
358        let mut transport =
359            FabricTransport::new(Arc::new(StringReplyFabric)).with(sym("doc"), number("1"));
360        let operation = Operation::new(set_value_op(number("2"))).with_result_shape(number_shape());
361
362        let err = transport
363            .realize_operation(&sym("doc"), &operation)
364            .unwrap_err();
365
366        assert!(
367            err.to_string()
368                .contains("fabric reply failed operation result_shape"),
369            "unexpected error: {err}"
370        );
371        assert_eq!(transport.read(&sym("doc")).unwrap(), number("1"));
372        assert!(transport.drain_events().is_empty());
373    }
374}