use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use hypen_engine::{Engine, Patch};
use serde_json::Value;
use crate::context::GlobalContext;
use crate::discovery::ComponentRegistry;
use crate::module::{ActionHandler, ModuleDefinition};
use crate::router::HypenRouter;
use crate::state::State;
use super::types::RemoteMessage;
pub struct SessionConfig {
pub module_name: String,
pub ui_source: String,
pub components: ComponentRegistry,
pub initial_state: Value,
pub action_names: Vec<String>,
pub resources: indexmap::IndexMap<String, String>,
pub modules: Vec<(String, Value, Vec<String>)>,
}
impl Default for SessionConfig {
fn default() -> Self {
Self {
module_name: String::new(),
ui_source: String::new(),
components: ComponentRegistry::new(),
initial_state: Value::Null,
action_names: Vec::new(),
resources: indexmap::IndexMap::new(),
modules: vec![],
}
}
}
type ActionHandlerFn = Arc<dyn Fn(&str, Option<&Value>, &Value) -> Value + Send + Sync>;
pub struct ModuleSessionConfig {
pub(crate) name: String,
pub(crate) initial_state: Value,
pub(crate) action_handler: ActionHandlerFn,
pub(crate) action_names: Vec<String>,
}
impl ModuleSessionConfig {
pub fn from_definition<S: State>(def: Arc<ModuleDefinition<S>>) -> Self {
let initial_state = serde_json::to_value(&def.initial_state).unwrap_or(Value::Null);
Self::build(def, initial_state)
}
pub fn from_definition_with_state<S: State>(def: Arc<ModuleDefinition<S>>, state: S) -> Self {
let initial_state = serde_json::to_value(&state).unwrap_or(Value::Null);
Self::build(def, initial_state)
}
fn build<S: State>(def: Arc<ModuleDefinition<S>>, initial_state: Value) -> Self {
let name = def.name.clone();
let action_names = def.action_names();
let handler: ActionHandlerFn = Arc::new(move |action, payload, state_json| {
if action == "__hypen_bind" {
if let Some(payload_val) = payload {
if let Some(obj) = payload_val.as_object() {
if let Some(path) = obj.get("path").and_then(|p| p.as_str()) {
let value = obj.get("value").cloned().unwrap_or(Value::Null);
if let Some(new_state) =
crate::state::apply_bind_to_json::<S>(state_json, path, value)
{
return new_state;
}
}
}
}
return state_json.clone();
}
let mut state: S = match serde_json::from_value(state_json.clone()) {
Ok(s) => s,
Err(_) => return state_json.clone(),
};
if let Some(ActionHandler::Sync(h)) = def.action_handlers.get(action) {
h(&mut state, payload, None);
}
serde_json::to_value(&state).unwrap_or_else(|_| state_json.clone())
});
Self {
name,
initial_state,
action_handler: handler,
action_names,
}
}
}
type DisconnectHandlerFn = Box<dyn Fn(&Value, &super::SessionInfo) + Send + Sync>;
type ReconnectHandlerFn = Box<dyn Fn(&mut Value, &super::SessionInfo, &Value) + Send + Sync>;
type ExpireHandlerFn = Box<dyn Fn(&super::SessionInfo) + Send + Sync>;
type RouteActivationFn = Box<
dyn Fn(&HashMap<String, String>, &mut HashMap<String, Value>, &Arc<GlobalContext>)
+ Send
+ Sync,
>;
pub struct RemoteSession {
inner: Mutex<SessionInner>,
state: Arc<Mutex<HashMap<String, Value>>>,
primary_handler: Arc<Mutex<Option<ActionHandlerFn>>>,
route_hooks: Arc<Mutex<Vec<(String, RouteActivationFn)>>>,
on_disconnect: Option<DisconnectHandlerFn>,
on_reconnect: Option<ReconnectHandlerFn>,
on_expire: Option<ExpireHandlerFn>,
router: Arc<HypenRouter>,
context: Arc<GlobalContext>,
module_name: String,
session_id: String,
}
struct SessionInner {
engine: Engine,
ui_source: String,
revision: u64,
state_subscribed: bool,
rendered: bool,
}
impl RemoteSession {
pub fn new(config: SessionConfig) -> Self {
let session_id = format!(
"session_{}",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_nanos()
);
let mut engine = Engine::new();
let registry = Arc::new(config.components);
let reg: Arc<ComponentRegistry> = Arc::clone(®istry);
engine.set_component_resolver(move |name, _ctx_path| {
reg.get(name).map(|entry| hypen_engine::ir::ResolvedComponent {
source: entry.source.clone(),
path: entry
.path
.as_ref()
.map(|p: &PathBuf| p.to_string_lossy().to_string())
.unwrap_or_default(),
passthrough: false,
lazy: false,
})
});
let module_meta = hypen_engine::Module::new(&config.module_name)
.with_actions(config.action_names.clone());
let engine_module =
hypen_engine::ModuleInstance::new(module_meta, config.initial_state.clone());
engine.set_module(engine_module);
for (name, svg) in &config.resources {
engine.register_resource(name, svg);
}
for (name, initial_state, action_names) in &config.modules {
let module_meta =
hypen_engine::Module::new(name).with_actions(action_names.clone());
let module_inst =
hypen_engine::ModuleInstance::new(module_meta, initial_state.clone());
engine.register_module(name, module_inst);
}
let mut state_map: HashMap<String, Value> = HashMap::new();
state_map.insert(String::new(), config.initial_state.clone());
for (name, initial_state, _) in &config.modules {
state_map.insert(name.to_lowercase(), initial_state.clone());
}
let state = Arc::new(Mutex::new(state_map));
let primary_handler: Arc<Mutex<Option<ActionHandlerFn>>> = Arc::new(Mutex::new(None));
for action_name in &config.action_names {
let name = action_name.clone();
let state_arc = Arc::clone(&state);
let handler_arc = Arc::clone(&primary_handler);
engine.on_action(name.clone(), move |action| {
let handler_guard = handler_arc.lock().unwrap();
let Some(handler) = handler_guard.as_ref() else { return };
let mut state_guard = state_arc.lock().unwrap();
let current = state_guard.get("").cloned().unwrap_or(Value::Null);
let new_state = handler(&name, action.payload.as_ref(), ¤t);
state_guard.insert(String::new(), new_state);
});
}
let router = Arc::new(HypenRouter::new());
let context = Arc::new(GlobalContext::new());
context.set_router(Arc::clone(&router));
let route_hooks: Arc<Mutex<Vec<(String, RouteActivationFn)>>> =
Arc::new(Mutex::new(Vec::new()));
let install_router_handler = |engine: &mut Engine, name: &'static str| {
let router = Arc::clone(&router);
let state_arc = Arc::clone(&state);
let hooks = Arc::clone(&route_hooks);
let ctx = Arc::clone(&context);
engine.on_action(name, move |action| {
let to = action
.payload
.as_ref()
.and_then(|p| p.get("to"))
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let new_path = match name {
"router.push" => to.and_then(|t| {
router.push(&t);
Some(router.current_path())
}),
"router.replace" => to.and_then(|t| {
router.replace(&t);
Some(router.current_path())
}),
"router.back" => {
router.back();
Some(router.current_path())
}
_ => None, };
let Some(path) = new_path else { return };
let mut g = state_arc.lock().unwrap();
if let Some(primary) = g.get_mut("") {
if let Some(obj) = primary.as_object_mut() {
if obj.contains_key("location") {
obj.insert("location".to_string(), Value::String(path.clone()));
}
}
}
let hooks_guard = hooks.lock().unwrap();
for (pattern, hook) in hooks_guard.iter() {
if let Some(m) = hypen_engine::match_path(pattern, &path) {
let params: HashMap<String, String> = m.params.into_iter().collect();
hook(¶ms, &mut g, &ctx);
}
}
});
};
install_router_handler(&mut engine, "router.push");
install_router_handler(&mut engine, "router.replace");
install_router_handler(&mut engine, "router.back");
install_router_handler(&mut engine, "router.forward");
Self {
inner: Mutex::new(SessionInner {
engine,
ui_source: config.ui_source,
revision: 0,
state_subscribed: false,
rendered: false,
}),
state,
primary_handler,
on_disconnect: None,
on_reconnect: None,
on_expire: None,
router,
context,
route_hooks,
module_name: config.module_name,
session_id,
}
}
pub fn on_route_enter<F>(&self, pattern: impl Into<String>, handler: F)
where
F: Fn(&HashMap<String, String>, &mut HashMap<String, Value>, &Arc<GlobalContext>)
+ Send
+ Sync
+ 'static,
{
self.route_hooks
.lock()
.unwrap()
.push((pattern.into(), Box::new(handler)));
}
pub fn router(&self) -> &Arc<HypenRouter> {
&self.router
}
pub fn context(&self) -> &Arc<GlobalContext> {
&self.context
}
pub fn from_definition<S: State>(
def: Arc<ModuleDefinition<S>>,
components: ComponentRegistry,
) -> Self {
Self::build_from_definition(def, components, None, vec![])
}
pub fn from_definition_with_state<S: State>(
def: Arc<ModuleDefinition<S>>,
components: ComponentRegistry,
initial_state: S,
modules: Vec<ModuleSessionConfig>,
) -> Self {
Self::build_from_definition(def, components, Some(initial_state), modules)
}
fn build_from_definition<S: State>(
def: Arc<ModuleDefinition<S>>,
components: ComponentRegistry,
state_override: Option<S>,
modules: Vec<ModuleSessionConfig>,
) -> Self {
let state_ref = state_override.as_ref().unwrap_or(&def.initial_state);
let initial_state_json = serde_json::to_value(state_ref).unwrap_or(Value::Null);
let ui_source = def
.ui_source
.clone()
.or_else(|| {
def.ui_file
.as_ref()
.and_then(|p| std::fs::read_to_string(p).ok())
})
.unwrap_or_default();
let raw_modules: Vec<(String, Value, Vec<String>)> = modules
.iter()
.map(|m| (m.name.clone(), m.initial_state.clone(), m.action_names.clone()))
.collect();
let config = SessionConfig {
module_name: def.name.clone(),
ui_source,
components,
initial_state: initial_state_json,
action_names: def.action_names(),
resources: def.resource_map.clone(),
modules: raw_modules,
};
let mut session = Self::new(config);
let def_for_disconnect = Arc::clone(&def);
let def_for_reconnect = Arc::clone(&def);
let def_for_expire = Arc::clone(&def);
session.set_action_handler(move |action, payload, state_json| {
if action == "__hypen_bind" {
if let Some(payload_val) = payload {
if let Some(obj) = payload_val.as_object() {
if let Some(path) = obj.get("path").and_then(|p| p.as_str()) {
let value = obj.get("value").cloned().unwrap_or(Value::Null);
if let Some(new_state) =
crate::state::apply_bind_to_json::<S>(state_json, path, value)
{
return new_state;
}
}
}
}
return state_json.clone();
}
let mut state: S = match serde_json::from_value(state_json.clone()) {
Ok(s) => s,
Err(_) => return state_json.clone(),
};
if let Some(ActionHandler::Sync(handler)) = def.action_handlers.get(action) {
handler(&mut state, payload, None);
}
serde_json::to_value(&state).unwrap_or_else(|_| state_json.clone())
});
{
let mut inner = session.inner.lock().unwrap();
for module_cfg in modules {
let scope_key = module_cfg.name.to_lowercase();
let handler = Arc::clone(&module_cfg.action_handler);
for action_name in &module_cfg.action_names {
let action = action_name.clone();
let scope = scope_key.clone();
let h = Arc::clone(&handler);
let state_arc = Arc::clone(&session.state);
inner.engine.on_action(action.clone(), move |evt| {
let mut state_guard = state_arc.lock().unwrap();
let current = state_guard.get(&scope).cloned().unwrap_or(Value::Null);
let new_state = h(&action, evt.payload.as_ref(), ¤t);
state_guard.insert(scope.clone(), new_state);
});
}
}
}
if def_for_disconnect.on_disconnect.is_some() {
session.on_disconnect = Some(Box::new(move |state_json, session_info| {
if let Some(ref handler) = def_for_disconnect.on_disconnect {
if let Ok(state) = serde_json::from_value::<S>(state_json.clone()) {
handler(&state, session_info);
}
}
}));
}
if def_for_reconnect.on_reconnect.is_some() {
session.on_reconnect = Some(Box::new(move |state_json, session_info, saved_state| {
if let Some(ref handler) = def_for_reconnect.on_reconnect {
if let Ok(mut state) = serde_json::from_value::<S>(state_json.clone()) {
handler(&mut state, session_info, saved_state);
if let Ok(new_json) = serde_json::to_value(&state) {
*state_json = new_json;
}
}
}
}));
}
if def_for_expire.on_expire.is_some() {
session.on_expire = Some(Box::new(move |session_info| {
if let Some(ref handler) = def_for_expire.on_expire {
handler(session_info);
}
}));
}
session
}
pub fn set_action_handler<F>(&self, handler: F)
where
F: Fn(&str, Option<&Value>, &Value) -> Value + Send + Sync + 'static,
{
*self.primary_handler.lock().unwrap() = Some(Arc::new(handler));
}
pub fn session_id(&self) -> &str {
&self.session_id
}
pub fn handle_hello(&self, client_session_id: Option<&str>) -> Vec<String> {
let mut inner = self.inner.lock().unwrap();
let mut messages = Vec::with_capacity(2);
let is_restored = client_session_id.is_some();
let ack = RemoteMessage::SessionAck {
session_id: client_session_id
.unwrap_or(&self.session_id)
.to_string(),
is_new: !is_restored,
is_restored,
};
if let Ok(json) = ack.to_json() {
messages.push(json);
}
let patches = if !inner.rendered {
inner.rendered = true;
let ui = inner.ui_source.clone();
render_and_capture(&mut inner.engine, &ui)
} else {
vec![]
};
let primary_state = self
.state
.lock()
.unwrap()
.get("")
.cloned()
.unwrap_or(Value::Null);
let initial = RemoteMessage::InitialTree {
module: self.module_name.clone(),
state: primary_state,
patches,
revision: 0,
};
if let Ok(json) = initial.to_json() {
messages.push(json);
}
messages
}
pub fn handle_message(&self, json: &str) -> Vec<String> {
let msg = match RemoteMessage::from_json(json) {
Ok(m) => m,
Err(_) => return vec![],
};
match msg {
RemoteMessage::Hello { session_id, .. } => {
self.handle_hello(session_id.as_deref())
}
RemoteMessage::DispatchAction {
module,
action,
payload,
} => self.handle_action(&module, &action, payload.as_ref()),
RemoteMessage::SubscribeState { .. } => {
self.inner.lock().unwrap().state_subscribed = true;
vec![]
}
_ => vec![],
}
}
fn handle_action(
&self,
_module: &str,
action: &str,
payload: Option<&Value>,
) -> Vec<String> {
let mut inner = self.inner.lock().unwrap();
let mut messages = Vec::new();
let mut action_obj = hypen_engine::dispatch::Action::new(action);
if let Some(p) = payload {
action_obj = action_obj.with_payload(p.clone());
}
let state_arc = Arc::clone(&self.state);
let pre: HashMap<String, Value> = state_arc.lock().unwrap().clone();
let patches = with_capture(&mut inner.engine, |engine| {
let _ = engine.dispatch_action(action_obj);
let post = state_arc.lock().unwrap().clone();
for (key, new_state) in &post {
if pre.get(key) != Some(new_state) {
let scope_opt = if key.is_empty() { None } else { Some(key.as_str()) };
engine.update_state(scope_opt, new_state.clone());
}
}
});
inner.revision += 1;
if !patches.is_empty() {
let patch_msg = RemoteMessage::Patch {
module: self.module_name.clone(),
patches,
revision: inner.revision,
};
if let Ok(json) = patch_msg.to_json() {
messages.push(json);
}
}
if inner.state_subscribed {
let primary_state = self
.state
.lock()
.unwrap()
.get("")
.cloned()
.unwrap_or(Value::Null);
let state_msg = RemoteMessage::StateUpdate {
module: self.module_name.clone(),
state: primary_state,
revision: inner.revision,
};
if let Ok(json) = state_msg.to_json() {
messages.push(json);
}
}
messages
}
pub fn get_state(&self) -> Value {
self.state
.lock()
.unwrap()
.get("")
.cloned()
.unwrap_or(Value::Null)
}
pub fn revision(&self) -> u64 {
self.inner.lock().unwrap().revision
}
pub fn fire_disconnect(&self, session_info: &super::SessionInfo) {
if let Some(ref handler) = self.on_disconnect {
let state = self.get_state();
handler(&state, session_info);
}
}
pub fn fire_reconnect(&self, session_info: &super::SessionInfo, saved_state: &Value) {
let mut state_guard = self.state.lock().unwrap();
let current = state_guard.get_mut("").unwrap();
if let Some(ref handler) = self.on_reconnect {
handler(current, session_info, saved_state);
} else {
*current = saved_state.clone();
}
}
pub fn fire_expire(&self, session_info: &super::SessionInfo) {
if let Some(ref handler) = self.on_expire {
handler(session_info);
}
}
}
fn with_capture<F>(engine: &mut Engine, mutate: F) -> Vec<Patch>
where
F: FnOnce(&mut Engine),
{
let patches = Arc::new(Mutex::new(Vec::<Patch>::new()));
let capture = Arc::clone(&patches);
engine.set_render_callback(move |p| {
capture.lock().unwrap().extend_from_slice(p);
});
mutate(engine);
engine.set_render_callback(|_| {});
let mut guard = patches.lock().unwrap();
std::mem::take(&mut *guard)
}
fn render_and_capture(engine: &mut Engine, ui_source: &str) -> Vec<Patch> {
with_capture(engine, |engine| {
if let Ok(doc) = hypen_parser::parse_document(ui_source) {
if let Some(component) = doc.components.first() {
let ir_node = hypen_engine::ast_to_ir_node(component);
engine.render_ir_node(&ir_node);
}
}
})
}
#[cfg(test)]
mod tests {
use super::*;
fn test_config() -> SessionConfig {
let mut components = ComponentRegistry::new();
components.register(
"Greeting",
r#"Text("Hello @{state.name}")"#,
None,
);
SessionConfig {
module_name: "App".to_string(),
ui_source: r#"Column { Text("Count: @{state.count}") }"#.to_string(),
components,
initial_state: serde_json::json!({
"count": 0,
"name": "World"
}),
action_names: vec!["increment".to_string()],
resources: indexmap::IndexMap::new(),
modules: Vec::new(),
}
}
#[test]
fn test_session_hello_returns_ack_and_tree() {
let session = RemoteSession::new(test_config());
let msgs = session.handle_hello(None);
assert_eq!(msgs.len(), 2);
assert!(msgs[0].contains("sessionAck"));
assert!(msgs[1].contains("initialTree"));
assert!(msgs[1].contains("\"count\":0"));
}
#[test]
fn test_session_dispatch_action() {
let session = RemoteSession::new(test_config());
session.set_action_handler(|action, _payload, state| {
let mut s = state.clone();
if action == "increment" {
if let Some(count) = s.get_mut("count").and_then(|v| v.as_i64()) {
s["count"] = serde_json::json!(count + 1);
}
}
s
});
let _ = session.handle_hello(None);
let action_json = r#"{"type":"dispatchAction","module":"App","action":"increment"}"#;
let _responses = session.handle_message(action_json);
assert!(session.get_state()["count"] == 1);
assert_eq!(session.revision(), 1);
}
#[test]
fn test_session_state_subscription() {
let session = RemoteSession::new(test_config());
session.set_action_handler(|_action, _payload, state| {
let mut s = state.clone();
s["count"] = serde_json::json!(42);
s
});
let _ = session.handle_hello(None);
let sub_json = r#"{"type":"subscribeState","module":"App"}"#;
session.handle_message(sub_json);
let action_json = r#"{"type":"dispatchAction","module":"App","action":"set"}"#;
let responses = session.handle_message(action_json);
let has_state_update = responses.iter().any(|r| r.contains("stateUpdate"));
assert!(has_state_update);
}
#[test]
fn test_session_hello_via_message() {
let session = RemoteSession::new(test_config());
let hello_json = r#"{"type":"hello"}"#;
let msgs = session.handle_message(hello_json);
assert_eq!(msgs.len(), 2);
assert!(msgs[0].contains("sessionAck"));
assert!(msgs[1].contains("initialTree"));
}
#[test]
fn test_session_resources_reach_engine_and_render() {
let components = ComponentRegistry::new();
let mut resources = indexmap::IndexMap::new();
let heart_svg = r#"<svg viewBox="0 0 24 24"><path d="M12 21s-7-4.5-7-11a5 5 0 0 1 9-3 5 5 0 0 1 9 3c0 6.5-7 11-7 11z" stroke="currentColor"/></svg>"#;
resources.insert("heart".to_string(), heart_svg.to_string());
let config = SessionConfig {
module_name: "App".to_string(),
ui_source: r#"Icon(@resources.heart)"#.to_string(),
components,
initial_state: serde_json::json!({}),
action_names: vec![],
resources,
modules: Vec::new(),
};
let session = RemoteSession::new(config);
let msgs = session.handle_hello(None);
assert_eq!(msgs.len(), 2, "expected ack + initialTree");
let initial_tree = &msgs[1];
assert!(
initial_tree.contains("__iconPaths"),
"initialTree does not contain __iconPaths — resources did not reach the engine. \
Payload: {}",
initial_tree
);
assert!(
initial_tree.contains(r#""d":"M12 21"#),
"resolved heart path d did not round-trip into the patch stream: {}",
initial_tree
);
}
}