use serde_wasm_bindgen::from_value;
use std::collections::HashSet;
use wasm_bindgen::prelude::*;
static NULL_STATE: serde_json::Value = serde_json::Value::Null;
fn structured_error(error_type: &str, message: &str) -> JsValue {
let obj = js_sys::Object::new();
let _ = js_sys::Reflect::set(&obj, &"type".into(), &JsValue::from_str(error_type));
let _ = js_sys::Reflect::set(&obj, &"message".into(), &JsValue::from_str(message));
obj.into()
}
use crate::{
dispatch::Action,
engine_core::EngineCore,
ir::{ast_to_ir_node, Element, IRNode},
lifecycle::ModuleInstance,
reconcile::Patch,
wasm::shared::{format_parse_errors, render_subtree_into, NodeIdIndex},
};
#[wasm_bindgen]
pub struct WasmEngine {
core: EngineCore,
patch_callback: Option<js_sys::Function>,
action_handlers: std::collections::HashMap<String, js_sys::Function>,
component_resolver: Option<js_sys::Function>,
import_visited: HashSet<String>,
node_id_index: NodeIdIndex,
data_source_action_handler: Option<js_sys::Function>,
}
#[wasm_bindgen]
impl WasmEngine {
#[allow(clippy::new_without_default)]
#[wasm_bindgen(constructor)]
pub fn new() -> Self {
#[cfg(feature = "console_error_panic_hook")]
console_error_panic_hook::set_once();
Self {
core: EngineCore::new(),
patch_callback: None,
action_handlers: std::collections::HashMap::new(),
component_resolver: None,
import_visited: HashSet::new(),
node_id_index: NodeIdIndex::new(),
data_source_action_handler: None,
}
}
#[wasm_bindgen(js_name = renderSource)]
pub fn render_source(&mut self, source: &str) -> Result<(), JsValue> {
let doc = hypen_parser::parse_document(source)
.map_err(|e| structured_error("parseError", &format_parse_errors(&e)))?;
self.import_visited.clear();
self.resolve_imports(&doc.imports);
if let Some(component) = doc.components.first() {
let ir_node = ast_to_ir_node(component);
self.render(&ir_node);
}
Ok(())
}
#[wasm_bindgen(js_name = setRenderCallback)]
pub fn set_render_callback(&mut self, callback: js_sys::Function) {
self.patch_callback = Some(callback);
}
#[wasm_bindgen(js_name = setComponentResolver)]
pub fn set_component_resolver(&mut self, resolver: js_sys::Function) {
self.component_resolver = Some(resolver);
}
#[wasm_bindgen(js_name = registerPrimitive)]
pub fn register_primitive(&mut self, name: &str) {
self.core.component_registry.register_primitive(name);
}
#[wasm_bindgen(js_name = registerDefaultPrimitives)]
pub fn register_default_primitives(&mut self) {
self.core.component_registry.register_default_primitives();
}
#[wasm_bindgen(js_name = clearResolvedComponents)]
pub fn clear_resolved_components(&mut self) {
self.core.component_registry.clear_resolved();
}
#[wasm_bindgen(js_name = discoverRouters)]
pub fn discover_routers(&self, source: &str) -> Result<JsValue, JsValue> {
let doc = hypen_parser::parse_document(source)
.map_err(|e| structured_error("parseError", &format_parse_errors(&e)))?;
let mut routers = Vec::new();
for component in &doc.components {
let ir = crate::ir::ast_to_ir_node(component);
routers.extend(crate::ir::discover_routers(&ir));
}
serde_wasm_bindgen::to_value(&routers)
.map_err(|e| structured_error("serializeError", &format!("{}", e)))
}
#[wasm_bindgen(js_name = registerResources)]
pub fn register_resources(&mut self, resources_js: JsValue) -> Result<(), JsValue> {
let map: indexmap::IndexMap<String, String> = from_value(resources_js)
.map_err(|e| structured_error("resourceError", &format!("Invalid resources: {}", e)))?;
#[cfg(debug_assertions)]
web_sys::console::log_1(&format!("[WASM] Registered {} resources", map.len()).into());
self.core.resource_registry.register_map(map);
Ok(())
}
#[wasm_bindgen(js_name = renderLazyComponent)]
pub fn render_lazy_component(&mut self, source: &str) -> Result<(), JsValue> {
self.render_source(source)
}
#[wasm_bindgen(js_name = renderInto)]
pub fn render_into(
&mut self,
source: &str,
parent_node_id_str: &str,
state_js: JsValue,
) -> Result<(), JsValue> {
let doc = hypen_parser::parse_document(source)
.map_err(|e| structured_error("parseError", &format_parse_errors(&e)))?;
self.import_visited.clear();
self.resolve_imports(&doc.imports);
let component = doc
.components
.first()
.ok_or_else(|| structured_error("parseError", "No component found in source"))?;
let ir_node = ast_to_ir_node(component);
self.resolve_ir_node_components(&ir_node);
let mut expanded = self.core.component_registry.expand_ir_node(&ir_node);
if !self.core.resource_registry.is_empty() {
crate::ir::resolve_icons_in_ir(&self.core.resource_registry, &mut expanded);
}
let state: serde_json::Value = if state_js.is_null() || state_js.is_undefined() {
serde_json::Value::Null
} else {
from_value(state_js).map_err(|e| structured_error("stateError", &e.to_string()))?
};
let parent_id = self
.node_id_index
.lookup(parent_node_id_str)
.ok_or_else(|| {
structured_error(
"renderError",
&format!("Parent node not found: {}", parent_node_id_str),
)
})?;
#[cfg(debug_assertions)]
web_sys::console::log_1(
&format!("[WASM] Rendering into parent node: {}", parent_node_id_str).into(),
);
let mut patches = Vec::new();
render_subtree_into(&mut self.core, parent_id, &expanded, &state, &mut patches);
self.emit_patches(patches);
Ok(())
}
fn render(&mut self, ir_node: &IRNode) {
self.resolve_ir_node_components(ir_node);
let patches = self.core.render_ir_node(ir_node);
self.emit_patches(patches);
}
fn resolve_imports(&mut self, imports: &[hypen_parser::ImportStatement]) {
let resolver = match self.component_resolver.clone() {
Some(r) => r,
None => return,
};
for import in imports {
let source_path = import.source_path();
for name in import.imported_names() {
if self
.core
.component_registry
.get(&name, Some(source_path))
.is_some()
{
continue;
}
let import_key = format!("{}:{}", source_path, name);
if self.import_visited.contains(&import_key) {
continue;
}
self.import_visited.insert(import_key);
let name_js = JsValue::from_str(&name);
let source_path_js = JsValue::from_str(source_path);
if let Ok(result) = resolver.call2(&JsValue::NULL, &name_js, &source_path_js) {
if result.is_null() || result.is_undefined() {
continue;
}
let source_val =
js_sys::Reflect::get(&result, &JsValue::from_str("source")).ok();
let path_val = js_sys::Reflect::get(&result, &JsValue::from_str("path")).ok();
let passthrough_val =
js_sys::Reflect::get(&result, &JsValue::from_str("passthrough")).ok();
let lazy_val = js_sys::Reflect::get(&result, &JsValue::from_str("lazy")).ok();
if let (Some(source_js), Some(path_js)) = (source_val, path_val) {
if let (Some(resolved_source), Some(path)) =
(source_js.as_string(), path_js.as_string())
{
let is_lazy = lazy_val.and_then(|v| v.as_bool()).unwrap_or(false);
let is_passthrough =
passthrough_val.and_then(|v| v.as_bool()).unwrap_or(false);
if is_lazy {
let dummy_element = Element::new(&name);
let component =
crate::ir::Component::new(name.clone(), move |_props| {
dummy_element.clone()
})
.with_source_path(&path)
.with_lazy(true);
self.core.component_registry.register(component);
} else if is_passthrough {
let dummy_element = Element::new(&name);
let component =
crate::ir::Component::new(name.clone(), move |_props| {
dummy_element.clone()
})
.with_source_path(&path)
.with_passthrough(true);
self.core.component_registry.register(component);
} else {
if let Ok(resolved_doc) =
hypen_parser::parse_document(&resolved_source)
{
self.resolve_imports(&resolved_doc.imports);
if let Some(component_spec) = resolved_doc.components.first() {
let ir_node = ast_to_ir_node(component_spec);
if let IRNode::Element(ir_element) = &ir_node {
let ir_element = ir_element.clone();
let component = crate::ir::Component::new(
name.clone(),
move |_props| ir_element.clone(),
)
.with_source_path(&path);
self.core.component_registry.register(component);
}
self.resolve_ir_node_components_with_context(
&ir_node,
Some(&path),
);
}
} else if let Ok(component_spec) =
hypen_parser::parse_component(&resolved_source)
{
let ir_node = ast_to_ir_node(&component_spec);
if let IRNode::Element(ir_element) = &ir_node {
let ir_element = ir_element.clone();
let component = crate::ir::Component::new(
name.clone(),
move |_props| ir_element.clone(),
)
.with_source_path(&path);
self.core.component_registry.register(component);
}
self.resolve_ir_node_components_with_context(
&ir_node,
Some(&path),
);
}
}
}
}
}
}
}
}
fn resolve_components_with_context(&mut self, element: &Element, context_path: Option<&str>) {
if self
.core
.component_registry
.is_primitive(&element.element_type)
{
for child_ir in &element.ir_children {
if let IRNode::Element(child) = child_ir {
self.resolve_components_with_context(child, context_path);
}
}
return;
}
if self
.core
.component_registry
.get(&element.element_type, context_path)
.is_none()
{
if let Some(ref resolver) = self.component_resolver.clone() {
let name_js = JsValue::from_str(&element.element_type);
let context_js = context_path.map(JsValue::from_str).unwrap_or(JsValue::NULL);
if let Ok(result) = resolver.call2(&JsValue::NULL, &name_js, &context_js) {
if !result.is_null() && !result.is_undefined() {
let source_val =
js_sys::Reflect::get(&result, &JsValue::from_str("source")).ok();
let path_val =
js_sys::Reflect::get(&result, &JsValue::from_str("path")).ok();
let passthrough_val =
js_sys::Reflect::get(&result, &JsValue::from_str("passthrough")).ok();
let lazy_val =
js_sys::Reflect::get(&result, &JsValue::from_str("lazy")).ok();
if let (Some(source_js), Some(path_js)) = (source_val, path_val) {
if let (Some(source), Some(path)) =
(source_js.as_string(), path_js.as_string())
{
let is_lazy = lazy_val.and_then(|v| v.as_bool()).unwrap_or(false);
let is_passthrough =
passthrough_val.and_then(|v| v.as_bool()).unwrap_or(false);
if is_lazy {
#[cfg(debug_assertions)]
web_sys::console::log_1(
&format!(
"[WASM] Registering lazy component: {}",
element.element_type
)
.into(),
);
let name = element.element_type.clone();
let dummy_element = Element::new(&name);
let component =
crate::ir::Component::new(name.clone(), move |_props| {
dummy_element.clone()
})
.with_source_path(&path)
.with_lazy(true);
self.core.component_registry.register(component);
} else if is_passthrough {
#[cfg(debug_assertions)]
web_sys::console::log_1(
&format!(
"[WASM] Registering passthrough component: {}",
element.element_type
)
.into(),
);
let name = element.element_type.clone();
let dummy_element = Element::new(&name);
let component =
crate::ir::Component::new(name.clone(), move |_props| {
dummy_element.clone()
})
.with_source_path(&path)
.with_passthrough(true);
self.core.component_registry.register(component);
} else {
if let Ok(component_spec) =
hypen_parser::parse_component(&source)
{
let ir_node = ast_to_ir_node(&component_spec);
let name = element.element_type.clone();
let path_clone = path.clone();
if let IRNode::Element(ir_element) = &ir_node {
let ir_element = ir_element.clone();
let spec_is_module = component_spec.declaration_type
== hypen_parser::DeclarationType::Module;
let spec_module_name = if spec_is_module {
Some(component_spec.name.to_lowercase())
} else {
None
};
let mut component = crate::ir::Component::new(
name.clone(),
move |_props| ir_element.clone(),
)
.with_source_path(&path);
if spec_is_module {
component.is_module = true;
component.module_name = spec_module_name;
}
self.core.component_registry.register(component);
}
self.resolve_ir_node_components_with_context(
&ir_node,
Some(&path_clone),
);
}
}
}
}
}
}
}
}
for child_ir in &element.ir_children {
if let IRNode::Element(child) = child_ir {
self.resolve_components_with_context(child, context_path);
}
}
}
fn resolve_ir_node_components(&mut self, node: &IRNode) {
self.resolve_ir_node_components_with_context(node, None);
}
fn resolve_ir_node_components_with_context(
&mut self,
node: &IRNode,
context_path: Option<&str>,
) {
match node {
IRNode::Element(element) => {
self.resolve_components_with_context(element, context_path);
for child in &element.ir_children {
self.resolve_ir_node_components_with_context(child, context_path);
}
}
IRNode::ForEach { template, .. } => {
for child in template {
self.resolve_ir_node_components_with_context(child, context_path);
}
}
IRNode::Conditional {
branches, fallback, ..
} => {
for branch in branches {
for child in &branch.children {
self.resolve_ir_node_components_with_context(child, context_path);
}
}
if let Some(fb) = fallback {
for child in fb {
self.resolve_ir_node_components_with_context(child, context_path);
}
}
}
IRNode::Router {
routes, fallback, ..
} => {
for route in routes {
for child in &route.children {
self.resolve_ir_node_components_with_context(child, context_path);
}
}
if let Some(fb) = fallback {
for child in fb {
self.resolve_ir_node_components_with_context(child, context_path);
}
}
}
}
}
fn emit_patches(&mut self, mut patches: Vec<Patch>) {
EngineCore::filter_spurious_removes(&mut patches);
self.node_id_index.index_creates(&patches, &self.core);
if let Some(ref callback) = self.patch_callback {
let serializer = serde_wasm_bindgen::Serializer::json_compatible();
if let Ok(patches_js) = serde::Serialize::serialize(&patches, &serializer) {
let _ = callback.call1(&JsValue::NULL, &patches_js);
}
}
}
#[wasm_bindgen(js_name = updateState)]
pub fn update_state(
&mut self,
scope: Option<String>,
state_patch: JsValue,
) -> Result<(), JsValue> {
let patch: serde_json::Value = from_value(state_patch)
.map_err(|e| structured_error("stateError", &format!("Invalid state patch: {}", e)))?;
let normalized = scope.as_deref().filter(|s| !s.is_empty());
if self.core.update_state(normalized, patch) {
self.render_dirty();
}
Ok(())
}
#[wasm_bindgen(js_name = updateStateSparse)]
pub fn update_state_sparse(
&mut self,
scope: Option<String>,
paths_js: JsValue,
values_js: JsValue,
) -> Result<(), JsValue> {
let paths: Vec<String> = from_value(paths_js)
.map_err(|e| structured_error("stateError", &format!("Invalid paths array: {}", e)))?;
let values: serde_json::Value = from_value(values_js).map_err(|e| {
structured_error("stateError", &format!("Invalid values object: {}", e))
})?;
let normalized = scope.as_deref().filter(|s| !s.is_empty());
if self.core.update_state_sparse(normalized, &paths, &values) {
self.render_dirty();
}
Ok(())
}
fn render_dirty(&mut self) {
let patches = self.core.render_dirty();
if !patches.is_empty() {
self.emit_patches(patches);
}
}
#[wasm_bindgen(js_name = setContext)]
pub fn set_context(&mut self, name: &str, data_js: JsValue) -> Result<(), JsValue> {
let data: serde_json::Value = from_value(data_js)
.map_err(|e| structured_error("stateError", &format!("Invalid context data: {}", e)))?;
self.core.set_context(name, data);
self.render_dirty();
Ok(())
}
#[wasm_bindgen(js_name = removeContext)]
pub fn remove_context(&mut self, name: &str) {
self.core.remove_context(name);
self.render_dirty();
}
#[wasm_bindgen(js_name = dispatchAction)]
pub fn dispatch_action(&mut self, name: &str, payload: JsValue) -> Result<(), JsValue> {
let payload: Option<serde_json::Value> = if payload.is_undefined() || payload.is_null() {
None
} else {
Some(from_value(payload).map_err(|e| {
structured_error("actionError", &format!("Invalid action payload: {}", e))
})?)
};
let payload = payload.unwrap_or(serde_json::Value::Null);
if let Some(handler) = self.action_handlers.get(name) {
let action = Action::new(name).with_payload(payload);
let serializer = serde_wasm_bindgen::Serializer::json_compatible();
if let Ok(action_js) = serde::Serialize::serialize(&action, &serializer) {
let _ = handler.call1(&JsValue::NULL, &action_js);
}
return Ok(());
}
if let Some(ds_action) = self.core.build_data_source_action(name, payload) {
if let Some(ref handler) = self.data_source_action_handler {
let serializer = serde_wasm_bindgen::Serializer::json_compatible();
if let Ok(action_js) = serde::Serialize::serialize(&ds_action, &serializer) {
let _ = handler.call1(&JsValue::NULL, &action_js);
}
}
}
Ok(())
}
#[wasm_bindgen(js_name = onAction)]
pub fn on_action(&mut self, action_name: &str, handler: js_sys::Function) {
self.action_handlers
.insert(action_name.to_string(), handler);
}
#[wasm_bindgen(js_name = onDataSourceAction)]
pub fn on_data_source_action(&mut self, handler: js_sys::Function) {
self.data_source_action_handler = Some(handler);
}
#[wasm_bindgen(js_name = clearTree)]
pub fn clear_tree(&mut self) {
self.core.tree.clear();
}
#[wasm_bindgen(js_name = debugParseComponent)]
pub fn debug_parse_component(&self, source: &str) -> Result<String, JsValue> {
let component = hypen_parser::parse_component(source)
.map_err(|e| structured_error("parseError", &format_parse_errors(&e)))?;
let debug_info = format!(
"Name: {} | DeclarationType: {:?} | Children: {} | Applicators: {}",
component.name,
component.declaration_type,
component.children.len(),
component.applicators.len(),
);
Ok(debug_info)
}
#[wasm_bindgen(js_name = setModule)]
pub fn set_module(
&mut self,
name: &str,
actions: Vec<String>,
state_keys: Vec<String>,
initial_state: JsValue,
) -> Result<(), JsValue> {
let state: serde_json::Value = from_value(initial_state).map_err(|e| {
structured_error("stateError", &format!("Invalid initial state: {}", e))
})?;
let instance = ModuleInstance::from_config(name, actions, state_keys, state);
self.core.set_module(instance);
Ok(())
}
#[wasm_bindgen(js_name = registerModule)]
pub fn register_module(
&mut self,
name: &str,
actions: Vec<String>,
state_keys: Vec<String>,
initial_state: JsValue,
) -> Result<(), JsValue> {
let state: serde_json::Value = from_value(initial_state).map_err(|e| {
structured_error("stateError", &format!("Invalid initial state: {}", e))
})?;
let instance = ModuleInstance::from_config(name, actions, state_keys, state);
self.core.register_module(name, instance);
Ok(())
}
#[wasm_bindgen(js_name = getRevision)]
pub fn get_revision(&self) -> u64 {
self.core.revision
}
#[wasm_bindgen(js_name = currentState)]
pub fn current_state(&self) -> JsValue {
let state = self
.core
.module
.as_ref()
.map(|m| m.get_state())
.unwrap_or(&NULL_STATE);
let serializer = serde_wasm_bindgen::Serializer::json_compatible();
serde::Serialize::serialize(state, &serializer).unwrap_or(JsValue::NULL)
}
#[wasm_bindgen(js_name = treeSize)]
pub fn tree_size(&self) -> usize {
self.core.tree.len()
}
#[wasm_bindgen(js_name = validate)]
pub fn validate(&self) -> JsValue {
if let Some(root_id) = self.core.tree.root() {
if self.core.tree.get(root_id).is_none() {
return JsValue::from_str("Root node ID references a non-existent node");
}
}
for (node_id, node) in self.core.tree.iter() {
for child_id in &node.children {
match self.core.tree.get(*child_id) {
None => {
return JsValue::from_str(&format!(
"Node {} references non-existent child {}",
crate::reconcile::node_id_str(node_id),
crate::reconcile::node_id_str(*child_id),
));
}
Some(child) => {
if child.parent != Some(node_id) {
return JsValue::from_str(&format!(
"Child {} parent back-reference does not point to {}",
crate::reconcile::node_id_str(*child_id),
crate::reconcile::node_id_str(node_id),
));
}
}
}
}
}
JsValue::NULL
}
#[wasm_bindgen(js_name = reset)]
pub fn reset(&mut self) {
self.core = EngineCore::new();
self.action_handlers.clear();
self.import_visited.clear();
self.node_id_index = NodeIdIndex::new();
}
}
#[wasm_bindgen(js_name = patchesToJson)]
pub fn patches_to_json(patches: JsValue) -> Result<String, JsValue> {
let patches: Vec<Patch> = from_value(patches)
.map_err(|e| structured_error("stateError", &format!("Invalid patches: {}", e)))?;
serde_json::to_string_pretty(&patches)
.map_err(|e| structured_error("renderError", &format!("Serialization error: {}", e)))
}
#[wasm_bindgen(js_name = parseToJson)]
pub fn parse_to_json(source: &str) -> Result<String, JsValue> {
let component = hypen_parser::parse_component(source)
.map_err(|e| structured_error("parseError", &format_parse_errors(&e)))?;
serde_json::to_string_pretty(&component)
.map_err(|e| structured_error("renderError", &format!("Serialization error: {}", e)))
}
#[wasm_bindgen(start)]
pub fn main() {}
#[wasm_bindgen(js_name = diffPaths)]
pub fn diff_paths_js(old_json: &str, new_json: &str) -> Result<String, JsValue> {
let old: serde_json::Value = serde_json::from_str(old_json)
.map_err(|e| structured_error("stateError", &format!("diffPaths: bad old JSON: {e}")))?;
let new: serde_json::Value = serde_json::from_str(new_json)
.map_err(|e| structured_error("stateError", &format!("diffPaths: bad new JSON: {e}")))?;
let entries: Vec<serde_json::Value> = crate::portable::diff_paths(&old, &new)
.into_iter()
.map(|e| serde_json::json!({ "path": e.path, "value": e.new_value }))
.collect();
serde_json::to_string(&entries)
.map_err(|e| structured_error("stateError", &format!("diffPaths: serialise: {e}")))
}
#[wasm_bindgen(js_name = matchPath)]
pub fn match_path_js(pattern: &str, path: &str) -> String {
match crate::portable::match_path(pattern, path) {
Some(m) => {
let params: serde_json::Map<String, serde_json::Value> = m
.params
.into_iter()
.map(|(k, v)| (k, serde_json::Value::String(v)))
.collect();
serde_json::json!({ "matched": true, "params": params }).to_string()
}
None => serde_json::json!({ "matched": false, "params": {} }).to_string(),
}
}
#[wasm_bindgen(js_name = pathGet)]
pub fn path_get_js(value_json: &str, path: &str) -> Result<String, JsValue> {
let v: serde_json::Value = serde_json::from_str(value_json)
.map_err(|e| structured_error("stateError", &format!("pathGet: bad JSON: {e}")))?;
let out = crate::portable::path_get(&v, path).unwrap_or(serde_json::Value::Null);
serde_json::to_string(&out)
.map_err(|e| structured_error("stateError", &format!("pathGet: serialise: {e}")))
}
#[wasm_bindgen(js_name = pathHas)]
pub fn path_has_js(value_json: &str, path: &str) -> Result<String, JsValue> {
let v: serde_json::Value = serde_json::from_str(value_json)
.map_err(|e| structured_error("stateError", &format!("pathHas: bad JSON: {e}")))?;
Ok(if crate::portable::path_has(&v, path) {
"true"
} else {
"false"
}
.to_string())
}
#[wasm_bindgen(js_name = pathSet)]
pub fn path_set_js(value_json: &str, path: &str, new_value_json: &str) -> Result<String, JsValue> {
let mut v: serde_json::Value = serde_json::from_str(value_json)
.map_err(|e| structured_error("stateError", &format!("pathSet: bad value JSON: {e}")))?;
let nv: serde_json::Value = serde_json::from_str(new_value_json).map_err(|e| {
structured_error("stateError", &format!("pathSet: bad new-value JSON: {e}"))
})?;
crate::portable::path_set(&mut v, path, nv);
serde_json::to_string(&v)
.map_err(|e| structured_error("stateError", &format!("pathSet: serialise: {e}")))
}
#[wasm_bindgen(js_name = pathDelete)]
pub fn path_delete_js(value_json: &str, path: &str) -> Result<String, JsValue> {
let mut v: serde_json::Value = serde_json::from_str(value_json)
.map_err(|e| structured_error("stateError", &format!("pathDelete: bad JSON: {e}")))?;
let removed = crate::portable::path_delete(&mut v, path);
serde_json::to_string(&serde_json::json!({ "json": v, "removed": removed }))
.map_err(|e| structured_error("stateError", &format!("pathDelete: serialise: {e}")))
}
#[wasm_bindgen(js_name = encodeUriComponent)]
pub fn encode_uri_component_js(input: &str) -> String {
crate::portable::encode_uri_component(input)
}
#[wasm_bindgen(js_name = decodeUriComponent)]
pub fn decode_uri_component_js(input: &str) -> String {
crate::portable::decode_uri_component(input)
}
#[wasm_bindgen(js_name = parseQuery)]
pub fn parse_query_js(full_path: &str) -> String {
let (path, query) = crate::portable::parse_query(full_path);
serde_json::json!({ "path": path, "query": query }).to_string()
}
#[wasm_bindgen(js_name = buildUrl)]
pub fn build_url_js(path: &str, query_json: &str) -> Result<String, JsValue> {
let map: std::collections::BTreeMap<String, String> = serde_json::from_str(query_json)
.map_err(|e| structured_error("stateError", &format!("buildUrl: bad query JSON: {e}")))?;
Ok(crate::portable::build_url(path, &map))
}
#[wasm_bindgen(js_name = sessionStep)]
pub fn session_step_js(state_json: &str, event_json: &str) -> Result<String, JsValue> {
let state: crate::portable::SessionState = serde_json::from_str(state_json).map_err(|e| {
structured_error("stateError", &format!("sessionStep: bad state JSON: {e}"))
})?;
let event: crate::portable::SessionEvent = serde_json::from_str(event_json).map_err(|e| {
structured_error("stateError", &format!("sessionStep: bad event JSON: {e}"))
})?;
let effect = crate::portable::session_step(&state, &event);
serde_json::to_string(&effect)
.map_err(|e| structured_error("stateError", &format!("sessionStep: serialise: {e}")))
}