From 9e13354c9823e6f78644f6eb9143203d245652a4 Mon Sep 17 00:00:00 2001 From: koe Date: Sat, 7 Oct 2023 12:44:57 -0500 Subject: [PATCH] add ClientConnectorWasm --- CHANGELOG.md | 1 + Cargo.toml | 7 +-- README.md | 11 ++--- src/client.rs | 25 ++++++++--- .../client_connector_tokio.rs | 12 ++++-- .../client_connector_wasm.rs | 43 +++++++++++++++++++ src/client_connectors/mod.rs | 16 +++++-- src/lib.rs | 1 + 8 files changed, 96 insertions(+), 20 deletions(-) create mode 100644 src/client_connectors/client_connector_wasm.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index c33ef1e..6e6dc2f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Use [`tokio-tungstenite-wasm`](https://github.com/TannerRogalsky/tokio-tungstenite-wasm) errors internally to better support cross-platform clients. - Use [`enfync`](https://github.com/UkoeHB/enfync) runtime handles internally to better support cross-platform clients. Default clients continue to use tokio. - Add `ClientConnector` abstraction for connecting clients and add `ezsockets::client::connect_with`. +- Add `ClientConnectorWasm` and `wasm_client` feature. Migration guide: diff --git a/Cargo.toml b/Cargo.toml index 6e46c0f..6c11ee2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,7 @@ base64 = "0.21.0" enfync = "0.1.0" futures = "0.3.21" http = "0.2.8" -tokio = { version = "1.17.0", features = ["sync", "rt", "macros", "time"] } +tokio = { version = "1.17.0", features = ["sync", "macros", "time"] } tracing = "0.1.31" url = "2.2.2" cfg-if = "1.0.0" @@ -42,11 +42,12 @@ tokio-native-tls = { version = "0.3.1", optional = true } default = ["native_client", "server"] client = ["tokio-tungstenite-wasm"] -native_client = ["client", "tokio-tungstenite"] +native_client = ["client", "tokio-tungstenite", "tokio/rt"] +wasm_client = ["client", "tokio-tungstenite-wasm"] tungstenite_common = ["tokio-tungstenite"] -server = ["tungstenite_common", "tokio-tungstenite-wasm"] +server = ["tungstenite_common", "tokio-tungstenite-wasm", "tokio/rt"] tungstenite = ["server"] axum = ["server", "dep:axum", "axum-core", "bytes", "futures-util", "http-body", "hyper", "sha-1"] diff --git a/README.md b/README.md index 0aefe16..9dcfe07 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,9 @@ Creating a WebSocket server or a client in Rust can be troublesome. This crate f - Traits to allow declarative and event-based programming. - Easy concurrency with Tokio and async/await. Server sessions are Clone'able and can be shared between tasks. - Heartbeat mechanism to keep the connection alive. -- Automatic reconnection of WebSocket Client. -- Support for multiple back-ends such as Axum or Tungstenite. +- Automatic reconnection of WebSocket Clients. +- Support for arbitrary client back-ends, with built-in native and WASM client connectors. +- Support for multiple server back-ends such as Axum or Tungstenite. - TLS support for servers with `rustls` and `native-tls`. ## Documentation @@ -25,14 +26,14 @@ View the full documentation at [docs.rs/ezsockets](http://docs.rs/ezsockets) ## Client -[`tokio-tungstenite`](https://github.com/snapview/tokio-tungstenite) is being used under the hood. +By default clients use [`tokio-tungstenite`](https://github.com/snapview/tokio-tungstenite) under the hood. Disable default features and enable `wasm_client` to run clients on WASM targets. See [examples/simple-client](https://github.com/gbaranski/ezsockets/tree/master/examples/simple-client) for a simple usage and [docs.rs/ezsockets/server](https://docs.rs/ezsockets/latest/ezsockets/client/index.html) for documentation. ## Server -WebSocket server can use one of supported back-ends: +WebSocket server can use one of the supported back-ends: - [`tokio-tungstenite`](https://github.com/snapview/tokio-tungstenite) - the simplest way to get started. - [`axum`](https://github.com/tokio-rs/axum) - ergonomic and modular web framework built with Tokio, Tower, and Hyper - [`actix-web`](https://github.com/actix/actix-web) - Work in progress at [#22](https://github.com/gbaranski/ezsockets/issues/22) @@ -46,4 +47,4 @@ Licensed under [MIT](https://choosealicense.com/licenses/mit/). # Contact -Reach me out on Discord `gbaranski#5119`, or mail me at me@gbaranski.com. \ No newline at end of file +Reach me out on Discord `gbaranski#5119`, or mail me at me@gbaranski.com. diff --git a/src/client.rs b/src/client.rs index 968161d..67ce786 100644 --- a/src/client.rs +++ b/src/client.rs @@ -188,7 +188,13 @@ impl ClientConfig { self } - fn connect_http_request(&self) -> Request { + /// Get the config's headers. + pub fn headers(&self) -> &http::HeaderMap { + &self.headers + } + + /// Extract a Websockets HTTP request. + pub fn connect_http_request(&self) -> Request { let mut http_request = Request::builder() .uri(self.url.as_str()) .method("GET") @@ -207,6 +213,13 @@ impl ClientConfig { } http_request } + + /// Extract the URL request. + /// + /// This is needed for WASM clients, where building HTTP requests is deferred to the `web_sys::Websocket` implementation. + pub fn connect_url(&self) -> &str { + self.url.as_str() + } } #[derive(Debug, Clone)] @@ -275,7 +288,10 @@ pub trait ClientConnector { /// Connect to a websocket server. /// /// Returns `Err` if the request is invalid. - async fn connect(&self, request: Request) -> Result; + async fn connect( + &self, + client_config: &ClientConfig, + ) -> Result; } /// An `ezsockets` client. @@ -381,7 +397,7 @@ pub async fn connect( client_fn: impl FnOnce(Client) -> E, config: ClientConfig, ) -> (Client, impl Future>) { - let client_connector = crate::client_connector_tokio::ClientConnectorTokio::default(); + let client_connector = crate::ClientConnectorTokio::default(); let (handle, mut future) = connect_with(client_fn, config, client_connector); let future = async move { future @@ -551,8 +567,7 @@ async fn client_connect( for i in 1.. { // connection attempt tracing::info!("connecting attempt no: {}...", i); - let connect_http_request = config.connect_http_request(); - let result = client_connector.connect(connect_http_request).await; + let result = client_connector.connect(config).await; match result { Ok(socket_impl) => { tracing::info!("successfully connected"); diff --git a/src/client_connectors/client_connector_tokio.rs b/src/client_connectors/client_connector_tokio.rs index 9979fd3..d064008 100644 --- a/src/client_connectors/client_connector_tokio.rs +++ b/src/client_connectors/client_connector_tokio.rs @@ -1,5 +1,4 @@ -use crate::client::ClientConnector; -use crate::Request; +use crate::client::{ClientConfig, ClientConnector}; use enfync::TryAdopt; use tokio_tungstenite::tungstenite; @@ -27,6 +26,12 @@ impl Default for ClientConnectorTokio { } } +impl From for ClientConnectorTokio { + fn from(handle: enfync::builtin::native::TokioHandle) -> Self { + Self { handle } + } +} + #[async_trait::async_trait] impl ClientConnector for ClientConnectorTokio { type Handle = enfync::builtin::native::TokioHandle; @@ -45,7 +50,8 @@ impl ClientConnector for ClientConnectorTokio { /// Connect to a websocket server. /// /// Returns `Err` if the request is invalid. - async fn connect(&self, request: Request) -> Result { + async fn connect(&self, config: &ClientConfig) -> Result { + let request = config.connect_http_request(); let (socket, _) = tokio_tungstenite::connect_async(request).await?; Ok(socket) } diff --git a/src/client_connectors/client_connector_wasm.rs b/src/client_connectors/client_connector_wasm.rs new file mode 100644 index 0000000..1d33dad --- /dev/null +++ b/src/client_connectors/client_connector_wasm.rs @@ -0,0 +1,43 @@ +use crate::client::{ClientConfig, ClientConnector}; + +/// Implementation of [`ClientConnector`] for tokio runtimes. +#[derive(Clone)] +pub struct ClientConnectorWasm { + handle: enfync::builtin::wasm::WASMHandle, +} + +impl Default for ClientConnectorWasm { + fn default() -> Self { + let handle = enfync::builtin::wasm::WASMHandle::default(); + Self { handle } + } +} + +#[async_trait::async_trait] +impl ClientConnector for ClientConnectorWasm { + type Handle = enfync::builtin::wasm::WASMHandle; + type Message = tokio_tungstenite_wasm::Message; + type WSError = tokio_tungstenite_wasm::Error; + type Socket = tokio_tungstenite_wasm::WebSocketStream; + type ConnectError = tokio_tungstenite_wasm::Error; + + /// Get the connector's runtime handle. + fn handle(&self) -> Self::Handle { + self.handle.clone() + } + + /// Connect to a websocket server. + /// + /// Returns `Err` if the request is invalid. + /// + /// Panics if any headers were added to the client config. Websockets on browser does not support + /// additional headers (use [`ClientConfig::query_parameter()`] instead). + async fn connect(&self, config: &ClientConfig) -> Result { + if config.headers().len() > 0 { + panic!("client may not submit HTTP headers in WASM connection requests"); + } + let request_url = config.connect_url(); + let (socket, _) = tokio_tungstenite_wasm::connect(request_url).await?; + Ok(socket) + } +} diff --git a/src/client_connectors/mod.rs b/src/client_connectors/mod.rs index 1c183c8..5603a46 100644 --- a/src/client_connectors/mod.rs +++ b/src/client_connectors/mod.rs @@ -1,5 +1,13 @@ -#[cfg(feature = "native_client")] -pub mod client_connector_tokio; +cfg_if::cfg_if! { + if #[cfg(all(feature = "native_client", not(target_family = "wasm")))] { + mod client_connector_tokio; + pub use client_connector_tokio::*; + } +} -//#[cfg(feature = "wasm_client")] -//pub mod client_connector_wasm; +cfg_if::cfg_if! { + if #[cfg(all(feature = "wasm_client", target_family = "wasm"))] { + mod client_connector_wasm; + pub use client_connector_wasm::*; + } +} diff --git a/src/lib.rs b/src/lib.rs index 30f352b..fba09bb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -28,6 +28,7 @@ cfg_if::cfg_if! { pub mod client; pub use client::connect; + pub use client::connect_with; pub use client::ClientConfig; pub use client::ClientExt; pub use client::Client;