1use sim_kernel::{Cx, Error, Expr, Result, Symbol};
11use sim_lib_view::{LensRegistry, Mode, universal_scene};
12
13use crate::transport::{SessionStatus, Transport};
14
15const MAX_PANES: usize = 64;
19
20const MAX_PANE_NAME: usize = 128;
22
23const MAX_RESOURCE_NAME: usize = 512;
26
27fn validate_pane_name(pane: &Symbol) -> Result<()> {
30 let name = pane.as_qualified_str();
31 if name.is_empty() || name.len() > MAX_PANE_NAME {
32 return Err(Error::HostError(format!(
33 "pane name must be 1..={MAX_PANE_NAME} bytes, got {}",
34 name.len()
35 )));
36 }
37 if !name.bytes().all(|byte| byte.is_ascii_graphic()) {
38 return Err(Error::HostError(
39 "pane name must be printable ASCII without spaces".to_owned(),
40 ));
41 }
42 Ok(())
43}
44
45fn validate_resource_name(resource: &Symbol) -> Result<()> {
49 let name = resource.as_qualified_str();
50 if name.is_empty() || name.len() > MAX_RESOURCE_NAME {
51 return Err(Error::HostError(format!(
52 "resource name must be 1..={MAX_RESOURCE_NAME} bytes, got {}",
53 name.len()
54 )));
55 }
56 Ok(())
57}
58
59struct Subscription {
61 pane: Symbol,
62 resource: Symbol,
63 view_lens: Symbol,
64 editor_lens: Symbol,
65 last_scene: Expr,
66}
67
68#[derive(Clone, Debug)]
70pub struct SceneUpdate {
71 pub pane: Symbol,
73 pub scene: Expr,
75 pub diff: Expr,
77}
78
79pub struct Session<T: Transport> {
83 transport: T,
84 subscriptions: Vec<Subscription>,
85 mode: Mode,
86}
87
88impl<T: Transport> Session<T> {
89 pub fn new(transport: T) -> Self {
91 Self {
92 transport,
93 subscriptions: Vec::new(),
94 mode: Mode::Builder,
95 }
96 }
97
98 pub fn status(&self) -> SessionStatus {
100 self.transport.status()
101 }
102
103 pub fn mode(&self) -> Mode {
105 self.mode
106 }
107
108 pub fn set_mode(&mut self, intent: &Expr) -> Result<()> {
111 match sim_value::access::field(intent, "kind") {
112 Some(Expr::Symbol(kind)) if &*kind.name == "set-mode" => {}
113 _ => {
114 return Err(Error::HostError(
115 "set_mode expects an intent/set-mode".to_owned(),
116 ));
117 }
118 }
119 let mode = match sim_value::access::field(intent, "mode") {
120 Some(Expr::Symbol(symbol)) => Mode::from_name(&symbol.name),
121 _ => None,
122 };
123 self.mode = mode.ok_or_else(|| {
124 Error::HostError(
125 "intent/set-mode 'mode' must be household, builder, or systems".to_owned(),
126 )
127 })?;
128 Ok(())
129 }
130
131 pub fn render_universal(&self, value: &Expr) -> Expr {
134 universal_scene(value, self.mode)
135 }
136
137 pub fn transport_mut(&mut self) -> &mut T {
140 &mut self.transport
141 }
142
143 pub fn open(
146 &mut self,
147 cx: &mut Cx,
148 registry: &LensRegistry,
149 pane: Symbol,
150 resource: Symbol,
151 view_lens: Symbol,
152 editor_lens: Symbol,
153 ) -> Result<Expr> {
154 validate_pane_name(&pane)?;
155 validate_resource_name(&resource)?;
156 let replacing = self.subscriptions.iter().any(|sub| sub.pane == pane);
159 if !replacing && self.subscriptions.len() >= MAX_PANES {
160 return Err(Error::HostError(format!(
161 "session is at its pane limit ({MAX_PANES}); close a pane before opening another"
162 )));
163 }
164 let value = self.transport.read(&resource)?;
165 let scene = registry.render(cx, &view_lens, &value)?;
166 self.subscriptions.retain(|sub| sub.pane != pane);
167 self.subscriptions.push(Subscription {
168 pane,
169 resource,
170 view_lens,
171 editor_lens,
172 last_scene: scene.clone(),
173 });
174 Ok(scene)
175 }
176
177 pub fn submit_intent(
180 &mut self,
181 cx: &mut Cx,
182 registry: &LensRegistry,
183 pane: &Symbol,
184 intent: &Expr,
185 ) -> Result<()> {
186 let (resource, editor) = {
187 let sub = self
188 .subscriptions
189 .iter()
190 .find(|sub| &sub.pane == pane)
191 .ok_or_else(|| Error::HostError(format!("pane '{pane}' is not open")))?;
192 (sub.resource.clone(), sub.editor_lens.clone())
193 };
194 let value = self.transport.read(&resource)?;
195 let draft = registry.propose(cx, &editor, &value, intent)?;
196 let operation = registry.commit(cx, &editor, &draft)?;
197 self.transport.realize(&resource, &operation.form)?;
198 Ok(())
199 }
200
201 pub fn pump(&mut self, cx: &mut Cx, registry: &LensRegistry) -> Result<Vec<SceneUpdate>> {
204 let events = self.transport.drain_events();
205 let mut updates = Vec::new();
206 let Self {
207 transport,
208 subscriptions,
209 ..
210 } = self;
211 for event in events {
212 for sub in subscriptions
213 .iter_mut()
214 .filter(|sub| sub.resource == event.resource)
215 {
216 let value = transport.read(&sub.resource)?;
217 let scene = registry.render(cx, &sub.view_lens, &value)?;
218 let diff = sim_lib_scene::diff(&sub.last_scene, &scene);
219 sub.last_scene = scene.clone();
220 updates.push(SceneUpdate {
221 pane: sub.pane.clone(),
222 scene,
223 diff,
224 });
225 }
226 }
227 Ok(updates)
228 }
229}
230
231#[cfg(test)]
232mod tests {
233
234 use sim_kernel::{Cx, Expr, Symbol};
235 use sim_lib_view::{
236 LensRegistry, UNIVERSAL_EDITOR_ID, UNIVERSAL_VIEW_ID, register_universal_default,
237 };
238
239 use super::{MAX_PANES, Session};
240 use crate::fixture::FixtureTransport;
241
242 use sim_value::build::keyword as sym;
243
244 use sim_kernel::testing::eager_cx as cx;
245
246 fn registry() -> LensRegistry {
247 let mut registry = LensRegistry::new();
248 register_universal_default(&mut registry, false);
249 registry
250 }
251
252 fn open(
253 session: &mut Session<FixtureTransport>,
254 cx: &mut Cx,
255 registry: &LensRegistry,
256 pane: &str,
257 ) -> sim_kernel::Result<Expr> {
258 session.open(
259 cx,
260 registry,
261 sym(pane),
262 sym("doc"),
263 Symbol::new(UNIVERSAL_VIEW_ID),
264 Symbol::new(UNIVERSAL_EDITOR_ID),
265 )
266 }
267
268 #[test]
269 fn open_bounds_the_number_of_panes() {
270 let mut cx = cx();
271 let registry = registry();
272 let mut session = Session::new(FixtureTransport::new().with(sym("doc"), Expr::Nil));
273
274 for index in 0..MAX_PANES {
275 open(&mut session, &mut cx, ®istry, &format!("pane-{index}")).unwrap();
276 }
277 assert!(
279 open(&mut session, &mut cx, ®istry, "pane-overflow").is_err(),
280 "opening past the pane cap must be refused"
281 );
282 open(&mut session, &mut cx, ®istry, "pane-0").unwrap();
284 }
285
286 #[test]
287 fn open_rejects_untrusted_pane_names() {
288 let mut cx = cx();
289 let registry = registry();
290 let mut session = Session::new(FixtureTransport::new().with(sym("doc"), Expr::Nil));
291
292 assert!(
293 open(&mut session, &mut cx, ®istry, "").is_err(),
294 "empty pane"
295 );
296 let huge = "p".repeat(super::MAX_PANE_NAME + 1);
297 assert!(
298 open(&mut session, &mut cx, ®istry, &huge).is_err(),
299 "over-long pane name"
300 );
301 assert!(
302 open(&mut session, &mut cx, ®istry, "has space").is_err(),
303 "pane name with a space"
304 );
305 }
306}