dioxus_use_js/
lib.rs

1#![cfg_attr(docsrs, feature(doc_cfg))]
2#![doc = include_str!("../README.md")]
3
4use std::ops::{Deref, DerefMut};
5use std::{error::Error, fmt::Display, sync::Arc};
6
7#[cfg(feature = "build")]
8mod build;
9#[cfg(feature = "build")]
10pub use build::*;
11
12use dioxus::document::{self, Eval};
13pub use dioxus_use_js_macro::use_js;
14
15// We export these so downstreams don't need `serde` or `serde_json` directly
16// exports used by macro.
17#[doc(hidden)]
18pub use serde::Serialize as SerdeSerialize;
19#[doc(hidden)]
20pub use serde::de::DeserializeOwned as SerdeDeDeserializeOwned;
21#[doc(hidden)]
22pub use serde::de::Error as SerdeDeError;
23#[doc(hidden)]
24pub use serde_json::Error as SerdeJsonError;
25#[doc(hidden)]
26pub use serde_json::Value as SerdeJsonValue;
27#[doc(hidden)]
28pub use serde_json::from_value as serde_json_from_value;
29#[doc(hidden)]
30pub const __CALLBACK_SEND_VALIDATION_MSG: &str =
31    "Callbacks should always send back a value that is an array of three.";
32#[doc(hidden)]
33pub const __RESULT_SEND_VALIDATION_MSG: &str =
34    "Result should always send back a value that is an array of two.";
35#[doc(hidden)]
36pub const __INDEX_VALIDATION_MSG: &str = "The index value was an unexpected type";
37#[doc(hidden)]
38pub const __BAD_CALL_MSG: &str = "Should only attempt to call known actions.";
39#[doc(hidden)]
40pub const __BAD_VOID_RETURN: &str =
41    "A function that should return no value instead returned a value";
42#[doc(hidden)]
43pub const __UNEXPECTED_CALLBACK_TYPE: &str = "The callback was called with the wrong type";
44// We do not export this so the dioxus version doing the eval is the same, otherwise it may compile but using two different versions of dioxus at runtime will likely cause a runtime error
45// be two different versions of dioxus in the graph
46// pub use dioxus::document::eval as dioxus_document_eval;
47// pub use dioxus::document::EvalError as DioxusEvalError;
48
49//************************************************************************//
50
51fn _send_sync_error_assert() {
52    fn is_send<T: Send>(_: &T) {}
53    fn is_sync<T: Sync>(_: &T) {}
54    fn is_error<T: Error>(_: &T) {}
55
56    let o: JsError = JsError::Threw { func: "" };
57    is_send(&o);
58    is_sync(&o);
59    is_error(&o);
60}
61
62/// An error related to the execution of a javascript operation
63#[derive(Debug)]
64pub enum JsError {
65    /// Error occurred during dioxus evalution.
66    /// If this occurs, it usually mean your js is not valid or the wrong type was returned from
67    /// your js function
68    Eval {
69        /// The name of the js function
70        func: &'static str,
71        error: dioxus::document::EvalError,
72    },
73    /// A js function that threw a value during execution. The actual error value is logged on the js side as a warning.
74    Threw {
75        /// Name of the js function
76        func: &'static str,
77    },
78}
79
80impl std::fmt::Display for JsError {
81    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82        match self {
83            JsError::Eval { func: name, error } => {
84                write!(f, "JavaScript function '{}' eval error: {}", name, error)
85            }
86            JsError::Threw { func: name } => {
87                write!(
88                    f,
89                    "JavaScript function '{}' threw an error during execution",
90                    name
91                )
92            }
93        }
94    }
95}
96
97impl std::error::Error for JsError {}
98
99//************************************************************************//
100
101/// A reference to a javascript value that can be held on the dioxus side and passed to functions generated
102/// by this crate.
103///
104/// An instance of this is created or used by e.g.
105/// ```rust,ignore
106/// dioxus_use_js::use_js!("ts/example.ts", "assets/example.js"::usingJsValue);
107/// ```
108/// Where `"ts/example.ts"` uses this marker type
109/// ```ts
110/// type JsValue<T = any> = T;
111/// ```
112///
113/// This uses `Arc` internally and the value on the js side is destroyed when the last reference is dropped
114// Dev Note: No `serde::Serialize` or `serde::Deserialize` on purpose since the value is destroyed when dropped
115#[derive(
116    serde::Serialize, serde::Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash,
117)]
118pub struct JsValue(Arc<JsValueInner>);
119
120/// Abstraction used to implement the one time drop
121#[derive(
122    serde::Serialize, serde::Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash,
123)]
124struct JsValueInner(String);
125
126impl JsValue {
127    #[deprecated(note = "This constructor is for internal use only. Do not use directly.")]
128    #[doc(hidden)]
129    pub fn internal_create(id: String) -> Self {
130        Self(Arc::new(JsValueInner(id)))
131    }
132
133    #[deprecated(note = "This is for internal use only. Do not use directly.")]
134    #[doc(hidden)]
135    pub fn internal_get(&self) -> &str {
136        self.0.0.as_str()
137    }
138}
139
140impl Display for JsValue {
141    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
142        write!(f, "Value in js: window[\"{}\"]", self.0.0)
143    }
144}
145
146impl Drop for JsValueInner {
147    fn drop(&mut self) {
148        let object_name = std::mem::take(&mut self.0);
149        // work around for no async drop trait
150        dioxus::core::spawn_forever(async move {
151            let eval =
152                dioxus::document::eval(&format!("delete window[\"{object_name}\"];return null;"));
153            if let Err(error) = eval.await {
154                dioxus::logger::tracing::error!(
155                    "Failed to clean up JavaScript object `window[\"{object_name}\"]`. Error: {error}"
156                );
157            } else {
158                dioxus::logger::tracing::trace!(
159                    "Cleaned up JavaScript object `window[\"{object_name}\"]`."
160                );
161            }
162        });
163    }
164}
165
166/// When a normal Eval drops. It does not signal to the channel that it has been dropped. Thus any `await dioxus.recv()`
167/// will be awaiting forever. Thus we only use one `await dioxus.recv()` after all the parameters have been sent to
168/// signal drop. This struct will send the value to resolve that promise on drop.
169#[doc(hidden)]
170pub struct EvalDrop(document::Eval);
171
172impl EvalDrop {
173    /// Create a new EvalDrop from a dioxus eval
174    pub fn new(eval: document::Eval) -> Self {
175        Self(eval)
176    }
177}
178
179impl Deref for EvalDrop {
180    type Target = document::Eval;
181
182    fn deref(&self) -> &Self::Target {
183        &self.0
184    }
185}
186
187impl DerefMut for EvalDrop {
188    fn deref_mut(&mut self) -> &mut Self::Target {
189        &mut self.0
190    }
191}
192
193impl Drop for EvalDrop {
194    fn drop(&mut self) {
195        if let Err(e) = self.0.send(serde_json::Value::Null) {
196            dioxus::logger::tracing::error!("Failed to notify about Eval drop: {}", e);
197        }
198    }
199}
200
201/// Used in generated code.
202#[doc(hidden)]
203#[derive(Clone)]
204pub struct CallbackResponder(Arc<CallbackResponderInner>);
205
206struct CallbackResponderInner(Eval);
207
208impl CallbackResponder {
209    pub fn new(invocation_id: &str) -> Self {
210        // r = [id, ok, data], i = id, o = ok, d = data, f = window[function_id], x = f[id] = [resolve, reject]
211        CallbackResponder(Arc::new(CallbackResponderInner(dioxus::document::eval(&format!(
212            "while(true){{let r=await dioxus.recv();if(!Array.isArray(r))break;let f=window[\"{invocation_id}\"];if(f==null)break;let i=r[0],o=r[1],d=r[2],x=f[i];delete f[i];if(o)x[0](d);else x[1](d);}}",
213        )))))
214    }
215
216    pub fn respond<T: serde::Serialize>(&self, request_id: u64, is_ok: bool, data: T) {
217        let payload = (request_id, is_ok, data);
218
219        let result = self.0.0.send(payload);
220        if let Err(e) = result {
221            dioxus::logger::tracing::error!(
222                "Failed to send callback response for invocation '{}' request '{}': {}",
223                request_id,
224                is_ok,
225                e
226            );
227        }
228    }
229}
230
231impl Drop for CallbackResponderInner {
232    fn drop(&mut self) {
233        // Send a non-array value to break the loop
234        let result = self.0.send(serde_json::Value::Null);
235        if let Err(e) = result {
236            dioxus::logger::tracing::error!("Failed to shut down callback responder: {}", e);
237        }
238    }
239}
240
241// pub struct JsNotifyOnce(Arc<JsNotifyOnceInner>);
242
243// impl JsNotifyOnce {
244//     pub fn new(notify_on_drop: bool) -> Self {
245//         static NOTIFY_ID_COUNTER: std::sync::atomic::AtomicU64 =
246//             std::sync::atomic::AtomicU64::new(0);
247//         let id = format!(
248//             "__js-notify-{}",
249//             NOTIFY_ID_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
250//         );
251//         dioxus::document::eval(&format!(
252//             "let r;let p=new Promise((res)=>{{r=res;}});window[\"{id}-p\"]=p;window[\"{id}-r\"]=r;"
253//         ));
254//         Self(Arc::new(JsNotifyOnceInner {
255//             id,
256//             notify_on_drop,
257//             is_notified: AtomicBool::new(false),
258//         }))
259//     }
260
261//     pub fn notify(&self) {
262//         self.0.notify();
263//     }
264
265//     fn id(&self) -> &str {
266//         &self.0.id
267//     }
268// }
269
270// struct JsNotifyOnceInner {
271//     id: String,
272//     notify_on_drop: bool,
273//     is_notified: AtomicBool,
274// }
275
276// impl JsNotifyOnceInner {
277//     fn notify(&self) {
278//         if self
279//             .is_notified
280//             .swap(true, std::sync::atomic::Ordering::AcqRel)
281//         {
282//             return;
283//         }
284//         let id = self.id.clone();
285//         dioxus::core::spawn_forever(async move {
286//             let eval = dioxus::document::eval(&format!(
287//                 "delete window[\"{id}-p\"];let r=window[\"{id}-r\"];if(r==undefined){{return null;}}r.resolve();delete window[\"{id}-r\"];return null;"
288//             ));
289//             if let Err(error) = eval.await {
290//                 dioxus::logger::tracing::error!(
291//                     "Failed to notify JavaScript object `window[\"{id}\"]`. Error: {error}"
292//                 );
293//             } else {
294//                 dioxus::logger::tracing::trace!("Notified JavaScript object `window[\"{id}\"]`.");
295//             }
296//         });
297//     }
298// }
299
300// impl Drop for JsNotifyOnceInner {
301//     fn drop(&mut self) {
302//         if self.notify_on_drop {
303//             self.notify();
304//         }
305//     }
306// }