pub use dioxus_js_bindgen_macro::bind_js;
pub use serde;
pub use serde_json;
pub use tracing;
pub mod watcher_guard;
pub use watcher_guard::WatcherGuard;
use std::sync::atomic::{AtomicU64, Ordering};
use dioxus::prelude::*;
use serde::{Deserialize, Serialize};
use thiserror::Error;
#[derive(Debug, Error, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum JsError {
#[error("JavaScript Exception: {message}\nStack: {stack:?}")]
Exception {
message: String,
stack: Option<String>,
},
#[error("Transport Error: {0}")]
Transport(String),
#[error("Module Unavailable: '{0}' (Context may have been reset)")]
ModuleUnavailable(String),
#[error("Deserialization Error: {0}")]
Deserialization(String),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RpcResponse<T> {
pub ok: bool,
pub data: Option<T>,
pub error: Option<String>,
pub stack: Option<String>,
}
impl<T> RpcResponse<T> {
pub fn as_error(&self) -> Option<&str> {
if !self.ok {
self.error.as_deref()
} else {
None
}
}
pub fn into_result(self) -> Result<T, JsError> {
if self.ok {
self.data.ok_or_else(|| JsError::Transport("Missing data in success response".into()))
} else {
Err(JsError::Exception {
message: self.error.unwrap_or_else(|| "Unknown JS error".into()),
stack: self.stack,
})
}
}
}
pub fn clear_js_cache() {
internal::GLOBAL_EPOCH.fetch_add(1, Ordering::SeqCst);
let _ = std::panic::catch_unwind(|| {
let _ = dioxus::document::eval(
r#"
if (window.__DIOXUS_BINDGEN_MODULES__) {
window.__DIOXUS_BINDGEN_MODULES__ = {};
}
"#,
);
});
}
pub use clear_js_cache as reset_module_registry;
pub fn use_watcher<W: 'static>(mut factory: impl FnMut() -> Option<W> + 'static) {
let mut current_watcher = dioxus::prelude::use_signal(|| None::<W>);
dioxus::prelude::use_effect(move || {
let new_watcher = factory();
current_watcher.set(new_watcher);
});
}
#[doc(hidden)]
pub mod internal {
use super::*;
pub static GLOBAL_EPOCH: AtomicU64 = AtomicU64::new(1);
static NEXT_SUB_ID: AtomicU64 = AtomicU64::new(1);
#[inline]
pub fn current_epoch() -> u64 {
GLOBAL_EPOCH.load(Ordering::Acquire)
}
#[inline]
pub fn next_subscription_id() -> u64 {
NEXT_SUB_ID.fetch_add(1, Ordering::Relaxed)
}
pub fn dispatch_cleanup(sub_id: u64) {
if dioxus::core::Runtime::try_current().is_none() {
return;
}
let _ = std::panic::catch_unwind(|| {
let _ = dioxus::document::eval(&format!(
r#"
(function() {{
const c = window.__DIOXUS_WATCHERS?.get({sub_id});
if (c) {{
try {{ c(); }} catch(e) {{ console.error("[Watcher Cleanup Error]:", e); }}
window.__DIOXUS_WATCHERS.delete({sub_id});
}}
}})();
"#
));
});
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_rpc_response_success() {
let resp = RpcResponse {
ok: true,
data: Some(42),
error: None,
stack: None,
};
assert_eq!(resp.into_result().unwrap(), 42);
}
#[test]
fn test_rpc_response_error() {
let resp: RpcResponse<i32> = RpcResponse {
ok: false,
data: None,
error: Some("Element not found".into()),
stack: Some("stack trace".into()),
};
match resp.into_result() {
Err(JsError::Exception { message, stack }) => {
assert_eq!(message, "Element not found");
assert_eq!(stack, Some("stack trace".into()));
}
_ => panic!("Expected JsError::Exception"),
}
}
#[test]
fn test_epoch_increment() {
let initial = internal::current_epoch();
clear_js_cache();
assert_eq!(internal::current_epoch(), initial + 1);
reset_module_registry();
assert_eq!(internal::current_epoch(), initial + 2);
}
#[test]
fn test_subscription_id_increment() {
let id1 = internal::next_subscription_id();
let id2 = internal::next_subscription_id();
assert!(id2 > id1);
}
#[test]
fn test_js_error_display() {
let err = JsError::ModuleUnavailable("module_123".into());
assert!(err.to_string().contains("module_123"));
let err2 = JsError::Transport("failed to connect".into());
assert!(err2.to_string().contains("failed to connect"));
}
#[test]
fn test_rpc_as_error() {
let resp: RpcResponse<()> = RpcResponse {
ok: false,
data: None,
error: Some("MODULE_NOT_FOUND".into()),
stack: None,
};
assert_eq!(resp.as_error(), Some("MODULE_NOT_FOUND"));
let ok_resp: RpcResponse<i32> = RpcResponse {
ok: true,
data: Some(10),
error: None,
stack: None,
};
assert_eq!(ok_resp.as_error(), None);
}
#[test]
fn test_watcher_guard_lifecycle() {
let guard = WatcherGuard::new("watch_resize", 42, None);
assert_eq!(guard.name(), "watch_resize");
assert_eq!(guard.subscription_id(), 42);
let debug_str = format!("{:?}", guard);
assert!(debug_str.contains("watch_resize"));
assert!(debug_str.contains("42"));
drop(guard);
}
}