1use sim_kernel::{Cx, Error, Expr, Result, Symbol};
11use sim_lib_view::{
12 LensRegistry, Mode, SurfaceCaps, UNIVERSAL_SURFACE_CODEC_ID, surface, universal_scene,
13};
14
15use crate::transport::{SessionStatus, Transport};
16
17const MAX_PANES: usize = 64;
21
22const MAX_PANE_NAME: usize = 128;
24
25const MAX_RESOURCE_NAME: usize = 512;
28
29fn validate_pane_name(pane: &Symbol) -> Result<()> {
32 let name = pane.as_qualified_str();
33 if name.is_empty() || name.len() > MAX_PANE_NAME {
34 return Err(Error::HostError(format!(
35 "pane name must be 1..={MAX_PANE_NAME} bytes, got {}",
36 name.len()
37 )));
38 }
39 if !name.bytes().all(|byte| byte.is_ascii_graphic()) {
40 return Err(Error::HostError(
41 "pane name must be printable ASCII without spaces".to_owned(),
42 ));
43 }
44 Ok(())
45}
46
47fn validate_resource_name(resource: &Symbol) -> Result<()> {
51 let name = resource.as_qualified_str();
52 if name.is_empty() || name.len() > MAX_RESOURCE_NAME {
53 return Err(Error::HostError(format!(
54 "resource name must be 1..={MAX_RESOURCE_NAME} bytes, got {}",
55 name.len()
56 )));
57 }
58 Ok(())
59}
60
61struct Subscription {
63 pane: Symbol,
64 resource: Symbol,
65 codec: Symbol,
66 caps: SurfaceCaps,
67 rendered_value: Expr,
68 last_scene: Expr,
69}
70
71#[derive(Clone, Debug)]
73pub struct SceneUpdate {
74 pub pane: Symbol,
76 pub scene: Expr,
78 pub diff: Expr,
80}
81
82pub struct Session<T: Transport> {
86 transport: T,
87 subscriptions: Vec<Subscription>,
88 mode: Mode,
89}
90
91impl<T: Transport> Session<T> {
92 pub fn new(transport: T) -> Self {
94 Self {
95 transport,
96 subscriptions: Vec::new(),
97 mode: Mode::Builder,
98 }
99 }
100
101 pub fn status(&self) -> SessionStatus {
103 self.transport.status()
104 }
105
106 pub fn mode(&self) -> Mode {
108 self.mode
109 }
110
111 pub fn set_mode(&mut self, intent: &Expr) -> Result<()> {
114 match sim_value::access::field(intent, "kind") {
115 Some(Expr::Symbol(kind)) if &*kind.name == "set-mode" => {}
116 _ => {
117 return Err(Error::HostError(
118 "set_mode expects an intent/set-mode".to_owned(),
119 ));
120 }
121 }
122 let mode = match sim_value::access::field(intent, "mode") {
123 Some(Expr::Symbol(symbol)) => Mode::from_name(&symbol.name),
124 _ => None,
125 };
126 self.mode = mode.ok_or_else(|| {
127 Error::HostError(
128 "intent/set-mode 'mode' must be household, builder, or systems".to_owned(),
129 )
130 })?;
131 Ok(())
132 }
133
134 pub fn render_universal(&self, value: &Expr) -> Expr {
137 universal_scene(value, self.mode)
138 }
139
140 pub fn transport_mut(&mut self) -> &mut T {
143 &mut self.transport
144 }
145
146 pub fn open_codec(
149 &mut self,
150 cx: &mut Cx,
151 registry: &LensRegistry,
152 pane: Symbol,
153 resource: Symbol,
154 codec: Symbol,
155 caps: SurfaceCaps,
156 ) -> Result<Expr> {
157 validate_pane_name(&pane)?;
158 validate_resource_name(&resource)?;
159 let replacing = self.subscriptions.iter().any(|sub| sub.pane == pane);
160 if !replacing && self.subscriptions.len() >= MAX_PANES {
161 return Err(Error::HostError(format!(
162 "session is at its pane limit ({MAX_PANES}); close a pane before opening another"
163 )));
164 }
165 let surface_codec = registry
166 .surface_codec(&codec)
167 .ok_or_else(|| Error::UnknownSymbol {
168 symbol: codec.clone(),
169 })?;
170 let value = self.transport.read(cx, &resource)?;
171 let scene = surface_codec.encode(cx, &value, &caps)?;
172 self.subscriptions.retain(|sub| sub.pane != pane);
173 self.subscriptions.push(Subscription {
174 pane,
175 resource,
176 codec,
177 caps,
178 rendered_value: value,
179 last_scene: scene.clone(),
180 });
181 Ok(scene)
182 }
183
184 pub fn open(
188 &mut self,
189 cx: &mut Cx,
190 registry: &LensRegistry,
191 pane: Symbol,
192 resource: Symbol,
193 _view_lens: Symbol,
194 _editor_lens: Symbol,
195 ) -> Result<Expr> {
196 self.open_codec(
197 cx,
198 registry,
199 pane,
200 resource,
201 Symbol::new(UNIVERSAL_SURFACE_CODEC_ID),
202 surface::preset("desktop").expect("desktop is a known surface preset"),
203 )
204 }
205
206 pub fn submit_intent(
209 &mut self,
210 cx: &mut Cx,
211 registry: &LensRegistry,
212 pane: &Symbol,
213 intent: &Expr,
214 ) -> Result<()> {
215 self.submit_intent_with_policy(cx, registry, pane, intent, false)
216 }
217
218 pub fn submit_intent_at_rendered_revision(
222 &mut self,
223 cx: &mut Cx,
224 registry: &LensRegistry,
225 pane: &Symbol,
226 intent: &Expr,
227 ) -> Result<()> {
228 self.submit_intent_with_policy(cx, registry, pane, intent, true)
229 }
230
231 fn submit_intent_with_policy(
232 &mut self,
233 cx: &mut Cx,
234 registry: &LensRegistry,
235 pane: &Symbol,
236 intent: &Expr,
237 require_rendered_revision: bool,
238 ) -> Result<()> {
239 let (resource, codec, rendered_value) = {
240 let sub = self
241 .subscriptions
242 .iter()
243 .find(|sub| &sub.pane == pane)
244 .ok_or_else(|| Error::HostError(format!("pane '{pane}' is not open")))?;
245 (
246 sub.resource.clone(),
247 sub.codec.clone(),
248 sub.rendered_value.clone(),
249 )
250 };
251 let value = if require_rendered_revision {
252 rendered_value.clone()
253 } else {
254 self.transport.read(cx, &resource)?
255 };
256 let surface_codec = registry
257 .surface_codec(&codec)
258 .ok_or(Error::UnknownSymbol { symbol: codec })?;
259 let draft = surface_codec.decode(cx, &value, intent)?;
260 let operation = surface_codec.commit(cx, &draft)?;
261 self.transport.commit_operation(
262 cx,
263 &resource,
264 &operation,
265 require_rendered_revision.then_some(&rendered_value),
266 )?;
267 Ok(())
268 }
269
270 pub fn pump(&mut self, cx: &mut Cx, registry: &LensRegistry) -> Result<Vec<SceneUpdate>> {
273 let events = self.transport.drain_events(cx)?;
274 let mut updates = Vec::new();
275 let Self {
276 transport,
277 subscriptions,
278 ..
279 } = self;
280 for event in events {
281 for sub in subscriptions
282 .iter_mut()
283 .filter(|sub| sub.resource == event.resource)
284 {
285 let value = transport.read(cx, &sub.resource)?;
286 let surface_codec =
287 registry
288 .surface_codec(&sub.codec)
289 .ok_or_else(|| Error::UnknownSymbol {
290 symbol: sub.codec.clone(),
291 })?;
292 let scene = surface_codec.encode(cx, &value, &sub.caps)?;
293 let diff = sim_lib_scene::diff(&sub.last_scene, &scene);
294 sub.rendered_value = value;
295 sub.last_scene = scene.clone();
296 updates.push(SceneUpdate {
297 pane: sub.pane.clone(),
298 scene,
299 diff,
300 });
301 }
302 }
303 Ok(updates)
304 }
305}
306
307#[cfg(test)]
308mod tests {
309
310 use sim_kernel::{Cx, Expr, Symbol};
311 use sim_lib_view::{
312 LensRegistry, UNIVERSAL_EDITOR_ID, UNIVERSAL_VIEW_ID, register_universal_default,
313 };
314
315 use super::{MAX_PANES, Session};
316 use crate::fixture::FixtureTransport;
317
318 use sim_value::build::keyword as sym;
319
320 use sim_kernel::testing::eager_cx as cx;
321
322 fn registry() -> LensRegistry {
323 let mut registry = LensRegistry::new();
324 register_universal_default(&mut registry, false);
325 registry
326 }
327
328 fn open(
329 session: &mut Session<FixtureTransport>,
330 cx: &mut Cx,
331 registry: &LensRegistry,
332 pane: &str,
333 ) -> sim_kernel::Result<Expr> {
334 session.open(
335 cx,
336 registry,
337 sym(pane),
338 sym("doc"),
339 Symbol::new(UNIVERSAL_VIEW_ID),
340 Symbol::new(UNIVERSAL_EDITOR_ID),
341 )
342 }
343
344 #[test]
345 fn open_bounds_the_number_of_panes() {
346 let mut cx = cx();
347 let registry = registry();
348 let mut session = Session::new(FixtureTransport::new().with(sym("doc"), Expr::Nil));
349
350 for index in 0..MAX_PANES {
351 open(&mut session, &mut cx, ®istry, &format!("pane-{index}")).unwrap();
352 }
353 assert!(
355 open(&mut session, &mut cx, ®istry, "pane-overflow").is_err(),
356 "opening past the pane cap must be refused"
357 );
358 open(&mut session, &mut cx, ®istry, "pane-0").unwrap();
360 }
361
362 #[test]
363 fn open_rejects_untrusted_pane_names() {
364 let mut cx = cx();
365 let registry = registry();
366 let mut session = Session::new(FixtureTransport::new().with(sym("doc"), Expr::Nil));
367
368 assert!(
369 open(&mut session, &mut cx, ®istry, "").is_err(),
370 "empty pane"
371 );
372 let huge = "p".repeat(super::MAX_PANE_NAME + 1);
373 assert!(
374 open(&mut session, &mut cx, ®istry, &huge).is_err(),
375 "over-long pane name"
376 );
377 assert!(
378 open(&mut session, &mut cx, ®istry, "has space").is_err(),
379 "pane name with a space"
380 );
381 }
382}