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