From f74b8dedb544e62a80caf9ad0af1a3497dd910c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Florkiewicz?= Date: Fri, 5 Apr 2024 23:04:30 +0200 Subject: [PATCH 01/35] wip --- Cargo.lock | 32 ++--- Cargo.toml | 2 + cli/static/run_node.js | 28 ++++- cli/static/worker.js | 71 +++++++++++ node-wasm/Cargo.toml | 4 + node-wasm/src/lib.rs | 1 + node-wasm/src/node.rs | 115 +++++++++++++++++- node-wasm/src/utils.rs | 149 ++++++++++++++++++++++- node-wasm/src/worker.rs | 260 ++++++++++++++++++++++++++++++++++++++++ node/src/p2p.rs | 1 - 10 files changed, 637 insertions(+), 26 deletions(-) create mode 100644 cli/static/worker.js create mode 100644 node-wasm/src/worker.rs diff --git a/Cargo.lock b/Cargo.lock index f4e41094..c64f49ec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2107,9 +2107,9 @@ checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" [[package]] name = "js-sys" -version = "0.3.68" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "406cda4b368d531c842222cf9d2600a9a4acce8d29423695379c6868a143a9ee" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" dependencies = [ "wasm-bindgen", ] @@ -2882,6 +2882,8 @@ dependencies = [ "anyhow", "celestia-types", "console_error_panic_hook", + "gloo-timers 0.3.0", + "instant", "js-sys", "libp2p", "lumina-node", @@ -2889,11 +2891,13 @@ dependencies = [ "serde-wasm-bindgen", "serde_repr", "time", + "tokio", "tracing", "tracing-subscriber", "tracing-web", "wasm-bindgen", "wasm-bindgen-futures", + "web-sys", ] [[package]] @@ -5153,9 +5157,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.91" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1e124130aee3fb58c5bdd6b639a0509486b0338acaaae0c84a5124b0f588b7f" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -5163,9 +5167,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.91" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e7e1900c352b609c8488ad12639a311045f40a35491fb69ba8c12f758af70b" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" dependencies = [ "bumpalo", "log", @@ -5190,9 +5194,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.91" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b30af9e2d358182b5c7449424f017eba305ed32a7010509ede96cdc4696c46ed" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -5200,9 +5204,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.91" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "642f325be6301eb8107a83d12a8ac6c1e1c54345a7ef1a9261962dfefda09e66" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", @@ -5213,9 +5217,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.91" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f186bd2dcf04330886ce82d6f33dd75a7bfcf69ecf5763b89fcde53b6ac9838" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" [[package]] name = "wasm-bindgen-test" @@ -5244,9 +5248,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.68" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96565907687f7aceb35bc5fc03770a8a0471d82e479f25832f54a0e3f4b28446" +checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" dependencies = [ "js-sys", "wasm-bindgen", diff --git a/Cargo.toml b/Cargo.toml index 6b646aaa..5c9aa779 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ nmt-rs = "0.1.0" celestia-tendermint = { version = "0.32.1", default-features = false } celestia-tendermint-proto = "0.32.1" blockstore = { version = "0.1.1", path = "blockstore" } +parking_lot = { version = "*", features = [ "deadlock_detection" ] } [patch.crates-io] # This is a workaround. @@ -20,6 +21,7 @@ blockstore = { version = "0.1.1", path = "blockstore" } # Because `beetswap` is outside of this workspace, cargo thinks that # it depends on a different `blockstore` crate. blockstore = { path = "blockstore" } +#beetswap = { path = "../beetswap" } # Uncomment to apply local changes #celestia-tendermint = { path = "../celestia-tendermint-rs/tendermint" } #celestia-tendermint-proto = { path = "../celestia-tendermint-rs/proto" } diff --git a/cli/static/run_node.js b/cli/static/run_node.js index e50d861a..93235a29 100644 --- a/cli/static/run_node.js +++ b/cli/static/run_node.js @@ -1,6 +1,6 @@ Error.stackTraceLimit = 99; // rust stack traces can get pretty big, increase the default -import init, { Node, NodeConfig } from "/wasm/lumina_node_wasm.js"; +import init, { Node, NodeConfig, NodeDriver } from "/wasm/lumina_node_wasm.js"; async function fetch_config() { const response = await fetch('/cfg.json'); @@ -20,7 +20,7 @@ async function fetch_config() { } async function show_stats(node) { - if (!node) { + if (!node || !await node.is_running()) { return; } const info = await node.syncer_info(); @@ -36,11 +36,12 @@ async function show_stats(node) { document.getElementById("peers").replaceChildren(peers_ul); - const network_head = node.get_network_head_header(); + const network_head = await node.get_network_head_header(); if (network_head == null) { return } + //console.log(network_head); const square_rows = network_head.dah.row_roots.length; const square_cols = network_head.dah.column_roots.length; @@ -93,24 +94,39 @@ function bind_config(data) { }); } +/* async function start_node(config) { - window.node = await new Node(config); + //window.node = await new Node(config); document.getElementById("peer-id").innerText = await window.node.local_peer_id(); document.querySelectorAll(".status").forEach(elem => elem.style.visibility = "visible"); } +*/ async function main(document, window) { await init(); + //window.worker = new SharedWorker('/js/worker.js', {name: 'lumina', type: 'module'}); + + window.driver = await new NodeDriver(); + bind_config(await fetch_config()); + if (await window.driver.is_running() === true) { + document.querySelectorAll('.config').forEach(elem => elem.disabled = true); + document.getElementById("peer-id").innerText = await window.driver.local_peer_id(); + document.querySelectorAll(".status").forEach(elem => elem.style.visibility = "visible"); + } + document.getElementById("start").addEventListener("click", async () => { document.querySelectorAll('.config').forEach(elem => elem.disabled = true); - start_node(window.config); + //start_node(window.config); + await window.driver.start(window.config) + document.getElementById("peer-id").innerText = await window.driver.local_peer_id(); + document.querySelectorAll(".status").forEach(elem => elem.style.visibility = "visible"); }); - setInterval(async () => await show_stats(window.node), 1000) + setInterval(async () => await show_stats(window.driver), 1000) } await main(document, window); diff --git a/cli/static/worker.js b/cli/static/worker.js new file mode 100644 index 00000000..3297f8e0 --- /dev/null +++ b/cli/static/worker.js @@ -0,0 +1,71 @@ +// The worker has its own scope and no direct access to functions/objects of the +// global scope. We import the generated JS file to make `wasm_bindgen` +// available which we need to initialize our Wasm code. +//importScripts('/wasm/lumina_node_wasm.js'); +import init, { run_worker } from "/wasm/lumina_node_wasm.js"; + +let queued = []; + +onconnect = (event) => { + console.log("Queued connection", event); + queued.push(event.ports[0]); +} + +console.log('Initializing worker') + +await init(); + +console.log('init run') + +await run_worker(queued); + + +/* +// In the worker, we have a different struct that we want to use as in +// `index.js`. +//const {NumberEval} = wasm_bindgen; + +async function fetch_config() { + const response = await fetch('/cfg.json'); + const json = await response.json(); + + console.log("Received config:", json); + + let config = NodeConfig.default(json.network); + if (json.bootnodes.length !== 0) { + config.bootnodes = json.bootnodes; + } + if (json.genesis) { + config.genesis = json.genesis; + } + + return config; +} + +async function init_wasm_in_worker() { + await init(); + // Load the wasm file by awaiting the Promise returned by `wasm_bindgen`. + //await wasm_bindgen('./pkg/wasm_in_web_worker_bg.wasm'); + + // Create a new object of the `NumberEval` struct. + //var num_eval = NumberEval.new(); + + let config = await fetch_config(); + + let node = await new Node(config); + + // Set callback to handle messages passed to the worker. + self.onmessage = async event => { + console.log("worker", node); + console.log("ev in worker", event); + // By using methods of a struct as reaction to messages passed to the + // worker, we can preserve our state between messages. + //var worker_result = num_eval.is_even(event.data); + + // Send response back to be handled by callback in main thread. + //self.postMessage(worker_result); + }; +}; + +init_wasm_in_worker(); +*/ diff --git a/node-wasm/Cargo.toml b/node-wasm/Cargo.toml index 88f6b085..e8b4462d 100644 --- a/node-wasm/Cargo.toml +++ b/node-wasm/Cargo.toml @@ -39,3 +39,7 @@ tracing-subscriber = { version = "0.3.18", features = ["time"] } tracing-web = "0.1.2" wasm-bindgen = "0.2.88" wasm-bindgen-futures = "0.4.37" +web-sys = { version = "0.3.69", features = ["BroadcastChannel", "MessageEvent", "Worker", "WorkerOptions", "WorkerType", "SharedWorker", "MessagePort", "SharedWorkerGlobalScope"]} +tokio = { version = "*", features = ["sync"]} +gloo-timers = "0.3" +instant = "0.1" diff --git a/node-wasm/src/lib.rs b/node-wasm/src/lib.rs index 49436ae5..a8d848e6 100644 --- a/node-wasm/src/lib.rs +++ b/node-wasm/src/lib.rs @@ -5,6 +5,7 @@ use wasm_bindgen::JsError; pub mod node; pub mod utils; +mod worker; mod wrapper; /// Alias for a `Result` with the error type [`JsError`]. diff --git a/node-wasm/src/node.rs b/node-wasm/src/node.rs index 841291cb..55d97fd1 100644 --- a/node-wasm/src/node.rs +++ b/node-wasm/src/node.rs @@ -10,23 +10,28 @@ use lumina_node::blockstore::IndexedDbBlockstore; use lumina_node::network::{canonical_network_bootnodes, network_genesis, network_id}; use lumina_node::node::{Node, NodeConfig}; use lumina_node::store::{IndexedDbStore, Store}; -use serde::Serialize; +use serde::{Deserialize, Serialize}; use serde_wasm_bindgen::{from_value, to_value}; use tracing::info; use wasm_bindgen::prelude::*; use crate::utils::js_value_from_display; +use crate::utils::BChannel; use crate::utils::JsContext; use crate::utils::Network; +use crate::worker::{NodeCommand, NodeResponse}; use crate::wrapper::libp2p::NetworkInfo; use crate::Result; +use web_sys::{SharedWorker, WorkerOptions, WorkerType}; + /// Lumina wasm node. #[wasm_bindgen(js_name = Node)] struct WasmNode(Node); /// Config for the lumina wasm node. #[wasm_bindgen(js_name = NodeConfig)] +#[derive(Serialize, Deserialize, Debug)] pub struct WasmNodeConfig { /// A network to connect to. pub network: Network, @@ -38,6 +43,106 @@ pub struct WasmNodeConfig { pub bootnodes: Vec, } +// TODO: add on_error handler +#[wasm_bindgen] +struct NodeDriver { + _worker: SharedWorker, + channel: BChannel, +} + +#[wasm_bindgen] +impl NodeDriver { + #[wasm_bindgen(constructor)] + pub async fn new() -> NodeDriver { + let mut opts = WorkerOptions::new(); + opts.type_(WorkerType::Module); + opts.name("lumina"); + let worker = SharedWorker::new_with_worker_options("/js/worker.js", &opts) + .expect("could not worker"); + + let channel = BChannel::new(worker.port()); + + Self { + _worker: worker, + channel, + } + } + + pub async fn is_running(&mut self) -> bool { + self.channel.send(NodeCommand::IsRunning); + let Some(NodeResponse::Running(running)) = self.channel.recv().await else { + panic!("wrong reponse"); + }; + running + } + + pub async fn start(&mut self, config: WasmNodeConfig) { + let command = NodeCommand::Start(config); + self.channel.send(command); + let Some(NodeResponse::Started(uptime)) = self.channel.recv().await else { + panic!("wrong reponse"); + }; + info!("started = {uptime:?}"); + } + + pub async fn local_peer_id(&mut self) -> String { + self.channel.send(NodeCommand::GetLocalPeerId); + let Some(NodeResponse::LocalPeerId(id)) = self.channel.recv().await else { + panic!("wrong reponse"); + }; + info!("peer id = {id:?}"); + id + } + + pub async fn syncer_info(&mut self) -> Result { + self.channel.send(NodeCommand::GetSyncerInfo); + let response = self.channel.recv().await; + let Some(NodeResponse::SyncerInfo(info)) = response else { + panic!("wrong response"); + }; + //info!("syncer info = {info:?}"); + Ok(info) + } + + pub async fn peer_tracker_info(&mut self) -> Result { + self.channel.send(NodeCommand::GetPeerTrackerInfo); + let Some(NodeResponse::PeerTrackerInfo(info)) = self.channel.recv().await else { + panic!("wrong response"); + }; + //info!("peer tracker info = {info:?}"); + Ok(info) + } + + pub async fn connected_peers(&mut self) -> Result { + self.channel.send(NodeCommand::GetConnectedPeers); + let Some(NodeResponse::ConnectedPeers(peers)) = self.channel.recv().await else { + panic!("wrong"); + }; + //info!("peers = {peers:?}"); + Ok(peers) + } + + pub async fn get_network_head_header(&mut self) -> Result { + self.channel.send(NodeCommand::GetNetworkHeadHeader); + let Some(NodeResponse::NetworkHeadHeader(header)) = self.channel.recv().await else { + panic!("wrong response"); + }; + //info!("network head header = {header:?}"); + Ok(header) + } + + /* + pub async fn network_info(&self) -> Result { + self.channel.send(NodeCommand::GetNetworkInfo); + let Some(NodeResponse::NetworkInfo(info)) = self.channel.recv().await else { + panic!("wrong response"); + }; + info!("network info = {info:?}"); + Ok(info) + } + */ +} + #[wasm_bindgen(js_class = Node)] impl WasmNode { /// Create a new Lumina node. @@ -152,8 +257,8 @@ impl WasmNode { /// Get the latest header announced in the network. pub fn get_network_head_header(&self) -> Result { - let maybe_head_hedaer = self.0.get_network_head_header(); - Ok(to_value(&maybe_head_hedaer)?) + let maybe_head_header = self.0.get_network_head_header(); + Ok(to_value(&maybe_head_header)?) } /// Get the latest locally synced header. @@ -236,7 +341,9 @@ impl WasmNodeConfig { } } - async fn into_node_config(self) -> Result> { + pub(crate) async fn into_node_config( + self, + ) -> Result> { let network_id = network_id(self.network.into()); let store = IndexedDbStore::new(network_id) .await diff --git a/node-wasm/src/utils.rs b/node-wasm/src/utils.rs index c1ad47d0..1fcd497c 100644 --- a/node-wasm/src/utils.rs +++ b/node-wasm/src/utils.rs @@ -1,6 +1,7 @@ //! Various utilities for interacting with node from wasm. use std::fmt; +use std::marker::PhantomData; use lumina_node::network; use serde_repr::{Deserialize_repr, Serialize_repr}; @@ -11,9 +12,152 @@ use tracing_subscriber::prelude::*; use tracing_web::{performance_layer, MakeConsoleWriter}; use wasm_bindgen::prelude::*; +use crate::node::WasmNodeConfig; +use serde::de::DeserializeOwned; +use serde::{Deserialize, Serialize}; +use serde_wasm_bindgen::from_value; +use serde_wasm_bindgen::to_value; +use tokio::sync::mpsc; +use wasm_bindgen_futures::spawn_local; +use web_sys::MessageEvent; +use web_sys::MessagePort; +use web_sys::SharedWorker; +use web_sys::SharedWorkerGlobalScope; + +pub struct BChannel { + _onmessage: Closure, + channel: MessagePort, + msg_rx: mpsc::Receiver, + send_type: PhantomData, +} + +impl BChannel +where + IN: Serialize, + OUT: DeserializeOwned + 'static, +{ + pub fn new(channel: MessagePort) -> Self { + let (tx, rx) = mpsc::channel(64); + + let near_tx = tx.clone(); + let f = move |ev: MessageEvent| { + let local_tx = near_tx.clone(); + spawn_local(async move { + let message_data = ev.data(); + + let data = from_value(message_data).unwrap(); + + local_tx.send(data).await.expect("send err"); + }) + }; + + let onmessage = Closure::new(f); + + channel.set_onmessage(Some(onmessage.as_ref().unchecked_ref())); + channel.start(); + Self { + _onmessage: onmessage, + channel, + msg_rx: rx, + send_type: PhantomData, + } + } + + pub fn send(&self, msg: IN) { + let v = to_value(&msg).unwrap(); + self.channel.post_message(&v).expect("err post"); + } + + pub async fn recv(&mut self) -> Option { + self.msg_rx.recv().await + } +} + +// TODO: cleanup JS objects on drop +// impl Drop + +/* +#[derive(Serialize, Deserialize)] +enum BroadcastMessage { + Foo +} + +fn create_channel() { + + let broadcast_channel = BChannel::new("lumina"); + + broadcast_channel.send(BroadcastMessage::Foo); +} +*/ + +/* +#[derive(Serialize, Deserialize, Debug)] +pub enum NodeCommand { + //ConnectDriver(u64), + Start(WasmNodeConfig), + GetLocalPeerId, +} + +#[derive(Serialize, Deserialize, Debug)] +pub enum NodeResponse { + LocalPeerId(String), +} +*/ + +/* +#[wasm_bindgen] +struct BSharedWorker { + worker: SharedWorker, + channel: BChannel, +} + +#[wasm_bindgen] +impl BSharedWorker { + pub fn new() -> Self { + let mut opts = WorkerOptions::new(); + opts.type_(WorkerType::Module); + let worker = SharedWorker::new_with_worker_options("/js/worker.js", &opts) + .expect("could not worker"); + + let channel = BChannel::new(worker.port()); + + Self { worker, channel } + } +} +*/ + +/* +#[wasm_bindgen] +pub fn launch_worker() -> BSharedWorker { + info!("doing the worker"); + let mut opts = WorkerOptions::new(); + opts.type_(WorkerType::Module); + let worker = + SharedWorker::new_with_worker_options("/js/worker.js", &opts).expect("could not worker"); + + let port = worker.port(); + + info!("did the worker"); +} +*/ + +pub(crate) trait WorkerSelf { + type GlobalScope; + + fn worker_self() -> Self::GlobalScope; +} + +impl WorkerSelf for SharedWorker { + type GlobalScope = SharedWorkerGlobalScope; + + fn worker_self() -> Self::GlobalScope { + JsValue::from(js_sys::global()).into() + } +} + /// Supported Celestia networks. #[wasm_bindgen] -#[derive(PartialEq, Eq, Clone, Copy, Serialize_repr, Deserialize_repr)] +#[derive(PartialEq, Eq, Clone, Copy, Serialize_repr, Deserialize_repr, Debug)] #[repr(u8)] pub enum Network { /// Celestia mainnet. @@ -42,6 +186,9 @@ pub fn setup_logging() { .with(fmt_layer) .with(perf_layer) .init(); + + //launch_worker(); + //create_channel(); } impl From for network::Network { diff --git a/node-wasm/src/worker.rs b/node-wasm/src/worker.rs new file mode 100644 index 00000000..3ca92794 --- /dev/null +++ b/node-wasm/src/worker.rs @@ -0,0 +1,260 @@ +use crate::node::WasmNodeConfig; +use crate::utils::BChannel; +use lumina_node::node::Node; +use lumina_node::store::{IndexedDbStore, Store}; +use serde::{Deserialize, Serialize}; +use tracing::{info, trace, warn}; +use wasm_bindgen::prelude::*; + +use crate::utils::js_value_from_display; +use crate::utils::WorkerSelf; +use crate::Result; +use instant::Instant; +use js_sys::Array; +use serde_wasm_bindgen::{from_value, to_value}; +use tokio::sync::mpsc; +use wasm_bindgen_futures::spawn_local; +use web_sys::MessageEvent; +use web_sys::MessagePort; +use web_sys::SharedWorker; + +#[derive(Serialize, Deserialize, Debug)] +pub enum NodeCommand { + IsRunning, + Start(WasmNodeConfig), + GetLocalPeerId, + GetSyncerInfo, + GetPeerTrackerInfo, + //GetNetworkInfo, + GetConnectedPeers, + GetNetworkHeadHeader, + GetLocalHeadHeader, +} + +#[derive(Serialize, Deserialize, Debug)] +pub enum NodeResponse { + Running(bool), + Started(u64), + LocalPeerId(String), + #[serde(with = "serde_wasm_bindgen::preserve")] + SyncerInfo(JsValue), + #[serde(with = "serde_wasm_bindgen::preserve")] + PeerTrackerInfo(JsValue), + //#[serde(with = "serde_wasm_bindgen::preserve")] + //NetworkInfo(NetworkInfo), + #[serde(with = "serde_wasm_bindgen::preserve")] + ConnectedPeers(Array), + #[serde(with = "serde_wasm_bindgen::preserve")] + NetworkHeadHeader(JsValue), + #[serde(with = "serde_wasm_bindgen::preserve")] + LocalHeadHeader(JsValue), +} + +struct NodeWorker { + node: Node, + start_timestamp: Instant, +} + +impl NodeWorker { + async fn new(config: WasmNodeConfig) -> Self { + let config = config.into_node_config().await.ok().unwrap(); + + if let Ok(store_height) = config.store.head_height().await { + info!("Initialised store with head height: {store_height}"); + } else { + info!("Initialized new empty store"); + } + + let node = Node::new(config).await.ok().unwrap(); + + Self { + node, + start_timestamp: Instant::now(), + } + } + + fn local_peer_id(&self) -> String { + self.node.local_peer_id().to_string() + } + + fn peer_tracker_info(&self) -> Result { + Ok(to_value(&self.node.peer_tracker_info())?) + } + + async fn syncer_info(&self) -> Result { + Ok(to_value(&self.node.syncer_info().await?)?) + } + + /* + async fn network_info(&self) -> Result { + Ok(self.node.network_info().await?.into()) + } + */ + + async fn connected_peers(&self) -> Result { + Ok(self + .node + .connected_peers() + .await? + .iter() + .map(js_value_from_display) + .collect()) + } + + async fn network_head_header(&self) -> Result { + Ok(to_value(&self.node.get_network_head_header())?) + } + + async fn local_head_header(&self) -> Result { + Ok(to_value(&self.node.get_local_head_header().await?)?) + } + + async fn process_command(&mut self, command: NodeCommand) -> NodeResponse { + match command { + NodeCommand::IsRunning => NodeResponse::Running(true), + NodeCommand::Start(_config) => NodeResponse::Started( + Instant::now() + .checked_duration_since(self.start_timestamp) + .map(|duration| duration.as_secs()) + .unwrap_or(0), + ), + NodeCommand::GetLocalPeerId => NodeResponse::LocalPeerId(self.local_peer_id()), + NodeCommand::GetSyncerInfo => { + NodeResponse::SyncerInfo(self.syncer_info().await.ok().unwrap()) + } + NodeCommand::GetPeerTrackerInfo => { + NodeResponse::PeerTrackerInfo(self.peer_tracker_info().ok().unwrap()) + } /* + NodeCommand::GetNetworkInfo => { + NodeResponse::NetworkInfo(self.network_info().await.ok().unwrap()) + } + */ + NodeCommand::GetConnectedPeers => { + NodeResponse::ConnectedPeers(self.connected_peers().await.ok().unwrap()) + } + NodeCommand::GetNetworkHeadHeader => { + NodeResponse::NetworkHeadHeader(self.network_head_header().await.ok().unwrap()) + } + NodeCommand::GetLocalHeadHeader => { + NodeResponse::LocalHeadHeader(self.local_head_header().await.ok().unwrap()) + } + } + } +} + +//type WorkerChannel = BChannel; + +enum WorkerMessage { + NewConnection(MessagePort), + Command((NodeCommand, ClientId)), +} + +#[derive(Debug)] +struct ClientId(usize); + +struct WorkerConnector { + // make sure the callback doesn't get dropped + _onconnect_callback: Closure, + callbacks: Vec>, + ports: Vec, + + command_channel: mpsc::Sender, +} + +impl WorkerConnector { + fn new(command_channel: mpsc::Sender) -> Self { + let worker_scope = SharedWorker::worker_self(); + let near_tx = command_channel.clone(); + let onconnect_callback: Closure = + Closure::new(move |ev: MessageEvent| { + let local_tx = near_tx.clone(); + spawn_local(async move { + let port: MessagePort = ev.ports().at(0).dyn_into().expect("invalid type"); + local_tx + .send(WorkerMessage::NewConnection(port)) + .await + .expect("send2 error"); + }) + }); + worker_scope.set_onconnect(Some(onconnect_callback.as_ref().unchecked_ref())); + + Self { + _onconnect_callback: onconnect_callback, + callbacks: Vec::new(), + ports: Vec::new(), + command_channel, + } + } + + fn add(&mut self, port: MessagePort) { + debug_assert_eq!(self.callbacks.len(), self.ports.len()); + let client_id = self.callbacks.len(); + + let near_tx = self.command_channel.clone(); + let client_message_callback: Closure = + Closure::new(move |ev: MessageEvent| { + let local_tx = near_tx.clone(); + spawn_local(async move { + let message_data = ev.data(); + let data = from_value(message_data).expect("could not from value"); + + local_tx + .send(WorkerMessage::Command((data, ClientId(client_id)))) + .await + .expect("send3 err"); + }) + }); + port.set_onmessage(Some(client_message_callback.as_ref().unchecked_ref())); + + self.callbacks.push(client_message_callback); + self.ports.push(port); + + info!("New connection: {client_id}"); + } + + fn respond_to(&self, client: ClientId, msg: NodeResponse) { + let off = client.0; + let v = to_value(&msg).expect("could not to_value"); + self.ports[off].post_message(&v).expect("err posttt"); + } +} + +#[wasm_bindgen] +pub async fn run_worker(queued_connections: Vec) { + let (tx, mut rx) = mpsc::channel(64); + let mut connector = WorkerConnector::new(tx.clone()); + + for connection in queued_connections { + connector.add(connection); + } + + let mut worker = None; + while let Some(message) = rx.recv().await { + match message { + WorkerMessage::NewConnection(connection) => { + connector.add(connection); + } + WorkerMessage::Command((command, client)) => { + let Some(worker) = &mut worker else { + match command { + NodeCommand::IsRunning => { + connector.respond_to(client, NodeResponse::Running(false)); + } + NodeCommand::Start(config) => { + worker = Some(NodeWorker::new(config).await); + connector.respond_to(client, NodeResponse::Started(0)); + } + _ => warn!("Worker not running"), + } + continue; + }; + + trace!("received: {command:?}"); + let response = worker.process_command(command).await; + connector.respond_to(client, response); + } + } + } + + warn!("EXIT EXIT EXIT"); +} diff --git a/node/src/p2p.rs b/node/src/p2p.rs index 0f170237..b03ff971 100644 --- a/node/src/p2p.rs +++ b/node/src/p2p.rs @@ -506,7 +506,6 @@ impl P2p { Ok(rx.await?) } } - /// Our network behaviour. #[derive(NetworkBehaviour)] struct Behaviour From cef4c0cc2c6856fb36dd224cbd0dbafe2e21044e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Florkiewicz?= Date: Tue, 9 Apr 2024 10:20:42 +0200 Subject: [PATCH 02/35] first pass --- node-wasm/src/node.rs | 155 +++++++++++++++++++++---- node-wasm/src/worker.rs | 193 ++++++++++++++++++++++++++++---- node-wasm/src/wrapper/libp2p.rs | 43 +++++++ 3 files changed, 350 insertions(+), 41 deletions(-) diff --git a/node-wasm/src/node.rs b/node-wasm/src/node.rs index 55d97fd1..4cd26e60 100644 --- a/node-wasm/src/node.rs +++ b/node-wasm/src/node.rs @@ -2,7 +2,8 @@ use std::result::Result as StdResult; -use celestia_types::{hash::Hash, ExtendedHeader}; +use celestia_types::hash::Hash; +use celestia_types::ExtendedHeader; use js_sys::Array; use libp2p::identity::Keypair; use libp2p::multiaddr::Protocol; @@ -19,8 +20,8 @@ use crate::utils::js_value_from_display; use crate::utils::BChannel; use crate::utils::JsContext; use crate::utils::Network; -use crate::worker::{NodeCommand, NodeResponse}; -use crate::wrapper::libp2p::NetworkInfo; +use crate::worker::{HeaderQuery, NodeCommand, NodeResponse}; +use crate::wrapper::libp2p::NetworkInfoSnapshot; use crate::Result; use web_sys::{SharedWorker, WorkerOptions, WorkerType}; @@ -94,25 +95,41 @@ impl NodeDriver { id } - pub async fn syncer_info(&mut self) -> Result { - self.channel.send(NodeCommand::GetSyncerInfo); - let response = self.channel.recv().await; - let Some(NodeResponse::SyncerInfo(info)) = response else { + pub async fn peer_tracker_info(&mut self) -> Result { + self.channel.send(NodeCommand::GetPeerTrackerInfo); + let Some(NodeResponse::PeerTrackerInfo(info)) = self.channel.recv().await else { panic!("wrong response"); }; - //info!("syncer info = {info:?}"); + //info!("peer tracker info = {info:?}"); Ok(info) } - pub async fn peer_tracker_info(&mut self) -> Result { - self.channel.send(NodeCommand::GetPeerTrackerInfo); - let Some(NodeResponse::PeerTrackerInfo(info)) = self.channel.recv().await else { + pub async fn wait_connected(&mut self) { + self.channel.send(NodeCommand::WaitConnected(false)); + let Some(NodeResponse::Connected(_)) = self.channel.recv().await else { panic!("wrong response"); }; - //info!("peer tracker info = {info:?}"); + } + pub async fn wait_connected_trusted(&mut self) { + self.channel.send(NodeCommand::WaitConnected(true)); + let Some(NodeResponse::Connected(_)) = self.channel.recv().await else { + panic!("wrong response"); + }; + } + + pub async fn network_info(&mut self) -> Result { + self.channel.send(NodeCommand::GetNetworkInfo); + let Some(NodeResponse::NetworkInfo(info)) = self.channel.recv().await else { + panic!("wrong response"); + }; + info!("network info = {info:?}"); Ok(info) } + pub async fn listeners(&mut self) -> Result { + todo!() + } + pub async fn connected_peers(&mut self) -> Result { self.channel.send(NodeCommand::GetConnectedPeers); let Some(NodeResponse::ConnectedPeers(peers)) = self.channel.recv().await else { @@ -122,25 +139,121 @@ impl NodeDriver { Ok(peers) } + pub async fn set_peer_trust(&mut self, peer_id: &str, is_trusted: bool) -> Result<()> { + self.channel.send(NodeCommand::SetPeerTrust { + peer_id: peer_id.to_string(), + is_trusted, + }); + let Some(NodeResponse::PeerTrust { + peer_id, + is_trusted, + }) = self.channel.recv().await + else { + panic!("wrong"); + }; + Ok(()) // todo: api v2 + } + + pub async fn request_head_header(&mut self) -> Result { + self.channel.send(NodeCommand::RequestHeadHeader); + let Some(NodeResponse::Header(head_header)) = self.channel.recv().await else { + panic!("wrong"); + }; + Ok(head_header) + } + pub async fn request_header_by_hash(&mut self, hash: &str) -> Result { + self.channel + .send(NodeCommand::RequestHeader(HeaderQuery::ByHash( + hash.parse()?, + ))); + let Some(NodeResponse::Header(header)) = self.channel.recv().await else { + panic!("wrong response"); + }; + Ok(header) + } + pub async fn request_header_by_height(&mut self, height: u64) -> Result { + self.channel + .send(NodeCommand::RequestHeader(HeaderQuery::ByHeight(height))); + let Some(NodeResponse::Header(header)) = self.channel.recv().await else { + panic!("wrong response"); + }; + Ok(header) + } + pub async fn request_verified_headers(&mut self, from: JsValue, amount: u64) -> Result { + self.channel + .send(NodeCommand::RequestHeader(HeaderQuery::GetVerified { + from, + amount, + })); + let Some(NodeResponse::VerifiedHeaders(header)) = self.channel.recv().await else { + panic!("wrong response"); + }; + Ok(header) + } + pub async fn syncer_info(&mut self) -> Result { + self.channel.send(NodeCommand::GetSyncerInfo); + let Some(NodeResponse::SyncerInfo(info)) = self.channel.recv().await else { + panic!("wrong response"); + }; + //info!("syncer info = {info:?}"); + Ok(info) + } + pub async fn get_network_head_header(&mut self) -> Result { self.channel.send(NodeCommand::GetNetworkHeadHeader); - let Some(NodeResponse::NetworkHeadHeader(header)) = self.channel.recv().await else { + let Some(NodeResponse::Header(header)) = self.channel.recv().await else { + panic!("wrong response"); + }; + //info!("network head header = {header:?}"); + Ok(header) + } + pub async fn get_local_head_header(&mut self) -> Result { + self.channel.send(NodeCommand::GetLocalHeadHeader); + let Some(NodeResponse::Header(header)) = self.channel.recv().await else { panic!("wrong response"); }; //info!("network head header = {header:?}"); Ok(header) } - /* - pub async fn network_info(&self) -> Result { - self.channel.send(NodeCommand::GetNetworkInfo); - let Some(NodeResponse::NetworkInfo(info)) = self.channel.recv().await else { + pub async fn get_header_by_hash(&mut self, hash: &str) -> Result { + self.channel + .send(NodeCommand::GetHeader(HeaderQuery::ByHash(hash.parse()?))); + let Some(NodeResponse::Header(header)) = self.channel.recv().await else { panic!("wrong response"); }; - info!("network info = {info:?}"); - Ok(info) + Ok(header) + } + pub async fn get_header_by_height(&mut self, height: u64) -> Result { + self.channel + .send(NodeCommand::GetHeader(HeaderQuery::ByHeight(height))); + let Some(NodeResponse::Header(header)) = self.channel.recv().await else { + panic!("wrong response"); + }; + Ok(header) + } + pub async fn get_headers( + &mut self, + start_height: Option, + end_height: Option, + ) -> Result { + self.channel + .send(NodeCommand::GetHeader(HeaderQuery::Range { + start_height, + end_height, + })); + let Some(NodeResponse::HeaderArray(headers)) = self.channel.recv().await else { + panic!("wrong response"); + }; + Ok(headers) + } + pub async fn get_sampling_metadata(&mut self, height: u64) -> Result { + self.channel.send(NodeCommand::GetSamplingMetadata(height)); + let Some(NodeResponse::SamplingMetadata(metadata)) = self.channel.recv().await else { + panic!("wrong response"); + }; + Ok(metadata) } - */ } #[wasm_bindgen(js_class = Node)] @@ -185,7 +298,7 @@ impl WasmNode { } /// Get current network info. - pub async fn network_info(&self) -> Result { + pub async fn network_info(&self) -> Result { Ok(self.0.network_info().await?.into()) } diff --git a/node-wasm/src/worker.rs b/node-wasm/src/worker.rs index 3ca92794..59edae8f 100644 --- a/node-wasm/src/worker.rs +++ b/node-wasm/src/worker.rs @@ -1,14 +1,9 @@ -use crate::node::WasmNodeConfig; -use crate::utils::BChannel; -use lumina_node::node::Node; -use lumina_node::store::{IndexedDbStore, Store}; use serde::{Deserialize, Serialize}; use tracing::{info, trace, warn}; use wasm_bindgen::prelude::*; -use crate::utils::js_value_from_display; -use crate::utils::WorkerSelf; -use crate::Result; +use celestia_types::hash::Hash; +use celestia_types::ExtendedHeader; use instant::Instant; use js_sys::Array; use serde_wasm_bindgen::{from_value, to_value}; @@ -18,6 +13,16 @@ use web_sys::MessageEvent; use web_sys::MessagePort; use web_sys::SharedWorker; +use lumina_node::node::Node; +use lumina_node::store::{IndexedDbStore, Store}; + +use crate::node::WasmNodeConfig; +use crate::utils::js_value_from_display; +use crate::utils::BChannel; +use crate::utils::WorkerSelf; +use crate::wrapper::libp2p::NetworkInfoSnapshot; +use crate::Result; + #[derive(Serialize, Deserialize, Debug)] pub enum NodeCommand { IsRunning, @@ -25,10 +30,32 @@ pub enum NodeCommand { GetLocalPeerId, GetSyncerInfo, GetPeerTrackerInfo, - //GetNetworkInfo, + GetNetworkInfo, GetConnectedPeers, GetNetworkHeadHeader, GetLocalHeadHeader, + SetPeerTrust { peer_id: String, is_trusted: bool }, + RequestHeadHeader, + WaitConnected(bool), + GetListeners, + RequestHeader(HeaderQuery), + GetHeader(HeaderQuery), + GetSamplingMetadata(u64), +} + +#[derive(Serialize, Deserialize, Debug)] +pub enum HeaderQuery { + ByHash(Hash), + ByHeight(u64), + GetVerified { + #[serde(with = "serde_wasm_bindgen::preserve")] + from: JsValue, + amount: u64, + }, + Range { + start_height: Option, + end_height: Option, + }, } #[derive(Serialize, Deserialize, Debug)] @@ -36,18 +63,26 @@ pub enum NodeResponse { Running(bool), Started(u64), LocalPeerId(String), + Connected(bool), #[serde(with = "serde_wasm_bindgen::preserve")] SyncerInfo(JsValue), #[serde(with = "serde_wasm_bindgen::preserve")] PeerTrackerInfo(JsValue), - //#[serde(with = "serde_wasm_bindgen::preserve")] - //NetworkInfo(NetworkInfo), + NetworkInfo(NetworkInfoSnapshot), #[serde(with = "serde_wasm_bindgen::preserve")] ConnectedPeers(Array), + PeerTrust { + peer_id: String, + is_trusted: bool, + }, #[serde(with = "serde_wasm_bindgen::preserve")] - NetworkHeadHeader(JsValue), + Header(JsValue), #[serde(with = "serde_wasm_bindgen::preserve")] - LocalHeadHeader(JsValue), + HeaderArray(Array), + #[serde(with = "serde_wasm_bindgen::preserve")] + VerifiedHeaders(Array), + #[serde(with = "serde_wasm_bindgen::preserve")] + SamplingMetadata(JsValue), } struct NodeWorker { @@ -85,11 +120,57 @@ impl NodeWorker { Ok(to_value(&self.node.syncer_info().await?)?) } - /* - async fn network_info(&self) -> Result { + async fn network_info(&self) -> Result { Ok(self.node.network_info().await?.into()) } - */ + + async fn request_header_by_hash(&self, hash: Hash) -> Result { + Ok(to_value(&self.node.request_header_by_hash(&hash).await?)?) + } + + async fn request_header_by_height(&self, height: u64) -> Result { + Ok(to_value( + &self.node.request_header_by_height(height).await?, + )?) + } + + async fn get_header_by_hash(&self, hash: Hash) -> Result { + Ok(to_value(&self.node.get_header_by_hash(&hash).await?)?) + } + + async fn get_header_by_height(&self, height: u64) -> Result { + Ok(to_value(&self.node.get_header_by_height(height).await?)?) + } + + async fn get_headers( + &self, + start_height: Option, + end_height: Option, + ) -> Result { + let headers = match (start_height, end_height) { + (None, None) => self.node.get_headers(..).await, + (Some(start), None) => self.node.get_headers(start..).await, + (None, Some(end)) => self.node.get_headers(..=end).await, + (Some(start), Some(end)) => self.node.get_headers(start..=end).await, + }?; + + Ok(to_value(&headers)?.into()) + } + + async fn request_verified_headers(&self, from: ExtendedHeader, amount: u64) -> Result { + Ok(to_value(&self.node.request_verified_headers(&from, amount).await?)?.into()) + } + + async fn get_sampling_metadata(&self, height: u64) -> Result { + Ok(to_value(&self.node.get_sampling_metadata(height).await?)?) + } + + async fn set_peer_trust(&self, peer_id: String, is_trusted: bool) -> Result<()> { + Ok(self + .node + .set_peer_trust(peer_id.parse()?, is_trusted) + .await?) + } async fn connected_peers(&self) -> Result { Ok(self @@ -105,12 +186,24 @@ impl NodeWorker { Ok(to_value(&self.node.get_network_head_header())?) } + async fn wait_connected(&self, trusted: bool) { + if trusted { + self.node.wait_connected().await; + } else { + self.node.wait_connected_trusted().await; + } + } + async fn local_head_header(&self) -> Result { Ok(to_value(&self.node.get_local_head_header().await?)?) } + async fn request_head_header(&self) -> Result { + Ok(to_value(&self.node.request_head_header().await?)?) + } async fn process_command(&mut self, command: NodeCommand) -> NodeResponse { match command { + // TODO: order NodeCommand::IsRunning => NodeResponse::Running(true), NodeCommand::Start(_config) => NodeResponse::Started( Instant::now() @@ -124,19 +217,79 @@ impl NodeWorker { } NodeCommand::GetPeerTrackerInfo => { NodeResponse::PeerTrackerInfo(self.peer_tracker_info().ok().unwrap()) - } /* + } NodeCommand::GetNetworkInfo => { - NodeResponse::NetworkInfo(self.network_info().await.ok().unwrap()) + NodeResponse::NetworkInfo(self.network_info().await.ok().unwrap()) } - */ NodeCommand::GetConnectedPeers => { NodeResponse::ConnectedPeers(self.connected_peers().await.ok().unwrap()) } NodeCommand::GetNetworkHeadHeader => { - NodeResponse::NetworkHeadHeader(self.network_head_header().await.ok().unwrap()) + NodeResponse::Header(self.network_head_header().await.ok().unwrap()) } NodeCommand::GetLocalHeadHeader => { - NodeResponse::LocalHeadHeader(self.local_head_header().await.ok().unwrap()) + NodeResponse::Header(self.local_head_header().await.ok().unwrap()) + } + NodeCommand::SetPeerTrust { + peer_id, + is_trusted, + } => { + //XXX + self.set_peer_trust(peer_id.clone(), is_trusted) + .await + .ok() + .unwrap(); + NodeResponse::PeerTrust { + peer_id, + is_trusted, + } + } + NodeCommand::RequestHeadHeader => { + NodeResponse::Header(self.request_head_header().await.ok().unwrap()) + } + NodeCommand::WaitConnected(trusted) => { + todo!() + } + NodeCommand::GetListeners => { + todo!() + } + NodeCommand::RequestHeader(HeaderQuery::ByHash(hash)) => { + NodeResponse::Header(self.request_header_by_hash(hash).await.ok().unwrap()) + } + NodeCommand::RequestHeader(HeaderQuery::ByHeight(height)) => { + NodeResponse::Header(self.request_header_by_height(height).await.ok().unwrap()) + } + NodeCommand::GetHeader(HeaderQuery::ByHash(hash)) => { + NodeResponse::Header(self.get_header_by_hash(hash).await.ok().unwrap()) + } + NodeCommand::GetHeader(HeaderQuery::ByHeight(height)) => { + NodeResponse::Header(self.get_header_by_height(height).await.ok().unwrap()) + } + NodeCommand::GetSamplingMetadata(height) => NodeResponse::SamplingMetadata( + self.get_sampling_metadata(height).await.ok().unwrap(), + ), + NodeCommand::RequestHeader(HeaderQuery::GetVerified { from, amount }) => { + NodeResponse::VerifiedHeaders( + self.request_verified_headers(from_value(from).ok().unwrap(), amount) + .await + .ok() + .unwrap(), + ) + } + NodeCommand::GetHeader(HeaderQuery::GetVerified { .. }) => unimplemented!(), + NodeCommand::RequestHeader(HeaderQuery::Range { .. }) => unimplemented!(), + NodeCommand::GetHeader(HeaderQuery::Range { + start_height, + end_height, + }) => NodeResponse::HeaderArray( + self.get_headers(start_height, end_height) + .await + .ok() + .unwrap(), + ), + NodeCommand::WaitConnected(trusted) => { + self.wait_connected(trusted).await; // TODO: nonblocking ( Promise? ) + NodeResponse::Connected(trusted) } } } diff --git a/node-wasm/src/wrapper/libp2p.rs b/node-wasm/src/wrapper/libp2p.rs index d3a86b33..b3d8c7e3 100644 --- a/node-wasm/src/wrapper/libp2p.rs +++ b/node-wasm/src/wrapper/libp2p.rs @@ -1,8 +1,51 @@ use libp2p::swarm::{ ConnectionCounters as SwarmConnectionCounters, NetworkInfo as SwarmNetworkInfo, }; +use serde::{Deserialize, Serialize}; use wasm_bindgen::prelude::*; +#[wasm_bindgen] +#[derive(Debug, Serialize, Deserialize)] +pub struct NetworkInfoSnapshot { + pub num_peers: usize, + connection_counters: ConnectionCountersSnapshot, +} + +#[wasm_bindgen] +#[derive(Debug, Serialize, Deserialize)] +pub struct ConnectionCountersSnapshot { + pub num_connections: u32, + pub num_pending: u32, + pub num_pending_incoming: u32, + pub num_pending_outgoing: u32, + pub num_established_incoming: u32, + pub num_established_outgoing: u32, + pub num_established: u32, +} + +impl From for NetworkInfoSnapshot { + fn from(info: SwarmNetworkInfo) -> Self { + Self { + num_peers: info.num_peers(), + connection_counters: ConnectionCountersSnapshot::from(info.connection_counters()), + } + } +} + +impl From<&SwarmConnectionCounters> for ConnectionCountersSnapshot { + fn from(counters: &SwarmConnectionCounters) -> Self { + Self { + num_connections: counters.num_connections(), + num_pending: counters.num_pending(), + num_pending_incoming: counters.num_pending_incoming(), + num_pending_outgoing: counters.num_pending_outgoing(), + num_established_incoming: counters.num_established_incoming(), + num_established_outgoing: counters.num_established_outgoing(), + num_established: counters.num_established(), + } + } +} + #[wasm_bindgen] pub struct NetworkInfo(SwarmNetworkInfo); From 830f8eb7adad4cbca3d012c70a5689571d7ce373 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Florkiewicz?= Date: Wed, 10 Apr 2024 22:32:17 +0200 Subject: [PATCH 03/35] add type safety --- node-wasm/src/node.rs | 224 +++++++-------- node-wasm/src/utils.rs | 110 +++---- node-wasm/src/worker.rs | 602 ++++++++++++++++++++++++++++++++------- node/src/peer_tracker.rs | 4 +- node/src/syncer.rs | 4 +- 5 files changed, 646 insertions(+), 298 deletions(-) diff --git a/node-wasm/src/node.rs b/node-wasm/src/node.rs index 4cd26e60..3c2ebbad 100644 --- a/node-wasm/src/node.rs +++ b/node-wasm/src/node.rs @@ -20,10 +20,18 @@ use crate::utils::js_value_from_display; use crate::utils::BChannel; use crate::utils::JsContext; use crate::utils::Network; -use crate::worker::{HeaderQuery, NodeCommand, NodeResponse}; +use crate::utils::NodeCommandType; +use crate::worker::{MultipleHeaderQuery, NodeCommand, NodeResponse, SingleHeaderQuery}; use crate::wrapper::libp2p::NetworkInfoSnapshot; use crate::Result; +use crate::worker::RequestMultipleHeaders; +use crate::worker::{ + GetConnectedPeers, GetHeader, GetListeners, GetLocalPeerId, GetMultipleHeaders, GetNetworkInfo, + GetPeerTrackerInfo, GetSamplingMetadata, GetSyncerInfo, IsRunning, RequestHeader, SetPeerTrust, + StartNode, WaitConnected, +}; + use web_sys::{SharedWorker, WorkerOptions, WorkerType}; /// Lumina wasm node. @@ -70,189 +78,153 @@ impl NodeDriver { } pub async fn is_running(&mut self) -> bool { - self.channel.send(NodeCommand::IsRunning); - let Some(NodeResponse::Running(running)) = self.channel.recv().await else { - panic!("wrong reponse"); - }; - running + let response = self.channel.send(IsRunning); + + response.await.unwrap() } pub async fn start(&mut self, config: WasmNodeConfig) { - let command = NodeCommand::Start(config); - self.channel.send(command); - let Some(NodeResponse::Started(uptime)) = self.channel.recv().await else { - panic!("wrong reponse"); - }; - info!("started = {uptime:?}"); + let command = StartNode(config); + let response = self.channel.send(command); + + response.await.unwrap(); // XXX return type } pub async fn local_peer_id(&mut self) -> String { - self.channel.send(NodeCommand::GetLocalPeerId); - let Some(NodeResponse::LocalPeerId(id)) = self.channel.recv().await else { - panic!("wrong reponse"); - }; - info!("peer id = {id:?}"); - id + let response = self.channel.send(GetLocalPeerId); + + response.await.unwrap() } pub async fn peer_tracker_info(&mut self) -> Result { - self.channel.send(NodeCommand::GetPeerTrackerInfo); - let Some(NodeResponse::PeerTrackerInfo(info)) = self.channel.recv().await else { - panic!("wrong response"); - }; - //info!("peer tracker info = {info:?}"); - Ok(info) + let response = self.channel.send(GetPeerTrackerInfo); + + Ok(to_value(&response.await.unwrap())?) } pub async fn wait_connected(&mut self) { - self.channel.send(NodeCommand::WaitConnected(false)); - let Some(NodeResponse::Connected(_)) = self.channel.recv().await else { - panic!("wrong response"); - }; + let command = WaitConnected { trusted: false }; + let response = self.channel.send(command); + + response.await.unwrap() } pub async fn wait_connected_trusted(&mut self) { - self.channel.send(NodeCommand::WaitConnected(true)); - let Some(NodeResponse::Connected(_)) = self.channel.recv().await else { - panic!("wrong response"); - }; + let command = WaitConnected { trusted: false }; + let response = self.channel.send(command); + + response.await.unwrap() } pub async fn network_info(&mut self) -> Result { - self.channel.send(NodeCommand::GetNetworkInfo); - let Some(NodeResponse::NetworkInfo(info)) = self.channel.recv().await else { - panic!("wrong response"); - }; - info!("network info = {info:?}"); - Ok(info) + let response = self.channel.send(GetNetworkInfo); + + Ok(response.await.unwrap()) } pub async fn listeners(&mut self) -> Result { - todo!() + let response = self.channel.send(GetListeners); + let response = response + .await + .unwrap() + .iter() + .map(js_value_from_display) + .collect(); + + Ok(response) } pub async fn connected_peers(&mut self) -> Result { - self.channel.send(NodeCommand::GetConnectedPeers); - let Some(NodeResponse::ConnectedPeers(peers)) = self.channel.recv().await else { - panic!("wrong"); - }; - //info!("peers = {peers:?}"); - Ok(peers) + let response = self.channel.send(GetConnectedPeers); + let response = response.await?.iter().map(js_value_from_display).collect(); + + Ok(response) } pub async fn set_peer_trust(&mut self, peer_id: &str, is_trusted: bool) -> Result<()> { - self.channel.send(NodeCommand::SetPeerTrust { + let command = SetPeerTrust { peer_id: peer_id.to_string(), is_trusted, - }); - let Some(NodeResponse::PeerTrust { - peer_id, - is_trusted, - }) = self.channel.recv().await - else { - panic!("wrong"); }; - Ok(()) // todo: api v2 + let response = self.channel.send(command); + + Ok(response.await.unwrap()) } pub async fn request_head_header(&mut self) -> Result { - self.channel.send(NodeCommand::RequestHeadHeader); - let Some(NodeResponse::Header(head_header)) = self.channel.recv().await else { - panic!("wrong"); - }; - Ok(head_header) + let command = RequestHeader(SingleHeaderQuery::Head); + let response = self.channel.send(command); + + Ok(to_value(&response.await.unwrap())?) } pub async fn request_header_by_hash(&mut self, hash: &str) -> Result { - self.channel - .send(NodeCommand::RequestHeader(HeaderQuery::ByHash( - hash.parse()?, - ))); - let Some(NodeResponse::Header(header)) = self.channel.recv().await else { - panic!("wrong response"); - }; - Ok(header) + let command = RequestHeader(SingleHeaderQuery::ByHash(hash.parse()?)); + let response = self.channel.send(command); + + Ok(to_value(&response.await.unwrap())?) } pub async fn request_header_by_height(&mut self, height: u64) -> Result { - self.channel - .send(NodeCommand::RequestHeader(HeaderQuery::ByHeight(height))); - let Some(NodeResponse::Header(header)) = self.channel.recv().await else { - panic!("wrong response"); - }; - Ok(header) + let command = RequestHeader(SingleHeaderQuery::ByHeight(height)); + let response = self.channel.send(command); + + Ok(to_value(&response.await.unwrap())?) } pub async fn request_verified_headers(&mut self, from: JsValue, amount: u64) -> Result { - self.channel - .send(NodeCommand::RequestHeader(HeaderQuery::GetVerified { - from, - amount, - })); - let Some(NodeResponse::VerifiedHeaders(header)) = self.channel.recv().await else { - panic!("wrong response"); - }; - Ok(header) + let command = RequestMultipleHeaders(MultipleHeaderQuery::GetVerified { from, amount }); + let response = self.channel.send(command); + + Ok(response.await.unwrap()) } pub async fn syncer_info(&mut self) -> Result { - self.channel.send(NodeCommand::GetSyncerInfo); - let Some(NodeResponse::SyncerInfo(info)) = self.channel.recv().await else { - panic!("wrong response"); - }; - //info!("syncer info = {info:?}"); - Ok(info) + let response = self.channel.send(GetSyncerInfo); + + let response = response.await.unwrap(); + + Ok(to_value(&response)?) } pub async fn get_network_head_header(&mut self) -> Result { - self.channel.send(NodeCommand::GetNetworkHeadHeader); - let Some(NodeResponse::Header(header)) = self.channel.recv().await else { - panic!("wrong response"); - }; - //info!("network head header = {header:?}"); - Ok(header) + let command = RequestHeader(SingleHeaderQuery::Head); // XXX ??? + let response = self.channel.send(command); + + Ok(to_value(&response.await.unwrap())?) } pub async fn get_local_head_header(&mut self) -> Result { - self.channel.send(NodeCommand::GetLocalHeadHeader); - let Some(NodeResponse::Header(header)) = self.channel.recv().await else { - panic!("wrong response"); - }; - //info!("network head header = {header:?}"); - Ok(header) + let command = GetHeader(SingleHeaderQuery::Head); // XXX ??? + let response = self.channel.send(command); + + Ok(to_value(&response.await.unwrap())?) } pub async fn get_header_by_hash(&mut self, hash: &str) -> Result { - self.channel - .send(NodeCommand::GetHeader(HeaderQuery::ByHash(hash.parse()?))); - let Some(NodeResponse::Header(header)) = self.channel.recv().await else { - panic!("wrong response"); - }; - Ok(header) + let command = GetHeader(SingleHeaderQuery::ByHash(hash.parse()?)); + let response = self.channel.send(command); + + Ok(to_value(&response.await.unwrap())?) } pub async fn get_header_by_height(&mut self, height: u64) -> Result { - self.channel - .send(NodeCommand::GetHeader(HeaderQuery::ByHeight(height))); - let Some(NodeResponse::Header(header)) = self.channel.recv().await else { - panic!("wrong response"); - }; - Ok(header) + let command = GetHeader(SingleHeaderQuery::ByHeight(height)); + let response = self.channel.send(command); + + Ok(to_value(&response.await.unwrap())?) } pub async fn get_headers( &mut self, start_height: Option, end_height: Option, ) -> Result { - self.channel - .send(NodeCommand::GetHeader(HeaderQuery::Range { - start_height, - end_height, - })); - let Some(NodeResponse::HeaderArray(headers)) = self.channel.recv().await else { - panic!("wrong response"); - }; - Ok(headers) + let command = GetMultipleHeaders(MultipleHeaderQuery::Range { + start_height, + end_height, + }); + let response = self.channel.send(command); + + Ok(response.await.unwrap()) } pub async fn get_sampling_metadata(&mut self, height: u64) -> Result { - self.channel.send(NodeCommand::GetSamplingMetadata(height)); - let Some(NodeResponse::SamplingMetadata(metadata)) = self.channel.recv().await else { - panic!("wrong response"); - }; - Ok(metadata) + let command = GetSamplingMetadata { height }; + let response = self.channel.send(command); + + Ok(to_value(&response.await.unwrap())?) } } diff --git a/node-wasm/src/utils.rs b/node-wasm/src/utils.rs index 1fcd497c..ba420df3 100644 --- a/node-wasm/src/utils.rs +++ b/node-wasm/src/utils.rs @@ -1,6 +1,7 @@ //! Various utilities for interacting with node from wasm. use std::fmt; +use std::fmt::Debug; use std::marker::PhantomData; use lumina_node::network; @@ -12,18 +13,51 @@ use tracing_subscriber::prelude::*; use tracing_web::{performance_layer, MakeConsoleWriter}; use wasm_bindgen::prelude::*; -use crate::node::WasmNodeConfig; +use crate::worker::NodeCommand; +use crate::worker::NodeResponse; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; use serde_wasm_bindgen::from_value; use serde_wasm_bindgen::to_value; use tokio::sync::mpsc; +use tokio::sync::oneshot; use wasm_bindgen_futures::spawn_local; use web_sys::MessageEvent; use web_sys::MessagePort; use web_sys::SharedWorker; use web_sys::SharedWorkerGlobalScope; +pub type CommandResponseChannel = oneshot::Sender; + +#[derive(Serialize, Deserialize, Debug)] +pub struct NodeCommandResponse(T::Output) +where + T: NodeCommandType, + T::Output: Debug + Serialize; +//{ output: T::Output, } + +//command: PhantomDataT, +//tx: oneshot::Sender, + +/* +#[derive(Serialize, Deserialize, Debug)] +pub struct NodeCommandSender { + parameters: T::Input, +} + +impl NodeCommandSender { + pub fn new(parameters: T::Input) -> Self { + Self { parameters } + } +} +*/ + +pub trait NodeCommandType: Debug + Into { + type Output; + + //fn response(&self, output: Self::Output) -> NodeResponse; +} + pub struct BChannel { _onmessage: Closure, channel: MessagePort, @@ -63,7 +97,14 @@ where } } - pub fn send(&self, msg: IN) { + pub fn send(&self, command: T) -> oneshot::Receiver { + let message: NodeCommand = command.into(); + self.send_enum(message); + + todo!() + } + + fn send_enum(&self, msg: NodeCommand) { let v = to_value(&msg).unwrap(); self.channel.post_message(&v).expect("err post"); } @@ -76,71 +117,6 @@ where // TODO: cleanup JS objects on drop // impl Drop -/* -#[derive(Serialize, Deserialize)] -enum BroadcastMessage { - Foo -} - -fn create_channel() { - - let broadcast_channel = BChannel::new("lumina"); - - broadcast_channel.send(BroadcastMessage::Foo); -} -*/ - -/* -#[derive(Serialize, Deserialize, Debug)] -pub enum NodeCommand { - //ConnectDriver(u64), - Start(WasmNodeConfig), - GetLocalPeerId, -} - -#[derive(Serialize, Deserialize, Debug)] -pub enum NodeResponse { - LocalPeerId(String), -} -*/ - -/* -#[wasm_bindgen] -struct BSharedWorker { - worker: SharedWorker, - channel: BChannel, -} - -#[wasm_bindgen] -impl BSharedWorker { - pub fn new() -> Self { - let mut opts = WorkerOptions::new(); - opts.type_(WorkerType::Module); - let worker = SharedWorker::new_with_worker_options("/js/worker.js", &opts) - .expect("could not worker"); - - let channel = BChannel::new(worker.port()); - - Self { worker, channel } - } -} -*/ - -/* -#[wasm_bindgen] -pub fn launch_worker() -> BSharedWorker { - info!("doing the worker"); - let mut opts = WorkerOptions::new(); - opts.type_(WorkerType::Module); - let worker = - SharedWorker::new_with_worker_options("/js/worker.js", &opts).expect("could not worker"); - - let port = worker.port(); - - info!("did the worker"); -} -*/ - pub(crate) trait WorkerSelf { type GlobalScope; diff --git a/node-wasm/src/worker.rs b/node-wasm/src/worker.rs index 59edae8f..127ed312 100644 --- a/node-wasm/src/worker.rs +++ b/node-wasm/src/worker.rs @@ -1,3 +1,5 @@ +use std::fmt::Debug; + use serde::{Deserialize, Serialize}; use tracing::{info, trace, warn}; use wasm_bindgen::prelude::*; @@ -15,38 +17,312 @@ use web_sys::SharedWorker; use lumina_node::node::Node; use lumina_node::store::{IndexedDbStore, Store}; +use lumina_node::syncer::SyncingInfo; use crate::node::WasmNodeConfig; use crate::utils::js_value_from_display; -use crate::utils::BChannel; +use crate::utils::CommandResponseChannel; +use crate::utils::NodeCommandResponse; +use crate::utils::NodeCommandType; use crate::utils::WorkerSelf; +//use crate::worker::NodeResponse; use crate::wrapper::libp2p::NetworkInfoSnapshot; use crate::Result; +use libp2p::Multiaddr; +use lumina_node::peer_tracker::PeerTrackerInfo; +use lumina_node::store::SamplingMetadata; +use tokio::sync::oneshot; + +#[derive(Debug, Serialize, Deserialize)] +pub struct IsRunning; +impl NodeCommandType for IsRunning { + type Output = bool; +} +impl From for NodeCommand { + fn from(command: IsRunning) -> NodeCommand { + NodeCommand::IsRunning(command) + } +} + +#[derive(Debug, Serialize, Deserialize)] +enum NodeState { + NodeStopped, + NodeStarted, + AlreadyRunning(u64), +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct StartNode(pub WasmNodeConfig); +impl NodeCommandType for StartNode { + type Output = NodeState; +} +impl From for NodeCommand { + fn from(command: StartNode) -> NodeCommand { + NodeCommand::StartNode(command) + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct GetLocalPeerId; +impl NodeCommandType for GetLocalPeerId { + type Output = String; +} +impl From for NodeCommand { + fn from(command: GetLocalPeerId) -> NodeCommand { + NodeCommand::GetLocalPeerId(command) + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct GetSyncerInfo; +impl NodeCommandType for GetSyncerInfo { + type Output = SyncingInfo; +} +impl From for NodeCommand { + fn from(command: GetSyncerInfo) -> NodeCommand { + NodeCommand::GetSyncerInfo(command) + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct GetPeerTrackerInfo; +impl NodeCommandType for GetPeerTrackerInfo { + type Output = PeerTrackerInfo; +} +impl From for NodeCommand { + fn from(command: GetPeerTrackerInfo) -> NodeCommand { + NodeCommand::GetPeerTrackerInfo(command) + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct GetNetworkInfo; +impl NodeCommandType for GetNetworkInfo { + type Output = NetworkInfoSnapshot; +} +impl From for NodeCommand { + fn from(command: GetNetworkInfo) -> NodeCommand { + NodeCommand::GetNetworkInfo(command) + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct GetConnectedPeers; +impl NodeCommandType for GetConnectedPeers { + type Output = Vec; +} +impl From for NodeCommand { + fn from(command: GetConnectedPeers) -> NodeCommand { + NodeCommand::GetConnectedPeers(command) + } +} + +/* +#[derive(Debug, Serialize, Deserialize)] +pub struct GetNetworkHeadHeader; +impl NodeCommandType for GetNetworkHeadHeader { + type Output = JsValue; +} +impl From for NodeCommand { + fn from(command: GetNetworkHeadHeader) -> NodeCommand { + NodeCommand::GetNetworkHeadHeader(command) + } +} +*/ + +/* +#[derive(Debug, Serialize, Deserialize)] +pub struct GetLocalHeadHeader; +impl NodeCommandType for GetLocalHeadHeader { + type Output = JsValue; +} +impl From for NodeCommand { + fn from(command: GetLocalHeadHeader) -> NodeCommand { + NodeCommand::GetLocalHeadHeader(command) + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct RequestHeadHeader; +impl NodeCommandType for RequestHeadHeader { + type Output = JsValue; +} +impl From for NodeCommand { + fn from(command: RequestHeadHeader) -> NodeCommand { + NodeCommand::RequestHeadHeader(command) + } +} +*/ + +#[derive(Debug, Serialize, Deserialize)] +pub struct SetPeerTrust { + peer_id: String, + is_trusted: bool, +} +impl NodeCommandType for SetPeerTrust { + type Output = (); +} +impl From for NodeCommand { + fn from(command: SetPeerTrust) -> NodeCommand { + NodeCommand::SetPeerTrust(command) + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct WaitConnected { + trusted: bool, +} +impl NodeCommandType for WaitConnected { + type Output = (); +} +impl From for NodeCommand { + fn from(command: WaitConnected) -> NodeCommand { + NodeCommand::WaitConnected(command) + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct GetListeners; +impl NodeCommandType for GetListeners { + type Output = Vec; +} +impl From for NodeCommand { + fn from(command: GetListeners) -> NodeCommand { + NodeCommand::GetListeners(command) + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct RequestHeader(pub SingleHeaderQuery); +impl NodeCommandType for RequestHeader { + type Output = ExtendedHeader; +} +impl From for NodeCommand { + fn from(command: RequestHeader) -> NodeCommand { + NodeCommand::RequestHeader(command) + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct RequestMultipleHeaders(pub MultipleHeaderQuery); +impl NodeCommandType for RequestMultipleHeaders { + type Output = Array; +} +impl From for NodeCommand { + fn from(command: RequestMultipleHeaders) -> NodeCommand { + NodeCommand::RequestMultipleHeaders(command) + } +} +#[derive(Debug, Serialize, Deserialize)] +pub struct GetHeader(pub SingleHeaderQuery); +impl NodeCommandType for GetHeader { + type Output = ExtendedHeader; +} +impl From for NodeCommand { + fn from(command: GetHeader) -> NodeCommand { + NodeCommand::GetHeader(command) + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct GetMultipleHeaders(pub MultipleHeaderQuery); +impl NodeCommandType for GetMultipleHeaders { + type Output = Array; +} +impl From for NodeCommand { + fn from(command: GetMultipleHeaders) -> NodeCommand { + NodeCommand::GetMultipleHeaders(command) + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct GetSamplingMetadata { + pub height: u64, +} +impl NodeCommandType for GetSamplingMetadata { + type Output = SamplingMetadata; +} +impl From for NodeCommand { + fn from(command: GetSamplingMetadata) -> NodeCommand { + NodeCommand::GetSamplingMetadata(command) + } +} + +// type being transmitted over the JS channel #[derive(Serialize, Deserialize, Debug)] pub enum NodeCommand { - IsRunning, - Start(WasmNodeConfig), - GetLocalPeerId, - GetSyncerInfo, - GetPeerTrackerInfo, - GetNetworkInfo, - GetConnectedPeers, - GetNetworkHeadHeader, - GetLocalHeadHeader, - SetPeerTrust { peer_id: String, is_trusted: bool }, - RequestHeadHeader, - WaitConnected(bool), - GetListeners, - RequestHeader(HeaderQuery), - GetHeader(HeaderQuery), - GetSamplingMetadata(u64), + IsRunning(IsRunning), + StartNode(StartNode), + GetLocalPeerId(GetLocalPeerId), + GetSyncerInfo(GetSyncerInfo), + GetPeerTrackerInfo(GetPeerTrackerInfo), + GetNetworkInfo(GetNetworkInfo), + GetConnectedPeers(GetConnectedPeers), + //GetNetworkHeadHeader, + //GetLocalHeadHeader, + SetPeerTrust(SetPeerTrust), + //RequestHeadHeader(RequestHeadHeader), + WaitConnected(WaitConnected), + GetListeners(GetListeners), + RequestHeader(RequestHeader), + RequestMultipleHeaders(RequestMultipleHeaders), + GetHeader(GetHeader), + GetMultipleHeaders(GetMultipleHeaders), + GetSamplingMetadata(GetSamplingMetadata), +} + +#[derive(Debug)] +pub enum NodeCommandWithChannel { + IsRunning((IsRunning, CommandResponseChannel)), + StartNode((StartNode, CommandResponseChannel)), + GetLocalPeerId((GetLocalPeerId, CommandResponseChannel)), + GetSyncerInfo((GetSyncerInfo, CommandResponseChannel)), + GetPeerTrackerInfo( + ( + GetPeerTrackerInfo, + CommandResponseChannel, + ), + ), + GetNetworkInfo((GetNetworkInfo, CommandResponseChannel)), + GetConnectedPeers((GetConnectedPeers, CommandResponseChannel)), + //GetNetworkHeadHeader, + //GetLocalHeadHeader, + SetPeerTrust((SetPeerTrust, CommandResponseChannel)), + //RequestHeadHeader((RequestHeadHeader, CommandResponseChannel)), + WaitConnected((WaitConnected, CommandResponseChannel)), + GetListeners((GetListeners, CommandResponseChannel)), + RequestHeader((RequestHeader, CommandResponseChannel)), + RequestMultipleHeaders( + ( + RequestMultipleHeaders, + CommandResponseChannel, + ), + ), + GetHeader((GetHeader, CommandResponseChannel)), + GetMultipleHeaders( + ( + GetMultipleHeaders, + CommandResponseChannel, + ), + ), + GetSamplingMetadata( + ( + GetSamplingMetadata, + CommandResponseChannel, + ), + ), } #[derive(Serialize, Deserialize, Debug)] -pub enum HeaderQuery { +pub enum SingleHeaderQuery { + Head, ByHash(Hash), ByHeight(u64), +} + +#[derive(Serialize, Deserialize, Debug)] +pub enum MultipleHeaderQuery { GetVerified { #[serde(with = "serde_wasm_bindgen::preserve")] from: JsValue, @@ -58,6 +334,27 @@ pub enum HeaderQuery { }, } +#[derive(Serialize, Deserialize, Debug)] +pub enum NodeResponse { + IsRunning(NodeCommandResponse), + StartNode(NodeCommandResponse), + GetLocalPeerId(NodeCommandResponse), + GetSyncerInfo(NodeCommandResponse), + GetPeerTrackerInfo(NodeCommandResponse), + GetNetworkInfo(NodeCommandResponse), + GetConnectedPeers(NodeCommandResponse), + //GetNetworkHeadHeader, + //GetLocalHeadHeader, + SetPeerTrust(NodeCommandResponse), + //RequestHeadHeader(NodeCommandResponse), + WaitConnected(NodeCommandResponse), + GetListeners(NodeCommandResponse), + RequestHeader(NodeCommandResponse), + GetHeader(NodeCommandResponse), + GetSamplingMetadata(NodeCommandResponse), +} + +/* #[derive(Serialize, Deserialize, Debug)] pub enum NodeResponse { Running(bool), @@ -83,7 +380,10 @@ pub enum NodeResponse { VerifiedHeaders(Array), #[serde(with = "serde_wasm_bindgen::preserve")] SamplingMetadata(JsValue), + #[serde(with = "serde_wasm_bindgen::preserve")] + Listeners(Array), } +*/ struct NodeWorker { node: Node, @@ -108,6 +408,7 @@ impl NodeWorker { } } + /* fn local_peer_id(&self) -> String { self.node.local_peer_id().to_string() } @@ -201,97 +502,184 @@ impl NodeWorker { async fn request_head_header(&self) -> Result { Ok(to_value(&self.node.request_head_header().await?)?) } - async fn process_command(&mut self, command: NodeCommand) -> NodeResponse { + */ + async fn process_command(&mut self, command: NodeCommandWithChannel) { match command { // TODO: order - NodeCommand::IsRunning => NodeResponse::Running(true), - NodeCommand::Start(_config) => NodeResponse::Started( - Instant::now() - .checked_duration_since(self.start_timestamp) - .map(|duration| duration.as_secs()) - .unwrap_or(0), - ), - NodeCommand::GetLocalPeerId => NodeResponse::LocalPeerId(self.local_peer_id()), - NodeCommand::GetSyncerInfo => { - NodeResponse::SyncerInfo(self.syncer_info().await.ok().unwrap()) + NodeCommandWithChannel::IsRunning((_, response)) => { + response.send(true).expect("channel_dropped") } - NodeCommand::GetPeerTrackerInfo => { - NodeResponse::PeerTrackerInfo(self.peer_tracker_info().ok().unwrap()) + NodeCommandWithChannel::StartNode((_, response)) => { + response + .send(NodeState::AlreadyRunning( + Instant::now() + .checked_duration_since(self.start_timestamp) + .map(|duration| duration.as_secs()) + .unwrap_or(0), + )) + .expect("channel_dropped"); } - NodeCommand::GetNetworkInfo => { - NodeResponse::NetworkInfo(self.network_info().await.ok().unwrap()) + NodeCommandWithChannel::GetLocalPeerId((_, response)) => { + response + .send(self.node.local_peer_id().to_string()) + .expect("channel_dropped"); } - NodeCommand::GetConnectedPeers => { - NodeResponse::ConnectedPeers(self.connected_peers().await.ok().unwrap()) + NodeCommandWithChannel::GetSyncerInfo((_, response)) => { + let syncer_info = self.node.syncer_info().await.unwrap(); + response.send(syncer_info).expect("channel_dropped"); } - NodeCommand::GetNetworkHeadHeader => { - NodeResponse::Header(self.network_head_header().await.ok().unwrap()) + NodeCommandWithChannel::GetPeerTrackerInfo((_, response)) => { + let peer_tracker_info = self.node.peer_tracker_info(); + response.send(peer_tracker_info).expect("channel_dropped"); } - NodeCommand::GetLocalHeadHeader => { - NodeResponse::Header(self.local_head_header().await.ok().unwrap()) + NodeCommandWithChannel::GetNetworkInfo((_, response)) => { + let network_info = self.node.network_info().await.expect("TODO").into(); + response.send(network_info).expect("channel_dropped") } - NodeCommand::SetPeerTrust { - peer_id, - is_trusted, - } => { - //XXX - self.set_peer_trust(peer_id.clone(), is_trusted) - .await - .ok() - .unwrap(); - NodeResponse::PeerTrust { + NodeCommandWithChannel::GetConnectedPeers((_, response)) => { + let connected_peers = self.node.connected_peers().await.expect("TODO"); + /* + .expect("TODO") + .iter() + .map(js_value_from_display) + .collect(); + */ + response + .send(connected_peers.iter().map(|id| id.to_string()).collect()) + .expect("channel_dropped"); + } + /* + NodeCommandWithChannel::GetNetworkHeadHeader(command) => { + let _ = to_value(&self.node.get_network_head_header()) + .ok() + .expect("TODO"); + NodeResponse::Header(self.network_head_header().await.ok().unwrap()) + } + NodeCommandWithChannel::GetLocalHeadHeader(command) => { + let _ = to_value(&self.node.get_local_head_header().await.unwrap()) + .ok() + .unwrap(); + NodeResponse::Header(self.local_head_header().await.ok().unwrap()) + } + */ + NodeCommandWithChannel::SetPeerTrust(( + SetPeerTrust { peer_id, is_trusted, - } - } - NodeCommand::RequestHeadHeader => { - NodeResponse::Header(self.request_head_header().await.ok().unwrap()) + }, + response, + )) => { + let _ = self + .node + .set_peer_trust(peer_id.parse().unwrap(), is_trusted) + .await + .unwrap(); + response.send(()).expect("channel_dropped"); } - NodeCommand::WaitConnected(trusted) => { - todo!() + NodeCommandWithChannel::WaitConnected((parameters, response)) => { + // TODO: nonblocking on channels + if parameters.trusted { + self.node.wait_connected().await; + } else { + self.node.wait_connected_trusted().await; + } + response.send(()).expect("channel_dropped") } - NodeCommand::GetListeners => { + NodeCommandWithChannel::GetListeners((_command, _response)) => { todo!() } - NodeCommand::RequestHeader(HeaderQuery::ByHash(hash)) => { - NodeResponse::Header(self.request_header_by_hash(hash).await.ok().unwrap()) - } - NodeCommand::RequestHeader(HeaderQuery::ByHeight(height)) => { - NodeResponse::Header(self.request_header_by_height(height).await.ok().unwrap()) - } - NodeCommand::GetHeader(HeaderQuery::ByHash(hash)) => { - NodeResponse::Header(self.get_header_by_hash(hash).await.ok().unwrap()) - } - NodeCommand::GetHeader(HeaderQuery::ByHeight(height)) => { - NodeResponse::Header(self.get_header_by_height(height).await.ok().unwrap()) - } - NodeCommand::GetSamplingMetadata(height) => NodeResponse::SamplingMetadata( - self.get_sampling_metadata(height).await.ok().unwrap(), - ), - NodeCommand::RequestHeader(HeaderQuery::GetVerified { from, amount }) => { - NodeResponse::VerifiedHeaders( - self.request_verified_headers(from_value(from).ok().unwrap(), amount) + NodeCommandWithChannel::RequestHeader((command, response)) => { + let header = match command.0 { + SingleHeaderQuery::Head => todo!(), + SingleHeaderQuery::ByHash(hash) => { + self.node.request_header_by_hash(&hash).await.ok().unwrap() + } + SingleHeaderQuery::ByHeight(height) => self + .node + .request_header_by_height(height) .await .ok() .unwrap(), - ) + }; + //let jsvalue = to_value(&header).ok().unwrap(); + response.send(header).expect("channel_dropped"); } - NodeCommand::GetHeader(HeaderQuery::GetVerified { .. }) => unimplemented!(), - NodeCommand::RequestHeader(HeaderQuery::Range { .. }) => unimplemented!(), - NodeCommand::GetHeader(HeaderQuery::Range { - start_height, - end_height, - }) => NodeResponse::HeaderArray( - self.get_headers(start_height, end_height) - .await + NodeCommandWithChannel::RequestMultipleHeaders((command, response)) => { + let headers = match command.0 { + MultipleHeaderQuery::GetVerified { from, amount } => { + let from_header = from_value(from).ok().unwrap(); + self.node + .request_verified_headers(&from_header, amount) + .await + .unwrap() + } + MultipleHeaderQuery::Range { + start_height, + end_height, + } => todo!(), /* + * match (start_height, end_height) { + (None, None) => node.get_headers(..).await, + (Some(start), None) => node.get_headers(start..).await, + (None, Some(end)) => node.get_headers(..=end).await, + (Some(start), Some(end)) => node.get_headers(start..=end).await, + } + .ok() + .unwrap(), + */ + }; + + let jsvalue = to_value(&headers).ok().unwrap().into(); // TODO: array fix? + response.send(jsvalue).expect("channel_dropped"); + } + NodeCommandWithChannel::GetHeader((command, response)) => { + let header = match command.0 { + SingleHeaderQuery::Head => todo!(), + SingleHeaderQuery::ByHash(hash) => { + self.node.get_header_by_hash(&hash).await.ok().unwrap() + } + SingleHeaderQuery::ByHeight(height) => { + self.node.get_header_by_height(height).await.ok().unwrap() + } + }; + //let jsvalue = to_value(&header).ok().unwrap(); + response.send(header).expect("channel_dropped"); + } + NodeCommandWithChannel::GetMultipleHeaders((command, response)) => { + let headers = match command.0 { + MultipleHeaderQuery::GetVerified { from, amount } => { + let from_header = from_value(from).ok().unwrap(); + self.node + .request_verified_headers(&from_header, amount) + .await + .unwrap() + } + MultipleHeaderQuery::Range { + start_height, + end_height, + } => match (start_height, end_height) { + (None, None) => self.node.get_headers(..).await, + (Some(start), None) => self.node.get_headers(start..).await, + (None, Some(end)) => self.node.get_headers(..=end).await, + (Some(start), Some(end)) => self.node.get_headers(start..=end).await, + } .ok() .unwrap(), - ), - NodeCommand::WaitConnected(trusted) => { - self.wait_connected(trusted).await; // TODO: nonblocking ( Promise? ) - NodeResponse::Connected(trusted) + }; + + let jsvalue = to_value(&headers).ok().unwrap().into(); // TODO: array fix? + response.send(jsvalue).expect("channel_dropped"); } - } + NodeCommandWithChannel::GetSamplingMetadata((command, response)) => { + let metadata = self + .node + .get_sampling_metadata(command.height) + .await + .ok() + .unwrap() + .unwrap(); + response.send(metadata).expect("channel_dropped"); + } + }; } } @@ -299,7 +687,7 @@ impl NodeWorker { enum WorkerMessage { NewConnection(MessagePort), - Command((NodeCommand, ClientId)), + Command(NodeCommandWithChannel), } #[derive(Debug)] @@ -349,12 +737,22 @@ impl WorkerConnector { let local_tx = near_tx.clone(); spawn_local(async move { let message_data = ev.data(); - let data = from_value(message_data).expect("could not from value"); + let data: NodeCommand = from_value(message_data).expect("could not from value"); + + let (tx, rx) = oneshot::channel(); + let command_with_channel = todo!(); local_tx - .send(WorkerMessage::Command((data, ClientId(client_id)))) + .send(WorkerMessage::Command(command_with_channel)) .await .expect("send3 err"); + + let response = rx.await.expect("forwardding channel error"); + let v = to_value(&response).expect("could not to_value"); + + self.ports[client_id] + .post_message(&v) + .expect("error posting"); }) }); port.set_onmessage(Some(client_message_callback.as_ref().unchecked_ref())); @@ -387,24 +785,26 @@ pub async fn run_worker(queued_connections: Vec) { WorkerMessage::NewConnection(connection) => { connector.add(connection); } - WorkerMessage::Command((command, client)) => { + WorkerMessage::Command(command_with_channel) => { let Some(worker) = &mut worker else { - match command { - NodeCommand::IsRunning => { - connector.respond_to(client, NodeResponse::Running(false)); + match command_with_channel { + NodeCommandWithChannel::IsRunning((_, response)) => { + response.send(false).expect("channel_dropped"); } - NodeCommand::Start(config) => { - worker = Some(NodeWorker::new(config).await); - connector.respond_to(client, NodeResponse::Started(0)); + NodeCommandWithChannel::StartNode((command, response)) => { + worker = Some(NodeWorker::new(command.0).await); + response + .send(NodeState::NodeStarted) + .expect("channel_dropped"); } _ => warn!("Worker not running"), } continue; }; - trace!("received: {command:?}"); - let response = worker.process_command(command).await; - connector.respond_to(client, response); + trace!("received: {command_with_channel:?}"); + let response = worker.process_command(command_with_channel).await; + //connector.respond_to(client, response); } } } diff --git a/node/src/peer_tracker.rs b/node/src/peer_tracker.rs index 773d3713..2d55e727 100644 --- a/node/src/peer_tracker.rs +++ b/node/src/peer_tracker.rs @@ -7,7 +7,7 @@ use dashmap::mapref::one::RefMut; use dashmap::DashMap; use libp2p::{identify, swarm::ConnectionId, Multiaddr, PeerId}; use rand::seq::SliceRandom; -use serde::Serialize; +use serde::{Deserialize, Serialize}; use smallvec::SmallVec; use tokio::sync::watch; @@ -19,7 +19,7 @@ pub struct PeerTracker { } /// Statistics of the connected peers -#[derive(Debug, Clone, Default, Serialize)] +#[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct PeerTrackerInfo { /// Number of the connected peers. pub num_connected_peers: u64, diff --git a/node/src/syncer.rs b/node/src/syncer.rs index 2b8ec00c..f9482a00 100644 --- a/node/src/syncer.rs +++ b/node/src/syncer.rs @@ -18,7 +18,7 @@ use backoff::ExponentialBackoffBuilder; use celestia_types::hash::Hash; use celestia_types::ExtendedHeader; use futures::FutureExt; -use serde::Serialize; +use serde::{Deserialize, Serialize}; use tokio::select; use tokio::sync::{mpsc, oneshot, watch}; use tokio_util::sync::CancellationToken; @@ -96,7 +96,7 @@ enum SyncerCmd { } /// Status of the synchronization. -#[derive(Debug, Serialize)] +#[derive(Debug, Serialize, Deserialize)] pub struct SyncingInfo { /// The height the [`Syncer`] is currently synchronized to. pub local_head: u64, From ac8c77e273ec5f3729ad92b68458c8094c814f47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Florkiewicz?= Date: Thu, 11 Apr 2024 09:30:08 +0200 Subject: [PATCH 04/35] Type safe finish --- Cargo.lock | 1 + node-wasm/Cargo.toml | 9 +- node-wasm/src/node.rs | 18 +- node-wasm/src/utils.rs | 8 +- node-wasm/src/worker.rs | 457 ++++++++++++++++++++-------------------- 5 files changed, 257 insertions(+), 236 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c64f49ec..c2a21fd4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2882,6 +2882,7 @@ dependencies = [ "anyhow", "celestia-types", "console_error_panic_hook", + "futures", "gloo-timers 0.3.0", "instant", "js-sys", diff --git a/node-wasm/Cargo.toml b/node-wasm/Cargo.toml index e8b4462d..9e5d32ee 100644 --- a/node-wasm/Cargo.toml +++ b/node-wasm/Cargo.toml @@ -29,17 +29,18 @@ lumina-node = { workspace = true } anyhow = "1.0.71" console_error_panic_hook = "0.1.7" +futures = "0.3" +gloo-timers = "0.3" +instant = "0.1" js-sys = "0.3.64" serde = { version = "1.0.164", features = ["derive"] } -serde_repr = "0.1" serde-wasm-bindgen = "0.6.0" +serde_repr = "0.1" time = { version = "0.3", features = ["wasm-bindgen"] } +tokio = { version = "*", features = ["sync"]} tracing = "0.1.37" tracing-subscriber = { version = "0.3.18", features = ["time"] } tracing-web = "0.1.2" wasm-bindgen = "0.2.88" wasm-bindgen-futures = "0.4.37" web-sys = { version = "0.3.69", features = ["BroadcastChannel", "MessageEvent", "Worker", "WorkerOptions", "WorkerType", "SharedWorker", "MessagePort", "SharedWorkerGlobalScope"]} -tokio = { version = "*", features = ["sync"]} -gloo-timers = "0.3" -instant = "0.1" diff --git a/node-wasm/src/node.rs b/node-wasm/src/node.rs index 3c2ebbad..8433b521 100644 --- a/node-wasm/src/node.rs +++ b/node-wasm/src/node.rs @@ -20,7 +20,6 @@ use crate::utils::js_value_from_display; use crate::utils::BChannel; use crate::utils::JsContext; use crate::utils::Network; -use crate::utils::NodeCommandType; use crate::worker::{MultipleHeaderQuery, NodeCommand, NodeResponse, SingleHeaderQuery}; use crate::wrapper::libp2p::NetworkInfoSnapshot; use crate::Result; @@ -172,7 +171,14 @@ impl NodeDriver { let command = RequestMultipleHeaders(MultipleHeaderQuery::GetVerified { from, amount }); let response = self.channel.send(command); - Ok(response.await.unwrap()) + let result = response + .await + .unwrap() + .iter() + .map(|h| to_value(&h).unwrap()) // XXX + .collect(); + + Ok(result) } pub async fn syncer_info(&mut self) -> Result { let response = self.channel.send(GetSyncerInfo); @@ -217,8 +223,14 @@ impl NodeDriver { end_height, }); let response = self.channel.send(command); + let result = response + .await + .unwrap() + .iter() + .map(|h| to_value(&h).unwrap()) + .collect(); - Ok(response.await.unwrap()) + Ok(result) } pub async fn get_sampling_metadata(&mut self, height: u64) -> Result { let command = GetSamplingMetadata { height }; diff --git a/node-wasm/src/utils.rs b/node-wasm/src/utils.rs index ba420df3..2008f4ed 100644 --- a/node-wasm/src/utils.rs +++ b/node-wasm/src/utils.rs @@ -14,7 +14,6 @@ use tracing_web::{performance_layer, MakeConsoleWriter}; use wasm_bindgen::prelude::*; use crate::worker::NodeCommand; -use crate::worker::NodeResponse; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; use serde_wasm_bindgen::from_value; @@ -27,10 +26,9 @@ use web_sys::MessagePort; use web_sys::SharedWorker; use web_sys::SharedWorkerGlobalScope; -pub type CommandResponseChannel = oneshot::Sender; - +pub type CommandResponseChannel = oneshot::Sender<::Output>; #[derive(Serialize, Deserialize, Debug)] -pub struct NodeCommandResponse(T::Output) +pub struct NodeCommandResponse(pub T::Output) where T: NodeCommandType, T::Output: Debug + Serialize; @@ -54,8 +52,6 @@ impl NodeCommandSender { pub trait NodeCommandType: Debug + Into { type Output; - - //fn response(&self, output: Self::Output) -> NodeResponse; } pub struct BChannel { diff --git a/node-wasm/src/worker.rs b/node-wasm/src/worker.rs index 127ed312..3a1ae326 100644 --- a/node-wasm/src/worker.rs +++ b/node-wasm/src/worker.rs @@ -1,37 +1,26 @@ use std::fmt::Debug; +use futures::future::{BoxFuture, FutureExt, TryFutureExt}; +use instant::Instant; +use libp2p::Multiaddr; use serde::{Deserialize, Serialize}; +use serde_wasm_bindgen::{from_value, to_value}; +use tokio::sync::{mpsc, oneshot}; use tracing::{info, trace, warn}; use wasm_bindgen::prelude::*; +use wasm_bindgen_futures::spawn_local; +use web_sys::{MessageEvent, MessagePort, SharedWorker}; use celestia_types::hash::Hash; use celestia_types::ExtendedHeader; -use instant::Instant; -use js_sys::Array; -use serde_wasm_bindgen::{from_value, to_value}; -use tokio::sync::mpsc; -use wasm_bindgen_futures::spawn_local; -use web_sys::MessageEvent; -use web_sys::MessagePort; -use web_sys::SharedWorker; - use lumina_node::node::Node; -use lumina_node::store::{IndexedDbStore, Store}; +use lumina_node::peer_tracker::PeerTrackerInfo; +use lumina_node::store::{IndexedDbStore, SamplingMetadata, Store}; use lumina_node::syncer::SyncingInfo; use crate::node::WasmNodeConfig; -use crate::utils::js_value_from_display; -use crate::utils::CommandResponseChannel; -use crate::utils::NodeCommandResponse; -use crate::utils::NodeCommandType; -use crate::utils::WorkerSelf; -//use crate::worker::NodeResponse; +use crate::utils::{CommandResponseChannel, NodeCommandResponse, NodeCommandType, WorkerSelf}; use crate::wrapper::libp2p::NetworkInfoSnapshot; -use crate::Result; -use libp2p::Multiaddr; -use lumina_node::peer_tracker::PeerTrackerInfo; -use lumina_node::store::SamplingMetadata; -use tokio::sync::oneshot; #[derive(Debug, Serialize, Deserialize)] pub struct IsRunning; @@ -45,7 +34,7 @@ impl From for NodeCommand { } #[derive(Debug, Serialize, Deserialize)] -enum NodeState { +pub enum NodeState { NodeStopped, NodeStarted, AlreadyRunning(u64), @@ -156,8 +145,8 @@ impl From for NodeCommand { #[derive(Debug, Serialize, Deserialize)] pub struct SetPeerTrust { - peer_id: String, - is_trusted: bool, + pub peer_id: String, + pub is_trusted: bool, } impl NodeCommandType for SetPeerTrust { type Output = (); @@ -170,7 +159,7 @@ impl From for NodeCommand { #[derive(Debug, Serialize, Deserialize)] pub struct WaitConnected { - trusted: bool, + pub trusted: bool, } impl NodeCommandType for WaitConnected { type Output = (); @@ -206,7 +195,7 @@ impl From for NodeCommand { #[derive(Debug, Serialize, Deserialize)] pub struct RequestMultipleHeaders(pub MultipleHeaderQuery); impl NodeCommandType for RequestMultipleHeaders { - type Output = Array; + type Output = Vec; } impl From for NodeCommand { fn from(command: RequestMultipleHeaders) -> NodeCommand { @@ -228,7 +217,7 @@ impl From for NodeCommand { #[derive(Debug, Serialize, Deserialize)] pub struct GetMultipleHeaders(pub MultipleHeaderQuery); impl NodeCommandType for GetMultipleHeaders { - type Output = Array; + type Output = Vec; } impl From for NodeCommand { fn from(command: GetMultipleHeaders) -> NodeCommand { @@ -259,10 +248,7 @@ pub enum NodeCommand { GetPeerTrackerInfo(GetPeerTrackerInfo), GetNetworkInfo(GetNetworkInfo), GetConnectedPeers(GetConnectedPeers), - //GetNetworkHeadHeader, - //GetLocalHeadHeader, SetPeerTrust(SetPeerTrust), - //RequestHeadHeader(RequestHeadHeader), WaitConnected(WaitConnected), GetListeners(GetListeners), RequestHeader(RequestHeader), @@ -272,6 +258,191 @@ pub enum NodeCommand { GetSamplingMetadata(GetSamplingMetadata), } +#[derive(Serialize, Deserialize, Debug)] +pub enum SingleHeaderQuery { + Head, + ByHash(Hash), + ByHeight(u64), +} + +#[derive(Serialize, Deserialize, Debug)] +pub enum MultipleHeaderQuery { + GetVerified { + #[serde(with = "serde_wasm_bindgen::preserve")] + from: JsValue, + amount: u64, + }, + Range { + start_height: Option, + end_height: Option, + }, +} + +impl NodeCommand { + fn add_response_channel( + self, + ) -> ( + NodeCommandWithChannel, + BoxFuture<'static, Result>, // XXX + // type cleanup + ) { + match self { + NodeCommand::IsRunning(cmd) => { + let (tx, rx) = oneshot::channel(); + ( + NodeCommandWithChannel::IsRunning((cmd, tx)), + rx.map_ok(|r| NodeResponse::IsRunning(NodeCommandResponse::(r))) + .boxed(), + ) + } + NodeCommand::StartNode(cmd) => { + let (tx, rx) = oneshot::channel(); + ( + NodeCommandWithChannel::StartNode((cmd, tx)), + rx.map_ok(|r| NodeResponse::StartNode(NodeCommandResponse::(r))) + .boxed(), + ) + } + NodeCommand::GetLocalPeerId(cmd) => { + let (tx, rx) = oneshot::channel(); + ( + NodeCommandWithChannel::GetLocalPeerId((cmd, tx)), + rx.map_ok(|r| { + NodeResponse::GetLocalPeerId(NodeCommandResponse::(r)) + }) + .boxed(), + ) + } + NodeCommand::GetSyncerInfo(cmd) => { + let (tx, rx) = oneshot::channel(); + ( + NodeCommandWithChannel::GetSyncerInfo((cmd, tx)), + rx.map_ok(|r| { + NodeResponse::GetSyncerInfo(NodeCommandResponse::(r)) + }) + .boxed(), + ) + } + NodeCommand::GetPeerTrackerInfo(cmd) => { + let (tx, rx) = oneshot::channel(); + ( + NodeCommandWithChannel::GetPeerTrackerInfo((cmd, tx)), + rx.map_ok(|r| { + NodeResponse::GetPeerTrackerInfo(NodeCommandResponse::( + r, + )) + }) + .boxed(), + ) + } + NodeCommand::GetNetworkInfo(cmd) => { + let (tx, rx) = oneshot::channel(); + ( + NodeCommandWithChannel::GetNetworkInfo((cmd, tx)), + rx.map_ok(|r| { + NodeResponse::GetNetworkInfo(NodeCommandResponse::(r)) + }) + .boxed(), + ) + } + NodeCommand::GetConnectedPeers(cmd) => { + let (tx, rx) = oneshot::channel(); + ( + NodeCommandWithChannel::GetConnectedPeers((cmd, tx)), + rx.map_ok(|r| { + NodeResponse::GetConnectedPeers(NodeCommandResponse::(r)) + }) + .boxed(), + ) + } + NodeCommand::SetPeerTrust(cmd) => { + let (tx, rx) = oneshot::channel(); + ( + NodeCommandWithChannel::SetPeerTrust((cmd, tx)), + rx.map_ok(|r| { + NodeResponse::SetPeerTrust(NodeCommandResponse::(r)) + }) + .boxed(), + ) + } + NodeCommand::WaitConnected(cmd) => { + let (tx, rx) = oneshot::channel(); + ( + NodeCommandWithChannel::WaitConnected((cmd, tx)), + rx.map_ok(|r| { + NodeResponse::WaitConnected(NodeCommandResponse::(r)) + }) + .boxed(), + ) + } + NodeCommand::GetListeners(cmd) => { + let (tx, rx) = oneshot::channel(); + ( + NodeCommandWithChannel::GetListeners((cmd, tx)), + rx.map_ok(|r| { + NodeResponse::GetListeners(NodeCommandResponse::(r)) + }) + .boxed(), + ) + } + NodeCommand::RequestHeader(cmd) => { + let (tx, rx) = oneshot::channel(); + ( + NodeCommandWithChannel::RequestHeader((cmd, tx)), + rx.map_ok(|r| { + NodeResponse::RequestHeader(NodeCommandResponse::(r)) + }) + .boxed(), + ) + } + NodeCommand::RequestMultipleHeaders(cmd) => { + let (tx, rx) = oneshot::channel(); + ( + NodeCommandWithChannel::RequestMultipleHeaders((cmd, tx)), + rx.map_ok(|r| { + NodeResponse::RequestMultipleHeaders(NodeCommandResponse::< + RequestMultipleHeaders, + >(r)) + }) + .boxed(), + ) + } + NodeCommand::GetHeader(cmd) => { + let (tx, rx) = oneshot::channel(); + ( + NodeCommandWithChannel::GetHeader((cmd, tx)), + rx.map_ok(|r| NodeResponse::GetHeader(NodeCommandResponse::(r))) + .boxed(), + ) + } + NodeCommand::GetMultipleHeaders(cmd) => { + let (tx, rx) = oneshot::channel(); + ( + NodeCommandWithChannel::GetMultipleHeaders((cmd, tx)), + rx.map_ok(|r| { + NodeResponse::GetMultipleHeaders(NodeCommandResponse::( + r, + )) + }) + .boxed(), + ) + } + NodeCommand::GetSamplingMetadata(cmd) => { + let (tx, rx) = oneshot::channel(); + ( + NodeCommandWithChannel::GetSamplingMetadata((cmd, tx)), + rx.map_ok(|r| { + NodeResponse::GetSamplingMetadata( + NodeCommandResponse::(r), + ) + }) + .boxed(), + ) + } + } + } +} + #[derive(Debug)] pub enum NodeCommandWithChannel { IsRunning((IsRunning, CommandResponseChannel)), @@ -286,10 +457,7 @@ pub enum NodeCommandWithChannel { ), GetNetworkInfo((GetNetworkInfo, CommandResponseChannel)), GetConnectedPeers((GetConnectedPeers, CommandResponseChannel)), - //GetNetworkHeadHeader, - //GetLocalHeadHeader, SetPeerTrust((SetPeerTrust, CommandResponseChannel)), - //RequestHeadHeader((RequestHeadHeader, CommandResponseChannel)), WaitConnected((WaitConnected, CommandResponseChannel)), GetListeners((GetListeners, CommandResponseChannel)), RequestHeader((RequestHeader, CommandResponseChannel)), @@ -314,26 +482,6 @@ pub enum NodeCommandWithChannel { ), } -#[derive(Serialize, Deserialize, Debug)] -pub enum SingleHeaderQuery { - Head, - ByHash(Hash), - ByHeight(u64), -} - -#[derive(Serialize, Deserialize, Debug)] -pub enum MultipleHeaderQuery { - GetVerified { - #[serde(with = "serde_wasm_bindgen::preserve")] - from: JsValue, - amount: u64, - }, - Range { - start_height: Option, - end_height: Option, - }, -} - #[derive(Serialize, Deserialize, Debug)] pub enum NodeResponse { IsRunning(NodeCommandResponse), @@ -343,48 +491,16 @@ pub enum NodeResponse { GetPeerTrackerInfo(NodeCommandResponse), GetNetworkInfo(NodeCommandResponse), GetConnectedPeers(NodeCommandResponse), - //GetNetworkHeadHeader, - //GetLocalHeadHeader, SetPeerTrust(NodeCommandResponse), - //RequestHeadHeader(NodeCommandResponse), WaitConnected(NodeCommandResponse), GetListeners(NodeCommandResponse), RequestHeader(NodeCommandResponse), + RequestMultipleHeaders(NodeCommandResponse), GetHeader(NodeCommandResponse), + GetMultipleHeaders(NodeCommandResponse), GetSamplingMetadata(NodeCommandResponse), } -/* -#[derive(Serialize, Deserialize, Debug)] -pub enum NodeResponse { - Running(bool), - Started(u64), - LocalPeerId(String), - Connected(bool), - #[serde(with = "serde_wasm_bindgen::preserve")] - SyncerInfo(JsValue), - #[serde(with = "serde_wasm_bindgen::preserve")] - PeerTrackerInfo(JsValue), - NetworkInfo(NetworkInfoSnapshot), - #[serde(with = "serde_wasm_bindgen::preserve")] - ConnectedPeers(Array), - PeerTrust { - peer_id: String, - is_trusted: bool, - }, - #[serde(with = "serde_wasm_bindgen::preserve")] - Header(JsValue), - #[serde(with = "serde_wasm_bindgen::preserve")] - HeaderArray(Array), - #[serde(with = "serde_wasm_bindgen::preserve")] - VerifiedHeaders(Array), - #[serde(with = "serde_wasm_bindgen::preserve")] - SamplingMetadata(JsValue), - #[serde(with = "serde_wasm_bindgen::preserve")] - Listeners(Array), -} -*/ - struct NodeWorker { node: Node, start_timestamp: Instant, @@ -408,101 +524,6 @@ impl NodeWorker { } } - /* - fn local_peer_id(&self) -> String { - self.node.local_peer_id().to_string() - } - - fn peer_tracker_info(&self) -> Result { - Ok(to_value(&self.node.peer_tracker_info())?) - } - - async fn syncer_info(&self) -> Result { - Ok(to_value(&self.node.syncer_info().await?)?) - } - - async fn network_info(&self) -> Result { - Ok(self.node.network_info().await?.into()) - } - - async fn request_header_by_hash(&self, hash: Hash) -> Result { - Ok(to_value(&self.node.request_header_by_hash(&hash).await?)?) - } - - async fn request_header_by_height(&self, height: u64) -> Result { - Ok(to_value( - &self.node.request_header_by_height(height).await?, - )?) - } - - async fn get_header_by_hash(&self, hash: Hash) -> Result { - Ok(to_value(&self.node.get_header_by_hash(&hash).await?)?) - } - - async fn get_header_by_height(&self, height: u64) -> Result { - Ok(to_value(&self.node.get_header_by_height(height).await?)?) - } - - async fn get_headers( - &self, - start_height: Option, - end_height: Option, - ) -> Result { - let headers = match (start_height, end_height) { - (None, None) => self.node.get_headers(..).await, - (Some(start), None) => self.node.get_headers(start..).await, - (None, Some(end)) => self.node.get_headers(..=end).await, - (Some(start), Some(end)) => self.node.get_headers(start..=end).await, - }?; - - Ok(to_value(&headers)?.into()) - } - - async fn request_verified_headers(&self, from: ExtendedHeader, amount: u64) -> Result { - Ok(to_value(&self.node.request_verified_headers(&from, amount).await?)?.into()) - } - - async fn get_sampling_metadata(&self, height: u64) -> Result { - Ok(to_value(&self.node.get_sampling_metadata(height).await?)?) - } - - async fn set_peer_trust(&self, peer_id: String, is_trusted: bool) -> Result<()> { - Ok(self - .node - .set_peer_trust(peer_id.parse()?, is_trusted) - .await?) - } - - async fn connected_peers(&self) -> Result { - Ok(self - .node - .connected_peers() - .await? - .iter() - .map(js_value_from_display) - .collect()) - } - - async fn network_head_header(&self) -> Result { - Ok(to_value(&self.node.get_network_head_header())?) - } - - async fn wait_connected(&self, trusted: bool) { - if trusted { - self.node.wait_connected().await; - } else { - self.node.wait_connected_trusted().await; - } - } - - async fn local_head_header(&self) -> Result { - Ok(to_value(&self.node.get_local_head_header().await?)?) - } - - async fn request_head_header(&self) -> Result { - Ok(to_value(&self.node.request_head_header().await?)?) - } - */ async fn process_command(&mut self, command: NodeCommandWithChannel) { match command { // TODO: order @@ -538,12 +559,6 @@ impl NodeWorker { } NodeCommandWithChannel::GetConnectedPeers((_, response)) => { let connected_peers = self.node.connected_peers().await.expect("TODO"); - /* - .expect("TODO") - .iter() - .map(js_value_from_display) - .collect(); - */ response .send(connected_peers.iter().map(|id| id.to_string()).collect()) .expect("channel_dropped"); @@ -579,9 +594,9 @@ impl NodeWorker { NodeCommandWithChannel::WaitConnected((parameters, response)) => { // TODO: nonblocking on channels if parameters.trusted { - self.node.wait_connected().await; + let _ = self.node.wait_connected().await; } else { - self.node.wait_connected_trusted().await; + let _ = self.node.wait_connected_trusted().await; } response.send(()).expect("channel_dropped") } @@ -601,7 +616,6 @@ impl NodeWorker { .ok() .unwrap(), }; - //let jsvalue = to_value(&header).ok().unwrap(); response.send(header).expect("channel_dropped"); } NodeCommandWithChannel::RequestMultipleHeaders((command, response)) => { @@ -613,23 +627,10 @@ impl NodeWorker { .await .unwrap() } - MultipleHeaderQuery::Range { - start_height, - end_height, - } => todo!(), /* - * match (start_height, end_height) { - (None, None) => node.get_headers(..).await, - (Some(start), None) => node.get_headers(start..).await, - (None, Some(end)) => node.get_headers(..=end).await, - (Some(start), Some(end)) => node.get_headers(start..=end).await, - } - .ok() - .unwrap(), - */ + MultipleHeaderQuery::Range { .. } => unreachable!(), }; - let jsvalue = to_value(&headers).ok().unwrap().into(); // TODO: array fix? - response.send(jsvalue).expect("channel_dropped"); + response.send(headers).expect("channel_dropped"); } NodeCommandWithChannel::GetHeader((command, response)) => { let header = match command.0 { @@ -641,7 +642,6 @@ impl NodeWorker { self.node.get_header_by_height(height).await.ok().unwrap() } }; - //let jsvalue = to_value(&header).ok().unwrap(); response.send(header).expect("channel_dropped"); } NodeCommandWithChannel::GetMultipleHeaders((command, response)) => { @@ -666,8 +666,7 @@ impl NodeWorker { .unwrap(), }; - let jsvalue = to_value(&headers).ok().unwrap().into(); // TODO: array fix? - response.send(jsvalue).expect("channel_dropped"); + response.send(headers).expect("channel_dropped"); } NodeCommandWithChannel::GetSamplingMetadata((command, response)) => { let metadata = self @@ -683,11 +682,15 @@ impl NodeWorker { } } -//type WorkerChannel = BChannel; - enum WorkerMessage { NewConnection(MessagePort), Command(NodeCommandWithChannel), + ResponseChannel( + ( + ClientId, + BoxFuture<'static, Result>, + ), + ), } #[derive(Debug)] @@ -737,22 +740,26 @@ impl WorkerConnector { let local_tx = near_tx.clone(); spawn_local(async move { let message_data = ev.data(); - let data: NodeCommand = from_value(message_data).expect("could not from value"); - - let (tx, rx) = oneshot::channel(); - let command_with_channel = todo!(); + let Ok(node_command) = from_value::(message_data) else { + warn!("could not deserialize message from client {client_id}"); + return; + }; + let (command_with_channel, response_channel) = + node_command.add_response_channel(); local_tx .send(WorkerMessage::Command(command_with_channel)) .await .expect("send3 err"); - let response = rx.await.expect("forwardding channel error"); - let v = to_value(&response).expect("could not to_value"); - - self.ports[client_id] - .post_message(&v) - .expect("error posting"); + // TODO: something cleaner? + local_tx + .send(WorkerMessage::ResponseChannel(( + ClientId(client_id), + response_channel, + ))) + .await + .expect("send4 err"); }) }); port.set_onmessage(Some(client_message_callback.as_ref().unchecked_ref())); @@ -803,8 +810,12 @@ pub async fn run_worker(queued_connections: Vec) { }; trace!("received: {command_with_channel:?}"); - let response = worker.process_command(command_with_channel).await; - //connector.respond_to(client, response); + worker.process_command(command_with_channel).await; + } + WorkerMessage::ResponseChannel((client_id, channel)) => { + // XXX: properly + let response = channel.await.expect("forwardding channel error"); + connector.respond_to(client_id, response); } } } From 556003a7b83a1bec0d09f6a2f178d4232f9d0e43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Florkiewicz?= Date: Fri, 12 Apr 2024 09:18:10 +0200 Subject: [PATCH 05/35] more macros less code --- cli/static/worker.js | 2 + node-wasm/src/node.rs | 115 +++---- node-wasm/src/utils.rs | 98 +++--- node-wasm/src/worker.rs | 671 ++++++++++------------------------------ 4 files changed, 284 insertions(+), 602 deletions(-) diff --git a/cli/static/worker.js b/cli/static/worker.js index 3297f8e0..c6f2f9a9 100644 --- a/cli/static/worker.js +++ b/cli/static/worker.js @@ -1,3 +1,5 @@ +Error.stackTraceLimit = 99; // rust stack traces can get pretty big, increase the default + // The worker has its own scope and no direct access to functions/objects of the // global scope. We import the generated JS file to make `wasm_bindgen` // available which we need to initialize our Wasm code. diff --git a/node-wasm/src/node.rs b/node-wasm/src/node.rs index 8433b521..e3e3509d 100644 --- a/node-wasm/src/node.rs +++ b/node-wasm/src/node.rs @@ -2,40 +2,28 @@ use std::result::Result as StdResult; -use celestia_types::hash::Hash; -use celestia_types::ExtendedHeader; use js_sys::Array; use libp2p::identity::Keypair; use libp2p::multiaddr::Protocol; use lumina_node::blockstore::IndexedDbBlockstore; use lumina_node::network::{canonical_network_bootnodes, network_genesis, network_id}; use lumina_node::node::{Node, NodeConfig}; -use lumina_node::store::{IndexedDbStore, Store}; +use lumina_node::store::IndexedDbStore; use serde::{Deserialize, Serialize}; use serde_wasm_bindgen::{from_value, to_value}; use tracing::info; use wasm_bindgen::prelude::*; +use web_sys::{SharedWorker, WorkerOptions, WorkerType}; -use crate::utils::js_value_from_display; -use crate::utils::BChannel; -use crate::utils::JsContext; -use crate::utils::Network; -use crate::worker::{MultipleHeaderQuery, NodeCommand, NodeResponse, SingleHeaderQuery}; -use crate::wrapper::libp2p::NetworkInfoSnapshot; -use crate::Result; - -use crate::worker::RequestMultipleHeaders; +use crate::utils::{js_value_from_display, BChannel, JsContext, Network}; use crate::worker::{ GetConnectedPeers, GetHeader, GetListeners, GetLocalPeerId, GetMultipleHeaders, GetNetworkInfo, - GetPeerTrackerInfo, GetSamplingMetadata, GetSyncerInfo, IsRunning, RequestHeader, SetPeerTrust, - StartNode, WaitConnected, + GetPeerTrackerInfo, GetSamplingMetadata, GetSyncerInfo, IsRunning, MultipleHeaderQuery, + NodeCommand, NodeResponse, RequestHeader, RequestMultipleHeaders, SetPeerTrust, + SingleHeaderQuery, StartNode, WaitConnected, }; - -use web_sys::{SharedWorker, WorkerOptions, WorkerType}; - -/// Lumina wasm node. -#[wasm_bindgen(js_name = Node)] -struct WasmNode(Node); +use crate::wrapper::libp2p::NetworkInfoSnapshot; +use crate::Result; /// Config for the lumina wasm node. #[wasm_bindgen(js_name = NodeConfig)] @@ -76,51 +64,51 @@ impl NodeDriver { } } - pub async fn is_running(&mut self) -> bool { + pub async fn is_running(&self) -> bool { let response = self.channel.send(IsRunning); response.await.unwrap() } - pub async fn start(&mut self, config: WasmNodeConfig) { + pub async fn start(&self, config: WasmNodeConfig) -> Result<()> { let command = StartNode(config); let response = self.channel.send(command); - response.await.unwrap(); // XXX return type + Ok(response.await.unwrap()?) } - pub async fn local_peer_id(&mut self) -> String { + pub async fn local_peer_id(&self) -> String { let response = self.channel.send(GetLocalPeerId); response.await.unwrap() } - pub async fn peer_tracker_info(&mut self) -> Result { + pub async fn peer_tracker_info(&self) -> Result { let response = self.channel.send(GetPeerTrackerInfo); Ok(to_value(&response.await.unwrap())?) } - pub async fn wait_connected(&mut self) { + pub async fn wait_connected(&self) { let command = WaitConnected { trusted: false }; let response = self.channel.send(command); response.await.unwrap() } - pub async fn wait_connected_trusted(&mut self) { + pub async fn wait_connected_trusted(&self) { let command = WaitConnected { trusted: false }; let response = self.channel.send(command); response.await.unwrap() } - pub async fn network_info(&mut self) -> Result { + pub async fn network_info(&self) -> Result { let response = self.channel.send(GetNetworkInfo); Ok(response.await.unwrap()) } - pub async fn listeners(&mut self) -> Result { + pub async fn listeners(&self) -> Result { let response = self.channel.send(GetListeners); let response = response .await @@ -132,14 +120,19 @@ impl NodeDriver { Ok(response) } - pub async fn connected_peers(&mut self) -> Result { + pub async fn connected_peers(&self) -> Result { let response = self.channel.send(GetConnectedPeers); - let response = response.await?.iter().map(js_value_from_display).collect(); + let response = response + .await + .unwrap() + .iter() + .map(js_value_from_display) + .collect(); Ok(response) } - pub async fn set_peer_trust(&mut self, peer_id: &str, is_trusted: bool) -> Result<()> { + pub async fn set_peer_trust(&self, peer_id: &str, is_trusted: bool) -> Result<()> { let command = SetPeerTrust { peer_id: peer_id.to_string(), is_trusted, @@ -149,72 +142,81 @@ impl NodeDriver { Ok(response.await.unwrap()) } - pub async fn request_head_header(&mut self) -> Result { + pub async fn request_head_header(&self) -> Result { let command = RequestHeader(SingleHeaderQuery::Head); let response = self.channel.send(command); - - Ok(to_value(&response.await.unwrap())?) + Ok(to_value(&response.await.unwrap()?)?) } - pub async fn request_header_by_hash(&mut self, hash: &str) -> Result { + + pub async fn request_header_by_hash(&self, hash: &str) -> Result { let command = RequestHeader(SingleHeaderQuery::ByHash(hash.parse()?)); let response = self.channel.send(command); - Ok(to_value(&response.await.unwrap())?) + Ok(to_value(&response.await.unwrap()?)?) } - pub async fn request_header_by_height(&mut self, height: u64) -> Result { + + pub async fn request_header_by_height(&self, height: u64) -> Result { let command = RequestHeader(SingleHeaderQuery::ByHeight(height)); let response = self.channel.send(command); - Ok(to_value(&response.await.unwrap())?) + Ok(to_value(&response.await.unwrap()?)?) } - pub async fn request_verified_headers(&mut self, from: JsValue, amount: u64) -> Result { + + pub async fn request_verified_headers( + &self, + from_header: JsValue, + amount: u64, + ) -> Result { + let from = from_value(from_header)?; let command = RequestMultipleHeaders(MultipleHeaderQuery::GetVerified { from, amount }); let response = self.channel.send(command); let result = response .await - .unwrap() .iter() .map(|h| to_value(&h).unwrap()) // XXX .collect(); Ok(result) } - pub async fn syncer_info(&mut self) -> Result { - let response = self.channel.send(GetSyncerInfo); - let response = response.await.unwrap(); + pub async fn syncer_info(&self) -> Result { + let response = self.channel.send(GetSyncerInfo); - Ok(to_value(&response)?) + Ok(to_value(&response.await.unwrap())?) } - pub async fn get_network_head_header(&mut self) -> Result { - let command = RequestHeader(SingleHeaderQuery::Head); // XXX ??? - let response = self.channel.send(command); + pub async fn get_network_head_header(&self) -> Result { + todo!() + /* + let command = todo!(); + let response = self.channel.send(command); - Ok(to_value(&response.await.unwrap())?) + Ok(to_value(&response.await.unwrap())?) + */ } - pub async fn get_local_head_header(&mut self) -> Result { - let command = GetHeader(SingleHeaderQuery::Head); // XXX ??? + + pub async fn get_local_head_header(&self) -> Result { + let command = GetHeader(SingleHeaderQuery::Head); let response = self.channel.send(command); Ok(to_value(&response.await.unwrap())?) } - pub async fn get_header_by_hash(&mut self, hash: &str) -> Result { + pub async fn get_header_by_hash(&self, hash: &str) -> Result { let command = GetHeader(SingleHeaderQuery::ByHash(hash.parse()?)); let response = self.channel.send(command); Ok(to_value(&response.await.unwrap())?) } - pub async fn get_header_by_height(&mut self, height: u64) -> Result { + pub async fn get_header_by_height(&self, height: u64) -> Result { let command = GetHeader(SingleHeaderQuery::ByHeight(height)); let response = self.channel.send(command); Ok(to_value(&response.await.unwrap())?) } pub async fn get_headers( - &mut self, + &self, start_height: Option, end_height: Option, ) -> Result { @@ -225,14 +227,13 @@ impl NodeDriver { let response = self.channel.send(command); let result = response .await - .unwrap() .iter() .map(|h| to_value(&h).unwrap()) .collect(); Ok(result) } - pub async fn get_sampling_metadata(&mut self, height: u64) -> Result { + pub async fn get_sampling_metadata(&self, height: u64) -> Result { let command = GetSamplingMetadata { height }; let response = self.channel.send(command); @@ -240,6 +241,7 @@ impl NodeDriver { } } +/* #[wasm_bindgen(js_class = Node)] impl WasmNode { /// Create a new Lumina node. @@ -423,6 +425,7 @@ impl WasmNode { Ok(to_value(&metadata)?) } } +*/ #[wasm_bindgen(js_class = NodeConfig)] impl WasmNodeConfig { diff --git a/node-wasm/src/utils.rs b/node-wasm/src/utils.rs index 2008f4ed..37396067 100644 --- a/node-wasm/src/utils.rs +++ b/node-wasm/src/utils.rs @@ -1,30 +1,28 @@ //! Various utilities for interacting with node from wasm. -use std::fmt; -use std::fmt::Debug; +use std::fmt::{self, Debug}; use std::marker::PhantomData; -use lumina_node::network; +use futures::future::BoxFuture; +use futures::FutureExt; +use futures::TryFutureExt; +use serde::de::DeserializeOwned; +use serde::{Deserialize, Serialize}; use serde_repr::{Deserialize_repr, Serialize_repr}; +use serde_wasm_bindgen::{from_value, to_value}; +use tokio::sync::{mpsc, oneshot}; use tracing_subscriber::filter::LevelFilter; use tracing_subscriber::fmt::format::Pretty; use tracing_subscriber::fmt::time::UtcTime; use tracing_subscriber::prelude::*; use tracing_web::{performance_layer, MakeConsoleWriter}; use wasm_bindgen::prelude::*; +use wasm_bindgen_futures::spawn_local; +use web_sys::{MessageEvent, MessagePort, SharedWorker, SharedWorkerGlobalScope}; + +use lumina_node::network; use crate::worker::NodeCommand; -use serde::de::DeserializeOwned; -use serde::{Deserialize, Serialize}; -use serde_wasm_bindgen::from_value; -use serde_wasm_bindgen::to_value; -use tokio::sync::mpsc; -use tokio::sync::oneshot; -use wasm_bindgen_futures::spawn_local; -use web_sys::MessageEvent; -use web_sys::MessagePort; -use web_sys::SharedWorker; -use web_sys::SharedWorkerGlobalScope; pub type CommandResponseChannel = oneshot::Sender<::Output>; #[derive(Serialize, Deserialize, Debug)] @@ -32,23 +30,6 @@ pub struct NodeCommandResponse(pub T::Output) where T: NodeCommandType, T::Output: Debug + Serialize; -//{ output: T::Output, } - -//command: PhantomDataT, -//tx: oneshot::Sender, - -/* -#[derive(Serialize, Deserialize, Debug)] -pub struct NodeCommandSender { - parameters: T::Input, -} - -impl NodeCommandSender { - pub fn new(parameters: T::Input) -> Self { - Self { parameters } - } -} -*/ pub trait NodeCommandType: Debug + Into { type Output; @@ -56,18 +37,20 @@ pub trait NodeCommandType: Debug + Into { pub struct BChannel { _onmessage: Closure, + _forwarding_task: (), channel: MessagePort, - msg_rx: mpsc::Receiver, + response_channels: mpsc::Sender>, send_type: PhantomData, } impl BChannel where IN: Serialize, - OUT: DeserializeOwned + 'static, + // XXX: send sync shouldn't be needed + OUT: DeserializeOwned + 'static + Send + Sync, { pub fn new(channel: MessagePort) -> Self { - let (tx, rx) = mpsc::channel(64); + let (tx, mut message_rx) = mpsc::channel(64); let near_tx = tx.clone(); let f = move |ev: MessageEvent| { @@ -81,33 +64,65 @@ where }) }; + let (response_tx, mut response_rx) = mpsc::channel(1); + let forwarding_task = spawn_local(async move { + loop { + let message = message_rx.recv().await.unwrap(); + let response_channel: oneshot::Sender = response_rx.recv().await.unwrap(); + response_channel + .send(message) + .ok() + .expect("forwarding broke"); + } + }); + let onmessage = Closure::new(f); channel.set_onmessage(Some(onmessage.as_ref().unchecked_ref())); channel.start(); Self { + response_channels: response_tx, _onmessage: onmessage, + _forwarding_task: forwarding_task, channel, - msg_rx: rx, send_type: PhantomData, } } - pub fn send(&self, command: T) -> oneshot::Receiver { + pub fn send( + &self, + command: T, + ) -> BoxFuture<'static, Result> + where + T::Output: Debug + Serialize, + NodeCommandResponse: TryFrom, + as TryFrom>::Error: Debug, + { let message: NodeCommand = command.into(); self.send_enum(message); - todo!() + let (tx, rx) = oneshot::channel(); + + self.response_channels.try_send(tx).expect("busy"); + + // unwrap "handles" invalid type + rx.map_ok(|r| { + tracing::info!("type = , expected = {}", std::any::type_name::()); + NodeCommandResponse::::try_from(r) + .expect("invalid type") + .0 + }) + .boxed() } fn send_enum(&self, msg: NodeCommand) { let v = to_value(&msg).unwrap(); self.channel.post_message(&v).expect("err post"); } +} - pub async fn recv(&mut self) -> Option { - self.msg_rx.recv().await - } +fn string_type_of(_: &T) -> String { + format!("{}", std::any::type_name::()) } // TODO: cleanup JS objects on drop @@ -158,9 +173,6 @@ pub fn setup_logging() { .with(fmt_layer) .with(perf_layer) .init(); - - //launch_worker(); - //create_channel(); } impl From for network::Network { diff --git a/node-wasm/src/worker.rs b/node-wasm/src/worker.rs index 3a1ae326..3fbb79c6 100644 --- a/node-wasm/src/worker.rs +++ b/node-wasm/src/worker.rs @@ -1,7 +1,7 @@ -use std::fmt::Debug; +use std::error::Error; +use std::fmt::{self, Debug, Display}; use futures::future::{BoxFuture, FutureExt, TryFutureExt}; -use instant::Instant; use libp2p::Multiaddr; use serde::{Deserialize, Serialize}; use serde_wasm_bindgen::{from_value, to_value}; @@ -13,7 +13,7 @@ use web_sys::{MessageEvent, MessagePort, SharedWorker}; use celestia_types::hash::Hash; use celestia_types::ExtendedHeader; -use lumina_node::node::Node; +use lumina_node::node::{Node, NodeError}; use lumina_node::peer_tracker::PeerTrackerInfo; use lumina_node::store::{IndexedDbStore, SamplingMetadata, Store}; use lumina_node::syncer::SyncingInfo; @@ -22,241 +22,155 @@ use crate::node::WasmNodeConfig; use crate::utils::{CommandResponseChannel, NodeCommandResponse, NodeCommandType, WorkerSelf}; use crate::wrapper::libp2p::NetworkInfoSnapshot; -#[derive(Debug, Serialize, Deserialize)] -pub struct IsRunning; -impl NodeCommandType for IsRunning { - type Output = bool; -} -impl From for NodeCommand { - fn from(command: IsRunning) -> NodeCommand { - NodeCommand::IsRunning(command) - } -} - -#[derive(Debug, Serialize, Deserialize)] -pub enum NodeState { - NodeStopped, - NodeStarted, - AlreadyRunning(u64), -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct StartNode(pub WasmNodeConfig); -impl NodeCommandType for StartNode { - type Output = NodeState; -} -impl From for NodeCommand { - fn from(command: StartNode) -> NodeCommand { - NodeCommand::StartNode(command) - } -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct GetLocalPeerId; -impl NodeCommandType for GetLocalPeerId { - type Output = String; -} -impl From for NodeCommand { - fn from(command: GetLocalPeerId) -> NodeCommand { - NodeCommand::GetLocalPeerId(command) - } -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct GetSyncerInfo; -impl NodeCommandType for GetSyncerInfo { - type Output = SyncingInfo; -} -impl From for NodeCommand { - fn from(command: GetSyncerInfo) -> NodeCommand { - NodeCommand::GetSyncerInfo(command) - } -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct GetPeerTrackerInfo; -impl NodeCommandType for GetPeerTrackerInfo { - type Output = PeerTrackerInfo; -} -impl From for NodeCommand { - fn from(command: GetPeerTrackerInfo) -> NodeCommand { - NodeCommand::GetPeerTrackerInfo(command) - } -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct GetNetworkInfo; -impl NodeCommandType for GetNetworkInfo { - type Output = NetworkInfoSnapshot; -} -impl From for NodeCommand { - fn from(command: GetNetworkInfo) -> NodeCommand { - NodeCommand::GetNetworkInfo(command) - } -} +type Result = std::result::Result; #[derive(Debug, Serialize, Deserialize)] -pub struct GetConnectedPeers; -impl NodeCommandType for GetConnectedPeers { - type Output = Vec; -} -impl From for NodeCommand { - fn from(command: GetConnectedPeers) -> NodeCommand { - NodeCommand::GetConnectedPeers(command) - } -} +pub struct WorkerError(String); -/* -#[derive(Debug, Serialize, Deserialize)] -pub struct GetNetworkHeadHeader; -impl NodeCommandType for GetNetworkHeadHeader { - type Output = JsValue; -} -impl From for NodeCommand { - fn from(command: GetNetworkHeadHeader) -> NodeCommand { - NodeCommand::GetNetworkHeadHeader(command) +impl Error for WorkerError {} +impl Display for WorkerError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "WorkerError({})", self.0) } } -*/ -/* -#[derive(Debug, Serialize, Deserialize)] -pub struct GetLocalHeadHeader; -impl NodeCommandType for GetLocalHeadHeader { - type Output = JsValue; -} -impl From for NodeCommand { - fn from(command: GetLocalHeadHeader) -> NodeCommand { - NodeCommand::GetLocalHeadHeader(command) +impl From for WorkerError { + fn from(error: NodeError) -> WorkerError { + WorkerError(error.to_string()) } } -#[derive(Debug, Serialize, Deserialize)] -pub struct RequestHeadHeader; -impl NodeCommandType for RequestHeadHeader { - type Output = JsValue; -} -impl From for NodeCommand { - fn from(command: RequestHeadHeader) -> NodeCommand { - NodeCommand::RequestHeadHeader(command) - } -} -*/ - -#[derive(Debug, Serialize, Deserialize)] -pub struct SetPeerTrust { - pub peer_id: String, - pub is_trusted: bool, -} -impl NodeCommandType for SetPeerTrust { - type Output = (); -} -impl From for NodeCommand { - fn from(command: SetPeerTrust) -> NodeCommand { - NodeCommand::SetPeerTrust(command) - } -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct WaitConnected { - pub trusted: bool, -} -impl NodeCommandType for WaitConnected { - type Output = (); -} -impl From for NodeCommand { - fn from(command: WaitConnected) -> NodeCommand { - NodeCommand::WaitConnected(command) - } -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct GetListeners; -impl NodeCommandType for GetListeners { - type Output = Vec; -} -impl From for NodeCommand { - fn from(command: GetListeners) -> NodeCommand { - NodeCommand::GetListeners(command) - } -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct RequestHeader(pub SingleHeaderQuery); -impl NodeCommandType for RequestHeader { - type Output = ExtendedHeader; -} -impl From for NodeCommand { - fn from(command: RequestHeader) -> NodeCommand { - NodeCommand::RequestHeader(command) - } +macro_rules! define_command_from_impl { + ($common_name:ident, $command_name:ident) => { + impl From<$command_name> for $common_name { + fn from(command: $command_name) -> Self { + $common_name::$command_name(command) + } + } + }; } -#[derive(Debug, Serialize, Deserialize)] -pub struct RequestMultipleHeaders(pub MultipleHeaderQuery); -impl NodeCommandType for RequestMultipleHeaders { - type Output = Vec; -} -impl From for NodeCommand { - fn from(command: RequestMultipleHeaders) -> NodeCommand { - NodeCommand::RequestMultipleHeaders(command) - } +macro_rules! define_command_type_impl { + ($common_type:ident, $command_name:ident, $output:ty) => { + impl $common_type for $command_name { + type Output = $output; + } + }; } -#[derive(Debug, Serialize, Deserialize)] -pub struct GetHeader(pub SingleHeaderQuery); -impl NodeCommandType for GetHeader { - type Output = ExtendedHeader; -} -impl From for NodeCommand { - fn from(command: GetHeader) -> NodeCommand { - NodeCommand::GetHeader(command) - } -} +macro_rules! define_response_try_from_impl { + ($common_type:ident, $helper_type:ident, $command_name:ident) => { + impl TryFrom<$common_type> for $helper_type<$command_name> { + type Error = (); + fn try_from(response: $common_type) -> Result { + if let $common_type::$command_name(cmd) = response { + Ok(cmd) + } else { + Err(()) + } + } + } + }; +} + +macro_rules! define_command { + ($command_name:ident -> $output:ty) => { + #[derive(Debug, Serialize, Deserialize)] + pub struct $command_name; + define_command_type_impl!(NodeCommandType, $command_name, $output); + define_command_from_impl!(NodeCommand, $command_name); + define_response_try_from_impl!(NodeResponse, NodeCommandResponse, $command_name); + }; + ($command_name:ident ($($param:ty),+) -> $output:ty) => { + #[derive(Debug, Serialize, Deserialize)] + pub struct $command_name($(pub $param,)+); + define_command_type_impl!(NodeCommandType, $command_name, $output); + define_command_from_impl!(NodeCommand, $command_name); + define_response_try_from_impl!(NodeResponse, NodeCommandResponse, $command_name); + }; + ($command_name:ident {$($param_name:ident : $param_type:ty),+} -> $output:ty) => { + #[derive(Debug, Serialize, Deserialize)] + pub struct $command_name { $(pub $param_name: $param_type,)+} + define_command_type_impl!(NodeCommandType, $command_name, $output); + define_command_from_impl!(NodeCommand, $command_name); + define_response_try_from_impl!(NodeResponse, NodeCommandResponse, $command_name); + }; +} + +macro_rules! define_common_types { + ($($command_name:ident),+ $(,)?) => { + #[derive(Serialize, Deserialize, Debug)] + pub enum NodeCommand { + $($command_name($command_name),)+ + } -#[derive(Debug, Serialize, Deserialize)] -pub struct GetMultipleHeaders(pub MultipleHeaderQuery); -impl NodeCommandType for GetMultipleHeaders { - type Output = Vec; -} -impl From for NodeCommand { - fn from(command: GetMultipleHeaders) -> NodeCommand { - NodeCommand::GetMultipleHeaders(command) - } -} + #[derive(Debug)] + pub enum NodeCommandWithChannel { + $($command_name(($command_name, CommandResponseChannel<$command_name>)),)+ + } -#[derive(Debug, Serialize, Deserialize)] -pub struct GetSamplingMetadata { - pub height: u64, -} -impl NodeCommandType for GetSamplingMetadata { - type Output = SamplingMetadata; -} -impl From for NodeCommand { - fn from(command: GetSamplingMetadata) -> NodeCommand { - NodeCommand::GetSamplingMetadata(command) - } -} + #[derive(Serialize, Deserialize, Debug)] + pub enum NodeResponse { + $($command_name(NodeCommandResponse<$command_name>),)+ + } -// type being transmitted over the JS channel -#[derive(Serialize, Deserialize, Debug)] -pub enum NodeCommand { - IsRunning(IsRunning), - StartNode(StartNode), - GetLocalPeerId(GetLocalPeerId), - GetSyncerInfo(GetSyncerInfo), - GetPeerTrackerInfo(GetPeerTrackerInfo), - GetNetworkInfo(GetNetworkInfo), - GetConnectedPeers(GetConnectedPeers), - SetPeerTrust(SetPeerTrust), - WaitConnected(WaitConnected), - GetListeners(GetListeners), - RequestHeader(RequestHeader), - RequestMultipleHeaders(RequestMultipleHeaders), - GetHeader(GetHeader), - GetMultipleHeaders(GetMultipleHeaders), - GetSamplingMetadata(GetSamplingMetadata), -} + impl NodeCommand { + fn add_response_channel(self) -> (NodeCommandWithChannel, + BoxFuture<'static, Result>, // XXX + ) { + match self { + $( + NodeCommand::$command_name(cmd) => { + let (tx, rx) = oneshot::channel(); + ( + NodeCommandWithChannel::$command_name((cmd, tx)), + rx.map_ok(|r| NodeResponse::$command_name( + NodeCommandResponse::<$command_name>(r) + )).boxed() + ) + } + )+ + } + } + } + }; +} + +define_common_types!( + IsRunning, + StartNode, + GetLocalPeerId, + GetSyncerInfo, + GetPeerTrackerInfo, + GetNetworkInfo, + GetConnectedPeers, + SetPeerTrust, + WaitConnected, + GetListeners, + RequestHeader, + RequestMultipleHeaders, + GetHeader, + GetMultipleHeaders, + LastSeenNetworkHead, + GetSamplingMetadata, +); + +define_command!(IsRunning -> bool); +define_command!(StartNode(WasmNodeConfig) -> Result<()>); +define_command!(GetLocalPeerId -> String); +define_command!(GetSyncerInfo -> SyncingInfo); +define_command!(GetPeerTrackerInfo -> PeerTrackerInfo); +define_command!(GetNetworkInfo -> NetworkInfoSnapshot); +define_command!(GetConnectedPeers -> Vec); +define_command!(SetPeerTrust { peer_id: String, is_trusted: bool } -> ()); +define_command!(WaitConnected { trusted: bool } -> ()); +define_command!(GetListeners -> Vec); +define_command!(RequestHeader(SingleHeaderQuery) -> Result); +define_command!(RequestMultipleHeaders(MultipleHeaderQuery) -> Vec); +define_command!(GetHeader(SingleHeaderQuery) -> ExtendedHeader); +define_command!(GetMultipleHeaders(MultipleHeaderQuery) -> Vec); +define_command!(LastSeenNetworkHead -> Option); +define_command!(GetSamplingMetadata { height: u64 } -> SamplingMetadata); #[derive(Serialize, Deserialize, Debug)] pub enum SingleHeaderQuery { @@ -268,8 +182,7 @@ pub enum SingleHeaderQuery { #[derive(Serialize, Deserialize, Debug)] pub enum MultipleHeaderQuery { GetVerified { - #[serde(with = "serde_wasm_bindgen::preserve")] - from: JsValue, + from: ExtendedHeader, amount: u64, }, Range { @@ -278,232 +191,8 @@ pub enum MultipleHeaderQuery { }, } -impl NodeCommand { - fn add_response_channel( - self, - ) -> ( - NodeCommandWithChannel, - BoxFuture<'static, Result>, // XXX - // type cleanup - ) { - match self { - NodeCommand::IsRunning(cmd) => { - let (tx, rx) = oneshot::channel(); - ( - NodeCommandWithChannel::IsRunning((cmd, tx)), - rx.map_ok(|r| NodeResponse::IsRunning(NodeCommandResponse::(r))) - .boxed(), - ) - } - NodeCommand::StartNode(cmd) => { - let (tx, rx) = oneshot::channel(); - ( - NodeCommandWithChannel::StartNode((cmd, tx)), - rx.map_ok(|r| NodeResponse::StartNode(NodeCommandResponse::(r))) - .boxed(), - ) - } - NodeCommand::GetLocalPeerId(cmd) => { - let (tx, rx) = oneshot::channel(); - ( - NodeCommandWithChannel::GetLocalPeerId((cmd, tx)), - rx.map_ok(|r| { - NodeResponse::GetLocalPeerId(NodeCommandResponse::(r)) - }) - .boxed(), - ) - } - NodeCommand::GetSyncerInfo(cmd) => { - let (tx, rx) = oneshot::channel(); - ( - NodeCommandWithChannel::GetSyncerInfo((cmd, tx)), - rx.map_ok(|r| { - NodeResponse::GetSyncerInfo(NodeCommandResponse::(r)) - }) - .boxed(), - ) - } - NodeCommand::GetPeerTrackerInfo(cmd) => { - let (tx, rx) = oneshot::channel(); - ( - NodeCommandWithChannel::GetPeerTrackerInfo((cmd, tx)), - rx.map_ok(|r| { - NodeResponse::GetPeerTrackerInfo(NodeCommandResponse::( - r, - )) - }) - .boxed(), - ) - } - NodeCommand::GetNetworkInfo(cmd) => { - let (tx, rx) = oneshot::channel(); - ( - NodeCommandWithChannel::GetNetworkInfo((cmd, tx)), - rx.map_ok(|r| { - NodeResponse::GetNetworkInfo(NodeCommandResponse::(r)) - }) - .boxed(), - ) - } - NodeCommand::GetConnectedPeers(cmd) => { - let (tx, rx) = oneshot::channel(); - ( - NodeCommandWithChannel::GetConnectedPeers((cmd, tx)), - rx.map_ok(|r| { - NodeResponse::GetConnectedPeers(NodeCommandResponse::(r)) - }) - .boxed(), - ) - } - NodeCommand::SetPeerTrust(cmd) => { - let (tx, rx) = oneshot::channel(); - ( - NodeCommandWithChannel::SetPeerTrust((cmd, tx)), - rx.map_ok(|r| { - NodeResponse::SetPeerTrust(NodeCommandResponse::(r)) - }) - .boxed(), - ) - } - NodeCommand::WaitConnected(cmd) => { - let (tx, rx) = oneshot::channel(); - ( - NodeCommandWithChannel::WaitConnected((cmd, tx)), - rx.map_ok(|r| { - NodeResponse::WaitConnected(NodeCommandResponse::(r)) - }) - .boxed(), - ) - } - NodeCommand::GetListeners(cmd) => { - let (tx, rx) = oneshot::channel(); - ( - NodeCommandWithChannel::GetListeners((cmd, tx)), - rx.map_ok(|r| { - NodeResponse::GetListeners(NodeCommandResponse::(r)) - }) - .boxed(), - ) - } - NodeCommand::RequestHeader(cmd) => { - let (tx, rx) = oneshot::channel(); - ( - NodeCommandWithChannel::RequestHeader((cmd, tx)), - rx.map_ok(|r| { - NodeResponse::RequestHeader(NodeCommandResponse::(r)) - }) - .boxed(), - ) - } - NodeCommand::RequestMultipleHeaders(cmd) => { - let (tx, rx) = oneshot::channel(); - ( - NodeCommandWithChannel::RequestMultipleHeaders((cmd, tx)), - rx.map_ok(|r| { - NodeResponse::RequestMultipleHeaders(NodeCommandResponse::< - RequestMultipleHeaders, - >(r)) - }) - .boxed(), - ) - } - NodeCommand::GetHeader(cmd) => { - let (tx, rx) = oneshot::channel(); - ( - NodeCommandWithChannel::GetHeader((cmd, tx)), - rx.map_ok(|r| NodeResponse::GetHeader(NodeCommandResponse::(r))) - .boxed(), - ) - } - NodeCommand::GetMultipleHeaders(cmd) => { - let (tx, rx) = oneshot::channel(); - ( - NodeCommandWithChannel::GetMultipleHeaders((cmd, tx)), - rx.map_ok(|r| { - NodeResponse::GetMultipleHeaders(NodeCommandResponse::( - r, - )) - }) - .boxed(), - ) - } - NodeCommand::GetSamplingMetadata(cmd) => { - let (tx, rx) = oneshot::channel(); - ( - NodeCommandWithChannel::GetSamplingMetadata((cmd, tx)), - rx.map_ok(|r| { - NodeResponse::GetSamplingMetadata( - NodeCommandResponse::(r), - ) - }) - .boxed(), - ) - } - } - } -} - -#[derive(Debug)] -pub enum NodeCommandWithChannel { - IsRunning((IsRunning, CommandResponseChannel)), - StartNode((StartNode, CommandResponseChannel)), - GetLocalPeerId((GetLocalPeerId, CommandResponseChannel)), - GetSyncerInfo((GetSyncerInfo, CommandResponseChannel)), - GetPeerTrackerInfo( - ( - GetPeerTrackerInfo, - CommandResponseChannel, - ), - ), - GetNetworkInfo((GetNetworkInfo, CommandResponseChannel)), - GetConnectedPeers((GetConnectedPeers, CommandResponseChannel)), - SetPeerTrust((SetPeerTrust, CommandResponseChannel)), - WaitConnected((WaitConnected, CommandResponseChannel)), - GetListeners((GetListeners, CommandResponseChannel)), - RequestHeader((RequestHeader, CommandResponseChannel)), - RequestMultipleHeaders( - ( - RequestMultipleHeaders, - CommandResponseChannel, - ), - ), - GetHeader((GetHeader, CommandResponseChannel)), - GetMultipleHeaders( - ( - GetMultipleHeaders, - CommandResponseChannel, - ), - ), - GetSamplingMetadata( - ( - GetSamplingMetadata, - CommandResponseChannel, - ), - ), -} - -#[derive(Serialize, Deserialize, Debug)] -pub enum NodeResponse { - IsRunning(NodeCommandResponse), - StartNode(NodeCommandResponse), - GetLocalPeerId(NodeCommandResponse), - GetSyncerInfo(NodeCommandResponse), - GetPeerTrackerInfo(NodeCommandResponse), - GetNetworkInfo(NodeCommandResponse), - GetConnectedPeers(NodeCommandResponse), - SetPeerTrust(NodeCommandResponse), - WaitConnected(NodeCommandResponse), - GetListeners(NodeCommandResponse), - RequestHeader(NodeCommandResponse), - RequestMultipleHeaders(NodeCommandResponse), - GetHeader(NodeCommandResponse), - GetMultipleHeaders(NodeCommandResponse), - GetSamplingMetadata(NodeCommandResponse), -} - struct NodeWorker { node: Node, - start_timestamp: Instant, } impl NodeWorker { @@ -518,10 +207,7 @@ impl NodeWorker { let node = Node::new(config).await.ok().unwrap(); - Self { - node, - start_timestamp: Instant::now(), - } + Self { node } } async fn process_command(&mut self, command: NodeCommandWithChannel) { @@ -532,12 +218,7 @@ impl NodeWorker { } NodeCommandWithChannel::StartNode((_, response)) => { response - .send(NodeState::AlreadyRunning( - Instant::now() - .checked_duration_since(self.start_timestamp) - .map(|duration| duration.as_secs()) - .unwrap_or(0), - )) + .send(Err(WorkerError("already running".to_string()))) .expect("channel_dropped"); } NodeCommandWithChannel::GetLocalPeerId((_, response)) => { @@ -563,20 +244,6 @@ impl NodeWorker { .send(connected_peers.iter().map(|id| id.to_string()).collect()) .expect("channel_dropped"); } - /* - NodeCommandWithChannel::GetNetworkHeadHeader(command) => { - let _ = to_value(&self.node.get_network_head_header()) - .ok() - .expect("TODO"); - NodeResponse::Header(self.network_head_header().await.ok().unwrap()) - } - NodeCommandWithChannel::GetLocalHeadHeader(command) => { - let _ = to_value(&self.node.get_local_head_header().await.unwrap()) - .ok() - .unwrap(); - NodeResponse::Header(self.local_head_header().await.ok().unwrap()) - } - */ NodeCommandWithChannel::SetPeerTrust(( SetPeerTrust { peer_id, @@ -600,41 +267,39 @@ impl NodeWorker { } response.send(()).expect("channel_dropped") } - NodeCommandWithChannel::GetListeners((_command, _response)) => { - todo!() - } + NodeCommandWithChannel::GetListeners((_, response)) => response + .send(self.node.listeners().await.unwrap()) + .expect("channel_dropped"), NodeCommandWithChannel::RequestHeader((command, response)) => { let header = match command.0 { - SingleHeaderQuery::Head => todo!(), + SingleHeaderQuery::Head => self.node.request_head_header().await, SingleHeaderQuery::ByHash(hash) => { - self.node.request_header_by_hash(&hash).await.ok().unwrap() + self.node.request_header_by_hash(&hash).await + } + SingleHeaderQuery::ByHeight(height) => { + self.node.request_header_by_height(height).await } - SingleHeaderQuery::ByHeight(height) => self - .node - .request_header_by_height(height) - .await - .ok() - .unwrap(), }; - response.send(header).expect("channel_dropped"); + response + .send(header.map_err(|e| e.into())) + .expect("channel_dropped"); } NodeCommandWithChannel::RequestMultipleHeaders((command, response)) => { let headers = match command.0 { - MultipleHeaderQuery::GetVerified { from, amount } => { - let from_header = from_value(from).ok().unwrap(); - self.node - .request_verified_headers(&from_header, amount) - .await - .unwrap() - } - MultipleHeaderQuery::Range { .. } => unreachable!(), + MultipleHeaderQuery::GetVerified { from, amount } => self + .node + .request_verified_headers(&from, amount) + .await + .unwrap(), + MultipleHeaderQuery::Range { .. } => unreachable!("invalid command"), }; response.send(headers).expect("channel_dropped"); } NodeCommandWithChannel::GetHeader((command, response)) => { let header = match command.0 { - SingleHeaderQuery::Head => todo!(), + SingleHeaderQuery::Head => self.node.get_local_head_header().await.unwrap(), + //SingleHeaderQuery::NetworkHead => self.node.get_network_head_header().unwrap(), SingleHeaderQuery::ByHash(hash) => { self.node.get_header_by_hash(&hash).await.ok().unwrap() } @@ -646,13 +311,11 @@ impl NodeWorker { } NodeCommandWithChannel::GetMultipleHeaders((command, response)) => { let headers = match command.0 { - MultipleHeaderQuery::GetVerified { from, amount } => { - let from_header = from_value(from).ok().unwrap(); - self.node - .request_verified_headers(&from_header, amount) - .await - .unwrap() - } + MultipleHeaderQuery::GetVerified { from, amount } => self + .node + .request_verified_headers(&from, amount) + .await + .unwrap(), MultipleHeaderQuery::Range { start_height, end_height, @@ -668,6 +331,10 @@ impl NodeWorker { response.send(headers).expect("channel_dropped"); } + NodeCommandWithChannel::LastSeenNetworkHead((_, response)) => { + let header = self.node.get_network_head_header(); + response.send(header).expect("channel_dropped"); + } NodeCommandWithChannel::GetSamplingMetadata((command, response)) => { let metadata = self .node @@ -800,9 +467,7 @@ pub async fn run_worker(queued_connections: Vec) { } NodeCommandWithChannel::StartNode((command, response)) => { worker = Some(NodeWorker::new(command.0).await); - response - .send(NodeState::NodeStarted) - .expect("channel_dropped"); + response.send(Ok(())).expect("channel_dropped"); } _ => warn!("Worker not running"), } From ad9a2f516edc63f8cdb33889db9afd6a12deb59f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Florkiewicz?= Date: Fri, 12 Apr 2024 14:41:07 +0200 Subject: [PATCH 06/35] cleanup --- cli/static/run_node.js | 2 +- node-wasm/src/node.rs | 5 +- node-wasm/src/utils.rs | 118 +--------------------------------------- node-wasm/src/worker.rs | 109 +++++++++++++++++++++++++++++++++++-- 4 files changed, 110 insertions(+), 124 deletions(-) diff --git a/cli/static/run_node.js b/cli/static/run_node.js index 93235a29..56d46efb 100644 --- a/cli/static/run_node.js +++ b/cli/static/run_node.js @@ -1,6 +1,6 @@ Error.stackTraceLimit = 99; // rust stack traces can get pretty big, increase the default -import init, { Node, NodeConfig, NodeDriver } from "/wasm/lumina_node_wasm.js"; +import init, { NodeConfig, NodeDriver } from "/wasm/lumina_node_wasm.js"; async function fetch_config() { const response = await fetch('/cfg.json'); diff --git a/node-wasm/src/node.rs b/node-wasm/src/node.rs index e3e3509d..835b62ca 100644 --- a/node-wasm/src/node.rs +++ b/node-wasm/src/node.rs @@ -15,7 +15,8 @@ use tracing::info; use wasm_bindgen::prelude::*; use web_sys::{SharedWorker, WorkerOptions, WorkerType}; -use crate::utils::{js_value_from_display, BChannel, JsContext, Network}; +use crate::utils::{js_value_from_display, JsContext, Network}; +use crate::worker::BChannel; use crate::worker::{ GetConnectedPeers, GetHeader, GetListeners, GetLocalPeerId, GetMultipleHeaders, GetNetworkInfo, GetPeerTrackerInfo, GetSamplingMetadata, GetSyncerInfo, IsRunning, MultipleHeaderQuery, @@ -64,7 +65,7 @@ impl NodeDriver { } } - pub async fn is_running(&self) -> bool { + pub async fn is_running(&mut self) -> bool { let response = self.channel.send(IsRunning); response.await.unwrap() diff --git a/node-wasm/src/utils.rs b/node-wasm/src/utils.rs index 37396067..322c3331 100644 --- a/node-wasm/src/utils.rs +++ b/node-wasm/src/utils.rs @@ -1,133 +1,17 @@ //! Various utilities for interacting with node from wasm. - use std::fmt::{self, Debug}; -use std::marker::PhantomData; -use futures::future::BoxFuture; -use futures::FutureExt; -use futures::TryFutureExt; -use serde::de::DeserializeOwned; -use serde::{Deserialize, Serialize}; use serde_repr::{Deserialize_repr, Serialize_repr}; -use serde_wasm_bindgen::{from_value, to_value}; -use tokio::sync::{mpsc, oneshot}; use tracing_subscriber::filter::LevelFilter; use tracing_subscriber::fmt::format::Pretty; use tracing_subscriber::fmt::time::UtcTime; use tracing_subscriber::prelude::*; use tracing_web::{performance_layer, MakeConsoleWriter}; use wasm_bindgen::prelude::*; -use wasm_bindgen_futures::spawn_local; -use web_sys::{MessageEvent, MessagePort, SharedWorker, SharedWorkerGlobalScope}; +use web_sys::{SharedWorker, SharedWorkerGlobalScope}; use lumina_node::network; -use crate::worker::NodeCommand; - -pub type CommandResponseChannel = oneshot::Sender<::Output>; -#[derive(Serialize, Deserialize, Debug)] -pub struct NodeCommandResponse(pub T::Output) -where - T: NodeCommandType, - T::Output: Debug + Serialize; - -pub trait NodeCommandType: Debug + Into { - type Output; -} - -pub struct BChannel { - _onmessage: Closure, - _forwarding_task: (), - channel: MessagePort, - response_channels: mpsc::Sender>, - send_type: PhantomData, -} - -impl BChannel -where - IN: Serialize, - // XXX: send sync shouldn't be needed - OUT: DeserializeOwned + 'static + Send + Sync, -{ - pub fn new(channel: MessagePort) -> Self { - let (tx, mut message_rx) = mpsc::channel(64); - - let near_tx = tx.clone(); - let f = move |ev: MessageEvent| { - let local_tx = near_tx.clone(); - spawn_local(async move { - let message_data = ev.data(); - - let data = from_value(message_data).unwrap(); - - local_tx.send(data).await.expect("send err"); - }) - }; - - let (response_tx, mut response_rx) = mpsc::channel(1); - let forwarding_task = spawn_local(async move { - loop { - let message = message_rx.recv().await.unwrap(); - let response_channel: oneshot::Sender = response_rx.recv().await.unwrap(); - response_channel - .send(message) - .ok() - .expect("forwarding broke"); - } - }); - - let onmessage = Closure::new(f); - - channel.set_onmessage(Some(onmessage.as_ref().unchecked_ref())); - channel.start(); - Self { - response_channels: response_tx, - _onmessage: onmessage, - _forwarding_task: forwarding_task, - channel, - send_type: PhantomData, - } - } - - pub fn send( - &self, - command: T, - ) -> BoxFuture<'static, Result> - where - T::Output: Debug + Serialize, - NodeCommandResponse: TryFrom, - as TryFrom>::Error: Debug, - { - let message: NodeCommand = command.into(); - self.send_enum(message); - - let (tx, rx) = oneshot::channel(); - - self.response_channels.try_send(tx).expect("busy"); - - // unwrap "handles" invalid type - rx.map_ok(|r| { - tracing::info!("type = , expected = {}", std::any::type_name::()); - NodeCommandResponse::::try_from(r) - .expect("invalid type") - .0 - }) - .boxed() - } - - fn send_enum(&self, msg: NodeCommand) { - let v = to_value(&msg).unwrap(); - self.channel.post_message(&v).expect("err post"); - } -} - -fn string_type_of(_: &T) -> String { - format!("{}", std::any::type_name::()) -} - -// TODO: cleanup JS objects on drop -// impl Drop - pub(crate) trait WorkerSelf { type GlobalScope; diff --git a/node-wasm/src/worker.rs b/node-wasm/src/worker.rs index 3fbb79c6..2d21638b 100644 --- a/node-wasm/src/worker.rs +++ b/node-wasm/src/worker.rs @@ -1,12 +1,14 @@ use std::error::Error; use std::fmt::{self, Debug, Display}; +use std::marker::PhantomData; use futures::future::{BoxFuture, FutureExt, TryFutureExt}; use libp2p::Multiaddr; +use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; use serde_wasm_bindgen::{from_value, to_value}; use tokio::sync::{mpsc, oneshot}; -use tracing::{info, trace, warn}; +use tracing::{error, info, trace, warn}; use wasm_bindgen::prelude::*; use wasm_bindgen_futures::spawn_local; use web_sys::{MessageEvent, MessagePort, SharedWorker}; @@ -19,7 +21,7 @@ use lumina_node::store::{IndexedDbStore, SamplingMetadata, Store}; use lumina_node::syncer::SyncingInfo; use crate::node::WasmNodeConfig; -use crate::utils::{CommandResponseChannel, NodeCommandResponse, NodeCommandType, WorkerSelf}; +use crate::utils::WorkerSelf; use crate::wrapper::libp2p::NetworkInfoSnapshot; type Result = std::result::Result; @@ -40,6 +42,106 @@ impl From for WorkerError { } } +pub type CommandResponseChannel = oneshot::Sender<::Output>; +#[derive(Serialize, Deserialize, Debug)] +pub struct NodeCommandResponse(pub T::Output) +where + T: NodeCommandType, + T::Output: Debug + Serialize; + +pub trait NodeCommandType: Debug + Into { + type Output; +} + +pub struct BChannel { + _onmessage: Closure, + _forwarding_task: (), + channel: MessagePort, + response_channels: mpsc::Sender>, + send_type: PhantomData, +} + +impl BChannel +where + IN: Serialize, + // XXX: send sync shouldn't be needed + OUT: DeserializeOwned + 'static + Send + Sync, +{ + pub fn new(channel: MessagePort) -> Self { + let (tx, mut message_rx) = mpsc::channel(64); + + let near_tx = tx.clone(); + let f = move |ev: MessageEvent| { + let local_tx = near_tx.clone(); + spawn_local(async move { + let message_data = ev.data(); + + let data = from_value(message_data).unwrap(); + + local_tx.send(data).await.expect("send err"); + }) + }; + + let (response_tx, mut response_rx) = mpsc::channel(1); + let forwarding_task = spawn_local(async move { + loop { + let message = message_rx.recv().await.unwrap(); + let response_channel: oneshot::Sender = response_rx.recv().await.unwrap(); + response_channel + .send(message) + .ok() + .expect("forwarding broke"); + } + }); + + let onmessage = Closure::new(f); + + channel.set_onmessage(Some(onmessage.as_ref().unchecked_ref())); + channel.start(); + Self { + response_channels: response_tx, + _onmessage: onmessage, + _forwarding_task: forwarding_task, + channel, + send_type: PhantomData, + } + } + + pub fn send( + &self, + command: T, + ) -> BoxFuture<'static, Result> + where + T::Output: Debug + Serialize, + NodeCommandResponse: TryFrom, + as TryFrom>::Error: Debug, + { + let message: NodeCommand = command.into(); + self.send_enum(message); + + let (tx, rx) = oneshot::channel(); + + self.response_channels.try_send(tx).expect("busy"); + + // unwrap "handles" invalid type + rx.map_ok(|r| { + tracing::info!("type = , expected = {}", std::any::type_name::()); + NodeCommandResponse::::try_from(r) + .expect("invalid type") + .0 + }) + .boxed() + } + + fn send_enum(&self, msg: NodeCommand) { + let v = to_value(&msg).unwrap(); + self.channel.post_message(&v).expect("err post"); + } +} + +// TODO: cleanup JS objects on drop +// impl Drop + macro_rules! define_command_from_impl { ($common_name:ident, $command_name:ident) => { impl From<$command_name> for $common_name { @@ -299,7 +401,6 @@ impl NodeWorker { NodeCommandWithChannel::GetHeader((command, response)) => { let header = match command.0 { SingleHeaderQuery::Head => self.node.get_local_head_header().await.unwrap(), - //SingleHeaderQuery::NetworkHead => self.node.get_network_head_header().unwrap(), SingleHeaderQuery::ByHash(hash) => { self.node.get_header_by_hash(&hash).await.ok().unwrap() } @@ -485,5 +586,5 @@ pub async fn run_worker(queued_connections: Vec) { } } - warn!("EXIT EXIT EXIT"); + error!("Worker exited, should not happen"); } From 46768ed2fdd3d6bfb89fd6e7e34b06876246c43e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Florkiewicz?= Date: Fri, 12 Apr 2024 20:04:22 +0200 Subject: [PATCH 07/35] Start squeezing the errors through the channel --- node-wasm/src/node.rs | 42 ++++++++------ node-wasm/src/worker.rs | 126 ++++++++++++++++++++++++++-------------- 2 files changed, 105 insertions(+), 63 deletions(-) diff --git a/node-wasm/src/node.rs b/node-wasm/src/node.rs index 835b62ca..eb7aa04a 100644 --- a/node-wasm/src/node.rs +++ b/node-wasm/src/node.rs @@ -1,27 +1,27 @@ //! A browser compatible wrappers for the [`lumina-node`]. - use std::result::Result as StdResult; use js_sys::Array; use libp2p::identity::Keypair; use libp2p::multiaddr::Protocol; -use lumina_node::blockstore::IndexedDbBlockstore; -use lumina_node::network::{canonical_network_bootnodes, network_genesis, network_id}; -use lumina_node::node::{Node, NodeConfig}; -use lumina_node::store::IndexedDbStore; use serde::{Deserialize, Serialize}; use serde_wasm_bindgen::{from_value, to_value}; -use tracing::info; +use tracing::error; use wasm_bindgen::prelude::*; -use web_sys::{SharedWorker, WorkerOptions, WorkerType}; +use web_sys::{MessageEvent, SharedWorker, WorkerOptions, WorkerType}; + +use lumina_node::blockstore::IndexedDbBlockstore; +use lumina_node::network::{canonical_network_bootnodes, network_genesis, network_id}; +use lumina_node::node::NodeConfig; +use lumina_node::store::IndexedDbStore; use crate::utils::{js_value_from_display, JsContext, Network}; -use crate::worker::BChannel; +use crate::worker::SharedWorkerChannel; use crate::worker::{ GetConnectedPeers, GetHeader, GetListeners, GetLocalPeerId, GetMultipleHeaders, GetNetworkInfo, - GetPeerTrackerInfo, GetSamplingMetadata, GetSyncerInfo, IsRunning, MultipleHeaderQuery, - NodeCommand, NodeResponse, RequestHeader, RequestMultipleHeaders, SetPeerTrust, - SingleHeaderQuery, StartNode, WaitConnected, + GetPeerTrackerInfo, GetSamplingMetadata, GetSyncerInfo, IsRunning, LastSeenNetworkHead, + MultipleHeaderQuery, NodeCommand, NodeResponse, RequestHeader, RequestMultipleHeaders, + SetPeerTrust, SingleHeaderQuery, StartNode, WaitConnected, }; use crate::wrapper::libp2p::NetworkInfoSnapshot; use crate::Result; @@ -44,7 +44,8 @@ pub struct WasmNodeConfig { #[wasm_bindgen] struct NodeDriver { _worker: SharedWorker, - channel: BChannel, + _onerror_callback: Closure, + channel: SharedWorkerChannel, } #[wasm_bindgen] @@ -57,10 +58,16 @@ impl NodeDriver { let worker = SharedWorker::new_with_worker_options("/js/worker.js", &opts) .expect("could not worker"); - let channel = BChannel::new(worker.port()); + let onerror_callback: Closure = Closure::new(|ev: MessageEvent| { + error!("received error from shared worker: {ev:?}"); + }); + worker.set_onerror(Some(onerror_callback.as_ref().unchecked_ref())); + + let channel = SharedWorkerChannel::new(worker.port()); Self { _worker: worker, + _onerror_callback: onerror_callback, channel, } } @@ -188,13 +195,10 @@ impl NodeDriver { } pub async fn get_network_head_header(&self) -> Result { - todo!() - /* - let command = todo!(); - let response = self.channel.send(command); + let command = LastSeenNetworkHead; + let response = self.channel.send(command); - Ok(to_value(&response.await.unwrap())?) - */ + Ok(to_value(&response.await.unwrap())?) } pub async fn get_local_head_header(&self) -> Result { diff --git a/node-wasm/src/worker.rs b/node-wasm/src/worker.rs index 2d21638b..642c0ee2 100644 --- a/node-wasm/src/worker.rs +++ b/node-wasm/src/worker.rs @@ -2,7 +2,7 @@ use std::error::Error; use std::fmt::{self, Debug, Display}; use std::marker::PhantomData; -use futures::future::{BoxFuture, FutureExt, TryFutureExt}; +use futures::future::{BoxFuture, Future, FutureExt, TryFuture, TryFutureExt}; use libp2p::Multiaddr; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; @@ -24,6 +24,12 @@ use crate::node::WasmNodeConfig; use crate::utils::WorkerSelf; use crate::wrapper::libp2p::NetworkInfoSnapshot; +#[derive(Debug, Serialize, Deserialize)] +pub enum WorkerError2 { + SharedWorkerChannelResponseChannelDropped, + SharedWorkerChannelInvalidType, +} + type Result = std::result::Result; #[derive(Debug, Serialize, Deserialize)] @@ -43,6 +49,7 @@ impl From for WorkerError { } pub type CommandResponseChannel = oneshot::Sender<::Output>; + #[derive(Serialize, Deserialize, Debug)] pub struct NodeCommandResponse(pub T::Output) where @@ -53,7 +60,7 @@ pub trait NodeCommandType: Debug + Into { type Output; } -pub struct BChannel { +pub struct SharedWorkerChannel { _onmessage: Closure, _forwarding_task: (), channel: MessagePort, @@ -61,17 +68,17 @@ pub struct BChannel { send_type: PhantomData, } -impl BChannel +impl SharedWorkerChannel where IN: Serialize, // XXX: send sync shouldn't be needed OUT: DeserializeOwned + 'static + Send + Sync, { pub fn new(channel: MessagePort) -> Self { - let (tx, mut message_rx) = mpsc::channel(64); + let (message_tx, mut message_rx) = mpsc::channel(64); - let near_tx = tx.clone(); - let f = move |ev: MessageEvent| { + let near_tx = message_tx.clone(); + let onmessage_callback = move |ev: MessageEvent| { let local_tx = near_tx.clone(); spawn_local(async move { let message_data = ev.data(); @@ -87,15 +94,15 @@ where loop { let message = message_rx.recv().await.unwrap(); let response_channel: oneshot::Sender = response_rx.recv().await.unwrap(); - response_channel - .send(message) - .ok() - .expect("forwarding broke"); + if response_channel.send(message).is_err() { + warn!( + "response channel closed before response could be sent, dropping message" + ); + } } }); - let onmessage = Closure::new(f); - + let onmessage = Closure::new(onmessage_callback); channel.set_onmessage(Some(onmessage.as_ref().unchecked_ref())); channel.start(); Self { @@ -110,7 +117,8 @@ where pub fn send( &self, command: T, - ) -> BoxFuture<'static, Result> + ) -> impl Future> + //BoxFuture<'static, Result> where T::Output: Debug + Serialize, NodeCommandResponse: TryFrom, @@ -123,14 +131,24 @@ where self.response_channels.try_send(tx).expect("busy"); - // unwrap "handles" invalid type - rx.map_ok(|r| { - tracing::info!("type = , expected = {}", std::any::type_name::()); - NodeCommandResponse::::try_from(r) - .expect("invalid type") - .0 - }) - .boxed() + rx.map_ok_or_else( + |_e| Err(WorkerError2::SharedWorkerChannelResponseChannelDropped), + |r| match NodeCommandResponse::::try_from(r) { + Ok(v) => Ok(v.0), + Err(_) => Err(WorkerError2::SharedWorkerChannelInvalidType), + }, + ) + + /* + // unwrap "handles" invalid type + rx.map_ok(|r| { + //tracing::info!("type = , expected = {}", std::any::type_name::()); + NodeCommandResponse::::try_from(r) + .expect("invalid type") + .0 + }) + .boxed() + */ } fn send_enum(&self, msg: NodeCommand) { @@ -465,42 +483,50 @@ enum WorkerMessage { struct ClientId(usize); struct WorkerConnector { - // make sure the callback doesn't get dropped + // same onconnect callback is used throughtout entire Worker lifetime. + // Keep a reference to make sure it doesn't get dropped. _onconnect_callback: Closure, - callbacks: Vec>, - ports: Vec, + // keep a MessagePort for each client to send messages over, as well as callback responsible + // for forwarding messages back + clients: Vec<(MessagePort, Closure)>, + + //callbacks: Vec>, + //ports: Vec, command_channel: mpsc::Sender, } impl WorkerConnector { fn new(command_channel: mpsc::Sender) -> Self { let worker_scope = SharedWorker::worker_self(); + let near_tx = command_channel.clone(); let onconnect_callback: Closure = Closure::new(move |ev: MessageEvent| { let local_tx = near_tx.clone(); spawn_local(async move { - let port: MessagePort = ev.ports().at(0).dyn_into().expect("invalid type"); - local_tx - .send(WorkerMessage::NewConnection(port)) - .await - .expect("send2 error"); + let Ok(port) = ev.ports().at(0).dyn_into() else { + error!("received connection event without MessagePort, should not happen"); + return; + }; + + if let Err(e) = local_tx.send(WorkerMessage::NewConnection(port)).await { + error!("command channel inside worker closed, should not happen: {e}"); + } }) }); + worker_scope.set_onconnect(Some(onconnect_callback.as_ref().unchecked_ref())); Self { _onconnect_callback: onconnect_callback, - callbacks: Vec::new(), - ports: Vec::new(), + clients: Vec::with_capacity(1), // we usually expect to have exactly one client command_channel, } } fn add(&mut self, port: MessagePort) { - debug_assert_eq!(self.callbacks.len(), self.ports.len()); - let client_id = self.callbacks.len(); + let client_id = self.clients.len(); let near_tx = self.command_channel.clone(); let client_message_callback: Closure = @@ -515,10 +541,12 @@ impl WorkerConnector { let (command_with_channel, response_channel) = node_command.add_response_channel(); - local_tx + if let Err(e) = local_tx .send(WorkerMessage::Command(command_with_channel)) .await - .expect("send3 err"); + { + error!("command channel inside worker closed, should not happen: {e}"); + } // TODO: something cleaner? local_tx @@ -532,16 +560,25 @@ impl WorkerConnector { }); port.set_onmessage(Some(client_message_callback.as_ref().unchecked_ref())); - self.callbacks.push(client_message_callback); - self.ports.push(port); + self.clients.push((port, client_message_callback)); - info!("New connection: {client_id}"); + info!("SharedWorker ready to receive commands from client {client_id}"); } - fn respond_to(&self, client: ClientId, msg: NodeResponse) { - let off = client.0; - let v = to_value(&msg).expect("could not to_value"); - self.ports[off].post_message(&v).expect("err posttt"); + fn respond_to(&self, client: ClientId, msg: Option) { + let offset = client.0; + let message = match to_value(&msg) { + Ok(jsvalue) => jsvalue, + Err(e) => { + warn!("provided response could not be coverted to JsValue: {e}, sending undefined instead"); + JsValue::UNDEFINED // we need to send something, client is waiting for a response + } + }; + + // XXX defensive programming with array checking? + if let Err(e) = self.clients[offset].0.post_message(&message) { + error!("could not post response message to client {client:?}: {e:?}"); + } } } @@ -579,8 +616,9 @@ pub async fn run_worker(queued_connections: Vec) { worker.process_command(command_with_channel).await; } WorkerMessage::ResponseChannel((client_id, channel)) => { - // XXX: properly - let response = channel.await.expect("forwardding channel error"); + // oneshot channel has one failure condition - Sender getting dropped + // we also _need_ to send some response, otherwise driver gets stuck waiting + let response = channel.await.ok(); connector.respond_to(client_id, response); } } From 2680863ebf2c0e72b307fc58218fca2ef796aa05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Florkiewicz?= Date: Tue, 16 Apr 2024 08:55:39 +0200 Subject: [PATCH 08/35] cleanup --- cli/static/run_node.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/static/run_node.js b/cli/static/run_node.js index 56d46efb..6e43e7b4 100644 --- a/cli/static/run_node.js +++ b/cli/static/run_node.js @@ -120,7 +120,7 @@ async function main(document, window) { document.getElementById("start").addEventListener("click", async () => { document.querySelectorAll('.config').forEach(elem => elem.disabled = true); - //start_node(window.config); + await window.driver.start(window.config) document.getElementById("peer-id").innerText = await window.driver.local_peer_id(); document.querySelectorAll(".status").forEach(elem => elem.style.visibility = "visible"); From 8b44387e03e0f42ee3b38104fd1d70abe7929a7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Florkiewicz?= Date: Wed, 17 Apr 2024 10:00:41 +0200 Subject: [PATCH 09/35] Organise the code, add error passing instead of unwraps --- Cargo.lock | 1 + cli/static/worker.js | 64 +----- node-wasm/Cargo.toml | 1 + node-wasm/src/node.rs | 329 ++++++++----------------------- node-wasm/src/worker.rs | 250 ++++------------------- node-wasm/src/worker/commands.rs | 168 ++++++++++++++++ 6 files changed, 303 insertions(+), 510 deletions(-) create mode 100644 node-wasm/src/worker/commands.rs diff --git a/Cargo.lock b/Cargo.lock index 60e630ca..649bb0af 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2890,6 +2890,7 @@ dependencies = [ "serde", "serde-wasm-bindgen", "serde_repr", + "thiserror", "time", "tokio", "tracing", diff --git a/cli/static/worker.js b/cli/static/worker.js index c6f2f9a9..ba386fd2 100644 --- a/cli/static/worker.js +++ b/cli/static/worker.js @@ -1,73 +1,17 @@ Error.stackTraceLimit = 99; // rust stack traces can get pretty big, increase the default -// The worker has its own scope and no direct access to functions/objects of the -// global scope. We import the generated JS file to make `wasm_bindgen` -// available which we need to initialize our Wasm code. -//importScripts('/wasm/lumina_node_wasm.js'); import init, { run_worker } from "/wasm/lumina_node_wasm.js"; +// unfortunately, `init()` takes long enough for the first connection (from the tab which +// starts Shared Worker) to be missed. As a workaround, we queue connections in js and then +// pass them to Rust, when it's ready. Rust code replaces `onconnect` handler, so this is +// only necessary on startup. let queued = []; - onconnect = (event) => { console.log("Queued connection", event); queued.push(event.ports[0]); } -console.log('Initializing worker') - await init(); - -console.log('init run') - await run_worker(queued); - -/* -// In the worker, we have a different struct that we want to use as in -// `index.js`. -//const {NumberEval} = wasm_bindgen; - -async function fetch_config() { - const response = await fetch('/cfg.json'); - const json = await response.json(); - - console.log("Received config:", json); - - let config = NodeConfig.default(json.network); - if (json.bootnodes.length !== 0) { - config.bootnodes = json.bootnodes; - } - if (json.genesis) { - config.genesis = json.genesis; - } - - return config; -} - -async function init_wasm_in_worker() { - await init(); - // Load the wasm file by awaiting the Promise returned by `wasm_bindgen`. - //await wasm_bindgen('./pkg/wasm_in_web_worker_bg.wasm'); - - // Create a new object of the `NumberEval` struct. - //var num_eval = NumberEval.new(); - - let config = await fetch_config(); - - let node = await new Node(config); - - // Set callback to handle messages passed to the worker. - self.onmessage = async event => { - console.log("worker", node); - console.log("ev in worker", event); - // By using methods of a struct as reaction to messages passed to the - // worker, we can preserve our state between messages. - //var worker_result = num_eval.is_even(event.data); - - // Send response back to be handled by callback in main thread. - //self.postMessage(worker_result); - }; -}; - -init_wasm_in_worker(); -*/ diff --git a/node-wasm/Cargo.toml b/node-wasm/Cargo.toml index 9e5d32ee..cc3e7884 100644 --- a/node-wasm/Cargo.toml +++ b/node-wasm/Cargo.toml @@ -36,6 +36,7 @@ js-sys = "0.3.64" serde = { version = "1.0.164", features = ["derive"] } serde-wasm-bindgen = "0.6.0" serde_repr = "0.1" +thiserror = "1.0" time = { version = "0.3", features = ["wasm-bindgen"] } tokio = { version = "*", features = ["sync"]} tracing = "0.1.37" diff --git a/node-wasm/src/node.rs b/node-wasm/src/node.rs index eb7aa04a..188688a5 100644 --- a/node-wasm/src/node.rs +++ b/node-wasm/src/node.rs @@ -16,16 +16,18 @@ use lumina_node::node::NodeConfig; use lumina_node::store::IndexedDbStore; use crate::utils::{js_value_from_display, JsContext, Network}; -use crate::worker::SharedWorkerChannel; -use crate::worker::{ +use crate::worker::commands::{ GetConnectedPeers, GetHeader, GetListeners, GetLocalPeerId, GetMultipleHeaders, GetNetworkInfo, GetPeerTrackerInfo, GetSamplingMetadata, GetSyncerInfo, IsRunning, LastSeenNetworkHead, MultipleHeaderQuery, NodeCommand, NodeResponse, RequestHeader, RequestMultipleHeaders, SetPeerTrust, SingleHeaderQuery, StartNode, WaitConnected, }; +use crate::worker::SharedWorkerChannel; use crate::wrapper::libp2p::NetworkInfoSnapshot; use crate::Result; +const LUMINA_SHARED_WORKER_NAME: &str = "lumina"; + /// Config for the lumina wasm node. #[wasm_bindgen(js_name = NodeConfig)] #[derive(Serialize, Deserialize, Debug)] @@ -40,7 +42,6 @@ pub struct WasmNodeConfig { pub bootnodes: Vec, } -// TODO: add on_error handler #[wasm_bindgen] struct NodeDriver { _worker: SharedWorker, @@ -50,11 +51,14 @@ struct NodeDriver { #[wasm_bindgen] impl NodeDriver { + /// Create a new connection to a Lumina node in a Shared Worker. + /// Note that single Shared Worker can be accessed from multiple tabs, so Lumina may already + /// be running be running, before `NodeDriver::start` call. #[wasm_bindgen(constructor)] pub async fn new() -> NodeDriver { let mut opts = WorkerOptions::new(); opts.type_(WorkerType::Module); - opts.name("lumina"); + opts.name(LUMINA_SHARED_WORKER_NAME); let worker = SharedWorker::new_with_worker_options("/js/worker.js", &opts) .expect("could not worker"); @@ -72,104 +76,111 @@ impl NodeDriver { } } - pub async fn is_running(&mut self) -> bool { - let response = self.channel.send(IsRunning); + /// Check whether Lumina is currently running + pub async fn is_running(&mut self) -> Result { + let response = self.channel.send(IsRunning).await?; - response.await.unwrap() + Ok(response.await?) } + /// Start a node with the provided config, if it's not running pub async fn start(&self, config: WasmNodeConfig) -> Result<()> { let command = StartNode(config); - let response = self.channel.send(command); + let response = self.channel.send(command).await?; - Ok(response.await.unwrap()?) + Ok(response.await??) } - pub async fn local_peer_id(&self) -> String { - let response = self.channel.send(GetLocalPeerId); + /// Get node's local peer ID. + pub async fn local_peer_id(&self) -> Result { + let response = self.channel.send(GetLocalPeerId).await?; - response.await.unwrap() + Ok(response.await?) } + /// Get current [`PeerTracker`] info. pub async fn peer_tracker_info(&self) -> Result { - let response = self.channel.send(GetPeerTrackerInfo); + let response = self.channel.send(GetPeerTrackerInfo).await?; - Ok(to_value(&response.await.unwrap())?) + Ok(to_value(&response.await?)?) } - pub async fn wait_connected(&self) { + /// Wait until the node is connected to at least 1 peer. + pub async fn wait_connected(&self) -> Result<()> { let command = WaitConnected { trusted: false }; - let response = self.channel.send(command); + let response = self.channel.send(command).await?; - response.await.unwrap() + Ok(response.await?) } - pub async fn wait_connected_trusted(&self) { + + /// Wait until the node is connected to at least 1 trusted peer. + pub async fn wait_connected_trusted(&self) -> Result<()> { let command = WaitConnected { trusted: false }; - let response = self.channel.send(command); + let response = self.channel.send(command).await?; - response.await.unwrap() + Ok(response.await?) } + /// Get current network info. pub async fn network_info(&self) -> Result { - let response = self.channel.send(GetNetworkInfo); + let response = self.channel.send(GetNetworkInfo).await?; - Ok(response.await.unwrap()) + Ok(response.await?) } + /// Get all the multiaddresses on which the node listens. pub async fn listeners(&self) -> Result { - let response = self.channel.send(GetListeners); - let response = response - .await - .unwrap() - .iter() - .map(js_value_from_display) - .collect(); + let response = self.channel.send(GetListeners).await?; + let response = response.await?.iter().map(js_value_from_display).collect(); Ok(response) } + /// Get all the peers that node is connected to. pub async fn connected_peers(&self) -> Result { - let response = self.channel.send(GetConnectedPeers); - let response = response - .await - .unwrap() - .iter() - .map(js_value_from_display) - .collect(); + let response = self.channel.send(GetConnectedPeers).await?; + let response = response.await?.iter().map(js_value_from_display).collect(); Ok(response) } + /// Trust or untrust the peer with a given ID. pub async fn set_peer_trust(&self, peer_id: &str, is_trusted: bool) -> Result<()> { let command = SetPeerTrust { peer_id: peer_id.to_string(), is_trusted, }; - let response = self.channel.send(command); + let response = self.channel.send(command).await?; - Ok(response.await.unwrap()) + Ok(response.await?) } + /// Request the head header from the network. pub async fn request_head_header(&self) -> Result { let command = RequestHeader(SingleHeaderQuery::Head); - let response = self.channel.send(command); - Ok(to_value(&response.await.unwrap()?)?) + let response = self.channel.send(command).await?; + Ok(to_value(&response.await??)?) } + /// Request a header for the block with a given hash from the network. pub async fn request_header_by_hash(&self, hash: &str) -> Result { let command = RequestHeader(SingleHeaderQuery::ByHash(hash.parse()?)); - let response = self.channel.send(command); + let response = self.channel.send(command).await?; - Ok(to_value(&response.await.unwrap()?)?) + Ok(to_value(&response.await??)?) } + /// Request a header for the block with a given height from the network. pub async fn request_header_by_height(&self, height: u64) -> Result { let command = RequestHeader(SingleHeaderQuery::ByHeight(height)); - let response = self.channel.send(command); + let response = self.channel.send(command).await?; - Ok(to_value(&response.await.unwrap()?)?) + Ok(to_value(&response.await??)?) } + /// Request headers in range (from, from + amount] from the network. + /// + /// The headers will be verified with the `from` header. pub async fn request_verified_headers( &self, from_header: JsValue, @@ -177,7 +188,7 @@ impl NodeDriver { ) -> Result { let from = from_value(from_header)?; let command = RequestMultipleHeaders(MultipleHeaderQuery::GetVerified { from, amount }); - let response = self.channel.send(command); + let response = self.channel.send(command).await?; let result = response .await @@ -188,38 +199,54 @@ impl NodeDriver { Ok(result) } + /// Get current header syncing info. pub async fn syncer_info(&self) -> Result { - let response = self.channel.send(GetSyncerInfo); + let response = self.channel.send(GetSyncerInfo).await?; - Ok(to_value(&response.await.unwrap())?) + Ok(to_value(&response.await?)?) } + /// Get the latest header announced in the network. pub async fn get_network_head_header(&self) -> Result { let command = LastSeenNetworkHead; - let response = self.channel.send(command); + let response = self.channel.send(command).await?; - Ok(to_value(&response.await.unwrap())?) + Ok(to_value(&response.await?)?) } + /// Get the latest locally synced header. pub async fn get_local_head_header(&self) -> Result { let command = GetHeader(SingleHeaderQuery::Head); - let response = self.channel.send(command); + let response = self.channel.send(command).await?; - Ok(to_value(&response.await.unwrap())?) + Ok(to_value(&response.await?)?) } + /// Get a synced header for the block with a given hash. pub async fn get_header_by_hash(&self, hash: &str) -> Result { let command = GetHeader(SingleHeaderQuery::ByHash(hash.parse()?)); - let response = self.channel.send(command); + let response = self.channel.send(command).await?; - Ok(to_value(&response.await.unwrap())?) + Ok(to_value(&response.await?)?) } + + /// Get a synced header for the block with a given height. pub async fn get_header_by_height(&self, height: u64) -> Result { let command = GetHeader(SingleHeaderQuery::ByHeight(height)); - let response = self.channel.send(command); + let response = self.channel.send(command).await?; - Ok(to_value(&response.await.unwrap())?) + Ok(to_value(&response.await?)?) } + + /// Get synced headers from the given heights range. + /// + /// If start of the range is undefined (None), the first returned header will be of height 1. + /// If end of the range is undefined (None), the last returned header will be the last header in the + /// store. + /// + /// # Errors + /// + /// If range contains a height of a header that is not found in the store. pub async fn get_headers( &self, start_height: Option, @@ -229,7 +256,7 @@ impl NodeDriver { start_height, end_height, }); - let response = self.channel.send(command); + let response = self.channel.send(command).await?; let result = response .await .iter() @@ -238,199 +265,15 @@ impl NodeDriver { Ok(result) } - pub async fn get_sampling_metadata(&self, height: u64) -> Result { - let command = GetSamplingMetadata { height }; - let response = self.channel.send(command); - - Ok(to_value(&response.await.unwrap())?) - } -} - -/* -#[wasm_bindgen(js_class = Node)] -impl WasmNode { - /// Create a new Lumina node. - #[wasm_bindgen(constructor)] - pub async fn new(config: WasmNodeConfig) -> Result { - let config = config.into_node_config().await?; - - if let Ok(store_height) = config.store.head_height().await { - info!("Initialised store with head height: {store_height}"); - } else { - info!("Initialized new empty store"); - } - - let node = Node::new(config) - .await - .js_context("Failed to start the node")?; - - Ok(Self(node)) - } - - /// Get node's local peer ID. - pub fn local_peer_id(&self) -> String { - self.0.local_peer_id().to_string() - } - - /// Get current [`PeerTracker`] info. - pub fn peer_tracker_info(&self) -> Result { - let peer_tracker_info = self.0.peer_tracker_info(); - Ok(to_value(&peer_tracker_info)?) - } - - /// Wait until the node is connected to at least 1 peer. - pub async fn wait_connected(&self) -> Result<()> { - Ok(self.0.wait_connected().await?) - } - - /// Wait until the node is connected to at least 1 trusted peer. - pub async fn wait_connected_trusted(&self) -> Result<()> { - Ok(self.0.wait_connected_trusted().await?) - } - - /// Get current network info. - pub async fn network_info(&self) -> Result { - Ok(self.0.network_info().await?.into()) - } - - /// Get all the multiaddresses on which the node listens. - pub async fn listeners(&self) -> Result { - let listeners = self.0.listeners().await?; - - Ok(listeners - .iter() - .map(js_value_from_display) - .collect::()) - } - - /// Get all the peers that node is connected to. - pub async fn connected_peers(&self) -> Result { - Ok(self - .0 - .connected_peers() - .await? - .iter() - .map(js_value_from_display) - .collect::()) - } - - /// Trust or untrust the peer with a given ID. - pub async fn set_peer_trust(&self, peer_id: &str, is_trusted: bool) -> Result<()> { - let peer_id = peer_id.parse().js_context("Parsing peer id failed")?; - Ok(self.0.set_peer_trust(peer_id, is_trusted).await?) - } - - /// Request the head header from the network. - pub async fn request_head_header(&self) -> Result { - let eh = self.0.request_head_header().await?; - Ok(to_value(&eh)?) - } - - /// Request a header for the block with a given hash from the network. - pub async fn request_header_by_hash(&self, hash: &str) -> Result { - let hash: Hash = hash.parse()?; - let eh = self.0.request_header_by_hash(&hash).await?; - Ok(to_value(&eh)?) - } - - /// Request a header for the block with a given height from the network. - pub async fn request_header_by_height(&self, height: u64) -> Result { - let eh = self.0.request_header_by_height(height).await?; - Ok(to_value(&eh)?) - } - - /// Request headers in range (from, from + amount] from the network. - /// - /// The headers will be verified with the `from` header. - pub async fn request_verified_headers(&self, from: JsValue, amount: u64) -> Result { - let header = - from_value::(from).js_context("Parsing extended header failed")?; - let verified_headers = self.0.request_verified_headers(&header, amount).await?; - - Ok(verified_headers - .iter() - .map(to_value) - .collect::>()?) - } - - /// Get current header syncing info. - pub async fn syncer_info(&self) -> Result { - let syncer_info = self.0.syncer_info().await?; - Ok(to_value(&syncer_info)?) - } - - /// Get the latest header announced in the network. - pub fn get_network_head_header(&self) -> Result { - let maybe_head_header = self.0.get_network_head_header(); - Ok(to_value(&maybe_head_header)?) - } - - /// Get the latest locally synced header. - pub async fn get_local_head_header(&self) -> Result { - let local_head = self.0.get_local_head_header().await?; - Ok(to_value(&local_head)?) - } - - /// Get a synced header for the block with a given hash. - pub async fn get_header_by_hash(&self, hash: &str) -> Result { - let hash: Hash = hash.parse().js_context("parsing hash failed")?; - let eh = self.0.get_header_by_hash(&hash).await?; - Ok(to_value(&eh)?) - } - - /// Get a synced header for the block with a given height. - pub async fn get_header_by_height(&self, height: u64) -> Result { - let eh = self.0.get_header_by_height(height).await?; - Ok(to_value(&eh)?) - } - - /// Get synced headers from the given heights range. - /// - /// If start of the range is undefined (None), the first returned header will be of height 1. - /// If end of the range is undefined (None), the last returned header will be the last header in the - /// store. - /// - /// # Errors - /// - /// If range contains a height of a header that is not found in the store. - pub async fn get_headers( - &self, - start_height: Option, - end_height: Option, - ) -> Result { - let headers = match (start_height, end_height) { - (None, None) => self.0.get_headers(..).await, - (Some(start), None) => self.0.get_headers(start..).await, - (None, Some(end)) => self.0.get_headers(..=end).await, - (Some(start), Some(end)) => self.0.get_headers(start..=end).await, - }?; - - Ok(to_value(&headers)?) - } /// Get data sampling metadata of an already sampled height. pub async fn get_sampling_metadata(&self, height: u64) -> Result { - let metadata = self.0.get_sampling_metadata(height).await?; - - #[derive(Serialize)] - struct Intermediate { - accepted: bool, - cids_sampled: Vec, - } - - let metadata = metadata.map(|m| Intermediate { - accepted: m.accepted, - cids_sampled: m - .cids_sampled - .into_iter() - .map(|cid| cid.to_string()) - .collect(), - }); + let command = GetSamplingMetadata { height }; + let response = self.channel.send(command).await?; - Ok(to_value(&metadata)?) + Ok(to_value(&response.await?)?) } } -*/ #[wasm_bindgen(js_class = NodeConfig)] impl WasmNodeConfig { diff --git a/node-wasm/src/worker.rs b/node-wasm/src/worker.rs index 642c0ee2..bfc0d85b 100644 --- a/node-wasm/src/worker.rs +++ b/node-wasm/src/worker.rs @@ -1,53 +1,49 @@ -use std::error::Error; -use std::fmt::{self, Debug, Display}; +use std::fmt::Debug; use std::marker::PhantomData; -use futures::future::{BoxFuture, Future, FutureExt, TryFuture, TryFutureExt}; -use libp2p::Multiaddr; +use futures::future::{BoxFuture, Future, TryFutureExt}; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; use serde_wasm_bindgen::{from_value, to_value}; +use thiserror::Error; use tokio::sync::{mpsc, oneshot}; use tracing::{error, info, trace, warn}; use wasm_bindgen::prelude::*; use wasm_bindgen_futures::spawn_local; use web_sys::{MessageEvent, MessagePort, SharedWorker}; -use celestia_types::hash::Hash; -use celestia_types::ExtendedHeader; use lumina_node::node::{Node, NodeError}; -use lumina_node::peer_tracker::PeerTrackerInfo; -use lumina_node::store::{IndexedDbStore, SamplingMetadata, Store}; -use lumina_node::syncer::SyncingInfo; +use lumina_node::store::{IndexedDbStore, Store}; use crate::node::WasmNodeConfig; use crate::utils::WorkerSelf; -use crate::wrapper::libp2p::NetworkInfoSnapshot; +use crate::worker::commands::*; -#[derive(Debug, Serialize, Deserialize)] -pub enum WorkerError2 { +pub(crate) mod commands; + +#[derive(Debug, Serialize, Deserialize, Error)] +pub enum WorkerError { + #[error("Command response channel has been dropped before response could be sent, should not happen")] SharedWorkerChannelResponseChannelDropped, + #[error("Command channel to lumina worker closed, should not happen")] + SharedWorkerCommandChannelClosed, + #[error("Channel expected different response type, should not happen")] SharedWorkerChannelInvalidType, -} - -type Result = std::result::Result; - -#[derive(Debug, Serialize, Deserialize)] -pub struct WorkerError(String); - -impl Error for WorkerError {} -impl Display for WorkerError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "WorkerError({})", self.0) - } + #[error("Lumina is already running, ignoring StartNode command")] + SharedWorkerAlreadyRunning, + #[error("node error: {0}")] + NodeError(String), + #[error("Worker is still handling previous command")] + WorkerBusy, } impl From for WorkerError { - fn from(error: NodeError) -> WorkerError { - WorkerError(error.to_string()) + fn from(error: NodeError) -> Self { + WorkerError::NodeError(error.to_string()) } } +type Result = std::result::Result; pub type CommandResponseChannel = oneshot::Sender<::Output>; #[derive(Serialize, Deserialize, Debug)] @@ -62,7 +58,6 @@ pub trait NodeCommandType: Debug + Into { pub struct SharedWorkerChannel { _onmessage: Closure, - _forwarding_task: (), channel: MessagePort, response_channels: mpsc::Sender>, send_type: PhantomData, @@ -71,8 +66,7 @@ pub struct SharedWorkerChannel { impl SharedWorkerChannel where IN: Serialize, - // XXX: send sync shouldn't be needed - OUT: DeserializeOwned + 'static + Send + Sync, + OUT: DeserializeOwned + 'static, { pub fn new(channel: MessagePort) -> Self { let (message_tx, mut message_rx) = mpsc::channel(64); @@ -89,8 +83,14 @@ where }) }; + // TODO: one of the ways to make sure we're synchronised with lumina in worker is forcing + // request-response interface over the channel (waiting for previous command to be handled + // before sending next one). Is it better for lumina to maintain this inside the worker? let (response_tx, mut response_rx) = mpsc::channel(1); - let forwarding_task = spawn_local(async move { + + // small task running concurently, which converts js's callback style into + // rust's awaiting on a channel message passing style + spawn_local(async move { loop { let message = message_rx.recv().await.unwrap(); let response_channel: oneshot::Sender = response_rx.recv().await.unwrap(); @@ -108,47 +108,36 @@ where Self { response_channels: response_tx, _onmessage: onmessage, - _forwarding_task: forwarding_task, channel, send_type: PhantomData, } } - pub fn send( + pub async fn send( &self, command: T, - ) -> impl Future> - //BoxFuture<'static, Result> + ) -> Result>> where T::Output: Debug + Serialize, NodeCommandResponse: TryFrom, as TryFrom>::Error: Debug, { - let message: NodeCommand = command.into(); - self.send_enum(message); + self.send_enum(command.into()); let (tx, rx) = oneshot::channel(); - self.response_channels.try_send(tx).expect("busy"); + self.response_channels + .send(tx) + .await + .map_err(|_| WorkerError::SharedWorkerCommandChannelClosed)?; - rx.map_ok_or_else( - |_e| Err(WorkerError2::SharedWorkerChannelResponseChannelDropped), + Ok(rx.map_ok_or_else( + |_e| Err(WorkerError::SharedWorkerChannelResponseChannelDropped), |r| match NodeCommandResponse::::try_from(r) { Ok(v) => Ok(v.0), - Err(_) => Err(WorkerError2::SharedWorkerChannelInvalidType), + Err(_) => Err(WorkerError::SharedWorkerChannelInvalidType), }, - ) - - /* - // unwrap "handles" invalid type - rx.map_ok(|r| { - //tracing::info!("type = , expected = {}", std::any::type_name::()); - NodeCommandResponse::::try_from(r) - .expect("invalid type") - .0 - }) - .boxed() - */ + )) } fn send_enum(&self, msg: NodeCommand) { @@ -160,157 +149,6 @@ where // TODO: cleanup JS objects on drop // impl Drop -macro_rules! define_command_from_impl { - ($common_name:ident, $command_name:ident) => { - impl From<$command_name> for $common_name { - fn from(command: $command_name) -> Self { - $common_name::$command_name(command) - } - } - }; -} - -macro_rules! define_command_type_impl { - ($common_type:ident, $command_name:ident, $output:ty) => { - impl $common_type for $command_name { - type Output = $output; - } - }; -} - -macro_rules! define_response_try_from_impl { - ($common_type:ident, $helper_type:ident, $command_name:ident) => { - impl TryFrom<$common_type> for $helper_type<$command_name> { - type Error = (); - fn try_from(response: $common_type) -> Result { - if let $common_type::$command_name(cmd) = response { - Ok(cmd) - } else { - Err(()) - } - } - } - }; -} - -macro_rules! define_command { - ($command_name:ident -> $output:ty) => { - #[derive(Debug, Serialize, Deserialize)] - pub struct $command_name; - define_command_type_impl!(NodeCommandType, $command_name, $output); - define_command_from_impl!(NodeCommand, $command_name); - define_response_try_from_impl!(NodeResponse, NodeCommandResponse, $command_name); - }; - ($command_name:ident ($($param:ty),+) -> $output:ty) => { - #[derive(Debug, Serialize, Deserialize)] - pub struct $command_name($(pub $param,)+); - define_command_type_impl!(NodeCommandType, $command_name, $output); - define_command_from_impl!(NodeCommand, $command_name); - define_response_try_from_impl!(NodeResponse, NodeCommandResponse, $command_name); - }; - ($command_name:ident {$($param_name:ident : $param_type:ty),+} -> $output:ty) => { - #[derive(Debug, Serialize, Deserialize)] - pub struct $command_name { $(pub $param_name: $param_type,)+} - define_command_type_impl!(NodeCommandType, $command_name, $output); - define_command_from_impl!(NodeCommand, $command_name); - define_response_try_from_impl!(NodeResponse, NodeCommandResponse, $command_name); - }; -} - -macro_rules! define_common_types { - ($($command_name:ident),+ $(,)?) => { - #[derive(Serialize, Deserialize, Debug)] - pub enum NodeCommand { - $($command_name($command_name),)+ - } - - #[derive(Debug)] - pub enum NodeCommandWithChannel { - $($command_name(($command_name, CommandResponseChannel<$command_name>)),)+ - } - - #[derive(Serialize, Deserialize, Debug)] - pub enum NodeResponse { - $($command_name(NodeCommandResponse<$command_name>),)+ - } - - impl NodeCommand { - fn add_response_channel(self) -> (NodeCommandWithChannel, - BoxFuture<'static, Result>, // XXX - ) { - match self { - $( - NodeCommand::$command_name(cmd) => { - let (tx, rx) = oneshot::channel(); - ( - NodeCommandWithChannel::$command_name((cmd, tx)), - rx.map_ok(|r| NodeResponse::$command_name( - NodeCommandResponse::<$command_name>(r) - )).boxed() - ) - } - )+ - } - } - } - }; -} - -define_common_types!( - IsRunning, - StartNode, - GetLocalPeerId, - GetSyncerInfo, - GetPeerTrackerInfo, - GetNetworkInfo, - GetConnectedPeers, - SetPeerTrust, - WaitConnected, - GetListeners, - RequestHeader, - RequestMultipleHeaders, - GetHeader, - GetMultipleHeaders, - LastSeenNetworkHead, - GetSamplingMetadata, -); - -define_command!(IsRunning -> bool); -define_command!(StartNode(WasmNodeConfig) -> Result<()>); -define_command!(GetLocalPeerId -> String); -define_command!(GetSyncerInfo -> SyncingInfo); -define_command!(GetPeerTrackerInfo -> PeerTrackerInfo); -define_command!(GetNetworkInfo -> NetworkInfoSnapshot); -define_command!(GetConnectedPeers -> Vec); -define_command!(SetPeerTrust { peer_id: String, is_trusted: bool } -> ()); -define_command!(WaitConnected { trusted: bool } -> ()); -define_command!(GetListeners -> Vec); -define_command!(RequestHeader(SingleHeaderQuery) -> Result); -define_command!(RequestMultipleHeaders(MultipleHeaderQuery) -> Vec); -define_command!(GetHeader(SingleHeaderQuery) -> ExtendedHeader); -define_command!(GetMultipleHeaders(MultipleHeaderQuery) -> Vec); -define_command!(LastSeenNetworkHead -> Option); -define_command!(GetSamplingMetadata { height: u64 } -> SamplingMetadata); - -#[derive(Serialize, Deserialize, Debug)] -pub enum SingleHeaderQuery { - Head, - ByHash(Hash), - ByHeight(u64), -} - -#[derive(Serialize, Deserialize, Debug)] -pub enum MultipleHeaderQuery { - GetVerified { - from: ExtendedHeader, - amount: u64, - }, - Range { - start_height: Option, - end_height: Option, - }, -} - struct NodeWorker { node: Node, } @@ -332,13 +170,12 @@ impl NodeWorker { async fn process_command(&mut self, command: NodeCommandWithChannel) { match command { - // TODO: order NodeCommandWithChannel::IsRunning((_, response)) => { response.send(true).expect("channel_dropped") } NodeCommandWithChannel::StartNode((_, response)) => { response - .send(Err(WorkerError("already running".to_string()))) + .send(Err(WorkerError::SharedWorkerAlreadyRunning)) .expect("channel_dropped"); } NodeCommandWithChannel::GetLocalPeerId((_, response)) => { @@ -491,8 +328,7 @@ struct WorkerConnector { // for forwarding messages back clients: Vec<(MessagePort, Closure)>, - //callbacks: Vec>, - //ports: Vec, + // sends events back to the main loop for processing command_channel: mpsc::Sender, } diff --git a/node-wasm/src/worker/commands.rs b/node-wasm/src/worker/commands.rs new file mode 100644 index 00000000..7bf20950 --- /dev/null +++ b/node-wasm/src/worker/commands.rs @@ -0,0 +1,168 @@ +use std::fmt::Debug; + +use futures::future::{BoxFuture, FutureExt, TryFutureExt}; +use libp2p::Multiaddr; +use serde::{Deserialize, Serialize}; +use tokio::sync::oneshot; + +use celestia_types::hash::Hash; +use celestia_types::ExtendedHeader; +use lumina_node::peer_tracker::PeerTrackerInfo; +use lumina_node::store::SamplingMetadata; +use lumina_node::syncer::SyncingInfo; + +use crate::node::WasmNodeConfig; +use crate::worker::Result; +use crate::worker::{CommandResponseChannel, NodeCommandResponse, NodeCommandType}; +use crate::wrapper::libp2p::NetworkInfoSnapshot; + +macro_rules! define_command_from_impl { + ($common_name:ident, $command_name:ident) => { + impl From<$command_name> for $common_name { + fn from(command: $command_name) -> Self { + $common_name::$command_name(command) + } + } + }; +} + +macro_rules! define_command_type_impl { + ($common_type:ident, $command_name:ident, $output:ty) => { + impl $common_type for $command_name { + type Output = $output; + } + }; +} + +macro_rules! define_response_try_from_impl { + ($common_type:ident, $helper_type:ident, $command_name:ident) => { + impl TryFrom<$common_type> for $helper_type<$command_name> { + type Error = (); + fn try_from(response: $common_type) -> Result { + if let $common_type::$command_name(cmd) = response { + Ok(cmd) + } else { + Err(()) + } + } + } + }; +} + +macro_rules! define_command { + ($command_name:ident -> $output:ty) => { + #[derive(Debug, Serialize, Deserialize)] + pub struct $command_name; + define_command_type_impl!(NodeCommandType, $command_name, $output); + define_command_from_impl!(NodeCommand, $command_name); + define_response_try_from_impl!(NodeResponse, NodeCommandResponse, $command_name); + }; + ($command_name:ident ($($param:ty),+) -> $output:ty) => { + #[derive(Debug, Serialize, Deserialize)] + pub struct $command_name($(pub $param,)+); + define_command_type_impl!(NodeCommandType, $command_name, $output); + define_command_from_impl!(NodeCommand, $command_name); + define_response_try_from_impl!(NodeResponse, NodeCommandResponse, $command_name); + }; + ($command_name:ident {$($param_name:ident : $param_type:ty),+} -> $output:ty) => { + #[derive(Debug, Serialize, Deserialize)] + pub struct $command_name { $(pub $param_name: $param_type,)+} + define_command_type_impl!(NodeCommandType, $command_name, $output); + define_command_from_impl!(NodeCommand, $command_name); + define_response_try_from_impl!(NodeResponse, NodeCommandResponse, $command_name); + }; +} + +macro_rules! define_common_types { + ($($command_name:ident),+ $(,)?) => { + #[derive(Serialize, Deserialize, Debug)] + pub(crate) enum NodeCommand { + $($command_name($command_name),)+ + } + + #[derive(Debug)] + pub(super) enum NodeCommandWithChannel { + $($command_name(($command_name, CommandResponseChannel<$command_name>)),)+ + } + + #[derive(Serialize, Deserialize, Debug)] + pub(crate) enum NodeResponse { + $($command_name(NodeCommandResponse<$command_name>),)+ + } + + impl NodeCommand { + pub(super) fn add_response_channel(self) -> (NodeCommandWithChannel, + BoxFuture<'static, Result>, // XXX + ) { + match self { + $( + NodeCommand::$command_name(cmd) => { + let (tx, rx) = oneshot::channel(); + ( + NodeCommandWithChannel::$command_name((cmd, tx)), + rx.map_ok(|r| NodeResponse::$command_name( + NodeCommandResponse::<$command_name>(r) + )).boxed() + ) + } + )+ + } + } + } + }; +} + +define_common_types!( + IsRunning, + StartNode, + GetLocalPeerId, + GetSyncerInfo, + GetPeerTrackerInfo, + GetNetworkInfo, + GetConnectedPeers, + SetPeerTrust, + WaitConnected, + GetListeners, + RequestHeader, + RequestMultipleHeaders, + GetHeader, + GetMultipleHeaders, + LastSeenNetworkHead, + GetSamplingMetadata, +); + +define_command!(IsRunning -> bool); +define_command!(StartNode(WasmNodeConfig) -> Result<()>); +define_command!(GetLocalPeerId -> String); +define_command!(GetSyncerInfo -> SyncingInfo); +define_command!(GetPeerTrackerInfo -> PeerTrackerInfo); +define_command!(GetNetworkInfo -> NetworkInfoSnapshot); +define_command!(GetConnectedPeers -> Vec); +define_command!(SetPeerTrust { peer_id: String, is_trusted: bool } -> ()); +define_command!(WaitConnected { trusted: bool } -> ()); +define_command!(GetListeners -> Vec); +define_command!(RequestHeader(SingleHeaderQuery) -> Result); +define_command!(RequestMultipleHeaders(MultipleHeaderQuery) -> Vec); +define_command!(GetHeader(SingleHeaderQuery) -> ExtendedHeader); +define_command!(GetMultipleHeaders(MultipleHeaderQuery) -> Vec); +define_command!(LastSeenNetworkHead -> Option); +define_command!(GetSamplingMetadata { height: u64 } -> SamplingMetadata); + +#[derive(Serialize, Deserialize, Debug)] +pub(crate) enum SingleHeaderQuery { + Head, + ByHash(Hash), + ByHeight(u64), +} + +#[derive(Serialize, Deserialize, Debug)] +pub(crate) enum MultipleHeaderQuery { + GetVerified { + from: ExtendedHeader, + amount: u64, + }, + Range { + start_height: Option, + end_height: Option, + }, +} From 700ddec7be5dddcd21a6010df11df94d4723d44c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Florkiewicz?= Date: Wed, 17 Apr 2024 10:23:59 +0200 Subject: [PATCH 10/35] appease clippy --- node-wasm/src/node.rs | 14 ++++---- node-wasm/src/worker.rs | 60 +++++++++++++------------------- node-wasm/src/worker/commands.rs | 21 +++-------- 3 files changed, 37 insertions(+), 58 deletions(-) diff --git a/node-wasm/src/node.rs b/node-wasm/src/node.rs index 188688a5..442a3173 100644 --- a/node-wasm/src/node.rs +++ b/node-wasm/src/node.rs @@ -17,10 +17,10 @@ use lumina_node::store::IndexedDbStore; use crate::utils::{js_value_from_display, JsContext, Network}; use crate::worker::commands::{ - GetConnectedPeers, GetHeader, GetListeners, GetLocalPeerId, GetMultipleHeaders, GetNetworkInfo, - GetPeerTrackerInfo, GetSamplingMetadata, GetSyncerInfo, IsRunning, LastSeenNetworkHead, - MultipleHeaderQuery, NodeCommand, NodeResponse, RequestHeader, RequestMultipleHeaders, - SetPeerTrust, SingleHeaderQuery, StartNode, WaitConnected, + GetConnectedPeers, GetHeader, GetHeadersRange, GetListeners, GetLocalPeerId, GetNetworkInfo, + GetPeerTrackerInfo, GetSamplingMetadata, GetSyncerInfo, GetVerifiedHeaders, IsRunning, + LastSeenNetworkHead, NodeCommand, NodeResponse, RequestHeader, SetPeerTrust, SingleHeaderQuery, + StartNode, WaitConnected, }; use crate::worker::SharedWorkerChannel; use crate::wrapper::libp2p::NetworkInfoSnapshot; @@ -187,7 +187,7 @@ impl NodeDriver { amount: u64, ) -> Result { let from = from_value(from_header)?; - let command = RequestMultipleHeaders(MultipleHeaderQuery::GetVerified { from, amount }); + let command = GetVerifiedHeaders { from, amount }; let response = self.channel.send(command).await?; let result = response @@ -252,10 +252,10 @@ impl NodeDriver { start_height: Option, end_height: Option, ) -> Result { - let command = GetMultipleHeaders(MultipleHeaderQuery::Range { + let command = GetHeadersRange { start_height, end_height, - }); + }; let response = self.channel.send(command).await?; let result = response .await diff --git a/node-wasm/src/worker.rs b/node-wasm/src/worker.rs index bfc0d85b..40260597 100644 --- a/node-wasm/src/worker.rs +++ b/node-wasm/src/worker.rs @@ -45,6 +45,7 @@ impl From for WorkerError { type Result = std::result::Result; pub type CommandResponseChannel = oneshot::Sender<::Output>; +type WorkerClientConnection = (MessagePort, Closure); #[derive(Serialize, Deserialize, Debug)] pub struct NodeCommandResponse(pub T::Output) @@ -208,8 +209,7 @@ impl NodeWorker { }, response, )) => { - let _ = self - .node + self.node .set_peer_trust(peer_id.parse().unwrap(), is_trusted) .await .unwrap(); @@ -241,18 +241,6 @@ impl NodeWorker { .send(header.map_err(|e| e.into())) .expect("channel_dropped"); } - NodeCommandWithChannel::RequestMultipleHeaders((command, response)) => { - let headers = match command.0 { - MultipleHeaderQuery::GetVerified { from, amount } => self - .node - .request_verified_headers(&from, amount) - .await - .unwrap(), - MultipleHeaderQuery::Range { .. } => unreachable!("invalid command"), - }; - - response.send(headers).expect("channel_dropped"); - } NodeCommandWithChannel::GetHeader((command, response)) => { let header = match command.0 { SingleHeaderQuery::Head => self.node.get_local_head_header().await.unwrap(), @@ -265,26 +253,28 @@ impl NodeWorker { }; response.send(header).expect("channel_dropped"); } - NodeCommandWithChannel::GetMultipleHeaders((command, response)) => { - let headers = match command.0 { - MultipleHeaderQuery::GetVerified { from, amount } => self - .node - .request_verified_headers(&from, amount) - .await - .unwrap(), - MultipleHeaderQuery::Range { - start_height, - end_height, - } => match (start_height, end_height) { - (None, None) => self.node.get_headers(..).await, - (Some(start), None) => self.node.get_headers(start..).await, - (None, Some(end)) => self.node.get_headers(..=end).await, - (Some(start), Some(end)) => self.node.get_headers(start..=end).await, - } - .ok() - .unwrap(), - }; - + NodeCommandWithChannel::GetVerifiedHeaders((command, response)) => { + let GetVerifiedHeaders { from, amount } = command; + let headers = self + .node + .request_verified_headers(&from, amount) + .await + .unwrap(); + response.send(headers).expect("channel_dropped"); + } + NodeCommandWithChannel::GetHeadersRange((command, response)) => { + let GetHeadersRange { + start_height, + end_height, + } = command; + let headers = match (start_height, end_height) { + (None, None) => self.node.get_headers(..).await, + (Some(start), None) => self.node.get_headers(start..).await, + (None, Some(end)) => self.node.get_headers(..=end).await, + (Some(start), Some(end)) => self.node.get_headers(start..=end).await, + } + .ok() + .unwrap(); response.send(headers).expect("channel_dropped"); } NodeCommandWithChannel::LastSeenNetworkHead((_, response)) => { @@ -326,7 +316,7 @@ struct WorkerConnector { // keep a MessagePort for each client to send messages over, as well as callback responsible // for forwarding messages back - clients: Vec<(MessagePort, Closure)>, + clients: Vec, // sends events back to the main loop for processing command_channel: mpsc::Sender, diff --git a/node-wasm/src/worker/commands.rs b/node-wasm/src/worker/commands.rs index 7bf20950..04db56ee 100644 --- a/node-wasm/src/worker/commands.rs +++ b/node-wasm/src/worker/commands.rs @@ -75,6 +75,7 @@ macro_rules! define_command { macro_rules! define_common_types { ($($command_name:ident),+ $(,)?) => { + #[allow(clippy::large_enum_variant)] #[derive(Serialize, Deserialize, Debug)] pub(crate) enum NodeCommand { $($command_name($command_name),)+ @@ -124,9 +125,9 @@ define_common_types!( WaitConnected, GetListeners, RequestHeader, - RequestMultipleHeaders, GetHeader, - GetMultipleHeaders, + GetHeadersRange, + GetVerifiedHeaders, LastSeenNetworkHead, GetSamplingMetadata, ); @@ -142,9 +143,9 @@ define_command!(SetPeerTrust { peer_id: String, is_trusted: bool } -> ()); define_command!(WaitConnected { trusted: bool } -> ()); define_command!(GetListeners -> Vec); define_command!(RequestHeader(SingleHeaderQuery) -> Result); -define_command!(RequestMultipleHeaders(MultipleHeaderQuery) -> Vec); +define_command!(GetVerifiedHeaders { from: ExtendedHeader, amount: u64 } -> Vec); +define_command!(GetHeadersRange { start_height: Option, end_height: Option } -> Vec); define_command!(GetHeader(SingleHeaderQuery) -> ExtendedHeader); -define_command!(GetMultipleHeaders(MultipleHeaderQuery) -> Vec); define_command!(LastSeenNetworkHead -> Option); define_command!(GetSamplingMetadata { height: u64 } -> SamplingMetadata); @@ -154,15 +155,3 @@ pub(crate) enum SingleHeaderQuery { ByHash(Hash), ByHeight(u64), } - -#[derive(Serialize, Deserialize, Debug)] -pub(crate) enum MultipleHeaderQuery { - GetVerified { - from: ExtendedHeader, - amount: u64, - }, - Range { - start_height: Option, - end_height: Option, - }, -} From eefd8318d20886f5001eed940fbbfd827ddec381 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Florkiewicz?= Date: Thu, 18 Apr 2024 20:18:12 +0200 Subject: [PATCH 11/35] Roll back to runtime type checking --- Cargo.lock | 5 + cli/static/run_node.js | 5 +- node-wasm/Cargo.toml | 3 +- node-wasm/src/node.rs | 256 ++++++++++++------ node-wasm/src/worker.rs | 441 +++++++++++++++++-------------- node-wasm/src/worker/commands.rs | 101 ++++++- 6 files changed, 510 insertions(+), 301 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 649bb0af..fe485ce6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2369,6 +2369,7 @@ dependencies = [ "quick-protobuf", "rand", "rw-stream-sink", + "serde", "smallvec", "thiserror", "tracing", @@ -2417,6 +2418,7 @@ dependencies = [ "quick-protobuf-codec 0.3.1", "rand", "regex", + "serde", "sha2 0.10.8", "smallvec", "tracing", @@ -2458,6 +2460,7 @@ dependencies = [ "multihash", "quick-protobuf", "rand", + "serde", "sha2 0.10.8", "thiserror", "tracing", @@ -2485,6 +2488,7 @@ dependencies = [ "quick-protobuf", "quick-protobuf-codec 0.3.1", "rand", + "serde", "sha2 0.10.8", "smallvec", "thiserror", @@ -2881,6 +2885,7 @@ dependencies = [ "anyhow", "celestia-types", "console_error_panic_hook", + "enum-as-inner", "futures", "gloo-timers 0.3.0", "instant", diff --git a/cli/static/run_node.js b/cli/static/run_node.js index 6e43e7b4..7a18574c 100644 --- a/cli/static/run_node.js +++ b/cli/static/run_node.js @@ -8,6 +8,8 @@ async function fetch_config() { console.log("Received config:", json); + window.nconfig = NodeConfig; + let config = NodeConfig.default(json.network); if (json.bootnodes.length !== 0) { config.bootnodes = json.bootnodes; @@ -41,7 +43,7 @@ async function show_stats(node) { return } - //console.log(network_head); + console.log(network_head); const square_rows = network_head.dah.row_roots.length; const square_cols = network_head.dah.column_roots.length; @@ -111,6 +113,7 @@ async function main(document, window) { window.driver = await new NodeDriver(); bind_config(await fetch_config()); + //return; if (await window.driver.is_running() === true) { document.querySelectorAll('.config').forEach(elem => elem.disabled = true); diff --git a/node-wasm/Cargo.toml b/node-wasm/Cargo.toml index cc3e7884..d2162fc3 100644 --- a/node-wasm/Cargo.toml +++ b/node-wasm/Cargo.toml @@ -24,7 +24,7 @@ crate-type = ["cdylib", "rlib"] [target.'cfg(target_arch = "wasm32")'.dependencies] celestia-types = { workspace = true } -libp2p = { workspace = true } +libp2p = { workspace = true, features = ["serde"] } lumina-node = { workspace = true } anyhow = "1.0.71" @@ -45,3 +45,4 @@ tracing-web = "0.1.2" wasm-bindgen = "0.2.88" wasm-bindgen-futures = "0.4.37" web-sys = { version = "0.3.69", features = ["BroadcastChannel", "MessageEvent", "Worker", "WorkerOptions", "WorkerType", "SharedWorker", "MessagePort", "SharedWorkerGlobalScope"]} +enum-as-inner = "0.6" diff --git a/node-wasm/src/node.rs b/node-wasm/src/node.rs index 442a3173..601d8370 100644 --- a/node-wasm/src/node.rs +++ b/node-wasm/src/node.rs @@ -16,13 +16,9 @@ use lumina_node::node::NodeConfig; use lumina_node::store::IndexedDbStore; use crate::utils::{js_value_from_display, JsContext, Network}; -use crate::worker::commands::{ - GetConnectedPeers, GetHeader, GetHeadersRange, GetListeners, GetLocalPeerId, GetNetworkInfo, - GetPeerTrackerInfo, GetSamplingMetadata, GetSyncerInfo, GetVerifiedHeaders, IsRunning, - LastSeenNetworkHead, NodeCommand, NodeResponse, RequestHeader, SetPeerTrust, SingleHeaderQuery, - StartNode, WaitConnected, -}; +use crate::worker::commands::{CheckableResponse, NodeCommand, SingleHeaderQuery}; use crate::worker::SharedWorkerChannel; +use crate::worker::WorkerError; use crate::wrapper::libp2p::NetworkInfoSnapshot; use crate::Result; @@ -46,7 +42,7 @@ pub struct WasmNodeConfig { struct NodeDriver { _worker: SharedWorker, _onerror_callback: Closure, - channel: SharedWorkerChannel, + channel: SharedWorkerChannel, } #[wasm_bindgen] @@ -78,104 +74,172 @@ impl NodeDriver { /// Check whether Lumina is currently running pub async fn is_running(&mut self) -> Result { - let response = self.channel.send(IsRunning).await?; - - Ok(response.await?) + let command = NodeCommand::IsRunning; + self.channel.send(command).await?; + let running = self + .channel + .recv() + .await? + .into_is_running() + .check_variant()?; + + Ok(running) } /// Start a node with the provided config, if it's not running pub async fn start(&self, config: WasmNodeConfig) -> Result<()> { - let command = StartNode(config); - let response = self.channel.send(command).await?; - - Ok(response.await??) + let command = NodeCommand::StartNode(config); + self.channel.send(command).await?; + let started = self + .channel + .recv() + .await? + .into_node_started() + .check_variant()?; + + Ok(started?) } /// Get node's local peer ID. pub async fn local_peer_id(&self) -> Result { - let response = self.channel.send(GetLocalPeerId).await?; - - Ok(response.await?) + let command = NodeCommand::GetLocalPeerId; + self.channel.send(command).await?; + let peer_id = self + .channel + .recv() + .await? + .into_local_peer_id() + .check_variant()?; + + Ok(peer_id) } /// Get current [`PeerTracker`] info. pub async fn peer_tracker_info(&self) -> Result { - let response = self.channel.send(GetPeerTrackerInfo).await?; - - Ok(to_value(&response.await?)?) + let command = NodeCommand::GetPeerTrackerInfo; + self.channel.send(command).await?; + let peer_info = self + .channel + .recv() + .await? + .into_peer_tracker_info() + .check_variant()?; + + Ok(to_value(&peer_info)?) } /// Wait until the node is connected to at least 1 peer. pub async fn wait_connected(&self) -> Result<()> { - let command = WaitConnected { trusted: false }; - let response = self.channel.send(command).await?; - - Ok(response.await?) + let command = NodeCommand::WaitConnected { trusted: false }; + self.channel.send(command).await?; + // slightly unfortunate `is_connected` name is generated by the enum-as-inner + // crate and checks whether enum variant is WorkerResponse::Connected + if !self.channel.recv().await?.is_connected() { + Err(WorkerError::InvalidResponseType.into()) + } else { + Ok(()) + } } /// Wait until the node is connected to at least 1 trusted peer. pub async fn wait_connected_trusted(&self) -> Result<()> { - let command = WaitConnected { trusted: false }; - let response = self.channel.send(command).await?; - - Ok(response.await?) + let command = NodeCommand::WaitConnected { trusted: false }; + self.channel.send(command).await?; + // slightly unfortunate `is_connected` name is generated by the enum-as-inner + // crate and checks whether enum variant is WorkerResponse::Connected + if !self.channel.recv().await?.is_connected() { + Err(WorkerError::InvalidResponseType.into()) + } else { + Ok(()) + } } /// Get current network info. pub async fn network_info(&self) -> Result { - let response = self.channel.send(GetNetworkInfo).await?; - - Ok(response.await?) + let command = NodeCommand::GetNetworkInfo; + self.channel.send(command).await?; + let network_info = self + .channel + .recv() + .await? + .into_network_info() + .check_variant()?; + + Ok(todo!()) } /// Get all the multiaddresses on which the node listens. pub async fn listeners(&self) -> Result { - let response = self.channel.send(GetListeners).await?; - let response = response.await?.iter().map(js_value_from_display).collect(); - - Ok(response) + let command = NodeCommand::GetListeners; + self.channel.send(command).await?; + let listeners = self + .channel + .recv() + .await? + .into_listeners() + .check_variant()?; + //let response = response.await?.iter().map(js_value_from_display).collect(); + + Ok(todo!()) } /// Get all the peers that node is connected to. pub async fn connected_peers(&self) -> Result { - let response = self.channel.send(GetConnectedPeers).await?; - let response = response.await?.iter().map(js_value_from_display).collect(); - - Ok(response) + let command = NodeCommand::GetConnectedPeers; + self.channel.send(command).await?; + let peers = self + .channel + .recv() + .await? + .into_connected_peers() + .check_variant()?; + //response.await?.iter().map(js_value_from_display).collect(); + + Ok(todo!()) } /// Trust or untrust the peer with a given ID. pub async fn set_peer_trust(&self, peer_id: &str, is_trusted: bool) -> Result<()> { - let command = SetPeerTrust { - peer_id: peer_id.to_string(), + let command = NodeCommand::SetPeerTrust { + peer_id: peer_id.parse()?, is_trusted, }; - let response = self.channel.send(command).await?; - - Ok(response.await?) + self.channel.send(command).await?; + let set_result = self + .channel + .recv() + .await? + .into_set_peer_trust() + .check_variant()?; + + Ok(set_result?) } /// Request the head header from the network. pub async fn request_head_header(&self) -> Result { - let command = RequestHeader(SingleHeaderQuery::Head); - let response = self.channel.send(command).await?; - Ok(to_value(&response.await??)?) + let command = NodeCommand::RequestHeader(SingleHeaderQuery::Head); + self.channel.send(command).await?; + let header = self.channel.recv().await?.into_header().check_variant()?; + + Ok(to_value(&header?)?) } /// Request a header for the block with a given hash from the network. pub async fn request_header_by_hash(&self, hash: &str) -> Result { - let command = RequestHeader(SingleHeaderQuery::ByHash(hash.parse()?)); - let response = self.channel.send(command).await?; + let command = NodeCommand::RequestHeader(SingleHeaderQuery::ByHash(hash.parse()?)); + self.channel.send(command).await?; + let header = self.channel.recv().await?.into_header().check_variant()?; - Ok(to_value(&response.await??)?) + Ok(to_value(&header?)?) } /// Request a header for the block with a given height from the network. pub async fn request_header_by_height(&self, height: u64) -> Result { - let command = RequestHeader(SingleHeaderQuery::ByHeight(height)); - let response = self.channel.send(command).await?; + let command = NodeCommand::RequestHeader(SingleHeaderQuery::ByHeight(height)); + self.channel.send(command).await?; + let header = self.channel.recv().await?.into_header().check_variant()?; - Ok(to_value(&response.await??)?) + Ok(to_value(&header?)?) } /// Request headers in range (from, from + amount] from the network. @@ -186,12 +250,14 @@ impl NodeDriver { from_header: JsValue, amount: u64, ) -> Result { - let from = from_value(from_header)?; - let command = GetVerifiedHeaders { from, amount }; - let response = self.channel.send(command).await?; + let command = NodeCommand::GetVerifiedHeaders { + from: from_value(from_header)?, + amount, + }; + self.channel.send(command).await?; + let headers = self.channel.recv().await?.into_headers().check_variant()?; - let result = response - .await + let result = headers? .iter() .map(|h| to_value(&h).unwrap()) // XXX .collect(); @@ -201,41 +267,57 @@ impl NodeDriver { /// Get current header syncing info. pub async fn syncer_info(&self) -> Result { - let response = self.channel.send(GetSyncerInfo).await?; - - Ok(to_value(&response.await?)?) + let command = NodeCommand::GetSyncerInfo; + self.channel.send(command).await?; + let syncer_info = self + .channel + .recv() + .await? + .into_syncer_info() + .check_variant()?; + + Ok(to_value(&syncer_info?)?) } /// Get the latest header announced in the network. pub async fn get_network_head_header(&self) -> Result { - let command = LastSeenNetworkHead; - let response = self.channel.send(command).await?; - - Ok(to_value(&response.await?)?) + let command = NodeCommand::LastSeenNetworkHead; + self.channel.send(command).await?; + let header = self + .channel + .recv() + .await? + .into_last_seen_network_head() + .check_variant()?; + + Ok(to_value(&header)?) } /// Get the latest locally synced header. pub async fn get_local_head_header(&self) -> Result { - let command = GetHeader(SingleHeaderQuery::Head); - let response = self.channel.send(command).await?; + let command = NodeCommand::GetHeader(SingleHeaderQuery::Head); + self.channel.send(command).await?; + let header = self.channel.recv().await?.into_header().check_variant()?; - Ok(to_value(&response.await?)?) + Ok(to_value(&header?)?) } /// Get a synced header for the block with a given hash. pub async fn get_header_by_hash(&self, hash: &str) -> Result { - let command = GetHeader(SingleHeaderQuery::ByHash(hash.parse()?)); - let response = self.channel.send(command).await?; + let command = NodeCommand::GetHeader(SingleHeaderQuery::ByHash(hash.parse()?)); + self.channel.send(command).await?; + let header = self.channel.recv().await?.into_header().check_variant()?; - Ok(to_value(&response.await?)?) + Ok(to_value(&header?)?) } /// Get a synced header for the block with a given height. pub async fn get_header_by_height(&self, height: u64) -> Result { - let command = GetHeader(SingleHeaderQuery::ByHeight(height)); - let response = self.channel.send(command).await?; + let command = NodeCommand::GetHeader(SingleHeaderQuery::ByHeight(height)); + self.channel.send(command).await?; + let header = self.channel.recv().await?.into_header().check_variant()?; - Ok(to_value(&response.await?)?) + Ok(to_value(&header?)?) } /// Get synced headers from the given heights range. @@ -252,26 +334,30 @@ impl NodeDriver { start_height: Option, end_height: Option, ) -> Result { - let command = GetHeadersRange { + let command = NodeCommand::GetHeadersRange { start_height, end_height, }; - let response = self.channel.send(command).await?; - let result = response - .await - .iter() - .map(|h| to_value(&h).unwrap()) - .collect(); + self.channel.send(command).await?; + let headers = self.channel.recv().await?.into_headers().check_variant()?; + + let result = headers?.iter().map(|h| to_value(&h).unwrap()).collect(); Ok(result) } /// Get data sampling metadata of an already sampled height. pub async fn get_sampling_metadata(&self, height: u64) -> Result { - let command = GetSamplingMetadata { height }; - let response = self.channel.send(command).await?; - - Ok(to_value(&response.await?)?) + let command = NodeCommand::GetSamplingMetadata { height }; + self.channel.send(command).await?; + let metadata = self + .channel + .recv() + .await? + .into_sampling_metadata() + .check_variant()?; + + Ok(to_value(&metadata?)?) } } diff --git a/node-wasm/src/worker.rs b/node-wasm/src/worker.rs index 40260597..3891df75 100644 --- a/node-wasm/src/worker.rs +++ b/node-wasm/src/worker.rs @@ -1,26 +1,32 @@ use std::fmt::Debug; use std::marker::PhantomData; -use futures::future::{BoxFuture, Future, TryFutureExt}; -use serde::de::DeserializeOwned; +use libp2p::Multiaddr; +use libp2p::PeerId; +use lumina_node::store::SamplingMetadata; use serde::{Deserialize, Serialize}; use serde_wasm_bindgen::{from_value, to_value}; use thiserror::Error; -use tokio::sync::{mpsc, oneshot}; +use tokio::sync::mpsc; use tracing::{error, info, trace, warn}; use wasm_bindgen::prelude::*; use wasm_bindgen_futures::spawn_local; use web_sys::{MessageEvent, MessagePort, SharedWorker}; +use celestia_types::ExtendedHeader; use lumina_node::node::{Node, NodeError}; use lumina_node::store::{IndexedDbStore, Store}; +use lumina_node::syncer::SyncingInfo; use crate::node::WasmNodeConfig; use crate::utils::WorkerSelf; -use crate::worker::commands::*; +use crate::worker::commands::*; // TODO pub(crate) mod commands; +/// actual type that's sent over a js message port +pub(crate) type WireMessage = Option>; + #[derive(Debug, Serialize, Deserialize, Error)] pub enum WorkerError { #[error("Command response channel has been dropped before response could be sent, should not happen")] @@ -31,10 +37,33 @@ pub enum WorkerError { SharedWorkerChannelInvalidType, #[error("Lumina is already running, ignoring StartNode command")] SharedWorkerAlreadyRunning, - #[error("node error: {0}")] - NodeError(String), #[error("Worker is still handling previous command")] WorkerBusy, + + #[error("Node hasn't been started yet")] + NodeNotRunning, + #[error("Node has already been started")] + NodeAlreadyRunning, + + #[error("node error: {0}")] + NodeError(String), + + #[error("could not send command to the worker: {0}")] + CommandSendingFailed(String), + + #[error("respose to command did not match expected type")] + InvalidResponseType, + + #[error("response message could not be serialised, should not happen: {0}")] + CouldNotSerialiseResponse(String), + #[error("response message could not be sent: {0}")] + CouldNotSendResponse(String), + + #[error("Received empty worker response, should not happen")] + EmptyWorkerResponse, + + #[error("Response channel to worker closed, should not happen")] + ResponseChannelDropped, } impl From for WorkerError { @@ -44,7 +73,6 @@ impl From for WorkerError { } type Result = std::result::Result; -pub type CommandResponseChannel = oneshot::Sender<::Output>; type WorkerClientConnection = (MessagePort, Closure); #[derive(Serialize, Deserialize, Debug)] @@ -57,93 +85,108 @@ pub trait NodeCommandType: Debug + Into { type Output; } -pub struct SharedWorkerChannel { +pub struct SharedWorkerChannel { _onmessage: Closure, channel: MessagePort, - response_channels: mpsc::Sender>, + response_channel: mpsc::Receiver, send_type: PhantomData, } -impl SharedWorkerChannel +impl SharedWorkerChannel where IN: Serialize, - OUT: DeserializeOwned + 'static, { pub fn new(channel: MessagePort) -> Self { - let (message_tx, mut message_rx) = mpsc::channel(64); + let (response_tx, mut response_rx) = mpsc::channel(64); - let near_tx = message_tx.clone(); + let near_tx = response_tx.clone(); let onmessage_callback = move |ev: MessageEvent| { let local_tx = near_tx.clone(); spawn_local(async move { let message_data = ev.data(); - let data = from_value(message_data).unwrap(); + let data = from_value(message_data) + .map_err(|e| { + error!("could not convert from JsValue: {e}"); + }) + .ok(); - local_tx.send(data).await.expect("send err"); + if let Err(e) = local_tx.send(data).await { + error!("message forwarding channel closed, should not happen"); + } }) }; // TODO: one of the ways to make sure we're synchronised with lumina in worker is forcing // request-response interface over the channel (waiting for previous command to be handled // before sending next one). Is it better for lumina to maintain this inside the worker? - let (response_tx, mut response_rx) = mpsc::channel(1); - - // small task running concurently, which converts js's callback style into - // rust's awaiting on a channel message passing style - spawn_local(async move { - loop { - let message = message_rx.recv().await.unwrap(); - let response_channel: oneshot::Sender = response_rx.recv().await.unwrap(); - if response_channel.send(message).is_err() { - warn!( - "response channel closed before response could be sent, dropping message" - ); - } - } - }); + //let (response_tx, mut response_rx) = mpsc::channel(1); + + /* + // small task running concurently, which converts js's callback style into + // rust's awaiting on a channel message passing style + spawn_local(async move { + loop { + let message = message_rx.recv().await.unwrap(); + let response_channel: oneshot::Sender = response_rx.recv().await.unwrap(); + if response_channel.send(message).is_err() { + warn!( + "response channel closed before response could be sent, dropping message" + ); + } + } + }); + */ let onmessage = Closure::new(onmessage_callback); channel.set_onmessage(Some(onmessage.as_ref().unchecked_ref())); channel.start(); Self { - response_channels: response_tx, + response_channel: response_rx, _onmessage: onmessage, channel, send_type: PhantomData, } } - pub async fn send( - &self, - command: T, - ) -> Result>> - where - T::Output: Debug + Serialize, - NodeCommandResponse: TryFrom, - as TryFrom>::Error: Debug, - { + pub async fn send(&self, command: IN) -> Result<(), WorkerError> { + let v = to_value(&command) + .map_err(|e| WorkerError::CouldNotSerialiseResponse(e.to_string()))?; + self.channel.post_message(&v).map_err(|e| { + WorkerError::CouldNotSendResponse(e.as_string().unwrap_or("UNDEFINED".to_string())) + }) + + /* self.send_enum(command.into()); + let (tx, rx) = oneshot::channel(); - let (tx, rx) = oneshot::channel(); + self.response_channels + .send(tx) + .await + .map_err(|_| WorkerError::SharedWorkerCommandChannelClosed)?; + + Ok(rx.map_ok_or_else( + |_e| Err(WorkerError::SharedWorkerChannelResponseChannelDropped), + |r| match NodeCommandResponse::::try_from(r) { + Ok(v) => Ok(v.0), + Err(_) => Err(WorkerError::SharedWorkerChannelInvalidType), + }, + )) + } - self.response_channels - .send(tx) - .await - .map_err(|_| WorkerError::SharedWorkerCommandChannelClosed)?; - - Ok(rx.map_ok_or_else( - |_e| Err(WorkerError::SharedWorkerChannelResponseChannelDropped), - |r| match NodeCommandResponse::::try_from(r) { - Ok(v) => Ok(v.0), - Err(_) => Err(WorkerError::SharedWorkerChannelInvalidType), - }, - )) + fn send_enum(&self, msg: NodeCommand) { + let v = to_value(&msg).unwrap(); + self.channel.post_message(&v).expect("err post"); + */ } - fn send_enum(&self, msg: NodeCommand) { - let v = to_value(&msg).unwrap(); - self.channel.post_message(&v).expect("err post"); + pub async fn recv(&self) -> Result { + let message: WireMessage = self + .response_channel + .recv() + .await + .ok_or(WorkerError::ResponseChannelDropped)?; + message.ok_or(WorkerError::EmptyWorkerResponse)? } } @@ -169,141 +212,132 @@ impl NodeWorker { Self { node } } - async fn process_command(&mut self, command: NodeCommandWithChannel) { + async fn get_syncer_info(&mut self) -> Result { + Ok(self.node.syncer_info().await?) + } + + async fn set_peer_trust(&mut self, peer_id: PeerId, is_trusted: bool) -> Result<()> { + Ok(self.node.set_peer_trust(peer_id, is_trusted).await?) + } + + async fn get_connected_peers(&mut self) -> Result> { + Ok(self + .node + .connected_peers() + .await? + .iter() + .map(|id| id.to_string()) + .collect()) + } + + async fn get_listeners(&mut self) -> Result> { + Ok(self.node.listeners().await?) + } + + async fn wait_connected(&mut self, trusted: bool) -> Result<()> { + if trusted { + self.node.wait_connected().await?; + } else { + self.node.wait_connected_trusted().await?; + } + Ok(()) + } + + async fn request_header(&mut self, query: SingleHeaderQuery) -> Result { + Ok(match query { + SingleHeaderQuery::Head => self.node.request_head_header().await, + SingleHeaderQuery::ByHash(hash) => self.node.request_header_by_hash(&hash).await, + SingleHeaderQuery::ByHeight(height) => self.node.request_header_by_height(height).await, + }?) + } + + async fn get_header(&mut self, query: SingleHeaderQuery) -> Result { + Ok(match query { + SingleHeaderQuery::Head => self.node.get_local_head_header().await, + SingleHeaderQuery::ByHash(hash) => self.node.get_header_by_hash(&hash).await, + SingleHeaderQuery::ByHeight(height) => self.node.get_header_by_height(height).await, + }?) + } + + async fn get_verified_headers( + &mut self, + from: &ExtendedHeader, + amount: u64, + ) -> Result> { + Ok(self.node.request_verified_headers(&from, amount).await?) + } + + async fn get_headers_range( + &mut self, + start_height: Option, + end_height: Option, + ) -> Result> { + Ok(match (start_height, end_height) { + (None, None) => self.node.get_headers(..).await, + (Some(start), None) => self.node.get_headers(start..).await, + (None, Some(end)) => self.node.get_headers(..=end).await, + (Some(start), Some(end)) => self.node.get_headers(start..=end).await, + }?) + } + + async fn get_sampling_metadata(&mut self, height: u64) -> Result> { + Ok(self.node.get_sampling_metadata(height).await?) + } + + async fn process_command(&mut self, command: NodeCommand) -> WorkerResponse { match command { - NodeCommandWithChannel::IsRunning((_, response)) => { - response.send(true).expect("channel_dropped") - } - NodeCommandWithChannel::StartNode((_, response)) => { - response - .send(Err(WorkerError::SharedWorkerAlreadyRunning)) - .expect("channel_dropped"); + NodeCommand::IsRunning => WorkerResponse::IsRunning(true), + NodeCommand::StartNode(_) => { + WorkerResponse::NodeStarted(Err(WorkerError::NodeAlreadyRunning)) } - NodeCommandWithChannel::GetLocalPeerId((_, response)) => { - response - .send(self.node.local_peer_id().to_string()) - .expect("channel_dropped"); + NodeCommand::GetLocalPeerId => { + WorkerResponse::LocalPeerId(self.node.local_peer_id().to_string()) } - NodeCommandWithChannel::GetSyncerInfo((_, response)) => { - let syncer_info = self.node.syncer_info().await.unwrap(); - response.send(syncer_info).expect("channel_dropped"); - } - NodeCommandWithChannel::GetPeerTrackerInfo((_, response)) => { + NodeCommand::GetSyncerInfo => WorkerResponse::SyncerInfo(self.get_syncer_info().await), + NodeCommand::GetPeerTrackerInfo => { let peer_tracker_info = self.node.peer_tracker_info(); - response.send(peer_tracker_info).expect("channel_dropped"); + WorkerResponse::PeerTrackerInfo(peer_tracker_info) } - NodeCommandWithChannel::GetNetworkInfo((_, response)) => { - let network_info = self.node.network_info().await.expect("TODO").into(); - response.send(network_info).expect("channel_dropped") + NodeCommand::GetNetworkInfo => { + let network_info = self.node.network_info().await; + //WorkerResponse::NetworkInfo(network_info) + todo!() } - NodeCommandWithChannel::GetConnectedPeers((_, response)) => { - let connected_peers = self.node.connected_peers().await.expect("TODO"); - response - .send(connected_peers.iter().map(|id| id.to_string()).collect()) - .expect("channel_dropped"); + NodeCommand::GetConnectedPeers => { + WorkerResponse::ConnectedPeers(self.get_connected_peers().await) } - NodeCommandWithChannel::SetPeerTrust(( - SetPeerTrust { - peer_id, - is_trusted, - }, - response, - )) => { - self.node - .set_peer_trust(peer_id.parse().unwrap(), is_trusted) - .await - .unwrap(); - response.send(()).expect("channel_dropped"); + NodeCommand::SetPeerTrust { + peer_id, + is_trusted, + } => WorkerResponse::SetPeerTrust(self.set_peer_trust(peer_id, is_trusted).await), + NodeCommand::WaitConnected { trusted } => { + WorkerResponse::Connected(self.wait_connected(trusted).await) } - NodeCommandWithChannel::WaitConnected((parameters, response)) => { - // TODO: nonblocking on channels - if parameters.trusted { - let _ = self.node.wait_connected().await; - } else { - let _ = self.node.wait_connected_trusted().await; - } - response.send(()).expect("channel_dropped") + NodeCommand::GetListeners => WorkerResponse::Listeners(self.get_listeners().await), + NodeCommand::RequestHeader(query) => { + WorkerResponse::Header(self.request_header(query).await) } - NodeCommandWithChannel::GetListeners((_, response)) => response - .send(self.node.listeners().await.unwrap()) - .expect("channel_dropped"), - NodeCommandWithChannel::RequestHeader((command, response)) => { - let header = match command.0 { - SingleHeaderQuery::Head => self.node.request_head_header().await, - SingleHeaderQuery::ByHash(hash) => { - self.node.request_header_by_hash(&hash).await - } - SingleHeaderQuery::ByHeight(height) => { - self.node.request_header_by_height(height).await - } - }; - response - .send(header.map_err(|e| e.into())) - .expect("channel_dropped"); + NodeCommand::GetHeader(query) => WorkerResponse::Header(self.get_header(query).await), + NodeCommand::GetVerifiedHeaders { from, amount } => { + WorkerResponse::Headers(self.get_verified_headers(&from, amount).await) } - NodeCommandWithChannel::GetHeader((command, response)) => { - let header = match command.0 { - SingleHeaderQuery::Head => self.node.get_local_head_header().await.unwrap(), - SingleHeaderQuery::ByHash(hash) => { - self.node.get_header_by_hash(&hash).await.ok().unwrap() - } - SingleHeaderQuery::ByHeight(height) => { - self.node.get_header_by_height(height).await.ok().unwrap() - } - }; - response.send(header).expect("channel_dropped"); + NodeCommand::GetHeadersRange { + start_height, + end_height, + } => WorkerResponse::Headers(self.get_headers_range(start_height, end_height).await), + NodeCommand::LastSeenNetworkHead => { + WorkerResponse::LastSeenNetworkHead(self.node.get_network_head_header()) } - NodeCommandWithChannel::GetVerifiedHeaders((command, response)) => { - let GetVerifiedHeaders { from, amount } = command; - let headers = self - .node - .request_verified_headers(&from, amount) - .await - .unwrap(); - response.send(headers).expect("channel_dropped"); - } - NodeCommandWithChannel::GetHeadersRange((command, response)) => { - let GetHeadersRange { - start_height, - end_height, - } = command; - let headers = match (start_height, end_height) { - (None, None) => self.node.get_headers(..).await, - (Some(start), None) => self.node.get_headers(start..).await, - (None, Some(end)) => self.node.get_headers(..=end).await, - (Some(start), Some(end)) => self.node.get_headers(start..=end).await, - } - .ok() - .unwrap(); - response.send(headers).expect("channel_dropped"); + NodeCommand::GetSamplingMetadata { height } => { + WorkerResponse::SamplingMetadata(self.get_sampling_metadata(height).await) } - NodeCommandWithChannel::LastSeenNetworkHead((_, response)) => { - let header = self.node.get_network_head_header(); - response.send(header).expect("channel_dropped"); - } - NodeCommandWithChannel::GetSamplingMetadata((command, response)) => { - let metadata = self - .node - .get_sampling_metadata(command.height) - .await - .ok() - .unwrap() - .unwrap(); - response.send(metadata).expect("channel_dropped"); - } - }; + } } } enum WorkerMessage { NewConnection(MessagePort), - Command(NodeCommandWithChannel), - ResponseChannel( - ( - ClientId, - BoxFuture<'static, Result>, - ), - ), + Command((NodeCommand, ClientId)), } #[derive(Debug)] @@ -360,28 +394,17 @@ impl WorkerConnector { let local_tx = near_tx.clone(); spawn_local(async move { let message_data = ev.data(); - let Ok(node_command) = from_value::(message_data) else { + let Ok(command) = from_value::(message_data) else { warn!("could not deserialize message from client {client_id}"); return; }; - let (command_with_channel, response_channel) = - node_command.add_response_channel(); if let Err(e) = local_tx - .send(WorkerMessage::Command(command_with_channel)) + .send(WorkerMessage::Command((command, ClientId(client_id)))) .await { error!("command channel inside worker closed, should not happen: {e}"); } - - // TODO: something cleaner? - local_tx - .send(WorkerMessage::ResponseChannel(( - ClientId(client_id), - response_channel, - ))) - .await - .expect("send4 err"); }) }); port.set_onmessage(Some(client_message_callback.as_ref().unchecked_ref())); @@ -391,9 +414,20 @@ impl WorkerConnector { info!("SharedWorker ready to receive commands from client {client_id}"); } - fn respond_to(&self, client: ClientId, msg: Option) { + fn respond_to(&self, client: ClientId, msg: WorkerResponse) { + self.send_response(client, Ok(msg)) + } + + fn respond_err_to(&self, client: ClientId, error: WorkerError) { + self.send_response(client, Err(error)) + } + + fn send_response(&self, client: ClientId, msg: Result) { let offset = client.0; - let message = match to_value(&msg) { + + let wire_message: WireMessage = Some(msg); + + let message = match to_value(&wire_message) { Ok(jsvalue) => jsvalue, Err(e) => { warn!("provided response could not be coverted to JsValue: {e}, sending undefined instead"); @@ -423,29 +457,26 @@ pub async fn run_worker(queued_connections: Vec) { WorkerMessage::NewConnection(connection) => { connector.add(connection); } - WorkerMessage::Command(command_with_channel) => { + WorkerMessage::Command((command, client_id)) => { let Some(worker) = &mut worker else { - match command_with_channel { - NodeCommandWithChannel::IsRunning((_, response)) => { - response.send(false).expect("channel_dropped"); + match command { + NodeCommand::IsRunning => { + connector.respond_to(client_id, WorkerResponse::IsRunning(false)); } - NodeCommandWithChannel::StartNode((command, response)) => { - worker = Some(NodeWorker::new(command.0).await); - response.send(Ok(())).expect("channel_dropped"); + NodeCommand::StartNode(config) => { + worker = Some(NodeWorker::new(config).await); + connector.respond_to(client_id, WorkerResponse::NodeStarted(Ok(()))); + } + _ => { + warn!("Worker not running"); + connector.respond_err_to(client_id, WorkerError::NodeNotRunning); } - _ => warn!("Worker not running"), } continue; }; - trace!("received: {command_with_channel:?}"); - worker.process_command(command_with_channel).await; - } - WorkerMessage::ResponseChannel((client_id, channel)) => { - // oneshot channel has one failure condition - Sender getting dropped - // we also _need_ to send some response, otherwise driver gets stuck waiting - let response = channel.await.ok(); - connector.respond_to(client_id, response); + trace!("received from {client_id:?}: {command:?}"); + worker.process_command(command).await; } } } diff --git a/node-wasm/src/worker/commands.rs b/node-wasm/src/worker/commands.rs index 04db56ee..f4ddd27c 100644 --- a/node-wasm/src/worker/commands.rs +++ b/node-wasm/src/worker/commands.rs @@ -1,9 +1,11 @@ use std::fmt::Debug; -use futures::future::{BoxFuture, FutureExt, TryFutureExt}; +use enum_as_inner::EnumAsInner; +//use futures::future::{BoxFuture, FutureExt, TryFutureExt}; use libp2p::Multiaddr; +use libp2p::PeerId; use serde::{Deserialize, Serialize}; -use tokio::sync::oneshot; +use tracing::error; use celestia_types::hash::Hash; use celestia_types::ExtendedHeader; @@ -13,9 +15,95 @@ use lumina_node::syncer::SyncingInfo; use crate::node::WasmNodeConfig; use crate::worker::Result; -use crate::worker::{CommandResponseChannel, NodeCommandResponse, NodeCommandType}; +use crate::worker::WorkerError; use crate::wrapper::libp2p::NetworkInfoSnapshot; +#[derive(Debug, Serialize, Deserialize)] +pub(crate) enum NodeCommand { + IsRunning, + StartNode(WasmNodeConfig), + GetLocalPeerId, + GetSyncerInfo, + GetPeerTrackerInfo, + GetNetworkInfo, + GetConnectedPeers, + SetPeerTrust { + peer_id: PeerId, + is_trusted: bool, + }, + WaitConnected { + trusted: bool, + }, + GetListeners, + RequestHeader(SingleHeaderQuery), + GetVerifiedHeaders { + from: ExtendedHeader, + amount: u64, + }, + GetHeadersRange { + start_height: Option, + end_height: Option, + }, + GetHeader(SingleHeaderQuery), + LastSeenNetworkHead, + GetSamplingMetadata { + height: u64, + }, +} + +#[derive(Serialize, Deserialize, Debug)] +pub(crate) enum SingleHeaderQuery { + Head, + ByHash(Hash), + ByHeight(u64), +} + +#[derive(Serialize, Deserialize, Debug, EnumAsInner)] +pub(crate) enum WorkerResponse { + IsRunning(bool), + NodeStarted(Result<()>), + LocalPeerId(String), + SyncerInfo(Result), + PeerTrackerInfo(PeerTrackerInfo), + NetworkInfo(Result), + ConnectedPeers(Result>), + SetPeerTrust(Result<()>), + Connected(Result<()>), + Listeners(Result>), + Header(Result), + Headers(Result>), + LastSeenNetworkHead(Option), + SamplingMetadata(Result>), +} + +pub(crate) trait CheckableResponse { + type Output; + fn check_variant(self) -> Result; +} + +impl CheckableResponse for Result { + type Output = T; + + fn check_variant(self) -> Result { + self.map_err(|response| { + error!("invalid response, received: {response:?}"); + WorkerError::InvalidResponseType + }) + } +} + +/* +#[derive(Debug, Serialize, Deserialize)] +enum WorkerErrorNg { + NodeNotRunning, + //NodeAlreadyRunning, +} +*/ + +#[derive(Debug, Serialize, Deserialize)] +pub(crate) enum NodeStartError {} + +/* macro_rules! define_command_from_impl { ($common_name:ident, $command_name:ident) => { impl From<$command_name> for $common_name { @@ -149,9 +237,4 @@ define_command!(GetHeader(SingleHeaderQuery) -> ExtendedHeader); define_command!(LastSeenNetworkHead -> Option); define_command!(GetSamplingMetadata { height: u64 } -> SamplingMetadata); -#[derive(Serialize, Deserialize, Debug)] -pub(crate) enum SingleHeaderQuery { - Head, - ByHash(Hash), - ByHeight(u64), -} +*/ From b94c5e0807f4832574dd5beb9bfe388d8ece36ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Florkiewicz?= Date: Fri, 19 Apr 2024 09:07:45 +0200 Subject: [PATCH 12/35] cleanup, rw fix --- cli/static/run_node.js | 7 +- node-wasm/src/node.rs | 174 ++++++++++--------------------- node-wasm/src/worker.rs | 74 +++++-------- node-wasm/src/worker/commands.rs | 1 + node-wasm/src/wrapper/libp2p.rs | 4 +- 5 files changed, 85 insertions(+), 175 deletions(-) diff --git a/cli/static/run_node.js b/cli/static/run_node.js index 7a18574c..6d54d69d 100644 --- a/cli/static/run_node.js +++ b/cli/static/run_node.js @@ -8,8 +8,6 @@ async function fetch_config() { console.log("Received config:", json); - window.nconfig = NodeConfig; - let config = NodeConfig.default(json.network); if (json.bootnodes.length !== 0) { config.bootnodes = json.bootnodes; @@ -108,12 +106,9 @@ async function start_node(config) { async function main(document, window) { await init(); - //window.worker = new SharedWorker('/js/worker.js', {name: 'lumina', type: 'module'}); - window.driver = await new NodeDriver(); bind_config(await fetch_config()); - //return; if (await window.driver.is_running() === true) { document.querySelectorAll('.config').forEach(elem => elem.disabled = true); @@ -129,7 +124,7 @@ async function main(document, window) { document.querySelectorAll(".status").forEach(elem => elem.style.visibility = "visible"); }); - setInterval(async () => await show_stats(window.driver), 1000) + //setInterval(async () => await show_stats(window.driver), 1000) } await main(document, window); diff --git a/node-wasm/src/node.rs b/node-wasm/src/node.rs index 601d8370..31620c02 100644 --- a/node-wasm/src/node.rs +++ b/node-wasm/src/node.rs @@ -18,7 +18,6 @@ use lumina_node::store::IndexedDbStore; use crate::utils::{js_value_from_display, JsContext, Network}; use crate::worker::commands::{CheckableResponse, NodeCommand, SingleHeaderQuery}; use crate::worker::SharedWorkerChannel; -use crate::worker::WorkerError; use crate::wrapper::libp2p::NetworkInfoSnapshot; use crate::Result; @@ -42,7 +41,7 @@ pub struct WasmNodeConfig { struct NodeDriver { _worker: SharedWorker, _onerror_callback: Closure, - channel: SharedWorkerChannel, + channel: SharedWorkerChannel, } #[wasm_bindgen] @@ -75,13 +74,8 @@ impl NodeDriver { /// Check whether Lumina is currently running pub async fn is_running(&mut self) -> Result { let command = NodeCommand::IsRunning; - self.channel.send(command).await?; - let running = self - .channel - .recv() - .await? - .into_is_running() - .check_variant()?; + let response = self.channel.exec(command).await?; + let running = response.into_is_running().check_variant()?; Ok(running) } @@ -89,13 +83,8 @@ impl NodeDriver { /// Start a node with the provided config, if it's not running pub async fn start(&self, config: WasmNodeConfig) -> Result<()> { let command = NodeCommand::StartNode(config); - self.channel.send(command).await?; - let started = self - .channel - .recv() - .await? - .into_node_started() - .check_variant()?; + let response = self.channel.exec(command).await?; + let started = response.into_node_started().check_variant()?; Ok(started?) } @@ -103,13 +92,8 @@ impl NodeDriver { /// Get node's local peer ID. pub async fn local_peer_id(&self) -> Result { let command = NodeCommand::GetLocalPeerId; - self.channel.send(command).await?; - let peer_id = self - .channel - .recv() - .await? - .into_local_peer_id() - .check_variant()?; + let response = self.channel.exec(command).await?; + let peer_id = response.into_local_peer_id().check_variant()?; Ok(peer_id) } @@ -117,13 +101,8 @@ impl NodeDriver { /// Get current [`PeerTracker`] info. pub async fn peer_tracker_info(&self) -> Result { let command = NodeCommand::GetPeerTrackerInfo; - self.channel.send(command).await?; - let peer_info = self - .channel - .recv() - .await? - .into_peer_tracker_info() - .check_variant()?; + let response = self.channel.exec(command).await?; + let peer_info = response.into_peer_tracker_info().check_variant()?; Ok(to_value(&peer_info)?) } @@ -131,71 +110,48 @@ impl NodeDriver { /// Wait until the node is connected to at least 1 peer. pub async fn wait_connected(&self) -> Result<()> { let command = NodeCommand::WaitConnected { trusted: false }; - self.channel.send(command).await?; - // slightly unfortunate `is_connected` name is generated by the enum-as-inner - // crate and checks whether enum variant is WorkerResponse::Connected - if !self.channel.recv().await?.is_connected() { - Err(WorkerError::InvalidResponseType.into()) - } else { - Ok(()) - } + let response = self.channel.exec(command).await?; + let result = response.into_connected().check_variant()?; + + Ok(result?) } /// Wait until the node is connected to at least 1 trusted peer. pub async fn wait_connected_trusted(&self) -> Result<()> { let command = NodeCommand::WaitConnected { trusted: false }; - self.channel.send(command).await?; - // slightly unfortunate `is_connected` name is generated by the enum-as-inner - // crate and checks whether enum variant is WorkerResponse::Connected - if !self.channel.recv().await?.is_connected() { - Err(WorkerError::InvalidResponseType.into()) - } else { - Ok(()) - } + let response = self.channel.exec(command).await?; + let result = response.into_connected().check_variant()?; + + Ok(result?) } /// Get current network info. pub async fn network_info(&self) -> Result { let command = NodeCommand::GetNetworkInfo; - self.channel.send(command).await?; - let network_info = self - .channel - .recv() - .await? - .into_network_info() - .check_variant()?; - - Ok(todo!()) + let response = self.channel.exec(command).await?; + let network_info = response.into_network_info().check_variant()?; + + Ok(network_info?) } /// Get all the multiaddresses on which the node listens. pub async fn listeners(&self) -> Result { let command = NodeCommand::GetListeners; - self.channel.send(command).await?; - let listeners = self - .channel - .recv() - .await? - .into_listeners() - .check_variant()?; - //let response = response.await?.iter().map(js_value_from_display).collect(); - - Ok(todo!()) + let response = self.channel.exec(command).await?; + let listeners = response.into_listeners().check_variant()?; + let result = listeners?.iter().map(js_value_from_display).collect(); + + Ok(result) } /// Get all the peers that node is connected to. pub async fn connected_peers(&self) -> Result { let command = NodeCommand::GetConnectedPeers; - self.channel.send(command).await?; - let peers = self - .channel - .recv() - .await? - .into_connected_peers() - .check_variant()?; - //response.await?.iter().map(js_value_from_display).collect(); - - Ok(todo!()) + let response = self.channel.exec(command).await?; + let peers = response.into_connected_peers().check_variant()?; + let result = peers?.iter().map(js_value_from_display).collect(); + + Ok(result) } /// Trust or untrust the peer with a given ID. @@ -204,13 +160,8 @@ impl NodeDriver { peer_id: peer_id.parse()?, is_trusted, }; - self.channel.send(command).await?; - let set_result = self - .channel - .recv() - .await? - .into_set_peer_trust() - .check_variant()?; + let response = self.channel.exec(command).await?; + let set_result = response.into_set_peer_trust().check_variant()?; Ok(set_result?) } @@ -218,8 +169,8 @@ impl NodeDriver { /// Request the head header from the network. pub async fn request_head_header(&self) -> Result { let command = NodeCommand::RequestHeader(SingleHeaderQuery::Head); - self.channel.send(command).await?; - let header = self.channel.recv().await?.into_header().check_variant()?; + let response = self.channel.exec(command).await?; + let header = response.into_header().check_variant()?; Ok(to_value(&header?)?) } @@ -227,8 +178,8 @@ impl NodeDriver { /// Request a header for the block with a given hash from the network. pub async fn request_header_by_hash(&self, hash: &str) -> Result { let command = NodeCommand::RequestHeader(SingleHeaderQuery::ByHash(hash.parse()?)); - self.channel.send(command).await?; - let header = self.channel.recv().await?.into_header().check_variant()?; + let response = self.channel.exec(command).await?; + let header = response.into_header().check_variant()?; Ok(to_value(&header?)?) } @@ -236,8 +187,8 @@ impl NodeDriver { /// Request a header for the block with a given height from the network. pub async fn request_header_by_height(&self, height: u64) -> Result { let command = NodeCommand::RequestHeader(SingleHeaderQuery::ByHeight(height)); - self.channel.send(command).await?; - let header = self.channel.recv().await?.into_header().check_variant()?; + let response = self.channel.exec(command).await?; + let header = response.into_header().check_variant()?; Ok(to_value(&header?)?) } @@ -254,8 +205,8 @@ impl NodeDriver { from: from_value(from_header)?, amount, }; - self.channel.send(command).await?; - let headers = self.channel.recv().await?.into_headers().check_variant()?; + let response = self.channel.exec(command).await?; + let headers = response.into_headers().check_variant()?; let result = headers? .iter() @@ -268,13 +219,8 @@ impl NodeDriver { /// Get current header syncing info. pub async fn syncer_info(&self) -> Result { let command = NodeCommand::GetSyncerInfo; - self.channel.send(command).await?; - let syncer_info = self - .channel - .recv() - .await? - .into_syncer_info() - .check_variant()?; + let response = self.channel.exec(command).await?; + let syncer_info = response.into_syncer_info().check_variant()?; Ok(to_value(&syncer_info?)?) } @@ -282,13 +228,8 @@ impl NodeDriver { /// Get the latest header announced in the network. pub async fn get_network_head_header(&self) -> Result { let command = NodeCommand::LastSeenNetworkHead; - self.channel.send(command).await?; - let header = self - .channel - .recv() - .await? - .into_last_seen_network_head() - .check_variant()?; + let response = self.channel.exec(command).await?; + let header = response.into_last_seen_network_head().check_variant()?; Ok(to_value(&header)?) } @@ -296,8 +237,8 @@ impl NodeDriver { /// Get the latest locally synced header. pub async fn get_local_head_header(&self) -> Result { let command = NodeCommand::GetHeader(SingleHeaderQuery::Head); - self.channel.send(command).await?; - let header = self.channel.recv().await?.into_header().check_variant()?; + let response = self.channel.exec(command).await?; + let header = response.into_header().check_variant()?; Ok(to_value(&header?)?) } @@ -305,8 +246,8 @@ impl NodeDriver { /// Get a synced header for the block with a given hash. pub async fn get_header_by_hash(&self, hash: &str) -> Result { let command = NodeCommand::GetHeader(SingleHeaderQuery::ByHash(hash.parse()?)); - self.channel.send(command).await?; - let header = self.channel.recv().await?.into_header().check_variant()?; + let response = self.channel.exec(command).await?; + let header = response.into_header().check_variant()?; Ok(to_value(&header?)?) } @@ -314,8 +255,8 @@ impl NodeDriver { /// Get a synced header for the block with a given height. pub async fn get_header_by_height(&self, height: u64) -> Result { let command = NodeCommand::GetHeader(SingleHeaderQuery::ByHeight(height)); - self.channel.send(command).await?; - let header = self.channel.recv().await?.into_header().check_variant()?; + let response = self.channel.exec(command).await?; + let header = response.into_header().check_variant()?; Ok(to_value(&header?)?) } @@ -338,8 +279,8 @@ impl NodeDriver { start_height, end_height, }; - self.channel.send(command).await?; - let headers = self.channel.recv().await?.into_headers().check_variant()?; + let response = self.channel.exec(command).await?; + let headers = response.into_headers().check_variant()?; let result = headers?.iter().map(|h| to_value(&h).unwrap()).collect(); @@ -349,13 +290,8 @@ impl NodeDriver { /// Get data sampling metadata of an already sampled height. pub async fn get_sampling_metadata(&self, height: u64) -> Result { let command = NodeCommand::GetSamplingMetadata { height }; - self.channel.send(command).await?; - let metadata = self - .channel - .recv() - .await? - .into_sampling_metadata() - .check_variant()?; + let response = self.channel.exec(command).await?; + let metadata = response.into_sampling_metadata().check_variant()?; Ok(to_value(&metadata?)?) } diff --git a/node-wasm/src/worker.rs b/node-wasm/src/worker.rs index 3891df75..7edfb11a 100644 --- a/node-wasm/src/worker.rs +++ b/node-wasm/src/worker.rs @@ -1,5 +1,4 @@ use std::fmt::Debug; -use std::marker::PhantomData; use libp2p::Multiaddr; use libp2p::PeerId; @@ -8,6 +7,7 @@ use serde::{Deserialize, Serialize}; use serde_wasm_bindgen::{from_value, to_value}; use thiserror::Error; use tokio::sync::mpsc; +use tokio::sync::Mutex; use tracing::{error, info, trace, warn}; use wasm_bindgen::prelude::*; use wasm_bindgen_futures::spawn_local; @@ -21,6 +21,7 @@ use lumina_node::syncer::SyncingInfo; use crate::node::WasmNodeConfig; use crate::utils::WorkerSelf; use crate::worker::commands::*; // TODO +use crate::wrapper::libp2p::NetworkInfoSnapshot; pub(crate) mod commands; @@ -55,9 +56,9 @@ pub enum WorkerError { InvalidResponseType, #[error("response message could not be serialised, should not happen: {0}")] - CouldNotSerialiseResponse(String), + CouldNotSerialiseCommand(String), #[error("response message could not be sent: {0}")] - CouldNotSendResponse(String), + CouldNotSendCommand(String), #[error("Received empty worker response, should not happen")] EmptyWorkerResponse, @@ -85,19 +86,15 @@ pub trait NodeCommandType: Debug + Into { type Output; } -pub struct SharedWorkerChannel { +pub struct SharedWorkerChannel { _onmessage: Closure, channel: MessagePort, - response_channel: mpsc::Receiver, - send_type: PhantomData, + response_channel: Mutex>, } -impl SharedWorkerChannel -where - IN: Serialize, -{ +impl SharedWorkerChannel { pub fn new(channel: MessagePort) -> Self { - let (response_tx, mut response_rx) = mpsc::channel(64); + let (response_tx, response_rx) = mpsc::channel(64); let near_tx = response_tx.clone(); let onmessage_callback = move |ev: MessageEvent| { @@ -112,7 +109,7 @@ where .ok(); if let Err(e) = local_tx.send(data).await { - error!("message forwarding channel closed, should not happen"); + error!("message forwarding channel closed, should not happen: {e}"); } }) }; @@ -142,47 +139,25 @@ where channel.set_onmessage(Some(onmessage.as_ref().unchecked_ref())); channel.start(); Self { - response_channel: response_rx, + response_channel: Mutex::new(response_rx), _onmessage: onmessage, channel, - send_type: PhantomData, } } - pub async fn send(&self, command: IN) -> Result<(), WorkerError> { - let v = to_value(&command) - .map_err(|e| WorkerError::CouldNotSerialiseResponse(e.to_string()))?; + fn send(&self, command: NodeCommand) -> Result<(), WorkerError> { + let v = + to_value(&command).map_err(|e| WorkerError::CouldNotSerialiseCommand(e.to_string()))?; self.channel.post_message(&v).map_err(|e| { - WorkerError::CouldNotSendResponse(e.as_string().unwrap_or("UNDEFINED".to_string())) + WorkerError::CouldNotSendCommand(e.as_string().unwrap_or("UNDEFINED".to_string())) }) - - /* - self.send_enum(command.into()); - let (tx, rx) = oneshot::channel(); - - self.response_channels - .send(tx) - .await - .map_err(|_| WorkerError::SharedWorkerCommandChannelClosed)?; - - Ok(rx.map_ok_or_else( - |_e| Err(WorkerError::SharedWorkerChannelResponseChannelDropped), - |r| match NodeCommandResponse::::try_from(r) { - Ok(v) => Ok(v.0), - Err(_) => Err(WorkerError::SharedWorkerChannelInvalidType), - }, - )) - } - - fn send_enum(&self, msg: NodeCommand) { - let v = to_value(&msg).unwrap(); - self.channel.post_message(&v).expect("err post"); - */ } - pub async fn recv(&self) -> Result { - let message: WireMessage = self - .response_channel + pub async fn exec(&self, command: NodeCommand) -> Result { + let mut channel = self.response_channel.lock().await; + self.send(command)?; + + let message: WireMessage = channel .recv() .await .ok_or(WorkerError::ResponseChannelDropped)?; @@ -216,6 +191,10 @@ impl NodeWorker { Ok(self.node.syncer_info().await?) } + async fn get_network_info(&mut self) -> Result { + Ok(self.node.network_info().await?.into()) + } + async fn set_peer_trust(&mut self, peer_id: PeerId, is_trusted: bool) -> Result<()> { Ok(self.node.set_peer_trust(peer_id, is_trusted).await?) } @@ -264,7 +243,7 @@ impl NodeWorker { from: &ExtendedHeader, amount: u64, ) -> Result> { - Ok(self.node.request_verified_headers(&from, amount).await?) + Ok(self.node.request_verified_headers(from, amount).await?) } async fn get_headers_range( @@ -299,9 +278,7 @@ impl NodeWorker { WorkerResponse::PeerTrackerInfo(peer_tracker_info) } NodeCommand::GetNetworkInfo => { - let network_info = self.node.network_info().await; - //WorkerResponse::NetworkInfo(network_info) - todo!() + WorkerResponse::NetworkInfo(self.get_network_info().await) } NodeCommand::GetConnectedPeers => { WorkerResponse::ConnectedPeers(self.get_connected_peers().await) @@ -335,6 +312,7 @@ impl NodeWorker { } } +#[allow(clippy::large_enum_variant)] enum WorkerMessage { NewConnection(MessagePort), Command((NodeCommand, ClientId)), diff --git a/node-wasm/src/worker/commands.rs b/node-wasm/src/worker/commands.rs index f4ddd27c..39f23529 100644 --- a/node-wasm/src/worker/commands.rs +++ b/node-wasm/src/worker/commands.rs @@ -18,6 +18,7 @@ use crate::worker::Result; use crate::worker::WorkerError; use crate::wrapper::libp2p::NetworkInfoSnapshot; +#[allow(clippy::large_enum_variant)] #[derive(Debug, Serialize, Deserialize)] pub(crate) enum NodeCommand { IsRunning, diff --git a/node-wasm/src/wrapper/libp2p.rs b/node-wasm/src/wrapper/libp2p.rs index b3d8c7e3..b2d14114 100644 --- a/node-wasm/src/wrapper/libp2p.rs +++ b/node-wasm/src/wrapper/libp2p.rs @@ -6,14 +6,14 @@ use wasm_bindgen::prelude::*; #[wasm_bindgen] #[derive(Debug, Serialize, Deserialize)] -pub struct NetworkInfoSnapshot { +pub(crate) struct NetworkInfoSnapshot { pub num_peers: usize, connection_counters: ConnectionCountersSnapshot, } #[wasm_bindgen] #[derive(Debug, Serialize, Deserialize)] -pub struct ConnectionCountersSnapshot { +pub(crate) struct ConnectionCountersSnapshot { pub num_connections: u32, pub num_pending: u32, pub num_pending_incoming: u32, From 431be7a31d4ed9078e1876375596859139d952ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Florkiewicz?= Date: Fri, 19 Apr 2024 16:08:49 +0200 Subject: [PATCH 13/35] code organisation and cleanup --- cli/static/run_node.js | 2 +- node-wasm/src/lib.rs | 2 +- node-wasm/src/node.rs | 26 ++-- node-wasm/src/worker.rs | 254 ++++--------------------------- node-wasm/src/worker/channel.rs | 192 +++++++++++++++++++++++ node-wasm/src/worker/commands.rs | 151 +----------------- node-wasm/src/wrapper/libp2p.rs | 68 --------- 7 files changed, 236 insertions(+), 459 deletions(-) create mode 100644 node-wasm/src/worker/channel.rs diff --git a/cli/static/run_node.js b/cli/static/run_node.js index 6d54d69d..9ac99499 100644 --- a/cli/static/run_node.js +++ b/cli/static/run_node.js @@ -119,7 +119,7 @@ async function main(document, window) { document.getElementById("start").addEventListener("click", async () => { document.querySelectorAll('.config').forEach(elem => elem.disabled = true); - await window.driver.start(window.config) + await window.driver.start(window.config); document.getElementById("peer-id").innerText = await window.driver.local_peer_id(); document.querySelectorAll(".status").forEach(elem => elem.style.visibility = "visible"); }); diff --git a/node-wasm/src/lib.rs b/node-wasm/src/lib.rs index a8d848e6..62812fc0 100644 --- a/node-wasm/src/lib.rs +++ b/node-wasm/src/lib.rs @@ -9,4 +9,4 @@ mod worker; mod wrapper; /// Alias for a `Result` with the error type [`JsError`]. -pub type Result = std::result::Result; +pub type Result = std::result::Result; diff --git a/node-wasm/src/node.rs b/node-wasm/src/node.rs index 31620c02..db658465 100644 --- a/node-wasm/src/node.rs +++ b/node-wasm/src/node.rs @@ -16,8 +16,8 @@ use lumina_node::node::NodeConfig; use lumina_node::store::IndexedDbStore; use crate::utils::{js_value_from_display, JsContext, Network}; -use crate::worker::commands::{CheckableResponse, NodeCommand, SingleHeaderQuery}; -use crate::worker::SharedWorkerChannel; +use crate::worker::commands::{CheckableResponseExt, NodeCommand, SingleHeaderQuery}; +use crate::worker::WorkerClient; use crate::wrapper::libp2p::NetworkInfoSnapshot; use crate::Result; @@ -41,7 +41,7 @@ pub struct WasmNodeConfig { struct NodeDriver { _worker: SharedWorker, _onerror_callback: Closure, - channel: SharedWorkerChannel, + channel: WorkerClient, } #[wasm_bindgen] @@ -50,29 +50,29 @@ impl NodeDriver { /// Note that single Shared Worker can be accessed from multiple tabs, so Lumina may already /// be running be running, before `NodeDriver::start` call. #[wasm_bindgen(constructor)] - pub async fn new() -> NodeDriver { + pub async fn new() -> Result { let mut opts = WorkerOptions::new(); opts.type_(WorkerType::Module); opts.name(LUMINA_SHARED_WORKER_NAME); let worker = SharedWorker::new_with_worker_options("/js/worker.js", &opts) - .expect("could not worker"); + .map_err(|e| JsError::new(&format!("could not create SharedWorker: {e:?}")))?; let onerror_callback: Closure = Closure::new(|ev: MessageEvent| { - error!("received error from shared worker: {ev:?}"); + error!("received error from SharedWorker: {ev:?}"); }); worker.set_onerror(Some(onerror_callback.as_ref().unchecked_ref())); - let channel = SharedWorkerChannel::new(worker.port()); + let channel = WorkerClient::new(worker.port()); - Self { + Ok(Self { _worker: worker, _onerror_callback: onerror_callback, channel, - } + }) } /// Check whether Lumina is currently running - pub async fn is_running(&mut self) -> Result { + pub async fn is_running(&self) -> Result { let command = NodeCommand::IsRunning; let response = self.channel.exec(command).await?; let running = response.into_is_running().check_variant()?; @@ -210,10 +210,10 @@ impl NodeDriver { let result = headers? .iter() - .map(|h| to_value(&h).unwrap()) // XXX - .collect(); + .map(|h| to_value(&h)) + .collect::>(); - Ok(result) + Ok(result?) } /// Get current header syncing info. diff --git a/node-wasm/src/worker.rs b/node-wasm/src/worker.rs index 7edfb11a..7171c627 100644 --- a/node-wasm/src/worker.rs +++ b/node-wasm/src/worker.rs @@ -1,35 +1,36 @@ use std::fmt::Debug; -use libp2p::Multiaddr; -use libp2p::PeerId; -use lumina_node::store::SamplingMetadata; +use libp2p::{Multiaddr, PeerId}; use serde::{Deserialize, Serialize}; -use serde_wasm_bindgen::{from_value, to_value}; use thiserror::Error; use tokio::sync::mpsc; -use tokio::sync::Mutex; -use tracing::{error, info, trace, warn}; +use tracing::{error, info, warn}; use wasm_bindgen::prelude::*; -use wasm_bindgen_futures::spawn_local; -use web_sys::{MessageEvent, MessagePort, SharedWorker}; +use web_sys::MessagePort; use celestia_types::ExtendedHeader; use lumina_node::node::{Node, NodeError}; -use lumina_node::store::{IndexedDbStore, Store}; +use lumina_node::store::{IndexedDbStore, SamplingMetadata, Store}; use lumina_node::syncer::SyncingInfo; use crate::node::WasmNodeConfig; -use crate::utils::WorkerSelf; -use crate::worker::commands::*; // TODO +use crate::worker::channel::{WorkerMessage, WorkerMessageServer}; +use crate::worker::commands::{NodeCommand, SingleHeaderQuery, WorkerResponse}; use crate::wrapper::libp2p::NetworkInfoSnapshot; +mod channel; pub(crate) mod commands; +pub(crate) use channel::WorkerClient; + +const WORKER_MESSAGE_SERVER_INCOMING_QUEUE_LENGTH: usize = 64; + /// actual type that's sent over a js message port -pub(crate) type WireMessage = Option>; +type Result = std::result::Result; #[derive(Debug, Serialize, Deserialize, Error)] pub enum WorkerError { + /* #[error("Command response channel has been dropped before response could be sent, should not happen")] SharedWorkerChannelResponseChannelDropped, #[error("Command channel to lumina worker closed, should not happen")] @@ -40,7 +41,7 @@ pub enum WorkerError { SharedWorkerAlreadyRunning, #[error("Worker is still handling previous command")] WorkerBusy, - + */ #[error("Node hasn't been started yet")] NodeNotRunning, #[error("Node has already been started")] @@ -73,101 +74,6 @@ impl From for WorkerError { } } -type Result = std::result::Result; -type WorkerClientConnection = (MessagePort, Closure); - -#[derive(Serialize, Deserialize, Debug)] -pub struct NodeCommandResponse(pub T::Output) -where - T: NodeCommandType, - T::Output: Debug + Serialize; - -pub trait NodeCommandType: Debug + Into { - type Output; -} - -pub struct SharedWorkerChannel { - _onmessage: Closure, - channel: MessagePort, - response_channel: Mutex>, -} - -impl SharedWorkerChannel { - pub fn new(channel: MessagePort) -> Self { - let (response_tx, response_rx) = mpsc::channel(64); - - let near_tx = response_tx.clone(); - let onmessage_callback = move |ev: MessageEvent| { - let local_tx = near_tx.clone(); - spawn_local(async move { - let message_data = ev.data(); - - let data = from_value(message_data) - .map_err(|e| { - error!("could not convert from JsValue: {e}"); - }) - .ok(); - - if let Err(e) = local_tx.send(data).await { - error!("message forwarding channel closed, should not happen: {e}"); - } - }) - }; - - // TODO: one of the ways to make sure we're synchronised with lumina in worker is forcing - // request-response interface over the channel (waiting for previous command to be handled - // before sending next one). Is it better for lumina to maintain this inside the worker? - //let (response_tx, mut response_rx) = mpsc::channel(1); - - /* - // small task running concurently, which converts js's callback style into - // rust's awaiting on a channel message passing style - spawn_local(async move { - loop { - let message = message_rx.recv().await.unwrap(); - let response_channel: oneshot::Sender = response_rx.recv().await.unwrap(); - if response_channel.send(message).is_err() { - warn!( - "response channel closed before response could be sent, dropping message" - ); - } - } - }); - */ - - let onmessage = Closure::new(onmessage_callback); - channel.set_onmessage(Some(onmessage.as_ref().unchecked_ref())); - channel.start(); - Self { - response_channel: Mutex::new(response_rx), - _onmessage: onmessage, - channel, - } - } - - fn send(&self, command: NodeCommand) -> Result<(), WorkerError> { - let v = - to_value(&command).map_err(|e| WorkerError::CouldNotSerialiseCommand(e.to_string()))?; - self.channel.post_message(&v).map_err(|e| { - WorkerError::CouldNotSendCommand(e.as_string().unwrap_or("UNDEFINED".to_string())) - }) - } - - pub async fn exec(&self, command: NodeCommand) -> Result { - let mut channel = self.response_channel.lock().await; - self.send(command)?; - - let message: WireMessage = channel - .recv() - .await - .ok_or(WorkerError::ResponseChannelDropped)?; - message.ok_or(WorkerError::EmptyWorkerResponse)? - } -} - -// TODO: cleanup JS objects on drop -// impl Drop - struct NodeWorker { node: Node, } @@ -179,7 +85,7 @@ impl NodeWorker { if let Ok(store_height) = config.store.head_height().await { info!("Initialised store with head height: {store_height}"); } else { - info!("Initialized new empty store"); + info!("Initialised new empty store"); } let node = Node::new(config).await.ok().unwrap(); @@ -312,152 +218,46 @@ impl NodeWorker { } } -#[allow(clippy::large_enum_variant)] -enum WorkerMessage { - NewConnection(MessagePort), - Command((NodeCommand, ClientId)), -} - -#[derive(Debug)] -struct ClientId(usize); - -struct WorkerConnector { - // same onconnect callback is used throughtout entire Worker lifetime. - // Keep a reference to make sure it doesn't get dropped. - _onconnect_callback: Closure, - - // keep a MessagePort for each client to send messages over, as well as callback responsible - // for forwarding messages back - clients: Vec, - - // sends events back to the main loop for processing - command_channel: mpsc::Sender, -} - -impl WorkerConnector { - fn new(command_channel: mpsc::Sender) -> Self { - let worker_scope = SharedWorker::worker_self(); - - let near_tx = command_channel.clone(); - let onconnect_callback: Closure = - Closure::new(move |ev: MessageEvent| { - let local_tx = near_tx.clone(); - spawn_local(async move { - let Ok(port) = ev.ports().at(0).dyn_into() else { - error!("received connection event without MessagePort, should not happen"); - return; - }; - - if let Err(e) = local_tx.send(WorkerMessage::NewConnection(port)).await { - error!("command channel inside worker closed, should not happen: {e}"); - } - }) - }); - - worker_scope.set_onconnect(Some(onconnect_callback.as_ref().unchecked_ref())); - - Self { - _onconnect_callback: onconnect_callback, - clients: Vec::with_capacity(1), // we usually expect to have exactly one client - command_channel, - } - } - - fn add(&mut self, port: MessagePort) { - let client_id = self.clients.len(); - - let near_tx = self.command_channel.clone(); - let client_message_callback: Closure = - Closure::new(move |ev: MessageEvent| { - let local_tx = near_tx.clone(); - spawn_local(async move { - let message_data = ev.data(); - let Ok(command) = from_value::(message_data) else { - warn!("could not deserialize message from client {client_id}"); - return; - }; - - if let Err(e) = local_tx - .send(WorkerMessage::Command((command, ClientId(client_id)))) - .await - { - error!("command channel inside worker closed, should not happen: {e}"); - } - }) - }); - port.set_onmessage(Some(client_message_callback.as_ref().unchecked_ref())); - - self.clients.push((port, client_message_callback)); - - info!("SharedWorker ready to receive commands from client {client_id}"); - } - - fn respond_to(&self, client: ClientId, msg: WorkerResponse) { - self.send_response(client, Ok(msg)) - } - - fn respond_err_to(&self, client: ClientId, error: WorkerError) { - self.send_response(client, Err(error)) - } - - fn send_response(&self, client: ClientId, msg: Result) { - let offset = client.0; - - let wire_message: WireMessage = Some(msg); - - let message = match to_value(&wire_message) { - Ok(jsvalue) => jsvalue, - Err(e) => { - warn!("provided response could not be coverted to JsValue: {e}, sending undefined instead"); - JsValue::UNDEFINED // we need to send something, client is waiting for a response - } - }; - - // XXX defensive programming with array checking? - if let Err(e) = self.clients[offset].0.post_message(&message) { - error!("could not post response message to client {client:?}: {e:?}"); - } - } -} - #[wasm_bindgen] pub async fn run_worker(queued_connections: Vec) { - let (tx, mut rx) = mpsc::channel(64); - let mut connector = WorkerConnector::new(tx.clone()); + let (tx, mut rx) = mpsc::channel(WORKER_MESSAGE_SERVER_INCOMING_QUEUE_LENGTH); + let mut message_server = WorkerMessageServer::new(tx.clone()); for connection in queued_connections { - connector.add(connection); + message_server.add(connection); } let mut worker = None; while let Some(message) = rx.recv().await { match message { WorkerMessage::NewConnection(connection) => { - connector.add(connection); + message_server.add(connection); } WorkerMessage::Command((command, client_id)) => { + info!("received from {client_id:?}: {command:?}"); let Some(worker) = &mut worker else { match command { NodeCommand::IsRunning => { - connector.respond_to(client_id, WorkerResponse::IsRunning(false)); + message_server.respond_to(client_id, WorkerResponse::IsRunning(false)); } NodeCommand::StartNode(config) => { worker = Some(NodeWorker::new(config).await); - connector.respond_to(client_id, WorkerResponse::NodeStarted(Ok(()))); + message_server + .respond_to(client_id, WorkerResponse::NodeStarted(Ok(()))); } _ => { warn!("Worker not running"); - connector.respond_err_to(client_id, WorkerError::NodeNotRunning); + message_server.respond_err_to(client_id, WorkerError::NodeNotRunning); } } continue; }; - trace!("received from {client_id:?}: {command:?}"); - worker.process_command(command).await; + let response = worker.process_command(command).await; + message_server.respond_to(client_id, response); } } } - error!("Worker exited, should not happen"); + error!("Channel to WorkerMessageServer closed, should not happen"); } diff --git a/node-wasm/src/worker/channel.rs b/node-wasm/src/worker/channel.rs new file mode 100644 index 00000000..451f6190 --- /dev/null +++ b/node-wasm/src/worker/channel.rs @@ -0,0 +1,192 @@ +use std::fmt::{self, Debug}; + +use serde_wasm_bindgen::{from_value, to_value}; +use tokio::sync::{mpsc, Mutex}; +use tracing::{debug, error, info, warn}; +use wasm_bindgen::prelude::*; +use wasm_bindgen_futures::spawn_local; +use web_sys::{MessageEvent, MessagePort, SharedWorker}; + +use crate::utils::WorkerSelf; +use crate::worker::commands::{NodeCommand, WorkerResponse}; +use crate::worker::WorkerError; + +type WireMessage = Option>; +type WorkerClientConnection = (MessagePort, Closure); + +// TODO: cleanup JS objects on drop +// impl Drop +pub(crate) struct WorkerClient { + _onmessage: Closure, + channel: MessagePort, + response_channel: Mutex>, +} + +impl WorkerClient { + pub fn new(channel: MessagePort) -> Self { + let (response_tx, response_rx) = mpsc::channel(64); + + let near_tx = response_tx.clone(); + let onmessage_callback = move |ev: MessageEvent| { + let local_tx = near_tx.clone(); + spawn_local(async move { + let message_data = ev.data(); + + let data = from_value(message_data) + .map_err(|e| { + error!("could not convert from JsValue: {e}"); + }) + .ok(); + + if let Err(e) = local_tx.send(data).await { + error!("message forwarding channel closed, should not happen: {e}"); + } + }) + }; + + let onmessage = Closure::new(onmessage_callback); + channel.set_onmessage(Some(onmessage.as_ref().unchecked_ref())); + channel.start(); + Self { + response_channel: Mutex::new(response_rx), + _onmessage: onmessage, + channel, + } + } + + pub async fn exec(&self, command: NodeCommand) -> Result { + let mut channel = self.response_channel.lock().await; + self.send(command)?; + + let message: WireMessage = channel + .recv() + .await + .ok_or(WorkerError::ResponseChannelDropped)?; + message.ok_or(WorkerError::EmptyWorkerResponse)? + } + + fn send(&self, command: NodeCommand) -> Result<(), WorkerError> { + let command_value = + to_value(&command).map_err(|e| WorkerError::CouldNotSerialiseCommand(e.to_string()))?; + self.channel.post_message(&command_value).map_err(|e| { + WorkerError::CouldNotSendCommand(e.as_string().unwrap_or("UNDEFINED".to_string())) + }) + } +} + +#[derive(Debug)] +pub(super) struct ClientId(usize); + +impl fmt::Display for ClientId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Client({})", self.0) + } +} + +#[allow(clippy::large_enum_variant)] +pub(super) enum WorkerMessage { + NewConnection(MessagePort), + Command((NodeCommand, ClientId)), +} + +pub(super) struct WorkerMessageServer { + // same onconnect callback is used throughtout entire Worker lifetime. + // Keep a reference to make sure it doesn't get dropped. + _onconnect_callback: Closure, + + // keep a MessagePort for each client to send messages over, as well as callback responsible + // for forwarding messages back + clients: Vec, + + // sends events back to the main loop for processing + command_channel: mpsc::Sender, +} + +impl WorkerMessageServer { + pub fn new(command_channel: mpsc::Sender) -> Self { + let near_tx = command_channel.clone(); + let onconnect_callback: Closure = + Closure::new(move |ev: MessageEvent| { + let local_tx = near_tx.clone(); + spawn_local(async move { + let Ok(port) = ev.ports().at(0).dyn_into() else { + error!("received connection event without MessagePort, should not happen"); + return; + }; + + if let Err(e) = local_tx.send(WorkerMessage::NewConnection(port)).await { + error!("command channel inside worker closed, should not happen: {e}"); + } + }) + }); + + let worker_scope = SharedWorker::worker_self(); + worker_scope.set_onconnect(Some(onconnect_callback.as_ref().unchecked_ref())); + + Self { + _onconnect_callback: onconnect_callback, + clients: Vec::with_capacity(1), // we usually expect to have exactly one client + command_channel, + } + } + + pub fn respond_to(&self, client: ClientId, msg: WorkerResponse) { + self.send_response(client, Ok(msg)) + } + + pub fn respond_err_to(&self, client: ClientId, error: WorkerError) { + self.send_response(client, Err(error)) + } + + pub fn add(&mut self, port: MessagePort) { + let client_id = self.clients.len(); + + let near_tx = self.command_channel.clone(); + let client_message_callback: Closure = + Closure::new(move |ev: MessageEvent| { + let local_tx = near_tx.clone(); + spawn_local(async move { + let client_id = ClientId(client_id); + let message_data = ev.data(); + let Ok(command) = from_value::(message_data) else { + warn!("could not deserialize message from client {client_id}"); + return; + }; + + debug!("received command from client {client_id}: {command:#?}"); + if let Err(e) = local_tx + .send(WorkerMessage::Command((command, client_id))) + .await + { + error!("command channel inside worker closed, should not happen: {e}"); + } + }) + }); + port.set_onmessage(Some(client_message_callback.as_ref().unchecked_ref())); + + self.clients.push((port, client_message_callback)); + + info!("SharedWorker ready to receive commands from client {client_id}"); + } + + fn send_response(&self, client: ClientId, msg: Result) { + let wire_message: WireMessage = Some(msg); + + let message = match to_value(&wire_message) { + Ok(jsvalue) => jsvalue, + Err(e) => { + warn!("provided response could not be coverted to JsValue: {e}, sending undefined instead"); + JsValue::UNDEFINED // we need to send something, client is waiting for a response + } + }; + + let Some((client_port, _)) = self.clients.get(client.0) else { + error!("client {client} not found on client list, should not happen"); + return; + }; + + if let Err(e) = client_port.post_message(&message) { + error!("could not post response message to client {client}: {e:?}"); + } + } +} diff --git a/node-wasm/src/worker/commands.rs b/node-wasm/src/worker/commands.rs index 39f23529..4dacb081 100644 --- a/node-wasm/src/worker/commands.rs +++ b/node-wasm/src/worker/commands.rs @@ -77,12 +77,12 @@ pub(crate) enum WorkerResponse { SamplingMetadata(Result>), } -pub(crate) trait CheckableResponse { +pub(crate) trait CheckableResponseExt { type Output; fn check_variant(self) -> Result; } -impl CheckableResponse for Result { +impl CheckableResponseExt for Result { type Output = T; fn check_variant(self) -> Result { @@ -92,150 +92,3 @@ impl CheckableResponse for Result { }) } } - -/* -#[derive(Debug, Serialize, Deserialize)] -enum WorkerErrorNg { - NodeNotRunning, - //NodeAlreadyRunning, -} -*/ - -#[derive(Debug, Serialize, Deserialize)] -pub(crate) enum NodeStartError {} - -/* -macro_rules! define_command_from_impl { - ($common_name:ident, $command_name:ident) => { - impl From<$command_name> for $common_name { - fn from(command: $command_name) -> Self { - $common_name::$command_name(command) - } - } - }; -} - -macro_rules! define_command_type_impl { - ($common_type:ident, $command_name:ident, $output:ty) => { - impl $common_type for $command_name { - type Output = $output; - } - }; -} - -macro_rules! define_response_try_from_impl { - ($common_type:ident, $helper_type:ident, $command_name:ident) => { - impl TryFrom<$common_type> for $helper_type<$command_name> { - type Error = (); - fn try_from(response: $common_type) -> Result { - if let $common_type::$command_name(cmd) = response { - Ok(cmd) - } else { - Err(()) - } - } - } - }; -} - -macro_rules! define_command { - ($command_name:ident -> $output:ty) => { - #[derive(Debug, Serialize, Deserialize)] - pub struct $command_name; - define_command_type_impl!(NodeCommandType, $command_name, $output); - define_command_from_impl!(NodeCommand, $command_name); - define_response_try_from_impl!(NodeResponse, NodeCommandResponse, $command_name); - }; - ($command_name:ident ($($param:ty),+) -> $output:ty) => { - #[derive(Debug, Serialize, Deserialize)] - pub struct $command_name($(pub $param,)+); - define_command_type_impl!(NodeCommandType, $command_name, $output); - define_command_from_impl!(NodeCommand, $command_name); - define_response_try_from_impl!(NodeResponse, NodeCommandResponse, $command_name); - }; - ($command_name:ident {$($param_name:ident : $param_type:ty),+} -> $output:ty) => { - #[derive(Debug, Serialize, Deserialize)] - pub struct $command_name { $(pub $param_name: $param_type,)+} - define_command_type_impl!(NodeCommandType, $command_name, $output); - define_command_from_impl!(NodeCommand, $command_name); - define_response_try_from_impl!(NodeResponse, NodeCommandResponse, $command_name); - }; -} - -macro_rules! define_common_types { - ($($command_name:ident),+ $(,)?) => { - #[allow(clippy::large_enum_variant)] - #[derive(Serialize, Deserialize, Debug)] - pub(crate) enum NodeCommand { - $($command_name($command_name),)+ - } - - #[derive(Debug)] - pub(super) enum NodeCommandWithChannel { - $($command_name(($command_name, CommandResponseChannel<$command_name>)),)+ - } - - #[derive(Serialize, Deserialize, Debug)] - pub(crate) enum NodeResponse { - $($command_name(NodeCommandResponse<$command_name>),)+ - } - - impl NodeCommand { - pub(super) fn add_response_channel(self) -> (NodeCommandWithChannel, - BoxFuture<'static, Result>, // XXX - ) { - match self { - $( - NodeCommand::$command_name(cmd) => { - let (tx, rx) = oneshot::channel(); - ( - NodeCommandWithChannel::$command_name((cmd, tx)), - rx.map_ok(|r| NodeResponse::$command_name( - NodeCommandResponse::<$command_name>(r) - )).boxed() - ) - } - )+ - } - } - } - }; -} - -define_common_types!( - IsRunning, - StartNode, - GetLocalPeerId, - GetSyncerInfo, - GetPeerTrackerInfo, - GetNetworkInfo, - GetConnectedPeers, - SetPeerTrust, - WaitConnected, - GetListeners, - RequestHeader, - GetHeader, - GetHeadersRange, - GetVerifiedHeaders, - LastSeenNetworkHead, - GetSamplingMetadata, -); - -define_command!(IsRunning -> bool); -define_command!(StartNode(WasmNodeConfig) -> Result<()>); -define_command!(GetLocalPeerId -> String); -define_command!(GetSyncerInfo -> SyncingInfo); -define_command!(GetPeerTrackerInfo -> PeerTrackerInfo); -define_command!(GetNetworkInfo -> NetworkInfoSnapshot); -define_command!(GetConnectedPeers -> Vec); -define_command!(SetPeerTrust { peer_id: String, is_trusted: bool } -> ()); -define_command!(WaitConnected { trusted: bool } -> ()); -define_command!(GetListeners -> Vec); -define_command!(RequestHeader(SingleHeaderQuery) -> Result); -define_command!(GetVerifiedHeaders { from: ExtendedHeader, amount: u64 } -> Vec); -define_command!(GetHeadersRange { start_height: Option, end_height: Option } -> Vec); -define_command!(GetHeader(SingleHeaderQuery) -> ExtendedHeader); -define_command!(LastSeenNetworkHead -> Option); -define_command!(GetSamplingMetadata { height: u64 } -> SamplingMetadata); - -*/ diff --git a/node-wasm/src/wrapper/libp2p.rs b/node-wasm/src/wrapper/libp2p.rs index b2d14114..66d98ebc 100644 --- a/node-wasm/src/wrapper/libp2p.rs +++ b/node-wasm/src/wrapper/libp2p.rs @@ -45,71 +45,3 @@ impl From<&SwarmConnectionCounters> for ConnectionCountersSnapshot { } } } - -#[wasm_bindgen] -pub struct NetworkInfo(SwarmNetworkInfo); - -impl From for NetworkInfo { - fn from(info: SwarmNetworkInfo) -> NetworkInfo { - NetworkInfo(info) - } -} - -#[wasm_bindgen] -impl NetworkInfo { - #[wasm_bindgen(getter)] - pub fn num_peers(&self) -> usize { - self.0.num_peers() - } - - pub fn connection_counters(&self) -> ConnectionCounters { - self.0.connection_counters().clone().into() - } -} - -#[wasm_bindgen] -pub struct ConnectionCounters(SwarmConnectionCounters); - -impl From for ConnectionCounters { - fn from(info: SwarmConnectionCounters) -> ConnectionCounters { - ConnectionCounters(info) - } -} - -#[wasm_bindgen] -impl ConnectionCounters { - #[wasm_bindgen(getter)] - pub fn num_connections(&self) -> u32 { - self.0.num_connections() - } - - #[wasm_bindgen(getter)] - pub fn num_pending(&self) -> u32 { - self.0.num_pending() - } - - #[wasm_bindgen(getter)] - pub fn num_pending_incoming(&self) -> u32 { - self.0.num_pending_incoming() - } - - #[wasm_bindgen(getter)] - pub fn num_pending_outgoing(&self) -> u32 { - self.0.num_pending_outgoing() - } - - #[wasm_bindgen(getter)] - pub fn num_established_incoming(&self) -> u32 { - self.0.num_established_incoming() - } - - #[wasm_bindgen(getter)] - pub fn num_established_outgoing(&self) -> u32 { - self.0.num_established_outgoing() - } - - #[wasm_bindgen(getter)] - pub fn num_established(&self) -> u32 { - self.0.num_established() - } -} From 49a3a28702e874e53fa850700d505a193ce36c16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Florkiewicz?= Date: Fri, 19 Apr 2024 21:17:24 +0200 Subject: [PATCH 14/35] finishing touchees --- Cargo.lock | 2 + node-wasm/Cargo.toml | 4 +- node-wasm/src/node.rs | 17 +++--- node-wasm/src/utils.rs | 28 ++++----- node-wasm/src/worker.rs | 132 ++++++++++++++++++++++++++++++---------- 5 files changed, 127 insertions(+), 56 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fe485ce6..5de2c561 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2883,6 +2883,8 @@ name = "lumina-node-wasm" version = "0.1.0" dependencies = [ "anyhow", + "blockstore", + "celestia-tendermint", "celestia-types", "console_error_panic_hook", "enum-as-inner", diff --git a/node-wasm/Cargo.toml b/node-wasm/Cargo.toml index d2162fc3..3300c218 100644 --- a/node-wasm/Cargo.toml +++ b/node-wasm/Cargo.toml @@ -26,6 +26,8 @@ crate-type = ["cdylib", "rlib"] celestia-types = { workspace = true } libp2p = { workspace = true, features = ["serde"] } lumina-node = { workspace = true } +blockstore = { workspace = true } +celestia-tendermint = { workspace = true } anyhow = "1.0.71" console_error_panic_hook = "0.1.7" @@ -44,5 +46,5 @@ tracing-subscriber = { version = "0.3.18", features = ["time"] } tracing-web = "0.1.2" wasm-bindgen = "0.2.88" wasm-bindgen-futures = "0.4.37" -web-sys = { version = "0.3.69", features = ["BroadcastChannel", "MessageEvent", "Worker", "WorkerOptions", "WorkerType", "SharedWorker", "MessagePort", "SharedWorkerGlobalScope"]} +web-sys = { version = "0.3.69", features = ["BroadcastChannel", "MessageEvent", "Worker", "WorkerOptions", "WorkerType", "SharedWorker", "MessagePort", "SharedWorkerGlobalScope", "Blob", "BlobPropertyBag", "Url"]} enum-as-inner = "0.6" diff --git a/node-wasm/src/node.rs b/node-wasm/src/node.rs index db658465..42a9bf80 100644 --- a/node-wasm/src/node.rs +++ b/node-wasm/src/node.rs @@ -15,9 +15,9 @@ use lumina_node::network::{canonical_network_bootnodes, network_genesis, network use lumina_node::node::NodeConfig; use lumina_node::store::IndexedDbStore; -use crate::utils::{js_value_from_display, JsContext, Network}; +use crate::utils::{js_value_from_display, Network}; use crate::worker::commands::{CheckableResponseExt, NodeCommand, SingleHeaderQuery}; -use crate::worker::WorkerClient; +use crate::worker::{WorkerClient, WorkerError}; use crate::wrapper::libp2p::NetworkInfoSnapshot; use crate::Result; @@ -51,6 +51,7 @@ impl NodeDriver { /// be running be running, before `NodeDriver::start` call. #[wasm_bindgen(constructor)] pub async fn new() -> Result { + //let worker = spawn_worker(LUMINA_SHARED_WORKER_NAME, "/wasm/lumina_node_wasm.js")?; let mut opts = WorkerOptions::new(); opts.type_(WorkerType::Module); opts.name(LUMINA_SHARED_WORKER_NAME); @@ -58,7 +59,7 @@ impl NodeDriver { .map_err(|e| JsError::new(&format!("could not create SharedWorker: {e:?}")))?; let onerror_callback: Closure = Closure::new(|ev: MessageEvent| { - error!("received error from SharedWorker: {ev:?}"); + error!("received error from SharedWorker: {:?}", ev.to_string()); }); worker.set_onerror(Some(onerror_callback.as_ref().unchecked_ref())); @@ -313,14 +314,10 @@ impl WasmNodeConfig { pub(crate) async fn into_node_config( self, - ) -> Result> { + ) -> Result, WorkerError> { let network_id = network_id(self.network.into()); - let store = IndexedDbStore::new(network_id) - .await - .js_context("Failed to open the store")?; - let blockstore = IndexedDbBlockstore::new(&format!("{network_id}-blockstore")) - .await - .js_context("Failed to open the blockstore")?; + let store = IndexedDbStore::new(network_id).await?; + let blockstore = IndexedDbBlockstore::new(&format!("{network_id}-blockstore")).await?; let p2p_local_keypair = Keypair::generate_ed25519(); diff --git a/node-wasm/src/utils.rs b/node-wasm/src/utils.rs index 322c3331..e147b0f9 100644 --- a/node-wasm/src/utils.rs +++ b/node-wasm/src/utils.rs @@ -12,20 +12,6 @@ use web_sys::{SharedWorker, SharedWorkerGlobalScope}; use lumina_node::network; -pub(crate) trait WorkerSelf { - type GlobalScope; - - fn worker_self() -> Self::GlobalScope; -} - -impl WorkerSelf for SharedWorker { - type GlobalScope = SharedWorkerGlobalScope; - - fn worker_self() -> Self::GlobalScope { - JsValue::from(js_sys::global()).into() - } -} - /// Supported Celestia networks. #[wasm_bindgen] #[derive(PartialEq, Eq, Clone, Copy, Serialize_repr, Deserialize_repr, Debug)] @@ -118,3 +104,17 @@ where }) } } + +pub(crate) trait WorkerSelf { + type GlobalScope; + + fn worker_self() -> Self::GlobalScope; +} + +impl WorkerSelf for SharedWorker { + type GlobalScope = SharedWorkerGlobalScope; + + fn worker_self() -> Self::GlobalScope { + JsValue::from(js_sys::global()).into() + } +} diff --git a/node-wasm/src/worker.rs b/node-wasm/src/worker.rs index 7171c627..2d33ef6e 100644 --- a/node-wasm/src/worker.rs +++ b/node-wasm/src/worker.rs @@ -8,9 +8,11 @@ use tracing::{error, info, warn}; use wasm_bindgen::prelude::*; use web_sys::MessagePort; +use celestia_tendermint::error::Error as TendermintError; use celestia_types::ExtendedHeader; +use libp2p::multiaddr::Error as MultiaddrError; use lumina_node::node::{Node, NodeError}; -use lumina_node::store::{IndexedDbStore, SamplingMetadata, Store}; +use lumina_node::store::{IndexedDbStore, SamplingMetadata, Store, StoreError}; use lumina_node::syncer::SyncingInfo; use crate::node::WasmNodeConfig; @@ -30,40 +32,28 @@ type Result = std::result::Result; #[derive(Debug, Serialize, Deserialize, Error)] pub enum WorkerError { - /* - #[error("Command response channel has been dropped before response could be sent, should not happen")] - SharedWorkerChannelResponseChannelDropped, - #[error("Command channel to lumina worker closed, should not happen")] - SharedWorkerCommandChannelClosed, - #[error("Channel expected different response type, should not happen")] - SharedWorkerChannelInvalidType, - #[error("Lumina is already running, ignoring StartNode command")] - SharedWorkerAlreadyRunning, - #[error("Worker is still handling previous command")] - WorkerBusy, - */ - #[error("Node hasn't been started yet")] + #[error("node hasn't been started yet")] NodeNotRunning, - #[error("Node has already been started")] + #[error("node has already been started")] NodeAlreadyRunning, - + #[error("could not create blockstore: {0}")] + BlockstoreCrationFailed(String), + #[error("could not create header store: {0}")] + StoreCrationFailed(String), + #[error("could not parse genesis hash: {0}")] + GenesisHashInvalid(String), + #[error("could not parse bootstrap node multiaddr: {0}")] + BootstrapNodeMultiaddrInvalid(String), #[error("node error: {0}")] NodeError(String), - - #[error("could not send command to the worker: {0}")] - CommandSendingFailed(String), - - #[error("respose to command did not match expected type")] + #[error("respose to command did not match expected type, should not happen")] InvalidResponseType, - #[error("response message could not be serialised, should not happen: {0}")] CouldNotSerialiseCommand(String), #[error("response message could not be sent: {0}")] CouldNotSendCommand(String), - #[error("Received empty worker response, should not happen")] EmptyWorkerResponse, - #[error("Response channel to worker closed, should not happen")] ResponseChannelDropped, } @@ -74,13 +64,36 @@ impl From for WorkerError { } } +impl From for WorkerError { + fn from(error: blockstore::Error) -> Self { + WorkerError::BlockstoreCrationFailed(error.to_string()) + } +} + +impl From for WorkerError { + fn from(error: StoreError) -> Self { + WorkerError::StoreCrationFailed(error.to_string()) + } +} + +impl From for WorkerError { + fn from(error: TendermintError) -> Self { + WorkerError::GenesisHashInvalid(error.to_string()) + } +} + +impl From for WorkerError { + fn from(error: MultiaddrError) -> Self { + WorkerError::BootstrapNodeMultiaddrInvalid(error.to_string()) + } +} struct NodeWorker { node: Node, } impl NodeWorker { - async fn new(config: WasmNodeConfig) -> Self { - let config = config.into_node_config().await.ok().unwrap(); + async fn new(config: WasmNodeConfig) -> Result { + let config = config.into_node_config().await?; if let Ok(store_height) = config.store.head_height().await { info!("Initialised store with head height: {store_height}"); @@ -88,9 +101,9 @@ impl NodeWorker { info!("Initialised new empty store"); } - let node = Node::new(config).await.ok().unwrap(); + let node = Node::new(config).await?; - Self { node } + Ok(Self { node }) } async fn get_syncer_info(&mut self) -> Result { @@ -241,9 +254,17 @@ pub async fn run_worker(queued_connections: Vec) { message_server.respond_to(client_id, WorkerResponse::IsRunning(false)); } NodeCommand::StartNode(config) => { - worker = Some(NodeWorker::new(config).await); - message_server - .respond_to(client_id, WorkerResponse::NodeStarted(Ok(()))); + match NodeWorker::new(config).await { + Ok(node) => { + worker = Some(node); + message_server + .respond_to(client_id, WorkerResponse::NodeStarted(Ok(()))); + } + Err(e) => { + message_server + .respond_to(client_id, WorkerResponse::NodeStarted(Err(e))); + } + }; } _ => { warn!("Worker not running"); @@ -261,3 +282,52 @@ pub async fn run_worker(queued_connections: Vec) { error!("Channel to WorkerMessageServer closed, should not happen"); } + +/* +/// SharedWorker can only be spawned from an [`URL`]. To eliminate a need to host a js shim +/// which calls into our Rust wasm code under specific path, we encode its entire contents +/// as a [`Blob`] which is then passed as an URL. +/// +/// [`URL`]: https://developer.mozilla.org/en-US/docs/Web/API/URL +/// ['Blob']: https://developer.mozilla.org/en-US/docs/Web/API/Blob + +pub(crate) fn spawn_worker(name: &str, wasm_url: &str) -> Result { + let script = format!( + r#" +Error.stackTraceLimit = 99; +import init, {{ run_worker }} from "/wasm/lumina_node_wasm.js"; + +let queued = []; +onconnect = (event) => {{ + console.log("Queued connection", event); + queued.push(event.ports[0]); +}} + +await init(); +await run_worker(queued); +"# + ); + + info!("js shim: {script}"); + + let array = Array::new(); + array.push(&script.into()); + let blob = Blob::new_with_str_sequence_and_options( + &array, + BlobPropertyBag::new().type_("application/javascript"), + ) + .map_err(|e| JsError::new(&format!("could not create SharedWorker Blob: {e:?}")))?; + + let url = Url::create_object_url_with_blob(&blob) + .map_err(|e| JsError::new(&format!("could not create SharedWorker Url: {e:?}")))?; + + let mut opts = WorkerOptions::new(); + opts.type_(WorkerType::Module); + opts.name(name); + + let worker = SharedWorker::new_with_worker_options(&url, &opts) + .map_err(|e| JsError::new(&format!("could not create SharedWorker Url: {e:?}")))?; + + Ok(worker) +} +*/ From 58496b031632d852ab78acd11abf947f63562094 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Florkiewicz?= Date: Tue, 23 Apr 2024 21:32:51 +0200 Subject: [PATCH 15/35] pass errors through correctly, fix url blob --- cli/static/run_node.js | 11 +--- node-wasm/src/node.rs | 47 +++++++--------- node-wasm/src/utils.rs | 64 +++++++++++++++++++++ node-wasm/src/worker.rs | 97 +++++++++++++++++++++++--------- node-wasm/src/worker/commands.rs | 15 +++-- 5 files changed, 164 insertions(+), 70 deletions(-) diff --git a/cli/static/run_node.js b/cli/static/run_node.js index 9ac99499..4eb91c3d 100644 --- a/cli/static/run_node.js +++ b/cli/static/run_node.js @@ -94,15 +94,6 @@ function bind_config(data) { }); } -/* -async function start_node(config) { - //window.node = await new Node(config); - - document.getElementById("peer-id").innerText = await window.node.local_peer_id(); - document.querySelectorAll(".status").forEach(elem => elem.style.visibility = "visible"); -} -*/ - async function main(document, window) { await init(); @@ -124,7 +115,7 @@ async function main(document, window) { document.querySelectorAll(".status").forEach(elem => elem.style.visibility = "visible"); }); - //setInterval(async () => await show_stats(window.driver), 1000) + setInterval(async () => await show_stats(window.driver), 1000) } await main(document, window); diff --git a/node-wasm/src/node.rs b/node-wasm/src/node.rs index 42a9bf80..70aa19fd 100644 --- a/node-wasm/src/node.rs +++ b/node-wasm/src/node.rs @@ -5,10 +5,10 @@ use js_sys::Array; use libp2p::identity::Keypair; use libp2p::multiaddr::Protocol; use serde::{Deserialize, Serialize}; -use serde_wasm_bindgen::{from_value, to_value}; +use serde_wasm_bindgen::to_value; use tracing::error; use wasm_bindgen::prelude::*; -use web_sys::{MessageEvent, SharedWorker, WorkerOptions, WorkerType}; +use web_sys::{MessageEvent, SharedWorker}; use lumina_node::blockstore::IndexedDbBlockstore; use lumina_node::network::{canonical_network_bootnodes, network_genesis, network_id}; @@ -17,7 +17,7 @@ use lumina_node::store::IndexedDbStore; use crate::utils::{js_value_from_display, Network}; use crate::worker::commands::{CheckableResponseExt, NodeCommand, SingleHeaderQuery}; -use crate::worker::{WorkerClient, WorkerError}; +use crate::worker::{spawn_worker, WorkerClient, WorkerError}; use crate::wrapper::libp2p::NetworkInfoSnapshot; use crate::Result; @@ -51,12 +51,14 @@ impl NodeDriver { /// be running be running, before `NodeDriver::start` call. #[wasm_bindgen(constructor)] pub async fn new() -> Result { - //let worker = spawn_worker(LUMINA_SHARED_WORKER_NAME, "/wasm/lumina_node_wasm.js")?; - let mut opts = WorkerOptions::new(); - opts.type_(WorkerType::Module); - opts.name(LUMINA_SHARED_WORKER_NAME); - let worker = SharedWorker::new_with_worker_options("/js/worker.js", &opts) - .map_err(|e| JsError::new(&format!("could not create SharedWorker: {e:?}")))?; + let worker = spawn_worker(LUMINA_SHARED_WORKER_NAME, "/wasm/lumina_node_wasm.js")?; + /* + let mut opts = WorkerOptions::new(); + opts.type_(WorkerType::Module); + opts.name(LUMINA_SHARED_WORKER_NAME); + let worker = SharedWorker::new_with_worker_options("/js/worker.js", &opts) + .map_err(|e| JsError::new(&format!("could not create SharedWorker: {e:?}")))?; + */ let onerror_callback: Closure = Closure::new(|ev: MessageEvent| { error!("received error from SharedWorker: {:?}", ev.to_string()); @@ -173,7 +175,7 @@ impl NodeDriver { let response = self.channel.exec(command).await?; let header = response.into_header().check_variant()?; - Ok(to_value(&header?)?) + Ok(header.to_result()?) } /// Request a header for the block with a given hash from the network. @@ -182,7 +184,7 @@ impl NodeDriver { let response = self.channel.exec(command).await?; let header = response.into_header().check_variant()?; - Ok(to_value(&header?)?) + Ok(header.to_result()?) } /// Request a header for the block with a given height from the network. @@ -191,7 +193,7 @@ impl NodeDriver { let response = self.channel.exec(command).await?; let header = response.into_header().check_variant()?; - Ok(to_value(&header?)?) + Ok(header.to_result()?) } /// Request headers in range (from, from + amount] from the network. @@ -203,18 +205,13 @@ impl NodeDriver { amount: u64, ) -> Result { let command = NodeCommand::GetVerifiedHeaders { - from: from_value(from_header)?, + from: from_header, amount, }; let response = self.channel.exec(command).await?; let headers = response.into_headers().check_variant()?; - let result = headers? - .iter() - .map(|h| to_value(&h)) - .collect::>(); - - Ok(result?) + Ok(headers.to_result()?) } /// Get current header syncing info. @@ -232,7 +229,7 @@ impl NodeDriver { let response = self.channel.exec(command).await?; let header = response.into_last_seen_network_head().check_variant()?; - Ok(to_value(&header)?) + Ok(header) } /// Get the latest locally synced header. @@ -241,7 +238,7 @@ impl NodeDriver { let response = self.channel.exec(command).await?; let header = response.into_header().check_variant()?; - Ok(to_value(&header?)?) + Ok(header.to_result()?) } /// Get a synced header for the block with a given hash. @@ -250,7 +247,7 @@ impl NodeDriver { let response = self.channel.exec(command).await?; let header = response.into_header().check_variant()?; - Ok(to_value(&header?)?) + Ok(header.to_result()?) } /// Get a synced header for the block with a given height. @@ -259,7 +256,7 @@ impl NodeDriver { let response = self.channel.exec(command).await?; let header = response.into_header().check_variant()?; - Ok(to_value(&header?)?) + Ok(header.to_result()?) } /// Get synced headers from the given heights range. @@ -283,9 +280,7 @@ impl NodeDriver { let response = self.channel.exec(command).await?; let headers = response.into_headers().check_variant()?; - let result = headers?.iter().map(|h| to_value(&h).unwrap()).collect(); - - Ok(result) + Ok(headers.to_result()?) } /// Get data sampling metadata of an already sampled height. diff --git a/node-wasm/src/utils.rs b/node-wasm/src/utils.rs index e147b0f9..28dbcf2d 100644 --- a/node-wasm/src/utils.rs +++ b/node-wasm/src/utils.rs @@ -1,6 +1,8 @@ //! Various utilities for interacting with node from wasm. use std::fmt::{self, Debug}; +use serde::de::DeserializeOwned; +use serde::{Deserialize, Serialize}; use serde_repr::{Deserialize_repr, Serialize_repr}; use tracing_subscriber::filter::LevelFilter; use tracing_subscriber::fmt::format::Pretty; @@ -118,3 +120,65 @@ impl WorkerSelf for SharedWorker { JsValue::from(js_sys::global()).into() } } + +#[derive(Serialize, Deserialize)] +pub(crate) enum JsResult +where + T: JsCast, + //E: Serialize + DeserializeOwned, +{ + #[serde(with = "serde_wasm_bindgen::preserve")] + Ok(T), + Err(E), +} + +// once try_trait_v2 is stabilised, this can go +impl<'de, T, E> JsResult +where + T: JsCast, + E: Serialize + DeserializeOwned, +{ + pub fn to_result(self) -> Result { + self.into() + } +} + +impl<'de, T, E> From> for JsResult +where + T: JsCast, + E: Serialize + DeserializeOwned, +{ + fn from(result: Result) -> Self { + match result { + Ok(v) => JsResult::Ok(v), + Err(e) => JsResult::Err(e), + } + } +} + +impl From> for Result +where + T: JsCast, + E: Serialize + DeserializeOwned, +{ + fn from(result: JsResult) -> Self { + match result { + JsResult::Ok(v) => Ok(v), + JsResult::Err(e) => Err(e), + } + } +} + +impl Debug for JsResult +where + T: JsCast, + E: Serialize + DeserializeOwned, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + JsResult::Ok(_) => f.debug_tuple("JsResult::Ok"), + JsResult::Err(_) => f.debug_tuple("JsResult::Err"), + } + .finish() + } +} diff --git a/node-wasm/src/worker.rs b/node-wasm/src/worker.rs index 2d33ef6e..e482a7a2 100644 --- a/node-wasm/src/worker.rs +++ b/node-wasm/src/worker.rs @@ -1,15 +1,16 @@ use std::fmt::Debug; +use js_sys::Array; use libp2p::{Multiaddr, PeerId}; use serde::{Deserialize, Serialize}; +use serde_wasm_bindgen::{from_value, to_value}; use thiserror::Error; use tokio::sync::mpsc; use tracing::{error, info, warn}; use wasm_bindgen::prelude::*; -use web_sys::MessagePort; +use web_sys::{Blob, BlobPropertyBag, MessagePort, SharedWorker, Url, WorkerOptions, WorkerType}; use celestia_tendermint::error::Error as TendermintError; -use celestia_types::ExtendedHeader; use libp2p::multiaddr::Error as MultiaddrError; use lumina_node::node::{Node, NodeError}; use lumina_node::store::{IndexedDbStore, SamplingMetadata, Store, StoreError}; @@ -48,8 +49,12 @@ pub enum WorkerError { NodeError(String), #[error("respose to command did not match expected type, should not happen")] InvalidResponseType, - #[error("response message could not be serialised, should not happen: {0}")] + #[error("Value could not be serialized, should not happen: {0}")] + SerdeError(String), + #[error("command message could not be serialised, should not happen: {0}")] CouldNotSerialiseCommand(String), + #[error("command response could not be serialised, should not happen: {0}")] + CouldNotSerialiseResponseValue(String), #[error("response message could not be sent: {0}")] CouldNotSendCommand(String), #[error("Received empty worker response, should not happen")] @@ -87,10 +92,20 @@ impl From for WorkerError { WorkerError::BootstrapNodeMultiaddrInvalid(error.to_string()) } } + +impl From for WorkerError { + fn from(error: serde_wasm_bindgen::Error) -> Self { + WorkerError::SerdeError(error.to_string()) + } +} struct NodeWorker { node: Node, } +fn to_jsvalue_or_undefined(value: &T) -> JsValue { + to_value(value).unwrap() +} + impl NodeWorker { async fn new(config: WasmNodeConfig) -> Result { let config = config.into_node_config().await?; @@ -141,41 +156,61 @@ impl NodeWorker { Ok(()) } - async fn request_header(&mut self, query: SingleHeaderQuery) -> Result { - Ok(match query { + async fn request_header(&mut self, query: SingleHeaderQuery) -> Result { + let header = match query { SingleHeaderQuery::Head => self.node.request_head_header().await, SingleHeaderQuery::ByHash(hash) => self.node.request_header_by_hash(&hash).await, SingleHeaderQuery::ByHeight(height) => self.node.request_header_by_height(height).await, - }?) + }?; + Ok(to_value(&header) + .map_err(|e| WorkerError::CouldNotSerialiseResponseValue(e.to_string()))?) } - async fn get_header(&mut self, query: SingleHeaderQuery) -> Result { - Ok(match query { + async fn get_header(&mut self, query: SingleHeaderQuery) -> Result { + let header = match query { SingleHeaderQuery::Head => self.node.get_local_head_header().await, SingleHeaderQuery::ByHash(hash) => self.node.get_header_by_hash(&hash).await, SingleHeaderQuery::ByHeight(height) => self.node.get_header_by_height(height).await, - }?) + }?; + Ok(to_value(&header) + .map_err(|e| WorkerError::CouldNotSerialiseResponseValue(e.to_string()))?) } - async fn get_verified_headers( - &mut self, - from: &ExtendedHeader, - amount: u64, - ) -> Result> { - Ok(self.node.request_verified_headers(from, amount).await?) + async fn get_verified_headers(&mut self, from: JsValue, amount: u64) -> Result { + let verified_headers = self + .node + .request_verified_headers(&from_value(from)?, amount) + .await?; + Ok(verified_headers + .iter() + .map(|h| to_value(&h)) + .collect::>()?) } async fn get_headers_range( &mut self, start_height: Option, end_height: Option, - ) -> Result> { - Ok(match (start_height, end_height) { + ) -> Result { + let headers = match (start_height, end_height) { (None, None) => self.node.get_headers(..).await, (Some(start), None) => self.node.get_headers(start..).await, (None, Some(end)) => self.node.get_headers(..=end).await, (Some(start), Some(end)) => self.node.get_headers(start..=end).await, - }?) + }?; + + Ok(headers + .iter() + .map(|h| to_value(&h)) + .collect::>()?) + } + + async fn get_last_seen_network_head(&mut self) -> JsValue { + self.node + .get_network_head_header() + .as_ref() + .map(to_jsvalue_or_undefined) + .unwrap_or(JsValue::UNDEFINED) } async fn get_sampling_metadata(&mut self, height: u64) -> Result> { @@ -211,18 +246,24 @@ impl NodeWorker { } NodeCommand::GetListeners => WorkerResponse::Listeners(self.get_listeners().await), NodeCommand::RequestHeader(query) => { - WorkerResponse::Header(self.request_header(query).await) + WorkerResponse::Header(self.request_header(query).await.into()) + } + NodeCommand::GetHeader(query) => { + WorkerResponse::Header(self.get_header(query).await.into()) } - NodeCommand::GetHeader(query) => WorkerResponse::Header(self.get_header(query).await), NodeCommand::GetVerifiedHeaders { from, amount } => { - WorkerResponse::Headers(self.get_verified_headers(&from, amount).await) + WorkerResponse::Headers(self.get_verified_headers(from, amount).await.into()) } NodeCommand::GetHeadersRange { start_height, end_height, - } => WorkerResponse::Headers(self.get_headers_range(start_height, end_height).await), + } => WorkerResponse::Headers( + self.get_headers_range(start_height, end_height) + .await + .into(), + ), NodeCommand::LastSeenNetworkHead => { - WorkerResponse::LastSeenNetworkHead(self.node.get_network_head_header()) + WorkerResponse::LastSeenNetworkHead(self.get_last_seen_network_head().await) } NodeCommand::GetSamplingMetadata { height } => { WorkerResponse::SamplingMetadata(self.get_sampling_metadata(height).await) @@ -233,6 +274,7 @@ impl NodeWorker { #[wasm_bindgen] pub async fn run_worker(queued_connections: Vec) { + info!("Entered run_worker"); let (tx, mut rx) = mpsc::channel(WORKER_MESSAGE_SERVER_INCOMING_QUEUE_LENGTH); let mut message_server = WorkerMessageServer::new(tx.clone()); @@ -240,6 +282,7 @@ pub async fn run_worker(queued_connections: Vec) { message_server.add(connection); } + info!("Entering SharedWorker message loop"); let mut worker = None; while let Some(message) = rx.recv().await { match message { @@ -283,7 +326,6 @@ pub async fn run_worker(queued_connections: Vec) { error!("Channel to WorkerMessageServer closed, should not happen"); } -/* /// SharedWorker can only be spawned from an [`URL`]. To eliminate a need to host a js shim /// which calls into our Rust wasm code under specific path, we encode its entire contents /// as a [`Blob`] which is then passed as an URL. @@ -295,7 +337,6 @@ pub(crate) fn spawn_worker(name: &str, wasm_url: &str) -> Result {{ @@ -303,8 +344,9 @@ onconnect = (event) => {{ queued.push(event.ports[0]); }} -await init(); -await run_worker(queued); +self.lumina = await import(self.location.origin + '{wasm_url}'); +await self.lumina.default(); +await self.lumina.run_worker(queued); "# ); @@ -330,4 +372,3 @@ await run_worker(queued); Ok(worker) } -*/ diff --git a/node-wasm/src/worker/commands.rs b/node-wasm/src/worker/commands.rs index 4dacb081..47623ed6 100644 --- a/node-wasm/src/worker/commands.rs +++ b/node-wasm/src/worker/commands.rs @@ -1,19 +1,20 @@ use std::fmt::Debug; use enum_as_inner::EnumAsInner; -//use futures::future::{BoxFuture, FutureExt, TryFutureExt}; +use js_sys::Array; use libp2p::Multiaddr; use libp2p::PeerId; use serde::{Deserialize, Serialize}; use tracing::error; +use wasm_bindgen::JsValue; use celestia_types::hash::Hash; -use celestia_types::ExtendedHeader; use lumina_node::peer_tracker::PeerTrackerInfo; use lumina_node::store::SamplingMetadata; use lumina_node::syncer::SyncingInfo; use crate::node::WasmNodeConfig; +use crate::utils::JsResult; use crate::worker::Result; use crate::worker::WorkerError; use crate::wrapper::libp2p::NetworkInfoSnapshot; @@ -38,7 +39,8 @@ pub(crate) enum NodeCommand { GetListeners, RequestHeader(SingleHeaderQuery), GetVerifiedHeaders { - from: ExtendedHeader, + #[serde(with = "serde_wasm_bindgen::preserve")] + from: JsValue, amount: u64, }, GetHeadersRange { @@ -71,9 +73,10 @@ pub(crate) enum WorkerResponse { SetPeerTrust(Result<()>), Connected(Result<()>), Listeners(Result>), - Header(Result), - Headers(Result>), - LastSeenNetworkHead(Option), + Header(JsResult), + Headers(JsResult), + #[serde(with = "serde_wasm_bindgen::preserve")] + LastSeenNetworkHead(JsValue), SamplingMetadata(Result>), } From fb8c5d6835a039f74d309e320850ead545b82310 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Florkiewicz?= Date: Tue, 23 Apr 2024 21:45:23 +0200 Subject: [PATCH 16/35] to all the clippies I loved --- node-wasm/src/node.rs | 16 ++++++++-------- node-wasm/src/utils.rs | 6 +++--- node-wasm/src/worker.rs | 6 ++---- node-wasm/src/worker/channel.rs | 1 - node-wasm/src/worker/commands.rs | 1 - 5 files changed, 13 insertions(+), 17 deletions(-) diff --git a/node-wasm/src/node.rs b/node-wasm/src/node.rs index 70aa19fd..7a34f904 100644 --- a/node-wasm/src/node.rs +++ b/node-wasm/src/node.rs @@ -175,7 +175,7 @@ impl NodeDriver { let response = self.channel.exec(command).await?; let header = response.into_header().check_variant()?; - Ok(header.to_result()?) + Ok(header.into_result()?) } /// Request a header for the block with a given hash from the network. @@ -184,7 +184,7 @@ impl NodeDriver { let response = self.channel.exec(command).await?; let header = response.into_header().check_variant()?; - Ok(header.to_result()?) + Ok(header.into_result()?) } /// Request a header for the block with a given height from the network. @@ -193,7 +193,7 @@ impl NodeDriver { let response = self.channel.exec(command).await?; let header = response.into_header().check_variant()?; - Ok(header.to_result()?) + Ok(header.into_result()?) } /// Request headers in range (from, from + amount] from the network. @@ -211,7 +211,7 @@ impl NodeDriver { let response = self.channel.exec(command).await?; let headers = response.into_headers().check_variant()?; - Ok(headers.to_result()?) + Ok(headers.into_result()?) } /// Get current header syncing info. @@ -238,7 +238,7 @@ impl NodeDriver { let response = self.channel.exec(command).await?; let header = response.into_header().check_variant()?; - Ok(header.to_result()?) + Ok(header.into_result()?) } /// Get a synced header for the block with a given hash. @@ -247,7 +247,7 @@ impl NodeDriver { let response = self.channel.exec(command).await?; let header = response.into_header().check_variant()?; - Ok(header.to_result()?) + Ok(header.into_result()?) } /// Get a synced header for the block with a given height. @@ -256,7 +256,7 @@ impl NodeDriver { let response = self.channel.exec(command).await?; let header = response.into_header().check_variant()?; - Ok(header.to_result()?) + Ok(header.into_result()?) } /// Get synced headers from the given heights range. @@ -280,7 +280,7 @@ impl NodeDriver { let response = self.channel.exec(command).await?; let headers = response.into_headers().check_variant()?; - Ok(headers.to_result()?) + Ok(headers.into_result()?) } /// Get data sampling metadata of an already sampled height. diff --git a/node-wasm/src/utils.rs b/node-wasm/src/utils.rs index 28dbcf2d..d809e1b8 100644 --- a/node-wasm/src/utils.rs +++ b/node-wasm/src/utils.rs @@ -133,17 +133,17 @@ where } // once try_trait_v2 is stabilised, this can go -impl<'de, T, E> JsResult +impl JsResult where T: JsCast, E: Serialize + DeserializeOwned, { - pub fn to_result(self) -> Result { + pub fn into_result(self) -> Result { self.into() } } -impl<'de, T, E> From> for JsResult +impl From> for JsResult where T: JsCast, E: Serialize + DeserializeOwned, diff --git a/node-wasm/src/worker.rs b/node-wasm/src/worker.rs index e482a7a2..aa99f557 100644 --- a/node-wasm/src/worker.rs +++ b/node-wasm/src/worker.rs @@ -162,8 +162,7 @@ impl NodeWorker { SingleHeaderQuery::ByHash(hash) => self.node.request_header_by_hash(&hash).await, SingleHeaderQuery::ByHeight(height) => self.node.request_header_by_height(height).await, }?; - Ok(to_value(&header) - .map_err(|e| WorkerError::CouldNotSerialiseResponseValue(e.to_string()))?) + to_value(&header).map_err(|e| WorkerError::CouldNotSerialiseResponseValue(e.to_string())) } async fn get_header(&mut self, query: SingleHeaderQuery) -> Result { @@ -172,8 +171,7 @@ impl NodeWorker { SingleHeaderQuery::ByHash(hash) => self.node.get_header_by_hash(&hash).await, SingleHeaderQuery::ByHeight(height) => self.node.get_header_by_height(height).await, }?; - Ok(to_value(&header) - .map_err(|e| WorkerError::CouldNotSerialiseResponseValue(e.to_string()))?) + to_value(&header).map_err(|e| WorkerError::CouldNotSerialiseResponseValue(e.to_string())) } async fn get_verified_headers(&mut self, from: JsValue, amount: u64) -> Result { diff --git a/node-wasm/src/worker/channel.rs b/node-wasm/src/worker/channel.rs index 451f6190..08937d37 100644 --- a/node-wasm/src/worker/channel.rs +++ b/node-wasm/src/worker/channel.rs @@ -83,7 +83,6 @@ impl fmt::Display for ClientId { } } -#[allow(clippy::large_enum_variant)] pub(super) enum WorkerMessage { NewConnection(MessagePort), Command((NodeCommand, ClientId)), diff --git a/node-wasm/src/worker/commands.rs b/node-wasm/src/worker/commands.rs index 47623ed6..81808170 100644 --- a/node-wasm/src/worker/commands.rs +++ b/node-wasm/src/worker/commands.rs @@ -19,7 +19,6 @@ use crate::worker::Result; use crate::worker::WorkerError; use crate::wrapper::libp2p::NetworkInfoSnapshot; -#[allow(clippy::large_enum_variant)] #[derive(Debug, Serialize, Deserialize)] pub(crate) enum NodeCommand { IsRunning, From a74ba16825c710b7beabfd4e39b355d83676b3d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Florkiewicz?= Date: Wed, 8 May 2024 08:43:15 +0200 Subject: [PATCH 17/35] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Yiannis Marangos Co-authored-by: Maciej ZwoliĊ„ski Signed-off-by: MikoĊ‚aj Florkiewicz --- node-wasm/src/utils.rs | 1 - node-wasm/src/worker.rs | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/node-wasm/src/utils.rs b/node-wasm/src/utils.rs index d809e1b8..cda846fa 100644 --- a/node-wasm/src/utils.rs +++ b/node-wasm/src/utils.rs @@ -125,7 +125,6 @@ impl WorkerSelf for SharedWorker { pub(crate) enum JsResult where T: JsCast, - //E: Serialize + DeserializeOwned, { #[serde(with = "serde_wasm_bindgen::preserve")] Ok(T), diff --git a/node-wasm/src/worker.rs b/node-wasm/src/worker.rs index aa99f557..e2343aad 100644 --- a/node-wasm/src/worker.rs +++ b/node-wasm/src/worker.rs @@ -98,6 +98,7 @@ impl From for WorkerError { WorkerError::SerdeError(error.to_string()) } } + struct NodeWorker { node: Node, } @@ -149,9 +150,9 @@ impl NodeWorker { async fn wait_connected(&mut self, trusted: bool) -> Result<()> { if trusted { - self.node.wait_connected().await?; - } else { self.node.wait_connected_trusted().await?; + } else { + self.node.wait_connected().await?; } Ok(()) } @@ -330,7 +331,6 @@ pub async fn run_worker(queued_connections: Vec) { /// /// [`URL`]: https://developer.mozilla.org/en-US/docs/Web/API/URL /// ['Blob']: https://developer.mozilla.org/en-US/docs/Web/API/Blob - pub(crate) fn spawn_worker(name: &str, wasm_url: &str) -> Result { let script = format!( r#" From 93fa8b89fa38364bd9f6fd5808b0e0a5845fda1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Florkiewicz?= Date: Wed, 8 May 2024 08:45:54 +0200 Subject: [PATCH 18/35] Update node-wasm/src/node.rs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Yiannis Marangos Signed-off-by: MikoĊ‚aj Florkiewicz --- node-wasm/src/node.rs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/node-wasm/src/node.rs b/node-wasm/src/node.rs index 7a34f904..57ff63ba 100644 --- a/node-wasm/src/node.rs +++ b/node-wasm/src/node.rs @@ -52,13 +52,6 @@ impl NodeDriver { #[wasm_bindgen(constructor)] pub async fn new() -> Result { let worker = spawn_worker(LUMINA_SHARED_WORKER_NAME, "/wasm/lumina_node_wasm.js")?; - /* - let mut opts = WorkerOptions::new(); - opts.type_(WorkerType::Module); - opts.name(LUMINA_SHARED_WORKER_NAME); - let worker = SharedWorker::new_with_worker_options("/js/worker.js", &opts) - .map_err(|e| JsError::new(&format!("could not create SharedWorker: {e:?}")))?; - */ let onerror_callback: Closure = Closure::new(|ev: MessageEvent| { error!("received error from SharedWorker: {:?}", ev.to_string()); From 618c332b1179387d2c842c7d4e09f37d8d9b9242 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Florkiewicz?= Date: Wed, 8 May 2024 08:51:19 +0200 Subject: [PATCH 19/35] Update node-wasm/src/node.rs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Yiannis Marangos Signed-off-by: MikoĊ‚aj Florkiewicz --- node-wasm/src/node.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node-wasm/src/node.rs b/node-wasm/src/node.rs index 57ff63ba..4d50b20b 100644 --- a/node-wasm/src/node.rs +++ b/node-wasm/src/node.rs @@ -114,7 +114,7 @@ impl NodeDriver { /// Wait until the node is connected to at least 1 trusted peer. pub async fn wait_connected_trusted(&self) -> Result<()> { - let command = NodeCommand::WaitConnected { trusted: false }; + let command = NodeCommand::WaitConnected { trusted: true }; let response = self.channel.exec(command).await?; let result = response.into_connected().check_variant()?; From 5ad5a9dcbf564c3cf3b114e5d2a4a0a020d75849 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Florkiewicz?= Date: Wed, 8 May 2024 08:45:08 +0200 Subject: [PATCH 20/35] fixes --- cli/static/worker.js | 17 ----------------- node-wasm/src/worker.rs | 2 +- node-wasm/src/worker/channel.rs | 4 +++- 3 files changed, 4 insertions(+), 19 deletions(-) delete mode 100644 cli/static/worker.js diff --git a/cli/static/worker.js b/cli/static/worker.js deleted file mode 100644 index ba386fd2..00000000 --- a/cli/static/worker.js +++ /dev/null @@ -1,17 +0,0 @@ -Error.stackTraceLimit = 99; // rust stack traces can get pretty big, increase the default - -import init, { run_worker } from "/wasm/lumina_node_wasm.js"; - -// unfortunately, `init()` takes long enough for the first connection (from the tab which -// starts Shared Worker) to be missed. As a workaround, we queue connections in js and then -// pass them to Rust, when it's ready. Rust code replaces `onconnect` handler, so this is -// only necessary on startup. -let queued = []; -onconnect = (event) => { - console.log("Queued connection", event); - queued.push(event.ports[0]); -} - -await init(); -await run_worker(queued); - diff --git a/node-wasm/src/worker.rs b/node-wasm/src/worker.rs index e2343aad..7935d137 100644 --- a/node-wasm/src/worker.rs +++ b/node-wasm/src/worker.rs @@ -104,7 +104,7 @@ struct NodeWorker { } fn to_jsvalue_or_undefined(value: &T) -> JsValue { - to_value(value).unwrap() + to_value(value).unwrap_or(JsValue::UNDEFINED) } impl NodeWorker { diff --git a/node-wasm/src/worker/channel.rs b/node-wasm/src/worker/channel.rs index 08937d37..31bce003 100644 --- a/node-wasm/src/worker/channel.rs +++ b/node-wasm/src/worker/channel.rs @@ -14,6 +14,8 @@ use crate::worker::WorkerError; type WireMessage = Option>; type WorkerClientConnection = (MessagePort, Closure); +const WORKER_CHANNEL_SIZE: usize = 1; + // TODO: cleanup JS objects on drop // impl Drop pub(crate) struct WorkerClient { @@ -24,7 +26,7 @@ pub(crate) struct WorkerClient { impl WorkerClient { pub fn new(channel: MessagePort) -> Self { - let (response_tx, response_rx) = mpsc::channel(64); + let (response_tx, response_rx) = mpsc::channel(WORKER_CHANNEL_SIZE); let near_tx = response_tx.clone(); let onmessage_callback = move |ev: MessageEvent| { From d9a779fe9ef6197f2899294d8c94074e0d42b033 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Florkiewicz?= Date: Wed, 8 May 2024 09:14:14 +0200 Subject: [PATCH 21/35] PR fixes --- node-wasm/src/worker/channel.rs | 41 +++++++++++++++++---------------- node/src/p2p.rs | 1 + 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/node-wasm/src/worker/channel.rs b/node-wasm/src/worker/channel.rs index 31bce003..c3bc1a32 100644 --- a/node-wasm/src/worker/channel.rs +++ b/node-wasm/src/worker/channel.rs @@ -28,9 +28,8 @@ impl WorkerClient { pub fn new(channel: MessagePort) -> Self { let (response_tx, response_rx) = mpsc::channel(WORKER_CHANNEL_SIZE); - let near_tx = response_tx.clone(); let onmessage_callback = move |ev: MessageEvent| { - let local_tx = near_tx.clone(); + let response_tx = response_tx.clone(); spawn_local(async move { let message_data = ev.data(); @@ -40,7 +39,7 @@ impl WorkerClient { }) .ok(); - if let Err(e) = local_tx.send(data).await { + if let Err(e) = response_tx.send(data).await { error!("message forwarding channel closed, should not happen: {e}"); } }) @@ -105,27 +104,29 @@ pub(super) struct WorkerMessageServer { impl WorkerMessageServer { pub fn new(command_channel: mpsc::Sender) -> Self { - let near_tx = command_channel.clone(); - let onconnect_callback: Closure = - Closure::new(move |ev: MessageEvent| { - let local_tx = near_tx.clone(); - spawn_local(async move { - let Ok(port) = ev.ports().at(0).dyn_into() else { - error!("received connection event without MessagePort, should not happen"); - return; - }; - - if let Err(e) = local_tx.send(WorkerMessage::NewConnection(port)).await { - error!("command channel inside worker closed, should not happen: {e}"); - } - }) - }); + let closure_command_channel = command_channel.clone(); + let onconnect: Closure = Closure::new(move |ev: MessageEvent| { + let command_channel = closure_command_channel.clone(); + spawn_local(async move { + let Ok(port) = ev.ports().at(0).dyn_into() else { + error!("received onconnect event without MessagePort, should not happen"); + return; + }; + + if let Err(e) = command_channel + .send(WorkerMessage::NewConnection(port)) + .await + { + error!("command channel inside worker closed, should not happen: {e}"); + } + }) + }); let worker_scope = SharedWorker::worker_self(); - worker_scope.set_onconnect(Some(onconnect_callback.as_ref().unchecked_ref())); + worker_scope.set_onconnect(Some(onconnect.as_ref().unchecked_ref())); Self { - _onconnect_callback: onconnect_callback, + _onconnect_callback: onconnect, clients: Vec::with_capacity(1), // we usually expect to have exactly one client command_channel, } diff --git a/node/src/p2p.rs b/node/src/p2p.rs index b227ccf5..16ec661f 100644 --- a/node/src/p2p.rs +++ b/node/src/p2p.rs @@ -506,6 +506,7 @@ impl P2p { Ok(rx.await?) } } + /// Our network behaviour. #[derive(NetworkBehaviour)] struct Behaviour From d35c4ffd2605f529f290dd720fabd47aaef17b14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Florkiewicz?= Date: Wed, 8 May 2024 17:04:44 +0200 Subject: [PATCH 22/35] another pass --- node-wasm/src/node.rs | 6 +-- node-wasm/src/utils.rs | 36 ++++++-------- node-wasm/src/worker.rs | 26 ++++++---- node-wasm/src/worker/channel.rs | 86 ++++++++++++++++++++------------- 4 files changed, 87 insertions(+), 67 deletions(-) diff --git a/node-wasm/src/node.rs b/node-wasm/src/node.rs index 4d50b20b..0ede99e2 100644 --- a/node-wasm/src/node.rs +++ b/node-wasm/src/node.rs @@ -46,9 +46,9 @@ struct NodeDriver { #[wasm_bindgen] impl NodeDriver { - /// Create a new connection to a Lumina node in a Shared Worker. - /// Note that single Shared Worker can be accessed from multiple tabs, so Lumina may already - /// be running be running, before `NodeDriver::start` call. + /// Create a new connection to a Lumina node running in a Shared Worker. + /// Note that single Shared Worker can be accessed from multiple tabs, so Lumina may + /// already have been started. Otherwise it needs to be started with [`NodeDriver::start`]. #[wasm_bindgen(constructor)] pub async fn new() -> Result { let worker = spawn_worker(LUMINA_SHARED_WORKER_NAME, "/wasm/lumina_node_wasm.js")?; diff --git a/node-wasm/src/utils.rs b/node-wasm/src/utils.rs index cda846fa..7d691086 100644 --- a/node-wasm/src/utils.rs +++ b/node-wasm/src/utils.rs @@ -4,6 +4,7 @@ use std::fmt::{self, Debug}; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; use serde_repr::{Deserialize_repr, Serialize_repr}; +use serde_wasm_bindgen::to_value; use tracing_subscriber::filter::LevelFilter; use tracing_subscriber::fmt::format::Pretty; use tracing_subscriber::fmt::time::UtcTime; @@ -107,6 +108,10 @@ where } } +pub(crate) fn to_jsvalue_or_undefined(value: &T) -> JsValue { + to_value(value).unwrap_or(JsValue::UNDEFINED) +} + pub(crate) trait WorkerSelf { type GlobalScope; @@ -121,10 +126,11 @@ impl WorkerSelf for SharedWorker { } } -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Debug)] pub(crate) enum JsResult where - T: JsCast, + T: JsCast + Debug, + E: Debug, { #[serde(with = "serde_wasm_bindgen::preserve")] Ok(T), @@ -134,8 +140,8 @@ where // once try_trait_v2 is stabilised, this can go impl JsResult where - T: JsCast, - E: Serialize + DeserializeOwned, + T: JsCast + Debug, + E: Serialize + DeserializeOwned + Debug, { pub fn into_result(self) -> Result { self.into() @@ -144,8 +150,8 @@ where impl From> for JsResult where - T: JsCast, - E: Serialize + DeserializeOwned, + T: JsCast + Debug, + E: Serialize + DeserializeOwned + Debug, { fn from(result: Result) -> Self { match result { @@ -157,8 +163,8 @@ where impl From> for Result where - T: JsCast, - E: Serialize + DeserializeOwned, + T: JsCast + Debug, + E: Serialize + DeserializeOwned + Debug, { fn from(result: JsResult) -> Self { match result { @@ -167,17 +173,3 @@ where } } } - -impl Debug for JsResult -where - T: JsCast, - E: Serialize + DeserializeOwned, -{ - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - JsResult::Ok(_) => f.debug_tuple("JsResult::Ok"), - JsResult::Err(_) => f.debug_tuple("JsResult::Err"), - } - .finish() - } -} diff --git a/node-wasm/src/worker.rs b/node-wasm/src/worker.rs index 7935d137..fea7c01c 100644 --- a/node-wasm/src/worker.rs +++ b/node-wasm/src/worker.rs @@ -1,3 +1,5 @@ +//! +//! use std::fmt::Debug; use js_sys::Array; @@ -17,6 +19,7 @@ use lumina_node::store::{IndexedDbStore, SamplingMetadata, Store, StoreError}; use lumina_node::syncer::SyncingInfo; use crate::node::WasmNodeConfig; +use crate::utils::to_jsvalue_or_undefined; use crate::worker::channel::{WorkerMessage, WorkerMessageServer}; use crate::worker::commands::{NodeCommand, SingleHeaderQuery, WorkerResponse}; use crate::wrapper::libp2p::NetworkInfoSnapshot; @@ -53,14 +56,24 @@ pub enum WorkerError { SerdeError(String), #[error("command message could not be serialised, should not happen: {0}")] CouldNotSerialiseCommand(String), + #[error("command message could not be deserialised, should not happen: {0}")] + CouldNotDeserialiseCommand(String), #[error("command response could not be serialised, should not happen: {0}")] CouldNotSerialiseResponseValue(String), + #[error("command response could not be deserialised, should not happen: {0}")] + CouldNotDeserialiseResponseValue(String), #[error("response message could not be sent: {0}")] CouldNotSendCommand(String), #[error("Received empty worker response, should not happen")] EmptyWorkerResponse, #[error("Response channel to worker closed, should not happen")] ResponseChannelDropped, + + #[error("Invalid command received")] + InvalidCommand, + + #[error("Invalid worker response")] + InvalidWorkerResponse, } impl From for WorkerError { @@ -103,10 +116,6 @@ struct NodeWorker { node: Node, } -fn to_jsvalue_or_undefined(value: &T) -> JsValue { - to_value(value).unwrap_or(JsValue::UNDEFINED) -} - impl NodeWorker { async fn new(config: WasmNodeConfig) -> Result { let config = config.into_node_config().await?; @@ -205,11 +214,7 @@ impl NodeWorker { } async fn get_last_seen_network_head(&mut self) -> JsValue { - self.node - .get_network_head_header() - .as_ref() - .map(to_jsvalue_or_undefined) - .unwrap_or(JsValue::UNDEFINED) + to_jsvalue_or_undefined(&self.node.get_network_head_header()) } async fn get_sampling_metadata(&mut self, height: u64) -> Result> { @@ -288,6 +293,9 @@ pub async fn run_worker(queued_connections: Vec) { WorkerMessage::NewConnection(connection) => { message_server.add(connection); } + WorkerMessage::InvalidCommandReceived(client_id) => { + message_server.respond_err_to(client_id, WorkerError::InvalidCommand); + } WorkerMessage::Command((command, client_id)) => { info!("received from {client_id:?}: {command:?}"); let Some(worker) = &mut worker else { diff --git a/node-wasm/src/worker/channel.rs b/node-wasm/src/worker/channel.rs index c3bc1a32..a35adcf0 100644 --- a/node-wasm/src/worker/channel.rs +++ b/node-wasm/src/worker/channel.rs @@ -11,33 +11,41 @@ use crate::utils::WorkerSelf; use crate::worker::commands::{NodeCommand, WorkerResponse}; use crate::worker::WorkerError; -type WireMessage = Option>; +type WireMessage = Result; type WorkerClientConnection = (MessagePort, Closure); const WORKER_CHANNEL_SIZE: usize = 1; // TODO: cleanup JS objects on drop // impl Drop +/// `WorkerClient` is responsible for sending messages to and receiving responses from [`WorkerMessageServer`]. +/// It covers JS details like callbacks, having to synchronise requests and responses and exposes +/// simple RPC-like function call interface. pub(crate) struct WorkerClient { _onmessage: Closure, - channel: MessagePort, + message_port: MessagePort, response_channel: Mutex>, } impl WorkerClient { - pub fn new(channel: MessagePort) -> Self { + /// Create a new `WorkerClient` out of a [`MessagePort`] that should be connected + /// to a [`SharedWorker`] running lumina. + /// + /// [`SharedWorker`]: https://developer.mozilla.org/en-US/docs/Web/API/SharedWorker + /// [`MessagePort`]: https://developer.mozilla.org/en-US/docs/Web/API/MessagePort + pub fn new(message_port: MessagePort) -> Self { let (response_tx, response_rx) = mpsc::channel(WORKER_CHANNEL_SIZE); let onmessage_callback = move |ev: MessageEvent| { let response_tx = response_tx.clone(); spawn_local(async move { - let message_data = ev.data(); - - let data = from_value(message_data) - .map_err(|e| { - error!("could not convert from JsValue: {e}"); - }) - .ok(); + let data: WireMessage = match from_value(ev.data()) { + Ok(jsvalue) => jsvalue, + Err(e) => { + error!("WorkerClient could not convert from JsValue: {e}"); + Err(WorkerError::CouldNotDeserialiseResponseValue(e.to_string())) + } + }; if let Err(e) = response_tx.send(data).await { error!("message forwarding channel closed, should not happen: {e}"); @@ -46,30 +54,37 @@ impl WorkerClient { }; let onmessage = Closure::new(onmessage_callback); - channel.set_onmessage(Some(onmessage.as_ref().unchecked_ref())); - channel.start(); + message_port.set_onmessage(Some(onmessage.as_ref().unchecked_ref())); + message_port.start(); Self { response_channel: Mutex::new(response_rx), _onmessage: onmessage, - channel, + message_port, } } + /// Send command to lumina and wait for a response. + /// + /// Response enum variant can be converted into appropriate type at runtime with a provided + /// [`CheckableResponseExt`] helper. + /// + /// [`CheckableResponseExt`]: crate::utils::CheckableResponseExt pub async fn exec(&self, command: NodeCommand) -> Result { - let mut channel = self.response_channel.lock().await; + let mut response_channel = self.response_channel.lock().await; self.send(command)?; - let message: WireMessage = channel + let message: WireMessage = response_channel .recv() .await .ok_or(WorkerError::ResponseChannelDropped)?; - message.ok_or(WorkerError::EmptyWorkerResponse)? + //message.ok_or(WorkerError::EmptyWorkerResponse)? + message } fn send(&self, command: NodeCommand) -> Result<(), WorkerError> { let command_value = to_value(&command).map_err(|e| WorkerError::CouldNotSerialiseCommand(e.to_string()))?; - self.channel.post_message(&command_value).map_err(|e| { + self.message_port.post_message(&command_value).map_err(|e| { WorkerError::CouldNotSendCommand(e.as_string().unwrap_or("UNDEFINED".to_string())) }) } @@ -86,9 +101,11 @@ impl fmt::Display for ClientId { pub(super) enum WorkerMessage { NewConnection(MessagePort), + InvalidCommandReceived(ClientId), Command((NodeCommand, ClientId)), } +/// pub(super) struct WorkerMessageServer { // same onconnect callback is used throughtout entire Worker lifetime. // Keep a reference to make sure it doesn't get dropped. @@ -149,36 +166,39 @@ impl WorkerMessageServer { let local_tx = near_tx.clone(); spawn_local(async move { let client_id = ClientId(client_id); - let message_data = ev.data(); - let Ok(command) = from_value::(message_data) else { - warn!("could not deserialize message from client {client_id}"); - return; + + let message = match from_value(ev.data()) { + Ok(command) => { + debug!("received command from client {client_id}: {command:#?}"); + WorkerMessage::Command((command, client_id)) + } + Err(e) => { + warn!("could not deserialize message from client {client_id}: {e}"); + WorkerMessage::InvalidCommandReceived(client_id) + } }; - debug!("received command from client {client_id}: {command:#?}"); - if let Err(e) = local_tx - .send(WorkerMessage::Command((command, client_id))) - .await - { + if let Err(e) = local_tx.send(message).await { error!("command channel inside worker closed, should not happen: {e}"); } }) }); - port.set_onmessage(Some(client_message_callback.as_ref().unchecked_ref())); self.clients.push((port, client_message_callback)); + let (port, callback) = self.clients.last().unwrap(); + port.set_onmessage(Some(callback.as_ref().unchecked_ref())); + info!("SharedWorker ready to receive commands from client {client_id}"); } - fn send_response(&self, client: ClientId, msg: Result) { - let wire_message: WireMessage = Some(msg); - - let message = match to_value(&wire_message) { + fn send_response(&self, client: ClientId, message: WireMessage) { + let message = match to_value(&message) { Ok(jsvalue) => jsvalue, Err(e) => { - warn!("provided response could not be coverted to JsValue: {e}, sending undefined instead"); - JsValue::UNDEFINED // we need to send something, client is waiting for a response + warn!("provided response could not be coverted to JsValue: {e}"); + to_value(&WorkerError::CouldNotSerialiseResponseValue(e.to_string())) + .expect("couldn't serialise serialisation error") } }; From dccdb253fbbf91553f325f89d2acbf7ae9ab92c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Florkiewicz?= Date: Wed, 8 May 2024 21:43:11 +0200 Subject: [PATCH 23/35] mostly error cleanup --- cli/static/run_node.js | 1 - node-wasm/src/utils.rs | 30 ++++++++++++-- node-wasm/src/worker.rs | 68 ++++++++++++++------------------ node-wasm/src/worker/channel.rs | 16 ++++---- node-wasm/src/worker/commands.rs | 8 ++-- 5 files changed, 67 insertions(+), 56 deletions(-) diff --git a/cli/static/run_node.js b/cli/static/run_node.js index 4eb91c3d..effcab9b 100644 --- a/cli/static/run_node.js +++ b/cli/static/run_node.js @@ -41,7 +41,6 @@ async function show_stats(node) { return } - console.log(network_head); const square_rows = network_head.dah.row_roots.length; const square_cols = network_head.dah.column_roots.length; diff --git a/node-wasm/src/utils.rs b/node-wasm/src/utils.rs index 0c1b5552..8a02a26d 100644 --- a/node-wasm/src/utils.rs +++ b/node-wasm/src/utils.rs @@ -1,6 +1,7 @@ //! Various utilities for interacting with node from wasm. use std::fmt::{self, Debug}; +use js_sys::JsString; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; use serde_repr::{Deserialize_repr, Serialize_repr}; @@ -74,13 +75,17 @@ pub(crate) fn js_value_from_display(value: D) -> JsValue { JsValue::from(value.to_string()) } -pub(crate) trait JsContext { +pub(crate) fn to_jsvalue_or_undefined(value: &T) -> JsValue { + to_value(value).unwrap_or(JsValue::UNDEFINED) +} + +pub(crate) trait JsContext { fn js_context(self, context: C) -> Result where C: fmt::Display + Send + Sync + 'static; } -impl JsContext for std::result::Result +impl JsContext for std::result::Result where E: std::error::Error, { @@ -92,8 +97,25 @@ where } } -pub(crate) fn to_jsvalue_or_undefined(value: &T) -> JsValue { - to_value(value).unwrap_or(JsValue::UNDEFINED) +pub(crate) trait JsValueToJsError { + fn to_error(self, context_fn: C) -> Result + where + C: fmt::Display + Send + Sync + 'static; +} + +impl JsValueToJsError for std::result::Result { + fn to_error(self, context: C) -> Result + where + C: fmt::Display + Send + Sync + 'static, + { + self.map_err(|e| { + let error_str = match e.dyn_ref::() { + Some(s) => format!("{context}: {s}"), + None => format!("{context}"), + }; + JsError::new(&error_str) + }) + } } pub(crate) trait WorkerSelf { diff --git a/node-wasm/src/worker.rs b/node-wasm/src/worker.rs index fea7c01c..bd174956 100644 --- a/node-wasm/src/worker.rs +++ b/node-wasm/src/worker.rs @@ -1,25 +1,25 @@ -//! +//! //! use std::fmt::Debug; use js_sys::Array; +use libp2p::multiaddr::Error as MultiaddrError; use libp2p::{Multiaddr, PeerId}; use serde::{Deserialize, Serialize}; use serde_wasm_bindgen::{from_value, to_value}; use thiserror::Error; use tokio::sync::mpsc; -use tracing::{error, info, warn}; +use tracing::{debug, error, info, warn}; use wasm_bindgen::prelude::*; use web_sys::{Blob, BlobPropertyBag, MessagePort, SharedWorker, Url, WorkerOptions, WorkerType}; use celestia_tendermint::error::Error as TendermintError; -use libp2p::multiaddr::Error as MultiaddrError; use lumina_node::node::{Node, NodeError}; use lumina_node::store::{IndexedDbStore, SamplingMetadata, Store, StoreError}; use lumina_node::syncer::SyncingInfo; use crate::node::WasmNodeConfig; -use crate::utils::to_jsvalue_or_undefined; +use crate::utils::{to_jsvalue_or_undefined, JsValueToJsError}; use crate::worker::channel::{WorkerMessage, WorkerMessageServer}; use crate::worker::commands::{NodeCommand, SingleHeaderQuery, WorkerResponse}; use crate::wrapper::libp2p::NetworkInfoSnapshot; @@ -31,7 +31,7 @@ pub(crate) use channel::WorkerClient; const WORKER_MESSAGE_SERVER_INCOMING_QUEUE_LENGTH: usize = 64; -/// actual type that's sent over a js message port +/// actual type that's sent over javascript MessagePort type Result = std::result::Result; #[derive(Debug, Serialize, Deserialize, Error)] @@ -41,56 +41,42 @@ pub enum WorkerError { #[error("node has already been started")] NodeAlreadyRunning, #[error("could not create blockstore: {0}")] - BlockstoreCrationFailed(String), + BlockstoreCreationFailed(String), #[error("could not create header store: {0}")] - StoreCrationFailed(String), + StoreCreationFailed(String), #[error("could not parse genesis hash: {0}")] GenesisHashInvalid(String), #[error("could not parse bootstrap node multiaddr: {0}")] BootstrapNodeMultiaddrInvalid(String), #[error("node error: {0}")] NodeError(String), - #[error("respose to command did not match expected type, should not happen")] - InvalidResponseType, - #[error("Value could not be serialized, should not happen: {0}")] + #[error("value could not be serialized: {0}")] SerdeError(String), #[error("command message could not be serialised, should not happen: {0}")] CouldNotSerialiseCommand(String), #[error("command message could not be deserialised, should not happen: {0}")] CouldNotDeserialiseCommand(String), #[error("command response could not be serialised, should not happen: {0}")] - CouldNotSerialiseResponseValue(String), + CouldNotSerialiseResponse(String), #[error("command response could not be deserialised, should not happen: {0}")] - CouldNotDeserialiseResponseValue(String), + CouldNotDeserialiseResponse(String), #[error("response message could not be sent: {0}")] CouldNotSendCommand(String), - #[error("Received empty worker response, should not happen")] - EmptyWorkerResponse, - #[error("Response channel to worker closed, should not happen")] + #[error("response channel from worker closed, should not happen")] ResponseChannelDropped, - - #[error("Invalid command received")] - InvalidCommand, - - #[error("Invalid worker response")] - InvalidWorkerResponse, -} - -impl From for WorkerError { - fn from(error: NodeError) -> Self { - WorkerError::NodeError(error.to_string()) - } + #[error("invalid command received")] + InvalidCommandReceived, } impl From for WorkerError { fn from(error: blockstore::Error) -> Self { - WorkerError::BlockstoreCrationFailed(error.to_string()) + WorkerError::BlockstoreCreationFailed(error.to_string()) } } impl From for WorkerError { fn from(error: StoreError) -> Self { - WorkerError::StoreCrationFailed(error.to_string()) + WorkerError::StoreCreationFailed(error.to_string()) } } @@ -100,6 +86,12 @@ impl From for WorkerError { } } +impl From for WorkerError { + fn from(error: NodeError) -> Self { + WorkerError::NodeError(error.to_string()) + } +} + impl From for WorkerError { fn from(error: MultiaddrError) -> Self { WorkerError::BootstrapNodeMultiaddrInvalid(error.to_string()) @@ -172,7 +164,7 @@ impl NodeWorker { SingleHeaderQuery::ByHash(hash) => self.node.request_header_by_hash(&hash).await, SingleHeaderQuery::ByHeight(height) => self.node.request_header_by_height(height).await, }?; - to_value(&header).map_err(|e| WorkerError::CouldNotSerialiseResponseValue(e.to_string())) + to_value(&header).map_err(|e| WorkerError::CouldNotSerialiseResponse(e.to_string())) } async fn get_header(&mut self, query: SingleHeaderQuery) -> Result { @@ -181,7 +173,7 @@ impl NodeWorker { SingleHeaderQuery::ByHash(hash) => self.node.get_header_by_hash(&hash).await, SingleHeaderQuery::ByHeight(height) => self.node.get_header_by_height(height).await, }?; - to_value(&header).map_err(|e| WorkerError::CouldNotSerialiseResponseValue(e.to_string())) + to_value(&header).map_err(|e| WorkerError::CouldNotSerialiseResponse(e.to_string())) } async fn get_verified_headers(&mut self, from: JsValue, amount: u64) -> Result { @@ -294,10 +286,10 @@ pub async fn run_worker(queued_connections: Vec) { message_server.add(connection); } WorkerMessage::InvalidCommandReceived(client_id) => { - message_server.respond_err_to(client_id, WorkerError::InvalidCommand); + message_server.respond_err_to(client_id, WorkerError::InvalidCommandReceived); } WorkerMessage::Command((command, client_id)) => { - info!("received from {client_id:?}: {command:?}"); + debug!("received from {client_id:?}: {command:?}"); let Some(worker) = &mut worker else { match command { NodeCommand::IsRunning => { @@ -356,25 +348,23 @@ await self.lumina.run_worker(queued); "# ); - info!("js shim: {script}"); - let array = Array::new(); array.push(&script.into()); let blob = Blob::new_with_str_sequence_and_options( &array, BlobPropertyBag::new().type_("application/javascript"), ) - .map_err(|e| JsError::new(&format!("could not create SharedWorker Blob: {e:?}")))?; + .to_error("could not create js shim Blob")?; - let url = Url::create_object_url_with_blob(&blob) - .map_err(|e| JsError::new(&format!("could not create SharedWorker Url: {e:?}")))?; + let url = + Url::create_object_url_with_blob(&blob).to_error("could not create js shim Blob Url")?; let mut opts = WorkerOptions::new(); opts.type_(WorkerType::Module); opts.name(name); let worker = SharedWorker::new_with_worker_options(&url, &opts) - .map_err(|e| JsError::new(&format!("could not create SharedWorker Url: {e:?}")))?; + .to_error("could not create SharedWorker")?; Ok(worker) } diff --git a/node-wasm/src/worker/channel.rs b/node-wasm/src/worker/channel.rs index a35adcf0..4375faf0 100644 --- a/node-wasm/src/worker/channel.rs +++ b/node-wasm/src/worker/channel.rs @@ -1,5 +1,6 @@ use std::fmt::{self, Debug}; +use js_sys::JsString; use serde_wasm_bindgen::{from_value, to_value}; use tokio::sync::{mpsc, Mutex}; use tracing::{debug, error, info, warn}; @@ -17,7 +18,7 @@ type WorkerClientConnection = (MessagePort, Closure); const WORKER_CHANNEL_SIZE: usize = 1; // TODO: cleanup JS objects on drop -// impl Drop + /// `WorkerClient` is responsible for sending messages to and receiving responses from [`WorkerMessageServer`]. /// It covers JS details like callbacks, having to synchronise requests and responses and exposes /// simple RPC-like function call interface. @@ -43,7 +44,7 @@ impl WorkerClient { Ok(jsvalue) => jsvalue, Err(e) => { error!("WorkerClient could not convert from JsValue: {e}"); - Err(WorkerError::CouldNotDeserialiseResponseValue(e.to_string())) + Err(WorkerError::CouldNotDeserialiseResponse(e.to_string())) } }; @@ -77,16 +78,15 @@ impl WorkerClient { .recv() .await .ok_or(WorkerError::ResponseChannelDropped)?; - //message.ok_or(WorkerError::EmptyWorkerResponse)? message } fn send(&self, command: NodeCommand) -> Result<(), WorkerError> { let command_value = to_value(&command).map_err(|e| WorkerError::CouldNotSerialiseCommand(e.to_string()))?; - self.message_port.post_message(&command_value).map_err(|e| { - WorkerError::CouldNotSendCommand(e.as_string().unwrap_or("UNDEFINED".to_string())) - }) + self.message_port + .post_message(&command_value) + .map_err(|e| WorkerError::CouldNotSendCommand(format!("{:?}", e.dyn_ref::()))) } } @@ -197,8 +197,8 @@ impl WorkerMessageServer { Ok(jsvalue) => jsvalue, Err(e) => { warn!("provided response could not be coverted to JsValue: {e}"); - to_value(&WorkerError::CouldNotSerialiseResponseValue(e.to_string())) - .expect("couldn't serialise serialisation error") + to_value(&WorkerError::CouldNotSerialiseResponse(e.to_string())) + .expect("something's wrong, couldn't serialise serialisation error") } }; diff --git a/node-wasm/src/worker/commands.rs b/node-wasm/src/worker/commands.rs index 81808170..7d12f759 100644 --- a/node-wasm/src/worker/commands.rs +++ b/node-wasm/src/worker/commands.rs @@ -6,7 +6,7 @@ use libp2p::Multiaddr; use libp2p::PeerId; use serde::{Deserialize, Serialize}; use tracing::error; -use wasm_bindgen::JsValue; +use wasm_bindgen::{JsError, JsValue}; use celestia_types::hash::Hash; use lumina_node::peer_tracker::PeerTrackerInfo; @@ -81,16 +81,16 @@ pub(crate) enum WorkerResponse { pub(crate) trait CheckableResponseExt { type Output; - fn check_variant(self) -> Result; + fn check_variant(self) -> Result; } impl CheckableResponseExt for Result { type Output = T; - fn check_variant(self) -> Result { + fn check_variant(self) -> Result { self.map_err(|response| { error!("invalid response, received: {response:?}"); - WorkerError::InvalidResponseType + JsError::new("invalid response received for the command sent") }) } } From aa6f1732c94085af0914ea1df7d24f99bc6c6917 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Florkiewicz?= Date: Wed, 8 May 2024 21:52:12 +0200 Subject: [PATCH 24/35] clippin --- node-wasm/src/utils.rs | 18 ------------------ node-wasm/src/worker.rs | 2 -- node-wasm/src/worker/channel.rs | 1 - 3 files changed, 21 deletions(-) diff --git a/node-wasm/src/utils.rs b/node-wasm/src/utils.rs index 8a02a26d..f940ef83 100644 --- a/node-wasm/src/utils.rs +++ b/node-wasm/src/utils.rs @@ -79,24 +79,6 @@ pub(crate) fn to_jsvalue_or_undefined(value: &T) -> JsValue { to_value(value).unwrap_or(JsValue::UNDEFINED) } -pub(crate) trait JsContext { - fn js_context(self, context: C) -> Result - where - C: fmt::Display + Send + Sync + 'static; -} - -impl JsContext for std::result::Result -where - E: std::error::Error, -{ - fn js_context(self, context: C) -> Result - where - C: fmt::Display + Send + Sync + 'static, - { - self.map_err(|e| JsError::new(&format!("{context}: {e}"))) - } -} - pub(crate) trait JsValueToJsError { fn to_error(self, context_fn: C) -> Result where diff --git a/node-wasm/src/worker.rs b/node-wasm/src/worker.rs index bd174956..b4ca596f 100644 --- a/node-wasm/src/worker.rs +++ b/node-wasm/src/worker.rs @@ -1,5 +1,3 @@ -//! -//! use std::fmt::Debug; use js_sys::Array; diff --git a/node-wasm/src/worker/channel.rs b/node-wasm/src/worker/channel.rs index 4375faf0..265db904 100644 --- a/node-wasm/src/worker/channel.rs +++ b/node-wasm/src/worker/channel.rs @@ -105,7 +105,6 @@ pub(super) enum WorkerMessage { Command((NodeCommand, ClientId)), } -/// pub(super) struct WorkerMessageServer { // same onconnect callback is used throughtout entire Worker lifetime. // Keep a reference to make sure it doesn't get dropped. From 662e8082c34ce7eb9a423c7dac579ff78be1e526 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Florkiewicz?= Date: Thu, 9 May 2024 09:13:40 +0200 Subject: [PATCH 25/35] add close, consolidate errors,go back to NodeClient --- cli/static/run_node.js | 14 ++++++------- node-wasm/src/node.rs | 29 +++++++++++++++++++++---- node-wasm/src/worker.rs | 36 +++++++++----------------------- node-wasm/src/worker/channel.rs | 2 -- node-wasm/src/worker/commands.rs | 2 ++ 5 files changed, 44 insertions(+), 39 deletions(-) diff --git a/cli/static/run_node.js b/cli/static/run_node.js index effcab9b..b4316ffe 100644 --- a/cli/static/run_node.js +++ b/cli/static/run_node.js @@ -1,6 +1,6 @@ Error.stackTraceLimit = 99; // rust stack traces can get pretty big, increase the default -import init, { NodeConfig, NodeDriver } from "/wasm/lumina_node_wasm.js"; +import init, { NodeConfig, NodeClient } from "/wasm/lumina_node_wasm.js"; async function fetch_config() { const response = await fetch('/cfg.json'); @@ -96,25 +96,25 @@ function bind_config(data) { async function main(document, window) { await init(); - window.driver = await new NodeDriver(); + window.node = await new NodeClient(); bind_config(await fetch_config()); - if (await window.driver.is_running() === true) { + if (await window.node.is_running() === true) { document.querySelectorAll('.config').forEach(elem => elem.disabled = true); - document.getElementById("peer-id").innerText = await window.driver.local_peer_id(); + document.getElementById("peer-id").innerText = await window.node.local_peer_id(); document.querySelectorAll(".status").forEach(elem => elem.style.visibility = "visible"); } document.getElementById("start").addEventListener("click", async () => { document.querySelectorAll('.config').forEach(elem => elem.disabled = true); - await window.driver.start(window.config); - document.getElementById("peer-id").innerText = await window.driver.local_peer_id(); + await window.node.start(window.config); + document.getElementById("peer-id").innerText = await window.node.local_peer_id(); document.querySelectorAll(".status").forEach(elem => elem.style.visibility = "visible"); }); - setInterval(async () => await show_stats(window.driver), 1000) + setInterval(async () => await show_stats(window.node), 1000) } await main(document, window); diff --git a/node-wasm/src/node.rs b/node-wasm/src/node.rs index 0ede99e2..fb932418 100644 --- a/node-wasm/src/node.rs +++ b/node-wasm/src/node.rs @@ -37,14 +37,14 @@ pub struct WasmNodeConfig { pub bootnodes: Vec, } -#[wasm_bindgen] +#[wasm_bindgen(js_name = NodeClient)] struct NodeDriver { _worker: SharedWorker, _onerror_callback: Closure, channel: WorkerClient, } -#[wasm_bindgen] +#[wasm_bindgen(js_class = NodeClient)] impl NodeDriver { /// Create a new connection to a Lumina node running in a Shared Worker. /// Note that single Shared Worker can be accessed from multiple tabs, so Lumina may @@ -284,6 +284,20 @@ impl NodeDriver { Ok(to_value(&metadata?)?) } + + /// Requests SharedWorker running lumina to close. Any events received afterwards wont + /// be processed and new NodeClient needs to be created to restart a node. + pub async fn close(&self) -> Result<()> { + let command = NodeCommand::CloseWorker; + let response = self.channel.exec(command).await?; + if response.is_worker_closed() { + Ok(()) + } else { + Err(JsError::new( + "invalid response received for the command sent", + )) + } + } } #[wasm_bindgen(js_class = NodeConfig)] @@ -309,12 +323,19 @@ impl WasmNodeConfig { let p2p_local_keypair = Keypair::generate_ed25519(); - let genesis_hash = self.genesis_hash.map(|h| h.parse()).transpose()?; + let genesis_hash = self + .genesis_hash + .map(|h| h.parse()) + .transpose() + .map_err(|e| WorkerError::NodeSetupFailed(format!("genesis hash invalid: {e}")))?; let p2p_bootnodes = self .bootnodes .iter() .map(|addr| addr.parse()) - .collect::>()?; + .collect::>() + .map_err(|e| { + WorkerError::NodeSetupFailed(format!("bootstrap multiaddr invalid: {e}")) + })?; Ok(NodeConfig { network_id: network_id.to_string(), diff --git a/node-wasm/src/worker.rs b/node-wasm/src/worker.rs index b4ca596f..ba0b1c50 100644 --- a/node-wasm/src/worker.rs +++ b/node-wasm/src/worker.rs @@ -1,7 +1,6 @@ use std::fmt::Debug; use js_sys::Array; -use libp2p::multiaddr::Error as MultiaddrError; use libp2p::{Multiaddr, PeerId}; use serde::{Deserialize, Serialize}; use serde_wasm_bindgen::{from_value, to_value}; @@ -11,13 +10,12 @@ use tracing::{debug, error, info, warn}; use wasm_bindgen::prelude::*; use web_sys::{Blob, BlobPropertyBag, MessagePort, SharedWorker, Url, WorkerOptions, WorkerType}; -use celestia_tendermint::error::Error as TendermintError; use lumina_node::node::{Node, NodeError}; use lumina_node::store::{IndexedDbStore, SamplingMetadata, Store, StoreError}; use lumina_node::syncer::SyncingInfo; use crate::node::WasmNodeConfig; -use crate::utils::{to_jsvalue_or_undefined, JsValueToJsError}; +use crate::utils::{to_jsvalue_or_undefined, JsValueToJsError, WorkerSelf}; use crate::worker::channel::{WorkerMessage, WorkerMessageServer}; use crate::worker::commands::{NodeCommand, SingleHeaderQuery, WorkerResponse}; use crate::wrapper::libp2p::NetworkInfoSnapshot; @@ -38,14 +36,8 @@ pub enum WorkerError { NodeNotRunning, #[error("node has already been started")] NodeAlreadyRunning, - #[error("could not create blockstore: {0}")] - BlockstoreCreationFailed(String), - #[error("could not create header store: {0}")] - StoreCreationFailed(String), - #[error("could not parse genesis hash: {0}")] - GenesisHashInvalid(String), - #[error("could not parse bootstrap node multiaddr: {0}")] - BootstrapNodeMultiaddrInvalid(String), + #[error("setting up node failed: {0}")] + NodeSetupFailed(String), #[error("node error: {0}")] NodeError(String), #[error("value could not be serialized: {0}")] @@ -68,19 +60,13 @@ pub enum WorkerError { impl From for WorkerError { fn from(error: blockstore::Error) -> Self { - WorkerError::BlockstoreCreationFailed(error.to_string()) + WorkerError::NodeSetupFailed(format!("could not create blockstore: {error}")) } } impl From for WorkerError { fn from(error: StoreError) -> Self { - WorkerError::StoreCreationFailed(error.to_string()) - } -} - -impl From for WorkerError { - fn from(error: TendermintError) -> Self { - WorkerError::GenesisHashInvalid(error.to_string()) + WorkerError::NodeSetupFailed(format!("could not create header store: {error}")) } } @@ -90,12 +76,6 @@ impl From for WorkerError { } } -impl From for WorkerError { - fn from(error: MultiaddrError) -> Self { - WorkerError::BootstrapNodeMultiaddrInvalid(error.to_string()) - } -} - impl From for WorkerError { fn from(error: serde_wasm_bindgen::Error) -> Self { WorkerError::SerdeError(error.to_string()) @@ -262,6 +242,10 @@ impl NodeWorker { NodeCommand::GetSamplingMetadata { height } => { WorkerResponse::SamplingMetadata(self.get_sampling_metadata(height).await) } + NodeCommand::CloseWorker => { + SharedWorker::worker_self().close(); + WorkerResponse::WorkerClosed + } } } } @@ -320,7 +304,7 @@ pub async fn run_worker(queued_connections: Vec) { } } - error!("Channel to WorkerMessageServer closed, should not happen"); + info!("Channel to WorkerMessageServer closed, exiting the SharedWorker"); } /// SharedWorker can only be spawned from an [`URL`]. To eliminate a need to host a js shim diff --git a/node-wasm/src/worker/channel.rs b/node-wasm/src/worker/channel.rs index 265db904..80759691 100644 --- a/node-wasm/src/worker/channel.rs +++ b/node-wasm/src/worker/channel.rs @@ -17,8 +17,6 @@ type WorkerClientConnection = (MessagePort, Closure); const WORKER_CHANNEL_SIZE: usize = 1; -// TODO: cleanup JS objects on drop - /// `WorkerClient` is responsible for sending messages to and receiving responses from [`WorkerMessageServer`]. /// It covers JS details like callbacks, having to synchronise requests and responses and exposes /// simple RPC-like function call interface. diff --git a/node-wasm/src/worker/commands.rs b/node-wasm/src/worker/commands.rs index 7d12f759..f4afcf6e 100644 --- a/node-wasm/src/worker/commands.rs +++ b/node-wasm/src/worker/commands.rs @@ -51,6 +51,7 @@ pub(crate) enum NodeCommand { GetSamplingMetadata { height: u64, }, + CloseWorker, } #[derive(Serialize, Deserialize, Debug)] @@ -77,6 +78,7 @@ pub(crate) enum WorkerResponse { #[serde(with = "serde_wasm_bindgen::preserve")] LastSeenNetworkHead(JsValue), SamplingMetadata(Result>), + WorkerClosed, } pub(crate) trait CheckableResponseExt { From 0495d0535a04b05c2b12e7a975172dbe0d60589a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Zwoli=C5=84ski?= Date: Fri, 10 May 2024 15:32:24 +0200 Subject: [PATCH 26/35] switch to spawning worker from a static script --- node-wasm/Cargo.toml | 18 ++++++++++++++--- node-wasm/js/worker.js | 24 ++++++++++++++++++++++ node-wasm/src/node.rs | 2 +- node-wasm/src/worker.rs | 44 ++++++++++------------------------------- 4 files changed, 50 insertions(+), 38 deletions(-) create mode 100644 node-wasm/js/worker.js diff --git a/node-wasm/Cargo.toml b/node-wasm/Cargo.toml index 18b6b411..169b97be 100644 --- a/node-wasm/Cargo.toml +++ b/node-wasm/Cargo.toml @@ -30,6 +30,7 @@ blockstore = { workspace = true } celestia-tendermint = { workspace = true } anyhow = "1.0.71" +enum-as-inner = "0.6" console_error_panic_hook = "0.1.7" futures = "0.3" gloo-timers = "0.3" @@ -40,11 +41,22 @@ serde-wasm-bindgen = "0.6.0" serde_repr = "0.1" thiserror = "1.0" time = { version = "0.3", features = ["wasm-bindgen"] } -tokio = { version = "*", features = ["sync"]} +tokio = { version = "*", features = ["sync"] } tracing = "0.1.37" tracing-subscriber = { version = "0.3.18", features = ["time"] } tracing-web = "0.1.2" wasm-bindgen = "0.2.88" wasm-bindgen-futures = "0.4.37" -web-sys = { version = "0.3.69", features = ["BroadcastChannel", "MessageEvent", "Worker", "WorkerOptions", "WorkerType", "SharedWorker", "MessagePort", "SharedWorkerGlobalScope", "Blob", "BlobPropertyBag", "Url"]} -enum-as-inner = "0.6" +web-sys = { version = "0.3.69", features = [ + "Blob", + "BlobPropertyBag", + "BroadcastChannel", + "MessageEvent", + "MessagePort", + "SharedWorker", + "SharedWorkerGlobalScope", + "Url", + "Worker", + "WorkerOptions", + "WorkerType", +] } diff --git a/node-wasm/js/worker.js b/node-wasm/js/worker.js new file mode 100644 index 00000000..d41dfd5a --- /dev/null +++ b/node-wasm/js/worker.js @@ -0,0 +1,24 @@ +// this file should be installed by wasm-pack in pkg/snippets/-/js/ +import init, { run_worker } from '../../../lumina_node_wasm.js'; + +// get the path to this file +export function worker_script_url() { + return import.meta.url; +} + +// if we are in a worker +if ( + typeof WorkerGlobalScope !== 'undefined' + && self instanceof WorkerGlobalScope +) { + Error.stackTraceLimit = 99; + + let queued = []; + onconnect = (event) => { + console.log("Queued connection", event); + queued.push(event.ports[0]); + } + + await init(); + await run_worker(queued); +} diff --git a/node-wasm/src/node.rs b/node-wasm/src/node.rs index fb932418..5f40aba2 100644 --- a/node-wasm/src/node.rs +++ b/node-wasm/src/node.rs @@ -51,7 +51,7 @@ impl NodeDriver { /// already have been started. Otherwise it needs to be started with [`NodeDriver::start`]. #[wasm_bindgen(constructor)] pub async fn new() -> Result { - let worker = spawn_worker(LUMINA_SHARED_WORKER_NAME, "/wasm/lumina_node_wasm.js")?; + let worker = spawn_worker(LUMINA_SHARED_WORKER_NAME)?; let onerror_callback: Closure = Closure::new(|ev: MessageEvent| { error!("received error from SharedWorker: {:?}", ev.to_string()); diff --git a/node-wasm/src/worker.rs b/node-wasm/src/worker.rs index ba0b1c50..b743280a 100644 --- a/node-wasm/src/worker.rs +++ b/node-wasm/src/worker.rs @@ -8,7 +8,7 @@ use thiserror::Error; use tokio::sync::mpsc; use tracing::{debug, error, info, warn}; use wasm_bindgen::prelude::*; -use web_sys::{Blob, BlobPropertyBag, MessagePort, SharedWorker, Url, WorkerOptions, WorkerType}; +use web_sys::{MessagePort, SharedWorker, WorkerOptions, WorkerType}; use lumina_node::node::{Node, NodeError}; use lumina_node::store::{IndexedDbStore, SamplingMetadata, Store, StoreError}; @@ -307,39 +307,9 @@ pub async fn run_worker(queued_connections: Vec) { info!("Channel to WorkerMessageServer closed, exiting the SharedWorker"); } -/// SharedWorker can only be spawned from an [`URL`]. To eliminate a need to host a js shim -/// which calls into our Rust wasm code under specific path, we encode its entire contents -/// as a [`Blob`] which is then passed as an URL. -/// -/// [`URL`]: https://developer.mozilla.org/en-US/docs/Web/API/URL -/// ['Blob']: https://developer.mozilla.org/en-US/docs/Web/API/Blob -pub(crate) fn spawn_worker(name: &str, wasm_url: &str) -> Result { - let script = format!( - r#" -Error.stackTraceLimit = 99; - -let queued = []; -onconnect = (event) => {{ - console.log("Queued connection", event); - queued.push(event.ports[0]); -}} - -self.lumina = await import(self.location.origin + '{wasm_url}'); -await self.lumina.default(); -await self.lumina.run_worker(queued); -"# - ); - - let array = Array::new(); - array.push(&script.into()); - let blob = Blob::new_with_str_sequence_and_options( - &array, - BlobPropertyBag::new().type_("application/javascript"), - ) - .to_error("could not create js shim Blob")?; - - let url = - Url::create_object_url_with_blob(&blob).to_error("could not create js shim Blob Url")?; +/// Spawn a new SharedWorker. +pub(crate) fn spawn_worker(name: &str) -> Result { + let url = worker_script_url(); let mut opts = WorkerOptions::new(); opts.type_(WorkerType::Module); @@ -350,3 +320,9 @@ await self.lumina.run_worker(queued); Ok(worker) } + +#[wasm_bindgen(module = "/js/worker.js")] +extern "C" { + // must be called in order to include this script in generated package + fn worker_script_url() -> String; +} From d3a2c92544cd4a8cdcd98b0bcbe998423a29e76f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Florkiewicz?= Date: Thu, 16 May 2024 13:40:35 +0200 Subject: [PATCH 27/35] Add Worker/SharedWorker switch for browser that need it --- node-wasm/Cargo.toml | 6 +- node-wasm/js/worker.js | 19 ++- node-wasm/src/node.rs | 113 ++++++++----- node-wasm/src/utils.rs | 46 +++++- node-wasm/src/worker.rs | 37 ++--- node-wasm/src/worker/channel.rs | 284 ++++++++++++++++++++++++-------- 6 files changed, 358 insertions(+), 147 deletions(-) diff --git a/node-wasm/Cargo.toml b/node-wasm/Cargo.toml index 169b97be..a32c3be7 100644 --- a/node-wasm/Cargo.toml +++ b/node-wasm/Cargo.toml @@ -51,12 +51,16 @@ web-sys = { version = "0.3.69", features = [ "Blob", "BlobPropertyBag", "BroadcastChannel", + "DedicatedWorkerGlobalScope", "MessageEvent", "MessagePort", + "Navigator", "SharedWorker", "SharedWorkerGlobalScope", "Url", "Worker", + "WorkerGlobalScope", + "WorkerNavigator", "WorkerOptions", - "WorkerType", + "WorkerType" ] } diff --git a/node-wasm/js/worker.js b/node-wasm/js/worker.js index d41dfd5a..5fba1529 100644 --- a/node-wasm/js/worker.js +++ b/node-wasm/js/worker.js @@ -7,18 +7,23 @@ export function worker_script_url() { } // if we are in a worker -if ( - typeof WorkerGlobalScope !== 'undefined' - && self instanceof WorkerGlobalScope -) { +if (typeof WorkerGlobalScope !== 'undefined' && self instanceof WorkerGlobalScope) { Error.stackTraceLimit = 99; + // for SharedWorker we queue incoming connections + // for dedicated Workerwe queue incoming messages (coming from the single client) let queued = []; - onconnect = (event) => { - console.log("Queued connection", event); - queued.push(event.ports[0]); + if (typeof SharedWorkerGlobalScope !== 'undefined' && self instanceof SharedWorkerGlobalScope) { + onconnect = (event) => { + queued.push(event) + } + } else { + onmessage = (event) => { + queued.push(event); + } } await init(); + console.log("starting worker, queued messages: ", queued.length); await run_worker(queued); } diff --git a/node-wasm/src/node.rs b/node-wasm/src/node.rs index 5f40aba2..8c90d2ba 100644 --- a/node-wasm/src/node.rs +++ b/node-wasm/src/node.rs @@ -6,22 +6,22 @@ use libp2p::identity::Keypair; use libp2p::multiaddr::Protocol; use serde::{Deserialize, Serialize}; use serde_wasm_bindgen::to_value; -use tracing::error; +use tracing::info; use wasm_bindgen::prelude::*; -use web_sys::{MessageEvent, SharedWorker}; +use web_sys::{SharedWorker, Worker, WorkerOptions, WorkerType}; use lumina_node::blockstore::IndexedDbBlockstore; use lumina_node::network::{canonical_network_bootnodes, network_genesis, network_id}; use lumina_node::node::NodeConfig; use lumina_node::store::IndexedDbStore; -use crate::utils::{js_value_from_display, Network}; +use crate::utils::{is_chrome, js_value_from_display, JsValueToJsError, Network}; use crate::worker::commands::{CheckableResponseExt, NodeCommand, SingleHeaderQuery}; -use crate::worker::{spawn_worker, WorkerClient, WorkerError}; +use crate::worker::{worker_script_url, WorkerClient, WorkerError}; use crate::wrapper::libp2p::NetworkInfoSnapshot; use crate::Result; -const LUMINA_SHARED_WORKER_NAME: &str = "lumina"; +const LUMINA_WORKER_NAME: &str = "lumina"; /// Config for the lumina wasm node. #[wasm_bindgen(js_name = NodeConfig)] @@ -37,11 +37,25 @@ pub struct WasmNodeConfig { pub bootnodes: Vec, } +/// `NodeDriver` represents lumina node running in a dedicated Worker/SharedWorker. +/// It's responsible for sending commands and receiving responses from the node. #[wasm_bindgen(js_name = NodeClient)] struct NodeDriver { - _worker: SharedWorker, - _onerror_callback: Closure, - channel: WorkerClient, + client: WorkerClient, +} + +/// Type of worker to run lumina in. Allows overriding automatically detected worker kind +/// (which should usually be appropriate). +#[wasm_bindgen] +pub enum NodeWorkerKind { + /// Run in [`SharedWorker`] + /// + /// [`SharedWorker`]: https://developer.mozilla.org/en-US/docs/Web/API/SharedWorker + Shared, + /// Run in [`Worker`] + /// + /// [`Worker`]: https://developer.mozilla.org/en-US/docs/Web/API/Worker + Dedicated, } #[wasm_bindgen(js_class = NodeClient)] @@ -50,27 +64,40 @@ impl NodeDriver { /// Note that single Shared Worker can be accessed from multiple tabs, so Lumina may /// already have been started. Otherwise it needs to be started with [`NodeDriver::start`]. #[wasm_bindgen(constructor)] - pub async fn new() -> Result { - let worker = spawn_worker(LUMINA_SHARED_WORKER_NAME)?; - - let onerror_callback: Closure = Closure::new(|ev: MessageEvent| { - error!("received error from SharedWorker: {:?}", ev.to_string()); - }); - worker.set_onerror(Some(onerror_callback.as_ref().unchecked_ref())); + pub async fn new(worker_type: Option) -> Result { + let url = worker_script_url(); + let mut opts = WorkerOptions::new(); + opts.type_(WorkerType::Module); + opts.name(LUMINA_WORKER_NAME); + + let default_worker_type = if is_chrome() { + NodeWorkerKind::Dedicated + } else { + NodeWorkerKind::Shared + }; - let channel = WorkerClient::new(worker.port()); + let client = match worker_type.unwrap_or(default_worker_type) { + NodeWorkerKind::Shared => { + info!("Starting SharedWorker"); + let worker = SharedWorker::new_with_worker_options(&url, &opts) + .to_error("could not create SharedWorker")?; + WorkerClient::from(worker) + } + NodeWorkerKind::Dedicated => { + info!("Starting Worker"); + let worker = + Worker::new_with_options(&url, &opts).to_error("could not create Worker")?; + WorkerClient::from(worker) + } + }; - Ok(Self { - _worker: worker, - _onerror_callback: onerror_callback, - channel, - }) + Ok(Self { client }) } /// Check whether Lumina is currently running pub async fn is_running(&self) -> Result { let command = NodeCommand::IsRunning; - let response = self.channel.exec(command).await?; + let response = self.client.exec(command).await?; let running = response.into_is_running().check_variant()?; Ok(running) @@ -79,7 +106,7 @@ impl NodeDriver { /// Start a node with the provided config, if it's not running pub async fn start(&self, config: WasmNodeConfig) -> Result<()> { let command = NodeCommand::StartNode(config); - let response = self.channel.exec(command).await?; + let response = self.client.exec(command).await?; let started = response.into_node_started().check_variant()?; Ok(started?) @@ -88,7 +115,7 @@ impl NodeDriver { /// Get node's local peer ID. pub async fn local_peer_id(&self) -> Result { let command = NodeCommand::GetLocalPeerId; - let response = self.channel.exec(command).await?; + let response = self.client.exec(command).await?; let peer_id = response.into_local_peer_id().check_variant()?; Ok(peer_id) @@ -97,7 +124,7 @@ impl NodeDriver { /// Get current [`PeerTracker`] info. pub async fn peer_tracker_info(&self) -> Result { let command = NodeCommand::GetPeerTrackerInfo; - let response = self.channel.exec(command).await?; + let response = self.client.exec(command).await?; let peer_info = response.into_peer_tracker_info().check_variant()?; Ok(to_value(&peer_info)?) @@ -106,7 +133,7 @@ impl NodeDriver { /// Wait until the node is connected to at least 1 peer. pub async fn wait_connected(&self) -> Result<()> { let command = NodeCommand::WaitConnected { trusted: false }; - let response = self.channel.exec(command).await?; + let response = self.client.exec(command).await?; let result = response.into_connected().check_variant()?; Ok(result?) @@ -115,7 +142,7 @@ impl NodeDriver { /// Wait until the node is connected to at least 1 trusted peer. pub async fn wait_connected_trusted(&self) -> Result<()> { let command = NodeCommand::WaitConnected { trusted: true }; - let response = self.channel.exec(command).await?; + let response = self.client.exec(command).await?; let result = response.into_connected().check_variant()?; Ok(result?) @@ -124,7 +151,7 @@ impl NodeDriver { /// Get current network info. pub async fn network_info(&self) -> Result { let command = NodeCommand::GetNetworkInfo; - let response = self.channel.exec(command).await?; + let response = self.client.exec(command).await?; let network_info = response.into_network_info().check_variant()?; Ok(network_info?) @@ -133,7 +160,7 @@ impl NodeDriver { /// Get all the multiaddresses on which the node listens. pub async fn listeners(&self) -> Result { let command = NodeCommand::GetListeners; - let response = self.channel.exec(command).await?; + let response = self.client.exec(command).await?; let listeners = response.into_listeners().check_variant()?; let result = listeners?.iter().map(js_value_from_display).collect(); @@ -143,7 +170,7 @@ impl NodeDriver { /// Get all the peers that node is connected to. pub async fn connected_peers(&self) -> Result { let command = NodeCommand::GetConnectedPeers; - let response = self.channel.exec(command).await?; + let response = self.client.exec(command).await?; let peers = response.into_connected_peers().check_variant()?; let result = peers?.iter().map(js_value_from_display).collect(); @@ -156,7 +183,7 @@ impl NodeDriver { peer_id: peer_id.parse()?, is_trusted, }; - let response = self.channel.exec(command).await?; + let response = self.client.exec(command).await?; let set_result = response.into_set_peer_trust().check_variant()?; Ok(set_result?) @@ -165,7 +192,7 @@ impl NodeDriver { /// Request the head header from the network. pub async fn request_head_header(&self) -> Result { let command = NodeCommand::RequestHeader(SingleHeaderQuery::Head); - let response = self.channel.exec(command).await?; + let response = self.client.exec(command).await?; let header = response.into_header().check_variant()?; Ok(header.into_result()?) @@ -174,7 +201,7 @@ impl NodeDriver { /// Request a header for the block with a given hash from the network. pub async fn request_header_by_hash(&self, hash: &str) -> Result { let command = NodeCommand::RequestHeader(SingleHeaderQuery::ByHash(hash.parse()?)); - let response = self.channel.exec(command).await?; + let response = self.client.exec(command).await?; let header = response.into_header().check_variant()?; Ok(header.into_result()?) @@ -183,7 +210,7 @@ impl NodeDriver { /// Request a header for the block with a given height from the network. pub async fn request_header_by_height(&self, height: u64) -> Result { let command = NodeCommand::RequestHeader(SingleHeaderQuery::ByHeight(height)); - let response = self.channel.exec(command).await?; + let response = self.client.exec(command).await?; let header = response.into_header().check_variant()?; Ok(header.into_result()?) @@ -201,7 +228,7 @@ impl NodeDriver { from: from_header, amount, }; - let response = self.channel.exec(command).await?; + let response = self.client.exec(command).await?; let headers = response.into_headers().check_variant()?; Ok(headers.into_result()?) @@ -210,7 +237,7 @@ impl NodeDriver { /// Get current header syncing info. pub async fn syncer_info(&self) -> Result { let command = NodeCommand::GetSyncerInfo; - let response = self.channel.exec(command).await?; + let response = self.client.exec(command).await?; let syncer_info = response.into_syncer_info().check_variant()?; Ok(to_value(&syncer_info?)?) @@ -219,7 +246,7 @@ impl NodeDriver { /// Get the latest header announced in the network. pub async fn get_network_head_header(&self) -> Result { let command = NodeCommand::LastSeenNetworkHead; - let response = self.channel.exec(command).await?; + let response = self.client.exec(command).await?; let header = response.into_last_seen_network_head().check_variant()?; Ok(header) @@ -228,7 +255,7 @@ impl NodeDriver { /// Get the latest locally synced header. pub async fn get_local_head_header(&self) -> Result { let command = NodeCommand::GetHeader(SingleHeaderQuery::Head); - let response = self.channel.exec(command).await?; + let response = self.client.exec(command).await?; let header = response.into_header().check_variant()?; Ok(header.into_result()?) @@ -237,7 +264,7 @@ impl NodeDriver { /// Get a synced header for the block with a given hash. pub async fn get_header_by_hash(&self, hash: &str) -> Result { let command = NodeCommand::GetHeader(SingleHeaderQuery::ByHash(hash.parse()?)); - let response = self.channel.exec(command).await?; + let response = self.client.exec(command).await?; let header = response.into_header().check_variant()?; Ok(header.into_result()?) @@ -246,7 +273,7 @@ impl NodeDriver { /// Get a synced header for the block with a given height. pub async fn get_header_by_height(&self, height: u64) -> Result { let command = NodeCommand::GetHeader(SingleHeaderQuery::ByHeight(height)); - let response = self.channel.exec(command).await?; + let response = self.client.exec(command).await?; let header = response.into_header().check_variant()?; Ok(header.into_result()?) @@ -270,7 +297,7 @@ impl NodeDriver { start_height, end_height, }; - let response = self.channel.exec(command).await?; + let response = self.client.exec(command).await?; let headers = response.into_headers().check_variant()?; Ok(headers.into_result()?) @@ -279,7 +306,7 @@ impl NodeDriver { /// Get data sampling metadata of an already sampled height. pub async fn get_sampling_metadata(&self, height: u64) -> Result { let command = NodeCommand::GetSamplingMetadata { height }; - let response = self.channel.exec(command).await?; + let response = self.client.exec(command).await?; let metadata = response.into_sampling_metadata().check_variant()?; Ok(to_value(&metadata?)?) @@ -289,7 +316,7 @@ impl NodeDriver { /// be processed and new NodeClient needs to be created to restart a node. pub async fn close(&self) -> Result<()> { let command = NodeCommand::CloseWorker; - let response = self.channel.exec(command).await?; + let response = self.client.exec(command).await?; if response.is_worker_closed() { Ok(()) } else { diff --git a/node-wasm/src/utils.rs b/node-wasm/src/utils.rs index f940ef83..4d2cfffd 100644 --- a/node-wasm/src/utils.rs +++ b/node-wasm/src/utils.rs @@ -12,7 +12,10 @@ use tracing_subscriber::fmt::time::UtcTime; use tracing_subscriber::prelude::*; use tracing_web::{performance_layer, MakeConsoleWriter}; use wasm_bindgen::prelude::*; -use web_sys::{SharedWorker, SharedWorkerGlobalScope}; +use web_sys::{ + window, DedicatedWorkerGlobalScope, SharedWorker, SharedWorkerGlobalScope, Worker, + WorkerGlobalScope, +}; use lumina_node::network; @@ -104,6 +107,7 @@ pub(crate) trait WorkerSelf { type GlobalScope; fn worker_self() -> Self::GlobalScope; + fn is_worker_type() -> bool; } impl WorkerSelf for SharedWorker { @@ -112,6 +116,22 @@ impl WorkerSelf for SharedWorker { fn worker_self() -> Self::GlobalScope { JsValue::from(js_sys::global()).into() } + + fn is_worker_type() -> bool { + js_sys::global().has_type::() + } +} + +impl WorkerSelf for Worker { + type GlobalScope = DedicatedWorkerGlobalScope; + + fn worker_self() -> Self::GlobalScope { + JsValue::from(js_sys::global()).into() + } + + fn is_worker_type() -> bool { + js_sys::global().has_type::() + } } #[derive(Serialize, Deserialize, Debug)] @@ -161,3 +181,27 @@ where } } } + +const CHROME_USER_AGENT_DETECTION: &str = "Chrome/"; + +// currently there's issue with SharedWorkers on Chrome, where restarting lumina's worker +// causes all network connections to fail. Until that's resolved detect chrome and apply +// a workaround. +pub(crate) fn is_chrome() -> bool { + let mut user_agent = None; + if let Some(window) = window() { + user_agent = Some(window.navigator().user_agent()); + }; + if let Some(worker_scope) = JsValue::from(js_sys::global()).dyn_ref::() { + user_agent = Some(worker_scope.navigator().user_agent()); + } + + if let Some(user_agent) = user_agent { + user_agent + .as_deref() + .unwrap_or("") + .contains(CHROME_USER_AGENT_DETECTION) + } else { + false + } +} diff --git a/node-wasm/src/worker.rs b/node-wasm/src/worker.rs index b743280a..07a2242d 100644 --- a/node-wasm/src/worker.rs +++ b/node-wasm/src/worker.rs @@ -8,15 +8,17 @@ use thiserror::Error; use tokio::sync::mpsc; use tracing::{debug, error, info, warn}; use wasm_bindgen::prelude::*; -use web_sys::{MessagePort, SharedWorker, WorkerOptions, WorkerType}; +use web_sys::{MessageEvent, SharedWorker}; use lumina_node::node::{Node, NodeError}; use lumina_node::store::{IndexedDbStore, SamplingMetadata, Store, StoreError}; use lumina_node::syncer::SyncingInfo; use crate::node::WasmNodeConfig; -use crate::utils::{to_jsvalue_or_undefined, JsValueToJsError, WorkerSelf}; -use crate::worker::channel::{WorkerMessage, WorkerMessageServer}; +use crate::utils::{to_jsvalue_or_undefined, WorkerSelf}; +use crate::worker::channel::{ + DedicatedWorkerMessageServer, MessageServer, SharedWorkerMessageServer, WorkerMessage, +}; use crate::worker::commands::{NodeCommand, SingleHeaderQuery, WorkerResponse}; use crate::wrapper::libp2p::NetworkInfoSnapshot; @@ -251,16 +253,17 @@ impl NodeWorker { } #[wasm_bindgen] -pub async fn run_worker(queued_connections: Vec) { +pub async fn run_worker(queued_events: Vec) { info!("Entered run_worker"); let (tx, mut rx) = mpsc::channel(WORKER_MESSAGE_SERVER_INCOMING_QUEUE_LENGTH); - let mut message_server = WorkerMessageServer::new(tx.clone()); - for connection in queued_connections { - message_server.add(connection); - } + let mut message_server: Box = if SharedWorker::is_worker_type() { + Box::new(SharedWorkerMessageServer::new(tx.clone(), queued_events)) + } else { + Box::new(DedicatedWorkerMessageServer::new(tx.clone(), queued_events).await) + }; - info!("Entering SharedWorker message loop"); + info!("Entering worker message loop"); let mut worker = None; while let Some(message) = rx.recv().await { match message { @@ -307,22 +310,8 @@ pub async fn run_worker(queued_connections: Vec) { info!("Channel to WorkerMessageServer closed, exiting the SharedWorker"); } -/// Spawn a new SharedWorker. -pub(crate) fn spawn_worker(name: &str) -> Result { - let url = worker_script_url(); - - let mut opts = WorkerOptions::new(); - opts.type_(WorkerType::Module); - opts.name(name); - - let worker = SharedWorker::new_with_worker_options(&url, &opts) - .to_error("could not create SharedWorker")?; - - Ok(worker) -} - #[wasm_bindgen(module = "/js/worker.js")] extern "C" { // must be called in order to include this script in generated package - fn worker_script_url() -> String; + pub(crate) fn worker_script_url() -> String; } diff --git a/node-wasm/src/worker/channel.rs b/node-wasm/src/worker/channel.rs index 80759691..56896c53 100644 --- a/node-wasm/src/worker/channel.rs +++ b/node-wasm/src/worker/channel.rs @@ -6,7 +6,7 @@ use tokio::sync::{mpsc, Mutex}; use tracing::{debug, error, info, warn}; use wasm_bindgen::prelude::*; use wasm_bindgen_futures::spawn_local; -use web_sys::{MessageEvent, MessagePort, SharedWorker}; +use web_sys::{DedicatedWorkerGlobalScope, MessageEvent, MessagePort, SharedWorker, Worker}; use crate::utils::WorkerSelf; use crate::worker::commands::{NodeCommand, WorkerResponse}; @@ -15,24 +15,22 @@ use crate::worker::WorkerError; type WireMessage = Result; type WorkerClientConnection = (MessagePort, Closure); +/// Access to sending channel is protected by mutex to make sure we only can hold a single +/// writable instance from JS. Thus we expect to have at most 1 message in-flight. const WORKER_CHANNEL_SIZE: usize = 1; /// `WorkerClient` is responsible for sending messages to and receiving responses from [`WorkerMessageServer`]. /// It covers JS details like callbacks, having to synchronise requests and responses and exposes /// simple RPC-like function call interface. pub(crate) struct WorkerClient { - _onmessage: Closure, - message_port: MessagePort, + worker: AnyWorker, response_channel: Mutex>, + _onmessage: Closure, + _onerror: Closure, } -impl WorkerClient { - /// Create a new `WorkerClient` out of a [`MessagePort`] that should be connected - /// to a [`SharedWorker`] running lumina. - /// - /// [`SharedWorker`]: https://developer.mozilla.org/en-US/docs/Web/API/SharedWorker - /// [`MessagePort`]: https://developer.mozilla.org/en-US/docs/Web/API/MessagePort - pub fn new(message_port: MessagePort) -> Self { +impl From for WorkerClient { + fn from(worker: Worker) -> WorkerClient { let (response_tx, response_rx) = mpsc::channel(WORKER_CHANNEL_SIZE); let onmessage_callback = move |ev: MessageEvent| { @@ -53,15 +51,68 @@ impl WorkerClient { }; let onmessage = Closure::new(onmessage_callback); + worker.set_onmessage(Some(onmessage.as_ref().unchecked_ref())); + + let onerror = Closure::new(|ev: MessageEvent| { + error!("received error from Worker: {:?}", ev.to_string()); + }); + worker.set_onerror(Some(onerror.as_ref().unchecked_ref())); + + Self { + worker: AnyWorker::DedicatedWorker(worker), + response_channel: Mutex::new(response_rx), + _onmessage: onmessage, + _onerror: onerror, + } + } +} + +impl From for WorkerClient { + fn from(worker: SharedWorker) -> WorkerClient { + let (response_tx, response_rx) = mpsc::channel(WORKER_CHANNEL_SIZE); + + let onmessage_callback = move |ev: MessageEvent| { + let response_tx = response_tx.clone(); + spawn_local(async move { + let data: WireMessage = match from_value(ev.data()) { + Ok(jsvalue) => jsvalue, + Err(e) => { + error!("WorkerClient could not convert from JsValue: {e}"); + Err(WorkerError::CouldNotDeserialiseResponse(e.to_string())) + } + }; + + if let Err(e) = response_tx.send(data).await { + error!("message forwarding channel closed, should not happen: {e}"); + } + }) + }; + + let onmessage = Closure::new(onmessage_callback); + let message_port = worker.port(); message_port.set_onmessage(Some(onmessage.as_ref().unchecked_ref())); + + let onerror: Closure = Closure::new(|ev: MessageEvent| { + error!("received error from SharedWorker: {:?}", ev.to_string()); + }); + worker.set_onerror(Some(onerror.as_ref().unchecked_ref())); + message_port.start(); Self { + worker: AnyWorker::SharedWorker(worker), response_channel: Mutex::new(response_rx), _onmessage: onmessage, - message_port, + _onerror: onerror, } } +} +enum AnyWorker { + DedicatedWorker(Worker), + SharedWorker(SharedWorker), +} + +impl WorkerClient { /// Send command to lumina and wait for a response. /// /// Response enum variant can be converted into appropriate type at runtime with a provided @@ -82,13 +133,22 @@ impl WorkerClient { fn send(&self, command: NodeCommand) -> Result<(), WorkerError> { let command_value = to_value(&command).map_err(|e| WorkerError::CouldNotSerialiseCommand(e.to_string()))?; - self.message_port - .post_message(&command_value) - .map_err(|e| WorkerError::CouldNotSendCommand(format!("{:?}", e.dyn_ref::()))) + match &self.worker { + AnyWorker::DedicatedWorker(worker) => { + worker.post_message(&command_value).map_err(|e| { + WorkerError::CouldNotSendCommand(format!("{:?}", e.dyn_ref::())) + }) + } + AnyWorker::SharedWorker(worker) => { + worker.port().post_message(&command_value).map_err(|e| { + WorkerError::CouldNotSendCommand(format!("{:?}", e.dyn_ref::())) + }) + } + } } } -#[derive(Debug)] +#[derive(Debug, Clone, Copy)] pub(super) struct ClientId(usize); impl fmt::Display for ClientId { @@ -103,10 +163,23 @@ pub(super) enum WorkerMessage { Command((NodeCommand, ClientId)), } -pub(super) struct WorkerMessageServer { +pub(super) trait MessageServer { + fn send_response(&self, client: ClientId, message: WireMessage); + fn add(&mut self, port: MessagePort); + + fn respond_to(&self, client: ClientId, msg: WorkerResponse) { + self.send_response(client, Ok(msg)) + } + + fn respond_err_to(&self, client: ClientId, error: WorkerError) { + self.send_response(client, Err(error)) + } +} + +pub(super) struct SharedWorkerMessageServer { // same onconnect callback is used throughtout entire Worker lifetime. // Keep a reference to make sure it doesn't get dropped. - _onconnect_callback: Closure, + _onconnect: Closure, // keep a MessagePort for each client to send messages over, as well as callback responsible // for forwarding messages back @@ -116,70 +189,36 @@ pub(super) struct WorkerMessageServer { command_channel: mpsc::Sender, } -impl WorkerMessageServer { - pub fn new(command_channel: mpsc::Sender) -> Self { - let closure_command_channel = command_channel.clone(); - let onconnect: Closure = Closure::new(move |ev: MessageEvent| { - let command_channel = closure_command_channel.clone(); - spawn_local(async move { - let Ok(port) = ev.ports().at(0).dyn_into() else { - error!("received onconnect event without MessagePort, should not happen"); - return; - }; - - if let Err(e) = command_channel - .send(WorkerMessage::NewConnection(port)) - .await - { - error!("command channel inside worker closed, should not happen: {e}"); - } - }) - }); - +impl SharedWorkerMessageServer { + pub fn new(command_channel: mpsc::Sender, queued: Vec) -> Self { let worker_scope = SharedWorker::worker_self(); + let onconnect = get_client_connect_callback(command_channel.clone()); worker_scope.set_onconnect(Some(onconnect.as_ref().unchecked_ref())); - Self { - _onconnect_callback: onconnect, + let mut server = Self { + _onconnect: onconnect, clients: Vec::with_capacity(1), // we usually expect to have exactly one client command_channel, + }; + + for event in queued { + if let Ok(port) = event.ports().at(0).dyn_into() { + server.add(port); + } else { + error!("received onconnect event without MessagePort, should not happen"); + } } - } - pub fn respond_to(&self, client: ClientId, msg: WorkerResponse) { - self.send_response(client, Ok(msg)) + server } +} - pub fn respond_err_to(&self, client: ClientId, error: WorkerError) { - self.send_response(client, Err(error)) - } +impl MessageServer for SharedWorkerMessageServer { + fn add(&mut self, port: MessagePort) { + let client_id = ClientId(self.clients.len()); - pub fn add(&mut self, port: MessagePort) { - let client_id = self.clients.len(); - - let near_tx = self.command_channel.clone(); - let client_message_callback: Closure = - Closure::new(move |ev: MessageEvent| { - let local_tx = near_tx.clone(); - spawn_local(async move { - let client_id = ClientId(client_id); - - let message = match from_value(ev.data()) { - Ok(command) => { - debug!("received command from client {client_id}: {command:#?}"); - WorkerMessage::Command((command, client_id)) - } - Err(e) => { - warn!("could not deserialize message from client {client_id}: {e}"); - WorkerMessage::InvalidCommandReceived(client_id) - } - }; - - if let Err(e) = local_tx.send(message).await { - error!("command channel inside worker closed, should not happen: {e}"); - } - }) - }); + let client_message_callback = + get_client_message_callback(self.command_channel.clone(), client_id); self.clients.push((port, client_message_callback)); @@ -209,3 +248,106 @@ impl WorkerMessageServer { } } } + +pub(super) struct DedicatedWorkerMessageServer { + // same onmessage callback is used throughtout entire Worker lifetime. + // Keep a reference to make sure it doesn't get dropped. + _onmessage: Closure, + // global scope we use to send messages + worker: DedicatedWorkerGlobalScope, +} + +impl DedicatedWorkerMessageServer { + pub async fn new( + command_channel: mpsc::Sender, + queued: Vec, + ) -> Self { + for event in queued { + let message = parse_message_event_to_worker_message(event, ClientId(0)); + + if let Err(e) = command_channel.send(message).await { + error!("command channel inside worker closed, should not happen: {e}"); + } + } + + let worker = Worker::worker_self(); + let onmessage = get_client_message_callback(command_channel, ClientId(0)); + worker.set_onmessage(Some(onmessage.as_ref().unchecked_ref())); + + Self { + _onmessage: onmessage, + worker, + } + } +} + +impl MessageServer for DedicatedWorkerMessageServer { + fn add(&mut self, _port: MessagePort) { + warn!("DedicatedWorkerMessageServer::add called, should not happen"); + } + + fn send_response(&self, client: ClientId, message: WireMessage) { + let message = match to_value(&message) { + Ok(jsvalue) => jsvalue, + Err(e) => { + warn!("provided response could not be coverted to JsValue: {e}"); + to_value(&WorkerError::CouldNotSerialiseResponse(e.to_string())) + .expect("something's wrong, couldn't serialise serialisation error") + } + }; + + if let Err(e) = self.worker.post_message(&message) { + error!("could not post response message to client {client}: {e:?}"); + } + } +} + +fn get_client_connect_callback( + command_channel: mpsc::Sender, +) -> Closure { + Closure::new(move |ev: MessageEvent| { + let command_channel = command_channel.clone(); + spawn_local(async move { + let Ok(port) = ev.ports().at(0).dyn_into() else { + error!("received onconnect event without MessagePort, should not happen"); + return; + }; + + if let Err(e) = command_channel + .send(WorkerMessage::NewConnection(port)) + .await + { + error!("command channel inside worker closed, should not happen: {e}"); + } + }) + }) +} + +fn get_client_message_callback( + command_channel: mpsc::Sender, + client: ClientId, +) -> Closure { + Closure::new(move |ev: MessageEvent| { + let command_channel = command_channel.clone(); + spawn_local(async move { + let message = parse_message_event_to_worker_message(ev, client); + + if let Err(e) = command_channel.send(message).await { + error!("command channel inside worker closed, should not happen: {e}"); + } + }) + }) +} + +fn parse_message_event_to_worker_message(ev: MessageEvent, client: ClientId) -> WorkerMessage { + match from_value(ev.data()) { + Ok(command) => { + debug!("received command from client {client}: {command:#?}"); + WorkerMessage::Command((command, client)) + } + Err(e) => { + warn!("could not deserialize message from client {client}: {e}"); + WorkerMessage::InvalidCommandReceived(client) + } + } +} From 0867f4b27cbf80842e0b10fb33a7b59f7b9698e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Florkiewicz?= Date: Fri, 17 May 2024 09:24:30 +0200 Subject: [PATCH 28/35] Update node-wasm/js/worker.js MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Maciej ZwoliĊ„ski Signed-off-by: MikoĊ‚aj Florkiewicz --- node-wasm/js/worker.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node-wasm/js/worker.js b/node-wasm/js/worker.js index 5fba1529..75d09dcd 100644 --- a/node-wasm/js/worker.js +++ b/node-wasm/js/worker.js @@ -11,7 +11,7 @@ if (typeof WorkerGlobalScope !== 'undefined' && self instanceof WorkerGlobalScop Error.stackTraceLimit = 99; // for SharedWorker we queue incoming connections - // for dedicated Workerwe queue incoming messages (coming from the single client) + // for dedicated Worker we queue incoming messages (coming from the single client) let queued = []; if (typeof SharedWorkerGlobalScope !== 'undefined' && self instanceof SharedWorkerGlobalScope) { onconnect = (event) => { From db19449372a5a457bc95f285bb0e43fe295fd3f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Florkiewicz?= Date: Thu, 20 Jun 2024 21:02:34 +0200 Subject: [PATCH 29/35] rework errors to use new js errors and results --- node-wasm/Cargo.toml | 2 +- node-wasm/src/error.rs | 5 +- node-wasm/src/node.rs | 143 ++++++++++--------------------- node-wasm/src/utils.rs | 9 +- node-wasm/src/worker.rs | 119 +++++++++++++------------ node-wasm/src/worker/channel.rs | 23 +++-- node-wasm/src/worker/commands.rs | 13 +-- node/src/store/header_ranges.rs | 4 +- 8 files changed, 141 insertions(+), 177 deletions(-) diff --git a/node-wasm/Cargo.toml b/node-wasm/Cargo.toml index 3c023378..a15efdb3 100644 --- a/node-wasm/Cargo.toml +++ b/node-wasm/Cargo.toml @@ -51,7 +51,7 @@ web-sys = { version = "0.3.69", features = [ "Blob", "BlobPropertyBag", "BroadcastChannel", - "Crypto" + "Crypto", "DedicatedWorkerGlobalScope", "MessageEvent", "MessagePort", diff --git a/node-wasm/src/error.rs b/node-wasm/src/error.rs index f5d453dd..210b8640 100644 --- a/node-wasm/src/error.rs +++ b/node-wasm/src/error.rs @@ -2,6 +2,7 @@ use std::fmt::Display; +use serde::{Deserialize, Serialize}; use wasm_bindgen::convert::IntoWasmAbi; use wasm_bindgen::describe::WasmDescribe; use wasm_bindgen::JsValue; @@ -10,7 +11,8 @@ use wasm_bindgen::JsValue; pub type Result = std::result::Result; /// An error that can cross the WASM ABI border. -pub struct Error(JsValue); +#[derive(Debug, Serialize, Deserialize)] +pub struct Error(#[serde(with = "serde_wasm_bindgen::preserve")] JsValue); impl Error { /// Create a new `Error` with the specified message. @@ -113,6 +115,7 @@ from_display! { libp2p::multiaddr::Error, lumina_node::node::NodeError, lumina_node::store::StoreError, + crate::worker::WorkerError, } /// Utility to add more context to the [`Error`]. diff --git a/node-wasm/src/node.rs b/node-wasm/src/node.rs index 0df566a2..9d3740a6 100644 --- a/node-wasm/src/node.rs +++ b/node-wasm/src/node.rs @@ -1,30 +1,24 @@ //! A browser compatible wrappers for the [`lumina-node`]. -use std::result::Result as StdResult; use js_sys::Array; use libp2p::identity::Keypair; use libp2p::multiaddr::Protocol; use serde::{Deserialize, Serialize}; -use serde_wasm_bindgen::{from_value, to_value}; +use serde_wasm_bindgen::to_value; use tracing::info; use wasm_bindgen::prelude::*; -use web_sys::{SharedWorker, Worker, WorkerOptions, WorkerType, BroadcastChannel}; -use wasm_bindgen_futures::spawn_local; +use web_sys::{BroadcastChannel, SharedWorker, Worker, WorkerOptions, WorkerType}; use lumina_node::blockstore::IndexedDbBlockstore; -use lumina_node::events::{EventSubscriber, NodeEventInfo}; use lumina_node::network::{canonical_network_bootnodes, network_genesis, network_id}; -use lumina_node::node::{Node, NodeConfig}; -use lumina_node::store::{IndexedDbStore, SamplingStatus, Store}; +use lumina_node::node::NodeConfig; +use lumina_node::store::IndexedDbStore; +use crate::error::{Context, Result}; use crate::utils::{is_chrome, js_value_from_display, JsValueToJsError, Network}; use crate::worker::commands::{CheckableResponseExt, NodeCommand, SingleHeaderQuery}; use crate::worker::{worker_script_url, WorkerClient, WorkerError}; use crate::wrapper::libp2p::NetworkInfoSnapshot; -use crate::Result; -use crate::error::{Context, Result}; -use crate::utils::{get_crypto, js_value_from_display, Network}; -use crate::wrapper::libp2p::NetworkInfo; const LUMINA_WORKER_NAME: &str = "lumina"; @@ -47,7 +41,6 @@ pub struct WasmNodeConfig { #[wasm_bindgen(js_name = NodeClient)] struct NodeDriver { client: WorkerClient, - events_channel_name: String, } /// Type of worker to run lumina in. Allows overriding automatically detected worker kind @@ -81,13 +74,6 @@ impl NodeDriver { } else { NodeWorkerKind::Shared }; - - let events_channel_name = format!("NodeEventChannel-{}", get_crypto()?.random_uuid()); - let events_channel = BroadcastChannel::new(&events_channel_name) - .context("Failed to allocate BroadcastChannel")?; - - let events_sub = node.event_subscriber(); - spawn_local(event_forwarder_task(events_sub, events_channel)); let client = match worker_type.unwrap_or(default_worker_type) { NodeWorkerKind::Shared => { @@ -104,7 +90,7 @@ impl NodeDriver { } }; - Ok(Self { client, events_channel_name }) + Ok(Self { client }) } /// Check whether Lumina is currently running @@ -120,9 +106,9 @@ impl NodeDriver { pub async fn start(&self, config: WasmNodeConfig) -> Result<()> { let command = NodeCommand::StartNode(config); let response = self.client.exec(command).await?; - let started = response.into_node_started().check_variant()?; + let _ = response.into_node_started().check_variant()?; - Ok(started?) + Ok(()) } /// Get node's local peer ID. @@ -147,27 +133,24 @@ impl NodeDriver { pub async fn wait_connected(&self) -> Result<()> { let command = NodeCommand::WaitConnected { trusted: false }; let response = self.client.exec(command).await?; - let result = response.into_connected().check_variant()?; + let _ = response.into_connected().check_variant()?; - Ok(result?) + Ok(()) } /// Wait until the node is connected to at least 1 trusted peer. pub async fn wait_connected_trusted(&self) -> Result<()> { let command = NodeCommand::WaitConnected { trusted: true }; let response = self.client.exec(command).await?; - let result = response.into_connected().check_variant()?; - - Ok(result?) + response.into_connected().check_variant()? } /// Get current network info. pub async fn network_info(&self) -> Result { let command = NodeCommand::GetNetworkInfo; let response = self.client.exec(command).await?; - let network_info = response.into_network_info().check_variant()?; - Ok(network_info?) + response.into_network_info().check_variant()? } /// Get all the multiaddresses on which the node listens. @@ -197,9 +180,7 @@ impl NodeDriver { is_trusted, }; let response = self.client.exec(command).await?; - let set_result = response.into_set_peer_trust().check_variant()?; - - Ok(set_result?) + response.into_set_peer_trust().check_variant()? } /// Request the head header from the network. @@ -208,7 +189,7 @@ impl NodeDriver { let response = self.client.exec(command).await?; let header = response.into_header().check_variant()?; - Ok(header.into_result()?) + header.into_result() } /// Request a header for the block with a given hash from the network. @@ -217,7 +198,7 @@ impl NodeDriver { let response = self.client.exec(command).await?; let header = response.into_header().check_variant()?; - Ok(header.into_result()?) + header.into_result() } /// Request a header for the block with a given height from the network. @@ -226,18 +207,25 @@ impl NodeDriver { let response = self.client.exec(command).await?; let header = response.into_header().check_variant()?; - Ok(header.into_result()?) + header.into_result() } /// Request headers in range (from, from + amount] from the network. /// /// The headers will be verified with the `from` header. - pub async fn request_verified_headers(&self, from: JsValue, amount: u64) -> Result { - let header = - from_value::(from).context("Parsing extended header failed")?; - let verified_headers = self.node.request_verified_headers(&header, amount).await?; + pub async fn request_verified_headers( + &self, + from_header: JsValue, + amount: u64, + ) -> Result { + let command = NodeCommand::GetVerifiedHeaders { + from: from_header, + amount, + }; + let response = self.client.exec(command).await?; + let headers = response.into_headers().check_variant()?; - Ok(headers.into_result()?) + headers.into_result() } /// Get current header syncing info. @@ -264,18 +252,7 @@ impl NodeDriver { let response = self.client.exec(command).await?; let header = response.into_header().check_variant()?; - Ok(header.into_result()?) - } - - /// Get ranges of headers currently stored. - pub async fn get_stored_header_ranges(&self) -> Result { - let ranges = self.node.get_stored_header_ranges().await?; - - Ok(ranges - .as_ref() - .iter() - .map(to_value) - .collect::>()?) + header.into_result() } /// Get a synced header for the block with a given hash. @@ -284,7 +261,7 @@ impl NodeDriver { let response = self.client.exec(command).await?; let header = response.into_header().check_variant()?; - Ok(header.into_result()?) + header.into_result() } /// Get a synced header for the block with a given height. @@ -293,7 +270,7 @@ impl NodeDriver { let response = self.client.exec(command).await?; let header = response.into_header().check_variant()?; - Ok(header.into_result()?) + header.into_result() } /// Get synced headers from the given heights range. @@ -317,7 +294,7 @@ impl NodeDriver { let response = self.client.exec(command).await?; let headers = response.into_headers().check_variant()?; - Ok(headers.into_result()?) + headers.into_result() } /// Get data sampling metadata of an already sampled height. @@ -334,18 +311,18 @@ impl NodeDriver { pub async fn close(&self) -> Result<()> { let command = NodeCommand::CloseWorker; let response = self.client.exec(command).await?; - if response.is_worker_closed() { - Ok(()) - } else { - Err(JsError::new( - "invalid response received for the command sent", - )) - } + response.into_worker_closed().check_variant()?; + + Ok(()) } /// Returns a [`BroadcastChannel`] for events generated by [`Node`]. - pub fn events_channel(&self) -> Result { - Ok(BroadcastChannel::new(&self.events_channel_name).unwrap()) + pub async fn events_channel(&self) -> Result { + let command = NodeCommand::GetEventsChannelName; + let response = self.client.exec(command).await?; + let name = response.into_events_channel_name().check_variant()?; + + Ok(BroadcastChannel::new(&name).unwrap()) } } @@ -367,17 +344,12 @@ impl WasmNodeConfig { self, ) -> Result, WorkerError> { let network_id = network_id(self.network.into()); -<<<<<<< HEAD - let store = IndexedDbStore::new(network_id).await?; - let blockstore = IndexedDbBlockstore::new(&format!("{network_id}-blockstore")).await?; -======= let store = IndexedDbStore::new(network_id) .await .context("Failed to open the store")?; let blockstore = IndexedDbBlockstore::new(&format!("{network_id}-blockstore")) .await .context("Failed to open the blockstore")?; ->>>>>>> origin/main let p2p_local_keypair = Keypair::generate_ed25519(); @@ -385,15 +357,14 @@ impl WasmNodeConfig { .genesis_hash .map(|h| h.parse()) .transpose() - .map_err(|e| WorkerError::NodeSetupFailed(format!("genesis hash invalid: {e}")))?; + .context("genesis hash invalid")?; + let p2p_bootnodes = self .bootnodes .iter() .map(|addr| addr.parse()) - .collect::>() - .map_err(|e| { - WorkerError::NodeSetupFailed(format!("bootstrap multiaddr invalid: {e}")) - })?; + .collect::>() + .context("bootstrap multiaddr invalid")?; Ok(NodeConfig { network_id: network_id.to_string(), @@ -406,27 +377,3 @@ impl WasmNodeConfig { }) } } - -async fn event_forwarder_task(mut events_sub: EventSubscriber, events_channel: BroadcastChannel) { - #[derive(Serialize)] - struct Event { - message: String, - #[serde(flatten)] - info: NodeEventInfo, - } - - while let Ok(ev) = events_sub.recv().await { - let ev = Event { - message: ev.event.to_string(), - info: ev, - }; - - if let Ok(val) = to_value(&ev) { - if events_channel.post_message(&val).is_err() { - break; - } - } - } - - events_channel.close(); -} diff --git a/node-wasm/src/utils.rs b/node-wasm/src/utils.rs index f423f4b8..e94987a4 100644 --- a/node-wasm/src/utils.rs +++ b/node-wasm/src/utils.rs @@ -2,6 +2,7 @@ use std::fmt::{self, Debug}; use js_sys::JsString; +use lumina_node::network; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; use serde_repr::{Deserialize_repr, Serialize_repr}; @@ -13,8 +14,8 @@ use tracing_subscriber::prelude::*; use tracing_web::{performance_layer, MakeConsoleWriter}; use wasm_bindgen::prelude::*; use web_sys::{ - window, DedicatedWorkerGlobalScope, SharedWorker, SharedWorkerGlobalScope, Worker, - WorkerGlobalScope, Crypto + window, Crypto, DedicatedWorkerGlobalScope, SharedWorker, SharedWorkerGlobalScope, Worker, + WorkerGlobalScope, }; use crate::error::{Context, Error, Result}; @@ -182,7 +183,7 @@ where } } -const CHROME_USER_AGENT_DETECTION: &str = "Chrome/"; +const CHROME_USER_AGENT_DETECTION_STR: &str = "Chrome/"; // currently there's issue with SharedWorkers on Chrome, where restarting lumina's worker // causes all network connections to fail. Until that's resolved detect chrome and apply @@ -200,7 +201,7 @@ pub(crate) fn is_chrome() -> bool { user_agent .as_deref() .unwrap_or("") - .contains(CHROME_USER_AGENT_DETECTION) + .contains(CHROME_USER_AGENT_DETECTION_STR) } else { false } diff --git a/node-wasm/src/worker.rs b/node-wasm/src/worker.rs index 07a2242d..f5401775 100644 --- a/node-wasm/src/worker.rs +++ b/node-wasm/src/worker.rs @@ -2,20 +2,23 @@ use std::fmt::Debug; use js_sys::Array; use libp2p::{Multiaddr, PeerId}; +use lumina_node::events::{EventSubscriber, NodeEventInfo}; use serde::{Deserialize, Serialize}; use serde_wasm_bindgen::{from_value, to_value}; use thiserror::Error; use tokio::sync::mpsc; use tracing::{debug, error, info, warn}; use wasm_bindgen::prelude::*; -use web_sys::{MessageEvent, SharedWorker}; +use wasm_bindgen_futures::spawn_local; +use web_sys::{BroadcastChannel, MessageEvent, SharedWorker}; -use lumina_node::node::{Node, NodeError}; -use lumina_node::store::{IndexedDbStore, SamplingMetadata, Store, StoreError}; +use lumina_node::node::Node; +use lumina_node::store::{IndexedDbStore, SamplingMetadata, Store}; use lumina_node::syncer::SyncingInfo; +use crate::error::{Context, Error, Result}; use crate::node::WasmNodeConfig; -use crate::utils::{to_jsvalue_or_undefined, WorkerSelf}; +use crate::utils::{get_crypto, to_jsvalue_or_undefined, WorkerSelf}; use crate::worker::channel::{ DedicatedWorkerMessageServer, MessageServer, SharedWorkerMessageServer, WorkerMessage, }; @@ -29,67 +32,35 @@ pub(crate) use channel::WorkerClient; const WORKER_MESSAGE_SERVER_INCOMING_QUEUE_LENGTH: usize = 64; -/// actual type that's sent over javascript MessagePort -type Result = std::result::Result; - #[derive(Debug, Serialize, Deserialize, Error)] pub enum WorkerError { #[error("node hasn't been started yet")] NodeNotRunning, - #[error("node has already been started")] - NodeAlreadyRunning, - #[error("setting up node failed: {0}")] - NodeSetupFailed(String), - #[error("node error: {0}")] - NodeError(String), - #[error("value could not be serialized: {0}")] - SerdeError(String), - #[error("command message could not be serialised, should not happen: {0}")] - CouldNotSerialiseCommand(String), - #[error("command message could not be deserialised, should not happen: {0}")] - CouldNotDeserialiseCommand(String), #[error("command response could not be serialised, should not happen: {0}")] CouldNotSerialiseResponse(String), #[error("command response could not be deserialised, should not happen: {0}")] CouldNotDeserialiseResponse(String), - #[error("response message could not be sent: {0}")] - CouldNotSendCommand(String), #[error("response channel from worker closed, should not happen")] ResponseChannelDropped, #[error("invalid command received")] InvalidCommandReceived, + #[error("Worker encountered an error: {0:?}")] + NodeError(Error), } -impl From for WorkerError { - fn from(error: blockstore::Error) -> Self { - WorkerError::NodeSetupFailed(format!("could not create blockstore: {error}")) - } -} - -impl From for WorkerError { - fn from(error: StoreError) -> Self { - WorkerError::NodeSetupFailed(format!("could not create header store: {error}")) - } -} - -impl From for WorkerError { - fn from(error: NodeError) -> Self { - WorkerError::NodeError(error.to_string()) - } -} - -impl From for WorkerError { - fn from(error: serde_wasm_bindgen::Error) -> Self { - WorkerError::SerdeError(error.to_string()) +impl From for WorkerError { + fn from(error: crate::error::Error) -> Self { + WorkerError::NodeError(error) } } struct NodeWorker { node: Node, + events_channel_name: String, } impl NodeWorker { - async fn new(config: WasmNodeConfig) -> Result { + async fn new(config: WasmNodeConfig) -> Result { let config = config.into_node_config().await?; if let Ok(store_height) = config.store.head_height().await { @@ -100,7 +71,17 @@ impl NodeWorker { let node = Node::new(config).await?; - Ok(Self { node }) + let events_channel_name = format!("NodeEventChannel-{}", get_crypto()?.random_uuid()); + let events_channel = BroadcastChannel::new(&events_channel_name) + .context("Failed to allocate BroadcastChannel")?; + + let events_sub = node.event_subscriber(); + spawn_local(event_forwarder_task(events_sub, events_channel)); + + Ok(Self { + node, + events_channel_name, + }) } async fn get_syncer_info(&mut self) -> Result { @@ -144,7 +125,7 @@ impl NodeWorker { SingleHeaderQuery::ByHash(hash) => self.node.request_header_by_hash(&hash).await, SingleHeaderQuery::ByHeight(height) => self.node.request_header_by_height(height).await, }?; - to_value(&header).map_err(|e| WorkerError::CouldNotSerialiseResponse(e.to_string())) + to_value(&header).context("could not serialise requested header") } async fn get_header(&mut self, query: SingleHeaderQuery) -> Result { @@ -153,18 +134,22 @@ impl NodeWorker { SingleHeaderQuery::ByHash(hash) => self.node.get_header_by_hash(&hash).await, SingleHeaderQuery::ByHeight(height) => self.node.get_header_by_height(height).await, }?; - to_value(&header).map_err(|e| WorkerError::CouldNotSerialiseResponse(e.to_string())) + to_value(&header).context("could not serialise requested header") } async fn get_verified_headers(&mut self, from: JsValue, amount: u64) -> Result { let verified_headers = self .node - .request_verified_headers(&from_value(from)?, amount) + .request_verified_headers( + &from_value(from).context("could not deserialise verified header")?, + amount, + ) .await?; - Ok(verified_headers + verified_headers .iter() .map(|h| to_value(&h)) - .collect::>()?) + .collect::>() + .context("could not serialise fetched headers") } async fn get_headers_range( @@ -179,10 +164,11 @@ impl NodeWorker { (Some(start), Some(end)) => self.node.get_headers(start..=end).await, }?; - Ok(headers + headers .iter() .map(|h| to_value(&h)) - .collect::>()?) + .collect::>() + .context("could not serialise fetched headers") } async fn get_last_seen_network_head(&mut self) -> JsValue { @@ -197,11 +183,14 @@ impl NodeWorker { match command { NodeCommand::IsRunning => WorkerResponse::IsRunning(true), NodeCommand::StartNode(_) => { - WorkerResponse::NodeStarted(Err(WorkerError::NodeAlreadyRunning)) + WorkerResponse::NodeStarted(Err(Error::new("Node already started"))) } NodeCommand::GetLocalPeerId => { WorkerResponse::LocalPeerId(self.node.local_peer_id().to_string()) } + NodeCommand::GetEventsChannelName => { + WorkerResponse::EventsChannelName(self.events_channel_name.clone()) + } NodeCommand::GetSyncerInfo => WorkerResponse::SyncerInfo(self.get_syncer_info().await), NodeCommand::GetPeerTrackerInfo => { let peer_tracker_info = self.node.peer_tracker_info(); @@ -246,7 +235,7 @@ impl NodeWorker { } NodeCommand::CloseWorker => { SharedWorker::worker_self().close(); - WorkerResponse::WorkerClosed + WorkerResponse::WorkerClosed(()) } } } @@ -310,6 +299,30 @@ pub async fn run_worker(queued_events: Vec) { info!("Channel to WorkerMessageServer closed, exiting the SharedWorker"); } +async fn event_forwarder_task(mut events_sub: EventSubscriber, events_channel: BroadcastChannel) { + #[derive(Serialize)] + struct Event { + message: String, + #[serde(flatten)] + info: NodeEventInfo, + } + + while let Ok(ev) = events_sub.recv().await { + let ev = Event { + message: ev.event.to_string(), + info: ev, + }; + + if let Ok(val) = to_value(&ev) { + if events_channel.post_message(&val).is_err() { + break; + } + } + } + + events_channel.close(); +} + #[wasm_bindgen(module = "/js/worker.js")] extern "C" { // must be called in order to include this script in generated package diff --git a/node-wasm/src/worker/channel.rs b/node-wasm/src/worker/channel.rs index 56896c53..0e729115 100644 --- a/node-wasm/src/worker/channel.rs +++ b/node-wasm/src/worker/channel.rs @@ -1,6 +1,5 @@ use std::fmt::{self, Debug}; -use js_sys::JsString; use serde_wasm_bindgen::{from_value, to_value}; use tokio::sync::{mpsc, Mutex}; use tracing::{debug, error, info, warn}; @@ -8,6 +7,7 @@ use wasm_bindgen::prelude::*; use wasm_bindgen_futures::spawn_local; use web_sys::{DedicatedWorkerGlobalScope, MessageEvent, MessagePort, SharedWorker, Worker}; +use crate::error::{Context, Result}; use crate::utils::WorkerSelf; use crate::worker::commands::{NodeCommand, WorkerResponse}; use crate::worker::WorkerError; @@ -130,20 +130,17 @@ impl WorkerClient { message } - fn send(&self, command: NodeCommand) -> Result<(), WorkerError> { + fn send(&self, command: NodeCommand) -> Result<()> { let command_value = - to_value(&command).map_err(|e| WorkerError::CouldNotSerialiseCommand(e.to_string()))?; + to_value(&command).context("could not serialise worker command to be sent")?; match &self.worker { - AnyWorker::DedicatedWorker(worker) => { - worker.post_message(&command_value).map_err(|e| { - WorkerError::CouldNotSendCommand(format!("{:?}", e.dyn_ref::())) - }) - } - AnyWorker::SharedWorker(worker) => { - worker.port().post_message(&command_value).map_err(|e| { - WorkerError::CouldNotSendCommand(format!("{:?}", e.dyn_ref::())) - }) - } + AnyWorker::DedicatedWorker(worker) => worker + .post_message(&command_value) + .context("could not send command to worker"), + AnyWorker::SharedWorker(worker) => worker + .port() + .post_message(&command_value) + .context("could not send command to worker"), } } } diff --git a/node-wasm/src/worker/commands.rs b/node-wasm/src/worker/commands.rs index f4afcf6e..f35ef8d2 100644 --- a/node-wasm/src/worker/commands.rs +++ b/node-wasm/src/worker/commands.rs @@ -13,16 +13,17 @@ use lumina_node::peer_tracker::PeerTrackerInfo; use lumina_node::store::SamplingMetadata; use lumina_node::syncer::SyncingInfo; +use crate::error::Error; +use crate::error::Result; use crate::node::WasmNodeConfig; use crate::utils::JsResult; -use crate::worker::Result; -use crate::worker::WorkerError; use crate::wrapper::libp2p::NetworkInfoSnapshot; #[derive(Debug, Serialize, Deserialize)] pub(crate) enum NodeCommand { IsRunning, StartNode(WasmNodeConfig), + GetEventsChannelName, GetLocalPeerId, GetSyncerInfo, GetPeerTrackerInfo, @@ -65,6 +66,7 @@ pub(crate) enum SingleHeaderQuery { pub(crate) enum WorkerResponse { IsRunning(bool), NodeStarted(Result<()>), + EventsChannelName(String), LocalPeerId(String), SyncerInfo(Result), PeerTrackerInfo(PeerTrackerInfo), @@ -73,16 +75,17 @@ pub(crate) enum WorkerResponse { SetPeerTrust(Result<()>), Connected(Result<()>), Listeners(Result>), - Header(JsResult), - Headers(JsResult), + Header(JsResult), + Headers(JsResult), #[serde(with = "serde_wasm_bindgen::preserve")] LastSeenNetworkHead(JsValue), SamplingMetadata(Result>), - WorkerClosed, + WorkerClosed(()), } pub(crate) trait CheckableResponseExt { type Output; + fn check_variant(self) -> Result; } diff --git a/node/src/store/header_ranges.rs b/node/src/store/header_ranges.rs index 93e7460f..0047bf85 100644 --- a/node/src/store/header_ranges.rs +++ b/node/src/store/header_ranges.rs @@ -5,7 +5,7 @@ use std::vec; #[cfg(any(test, feature = "test-utils"))] use celestia_types::test_utils::ExtendedHeaderGenerator; use celestia_types::ExtendedHeader; -use serde::Serialize; +use serde::{Deserialize, Serialize}; use smallvec::{IntoIter, SmallVec}; use crate::store::utils::{ranges_intersection, try_consolidate_ranges, RangeScanResult}; @@ -119,7 +119,7 @@ impl RangeLengthExt for RangeInclusive { } /// Represents possibly multiple non-overlapping, sorted ranges of header heights -#[derive(Debug, Clone, PartialEq, Default, Serialize)] +#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)] pub struct HeaderRanges(SmallVec<[HeaderRange; 2]>); pub(crate) trait HeaderRangesExt { From 0d3d7e53be6d7a407035c0131c68e226a613f28d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Florkiewicz?= Date: Fri, 21 Jun 2024 09:46:03 +0200 Subject: [PATCH 30/35] Commit to the bit --- node-wasm/Cargo.toml | 2 +- node-wasm/src/node.rs | 4 +-- node-wasm/src/worker.rs | 19 ++++------- node-wasm/src/worker/channel.rs | 57 ++++++++++++++++----------------- 4 files changed, 37 insertions(+), 45 deletions(-) diff --git a/node-wasm/Cargo.toml b/node-wasm/Cargo.toml index a15efdb3..53b55476 100644 --- a/node-wasm/Cargo.toml +++ b/node-wasm/Cargo.toml @@ -41,7 +41,7 @@ serde-wasm-bindgen = "0.6.5" serde_repr = "0.1.19" thiserror = "1.0" time = { version = "0.3.36", features = ["wasm-bindgen"] } -tokio = { version = "*", features = ["sync"] } +tokio = { version = "1.38.0", features = ["sync"] } tracing = "0.1.40" tracing-subscriber = { version = "0.3.18", features = ["time"] } tracing-web = "0.1.3" diff --git a/node-wasm/src/node.rs b/node-wasm/src/node.rs index 9d3740a6..a4a5fcd3 100644 --- a/node-wasm/src/node.rs +++ b/node-wasm/src/node.rs @@ -17,7 +17,7 @@ use lumina_node::store::IndexedDbStore; use crate::error::{Context, Result}; use crate::utils::{is_chrome, js_value_from_display, JsValueToJsError, Network}; use crate::worker::commands::{CheckableResponseExt, NodeCommand, SingleHeaderQuery}; -use crate::worker::{worker_script_url, WorkerClient, WorkerError}; +use crate::worker::{worker_script_url, WorkerClient}; use crate::wrapper::libp2p::NetworkInfoSnapshot; const LUMINA_WORKER_NAME: &str = "lumina"; @@ -342,7 +342,7 @@ impl WasmNodeConfig { pub(crate) async fn into_node_config( self, - ) -> Result, WorkerError> { + ) -> Result> { let network_id = network_id(self.network.into()); let store = IndexedDbStore::new(network_id) .await diff --git a/node-wasm/src/worker.rs b/node-wasm/src/worker.rs index f5401775..e1179a1b 100644 --- a/node-wasm/src/worker.rs +++ b/node-wasm/src/worker.rs @@ -34,26 +34,21 @@ const WORKER_MESSAGE_SERVER_INCOMING_QUEUE_LENGTH: usize = 64; #[derive(Debug, Serialize, Deserialize, Error)] pub enum WorkerError { + /// Worker is initialised, but the node has not been started yet. Use [`NodeDriver::start`]. #[error("node hasn't been started yet")] NodeNotRunning, - #[error("command response could not be serialised, should not happen: {0}")] - CouldNotSerialiseResponse(String), - #[error("command response could not be deserialised, should not happen: {0}")] - CouldNotDeserialiseResponse(String), - #[error("response channel from worker closed, should not happen")] - ResponseChannelDropped, + /// Communication with worker has been broken and we're unable to send or receive messages from it. + /// Try creating new [`NodeDriver`] instance. + #[error("error trying to communicate with worker")] + WorkerCommunicationError(Error), + /// Worker received unrecognised command #[error("invalid command received")] InvalidCommandReceived, + /// Worker encountered error coming from lumina-node #[error("Worker encountered an error: {0:?}")] NodeError(Error), } -impl From for WorkerError { - fn from(error: crate::error::Error) -> Self { - WorkerError::NodeError(error) - } -} - struct NodeWorker { node: Node, events_channel_name: String, diff --git a/node-wasm/src/worker/channel.rs b/node-wasm/src/worker/channel.rs index 0e729115..7a0210e3 100644 --- a/node-wasm/src/worker/channel.rs +++ b/node-wasm/src/worker/channel.rs @@ -7,7 +7,7 @@ use wasm_bindgen::prelude::*; use wasm_bindgen_futures::spawn_local; use web_sys::{DedicatedWorkerGlobalScope, MessageEvent, MessagePort, SharedWorker, Worker}; -use crate::error::{Context, Result}; +use crate::error::{Context, Error, Result}; use crate::utils::WorkerSelf; use crate::worker::commands::{NodeCommand, WorkerResponse}; use crate::worker::WorkerError; @@ -40,7 +40,8 @@ impl From for WorkerClient { Ok(jsvalue) => jsvalue, Err(e) => { error!("WorkerClient could not convert from JsValue: {e}"); - Err(WorkerError::CouldNotDeserialiseResponse(e.to_string())) + let error = Error::from(e).context("could not deserialise worker response"); + Err(WorkerError::WorkerCommunicationError(error)) } }; @@ -78,7 +79,8 @@ impl From for WorkerClient { Ok(jsvalue) => jsvalue, Err(e) => { error!("WorkerClient could not convert from JsValue: {e}"); - Err(WorkerError::CouldNotDeserialiseResponse(e.to_string())) + let error = Error::from(e).context("could not deserialise worker response"); + Err(WorkerError::WorkerCommunicationError(error)) } }; @@ -121,12 +123,13 @@ impl WorkerClient { /// [`CheckableResponseExt`]: crate::utils::CheckableResponseExt pub async fn exec(&self, command: NodeCommand) -> Result { let mut response_channel = self.response_channel.lock().await; - self.send(command)?; + self.send(command) + .map_err(WorkerError::WorkerCommunicationError)?; + + let message: WireMessage = response_channel.recv().await.ok_or_else(|| { + WorkerError::WorkerCommunicationError(Error::new("worker response channel dropped")) + })?; - let message: WireMessage = response_channel - .recv() - .await - .ok_or(WorkerError::ResponseChannelDropped)?; message } @@ -171,6 +174,18 @@ pub(super) trait MessageServer { fn respond_err_to(&self, client: ClientId, error: WorkerError) { self.send_response(client, Err(error)) } + + fn prepare_message(&self, message: &WireMessage) -> JsValue { + match to_value(message) { + Ok(jsvalue) => jsvalue, + Err(e) => { + warn!("provided response could not be coverted to JsValue: {e}"); + let error = Error::from(e).context("couldn't serialise worker response"); + to_value(&WorkerError::WorkerCommunicationError(error)) + .expect("something's very wrong, couldn't serialise serialisation error") + } + } + } } pub(super) struct SharedWorkerMessageServer { @@ -226,21 +241,12 @@ impl MessageServer for SharedWorkerMessageServer { } fn send_response(&self, client: ClientId, message: WireMessage) { - let message = match to_value(&message) { - Ok(jsvalue) => jsvalue, - Err(e) => { - warn!("provided response could not be coverted to JsValue: {e}"); - to_value(&WorkerError::CouldNotSerialiseResponse(e.to_string())) - .expect("something's wrong, couldn't serialise serialisation error") - } - }; - let Some((client_port, _)) = self.clients.get(client.0) else { - error!("client {client} not found on client list, should not happen"); + error!("client {client} not found on the client list, should not happen"); return; }; - if let Err(e) = client_port.post_message(&message) { + if let Err(e) = client_port.post_message(&self.prepare_message(&message)) { error!("could not post response message to client {client}: {e:?}"); } } @@ -280,20 +286,11 @@ impl DedicatedWorkerMessageServer { impl MessageServer for DedicatedWorkerMessageServer { fn add(&mut self, _port: MessagePort) { - warn!("DedicatedWorkerMessageServer::add called, should not happen"); + error!("DedicatedWorkerMessageServer::add called, should not happen"); } fn send_response(&self, client: ClientId, message: WireMessage) { - let message = match to_value(&message) { - Ok(jsvalue) => jsvalue, - Err(e) => { - warn!("provided response could not be coverted to JsValue: {e}"); - to_value(&WorkerError::CouldNotSerialiseResponse(e.to_string())) - .expect("something's wrong, couldn't serialise serialisation error") - } - }; - - if let Err(e) = self.worker.post_message(&message) { + if let Err(e) = self.worker.post_message(&self.prepare_message(&message)) { error!("could not post response message to client {client}: {e:?}"); } } From 1743bf9392edb5bc01629dc1eea41fdcedd91246 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Florkiewicz?= Date: Sat, 22 Jun 2024 13:06:21 +0200 Subject: [PATCH 31/35] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Yiannis Marangos Signed-off-by: MikoĊ‚aj Florkiewicz --- node-wasm/Cargo.toml | 5 ----- node-wasm/js/worker.js | 2 +- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/node-wasm/Cargo.toml b/node-wasm/Cargo.toml index 53b55476..69d8fc6a 100644 --- a/node-wasm/Cargo.toml +++ b/node-wasm/Cargo.toml @@ -48,20 +48,15 @@ tracing-web = "0.1.3" wasm-bindgen = "0.2.92" wasm-bindgen-futures = "0.4.42" web-sys = { version = "0.3.69", features = [ - "Blob", - "BlobPropertyBag", "BroadcastChannel", "Crypto", "DedicatedWorkerGlobalScope", "MessageEvent", "MessagePort", - "Navigator", "SharedWorker", "SharedWorkerGlobalScope", - "Url", "Worker", "WorkerGlobalScope", - "WorkerNavigator", "WorkerOptions", "WorkerType" ] } diff --git a/node-wasm/js/worker.js b/node-wasm/js/worker.js index 75d09dcd..5ac77858 100644 --- a/node-wasm/js/worker.js +++ b/node-wasm/js/worker.js @@ -1,4 +1,4 @@ -// this file should be installed by wasm-pack in pkg/snippets/-/js/ +// this file will be installed by wasm-pack in pkg/snippets/-/js/ import init, { run_worker } from '../../../lumina_node_wasm.js'; // get the path to this file From 614cf9e3d5464f23a02d963a503ec82e94013cf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Florkiewicz?= Date: Sat, 22 Jun 2024 13:06:44 +0200 Subject: [PATCH 32/35] channel simplification 1/2 --- node-wasm/Cargo.toml | 10 +-- node-wasm/src/node.rs | 6 +- node-wasm/src/utils.rs | 30 +++----- node-wasm/src/worker/channel.rs | 132 ++++++++++++++++---------------- 4 files changed, 85 insertions(+), 93 deletions(-) diff --git a/node-wasm/Cargo.toml b/node-wasm/Cargo.toml index 69d8fc6a..2ee18503 100644 --- a/node-wasm/Cargo.toml +++ b/node-wasm/Cargo.toml @@ -31,15 +31,15 @@ lumina-node = { workspace = true } anyhow = "1.0.86" console_error_panic_hook = "0.1.7" -enum-as-inner = "0.6" -futures = "0.3" -gloo-timers = "0.3" -instant = "0.1" +enum-as-inner = "0.6.0" +futures = "0.3.30" +gloo-timers = "0.3.0" +instant = "0.1.13" js-sys = "0.3.69" serde = { version = "1.0.203", features = ["derive"] } serde-wasm-bindgen = "0.6.5" serde_repr = "0.1.19" -thiserror = "1.0" +thiserror = "1.0.61" time = { version = "0.3.36", features = ["wasm-bindgen"] } tokio = { version = "1.38.0", features = ["sync"] } tracing = "0.1.40" diff --git a/node-wasm/src/node.rs b/node-wasm/src/node.rs index a4a5fcd3..16de76ea 100644 --- a/node-wasm/src/node.rs +++ b/node-wasm/src/node.rs @@ -69,7 +69,7 @@ impl NodeDriver { opts.type_(WorkerType::Module); opts.name(LUMINA_WORKER_NAME); - let default_worker_type = if is_chrome() { + let default_worker_type = if is_chrome().unwrap_or(false) { NodeWorkerKind::Dedicated } else { NodeWorkerKind::Shared @@ -80,13 +80,13 @@ impl NodeDriver { info!("Starting SharedWorker"); let worker = SharedWorker::new_with_worker_options(&url, &opts) .to_error("could not create SharedWorker")?; - WorkerClient::from(worker) + WorkerClient::new(worker.into()) } NodeWorkerKind::Dedicated => { info!("Starting Worker"); let worker = Worker::new_with_options(&url, &opts).to_error("could not create Worker")?; - WorkerClient::from(worker) + WorkerClient::new(worker.into()) } }; diff --git a/node-wasm/src/utils.rs b/node-wasm/src/utils.rs index e94987a4..482eb25a 100644 --- a/node-wasm/src/utils.rs +++ b/node-wasm/src/utils.rs @@ -13,10 +13,7 @@ use tracing_subscriber::fmt::time::UtcTime; use tracing_subscriber::prelude::*; use tracing_web::{performance_layer, MakeConsoleWriter}; use wasm_bindgen::prelude::*; -use web_sys::{ - window, Crypto, DedicatedWorkerGlobalScope, SharedWorker, SharedWorkerGlobalScope, Worker, - WorkerGlobalScope, -}; +use web_sys::{Crypto, DedicatedWorkerGlobalScope, SharedWorker, SharedWorkerGlobalScope, Worker}; use crate::error::{Context, Error, Result}; @@ -188,23 +185,14 @@ const CHROME_USER_AGENT_DETECTION_STR: &str = "Chrome/"; // currently there's issue with SharedWorkers on Chrome, where restarting lumina's worker // causes all network connections to fail. Until that's resolved detect chrome and apply // a workaround. -pub(crate) fn is_chrome() -> bool { - let mut user_agent = None; - if let Some(window) = window() { - user_agent = Some(window.navigator().user_agent()); - }; - if let Some(worker_scope) = JsValue::from(js_sys::global()).dyn_ref::() { - user_agent = Some(worker_scope.navigator().user_agent()); - } - - if let Some(user_agent) = user_agent { - user_agent - .as_deref() - .unwrap_or("") - .contains(CHROME_USER_AGENT_DETECTION_STR) - } else { - false - } +pub(crate) fn is_chrome() -> Result { + js_sys::Reflect::get(&js_sys::global(), &JsValue::from_str("navigator")) + .context("failed to get `navigator` from global object")? + .dyn_into::() + .context("`navigator` is not instanceof `Navigator`")? + .user_agent() + .context("could not get UserAgent from Navigator") + .map(|user_agent| user_agent.contains(CHROME_USER_AGENT_DETECTION_STR)) } pub(crate) fn get_crypto() -> Result { diff --git a/node-wasm/src/worker/channel.rs b/node-wasm/src/worker/channel.rs index 7a0210e3..01f7fbd7 100644 --- a/node-wasm/src/worker/channel.rs +++ b/node-wasm/src/worker/channel.rs @@ -20,7 +20,7 @@ type WorkerClientConnection = (MessagePort, Closure); const WORKER_CHANNEL_SIZE: usize = 1; /// `WorkerClient` is responsible for sending messages to and receiving responses from [`WorkerMessageServer`]. -/// It covers JS details like callbacks, having to synchronise requests and responses and exposes +/// It covers JS details like callbacks, having to synchronise requests, responses, and exposes /// simple RPC-like function call interface. pub(crate) struct WorkerClient { worker: AnyWorker, @@ -29,10 +29,28 @@ pub(crate) struct WorkerClient { _onerror: Closure, } -impl From for WorkerClient { - fn from(worker: Worker) -> WorkerClient { - let (response_tx, response_rx) = mpsc::channel(WORKER_CHANNEL_SIZE); +pub(crate) enum AnyWorker { + DedicatedWorker(Worker), + SharedWorker(SharedWorker), +} + +impl From for AnyWorker { + fn from(worker: SharedWorker) -> Self { + AnyWorker::SharedWorker(worker) + } +} + +impl From for AnyWorker { + fn from(worker: Worker) -> Self { + AnyWorker::DedicatedWorker(worker) + } +} +impl AnyWorker { + fn setup_on_message_callback( + &self, + response_tx: mpsc::Sender, + ) -> Closure { let onmessage_callback = move |ev: MessageEvent| { let response_tx = response_tx.clone(); spawn_local(async move { @@ -52,76 +70,59 @@ impl From for WorkerClient { }; let onmessage = Closure::new(onmessage_callback); - worker.set_onmessage(Some(onmessage.as_ref().unchecked_ref())); + match self { + AnyWorker::SharedWorker(worker) => { + let message_port = worker.port(); + message_port.set_onmessage(Some(onmessage.as_ref().unchecked_ref())); + } + AnyWorker::DedicatedWorker(worker) => { + worker.set_onmessage(Some(onmessage.as_ref().unchecked_ref())) + } + } + onmessage + } + fn setup_on_error_callback(&self) -> Closure { let onerror = Closure::new(|ev: MessageEvent| { error!("received error from Worker: {:?}", ev.to_string()); }); - worker.set_onerror(Some(onerror.as_ref().unchecked_ref())); - - Self { - worker: AnyWorker::DedicatedWorker(worker), - response_channel: Mutex::new(response_rx), - _onmessage: onmessage, - _onerror: onerror, + match self { + AnyWorker::SharedWorker(worker) => { + worker.set_onerror(Some(onerror.as_ref().unchecked_ref())) + } + AnyWorker::DedicatedWorker(worker) => { + worker.set_onerror(Some(onerror.as_ref().unchecked_ref())) + } } + + onerror } } -impl From for WorkerClient { - fn from(worker: SharedWorker) -> WorkerClient { +impl WorkerClient { + /// Create a new WorkerClient to control newly created Shared or Dedicated Worker running + /// MessageServer + pub(crate) fn new(worker: AnyWorker) -> Self { let (response_tx, response_rx) = mpsc::channel(WORKER_CHANNEL_SIZE); - let onmessage_callback = move |ev: MessageEvent| { - let response_tx = response_tx.clone(); - spawn_local(async move { - let data: WireMessage = match from_value(ev.data()) { - Ok(jsvalue) => jsvalue, - Err(e) => { - error!("WorkerClient could not convert from JsValue: {e}"); - let error = Error::from(e).context("could not deserialise worker response"); - Err(WorkerError::WorkerCommunicationError(error)) - } - }; + let onmessage = worker.setup_on_message_callback(response_tx); + let onerror = worker.setup_on_error_callback(); - if let Err(e) = response_tx.send(data).await { - error!("message forwarding channel closed, should not happen: {e}"); - } - }) - }; - - let onmessage = Closure::new(onmessage_callback); - let message_port = worker.port(); - message_port.set_onmessage(Some(onmessage.as_ref().unchecked_ref())); - - let onerror: Closure = Closure::new(|ev: MessageEvent| { - error!("received error from SharedWorker: {:?}", ev.to_string()); - }); - worker.set_onerror(Some(onerror.as_ref().unchecked_ref())); - - message_port.start(); Self { - worker: AnyWorker::SharedWorker(worker), + worker, response_channel: Mutex::new(response_rx), _onmessage: onmessage, _onerror: onerror, } } -} - -enum AnyWorker { - DedicatedWorker(Worker), - SharedWorker(SharedWorker), -} -impl WorkerClient { /// Send command to lumina and wait for a response. /// /// Response enum variant can be converted into appropriate type at runtime with a provided /// [`CheckableResponseExt`] helper. /// /// [`CheckableResponseExt`]: crate::utils::CheckableResponseExt - pub async fn exec(&self, command: NodeCommand) -> Result { + pub(crate) async fn exec(&self, command: NodeCommand) -> Result { let mut response_channel = self.response_channel.lock().await; self.send(command) .map_err(WorkerError::WorkerCommunicationError)?; @@ -174,18 +175,6 @@ pub(super) trait MessageServer { fn respond_err_to(&self, client: ClientId, error: WorkerError) { self.send_response(client, Err(error)) } - - fn prepare_message(&self, message: &WireMessage) -> JsValue { - match to_value(message) { - Ok(jsvalue) => jsvalue, - Err(e) => { - warn!("provided response could not be coverted to JsValue: {e}"); - let error = Error::from(e).context("couldn't serialise worker response"); - to_value(&WorkerError::WorkerCommunicationError(error)) - .expect("something's very wrong, couldn't serialise serialisation error") - } - } - } } pub(super) struct SharedWorkerMessageServer { @@ -246,7 +235,7 @@ impl MessageServer for SharedWorkerMessageServer { return; }; - if let Err(e) = client_port.post_message(&self.prepare_message(&message)) { + if let Err(e) = client_port.post_message(&serialize_response_message(&message)) { error!("could not post response message to client {client}: {e:?}"); } } @@ -290,7 +279,10 @@ impl MessageServer for DedicatedWorkerMessageServer { } fn send_response(&self, client: ClientId, message: WireMessage) { - if let Err(e) = self.worker.post_message(&self.prepare_message(&message)) { + if let Err(e) = self + .worker + .post_message(&serialize_response_message(&message)) + { error!("could not post response message to client {client}: {e:?}"); } } @@ -345,3 +337,15 @@ fn parse_message_event_to_worker_message(ev: MessageEvent, client: ClientId) -> } } } + +fn serialize_response_message(message: &WireMessage) -> JsValue { + match to_value(message) { + Ok(jsvalue) => jsvalue, + Err(e) => { + warn!("provided response could not be coverted to JsValue: {e}"); + let error = Error::from(e).context("couldn't serialise worker response"); + to_value(&WorkerError::WorkerCommunicationError(error)) + .expect("something's very wrong, couldn't serialise serialisation error") + } + } +} From 48670eee2610d18d2d5e68f339fcd1062fb0efc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Florkiewicz?= Date: Sat, 22 Jun 2024 13:29:25 +0200 Subject: [PATCH 33/35] pr review --- node-wasm/Cargo.toml | 3 ++- node-wasm/src/node.rs | 38 ++++++++++++++------------------- node-wasm/src/utils.rs | 22 ------------------- node-wasm/src/worker.rs | 2 +- node-wasm/src/worker/channel.rs | 35 ++++++++++++++++++++++++++---- 5 files changed, 50 insertions(+), 50 deletions(-) diff --git a/node-wasm/Cargo.toml b/node-wasm/Cargo.toml index 2ee18503..5104f8d2 100644 --- a/node-wasm/Cargo.toml +++ b/node-wasm/Cargo.toml @@ -53,12 +53,13 @@ web-sys = { version = "0.3.69", features = [ "DedicatedWorkerGlobalScope", "MessageEvent", "MessagePort", + "Navigator", "SharedWorker", "SharedWorkerGlobalScope", "Worker", "WorkerGlobalScope", "WorkerOptions", - "WorkerType" + "WorkerType", ] } [package.metadata.docs.rs] diff --git a/node-wasm/src/node.rs b/node-wasm/src/node.rs index 16de76ea..71dc291c 100644 --- a/node-wasm/src/node.rs +++ b/node-wasm/src/node.rs @@ -5,9 +5,8 @@ use libp2p::identity::Keypair; use libp2p::multiaddr::Protocol; use serde::{Deserialize, Serialize}; use serde_wasm_bindgen::to_value; -use tracing::info; use wasm_bindgen::prelude::*; -use web_sys::{BroadcastChannel, SharedWorker, Worker, WorkerOptions, WorkerType}; +use web_sys::BroadcastChannel; use lumina_node::blockstore::IndexedDbBlockstore; use lumina_node::network::{canonical_network_bootnodes, network_genesis, network_id}; @@ -15,9 +14,9 @@ use lumina_node::node::NodeConfig; use lumina_node::store::IndexedDbStore; use crate::error::{Context, Result}; -use crate::utils::{is_chrome, js_value_from_display, JsValueToJsError, Network}; +use crate::utils::{is_chrome, js_value_from_display, Network}; use crate::worker::commands::{CheckableResponseExt, NodeCommand, SingleHeaderQuery}; -use crate::worker::{worker_script_url, WorkerClient}; +use crate::worker::{worker_script_url, AnyWorker, WorkerClient}; use crate::wrapper::libp2p::NetworkInfoSnapshot; const LUMINA_WORKER_NAME: &str = "lumina"; @@ -65,32 +64,27 @@ impl NodeDriver { #[wasm_bindgen(constructor)] pub async fn new(worker_type: Option) -> Result { let url = worker_script_url(); - let mut opts = WorkerOptions::new(); - opts.type_(WorkerType::Module); - opts.name(LUMINA_WORKER_NAME); + // For chrome we default to running in a dedicated Worker because: + // 1. Chrome Android does not support SharedWorkers at all + // 2. On desktop Chrome, if tab running lumina is reloaded, it fails to re-connect to + // previous worker instance and doesn't create a new one, leaving it in non functioning + // limbo let default_worker_type = if is_chrome().unwrap_or(false) { NodeWorkerKind::Dedicated } else { NodeWorkerKind::Shared }; - let client = match worker_type.unwrap_or(default_worker_type) { - NodeWorkerKind::Shared => { - info!("Starting SharedWorker"); - let worker = SharedWorker::new_with_worker_options(&url, &opts) - .to_error("could not create SharedWorker")?; - WorkerClient::new(worker.into()) - } - NodeWorkerKind::Dedicated => { - info!("Starting Worker"); - let worker = - Worker::new_with_options(&url, &opts).to_error("could not create Worker")?; - WorkerClient::new(worker.into()) - } - }; + let worker = AnyWorker::new( + worker_type.unwrap_or(default_worker_type), + &url, + LUMINA_WORKER_NAME, + )?; - Ok(Self { client }) + Ok(Self { + client: WorkerClient::new(worker), + }) } /// Check whether Lumina is currently running diff --git a/node-wasm/src/utils.rs b/node-wasm/src/utils.rs index 482eb25a..5f151b82 100644 --- a/node-wasm/src/utils.rs +++ b/node-wasm/src/utils.rs @@ -1,7 +1,6 @@ //! Various utilities for interacting with node from wasm. use std::fmt::{self, Debug}; -use js_sys::JsString; use lumina_node::network; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; @@ -80,27 +79,6 @@ pub(crate) fn to_jsvalue_or_undefined(value: &T) -> JsValue { to_value(value).unwrap_or(JsValue::UNDEFINED) } -pub(crate) trait JsValueToJsError { - fn to_error(self, context_fn: C) -> Result - where - C: fmt::Display + Send + Sync + 'static; -} - -impl JsValueToJsError for std::result::Result { - fn to_error(self, context: C) -> Result - where - C: fmt::Display + Send + Sync + 'static, - { - self.map_err(|e| { - let error_str = match e.dyn_ref::() { - Some(s) => format!("{context}: {s}"), - None => format!("{context}"), - }; - JsError::new(&error_str) - }) - } -} - pub(crate) trait WorkerSelf { type GlobalScope; diff --git a/node-wasm/src/worker.rs b/node-wasm/src/worker.rs index e1179a1b..ffdf4387 100644 --- a/node-wasm/src/worker.rs +++ b/node-wasm/src/worker.rs @@ -28,7 +28,7 @@ use crate::wrapper::libp2p::NetworkInfoSnapshot; mod channel; pub(crate) mod commands; -pub(crate) use channel::WorkerClient; +pub(crate) use channel::{AnyWorker, WorkerClient}; const WORKER_MESSAGE_SERVER_INCOMING_QUEUE_LENGTH: usize = 64; diff --git a/node-wasm/src/worker/channel.rs b/node-wasm/src/worker/channel.rs index 01f7fbd7..cd3da1d8 100644 --- a/node-wasm/src/worker/channel.rs +++ b/node-wasm/src/worker/channel.rs @@ -5,9 +5,13 @@ use tokio::sync::{mpsc, Mutex}; use tracing::{debug, error, info, warn}; use wasm_bindgen::prelude::*; use wasm_bindgen_futures::spawn_local; -use web_sys::{DedicatedWorkerGlobalScope, MessageEvent, MessagePort, SharedWorker, Worker}; +use web_sys::{ + DedicatedWorkerGlobalScope, MessageEvent, MessagePort, SharedWorker, Worker, WorkerOptions, + WorkerType, +}; use crate::error::{Context, Error, Result}; +use crate::node::NodeWorkerKind; use crate::utils::WorkerSelf; use crate::worker::commands::{NodeCommand, WorkerResponse}; use crate::worker::WorkerError; @@ -47,6 +51,28 @@ impl From for AnyWorker { } impl AnyWorker { + pub(crate) fn new(kind: NodeWorkerKind, url: &str, name: &str) -> Result { + let mut opts = WorkerOptions::new(); + opts.type_(WorkerType::Module); + opts.name(name); + + Ok(match kind { + NodeWorkerKind::Shared => { + info!("Starting SharedWorker"); + AnyWorker::SharedWorker( + SharedWorker::new_with_worker_options(url, &opts) + .context("could not create SharedWorker")?, + ) + } + NodeWorkerKind::Dedicated => { + info!("Starting Worker"); + AnyWorker::DedicatedWorker( + Worker::new_with_options(url, &opts).context("could not create Worker")?, + ) + } + }) + } + fn setup_on_message_callback( &self, response_tx: mpsc::Sender, @@ -127,9 +153,10 @@ impl WorkerClient { self.send(command) .map_err(WorkerError::WorkerCommunicationError)?; - let message: WireMessage = response_channel.recv().await.ok_or_else(|| { - WorkerError::WorkerCommunicationError(Error::new("worker response channel dropped")) - })?; + let message: WireMessage = response_channel + .recv() + .await + .expect("response channel should never be dropped"); message } From d93a126da56ca9dfb570a79a2aea87d2a89ba283 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Florkiewicz?= Date: Mon, 24 Jun 2024 08:39:30 +0200 Subject: [PATCH 34/35] pr reviews --- node-wasm/src/node.rs | 20 +++++++++----------- node-wasm/src/utils.rs | 35 ++++++++++++++++++----------------- 2 files changed, 27 insertions(+), 28 deletions(-) diff --git a/node-wasm/src/node.rs b/node-wasm/src/node.rs index 71dc291c..a8949006 100644 --- a/node-wasm/src/node.rs +++ b/node-wasm/src/node.rs @@ -67,9 +67,7 @@ impl NodeDriver { // For chrome we default to running in a dedicated Worker because: // 1. Chrome Android does not support SharedWorkers at all - // 2. On desktop Chrome, if tab running lumina is reloaded, it fails to re-connect to - // previous worker instance and doesn't create a new one, leaving it in non functioning - // limbo + // 2. On desktop Chrome, restarting Lumina's worker causes all network connections to fail. let default_worker_type = if is_chrome().unwrap_or(false) { NodeWorkerKind::Dedicated } else { @@ -183,7 +181,7 @@ impl NodeDriver { let response = self.client.exec(command).await?; let header = response.into_header().check_variant()?; - header.into_result() + header.into() } /// Request a header for the block with a given hash from the network. @@ -192,7 +190,7 @@ impl NodeDriver { let response = self.client.exec(command).await?; let header = response.into_header().check_variant()?; - header.into_result() + header.into() } /// Request a header for the block with a given height from the network. @@ -201,7 +199,7 @@ impl NodeDriver { let response = self.client.exec(command).await?; let header = response.into_header().check_variant()?; - header.into_result() + header.into() } /// Request headers in range (from, from + amount] from the network. @@ -219,7 +217,7 @@ impl NodeDriver { let response = self.client.exec(command).await?; let headers = response.into_headers().check_variant()?; - headers.into_result() + headers.into() } /// Get current header syncing info. @@ -246,7 +244,7 @@ impl NodeDriver { let response = self.client.exec(command).await?; let header = response.into_header().check_variant()?; - header.into_result() + header.into() } /// Get a synced header for the block with a given hash. @@ -255,7 +253,7 @@ impl NodeDriver { let response = self.client.exec(command).await?; let header = response.into_header().check_variant()?; - header.into_result() + header.into() } /// Get a synced header for the block with a given height. @@ -264,7 +262,7 @@ impl NodeDriver { let response = self.client.exec(command).await?; let header = response.into_header().check_variant()?; - header.into_result() + header.into() } /// Get synced headers from the given heights range. @@ -288,7 +286,7 @@ impl NodeDriver { let response = self.client.exec(command).await?; let headers = response.into_headers().check_variant()?; - headers.into_result() + headers.into() } /// Get data sampling metadata of an already sampled height. diff --git a/node-wasm/src/utils.rs b/node-wasm/src/utils.rs index 5f151b82..ea7d7b6a 100644 --- a/node-wasm/src/utils.rs +++ b/node-wasm/src/utils.rs @@ -12,7 +12,9 @@ use tracing_subscriber::fmt::time::UtcTime; use tracing_subscriber::prelude::*; use tracing_web::{performance_layer, MakeConsoleWriter}; use wasm_bindgen::prelude::*; -use web_sys::{Crypto, DedicatedWorkerGlobalScope, SharedWorker, SharedWorkerGlobalScope, Worker}; +use web_sys::{ + Crypto, DedicatedWorkerGlobalScope, Navigator, SharedWorker, SharedWorkerGlobalScope, Worker, +}; use crate::error::{Context, Error, Result}; @@ -110,6 +112,12 @@ impl WorkerSelf for Worker { } } +/// This type is useful in cases where we want to deal with de/serialising `Result`, with +/// [`serde_wasm_bindgen::preserve`] where `T` is a JavaScript object (which are not serializable by +/// Rust standards, but can be passed through unchanged via cast as they implement [`JsCast`]). +/// +/// [`serde_wasm_bindgen::preserve`]: https://docs.rs/serde-wasm-bindgen/latest/serde_wasm_bindgen/preserve +/// [`JsCast`]: https://docs.rs/wasm-bindgen/latest/wasm_bindgen/trait.JsCast.html #[derive(Serialize, Deserialize, Debug)] pub(crate) enum JsResult where @@ -121,17 +129,6 @@ where Err(E), } -// once try_trait_v2 is stabilised, this can go -impl JsResult -where - T: JsCast + Debug, - E: Serialize + DeserializeOwned + Debug, -{ - pub fn into_result(self) -> Result { - self.into() - } -} - impl From> for JsResult where T: JsCast + Debug, @@ -160,19 +157,23 @@ where const CHROME_USER_AGENT_DETECTION_STR: &str = "Chrome/"; -// currently there's issue with SharedWorkers on Chrome, where restarting lumina's worker +// Currently, there's an issue with SharedWorkers on Chrome where restarting Lumina's worker // causes all network connections to fail. Until that's resolved detect chrome and apply // a workaround. pub(crate) fn is_chrome() -> Result { - js_sys::Reflect::get(&js_sys::global(), &JsValue::from_str("navigator")) - .context("failed to get `navigator` from global object")? - .dyn_into::() - .context("`navigator` is not instanceof `Navigator`")? + get_navigator()? .user_agent() .context("could not get UserAgent from Navigator") .map(|user_agent| user_agent.contains(CHROME_USER_AGENT_DETECTION_STR)) } +pub(crate) fn get_navigator() -> Result { + js_sys::Reflect::get(&js_sys::global(), &JsValue::from_str("navigator")) + .context("failed to get `navigator` from global object")? + .dyn_into::() + .context("`navigator` is not instanceof `Navigator`") +} + pub(crate) fn get_crypto() -> Result { js_sys::Reflect::get(&js_sys::global(), &JsValue::from_str("crypto")) .context("failed to get `crypto` from global object")? From 7b4f80b4fee79360a86da22e0846da33859a9639 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Florkiewicz?= Date: Tue, 25 Jun 2024 10:58:07 +0200 Subject: [PATCH 35/35] final touch --- node-wasm/src/utils.rs | 5 -- node-wasm/src/worker.rs | 5 +- node-wasm/src/worker/channel.rs | 137 ++++++++++++++++---------------- 3 files changed, 73 insertions(+), 74 deletions(-) diff --git a/node-wasm/src/utils.rs b/node-wasm/src/utils.rs index ea7d7b6a..96286193 100644 --- a/node-wasm/src/utils.rs +++ b/node-wasm/src/utils.rs @@ -5,7 +5,6 @@ use lumina_node::network; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; use serde_repr::{Deserialize_repr, Serialize_repr}; -use serde_wasm_bindgen::to_value; use tracing_subscriber::filter::LevelFilter; use tracing_subscriber::fmt::format::Pretty; use tracing_subscriber::fmt::time::UtcTime; @@ -77,10 +76,6 @@ pub(crate) fn js_value_from_display(value: D) -> JsValue { JsValue::from(value.to_string()) } -pub(crate) fn to_jsvalue_or_undefined(value: &T) -> JsValue { - to_value(value).unwrap_or(JsValue::UNDEFINED) -} - pub(crate) trait WorkerSelf { type GlobalScope; diff --git a/node-wasm/src/worker.rs b/node-wasm/src/worker.rs index ffdf4387..e94ea9cc 100644 --- a/node-wasm/src/worker.rs +++ b/node-wasm/src/worker.rs @@ -18,7 +18,7 @@ use lumina_node::syncer::SyncingInfo; use crate::error::{Context, Error, Result}; use crate::node::WasmNodeConfig; -use crate::utils::{get_crypto, to_jsvalue_or_undefined, WorkerSelf}; +use crate::utils::{get_crypto, WorkerSelf}; use crate::worker::channel::{ DedicatedWorkerMessageServer, MessageServer, SharedWorkerMessageServer, WorkerMessage, }; @@ -167,7 +167,8 @@ impl NodeWorker { } async fn get_last_seen_network_head(&mut self) -> JsValue { - to_jsvalue_or_undefined(&self.node.get_network_head_header()) + // JS interface returns `undefined`, if node haven't received any headers from HeaderSub yet + to_value(&self.node.get_network_head_header()).unwrap_or(JsValue::UNDEFINED) } async fn get_sampling_metadata(&mut self, height: u64) -> Result> { diff --git a/node-wasm/src/worker/channel.rs b/node-wasm/src/worker/channel.rs index cd3da1d8..1d9489b0 100644 --- a/node-wasm/src/worker/channel.rs +++ b/node-wasm/src/worker/channel.rs @@ -33,6 +33,57 @@ pub(crate) struct WorkerClient { _onerror: Closure, } +impl WorkerClient { + /// Create a new WorkerClient to control newly created Shared or Dedicated Worker running + /// MessageServer + pub(crate) fn new(worker: AnyWorker) -> Self { + let (response_tx, response_rx) = mpsc::channel(WORKER_CHANNEL_SIZE); + + let onmessage = worker.setup_on_message_callback(response_tx); + let onerror = worker.setup_on_error_callback(); + + Self { + worker, + response_channel: Mutex::new(response_rx), + _onmessage: onmessage, + _onerror: onerror, + } + } + + /// Send command to lumina and wait for a response. + /// + /// Response enum variant can be converted into appropriate type at runtime with a provided + /// [`CheckableResponseExt`] helper. + /// + /// [`CheckableResponseExt`]: crate::utils::CheckableResponseExt + pub(crate) async fn exec(&self, command: NodeCommand) -> Result { + let mut response_channel = self.response_channel.lock().await; + self.send(command) + .map_err(WorkerError::WorkerCommunicationError)?; + + let message: WireMessage = response_channel + .recv() + .await + .expect("response channel should never be dropped"); + + message + } + + fn send(&self, command: NodeCommand) -> Result<()> { + let command_value = + to_value(&command).context("could not serialise worker command to be sent")?; + match &self.worker { + AnyWorker::DedicatedWorker(worker) => worker + .post_message(&command_value) + .context("could not send command to worker"), + AnyWorker::SharedWorker(worker) => worker + .port() + .post_message(&command_value) + .context("could not send command to worker"), + } + } +} + pub(crate) enum AnyWorker { DedicatedWorker(Worker), SharedWorker(SharedWorker), @@ -125,57 +176,6 @@ impl AnyWorker { } } -impl WorkerClient { - /// Create a new WorkerClient to control newly created Shared or Dedicated Worker running - /// MessageServer - pub(crate) fn new(worker: AnyWorker) -> Self { - let (response_tx, response_rx) = mpsc::channel(WORKER_CHANNEL_SIZE); - - let onmessage = worker.setup_on_message_callback(response_tx); - let onerror = worker.setup_on_error_callback(); - - Self { - worker, - response_channel: Mutex::new(response_rx), - _onmessage: onmessage, - _onerror: onerror, - } - } - - /// Send command to lumina and wait for a response. - /// - /// Response enum variant can be converted into appropriate type at runtime with a provided - /// [`CheckableResponseExt`] helper. - /// - /// [`CheckableResponseExt`]: crate::utils::CheckableResponseExt - pub(crate) async fn exec(&self, command: NodeCommand) -> Result { - let mut response_channel = self.response_channel.lock().await; - self.send(command) - .map_err(WorkerError::WorkerCommunicationError)?; - - let message: WireMessage = response_channel - .recv() - .await - .expect("response channel should never be dropped"); - - message - } - - fn send(&self, command: NodeCommand) -> Result<()> { - let command_value = - to_value(&command).context("could not serialise worker command to be sent")?; - match &self.worker { - AnyWorker::DedicatedWorker(worker) => worker - .post_message(&command_value) - .context("could not send command to worker"), - AnyWorker::SharedWorker(worker) => worker - .port() - .post_message(&command_value) - .context("could not send command to worker"), - } - } -} - #[derive(Debug, Clone, Copy)] pub(super) struct ClientId(usize); @@ -191,6 +191,22 @@ pub(super) enum WorkerMessage { Command((NodeCommand, ClientId)), } +impl From<(MessageEvent, ClientId)> for WorkerMessage { + fn from(value: (MessageEvent, ClientId)) -> Self { + let (event, client) = value; + match from_value(event.data()) { + Ok(command) => { + debug!("received command from client {client}: {command:#?}"); + WorkerMessage::Command((command, client)) + } + Err(e) => { + warn!("could not deserialize message from client {client}: {e}"); + WorkerMessage::InvalidCommandReceived(client) + } + } + } +} + pub(super) trait MessageServer { fn send_response(&self, client: ClientId, message: WireMessage); fn add(&mut self, port: MessagePort); @@ -225,7 +241,7 @@ impl SharedWorkerMessageServer { let mut server = Self { _onconnect: onconnect, - clients: Vec::with_capacity(1), // we usually expect to have exactly one client + clients: Vec::with_capacity(usize::max(queued.len(), 1)), command_channel, }; @@ -282,7 +298,7 @@ impl DedicatedWorkerMessageServer { queued: Vec, ) -> Self { for event in queued { - let message = parse_message_event_to_worker_message(event, ClientId(0)); + let message = WorkerMessage::from((event, ClientId(0))); if let Err(e) = command_channel.send(message).await { error!("command channel inside worker closed, should not happen: {e}"); @@ -343,7 +359,7 @@ fn get_client_message_callback( Closure::new(move |ev: MessageEvent| { let command_channel = command_channel.clone(); spawn_local(async move { - let message = parse_message_event_to_worker_message(ev, client); + let message = WorkerMessage::from((ev, client)); if let Err(e) = command_channel.send(message).await { error!("command channel inside worker closed, should not happen: {e}"); @@ -352,19 +368,6 @@ fn get_client_message_callback( }) } -fn parse_message_event_to_worker_message(ev: MessageEvent, client: ClientId) -> WorkerMessage { - match from_value(ev.data()) { - Ok(command) => { - debug!("received command from client {client}: {command:#?}"); - WorkerMessage::Command((command, client)) - } - Err(e) => { - warn!("could not deserialize message from client {client}: {e}"); - WorkerMessage::InvalidCommandReceived(client) - } - } -} - fn serialize_response_message(message: &WireMessage) -> JsValue { match to_value(message) { Ok(jsvalue) => jsvalue,