Skip to main content

sim_lib_expr_tree_server/
web.rs

1//! Product composition for the generic SIM browser shell.
2
3use std::sync::Arc;
4
5use sim_kernel::{Cx, Error, Expr, Result, Symbol};
6use sim_lib_server::ServerAddress;
7use sim_lib_view::{LensRegistry, SurfaceCaps, surface};
8use sim_lib_view_expr_tree::{
9    expression_tree_surface_codec_symbol, register_expression_tree_surface_codec,
10};
11use sim_lib_web_bridge::{RemoteTransport, SceneUpdate, Session};
12use sim_web_shell::{LiveSurface, LiveSurfaceFactory};
13
14type ContextFactory = dyn Fn() -> Result<Cx> + Send + Sync;
15
16/// One browser-owned expression-tree surface over a real server transport.
17///
18/// The surface composes the product codec with the generic session bus. It owns
19/// no HTTP routes, JavaScript, Scene interpretation, or tree semantics.
20pub struct ExpressionTreeWebSurface {
21    session: Session<RemoteTransport>,
22    registry: LensRegistry,
23    cx: Cx,
24    caps: SurfaceCaps,
25    browser_resource: String,
26    authoritative_resource: Symbol,
27}
28
29impl ExpressionTreeWebSurface {
30    fn connect(
31        endpoint: String,
32        address: ServerAddress,
33        offered_codecs: Vec<Symbol>,
34        caps: SurfaceCaps,
35        browser_resource: String,
36        authoritative_resource: Symbol,
37        mut cx: Cx,
38    ) -> Result<Self> {
39        let mut transport = RemoteTransport::local_server_address(endpoint, address)
40            .with_offered_codecs(offered_codecs);
41        transport.connect(&mut cx)?;
42        let mut registry = LensRegistry::new();
43        register_expression_tree_surface_codec(&mut registry);
44        Ok(Self {
45            session: Session::new(transport),
46            registry,
47            cx,
48            caps,
49            browser_resource,
50            authoritative_resource,
51        })
52    }
53
54    fn checked_resource(&self, requested: &str) -> Result<Symbol> {
55        if requested == self.browser_resource {
56            Ok(self.authoritative_resource.clone())
57        } else {
58            Err(Error::HostError(format!(
59                "browser resource {requested:?} is outside this expression-tree surface"
60            )))
61        }
62    }
63}
64
65impl LiveSurface for ExpressionTreeWebSurface {
66    fn open(&mut self, resource: &str, pane: &str) -> Result<Expr> {
67        let resource = self.checked_resource(resource)?;
68        self.session.open_codec(
69            &mut self.cx,
70            &self.registry,
71            Symbol::new(pane),
72            resource,
73            expression_tree_surface_codec_symbol(),
74            self.caps.clone(),
75        )
76    }
77
78    fn submit(&mut self, pane: &str, intent: &Expr) -> Result<Vec<SceneUpdate>> {
79        self.session.submit_intent_at_rendered_revision(
80            &mut self.cx,
81            &self.registry,
82            &Symbol::new(pane),
83            intent,
84        )?;
85        self.session.pump(&mut self.cx, &self.registry)
86    }
87}
88
89/// Builds isolated browser surfaces for one authoritative expression-tree
90/// resource.
91///
92/// Each call to [`LiveSurfaceFactory::create`] obtains a fresh caller context
93/// from `context_factory`, negotiates a fresh [`RemoteTransport`], and owns a
94/// separate reversible session. The opaque browser alias is mapped to exactly
95/// one server resource, so callers cannot select another tree by editing a URL.
96pub struct ExpressionTreeWebSurfaceFactory {
97    endpoint: String,
98    address: ServerAddress,
99    offered_codecs: Vec<Symbol>,
100    caps: SurfaceCaps,
101    browser_resource: String,
102    authoritative_resource: Symbol,
103    context_factory: Arc<ContextFactory>,
104}
105
106impl ExpressionTreeWebSurfaceFactory {
107    /// Creates a factory using the `webui` surface capability preset.
108    pub fn new(
109        endpoint: impl Into<String>,
110        address: ServerAddress,
111        browser_resource: impl Into<String>,
112        authoritative_resource: Symbol,
113        context_factory: impl Fn() -> Result<Cx> + Send + Sync + 'static,
114    ) -> Self {
115        Self {
116            endpoint: endpoint.into(),
117            address,
118            offered_codecs: vec![Symbol::qualified("codec", "lisp")],
119            caps: surface::preset("webui").expect("webui is a known surface preset"),
120            browser_resource: browser_resource.into(),
121            authoritative_resource,
122            context_factory: Arc::new(context_factory),
123        }
124    }
125
126    /// Selects the server frame codecs offered by every new remote transport.
127    pub fn with_offered_codecs(mut self, offered_codecs: Vec<Symbol>) -> Self {
128        self.offered_codecs = offered_codecs;
129        self
130    }
131
132    /// Selects open surface capabilities for every new browser projection.
133    pub fn with_surface_caps(mut self, caps: SurfaceCaps) -> Self {
134        self.caps = caps;
135        self
136    }
137}
138
139impl LiveSurfaceFactory for ExpressionTreeWebSurfaceFactory {
140    fn create(&self) -> Result<Box<dyn LiveSurface>> {
141        let cx = (self.context_factory)()?;
142        ExpressionTreeWebSurface::connect(
143            self.endpoint.clone(),
144            self.address.clone(),
145            self.offered_codecs.clone(),
146            self.caps.clone(),
147            self.browser_resource.clone(),
148            self.authoritative_resource.clone(),
149            cx,
150        )
151        .map(|surface| Box::new(surface) as Box<dyn LiveSurface>)
152    }
153}