Skip to main content

oxirast_core/
lib.rs

1pub use wasm_bindgen;
2use std::any::{Any, TypeId};
3use std::cell::RefCell;
4use std::collections::HashMap;
5use std::rc::Rc;
6
7// ==========================================
8// 0. ISOMORPHIC ARCHITECTURE (Server vs Client)
9// ==========================================
10#[cfg(target_arch = "wasm32")]
11pub use web_sys;
12#[cfg(target_arch = "wasm32")]
13use wasm_bindgen::prelude::*;
14#[cfg(target_arch = "wasm32")]
15use wasm_bindgen::JsCast;
16#[cfg(target_arch = "wasm32")]
17use web_sys::{window, Document, Element, Event, Node, HtmlElement};
18#[cfg(target_arch = "wasm32")]
19use std::cell::Cell;
20#[cfg(target_arch = "wasm32")]
21use serde::de::DeserializeOwned;
22
23// Dummy types for the Server so the VNode struct compiles universally
24#[cfg(not(target_arch = "wasm32"))]
25#[derive(Clone)]
26pub struct Event;
27
28// THE FIX: Dummy JsValue so the server compiler doesn't panic
29#[cfg(not(target_arch = "wasm32"))]
30pub struct JsValue; 
31
32#[cfg(not(target_arch = "wasm32"))]
33pub struct Closure<T: ?Sized> { _marker: std::marker::PhantomData<T> }
34
35#[cfg(not(target_arch = "wasm32"))]
36impl<T: ?Sized> Closure<T> { 
37    pub fn wrap(_: Box<T>) -> Self { Self { _marker: std::marker::PhantomData } } 
38    pub fn as_ref(&self) -> &JsValue { unimplemented!() }
39    pub fn forget(self) {} 
40}
41
42
43// ==========================================
44// 1. ERROR BOUNDARIES (Client Only)
45// ==========================================
46#[cfg(target_arch = "wasm32")]
47pub fn init_error_boundary() {
48    std::panic::set_hook(Box::new(|panic_info| {
49        let msg = if let Some(s) = panic_info.payload().downcast_ref::<&str>() { *s } 
50        else if let Some(s) = panic_info.payload().downcast_ref::<String>() { s.as_str() } 
51        else { "Unknown Rust Panic inside WebAssembly" };
52        let location = panic_info.location().map(|l| format!("{}:{}", l.file(), l.line())).unwrap_or_else(|| String::from("Unknown location"));
53        let doc = window().unwrap().document().unwrap();
54        let body = doc.body().unwrap();
55        let overlay = doc.create_element("div").unwrap();
56        overlay.set_attribute("style", "position:fixed;top:0;left:0;width:100vw;height:100vh;background:rgba(30,0,0,0.95);color:#ff5555;padding:3rem;z-index:99999;font-family:monospace;").unwrap();
57        overlay.set_inner_html(&format!(r#"<h1>⚠️ Oxirast Crash Report</h1><div style="background:#220000;border:1px solid #ff0000;padding:1.5rem;"><h3>Error:</h3><p>{}</p><hr/><h3>Location:</h3><p>{}</p></div>"#, msg, location));
58        body.append_child(&overlay).unwrap();
59        web_sys::console::error_1(&format!("Oxirast Panic: {} at {}", msg, location).into());
60    }));
61}
62
63// ==========================================
64// 2. ISOMORPHIC VDOM STRUCTURES
65// ==========================================
66pub type EventCallback = Rc<RefCell<Box<dyn FnMut(Event)>>>;
67pub type LifecycleCallback = Rc<RefCell<Box<dyn FnMut()>>>;
68
69#[derive(Clone)]
70pub enum VNode { Element(VElement), Text(String) }
71
72#[derive(Clone)]
73pub struct VElement {
74    pub tag: String,
75    pub attrs: HashMap<String, String>,
76    pub bound_attrs: HashMap<String, Signal<String>>, 
77    pub children: Vec<VNode>,
78    pub events: HashMap<String, EventCallback>,
79    pub bound_text: Option<Signal<String>>, 
80    pub bound_show: Option<Signal<bool>>, 
81    pub on_mount: Option<LifecycleCallback>,
82    pub on_cleanup: Option<LifecycleCallback>,
83}
84
85impl VNode {
86    pub fn element(tag: &str) -> VElement { 
87        VElement { tag: tag.to_string(), attrs: HashMap::new(), bound_attrs: HashMap::new(), children: Vec::new(), events: HashMap::new(), bound_text: None, bound_show: None, on_mount: None, on_cleanup: None } 
88    }
89    pub fn text(s: &str) -> VNode { VNode::Text(s.to_string()) }
90}
91
92impl VElement {
93    pub fn attr(mut self, key: &str, val: &str) -> Self { self.attrs.insert(key.to_string(), val.to_string()); self }
94    pub fn bind_attr(mut self, key: &str, sig: Signal<String>) -> Self { self.bound_attrs.insert(key.to_string(), sig); self }
95    pub fn on(mut self, event: &str, cb: EventCallback) -> Self { self.events.insert(event.to_string(), cb); self }
96    pub fn child(mut self, node: VNode) -> Self { self.children.push(node); self }
97    pub fn bind_text(mut self, sig: Signal<String>) -> Self { self.bound_text = Some(sig); self } 
98    pub fn bind_show(mut self, sig: Signal<bool>) -> Self { self.bound_show = Some(sig); self } 
99    pub fn on_mount(mut self, cb: LifecycleCallback) -> Self { self.on_mount = Some(cb); self }
100    pub fn on_cleanup(mut self, cb: LifecycleCallback) -> Self { self.on_cleanup = Some(cb); self }
101    pub fn build(self) -> VNode { VNode::Element(self) }
102}
103
104// ==========================================
105// 3. SSR HTML GENERATOR (Runs Everywhere)
106// ==========================================
107pub fn render_to_string(vnode: &VNode) -> String {
108    match vnode {
109        VNode::Text(text) => text.clone(),
110        VNode::Element(el) => {
111            let mut attrs = String::new();
112            for (k, v) in &el.attrs { attrs.push_str(&format!(" {}=\"{}\"", k, v)); }
113            for (k, sig) in &el.bound_attrs { attrs.push_str(&format!(" {}=\"{}\"", k, sig.get())); }
114            if let Some(sig) = &el.bound_show { if !sig.get() { attrs.push_str(" style=\"display:none;\""); } }
115            
116            let mut children = String::new();
117            if let Some(sig) = &el.bound_text { children.push_str(&sig.get()); } 
118            else { for child in &el.children { children.push_str(&render_to_string(child)); } }
119            format!("<{}{}>{}</{}>", el.tag, attrs, children, el.tag)
120        }
121    }
122}
123
124// ==========================================
125// 4. CLIENT BROWSER DOM ENGINE (WASM ONLY)
126// ==========================================
127#[cfg(target_arch = "wasm32")]
128thread_local! {
129    static NEXT_ID: Cell<usize> = Cell::new(1);
130    static EVENT_REGISTRY: RefCell<HashMap<String, Vec<Closure<dyn FnMut(Event)>>>> = RefCell::new(HashMap::new());
131    static CLEANUP_REGISTRY: RefCell<HashMap<String, LifecycleCallback>> = RefCell::new(HashMap::new());
132}
133
134#[cfg(target_arch = "wasm32")]
135pub fn document() -> Document { window().expect("No window").document().expect("No document") }
136#[cfg(target_arch = "wasm32")]
137pub fn mount_to_body(element: &Node) { document().body().unwrap().append_child(element).unwrap(); }
138
139#[cfg(target_arch = "wasm32")]
140pub fn render_vnode(vnode: &VNode) -> Node {
141    let doc = document();
142    match vnode {
143        VNode::Text(text) => doc.create_text_node(text).into(),
144        VNode::Element(vel) => {
145            let el = doc.create_element(&vel.tag).unwrap();
146            for (k, v) in &vel.attrs { el.set_attribute(k, v).ok(); }
147            for (k, sig) in &vel.bound_attrs { el.set_attribute(k, &sig.get()).ok(); let el_c = el.clone(); let k_c = k.clone(); sig.subscribe(move |v| { el_c.set_attribute(&k_c, v).ok(); }); }
148            if let Some(sig) = &vel.bound_text { el.set_text_content(Some(&sig.get())); let el_c = el.clone(); sig.subscribe(move |v| { el_c.set_text_content(Some(v)); }); }
149            if let Some(sig) = &vel.bound_show {
150                let html_el = el.clone().dyn_into::<HtmlElement>().unwrap(); html_el.style().set_property("display", if sig.get() { "" } else { "none" }).unwrap();
151                let el_c = html_el.clone(); sig.subscribe(move |v| { el_c.style().set_property("display", if *v { "" } else { "none" }).unwrap(); });
152            }
153
154            let needs_id = !vel.events.is_empty() || vel.on_cleanup.is_some();
155            let mut id = String::new();
156            if needs_id { id = NEXT_ID.with(|n| { let val = n.get(); n.set(val + 1); val.to_string() }); el.set_attribute("data-ox-id", &id).unwrap(); }
157
158            if !vel.events.is_empty() {
159                let mut closures = Vec::new();
160                for (name, cb) in &vel.events {
161                    let cb_c = cb.clone(); let closure = Closure::wrap(Box::new(move |e: Event| { if let Ok(mut f) = cb_c.try_borrow_mut() { f(e); } }) as Box<dyn FnMut(_)>);
162                    el.add_event_listener_with_callback(name, closure.as_ref().unchecked_ref()).unwrap(); closures.push(closure); 
163                }
164                EVENT_REGISTRY.with(|reg| reg.borrow_mut().insert(id.clone(), closures)); 
165            }
166            if let Some(m_cb) = &vel.on_mount { if let Ok(mut f) = m_cb.try_borrow_mut() { f(); } }
167            if let Some(c_cb) = &vel.on_cleanup { CLEANUP_REGISTRY.with(|reg| reg.borrow_mut().insert(id.clone(), c_cb.clone())); }
168            for child in &vel.children { el.append_child(&render_vnode(child)).ok(); }
169            el.into()
170        }
171    }
172}
173
174#[cfg(target_arch = "wasm32")]
175pub fn hydrate_dom(dom_node: &Node, vnode: &VNode) {
176    match vnode {
177        VNode::Element(vel) => {
178            if let Ok(el) = dom_node.clone().dyn_into::<Element>() {
179                let needs_id = !vel.events.is_empty() || vel.on_cleanup.is_some();
180                let mut id = String::new();
181                if needs_id { id = NEXT_ID.with(|n| { let val = n.get(); n.set(val + 1); val.to_string() }); let _ = el.set_attribute("data-ox-id", &id); }
182
183                for (k, sig) in &vel.bound_attrs { let el_c = el.clone(); let k_c = k.clone(); sig.subscribe(move |v| { el_c.set_attribute(&k_c, v).ok(); }); }
184                if let Some(sig) = &vel.bound_text { let el_c = el.clone(); sig.subscribe(move |v| { el_c.set_text_content(Some(v)); }); }
185                if let Some(sig) = &vel.bound_show { if let Ok(html_el) = el.clone().dyn_into::<HtmlElement>() { let el_c = html_el.clone(); sig.subscribe(move |v| { el_c.style().set_property("display", if *v { "" } else { "none" }).unwrap(); }); } }
186
187                if !vel.events.is_empty() {
188                    let mut closures = Vec::new();
189                    for (name, cb) in &vel.events {
190                        let cb_c = cb.clone(); let closure = Closure::wrap(Box::new(move |e: Event| { if let Ok(mut f) = cb_c.try_borrow_mut() { f(e); } }) as Box<dyn FnMut(_)>);
191                        el.add_event_listener_with_callback(name, closure.as_ref().unchecked_ref()).unwrap(); closures.push(closure);
192                    }
193                    EVENT_REGISTRY.with(|reg| reg.borrow_mut().insert(id.clone(), closures));
194                }
195                if let Some(m_cb) = &vel.on_mount { if let Ok(mut f) = m_cb.try_borrow_mut() { f(); } }
196                if let Some(c_cb) = &vel.on_cleanup { CLEANUP_REGISTRY.with(|reg| reg.borrow_mut().insert(id.clone(), c_cb.clone())); }
197
198                let child_nodes = el.child_nodes();
199                for i in 0..vel.children.len() { if let Some(child_dom) = child_nodes.item(i as u32) { hydrate_dom(&child_dom, &vel.children[i]); } }
200            }
201        }
202        VNode::Text(_) => {} 
203    }
204}
205
206#[cfg(target_arch = "wasm32")]
207pub fn cleanup_node(node: &Node) {
208    if let Ok(el) = node.clone().dyn_into::<Element>() {
209        if let Some(id) = el.get_attribute("data-ox-id") { 
210            EVENT_REGISTRY.with(|reg| { reg.borrow_mut().remove(&id); }); 
211            CLEANUP_REGISTRY.with(|reg| { if let Some(c_cb) = reg.borrow_mut().remove(&id) { if let Ok(mut f) = c_cb.try_borrow_mut() { f(); } } });
212        }
213        let children = el.child_nodes();
214        for i in 0..children.length() { if let Some(child) = children.item(i) { cleanup_node(&child); } }
215    }
216}
217
218// ==========================================
219// 5. REACTIVITY (Runs Everywhere)
220// ==========================================
221#[derive(Clone)]
222pub struct Signal<T> {
223    value: Rc<RefCell<T>>,
224    listeners: Rc<RefCell<Vec<Box<dyn FnMut(&T)>>>>,
225}
226
227impl<T: Clone + 'static> Signal<T> {
228    pub fn new(initial_value: T) -> Self { Self { value: Rc::new(RefCell::new(initial_value)), listeners: Rc::new(RefCell::new(Vec::new())) } }
229    pub fn get(&self) -> T { self.value.borrow().clone() }
230    pub fn set(&self, new_value: T) { *self.value.borrow_mut() = new_value.clone(); for listener in self.listeners.borrow_mut().iter_mut() { listener(&new_value); } }
231    pub fn subscribe<F>(&self, callback: F) where F: FnMut(&T) + 'static { self.listeners.borrow_mut().push(Box::new(callback)); }
232}
233pub fn use_state<T: Clone + 'static>(initial: T) -> Signal<T> { Signal::new(initial) }
234pub fn use_memo<T: Clone + 'static, U: Clone + 'static, F: Fn(&T) -> U + 'static>(dep: &Signal<T>, calc: F) -> Signal<U> {
235    let memo_sig = use_state(calc(&dep.get())); let memo_clone = memo_sig.clone(); dep.subscribe(move |v| { memo_clone.set(calc(v)); }); memo_sig
236}
237pub fn use_effect<T: Clone + 'static, F: FnMut(&T) + 'static>(dep: &Signal<T>, mut effect: F) { effect(&dep.get()); dep.subscribe(effect); }
238
239thread_local! { static GLOBAL_CONTEXT: RefCell<HashMap<TypeId, Rc<dyn Any>>> = RefCell::new(HashMap::new()); }
240pub fn provide_context<T: 'static>(value: T) { GLOBAL_CONTEXT.with(|ctx| { ctx.borrow_mut().insert(TypeId::of::<T>(), Rc::new(value)); }); }
241pub fn use_context<T: Clone + 'static>() -> Option<T> { GLOBAL_CONTEXT.with(|ctx| { ctx.borrow().get(&TypeId::of::<T>()).and_then(|rc| rc.downcast_ref::<T>().cloned()) }) }
242
243
244// ==========================================
245// 6. ROUTER & SSR (Isomorphic)
246// ==========================================
247thread_local! { static ROUTE_PARAMS: RefCell<HashMap<String, String>> = RefCell::new(HashMap::new()); }
248pub fn use_params() -> HashMap<String, String> { ROUTE_PARAMS.with(|p| p.borrow().clone()) }
249
250#[cfg(target_arch = "wasm32")]
251pub fn use_query() -> HashMap<String, String> {
252    let search = window().unwrap().location().search().unwrap_or_default();
253    let mut query = HashMap::new();
254    if search.starts_with('?') {
255        for pair in search[1..].split('&') {
256            if pair.is_empty() { continue; }
257            let mut parts = pair.splitn(2, '=');
258            query.insert(parts.next().unwrap_or_default().to_string(), parts.next().unwrap_or_default().to_string());
259        }
260    }
261    query
262}
263
264#[cfg(target_arch = "wasm32")]
265pub fn current_path() -> String { window().expect("no window").location().pathname().unwrap_or_else(|_| String::from("/")) }
266#[cfg(target_arch = "wasm32")]
267pub fn push_route(path: &str) { window().unwrap().history().unwrap().push_state_with_url(&JsValue::NULL, "", Some(path)).unwrap(); }
268
269#[derive(Clone)]
270pub struct RouteInfo {
271    pub component: fn(Signal<String>) -> VNode,
272    pub guard: Option<(fn() -> bool, String)>, 
273}
274
275pub struct Router {
276    root_id: String,
277    routes: HashMap<String, RouteInfo>, 
278}
279
280impl Router {
281    pub fn new(root_id: &str) -> Self { Self { root_id: root_id.to_string(), routes: HashMap::new() } }
282    
283    pub fn route(mut self, path: &str, component: fn(Signal<String>) -> VNode) -> Self { 
284        self.routes.insert(path.to_string(), RouteInfo { component, guard: None }); self 
285    }
286    pub fn guarded_route(mut self, path: &str, component: fn(Signal<String>) -> VNode, guard: fn() -> bool, redirect: &str) -> Self {
287        self.routes.insert(path.to_string(), RouteInfo { component, guard: Some((guard, redirect.to_string())) }); self
288    }
289
290    // --- NEW: SERVER-SIDE RENDERING HOOK ---
291    // Backend Axum servers call this to instantly generate the requested HTML page!
292    pub fn render_route_to_string(&self, request_path: &str) -> String {
293        let dummy_nav = use_state(request_path.to_string());
294        for (route_path, info) in self.routes.iter() {
295            if route_path == request_path {
296                let vnode = (info.component)(dummy_nav);
297                return render_to_string(&vnode);
298            }
299        }
300        String::from("<h1>404 Not Found</h1>")
301    }
302    
303    // Client-side Browser Boot
304    #[cfg(target_arch = "wasm32")]
305    pub fn start(self) {
306        init_error_boundary();
307        let route_signal = use_state(current_path());
308        let routes = self.routes.clone();
309        let root_id = self.root_id.clone();
310        let nav_signal = route_signal.clone();
311        let is_initial_load = Rc::new(std::cell::Cell::new(true));
312
313        route_signal.subscribe(move |new_path| {
314            if current_path() != *new_path { push_route(new_path); }
315            let root = document().get_element_by_id(&root_id).expect("Root div not found!");
316            
317            let mut matched_route = None;
318            let mut extracted_params = HashMap::new();
319
320            for (route_path, info) in routes.iter() {
321                let route_segments: Vec<&str> = route_path.split('/').collect();
322                let url_segments: Vec<&str> = new_path.split('/').collect();
323                let mut is_match = true;
324                let mut local_params = HashMap::new();
325
326                for (i, rs) in route_segments.iter().enumerate() {
327                    if *rs == "*" { local_params.insert("wildcard".to_string(), url_segments[i..].join("/")); break; }
328                    if i >= url_segments.len() { is_match = false; break; }
329                    if rs.starts_with(':') { local_params.insert(rs[1..].to_string(), url_segments[i].to_string()); } 
330                    else if *rs != url_segments[i] { is_match = false; break; }
331                }
332                if is_match && (route_segments.last() == Some(&"*") || route_segments.len() == url_segments.len()) {
333                    matched_route = Some(info); extracted_params = local_params; break;
334                }
335            }
336
337            ROUTE_PARAMS.with(|p| { *p.borrow_mut() = extracted_params; });
338
339            let page_vnode = match matched_route {
340                Some(info) => {
341                    if let Some((guard_fn, redirect)) = &info.guard { if !guard_fn() { nav_signal.set(redirect.clone()); return; } }
342                    (info.component)(nav_signal.clone())
343                },
344                None => VNode::element("h1").child(VNode::text("404 Not Found")).build()
345            };
346
347            if is_initial_load.get() {
348                is_initial_load.set(false); 
349                if root.child_nodes().length() > 0 { if let Some(first) = root.child_nodes().item(0) { hydrate_dom(&first, &page_vnode); } } 
350                else { cleanup_node(&root); root.set_inner_html(""); root.append_child(&render_vnode(&page_vnode)).unwrap(); }
351            } else { cleanup_node(&root); root.set_inner_html(""); root.append_child(&render_vnode(&page_vnode)).unwrap(); }
352        });
353
354        let nav_signal_for_pop = route_signal.clone();
355        let closure = Closure::wrap(Box::new(move |_e: JsValue| { nav_signal_for_pop.set(current_path()); }) as Box<dyn FnMut(JsValue)>);
356        window().unwrap().add_event_listener_with_callback("popstate", closure.as_ref().unchecked_ref()).unwrap();
357        closure.forget();
358        route_signal.set(current_path());
359    }
360}
361
362// ==========================================
363// 7. ASYNC FETCH (Client Only for now)
364// ==========================================
365#[cfg(target_arch = "wasm32")]
366pub fn use_fetch<T>(url: &str) -> (Signal<Option<T>>, Signal<bool>, Signal<Option<String>>)
367where T: DeserializeOwned + Clone + 'static, {
368    let data: Signal<Option<T>> = use_state(None);
369    let is_loading = use_state(true);
370    let error: Signal<Option<String>> = use_state(None);
371
372    let data_c = data.clone(); let loading_c = is_loading.clone(); let error_c = error.clone(); let url_s = url.to_string();
373
374    wasm_bindgen_futures::spawn_local(async move {
375        match reqwest::get(&url_s).await {
376            Ok(resp) => match resp.json::<T>().await { Ok(json) => data_c.set(Some(json)), Err(_) => error_c.set(Some("Parse Error".to_string())), },
377            Err(e) => error_c.set(Some(e.to_string())),
378        }
379        loading_c.set(false);
380    });
381    (data, is_loading, error)
382}
383
384
385// ==========================================
386// 8. WEB3 WALLET ENGINE (The Magic Bridge)
387// ==========================================
388#[cfg(target_arch = "wasm32")]
389#[wasm_bindgen(inline_js = "
390    export async function connect_evm() {
391        if (window.ethereum) {
392            const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
393            return accounts[0];
394        }
395        throw new Error('MetaMask not found');
396    }
397    export async function connect_sol() {
398        if (window.solana && window.solana.isPhantom) {
399            const resp = await window.solana.connect();
400            return resp.publicKey.toString();
401        }
402        throw new Error('Phantom wallet not found');
403    }
404")]
405extern "C" {
406    #[wasm_bindgen(catch)]
407    async fn connect_evm() -> Result<JsValue, JsValue>;
408    #[wasm_bindgen(catch)]
409    async fn connect_sol() -> Result<JsValue, JsValue>;
410}
411
412#[cfg(target_arch = "wasm32")]
413pub fn use_wallet(chain: &str) -> (Signal<Option<String>>, Signal<bool>, Rc<dyn Fn()>) {
414    let address: Signal<Option<String>> = use_state(None);
415    let is_connecting = use_state(false);
416    
417    let addr_clone = address.clone();
418    let loading_clone = is_connecting.clone();
419    let chain_type = chain.to_string();
420
421    let connect_fn = Rc::new(move || {
422        let addr_c = addr_clone.clone();
423        let load_c = loading_clone.clone();
424        let ct = chain_type.clone();
425        
426        load_c.set(true);
427        
428        wasm_bindgen_futures::spawn_local(async move {
429            let result = if ct == "solana" { connect_sol().await } else { connect_evm().await };
430            match result {
431                Ok(val) => if let Some(s) = val.as_string() { addr_c.set(Some(s)); },
432                Err(e) => web_sys::console::error_1(&e),
433            }
434            load_c.set(false);
435        });
436    });
437
438    (address, is_connecting, connect_fn)
439}