lumina_node_wasm/
utils.rs

1//! Various utilities for interacting with node from wasm.
2use 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/// Supported Celestia networks.
26#[wasm_bindgen]
27#[derive(PartialEq, Eq, Clone, Copy, Serialize_repr, Deserialize_repr, Debug)]
28#[repr(u8)]
29pub enum Network {
30    /// Celestia mainnet.
31    Mainnet,
32    /// Arabica testnet.
33    Arabica,
34    /// Mocha testnet.
35    Mocha,
36    /// Private local network.
37    Private,
38}
39
40/// Set up a logging layer that direct logs to the browser's console.
41#[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()) // std::time is not available in browsers
48        .with_writer(MakeConsoleWriter) // write events to the console
49        .with_filter(LevelFilter::INFO); // TODO: allow customizing the log level
50
51    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
125/// Request persistent storage from user for us, which has side effect of increasing the quota we
126/// have. This function doesn't `await` on JavaScript promise, as that would block until user
127/// either allows or blocks our request in a prompt (and we cannot do much with the result anyway).
128pub(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    // don't drop the promise, we'll log the result and hope the user clicked the right button
154    let _promise = storage_manager.persist()?.then2(&fullfiled, &rejected);
155
156    // stop rust from dropping them
157    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    // Chrome contains `Safari/`, so make sure user agent doesn't contain `Chrome/`
198    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    // For chrome we default to running in a dedicated Worker because:
205    // 1. Chrome Android does not support SharedWorkers at all
206    // 2. On desktop Chrome, restarting Lumina's worker causes all network connections to fail.
207    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}