1use std::path::PathBuf;
2use std::sync::{Arc, Mutex};
3
4use hypen_engine::{Engine, Patch};
5use serde_json::Value;
6
7use crate::discovery::ComponentRegistry;
8
9use super::types::RemoteMessage;
10
11pub struct SessionConfig {
13 pub module_name: String,
15 pub ui_source: String,
17 pub components: ComponentRegistry,
19 pub initial_state: Value,
21 pub action_names: Vec<String>,
23}
24
25type ActionHandlerFn = Box<dyn Fn(&str, Option<&Value>, &Value) -> Value + Send + Sync>;
27
28pub struct RemoteSession {
49 inner: Mutex<SessionInner>,
50 module_name: String,
51 session_id: String,
52}
53
54struct SessionInner {
55 engine: Engine,
56 state: Value,
57 ui_source: String,
58 revision: u64,
59 state_subscribed: bool,
60 action_handler: Option<ActionHandlerFn>,
61 rendered: bool,
62}
63
64impl RemoteSession {
65 pub fn new(config: SessionConfig) -> Self {
70 let session_id = format!(
71 "session_{}",
72 std::time::SystemTime::now()
73 .duration_since(std::time::UNIX_EPOCH)
74 .unwrap_or_default()
75 .as_nanos()
76 );
77
78 let mut engine = Engine::new();
79
80 let registry = Arc::new(config.components);
82 let reg: Arc<ComponentRegistry> = Arc::clone(®istry);
83 engine.set_component_resolver(move |name, _ctx_path| {
84 reg.get(name).map(|entry| hypen_engine::ir::ResolvedComponent {
85 source: entry.source.clone(),
86 path: entry
87 .path
88 .as_ref()
89 .map(|p: &PathBuf| p.to_string_lossy().to_string())
90 .unwrap_or_default(),
91 passthrough: false,
92 lazy: false,
93 })
94 });
95
96 let module_meta =
98 hypen_engine::Module::new(&config.module_name).with_actions(config.action_names);
99 let engine_module =
100 hypen_engine::ModuleInstance::new(module_meta, config.initial_state.clone());
101 engine.set_module(engine_module);
102
103 Self {
104 inner: Mutex::new(SessionInner {
105 engine,
106 state: config.initial_state,
107 ui_source: config.ui_source,
108 revision: 0,
109 state_subscribed: false,
110 action_handler: None,
111 rendered: false,
112 }),
113 module_name: config.module_name,
114 session_id,
115 }
116 }
117
118 pub fn set_action_handler<F>(&self, handler: F)
123 where
124 F: Fn(&str, Option<&Value>, &Value) -> Value + Send + Sync + 'static,
125 {
126 self.inner.lock().unwrap().action_handler = Some(Box::new(handler));
127 }
128
129 pub fn session_id(&self) -> &str {
131 &self.session_id
132 }
133
134 pub fn handle_hello(&self, _client_session_id: Option<&str>) -> Vec<String> {
140 let mut inner = self.inner.lock().unwrap();
141 let mut messages = Vec::with_capacity(2);
142
143 let ack = RemoteMessage::SessionAck {
145 session_id: self.session_id.clone(),
146 is_new: true,
147 is_restored: false,
148 };
149 if let Ok(json) = ack.to_json() {
150 messages.push(json);
151 }
152
153 let patches = if !inner.rendered {
155 inner.rendered = true;
156 let ui = inner.ui_source.clone();
157 render_and_capture(&mut inner.engine, &ui)
158 } else {
159 vec![]
160 };
161
162 let initial = RemoteMessage::InitialTree {
164 module: self.module_name.clone(),
165 state: inner.state.clone(),
166 patches,
167 revision: 0,
168 };
169 if let Ok(json) = initial.to_json() {
170 messages.push(json);
171 }
172
173 messages
174 }
175
176 pub fn handle_message(&self, json: &str) -> Vec<String> {
178 let msg = match RemoteMessage::from_json(json) {
179 Ok(m) => m,
180 Err(_) => return vec![],
181 };
182
183 match msg {
184 RemoteMessage::Hello { session_id, .. } => {
185 self.handle_hello(session_id.as_deref())
186 }
187
188 RemoteMessage::DispatchAction {
189 action, payload, ..
190 } => self.handle_action(&action, payload.as_ref()),
191
192 RemoteMessage::SubscribeState { .. } => {
193 self.inner.lock().unwrap().state_subscribed = true;
194 vec![]
195 }
196
197 _ => vec![],
198 }
199 }
200
201 fn handle_action(&self, action: &str, payload: Option<&Value>) -> Vec<String> {
203 let mut inner = self.inner.lock().unwrap();
204 let mut messages = Vec::new();
205
206 let new_state = if let Some(ref handler) = inner.action_handler {
208 handler(action, payload, &inner.state)
209 } else {
210 return messages;
211 };
212
213 inner.state = new_state.clone();
214
215 let patches = update_state_and_capture(&mut inner.engine, new_state.clone());
217 inner.revision += 1;
218
219 if !patches.is_empty() {
221 let patch_msg = RemoteMessage::Patch {
222 module: self.module_name.clone(),
223 patches,
224 revision: inner.revision,
225 };
226 if let Ok(json) = patch_msg.to_json() {
227 messages.push(json);
228 }
229 }
230
231 if inner.state_subscribed {
233 let state_msg = RemoteMessage::StateUpdate {
234 module: self.module_name.clone(),
235 state: new_state,
236 revision: inner.revision,
237 };
238 if let Ok(json) = state_msg.to_json() {
239 messages.push(json);
240 }
241 }
242
243 messages
244 }
245
246 pub fn get_state(&self) -> Value {
248 self.inner.lock().unwrap().state.clone()
249 }
250
251 pub fn revision(&self) -> u64 {
253 self.inner.lock().unwrap().revision
254 }
255}
256
257fn render_and_capture(engine: &mut Engine, ui_source: &str) -> Vec<Patch> {
259 let patches = Arc::new(Mutex::new(Vec::<Patch>::new()));
260 let capture = Arc::clone(&patches);
261 engine.set_render_callback(move |p| {
262 capture.lock().unwrap().extend_from_slice(p);
263 });
264
265 if let Ok(doc) = hypen_parser::parse_document(ui_source) {
266 if let Some(component) = doc.components.first() {
267 let ir_node = hypen_engine::ast_to_ir_node(component);
268 engine.render_ir_node(&ir_node);
269 }
270 }
271
272 engine.set_render_callback(|_| {});
274
275 let result = patches.lock().unwrap().drain(..).collect();
276 result
277}
278
279fn update_state_and_capture(engine: &mut Engine, new_state: Value) -> Vec<Patch> {
281 let patches = Arc::new(Mutex::new(Vec::<Patch>::new()));
282 let capture = Arc::clone(&patches);
283 engine.set_render_callback(move |p| {
284 capture.lock().unwrap().extend_from_slice(p);
285 });
286
287 engine.update_state(new_state);
288
289 engine.set_render_callback(|_| {});
290
291 let result = patches.lock().unwrap().drain(..).collect();
292 result
293}
294
295#[cfg(test)]
296mod tests {
297 use super::*;
298
299 fn test_config() -> SessionConfig {
300 let mut components = ComponentRegistry::new();
301 components.register(
302 "Greeting",
303 r#"Text("Hello ${state.name}")"#,
304 None,
305 );
306
307 SessionConfig {
308 module_name: "App".to_string(),
309 ui_source: r#"Column { Text("Count: ${state.count}") }"#.to_string(),
310 components,
311 initial_state: serde_json::json!({
312 "count": 0,
313 "name": "World"
314 }),
315 action_names: vec!["increment".to_string()],
316 }
317 }
318
319 #[test]
320 fn test_session_hello_returns_ack_and_tree() {
321 let session = RemoteSession::new(test_config());
322 let msgs = session.handle_hello(None);
323
324 assert_eq!(msgs.len(), 2);
325 assert!(msgs[0].contains("sessionAck"));
326 assert!(msgs[1].contains("initialTree"));
327 assert!(msgs[1].contains("\"count\":0"));
328 }
329
330 #[test]
331 fn test_session_dispatch_action() {
332 let session = RemoteSession::new(test_config());
333 session.set_action_handler(|action, _payload, state| {
334 let mut s = state.clone();
335 if action == "increment" {
336 if let Some(count) = s.get_mut("count").and_then(|v| v.as_i64()) {
337 s["count"] = serde_json::json!(count + 1);
338 }
339 }
340 s
341 });
342
343 let _ = session.handle_hello(None);
345
346 let action_json = r#"{"type":"dispatchAction","module":"App","action":"increment"}"#;
348 let responses = session.handle_message(action_json);
349
350 assert!(session.get_state()["count"] == 1);
352 assert_eq!(session.revision(), 1);
353 }
354
355 #[test]
356 fn test_session_state_subscription() {
357 let session = RemoteSession::new(test_config());
358 session.set_action_handler(|_action, _payload, state| {
359 let mut s = state.clone();
360 s["count"] = serde_json::json!(42);
361 s
362 });
363
364 let _ = session.handle_hello(None);
365
366 let sub_json = r#"{"type":"subscribeState","module":"App"}"#;
368 session.handle_message(sub_json);
369
370 let action_json = r#"{"type":"dispatchAction","module":"App","action":"set"}"#;
372 let responses = session.handle_message(action_json);
373
374 let has_state_update = responses.iter().any(|r| r.contains("stateUpdate"));
376 assert!(has_state_update);
377 }
378
379 #[test]
380 fn test_session_hello_via_message() {
381 let session = RemoteSession::new(test_config());
382 let hello_json = r#"{"type":"hello"}"#;
383 let msgs = session.handle_message(hello_json);
384
385 assert_eq!(msgs.len(), 2);
386 assert!(msgs[0].contains("sessionAck"));
387 assert!(msgs[1].contains("initialTree"));
388 }
389}