1pub use dioxus_js_bindgen_macro::bind_js;
6pub use serde;
7pub use serde_json;
8pub use tracing;
9
10pub mod watcher_guard;
11pub use watcher_guard::WatcherGuard;
12
13use std::sync::atomic::{AtomicU64, Ordering};
14use dioxus::prelude::*;
15use serde::{Deserialize, Serialize};
16use thiserror::Error;
17
18#[derive(Debug, Error, Clone, Serialize, Deserialize, PartialEq, Eq)]
20pub enum JsError {
21 #[error("JavaScript Exception: {message}\nStack: {stack:?}")]
22 Exception {
23 message: String,
24 stack: Option<String>,
25 },
26 #[error("Transport Error: {0}")]
27 Transport(String),
28 #[error("Module Unavailable: '{0}' (Context may have been reset)")]
29 ModuleUnavailable(String),
30 #[error("Deserialization Error: {0}")]
31 Deserialization(String),
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct RpcResponse<T> {
37 pub ok: bool,
38 pub data: Option<T>,
39 pub error: Option<String>,
40 pub stack: Option<String>,
41}
42
43impl<T> RpcResponse<T> {
44 pub fn as_error(&self) -> Option<&str> {
45 if !self.ok {
46 self.error.as_deref()
47 } else {
48 None
49 }
50 }
51
52 pub fn into_result(self) -> Result<T, JsError> {
53 if self.ok {
54 self.data.ok_or_else(|| JsError::Transport("Missing data in success response".into()))
55 } else {
56 Err(JsError::Exception {
57 message: self.error.unwrap_or_else(|| "Unknown JS error".into()),
58 stack: self.stack,
59 })
60 }
61 }
62}
63
64pub fn clear_js_cache() {
69 internal::GLOBAL_EPOCH.fetch_add(1, Ordering::SeqCst);
70 let _ = std::panic::catch_unwind(|| {
71 let _ = dioxus::document::eval(
72 r#"
73 if (window.__DIOXUS_BINDGEN_MODULES__) {
74 window.__DIOXUS_BINDGEN_MODULES__ = {};
75 }
76 "#,
77 );
78 });
79}
80
81pub use clear_js_cache as reset_module_registry;
83
84pub fn use_watcher<W: 'static>(mut factory: impl FnMut() -> Option<W> + 'static) {
86 let mut current_watcher = dioxus::prelude::use_signal(|| None::<W>);
87
88 dioxus::prelude::use_effect(move || {
89 let new_watcher = factory();
90 current_watcher.set(new_watcher);
91 });
92}
93
94#[doc(hidden)]
95pub mod internal {
96 use super::*;
97
98 pub static GLOBAL_EPOCH: AtomicU64 = AtomicU64::new(1);
99 static NEXT_SUB_ID: AtomicU64 = AtomicU64::new(1);
100
101 #[inline]
102 pub fn current_epoch() -> u64 {
103 GLOBAL_EPOCH.load(Ordering::Acquire)
104 }
105
106 #[inline]
107 pub fn next_subscription_id() -> u64 {
108 NEXT_SUB_ID.fetch_add(1, Ordering::Relaxed)
109 }
110
111 pub fn dispatch_cleanup(sub_id: u64) {
112 if dioxus::core::Runtime::try_current().is_none() {
113 return;
114 }
115 let _ = std::panic::catch_unwind(|| {
116 let _ = dioxus::document::eval(&format!(
117 r#"
118 (function() {{
119 const c = window.__DIOXUS_WATCHERS?.get({sub_id});
120 if (c) {{
121 try {{ c(); }} catch(e) {{ console.error("[Watcher Cleanup Error]:", e); }}
122 window.__DIOXUS_WATCHERS.delete({sub_id});
123 }}
124 }})();
125 "#
126 ));
127 });
128 }
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134
135 #[test]
136 fn test_rpc_response_success() {
137 let resp = RpcResponse {
138 ok: true,
139 data: Some(42),
140 error: None,
141 stack: None,
142 };
143 assert_eq!(resp.into_result().unwrap(), 42);
144 }
145
146 #[test]
147 fn test_rpc_response_error() {
148 let resp: RpcResponse<i32> = RpcResponse {
149 ok: false,
150 data: None,
151 error: Some("Element not found".into()),
152 stack: Some("stack trace".into()),
153 };
154 match resp.into_result() {
155 Err(JsError::Exception { message, stack }) => {
156 assert_eq!(message, "Element not found");
157 assert_eq!(stack, Some("stack trace".into()));
158 }
159 _ => panic!("Expected JsError::Exception"),
160 }
161 }
162
163 #[test]
164 fn test_epoch_increment() {
165 let initial = internal::current_epoch();
166 clear_js_cache();
167 assert_eq!(internal::current_epoch(), initial + 1);
168
169 reset_module_registry();
171 assert_eq!(internal::current_epoch(), initial + 2);
172 }
173
174 #[test]
175 fn test_subscription_id_increment() {
176 let id1 = internal::next_subscription_id();
177 let id2 = internal::next_subscription_id();
178 assert!(id2 > id1);
179 }
180
181 #[test]
182 fn test_js_error_display() {
183 let err = JsError::ModuleUnavailable("module_123".into());
184 assert!(err.to_string().contains("module_123"));
185
186 let err2 = JsError::Transport("failed to connect".into());
187 assert!(err2.to_string().contains("failed to connect"));
188 }
189
190 #[test]
191 fn test_rpc_as_error() {
192 let resp: RpcResponse<()> = RpcResponse {
193 ok: false,
194 data: None,
195 error: Some("MODULE_NOT_FOUND".into()),
196 stack: None,
197 };
198 assert_eq!(resp.as_error(), Some("MODULE_NOT_FOUND"));
199
200 let ok_resp: RpcResponse<i32> = RpcResponse {
201 ok: true,
202 data: Some(10),
203 error: None,
204 stack: None,
205 };
206 assert_eq!(ok_resp.as_error(), None);
207 }
208
209 #[test]
210 fn test_watcher_guard_lifecycle() {
211 let guard = WatcherGuard::new("watch_resize", 42, None);
212 assert_eq!(guard.name(), "watch_resize");
213 assert_eq!(guard.subscription_id(), 42);
214 let debug_str = format!("{:?}", guard);
215 assert!(debug_str.contains("watch_resize"));
216 assert!(debug_str.contains("42"));
217 drop(guard);
219 }
220}