diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..4ec2f3b --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[target.wasm32-unknown-unknown] +runner = 'wasm-bindgen-test-runner' diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index 31962c2..c0e314f 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -10,7 +10,7 @@ env: CARGO_TERM_COLOR: always jobs: - rust: + linux: runs-on: ubuntu-latest @@ -33,6 +33,7 @@ jobs: - uses: Swatinem/rust-cache@v2 with: cache-all-crates: "true" + prefix-key: "linux" - name: Build, Test and Publish Coverage run: | if [ -n "${{ secrets.COVERALLS_REPO_TOKEN }}" ]; then @@ -44,4 +45,33 @@ jobs: cargo build --verbose --all-features cargo test --verbose --all-features fi - + macos: + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + - name: Clone spacebar server + run: | + git clone https://github.com/bitfl0wer/server.git + - uses: actions/setup-node@v3 + with: + node-version: 18 + cache: 'npm' + cache-dependency-path: server/package-lock.json + - name: Prepare and start Spacebar server + run: | + npm install + npm run setup + npm run start & + working-directory: ./server + - uses: Swatinem/rust-cache@v2 + with: + cache-all-crates: "true" + prefix-key: "macos" + - name: Run WASM tests with Safari, Firefox, Chrome + run: | + rustup target add wasm32-unknown-unknown + curl -L --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh | bash + cargo binstall --no-confirm wasm-bindgen-cli --version "0.2.88" --force + SAFARIDRIVER=$(which safaridriver) cargo test --target wasm32-unknown-unknown --no-default-features --features="client, rt" + GECKODRIVER=$(which geckodriver) cargo test --target wasm32-unknown-unknown --no-default-features --features="client, rt" + CHROMEDRIVER=$(which chromedriver) cargo test --target wasm32-unknown-unknown --no-default-features --features="client, rt" \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index f7379c1..7c74e2a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -205,7 +205,6 @@ dependencies = [ "lazy_static", "log", "native-tls", - "pharos", "poem", "rand", "regex", @@ -223,6 +222,8 @@ dependencies = [ "tokio", "tokio-tungstenite", "url", + "wasm-bindgen", + "wasm-bindgen-test", "ws_stream_wasm", ] @@ -252,6 +253,16 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + [[package]] name = "const-oid" version = "0.9.5" @@ -1674,6 +1685,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + [[package]] name = "scopeguard" version = "1.2.0" @@ -2627,6 +2644,31 @@ version = "0.2.88" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d046c5d029ba91a1ed14da14dca44b68bf2f124cfbaf741c54151fdb3e0750b" +[[package]] +name = "wasm-bindgen-test" +version = "0.3.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6433b7c56db97397842c46b67e11873eda263170afeb3a2dc74a7cb370fee0d" +dependencies = [ + "console_error_panic_hook", + "js-sys", + "scoped-tls", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-bindgen-test-macro", +] + +[[package]] +name = "wasm-bindgen-test-macro" +version = "0.3.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "493fcbab756bb764fa37e6bee8cec2dd709eb4273d06d0c282a5e74275ded735" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + [[package]] name = "web-sys" version = "0.3.65" diff --git a/Cargo.toml b/Cargo.toml index 72028c6..24a7114 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,9 +54,6 @@ sqlx = { version = "0.7.1", features = [ ], optional = true } safina-timer = "0.1.11" rand = "0.8.5" -# TODO: Remove the below 2 imports for production! -ws_stream_wasm = "0.7.4" -pharos = "0.5.3" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] rustls = "0.21.8" @@ -70,10 +67,10 @@ hostname = "0.3.1" [target.'cfg(target_arch = "wasm32")'.dependencies] getrandom = { version = "0.2.11", features = ["js"] } -tokio-tungstenite = { version = "0.20.1", default-features = false } ws_stream_wasm = "0.7.4" -pharos = "0.5.3" [dev-dependencies] lazy_static = "1.4.0" +wasm-bindgen-test = "0.3.38" +wasm-bindgen = "0.2.88" diff --git a/examples/gateway_observers.rs b/examples/gateway_observers.rs index d4e690c..a13c935 100644 --- a/examples/gateway_observers.rs +++ b/examples/gateway_observers.rs @@ -6,7 +6,7 @@ use chorus::{ types::{GatewayIdentifyPayload, GatewayReady}, }; use std::{sync::Arc, time::Duration}; -use tokio::{self, time::sleep}; +use tokio::{self}; // This example creates a simple gateway connection and a basic observer struct @@ -54,9 +54,10 @@ async fn main() { let mut identify = GatewayIdentifyPayload::common(); identify.token = token; gateway.send_identify(identify).await; + safina_timer::start_timer_thread(); // Do something on the main thread so we don't quit loop { - sleep(Duration::MAX).await; + safina_timer::sleep_for(Duration::MAX).await } } diff --git a/examples/gateway_simple.rs b/examples/gateway_simple.rs index a9c019b..2996283 100644 --- a/examples/gateway_simple.rs +++ b/examples/gateway_simple.rs @@ -2,7 +2,6 @@ use std::time::Duration; use chorus::gateway::Gateway; use chorus::{self, types::GatewayIdentifyPayload}; -use tokio::time::sleep; /// This example creates a simple gateway connection and a session with an Identify event #[tokio::main(flavor = "current_thread")] @@ -11,7 +10,7 @@ async fn main() { let websocket_url_spacebar = "wss://gateway.old.server.spacebar.chat/".to_string(); // Initiate the gateway connection, starting a listener in one thread and a heartbeat handler in another - let gateway = Gateway::spawn(websocket_url_spacebar).await.unwrap(); + let _ = Gateway::spawn(websocket_url_spacebar).await.unwrap(); // At this point, we are connected to the server and are sending heartbeats, however we still haven't authenticated @@ -27,10 +26,10 @@ async fn main() { identify.token = token; // Send off the event - gateway.send_identify(identify).await; + safina_timer::start_timer_thread(); // Do something on the main thread so we don't quit loop { - sleep(Duration::MAX).await; + safina_timer::sleep_for(Duration::MAX).await } } diff --git a/src/gateway/backends/mod.rs b/src/gateway/backends/mod.rs new file mode 100644 index 0000000..edb5dc9 --- /dev/null +++ b/src/gateway/backends/mod.rs @@ -0,0 +1,23 @@ +#[cfg(all(not(target_arch = "wasm32"), feature = "client"))] +pub mod tungstenite; +#[cfg(all(not(target_arch = "wasm32"), feature = "client"))] +pub use tungstenite::*; + +#[cfg(all(target_arch = "wasm32", feature = "client"))] +pub mod wasm; +#[cfg(all(target_arch = "wasm32", feature = "client"))] +pub use wasm::*; + +#[cfg(all(not(target_arch = "wasm32"), feature = "client"))] +pub type Sink = tungstenite::TungsteniteSink; +#[cfg(all(not(target_arch = "wasm32"), feature = "client"))] +pub type Stream = tungstenite::TungsteniteStream; +#[cfg(all(not(target_arch = "wasm32"), feature = "client"))] +pub type WebSocketBackend = tungstenite::TungsteniteBackend; + +#[cfg(all(target_arch = "wasm32", feature = "client"))] +pub type Sink = wasm::WasmSink; +#[cfg(all(target_arch = "wasm32", feature = "client"))] +pub type Stream = wasm::WasmStream; +#[cfg(all(target_arch = "wasm32", feature = "client"))] +pub type WebSocketBackend = wasm::WasmBackend; diff --git a/src/gateway/backend_tungstenite.rs b/src/gateway/backends/tungstenite.rs similarity index 81% rename from src/gateway/backend_tungstenite.rs rename to src/gateway/backends/tungstenite.rs index 53b6982..5184329 100644 --- a/src/gateway/backend_tungstenite.rs +++ b/src/gateway/backends/tungstenite.rs @@ -7,20 +7,21 @@ use tokio_tungstenite::{ connect_async_tls_with_config, tungstenite, Connector, MaybeTlsStream, WebSocketStream, }; -use super::GatewayMessage; use crate::errors::GatewayError; +use crate::gateway::GatewayMessage; #[derive(Debug, Clone)] -pub struct WebSocketBackend; +pub struct TungsteniteBackend; // These could be made into inherent associated types when that's stabilized -pub type WsSink = SplitSink>, tungstenite::Message>; -pub type WsStream = SplitStream>>; +pub type TungsteniteSink = + SplitSink>, tungstenite::Message>; +pub type TungsteniteStream = SplitStream>>; -impl WebSocketBackend { +impl TungsteniteBackend { pub async fn connect( websocket_url: &str, - ) -> Result<(WsSink, WsStream), crate::errors::GatewayError> { + ) -> Result<(TungsteniteSink, TungsteniteStream), crate::errors::GatewayError> { let mut roots = rustls::RootCertStore::empty(); for cert in rustls_native_certs::load_native_certs().expect("could not load platform certs") { diff --git a/src/gateway/backends/wasm.rs b/src/gateway/backends/wasm.rs new file mode 100644 index 0000000..e9927ac --- /dev/null +++ b/src/gateway/backends/wasm.rs @@ -0,0 +1,50 @@ +use futures_util::{ + stream::{SplitSink, SplitStream}, + StreamExt, +}; + +use ws_stream_wasm::*; + +use crate::errors::GatewayError; +use crate::gateway::GatewayMessage; + +#[derive(Debug, Clone)] +pub struct WasmBackend; + +// These could be made into inherent associated types when that's stabilized +pub type WasmSink = SplitSink; +pub type WasmStream = SplitStream; + +impl WasmBackend { + pub async fn connect( + websocket_url: &str, + ) -> Result<(WasmSink, WasmStream), crate::errors::GatewayError> { + let (_, websocket_stream) = match WsMeta::connect(websocket_url, None).await { + Ok(stream) => Ok(stream), + Err(e) => Err(GatewayError::CannotConnect { + error: e.to_string(), + }), + }?; + + Ok(websocket_stream.split()) + } +} + +impl From for WsMessage { + fn from(message: GatewayMessage) -> Self { + Self::Text(message.0) + } +} + +impl From for GatewayMessage { + fn from(value: WsMessage) -> Self { + match value { + WsMessage::Text(text) => Self(text), + WsMessage::Binary(bin) => { + let mut text = String::new(); + let _ = bin.iter().map(|v| text.push_str(&v.to_string())); + Self(text) + } + } + } +} diff --git a/src/gateway/gateway.rs b/src/gateway/gateway.rs index 9e3410c..d10dbfb 100644 --- a/src/gateway/gateway.rs +++ b/src/gateway/gateway.rs @@ -6,7 +6,7 @@ use tokio::task; use self::event::Events; use super::*; -use super::{WsSink, WsStream}; +use super::{Sink, Stream}; use crate::types::{ self, AutoModerationRule, AutoModerationRuleUpdate, Channel, ChannelCreate, ChannelDelete, ChannelUpdate, Guild, GuildRoleCreate, GuildRoleUpdate, JsonField, RoleObject, SourceUrlField, @@ -17,8 +17,8 @@ use crate::types::{ pub struct Gateway { events: Arc>, heartbeat_handler: HeartbeatHandler, - websocket_send: Arc>, - websocket_receive: WsStream, + websocket_send: Arc>, + websocket_receive: Stream, kill_send: tokio::sync::broadcast::Sender<()>, store: Arc>>>>, url: String, @@ -37,7 +37,10 @@ impl Gateway { // Wait for the first hello and then spawn both tasks so we avoid nested tasks // This automatically spawns the heartbeat task, but from the main thread + #[cfg(not(target_arch = "wasm32"))] let msg: GatewayMessage = websocket_receive.next().await.unwrap().unwrap().into(); + #[cfg(target_arch = "wasm32")] + let msg: GatewayMessage = websocket_receive.next().await.unwrap().into(); let gateway_payload: types::GatewayReceivePayload = serde_json::from_str(&msg.0).unwrap(); if gateway_payload.op_code != GATEWAY_HELLO { @@ -91,11 +94,18 @@ impl Gateway { loop { let msg = self.websocket_receive.next().await; + // PRETTYFYME: Remove inline conditional compiling // This if chain can be much better but if let is unstable on stable rust + #[cfg(not(target_arch = "wasm32"))] if let Some(Ok(message)) = msg { self.handle_message(message.into()).await; continue; } + #[cfg(target_arch = "wasm32")] + if let Some(message) = msg { + self.handle_message(message.into()).await; + continue; + } // We couldn't receive the next message or it was an error, something is wrong with the websocket, close warn!("GW: Websocket is broken, stopping gateway"); diff --git a/src/gateway/handle.rs b/src/gateway/handle.rs index 9a3c509..620faba 100644 --- a/src/gateway/handle.rs +++ b/src/gateway/handle.rs @@ -14,7 +14,7 @@ use crate::types::{self, Composite}; pub struct GatewayHandle { pub url: String, pub events: Arc>, - pub websocket_send: Arc>, + pub websocket_send: Arc>, /// Tells gateway tasks to close pub(super) kill_send: tokio::sync::broadcast::Sender<()>, pub(crate) store: Arc>>>>, diff --git a/src/gateway/heartbeat.rs b/src/gateway/heartbeat.rs index a5875a4..b8e4bec 100644 --- a/src/gateway/heartbeat.rs +++ b/src/gateway/heartbeat.rs @@ -27,7 +27,7 @@ pub(super) struct HeartbeatHandler { impl HeartbeatHandler { pub fn new( heartbeat_interval: Duration, - websocket_tx: Arc>, + websocket_tx: Arc>, kill_rc: tokio::sync::broadcast::Receiver<()>, ) -> Self { let (send, receive) = tokio::sync::mpsc::channel(32); @@ -49,7 +49,7 @@ impl HeartbeatHandler { /// Can be killed by the kill broadcast; /// If the websocket is closed, will die out next time it tries to send a heartbeat; pub async fn heartbeat_task( - websocket_tx: Arc>, + websocket_tx: Arc>, heartbeat_interval: Duration, mut receive: Receiver, mut kill_receive: tokio::sync::broadcast::Receiver<()>, diff --git a/src/gateway/mod.rs b/src/gateway/mod.rs index 8314999..076ed54 100644 --- a/src/gateway/mod.rs +++ b/src/gateway/mod.rs @@ -1,13 +1,13 @@ use async_trait::async_trait; -#[cfg(not(target_arch = "wasm32"))] -pub mod backend_tungstenite; +pub mod backends; pub mod events; pub mod gateway; pub mod handle; pub mod heartbeat; pub mod message; +pub use backends::*; pub use gateway::*; pub use handle::*; use heartbeat::*; @@ -22,13 +22,6 @@ use std::sync::{Arc, RwLock}; use tokio::sync::Mutex; -#[cfg(not(target_arch = "wasm32"))] -pub type WsSink = backend_tungstenite::WsSink; -#[cfg(not(target_arch = "wasm32"))] -pub type WsStream = backend_tungstenite::WsStream; -#[cfg(not(target_arch = "wasm32"))] -pub type WebSocketBackend = backend_tungstenite::WebSocketBackend; - // Gateway opcodes /// Opcode received when the server dispatches a [crate::types::WebSocketEvent] const GATEWAY_DISPATCH: u8 = 0; diff --git a/tests/wasm.rs b/tests/wasm.rs new file mode 100644 index 0000000..d5d26c1 --- /dev/null +++ b/tests/wasm.rs @@ -0,0 +1,7 @@ +use wasm_bindgen_test::wasm_bindgen_test; + +wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); +#[wasm_bindgen_test] +fn pass() { + let _ = String::new(); +}