initial wasm32 'support' (#443)

* Give tungstenite types distinct names

* reorganize files

* Better feature locking, add wasm.rs

* Implement wasm Backend

* add wasm-bindgen-test

* Build & Test for wasm

* Add macos safari wasm test

* Add wasm32 target

* Add wasm.rs test

* Move wasm-pack installation before test execution

* Fix build on wasm32

* Fix examples depending on tokio::time

* fix clippy warn

* Add example wasm bindgen test

* Add wasm-bindgen to Cargo.toml

* Add wasm test configuration

* Install wasm-bindgen-cli on linux

* Add  wasm-bindgen-cli to macos

* Correct "vers" to "version"

* Attempt to locate correct geckodriver

* Run wasm tests first

* maybe this will fix ci :clueless:

* Move wasm-bindgen-cli install

* Add cargo-binstall installation script for
wasm-bindgen-cli

* Try using only one browser

* remove geckodriver

* Move all wasm related tests to macos

* Rename macOS test step for clarity

* Try out combined coverage report

* try different strategy to skip coverage on forks

* Revert "try different strategy to skip coverage on forks"

This reverts commit fb46ab83ac.

* Revert "Try out combined coverage report"

This reverts commit d34a813d8a.
This commit is contained in:
Flori 2023-11-20 13:40:55 +01:00 committed by GitHub
parent 5dbb3b1bf0
commit 06f3046134
14 changed files with 190 additions and 35 deletions

2
.cargo/config.toml Normal file
View File

@ -0,0 +1,2 @@
[target.wasm32-unknown-unknown]
runner = 'wasm-bindgen-test-runner'

View File

@ -10,7 +10,7 @@ env:
CARGO_TERM_COLOR: always CARGO_TERM_COLOR: always
jobs: jobs:
rust: linux:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -33,6 +33,7 @@ jobs:
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
with: with:
cache-all-crates: "true" cache-all-crates: "true"
prefix-key: "linux"
- name: Build, Test and Publish Coverage - name: Build, Test and Publish Coverage
run: | run: |
if [ -n "${{ secrets.COVERALLS_REPO_TOKEN }}" ]; then if [ -n "${{ secrets.COVERALLS_REPO_TOKEN }}" ]; then
@ -44,4 +45,33 @@ jobs:
cargo build --verbose --all-features cargo build --verbose --all-features
cargo test --verbose --all-features cargo test --verbose --all-features
fi 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"

44
Cargo.lock generated
View File

@ -205,7 +205,6 @@ dependencies = [
"lazy_static", "lazy_static",
"log", "log",
"native-tls", "native-tls",
"pharos",
"poem", "poem",
"rand", "rand",
"regex", "regex",
@ -223,6 +222,8 @@ dependencies = [
"tokio", "tokio",
"tokio-tungstenite", "tokio-tungstenite",
"url", "url",
"wasm-bindgen",
"wasm-bindgen-test",
"ws_stream_wasm", "ws_stream_wasm",
] ]
@ -252,6 +253,16 @@ dependencies = [
"windows-targets", "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]] [[package]]
name = "const-oid" name = "const-oid"
version = "0.9.5" version = "0.9.5"
@ -1674,6 +1685,12 @@ dependencies = [
"windows-sys", "windows-sys",
] ]
[[package]]
name = "scoped-tls"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
[[package]] [[package]]
name = "scopeguard" name = "scopeguard"
version = "1.2.0" version = "1.2.0"
@ -2627,6 +2644,31 @@ version = "0.2.88"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d046c5d029ba91a1ed14da14dca44b68bf2f124cfbaf741c54151fdb3e0750b" 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]] [[package]]
name = "web-sys" name = "web-sys"
version = "0.3.65" version = "0.3.65"

View File

@ -54,9 +54,6 @@ sqlx = { version = "0.7.1", features = [
], optional = true } ], optional = true }
safina-timer = "0.1.11" safina-timer = "0.1.11"
rand = "0.8.5" 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] [target.'cfg(not(target_arch = "wasm32"))'.dependencies]
rustls = "0.21.8" rustls = "0.21.8"
@ -70,10 +67,10 @@ hostname = "0.3.1"
[target.'cfg(target_arch = "wasm32")'.dependencies] [target.'cfg(target_arch = "wasm32")'.dependencies]
getrandom = { version = "0.2.11", features = ["js"] } getrandom = { version = "0.2.11", features = ["js"] }
tokio-tungstenite = { version = "0.20.1", default-features = false }
ws_stream_wasm = "0.7.4" ws_stream_wasm = "0.7.4"
pharos = "0.5.3"
[dev-dependencies] [dev-dependencies]
lazy_static = "1.4.0" lazy_static = "1.4.0"
wasm-bindgen-test = "0.3.38"
wasm-bindgen = "0.2.88"

View File

@ -6,7 +6,7 @@ use chorus::{
types::{GatewayIdentifyPayload, GatewayReady}, types::{GatewayIdentifyPayload, GatewayReady},
}; };
use std::{sync::Arc, time::Duration}; 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 // This example creates a simple gateway connection and a basic observer struct
@ -54,9 +54,10 @@ async fn main() {
let mut identify = GatewayIdentifyPayload::common(); let mut identify = GatewayIdentifyPayload::common();
identify.token = token; identify.token = token;
gateway.send_identify(identify).await; gateway.send_identify(identify).await;
safina_timer::start_timer_thread();
// Do something on the main thread so we don't quit // Do something on the main thread so we don't quit
loop { loop {
sleep(Duration::MAX).await; safina_timer::sleep_for(Duration::MAX).await
} }
} }

View File

@ -2,7 +2,6 @@ use std::time::Duration;
use chorus::gateway::Gateway; use chorus::gateway::Gateway;
use chorus::{self, types::GatewayIdentifyPayload}; use chorus::{self, types::GatewayIdentifyPayload};
use tokio::time::sleep;
/// This example creates a simple gateway connection and a session with an Identify event /// This example creates a simple gateway connection and a session with an Identify event
#[tokio::main(flavor = "current_thread")] #[tokio::main(flavor = "current_thread")]
@ -11,7 +10,7 @@ async fn main() {
let websocket_url_spacebar = "wss://gateway.old.server.spacebar.chat/".to_string(); 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 // 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 // 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; identify.token = token;
// Send off the event // 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 // Do something on the main thread so we don't quit
loop { loop {
sleep(Duration::MAX).await; safina_timer::sleep_for(Duration::MAX).await
} }
} }

View File

@ -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;

View File

@ -7,20 +7,21 @@ use tokio_tungstenite::{
connect_async_tls_with_config, tungstenite, Connector, MaybeTlsStream, WebSocketStream, connect_async_tls_with_config, tungstenite, Connector, MaybeTlsStream, WebSocketStream,
}; };
use super::GatewayMessage;
use crate::errors::GatewayError; use crate::errors::GatewayError;
use crate::gateway::GatewayMessage;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct WebSocketBackend; pub struct TungsteniteBackend;
// These could be made into inherent associated types when that's stabilized // These could be made into inherent associated types when that's stabilized
pub type WsSink = SplitSink<WebSocketStream<MaybeTlsStream<TcpStream>>, tungstenite::Message>; pub type TungsteniteSink =
pub type WsStream = SplitStream<WebSocketStream<MaybeTlsStream<TcpStream>>>; SplitSink<WebSocketStream<MaybeTlsStream<TcpStream>>, tungstenite::Message>;
pub type TungsteniteStream = SplitStream<WebSocketStream<MaybeTlsStream<TcpStream>>>;
impl WebSocketBackend { impl TungsteniteBackend {
pub async fn connect( pub async fn connect(
websocket_url: &str, websocket_url: &str,
) -> Result<(WsSink, WsStream), crate::errors::GatewayError> { ) -> Result<(TungsteniteSink, TungsteniteStream), crate::errors::GatewayError> {
let mut roots = rustls::RootCertStore::empty(); let mut roots = rustls::RootCertStore::empty();
for cert in rustls_native_certs::load_native_certs().expect("could not load platform certs") for cert in rustls_native_certs::load_native_certs().expect("could not load platform certs")
{ {

View File

@ -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<WsStream, WsMessage>;
pub type WasmStream = SplitStream<WsStream>;
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<GatewayMessage> for WsMessage {
fn from(message: GatewayMessage) -> Self {
Self::Text(message.0)
}
}
impl From<WsMessage> 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)
}
}
}
}

View File

@ -6,7 +6,7 @@ use tokio::task;
use self::event::Events; use self::event::Events;
use super::*; use super::*;
use super::{WsSink, WsStream}; use super::{Sink, Stream};
use crate::types::{ use crate::types::{
self, AutoModerationRule, AutoModerationRuleUpdate, Channel, ChannelCreate, ChannelDelete, self, AutoModerationRule, AutoModerationRuleUpdate, Channel, ChannelCreate, ChannelDelete,
ChannelUpdate, Guild, GuildRoleCreate, GuildRoleUpdate, JsonField, RoleObject, SourceUrlField, ChannelUpdate, Guild, GuildRoleCreate, GuildRoleUpdate, JsonField, RoleObject, SourceUrlField,
@ -17,8 +17,8 @@ use crate::types::{
pub struct Gateway { pub struct Gateway {
events: Arc<Mutex<Events>>, events: Arc<Mutex<Events>>,
heartbeat_handler: HeartbeatHandler, heartbeat_handler: HeartbeatHandler,
websocket_send: Arc<Mutex<WsSink>>, websocket_send: Arc<Mutex<Sink>>,
websocket_receive: WsStream, websocket_receive: Stream,
kill_send: tokio::sync::broadcast::Sender<()>, kill_send: tokio::sync::broadcast::Sender<()>,
store: Arc<Mutex<HashMap<Snowflake, Arc<RwLock<ObservableObject>>>>>, store: Arc<Mutex<HashMap<Snowflake, Arc<RwLock<ObservableObject>>>>>,
url: String, url: String,
@ -37,7 +37,10 @@ impl Gateway {
// Wait for the first hello and then spawn both tasks so we avoid nested tasks // 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 // 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(); 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(); let gateway_payload: types::GatewayReceivePayload = serde_json::from_str(&msg.0).unwrap();
if gateway_payload.op_code != GATEWAY_HELLO { if gateway_payload.op_code != GATEWAY_HELLO {
@ -91,11 +94,18 @@ impl Gateway {
loop { loop {
let msg = self.websocket_receive.next().await; 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 // 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 { if let Some(Ok(message)) = msg {
self.handle_message(message.into()).await; self.handle_message(message.into()).await;
continue; 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 // 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"); warn!("GW: Websocket is broken, stopping gateway");

View File

@ -14,7 +14,7 @@ use crate::types::{self, Composite};
pub struct GatewayHandle { pub struct GatewayHandle {
pub url: String, pub url: String,
pub events: Arc<Mutex<Events>>, pub events: Arc<Mutex<Events>>,
pub websocket_send: Arc<Mutex<WsSink>>, pub websocket_send: Arc<Mutex<Sink>>,
/// Tells gateway tasks to close /// Tells gateway tasks to close
pub(super) kill_send: tokio::sync::broadcast::Sender<()>, pub(super) kill_send: tokio::sync::broadcast::Sender<()>,
pub(crate) store: Arc<Mutex<HashMap<Snowflake, Arc<RwLock<ObservableObject>>>>>, pub(crate) store: Arc<Mutex<HashMap<Snowflake, Arc<RwLock<ObservableObject>>>>>,

View File

@ -27,7 +27,7 @@ pub(super) struct HeartbeatHandler {
impl HeartbeatHandler { impl HeartbeatHandler {
pub fn new( pub fn new(
heartbeat_interval: Duration, heartbeat_interval: Duration,
websocket_tx: Arc<Mutex<WsSink>>, websocket_tx: Arc<Mutex<Sink>>,
kill_rc: tokio::sync::broadcast::Receiver<()>, kill_rc: tokio::sync::broadcast::Receiver<()>,
) -> Self { ) -> Self {
let (send, receive) = tokio::sync::mpsc::channel(32); let (send, receive) = tokio::sync::mpsc::channel(32);
@ -49,7 +49,7 @@ impl HeartbeatHandler {
/// Can be killed by the kill broadcast; /// Can be killed by the kill broadcast;
/// If the websocket is closed, will die out next time it tries to send a heartbeat; /// If the websocket is closed, will die out next time it tries to send a heartbeat;
pub async fn heartbeat_task( pub async fn heartbeat_task(
websocket_tx: Arc<Mutex<WsSink>>, websocket_tx: Arc<Mutex<Sink>>,
heartbeat_interval: Duration, heartbeat_interval: Duration,
mut receive: Receiver<HeartbeatThreadCommunication>, mut receive: Receiver<HeartbeatThreadCommunication>,
mut kill_receive: tokio::sync::broadcast::Receiver<()>, mut kill_receive: tokio::sync::broadcast::Receiver<()>,

View File

@ -1,13 +1,13 @@
use async_trait::async_trait; use async_trait::async_trait;
#[cfg(not(target_arch = "wasm32"))] pub mod backends;
pub mod backend_tungstenite;
pub mod events; pub mod events;
pub mod gateway; pub mod gateway;
pub mod handle; pub mod handle;
pub mod heartbeat; pub mod heartbeat;
pub mod message; pub mod message;
pub use backends::*;
pub use gateway::*; pub use gateway::*;
pub use handle::*; pub use handle::*;
use heartbeat::*; use heartbeat::*;
@ -22,13 +22,6 @@ use std::sync::{Arc, RwLock};
use tokio::sync::Mutex; 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 // Gateway opcodes
/// Opcode received when the server dispatches a [crate::types::WebSocketEvent] /// Opcode received when the server dispatches a [crate::types::WebSocketEvent]
const GATEWAY_DISPATCH: u8 = 0; const GATEWAY_DISPATCH: u8 = 0;

7
tests/wasm.rs Normal file
View File

@ -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();
}