use std::fmt::{self, Debug};
use std::future::Future;
use gloo_timers::future::TimeoutFuture;
use js_sys::Math;
use serde::Serialize;
use serde_repr::{Deserialize_repr, Serialize_repr};
use serde_wasm_bindgen::Serializer;
use tracing::{info, warn};
use tracing_subscriber::filter::LevelFilter;
use tracing_subscriber::fmt::time::UtcTime;
use tracing_subscriber::prelude::*;
use tracing_web::MakeConsoleWriter;
use wasm_bindgen::prelude::*;
use web_sys::{
DedicatedWorkerGlobalScope, MessageChannel, MessageEvent, MessagePort, ServiceWorker,
ServiceWorkerGlobalScope, SharedWorker, SharedWorkerGlobalScope, Worker,
};
use lumina_node::network;
use crate::error::{Error, Result};
use crate::ports::MessagePortLike;
#[wasm_bindgen]
#[derive(PartialEq, Eq, Clone, Copy, Serialize_repr, Deserialize_repr, Debug)]
#[repr(u8)]
pub enum Network {
Mainnet,
Arabica,
Mocha,
Private,
}
#[wasm_bindgen(start)]
pub fn setup_logging() {
console_error_panic_hook::set_once();
let fmt_layer = tracing_subscriber::fmt::layer()
.with_ansi(false)
.with_timer(UtcTime::rfc_3339()) .with_writer(MakeConsoleWriter) .with_filter(LevelFilter::INFO);
let _ = tracing_subscriber::registry().with(fmt_layer).try_init();
}
impl From<Network> for network::Network {
fn from(network: Network) -> network::Network {
match network {
Network::Mainnet => network::Network::Mainnet,
Network::Arabica => network::Network::Arabica,
Network::Mocha => network::Network::Mocha,
Network::Private => network::Network::custom("private").expect("invalid network id"),
}
}
}
impl TryFrom<network::Network> for Network {
type Error = Error;
fn try_from(network: network::Network) -> Result<Network, Error> {
match network {
network::Network::Mainnet => Ok(Network::Mainnet),
network::Network::Arabica => Ok(Network::Arabica),
network::Network::Mocha => Ok(Network::Mocha),
network::Network::Custom(id) => match id.as_ref() {
"private" => Ok(Network::Private),
_ => Err(Error::new("Unsupported network id: {id}")),
},
}
}
}
pub(crate) fn js_value_from_display<D: fmt::Display>(value: D) -> JsValue {
JsValue::from(value.to_string())
}
trait WorkerSelf {
type GlobalScope: JsCast;
fn worker_self() -> Self::GlobalScope {
js_sys::global().unchecked_into()
}
fn is_worker_type() -> bool {
js_sys::global().has_type::<Self::GlobalScope>()
}
}
impl WorkerSelf for SharedWorker {
type GlobalScope = SharedWorkerGlobalScope;
}
impl WorkerSelf for Worker {
type GlobalScope = DedicatedWorkerGlobalScope;
}
impl WorkerSelf for ServiceWorker {
type GlobalScope = ServiceWorkerGlobalScope;
}
pub(crate) trait MessageEventExt {
fn get_ports(&self) -> Vec<MessagePortLike>;
}
impl MessageEventExt for MessageEvent {
fn get_ports(&self) -> Vec<MessagePortLike> {
let ports = self.ports();
if ports.is_array() {
ports.iter().map(MessagePortLike::from).collect()
} else {
vec![]
}
}
}
pub(crate) trait MessageChannelExt {
fn new_ports() -> Result<(MessagePort, MessagePort)>;
}
impl MessageChannelExt for MessageChannel {
fn new_ports() -> Result<(MessagePort, MessagePort)> {
let channel = MessageChannel::new()?;
Ok((channel.port1(), channel.port2()))
}
}
pub(crate) async fn request_storage_persistence() -> Result<(), Error> {
let storage_manager = if let Some(window) = web_sys::window() {
window.navigator().storage()
} else if Worker::is_worker_type() {
Worker::worker_self().navigator().storage()
} else if SharedWorker::is_worker_type() {
SharedWorker::worker_self().navigator().storage()
} else if ServiceWorker::is_worker_type() {
warn!("ServiceWorker doesn't have access to StorageManager");
return Ok(());
} else {
return Err(Error::new("`navigator.storage` not found in global scope"));
};
let fullfiled = Closure::once(move |granted: JsValue| {
if granted.is_truthy() {
info!("Storage persistence acquired: {:?}", granted);
} else {
warn!("User rejected storage persistance request")
}
});
let rejected = Closure::once(move |_ev: JsValue| {
warn!("Error during persistant storage request");
});
let _promise = storage_manager.persist()?.then2(&fullfiled, &rejected);
fullfiled.forget();
rejected.forget();
Ok(())
}
const CHROME_USER_AGENT_DETECTION_STR: &str = "Chrome/";
const FIREFOX_USER_AGENT_DETECTION_STR: &str = "Firefox/";
const SAFARI_USER_AGENT_DETECTION_STR: &str = "Safari/";
pub(crate) fn get_user_agent() -> Result<String, Error> {
if let Some(window) = web_sys::window() {
Ok(window.navigator().user_agent()?)
} else if Worker::is_worker_type() {
Ok(Worker::worker_self().navigator().user_agent()?)
} else if SharedWorker::is_worker_type() {
Ok(SharedWorker::worker_self().navigator().user_agent()?)
} else if ServiceWorker::is_worker_type() {
Ok(ServiceWorker::worker_self().navigator().user_agent()?)
} else {
Err(Error::new(
"`navigator.user_agent` not found in global scope",
))
}
}
#[allow(dead_code)]
pub(crate) fn is_chrome() -> Result<bool, Error> {
let user_agent = get_user_agent()?;
Ok(user_agent.contains(CHROME_USER_AGENT_DETECTION_STR))
}
#[allow(dead_code)]
pub(crate) fn is_firefox() -> Result<bool, Error> {
let user_agent = get_user_agent()?;
Ok(user_agent.contains(FIREFOX_USER_AGENT_DETECTION_STR))
}
pub(crate) fn is_safari() -> Result<bool, Error> {
let user_agent = get_user_agent()?;
Ok(user_agent.contains(SAFARI_USER_AGENT_DETECTION_STR)
&& !user_agent.contains(CHROME_USER_AGENT_DETECTION_STR))
}
#[allow(dead_code)]
pub(crate) fn shared_workers_supported() -> Result<bool, Error> {
Ok(is_firefox()? || is_safari()?)
}
pub(crate) fn random_id() -> u32 {
(Math::random() * f64::from(u32::MAX)).floor() as u32
}
pub(crate) async fn timeout<F: Future>(millis: u32, fut: F) -> Result<F::Output, ()> {
let timeout = TimeoutFuture::new(millis);
tokio::select! {
_ = timeout => Err(()),
res = fut => Ok(res),
}
}
pub(crate) fn to_json_value<T: Serialize + ?Sized>(
value: &T,
) -> Result<JsValue, serde_wasm_bindgen::Error> {
value.serialize(&Serializer::json_compatible())
}