Skip to main content

hypen_server/remote/
session.rs

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
11/// Configuration for creating a [`RemoteSession`].
12pub struct SessionConfig {
13    /// Module name (e.g., "App").
14    pub module_name: String,
15    /// Hypen DSL source for the root UI.
16    pub ui_source: String,
17    /// Component registry with discovered components.
18    pub components: ComponentRegistry,
19    /// Initial state as JSON.
20    pub initial_state: Value,
21    /// Action names registered on this module.
22    pub action_names: Vec<String>,
23}
24
25/// Type-erased action handler: `(action_name, payload, current_state) -> new_state`.
26type ActionHandlerFn = Box<dyn Fn(&str, Option<&Value>, &Value) -> Value + Send + Sync>;
27
28/// Per-client remote session managing an engine, state, and the wire protocol.
29///
30/// Framework-agnostic: feed it JSON strings, get JSON strings back.
31/// Wire it into any WebSocket library (Axum, Actix, Tungstenite, etc.).
32///
33/// # Usage
34///
35/// ```rust,ignore
36/// let config = SessionConfig { /* ... */ };
37/// let session = RemoteSession::new(config);
38/// session.set_action_handler(|action, payload, state| { /* ... */ });
39///
40/// // On client connect:
41/// let msgs = session.handle_hello(None);
42/// for m in msgs { ws.send(m); }
43///
44/// // On each incoming message:
45/// let responses = session.handle_message(&incoming_json);
46/// for r in responses { ws.send(r); }
47/// ```
48pub 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    /// Create a new remote session.
66    ///
67    /// Sets up the engine with the component resolver and module, but does NOT
68    /// render yet. The initial render happens in [`handle_hello`].
69    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        // Wire up component resolver from the registry
81        let registry = Arc::new(config.components);
82        let reg: Arc<ComponentRegistry> = Arc::clone(&registry);
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        // Set the module (state + action declarations)
97        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    /// Set the action handler for this session.
119    ///
120    /// Called when a `dispatchAction` message arrives. Receives the action name,
121    /// optional payload, and current state. Must return the new state.
122    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    /// The session ID assigned to this client.
130    pub fn session_id(&self) -> &str {
131        &self.session_id
132    }
133
134    /// Handle the hello handshake. Returns `[sessionAck, initialTree]` as JSON.
135    ///
136    /// Call this either:
137    /// - When you receive a `hello` message from the client, or
138    /// - Immediately after connection (for clients that don't send hello)
139    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        // 1. sessionAck
144        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        // 2. Render the UI (first time only) and capture patches
154        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        // 3. initialTree
163        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    /// Handle an incoming JSON message. Returns response messages as JSON strings.
177    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    /// Dispatch an action and return response messages.
202    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        // Run the user's action handler
207        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        // Update engine state → triggers reactive re-render → produces patches
216        let patches = update_state_and_capture(&mut inner.engine, new_state.clone());
217        inner.revision += 1;
218
219        // Send patches
220        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        // Send state update if subscribed (for Studio / debugging)
232        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    /// Get a snapshot of the current state.
247    pub fn get_state(&self) -> Value {
248        self.inner.lock().unwrap().state.clone()
249    }
250
251    /// Get the current revision number.
252    pub fn revision(&self) -> u64 {
253        self.inner.lock().unwrap().revision
254    }
255}
256
257/// Parse + render DSL source, capturing patches via a temporary render callback.
258fn 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    // Detach callback
273    engine.set_render_callback(|_| {});
274
275    let result = patches.lock().unwrap().drain(..).collect();
276    result
277}
278
279/// Update engine state and capture the resulting patches.
280fn 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        // Initial render
344        let _ = session.handle_hello(None);
345
346        // Dispatch action
347        let action_json = r#"{"type":"dispatchAction","module":"App","action":"increment"}"#;
348        let responses = session.handle_message(action_json);
349
350        // Should get patch message back (if engine produced patches)
351        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        // Subscribe to state
367        let sub_json = r#"{"type":"subscribeState","module":"App"}"#;
368        session.handle_message(sub_json);
369
370        // Now dispatch — should get stateUpdate in response
371        let action_json = r#"{"type":"dispatchAction","module":"App","action":"set"}"#;
372        let responses = session.handle_message(action_json);
373
374        // Should contain a stateUpdate message
375        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}