lumina_node_wasm/
utils.rs1use std::fmt::{self, Debug};
3use std::future::Future;
4
5use gloo_timers::future::TimeoutFuture;
6use js_sys::Math;
7use serde::Serialize;
8use serde_repr::{Deserialize_repr, Serialize_repr};
9use serde_wasm_bindgen::Serializer;
10use tracing::{info, warn};
11use tracing_subscriber::filter::LevelFilter;
12use tracing_subscriber::fmt::time::UtcTime;
13use tracing_subscriber::prelude::*;
14use tracing_web::MakeConsoleWriter;
15use wasm_bindgen::prelude::*;
16use web_sys::{
17 DedicatedWorkerGlobalScope, MessageEvent, ServiceWorker, ServiceWorkerGlobalScope,
18 SharedWorker, SharedWorkerGlobalScope, Worker,
19};
20
21use lumina_node::network;
22
23use crate::error::{Error, Result};
24
25#[wasm_bindgen]
27#[derive(PartialEq, Eq, Clone, Copy, Serialize_repr, Deserialize_repr, Debug)]
28#[repr(u8)]
29pub enum Network {
30 Mainnet,
32 Arabica,
34 Mocha,
36 Private,
38}
39
40#[wasm_bindgen(start)]
42pub fn setup_logging() {
43 console_error_panic_hook::set_once();
44
45 let fmt_layer = tracing_subscriber::fmt::layer()
46 .with_ansi(false)
47 .with_timer(UtcTime::rfc_3339()) .with_writer(MakeConsoleWriter) .with_filter(LevelFilter::INFO); let _ = tracing_subscriber::registry().with(fmt_layer).try_init();
52}
53
54impl From<Network> for network::Network {
55 fn from(network: Network) -> network::Network {
56 match network {
57 Network::Mainnet => network::Network::Mainnet,
58 Network::Arabica => network::Network::Arabica,
59 Network::Mocha => network::Network::Mocha,
60 Network::Private => network::Network::custom("private").expect("invalid network id"),
61 }
62 }
63}
64
65impl TryFrom<network::Network> for Network {
66 type Error = Error;
67
68 fn try_from(network: network::Network) -> Result<Network, Error> {
69 match network {
70 network::Network::Mainnet => Ok(Network::Mainnet),
71 network::Network::Arabica => Ok(Network::Arabica),
72 network::Network::Mocha => Ok(Network::Mocha),
73 network::Network::Custom(id) => match id.as_ref() {
74 "private" => Ok(Network::Private),
75 _ => Err(Error::new("Unsupported network id: {id}")),
76 },
77 }
78 }
79}
80
81pub(crate) fn js_value_from_display<D: fmt::Display>(value: D) -> JsValue {
82 JsValue::from(value.to_string())
83}
84
85trait WorkerSelf {
86 type GlobalScope: JsCast;
87
88 fn worker_self() -> Self::GlobalScope {
89 js_sys::global().unchecked_into()
90 }
91
92 fn is_worker_type() -> bool {
93 js_sys::global().has_type::<Self::GlobalScope>()
94 }
95}
96
97impl WorkerSelf for SharedWorker {
98 type GlobalScope = SharedWorkerGlobalScope;
99}
100
101impl WorkerSelf for Worker {
102 type GlobalScope = DedicatedWorkerGlobalScope;
103}
104
105impl WorkerSelf for ServiceWorker {
106 type GlobalScope = ServiceWorkerGlobalScope;
107}
108
109pub(crate) trait MessageEventExt {
110 fn get_port(&self) -> Option<JsValue>;
111}
112impl MessageEventExt for MessageEvent {
113 fn get_port(&self) -> Option<JsValue> {
114 let ports = self.ports();
115 if ports.is_array() {
116 let port = ports.get(0);
117 if !port.is_undefined() {
118 return Some(port);
119 }
120 }
121 None
122 }
123}
124
125pub(crate) async fn request_storage_persistence() -> Result<(), Error> {
129 let storage_manager = if let Some(window) = web_sys::window() {
130 window.navigator().storage()
131 } else if Worker::is_worker_type() {
132 Worker::worker_self().navigator().storage()
133 } else if SharedWorker::is_worker_type() {
134 SharedWorker::worker_self().navigator().storage()
135 } else if ServiceWorker::is_worker_type() {
136 warn!("ServiceWorker doesn't have access to StorageManager");
137 return Ok(());
138 } else {
139 return Err(Error::new("`navigator.storage` not found in global scope"));
140 };
141
142 let fullfiled = Closure::once(move |granted: JsValue| {
143 if granted.is_truthy() {
144 info!("Storage persistence acquired: {:?}", granted);
145 } else {
146 warn!("User rejected storage persistance request")
147 }
148 });
149 let rejected = Closure::once(move |_ev: JsValue| {
150 warn!("Error during persistant storage request");
151 });
152
153 let _promise = storage_manager.persist()?.then2(&fullfiled, &rejected);
155
156 fullfiled.forget();
158 rejected.forget();
159
160 Ok(())
161}
162
163const CHROME_USER_AGENT_DETECTION_STR: &str = "Chrome/";
164const FIREFOX_USER_AGENT_DETECTION_STR: &str = "Firefox/";
165const SAFARI_USER_AGENT_DETECTION_STR: &str = "Safari/";
166
167pub(crate) fn get_user_agent() -> Result<String, Error> {
168 if let Some(window) = web_sys::window() {
169 Ok(window.navigator().user_agent()?)
170 } else if Worker::is_worker_type() {
171 Ok(Worker::worker_self().navigator().user_agent()?)
172 } else if SharedWorker::is_worker_type() {
173 Ok(SharedWorker::worker_self().navigator().user_agent()?)
174 } else if ServiceWorker::is_worker_type() {
175 Ok(ServiceWorker::worker_self().navigator().user_agent()?)
176 } else {
177 Err(Error::new(
178 "`navigator.user_agent` not found in global scope",
179 ))
180 }
181}
182
183#[allow(dead_code)]
184pub(crate) fn is_chrome() -> Result<bool, Error> {
185 let user_agent = get_user_agent()?;
186 Ok(user_agent.contains(CHROME_USER_AGENT_DETECTION_STR))
187}
188
189#[allow(dead_code)]
190pub(crate) fn is_firefox() -> Result<bool, Error> {
191 let user_agent = get_user_agent()?;
192 Ok(user_agent.contains(FIREFOX_USER_AGENT_DETECTION_STR))
193}
194
195pub(crate) fn is_safari() -> Result<bool, Error> {
196 let user_agent = get_user_agent()?;
197 Ok(user_agent.contains(SAFARI_USER_AGENT_DETECTION_STR)
199 && !user_agent.contains(CHROME_USER_AGENT_DETECTION_STR))
200}
201
202#[allow(dead_code)]
203pub(crate) fn shared_workers_supported() -> Result<bool, Error> {
204 Ok(is_firefox()? || is_safari()?)
208}
209
210pub(crate) fn random_id() -> u32 {
211 (Math::random() * f64::from(u32::MAX)).floor() as u32
212}
213
214pub(crate) async fn timeout<F: Future>(millis: u32, fut: F) -> Result<F::Output, ()> {
215 let timeout = TimeoutFuture::new(millis);
216 tokio::select! {
217 _ = timeout => Err(()),
218 res = fut => Ok(res),
219 }
220}
221
222pub(crate) fn to_json_value<T: Serialize + ?Sized>(
223 value: &T,
224) -> Result<JsValue, serde_wasm_bindgen::Error> {
225 value.serialize(&Serializer::json_compatible())
226}