From 023f7033a87b72d7396b6e68a8df3d55e202dfa2 Mon Sep 17 00:00:00 2001 From: Hamza Date: Thu, 14 Jan 2021 21:29:40 +0500 Subject: [PATCH 01/36] Initial commit --- .gitignore | 2 + .idea/.gitignore | 8 ++ .idea/modules.xml | 8 ++ .idea/reqwasm.iml | 23 +++++ .idea/vcs.xml | 6 ++ Cargo.toml | 39 +++++++ README.md | 13 +++ src/error.rs | 31 ++++++ src/http.rs | 251 ++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 18 ++++ tests/http.rs | 76 ++++++++++++++ 11 files changed, 475 insertions(+) create mode 100644 .gitignore create mode 100644 .idea/.gitignore create mode 100644 .idea/modules.xml create mode 100644 .idea/reqwasm.iml create mode 100644 .idea/vcs.xml create mode 100644 Cargo.toml create mode 100644 README.md create mode 100644 src/error.rs create mode 100644 src/http.rs create mode 100644 src/lib.rs create mode 100644 tests/http.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..96ef6c0b --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +Cargo.lock diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 00000000..73f69e09 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml +# Editor-based HTTP Client requests +/httpRequests/ diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 00000000..3bc95263 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/reqwasm.iml b/.idea/reqwasm.iml new file mode 100644 index 00000000..9c8f511c --- /dev/null +++ b/.idea/reqwasm.iml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 00000000..35eb1ddf --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 00000000..1b28b6cd --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,39 @@ +[package] +name = "reqwasm" +version = "0.1.0" +authors = ["Hamza "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +wasm-bindgen = { version = "0.2", features = ["serde-serialize"] } +js-sys = "0.3" +wasm-bindgen-futures = "0.4" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +anyhow = "1.0" +thiserror = "1.0" + +[dependencies.web-sys] +version = "0.3.4" +features = [ + 'Headers', + 'Request', + 'RequestInit', + 'RequestMode', + 'Response', + 'Window', + 'RequestCache', + 'RequestCredentials', + 'ObserverCallback', + 'RequestRedirect', + 'ReferrerPolicy', + 'AbortSignal', + 'ReadableStream', + 'Blob', + 'FormData' +] + +[dev-dependencies] +wasm-bindgen-test = "0.3" diff --git a/README.md b/README.md new file mode 100644 index 00000000..5d4f4283 --- /dev/null +++ b/README.md @@ -0,0 +1,13 @@ +# Reqwasm + +HTTP requests library for WASM Apps + +## Example + +```rust +let resp = Request::get("/path") + .send() + .await + .unwrap(); +assert_eq!(resp.status(), 200); +``` diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 00000000..00d1916d --- /dev/null +++ b/src/error.rs @@ -0,0 +1,31 @@ +use wasm_bindgen::{JsValue, JsCast}; +use js_sys::TypeError; +use thiserror::Error; + +/// All the errors returned by this crate. +#[derive(Debug, Error)] +pub enum Error { + /// Generic error returned by JavaScript. + #[error("{0:?}")] + JsError(JsValue), + /// TypeError returned by JavaScript. + #[error("{0:?}")] + TypeError(TypeError), + /// Error returned by `serde` during deserialization. + #[error("{0}")] + SerdeError( + #[source] + #[from] + serde_json::Error, + ), + /// Unknown error. + #[error("{0}")] + Other(anyhow::Error), +} + +pub(crate) fn js_to_error(js_value: JsValue) -> Error { + match js_value.dyn_into::() { + Ok(type_error) => Error::TypeError(type_error), + Err(val) => Error::JsError(val), + } +} diff --git a/src/http.rs b/src/http.rs new file mode 100644 index 00000000..03ad520b --- /dev/null +++ b/src/http.rs @@ -0,0 +1,251 @@ +use crate::{Error, js_to_error}; +use serde::de::DeserializeOwned; +use std::fmt; +use wasm_bindgen::prelude::*; +use wasm_bindgen::JsCast; +use wasm_bindgen_futures::JsFuture; +use web_sys::window; +pub use web_sys::{ + AbortSignal, Blob, FormData, Headers, ObserverCallback, ReadableStream, ReferrerPolicy, + RequestCache, RequestCredentials, RequestMode, RequestRedirect, +}; + +/// Valid request methods. +#[derive(Clone, Copy, Debug)] +pub enum RequestMethod { + GET, + POST, + PATCH, + DELETE, + PUT, +} + +impl fmt::Display for RequestMethod { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let s = match self { + RequestMethod::GET => "GET", + RequestMethod::POST => "POST", + RequestMethod::PATCH => "PATCH", + RequestMethod::DELETE => "DELETE", + RequestMethod::PUT => "PUT", + }; + write!(f, "{}", s) + } +} + +/// A request. +pub struct Request { + options: web_sys::RequestInit, + headers: web_sys::Headers, + url: String, +} + +impl Request { + /// Creates a new request with a url. + pub fn new(url: &str) -> Self { + Self { + options: web_sys::RequestInit::new(), + headers: web_sys::Headers::new().expect("headers"), + url: url.into(), + } + } + + /// Sets the body. + pub fn body(mut self, body: impl Into) -> Self { + self.options.body(Some(&body.into())); + self + } + + /// Sets the request cache. + pub fn cache(mut self, cache: RequestCache) -> Self { + self.options.cache(cache); + self + } + + /// Sets the request credentials. + pub fn credentials(mut self, credentials: RequestCredentials) -> Self { + self.options.credentials(credentials); + self + } + + /// Sets a header. + pub fn header(self, key: &str, value: &str) -> Self { + self.headers.set(key, value).expect("set header"); + self + } + + /// Sets the request integrity. + pub fn integrity(mut self, integrity: &str) -> Self { + self.options.integrity(integrity); + self + } + + /// Sets the request method. + pub fn method(mut self, method: RequestMethod) -> Self { + self.options.method(&method.to_string()); + self + } + + /// Sets the request mode. + pub fn mode(mut self, mode: RequestMode) -> Self { + self.options.mode(mode); + self + } + + /// Sets the observer callback. + pub fn observe(mut self, observe: &ObserverCallback) -> Self { + self.options.observe(observe); + self + } + + /// Sets the request redirect. + pub fn redirect(mut self, redirect: RequestRedirect) -> Self { + self.options.redirect(redirect); + self + } + + /// Sets the request referrer. + pub fn referrer(mut self, referrer: &str) -> Self { + self.options.referrer(referrer); + self + } + + /// Sets the request referrer policy. + pub fn referrer_policy(mut self, referrer_policy: ReferrerPolicy) -> Self { + self.options.referrer_policy(referrer_policy); + self + } + + /// Sets the request abort signal. + pub fn abort_signal(mut self, signal: Option<&AbortSignal>) -> Self { + self.options.signal(signal); + self + } + + /// Executes the request. + pub async fn send(mut self) -> Result { + self.options.headers(&self.headers); + + let request = web_sys::Request::new_with_str_and_init(&self.url, &self.options) + .map_err(js_to_error)?; + + let promise = window().unwrap().fetch_with_request(&request); + let response = JsFuture::from(promise).await.map_err(js_to_error)?; + match response.dyn_into::() { + Ok(response) => Ok(Response { + response: response.unchecked_into(), + }), + Err(_) => Err(Error::Other(anyhow::anyhow!("can't convert to Response"))), + } + } + + /// Creates a new [`GET`][RequestMethod::GET] `Request` with url. + pub fn get(url: &str) -> Self { + Self::new(url).method(RequestMethod::GET) + } + + /// Creates a new [`POST`][RequestMethod::POST] `Request` with url. + pub fn post(url: &str) -> Self { + Self::new(url).method(RequestMethod::POST) + } + + /// Creates a new [`PUT`][RequestMethod::PUT] `Request` with url. + pub fn put(url: &str) -> Self { + Self::new(url).method(RequestMethod::PUT) + } + + /// Creates a new [`DELETE`][RequestMethod::DELETE] `Request` with url. + pub fn delete(url: &str) -> Self { + Self::new(url).method(RequestMethod::DELETE) + } + + /// Creates a new [`PATCH`][RequestMethod::PATCH] `Request` with url. + pub fn patch(url: &str) -> Self { + Self::new(url).method(RequestMethod::PATCH) + } +} + +/// The [`Request`]'s response +pub struct Response { + response: web_sys::Response, +} + +impl Response { + /// Gets the url. + pub fn url(&self) -> String { + self.response.url() + } + + /// Whether the request was redirected. + pub fn redirected(&self) -> bool { + self.response.redirected() + } + + /// Gets the status. + pub fn status(&self) -> u16 { + self.response.status() + } + + /// Whether the response was `ok`. + pub fn ok(&self) -> bool { + self.response.ok() + } + + /// Gets the status text. + pub fn status_text(&self) -> String { + self.response.status_text() + } + + /// Gets the headers. + pub fn headers(&self) -> Headers { + self.response.headers() + } + + /// Whether the body was used. + pub fn body_used(&self) -> bool { + self.response.body_used() + } + + /// Gets the body. + pub fn body(&self) -> Option { + self.response.body() + } + + /// Gets as array buffer. + pub async fn array_buffer(&self) -> Result { + let promise = self.response.array_buffer().map_err(js_to_error)?; + let val = JsFuture::from(promise).await.map_err(js_to_error)?; + Ok(js_sys::ArrayBuffer::from(val)) + } + + /// Gets as blob. + pub async fn blob(&self) -> Result { + let promise = self.response.blob().map_err(js_to_error)?; + let val = JsFuture::from(promise).await.map_err(js_to_error)?; + Ok(Blob::from(val)) + } + + /// Gets the form data. + pub async fn form_data(&self) -> Result { + let promise = self.response.form_data().map_err(js_to_error)?; + let val = JsFuture::from(promise).await.map_err(js_to_error)?; + Ok(FormData::from(val)) + } + + /// Gets and parses the json. + pub async fn json(&self) -> Result { + let promise = self.response.json().map_err(js_to_error)?; + let json = JsFuture::from(promise).await.map_err(js_to_error)?; + + Ok(json.into_serde()?) + } + + /// Gets the response text. + pub async fn text(&self) -> Result { + let promise = self.response.text().unwrap(); + let val = JsFuture::from(promise).await.map_err(js_to_error)?; + let string = js_sys::JsString::from(val); + Ok(String::from(&string)) + } +} + diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 00000000..6f1fc300 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,18 @@ +//! HTTP requests library for WASM Apps +//! +//! # Example +//! +//! ```no_run +//! # use reqwasm::Request; +//! let resp = Request::get("/path") +//! .send() +//! .await +//! .unwrap(); +//! assert_eq!(resp.status(), 200); +//! ``` + +mod http; +mod error; + +pub use http::*; +pub use error::*; diff --git a/tests/http.rs b/tests/http.rs new file mode 100644 index 00000000..20d691a7 --- /dev/null +++ b/tests/http.rs @@ -0,0 +1,76 @@ +use reqwasm::*; +use serde::{Deserialize, Serialize}; +use wasm_bindgen_test::*; + +wasm_bindgen_test_configure!(run_in_browser); + +const HTTPBIN_URL: &str = env!("HTTPBIN_URL"); + +#[wasm_bindgen_test] +async fn fetch() { + let resp = Request::get(&format!("{}/get", HTTPBIN_URL)) + .send() + .await + .unwrap(); + assert_eq!(resp.status(), 200); +} + +#[wasm_bindgen_test] +async fn fetch_json() { + #[derive(Deserialize, Debug)] + struct HttpBin { + url: String, + } + + let url = format!("{}/get", HTTPBIN_URL); + let resp = Request::get(&url).send().await.unwrap(); + let json: HttpBin = resp.json().await.unwrap(); + assert_eq!(resp.status(), 200); + assert_eq!(json.url, url); +} + +#[wasm_bindgen_test] +async fn auth_valid_bearer() { + let resp = Request::get(&format!("{}/get", HTTPBIN_URL)) + .header("Authorization", "Bearer token") + .send() + .await + .unwrap(); + + assert_eq!(resp.status(), 200); +} + +#[wasm_bindgen_test] +async fn gzip_response() { + #[derive(Deserialize, Debug)] + struct HttpBin { + gzipped: bool, + } + + let resp = Request::get(&format!("{}/gzip", HTTPBIN_URL)).send().await.unwrap(); + let json: HttpBin = resp.json().await.unwrap(); + assert_eq!(resp.status(), 200); + assert_eq!(json.gzipped, true); +} + +#[wasm_bindgen_test] +async fn post_json() { + #[derive(Serialize, Deserialize, Debug)] + struct Payload { + data: String + } + + #[derive(Deserialize, Debug)] + struct HttpBin { + json: Payload, + } + + let resp = Request::post(&format!("{}/anything", HTTPBIN_URL)) + .body(serde_json::to_string(&Payload { data: "data".to_string() }).unwrap()) + .send() + .await.unwrap(); + let json: HttpBin = resp.json().await.unwrap(); + assert_eq!(resp.status(), 200); + assert_eq!(json.json.data, "data"); +} + From 6dc24dcf54cea15d98bcb6128aba4f31435c5aef Mon Sep 17 00:00:00 2001 From: Hamza Date: Fri, 22 Jan 2021 18:37:18 +0500 Subject: [PATCH 02/36] provide a better interface for errors, rename `RequestMethod` to `Method` --- src/error.rs | 38 ++++++++++++++++++++++++++------------ src/http.rs | 26 +++++++++++++------------- 2 files changed, 39 insertions(+), 25 deletions(-) diff --git a/src/error.rs b/src/error.rs index 00d1916d..42da905e 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,16 +1,26 @@ use wasm_bindgen::{JsValue, JsCast}; -use js_sys::TypeError; -use thiserror::Error; +use thiserror::Error as ThisError; +use std::fmt; + +#[derive(Debug)] +pub struct JsError { + pub name: String, + pub message: String, + js_to_string: String, +} + +impl fmt::Display for JsError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.js_to_string) + } +} /// All the errors returned by this crate. -#[derive(Debug, Error)] +#[derive(Debug, ThisError)] pub enum Error { - /// Generic error returned by JavaScript. - #[error("{0:?}")] - JsError(JsValue), - /// TypeError returned by JavaScript. - #[error("{0:?}")] - TypeError(TypeError), + /// Error returned by JavaScript. + #[error("{0}")] + JsError(JsError), /// Error returned by `serde` during deserialization. #[error("{0}")] SerdeError( @@ -24,8 +34,12 @@ pub enum Error { } pub(crate) fn js_to_error(js_value: JsValue) -> Error { - match js_value.dyn_into::() { - Ok(type_error) => Error::TypeError(type_error), - Err(val) => Error::JsError(val), + match js_value.dyn_into::() { + Ok(error) => Error::JsError(JsError { + name: String::from(error.name()), + message: String::from(error.message()), + js_to_string: String::from(error.to_string()) + }), + Err(_) => unreachable!("JsValue passed is not an Error type -- this is a bug"), } } diff --git a/src/http.rs b/src/http.rs index 03ad520b..68867b27 100644 --- a/src/http.rs +++ b/src/http.rs @@ -12,7 +12,7 @@ pub use web_sys::{ /// Valid request methods. #[derive(Clone, Copy, Debug)] -pub enum RequestMethod { +pub enum Method { GET, POST, PATCH, @@ -20,14 +20,14 @@ pub enum RequestMethod { PUT, } -impl fmt::Display for RequestMethod { +impl fmt::Display for Method { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let s = match self { - RequestMethod::GET => "GET", - RequestMethod::POST => "POST", - RequestMethod::PATCH => "PATCH", - RequestMethod::DELETE => "DELETE", - RequestMethod::PUT => "PUT", + Method::GET => "GET", + Method::POST => "POST", + Method::PATCH => "PATCH", + Method::DELETE => "DELETE", + Method::PUT => "PUT", }; write!(f, "{}", s) } @@ -81,7 +81,7 @@ impl Request { } /// Sets the request method. - pub fn method(mut self, method: RequestMethod) -> Self { + pub fn method(mut self, method: Method) -> Self { self.options.method(&method.to_string()); self } @@ -141,27 +141,27 @@ impl Request { /// Creates a new [`GET`][RequestMethod::GET] `Request` with url. pub fn get(url: &str) -> Self { - Self::new(url).method(RequestMethod::GET) + Self::new(url).method(Method::GET) } /// Creates a new [`POST`][RequestMethod::POST] `Request` with url. pub fn post(url: &str) -> Self { - Self::new(url).method(RequestMethod::POST) + Self::new(url).method(Method::POST) } /// Creates a new [`PUT`][RequestMethod::PUT] `Request` with url. pub fn put(url: &str) -> Self { - Self::new(url).method(RequestMethod::PUT) + Self::new(url).method(Method::PUT) } /// Creates a new [`DELETE`][RequestMethod::DELETE] `Request` with url. pub fn delete(url: &str) -> Self { - Self::new(url).method(RequestMethod::DELETE) + Self::new(url).method(Method::DELETE) } /// Creates a new [`PATCH`][RequestMethod::PATCH] `Request` with url. pub fn patch(url: &str) -> Self { - Self::new(url).method(RequestMethod::PATCH) + Self::new(url).method(Method::PATCH) } } From 196736a6562bbb5a3343b138d57055fe9e7f88b7 Mon Sep 17 00:00:00 2001 From: Hamza Date: Wed, 3 Feb 2021 19:58:36 +0500 Subject: [PATCH 03/36] remove method for array buffer and blob in favor of as_raw --- Cargo.toml | 3 ++- src/http.rs | 17 ++++------------- 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1b28b6cd..75362044 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,7 +32,8 @@ features = [ 'AbortSignal', 'ReadableStream', 'Blob', - 'FormData' + 'FormData', + 'FileReader', ] [dev-dependencies] diff --git a/src/http.rs b/src/http.rs index 68867b27..24cc3fc3 100644 --- a/src/http.rs +++ b/src/http.rs @@ -6,7 +6,7 @@ use wasm_bindgen::JsCast; use wasm_bindgen_futures::JsFuture; use web_sys::window; pub use web_sys::{ - AbortSignal, Blob, FormData, Headers, ObserverCallback, ReadableStream, ReferrerPolicy, + AbortSignal, FormData, Headers, ObserverCallback, ReadableStream, ReferrerPolicy, RequestCache, RequestCredentials, RequestMode, RequestRedirect, }; @@ -211,18 +211,9 @@ impl Response { self.response.body() } - /// Gets as array buffer. - pub async fn array_buffer(&self) -> Result { - let promise = self.response.array_buffer().map_err(js_to_error)?; - let val = JsFuture::from(promise).await.map_err(js_to_error)?; - Ok(js_sys::ArrayBuffer::from(val)) - } - - /// Gets as blob. - pub async fn blob(&self) -> Result { - let promise = self.response.blob().map_err(js_to_error)?; - let val = JsFuture::from(promise).await.map_err(js_to_error)?; - Ok(Blob::from(val)) + /// Gets the raw [`Response`][web_sys::Response] object. + pub fn as_raw(&self) -> &web_sys::Response { + &self.response } /// Gets the form data. From b2fc17238675b4dafc5635b25f74696bb650b7ef Mon Sep 17 00:00:00 2001 From: Hamza Date: Wed, 3 Feb 2021 20:13:17 +0500 Subject: [PATCH 04/36] prepare for release --- Cargo.toml | 10 ++- LICENSE-APACHE.md | 201 ++++++++++++++++++++++++++++++++++++++++++++++ LICENSE-MIT.md | 21 +++++ README.md | 6 +- 4 files changed, 234 insertions(+), 4 deletions(-) create mode 100644 LICENSE-APACHE.md create mode 100644 LICENSE-MIT.md diff --git a/Cargo.toml b/Cargo.toml index 75362044..262e40b2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,11 +3,15 @@ name = "reqwasm" version = "0.1.0" authors = ["Hamza "] edition = "2018" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +license = "MIT OR Apache-2.0" +repository = "https://github.com/hamza1311/reqwasm" +description = "HTTP requests library for WASM Apps" +readme = "README.md" +keywords = ["requests", "http", "wasm"] +categories = ["wasm", "web-programming::http-client", "api-bindings"] [dependencies] -wasm-bindgen = { version = "0.2", features = ["serde-serialize"] } +wasm-bindgen = { version = "0.2", features = ["serde-serialize"] } js-sys = "0.3" wasm-bindgen-futures = "0.4" serde = { version = "1.0", features = ["derive"] } diff --git a/LICENSE-APACHE.md b/LICENSE-APACHE.md new file mode 100644 index 00000000..c319da33 --- /dev/null +++ b/LICENSE-APACHE.md @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/LICENSE-MIT.md b/LICENSE-MIT.md new file mode 100644 index 00000000..d8f92278 --- /dev/null +++ b/LICENSE-MIT.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Muhammad Hamza + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 5d4f4283..9e16f730 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Reqwasm -HTTP requests library for WASM Apps +HTTP requests library for WASM Apps. It provides idiomatic Rust bindings for the `web_sys` `fetch` API ## Example @@ -11,3 +11,7 @@ let resp = Request::get("/path") .unwrap(); assert_eq!(resp.status(), 200); ``` + +## Contributions + +Your PRs and Issues are welcome. Note that all the contribution submitted by you, shall be licensed as MIT or APACHE 2.0 at your choice, without any additional terms or conditions. From a66c877825eb3e4f53b3ad9d25f078b0479bb82f Mon Sep 17 00:00:00 2001 From: Hamza Date: Wed, 3 Feb 2021 20:53:35 +0500 Subject: [PATCH 05/36] add CI, update readme --- .github/workflows/ci.yml | 91 ++++++++++++++++++++++++++++++++++++++++ README.md | 5 +++ src/error.rs | 6 +-- src/http.rs | 7 ++-- src/lib.rs | 4 +- tests/http.rs | 18 +++++--- 6 files changed, 117 insertions(+), 14 deletions(-) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..15a1d245 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,91 @@ +name: CI + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +env: + CARGO_TERM_COLOR: always + +jobs: + fmt: + runs-on: ubuntu-latest + steps: + - name: Format + - uses: actions/checkout@v2 + + - uses: actions/cache@v2 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: cargo-${{ runner.os }}-fmt-${{ hashFiles('**/Cargo.toml') }} + restore-keys: | + cargo-${{ runner.os }}-fmt- + cargo-${{ runner.os }}- + + - name: Run cargo fmt + uses: actions-rs/cargo@v1 + with: + command: fmt + args: -- --check + + clippy: + name: Clippy + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + toolchain: stable + profile: minimal + components: clippy + target: wasm32-unknown-unknown + + - uses: actions/cache@v2 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: cargo-${{ runner.os }}-clippy-${{ hashFiles('**/Cargo.toml') }} + restore-keys: | + cargo-${{ runner.os }}-clippy- + cargo-${{ runner.os }}- + + - name: Run clippy + uses: actions-rs/cargo@v1 + with: + command: clippy + args: -- -D warnings + + test: + runs-on: ubuntu-latest + services: + httpbin: + image: kennethreitz/httpbin@sha256:599fe5e5073102dbb0ee3dbb65f049dab44fa9fc251f6835c9990f8fb196a72b + ports: + - 8080:80 + + steps: + - uses: actions/checkout@v2 + + - uses: actions/cache@v2 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: cargo-${{ runner.os }}-test-${{ hashFiles('**/Cargo.toml') }} + restore-keys: | + cargo-${{ runner.os }}-test- + cargo-${{ runner.os }}- + + - name: Run tests - yew-services + env: + HTTPBIN_URL: "http://localhost:8080" + run: | + wasm-pack test --chrome --firefox --headless diff --git a/README.md b/README.md index 9e16f730..3f9d3e46 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,10 @@ # Reqwasm +![GitHub branch checks state](https://img.shields.io/github/checks-status/hamza1311/reqwasm/master) +[![crates.io](https://img.shields.io/crates/v/reqwasm.svg?style=flat)](https://crates.io/crates/reqwasm) +[![docs.rs](https://img.shields.io/docsrs/reqwasm)](https://docs.rs/reqwasm/) +![licence](https://img.shields.io/crates/l/reqwasm) + HTTP requests library for WASM Apps. It provides idiomatic Rust bindings for the `web_sys` `fetch` API ## Example diff --git a/src/error.rs b/src/error.rs index 42da905e..3e7308a6 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,6 +1,6 @@ -use wasm_bindgen::{JsValue, JsCast}; -use thiserror::Error as ThisError; use std::fmt; +use thiserror::Error as ThisError; +use wasm_bindgen::{JsCast, JsValue}; #[derive(Debug)] pub struct JsError { @@ -38,7 +38,7 @@ pub(crate) fn js_to_error(js_value: JsValue) -> Error { Ok(error) => Error::JsError(JsError { name: String::from(error.name()), message: String::from(error.message()), - js_to_string: String::from(error.to_string()) + js_to_string: String::from(error.to_string()), }), Err(_) => unreachable!("JsValue passed is not an Error type -- this is a bug"), } diff --git a/src/http.rs b/src/http.rs index 24cc3fc3..0ab5a154 100644 --- a/src/http.rs +++ b/src/http.rs @@ -1,4 +1,4 @@ -use crate::{Error, js_to_error}; +use crate::{js_to_error, Error}; use serde::de::DeserializeOwned; use std::fmt; use wasm_bindgen::prelude::*; @@ -6,8 +6,8 @@ use wasm_bindgen::JsCast; use wasm_bindgen_futures::JsFuture; use web_sys::window; pub use web_sys::{ - AbortSignal, FormData, Headers, ObserverCallback, ReadableStream, ReferrerPolicy, - RequestCache, RequestCredentials, RequestMode, RequestRedirect, + AbortSignal, FormData, Headers, ObserverCallback, ReadableStream, ReferrerPolicy, RequestCache, + RequestCredentials, RequestMode, RequestRedirect, }; /// Valid request methods. @@ -239,4 +239,3 @@ impl Response { Ok(String::from(&string)) } } - diff --git a/src/lib.rs b/src/lib.rs index 6f1fc300..00f9a1f8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,8 +11,8 @@ //! assert_eq!(resp.status(), 200); //! ``` -mod http; mod error; +mod http; -pub use http::*; pub use error::*; +pub use http::*; diff --git a/tests/http.rs b/tests/http.rs index 20d691a7..0e07b980 100644 --- a/tests/http.rs +++ b/tests/http.rs @@ -47,7 +47,10 @@ async fn gzip_response() { gzipped: bool, } - let resp = Request::get(&format!("{}/gzip", HTTPBIN_URL)).send().await.unwrap(); + let resp = Request::get(&format!("{}/gzip", HTTPBIN_URL)) + .send() + .await + .unwrap(); let json: HttpBin = resp.json().await.unwrap(); assert_eq!(resp.status(), 200); assert_eq!(json.gzipped, true); @@ -57,7 +60,7 @@ async fn gzip_response() { async fn post_json() { #[derive(Serialize, Deserialize, Debug)] struct Payload { - data: String + data: String, } #[derive(Deserialize, Debug)] @@ -66,11 +69,16 @@ async fn post_json() { } let resp = Request::post(&format!("{}/anything", HTTPBIN_URL)) - .body(serde_json::to_string(&Payload { data: "data".to_string() }).unwrap()) + .body( + serde_json::to_string(&Payload { + data: "data".to_string(), + }) + .unwrap(), + ) .send() - .await.unwrap(); + .await + .unwrap(); let json: HttpBin = resp.json().await.unwrap(); assert_eq!(resp.status(), 200); assert_eq!(json.json.data, "data"); } - From 7fd401c83d6714cac5ff272ee4348432c7534f5b Mon Sep 17 00:00:00 2001 From: Hamza Date: Wed, 3 Feb 2021 21:03:09 +0500 Subject: [PATCH 06/36] hide JsError in the docs --- src/error.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/error.rs b/src/error.rs index 3e7308a6..939e5d55 100644 --- a/src/error.rs +++ b/src/error.rs @@ -3,6 +3,7 @@ use thiserror::Error as ThisError; use wasm_bindgen::{JsCast, JsValue}; #[derive(Debug)] +#[doc(hidden)] pub struct JsError { pub name: String, pub message: String, From 408bd000fc41c363e05ee2f0a20d5d503d2eb8a0 Mon Sep 17 00:00:00 2001 From: Hamza Date: Wed, 3 Feb 2021 21:05:49 +0500 Subject: [PATCH 07/36] fix CI? --- .github/workflows/ci.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 15a1d245..8465023e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,9 +11,9 @@ env: jobs: fmt: + name: Format runs-on: ubuntu-latest steps: - - name: Format - uses: actions/checkout@v2 - uses: actions/cache@v2 @@ -60,7 +60,7 @@ jobs: uses: actions-rs/cargo@v1 with: command: clippy - args: -- -D warnings + args: --target wasm32-unknown-unknown -- -D warnings test: runs-on: ubuntu-latest @@ -87,5 +87,4 @@ jobs: - name: Run tests - yew-services env: HTTPBIN_URL: "http://localhost:8080" - run: | - wasm-pack test --chrome --firefox --headless + run: wasm-pack test --chrome --firefox --headless From 1280c9712fb1f632955631b00a625a6b5af54f55 Mon Sep 17 00:00:00 2001 From: Hamza Date: Wed, 3 Feb 2021 21:09:31 +0500 Subject: [PATCH 08/36] Install wasm-pack in CI --- .github/workflows/ci.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8465023e..9035a094 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -63,6 +63,7 @@ jobs: args: --target wasm32-unknown-unknown -- -D warnings test: + name: Test runs-on: ubuntu-latest services: httpbin: @@ -73,6 +74,9 @@ jobs: steps: - uses: actions/checkout@v2 + - name: Install wasm-pack + run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh + - uses: actions/cache@v2 with: path: | @@ -84,7 +88,7 @@ jobs: cargo-${{ runner.os }}-test- cargo-${{ runner.os }}- - - name: Run tests - yew-services + - name: Run tests env: HTTPBIN_URL: "http://localhost:8080" run: wasm-pack test --chrome --firefox --headless From f41826609a14df75d6654454b407bbca6a5e85bd Mon Sep 17 00:00:00 2001 From: Hamza Date: Sun, 25 Apr 2021 13:29:52 +0500 Subject: [PATCH 09/36] misc --- .idea/reqwasm.iml | 10 ++++++++++ src/http.rs | 37 +++++++++++++++++++++++++++++++++---- src/lib.rs | 4 +++- 3 files changed, 46 insertions(+), 5 deletions(-) diff --git a/.idea/reqwasm.iml b/.idea/reqwasm.iml index 9c8f511c..0c0f13b4 100644 --- a/.idea/reqwasm.iml +++ b/.idea/reqwasm.iml @@ -15,6 +15,16 @@ + + + + + + + + + + diff --git a/src/http.rs b/src/http.rs index 0ab5a154..cd32b354 100644 --- a/src/http.rs +++ b/src/http.rs @@ -10,24 +10,33 @@ pub use web_sys::{ RequestCredentials, RequestMode, RequestRedirect, }; +#[allow(missing_docs, missing_debug_implementations, clippy::upper_case_acronyms)] /// Valid request methods. #[derive(Clone, Copy, Debug)] pub enum Method { GET, + HEAD, POST, - PATCH, - DELETE, PUT, + DELETE, + CONNECT, + OPTIONS, + TRACE, + PATCH, } impl fmt::Display for Method { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let s = match self { Method::GET => "GET", + Method::HEAD => "HEAD", Method::POST => "POST", - Method::PATCH => "PATCH", - Method::DELETE => "DELETE", Method::PUT => "PUT", + Method::DELETE => "DELETE", + Method::CONNECT => "CONNECT", + Method::OPTIONS => "OPTIONS", + Method::TRACE => "TRACE", + Method::PATCH => "PATCH", }; write!(f, "{}", s) } @@ -165,6 +174,14 @@ impl Request { } } +impl fmt::Debug for Request { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Request") + .field("url", &self.url) + .finish() + } +} + /// The [`Request`]'s response pub struct Response { response: web_sys::Response, @@ -239,3 +256,15 @@ impl Response { Ok(String::from(&string)) } } + +impl fmt::Debug for Response { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Response") + .field("url", &self.url()) + .field("redirected", &self.redirected()) + .field("status", &self.status()) + .field("headers", &self.headers()) + .field("body_used", &self.body_used()) + .finish() + } +} diff --git a/src/lib.rs b/src/lib.rs index 00f9a1f8..b951a4c6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,4 @@ -//! HTTP requests library for WASM Apps +//! HTTP requests library for WASM apps. It provides idiomatic Rust bindings for the `web_sys` `fetch` API. //! //! # Example //! @@ -11,6 +11,8 @@ //! assert_eq!(resp.status(), 200); //! ``` +#![deny(missing_docs, missing_debug_implementations, missing_copy_implementations)] + mod error; mod http; From 35828ecdfb84ee73027e11c9d1955d2f1106451b Mon Sep 17 00:00:00 2001 From: Hamza Date: Sun, 25 Apr 2021 18:34:34 +0500 Subject: [PATCH 10/36] websocket API Fixes: https://github.com/hamza1311/reqwasm/issues/1 --- .idea/reqwasm.iml | 3 ++ Cargo.toml | 10 ++++ src/http.rs | 10 ++-- src/lib.rs | 8 ++- src/websocket.rs | 132 ++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 158 insertions(+), 5 deletions(-) create mode 100644 src/websocket.rs diff --git a/.idea/reqwasm.iml b/.idea/reqwasm.iml index 0c0f13b4..fe377a5e 100644 --- a/.idea/reqwasm.iml +++ b/.idea/reqwasm.iml @@ -17,6 +17,9 @@ + + + diff --git a/Cargo.toml b/Cargo.toml index 262e40b2..50cd6001 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,9 @@ serde_json = "1.0" anyhow = "1.0" thiserror = "1.0" +gloo = "0.2" +futures = "0.3.14" + [dependencies.web-sys] version = "0.3.4" features = [ @@ -38,6 +41,13 @@ features = [ 'Blob', 'FormData', 'FileReader', + + 'WebSocket', + 'ErrorEvent', + 'FileReader', + 'MessageEvent', + 'ProgressEvent', + 'BinaryType', ] [dev-dependencies] diff --git a/src/http.rs b/src/http.rs index cd32b354..fb762c21 100644 --- a/src/http.rs +++ b/src/http.rs @@ -10,7 +10,11 @@ pub use web_sys::{ RequestCredentials, RequestMode, RequestRedirect, }; -#[allow(missing_docs, missing_debug_implementations, clippy::upper_case_acronyms)] +#[allow( + missing_docs, + missing_debug_implementations, + clippy::upper_case_acronyms +)] /// Valid request methods. #[derive(Clone, Copy, Debug)] pub enum Method { @@ -176,9 +180,7 @@ impl Request { impl fmt::Debug for Request { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("Request") - .field("url", &self.url) - .finish() + f.debug_struct("Request").field("url", &self.url).finish() } } diff --git a/src/lib.rs b/src/lib.rs index b951a4c6..3f2bac90 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,10 +11,16 @@ //! assert_eq!(resp.status(), 200); //! ``` -#![deny(missing_docs, missing_debug_implementations, missing_copy_implementations)] +#![deny( + missing_docs, + missing_debug_implementations, + missing_copy_implementations +)] mod error; mod http; +mod websocket; pub use error::*; pub use http::*; +pub use websocket::*; diff --git a/src/websocket.rs b/src/websocket.rs new file mode 100644 index 00000000..ea0faaf1 --- /dev/null +++ b/src/websocket.rs @@ -0,0 +1,132 @@ +use crate::{js_to_error, JsError}; +use futures::channel::mpsc; +use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender}; +use futures::StreamExt; +pub use gloo::file::Blob; +use wasm_bindgen::prelude::*; +use wasm_bindgen::JsCast; +use web_sys::{ErrorEvent, MessageEvent}; + +/// Wrapper around browser's WebSocket API. +#[allow(missing_debug_implementations)] +pub struct WebSocket { + /// Raw websocket instance + pub websocket: web_sys::WebSocket, + /// Channel's receiver component used to receive messages from the WebSocket + pub receiver: UnboundedReceiver>, + /// Channel's sender component used to send messages over the WebSocket + pub sender: UnboundedSender, +} + +/// Message received from WebSocket. +#[derive(Debug)] +pub enum Message { + /// String message + Text(String), + /// ArrayBuffer parsed into bytes + Bytes(Vec), +} + +impl WebSocket { + /// Establish a WebSocket connection. + pub fn open(url: &str) -> Result { + let ws = web_sys::WebSocket::new(url).map_err(js_to_error)?; + + let (internal_sender, receiver) = mpsc::unbounded(); + let (sender, mut internal_receiver) = mpsc::unbounded(); + + let (notify_sender, mut notify_receiver) = mpsc::unbounded(); + + let open_callback: Closure = { + Closure::wrap(Box::new(move || { + notify_sender.unbounded_send(()).unwrap(); + }) as Box) + }; + + ws.set_onopen(Some(open_callback.as_ref().unchecked_ref())); + open_callback.forget(); + + let message_callback: Closure = { + let sender = internal_sender.clone(); + Closure::wrap(Box::new(move |e: MessageEvent| { + sender + .unbounded_send(Ok(parse_message(e))) + .expect("message send") + }) as Box) + }; + + ws.set_onmessage(Some(message_callback.as_ref().unchecked_ref())); + message_callback.forget(); + + let error_callback: Closure = { + let sender = internal_sender.clone(); + Closure::wrap(Box::new(move |e: ErrorEvent| { + sender.unbounded_send(parse_error(e)).expect("message send") + }) as Box) + }; + + ws.set_onerror(Some(error_callback.as_ref().unchecked_ref())); + error_callback.forget(); + + let fut = { + let ws = ws.clone(); + let sender = internal_sender; + async move { + notify_receiver.next().await; + while let Some(message) = internal_receiver.next().await { + let result = match message { + Message::Bytes(bytes) => ws.send_with_u8_array(&bytes), + Message::Text(message) => ws.send_with_str(&message), + }; + + if let Err(e) = result { + match js_to_error(e) { + crate::Error::JsError(error) => sender + .unbounded_send(Err(WebSocketError::JsError(error))) + .unwrap(), + _ => unreachable!(), + } + } + } + } + }; + wasm_bindgen_futures::spawn_local(fut); + + Ok(Self { + websocket: ws, + receiver, + sender, + }) + } +} + +fn parse_error(event: ErrorEvent) -> Result { + Err(WebSocketError::ConnectionError { + message: event.message(), + }) +} + +/// Error from a WebSocket +#[derive(Debug, thiserror::Error)] +pub enum WebSocketError { + /// This is created from [`ErrorEvent`] received from `onerror` listener of the WebSocket. + #[error("{message}")] + ConnectionError { + /// The error message. + message: String, + }, + /// Error from JavaScript + #[error("{0}")] + JsError(JsError), +} + +fn parse_message(event: MessageEvent) -> Message { + if let Ok(array_buffer) = event.data().dyn_into::() { + let array = js_sys::Uint8Array::new(&array_buffer); + Message::Bytes(array.to_vec()) + } else if let Ok(txt) = event.data().dyn_into::() { + Message::Text(String::from(&txt)) + } else { + unreachable!("message event, received Unknown: {:?}", event.data()); + } +} From cf25cd318708d31b15c21590e57bdd9465fa0d2f Mon Sep 17 00:00:00 2001 From: Hamza Date: Mon, 26 Apr 2021 16:57:13 +0500 Subject: [PATCH 11/36] add tests for websocket --- .github/workflows/ci.yml | 5 +++++ src/websocket.rs | 2 +- tests/websocket.rs | 26 ++++++++++++++++++++++++++ 3 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 tests/websocket.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9035a094..634d2b41 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -70,6 +70,10 @@ jobs: image: kennethreitz/httpbin@sha256:599fe5e5073102dbb0ee3dbb65f049dab44fa9fc251f6835c9990f8fb196a72b ports: - 8080:80 + echo_server: + image: jmalloc/echo-server@sha256:c461e7e54d947a8777413aaf9c624b4ad1f1bac5d8272475da859ae82c1abd7d + ports: + - 8081:8080 steps: - uses: actions/checkout@v2 @@ -91,4 +95,5 @@ jobs: - name: Run tests env: HTTPBIN_URL: "http://localhost:8080" + ECHO_SERVER_URL: "ws://localhost:8081" run: wasm-pack test --chrome --firefox --headless diff --git a/src/websocket.rs b/src/websocket.rs index ea0faaf1..49946684 100644 --- a/src/websocket.rs +++ b/src/websocket.rs @@ -19,7 +19,7 @@ pub struct WebSocket { } /// Message received from WebSocket. -#[derive(Debug)] +#[derive(Debug, PartialEq)] pub enum Message { /// String message Text(String), diff --git a/tests/websocket.rs b/tests/websocket.rs new file mode 100644 index 00000000..7ac38a1a --- /dev/null +++ b/tests/websocket.rs @@ -0,0 +1,26 @@ +use futures::{SinkExt, StreamExt}; +use reqwasm::*; +use wasm_bindgen_test::*; + +wasm_bindgen_test_configure!(run_in_browser); + +const ECHO_SERVER_URL: &str = env!("ECHO_SERVER_URL"); + +#[wasm_bindgen_test] +async fn websocket_works() { + let ws = reqwasm::WebSocket::open(ECHO_SERVER_URL).unwrap(); + + let (mut sender, mut receiver) = (ws.sender, ws.receiver); + + sender + .send(Message::Text("test".to_string())) + .await + .unwrap(); + + // ignore the first message + let _ = receiver.next().await; + assert_eq!( + receiver.next().await.unwrap().unwrap(), + Message::Text("test".to_string()) + ) +} From f8495aeedebaf6f695b9abe8693ae3a90404106d Mon Sep 17 00:00:00 2001 From: Hamza Date: Mon, 26 Apr 2021 17:37:17 +0500 Subject: [PATCH 12/36] update documentation, prepare for release --- .github/workflows/ci.yml | 10 +++++++++- Cargo.toml | 9 +++++---- README.md | 28 +++++++++++++++++++++++++--- src/http.rs | 25 ++++++++++++++++++++----- src/lib.rs | 20 +++++--------------- src/websocket.rs | 34 ++++++++++++++++++++++++++++++++-- tests/http.rs | 2 +- tests/websocket.rs | 4 ++-- 8 files changed, 99 insertions(+), 33 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 634d2b41..92a7015d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -92,8 +92,16 @@ jobs: cargo-${{ runner.os }}-test- cargo-${{ runner.os }}- - - name: Run tests + - name: Run browser tests env: HTTPBIN_URL: "http://localhost:8080" ECHO_SERVER_URL: "ws://localhost:8081" run: wasm-pack test --chrome --firefox --headless + + - name: Run browser tests + env: + HTTPBIN_URL: "http://localhost:8080" + ECHO_SERVER_URL: "ws://localhost:8081" + uses: actions-rs/cargo@v1 + with: + command: test diff --git a/Cargo.toml b/Cargo.toml index 50cd6001..5c638954 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,14 +1,17 @@ [package] name = "reqwasm" -version = "0.1.0" +version = "0.2.0" authors = ["Hamza "] edition = "2018" license = "MIT OR Apache-2.0" repository = "https://github.com/hamza1311/reqwasm" description = "HTTP requests library for WASM Apps" readme = "README.md" -keywords = ["requests", "http", "wasm"] +keywords = ["requests", "http", "wasm", "websockets"] categories = ["wasm", "web-programming::http-client", "api-bindings"] +exclude = [ + ".idea", +] [dependencies] wasm-bindgen = { version = "0.2", features = ["serde-serialize"] } @@ -18,8 +21,6 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" anyhow = "1.0" thiserror = "1.0" - -gloo = "0.2" futures = "0.3.14" [dependencies.web-sys] diff --git a/README.md b/README.md index 3f9d3e46..c5a82bc2 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,14 @@ # Reqwasm -![GitHub branch checks state](https://img.shields.io/github/checks-status/hamza1311/reqwasm/master) [![crates.io](https://img.shields.io/crates/v/reqwasm.svg?style=flat)](https://crates.io/crates/reqwasm) [![docs.rs](https://img.shields.io/docsrs/reqwasm)](https://docs.rs/reqwasm/) ![licence](https://img.shields.io/crates/l/reqwasm) -HTTP requests library for WASM Apps. It provides idiomatic Rust bindings for the `web_sys` `fetch` API +HTTP requests library for WASM Apps. It provides idiomatic Rust bindings for the `web_sys` `fetch` and `WebSocket` API -## Example +## Examples + +### HTTP ```rust let resp = Request::get("/path") @@ -17,6 +18,27 @@ let resp = Request::get("/path") assert_eq!(resp.status(), 200); ``` +### WebSocket + +```rust +let ws = WebSocket::open("wss://echo.websocket.org").unwrap(); + +let (mut sender, mut receiver) = (ws.sender, ws.receiver); + +spawn_local(async move { + while let Some(m) = receiver.next().await { + match m { + Ok(Message::Text(m)) => console_log!("message", m), + Ok(Message::Bytes(m)) => console_log!("message", format!("{:?}", m)), + Err(e) => {} + } + } +}); + +spawn_local(async move { + sender.send(Message::Text("test".to_string())).await.unwrap(); +}) +``` ## Contributions Your PRs and Issues are welcome. Note that all the contribution submitted by you, shall be licensed as MIT or APACHE 2.0 at your choice, without any additional terms or conditions. diff --git a/src/http.rs b/src/http.rs index fb762c21..e504b4fe 100644 --- a/src/http.rs +++ b/src/http.rs @@ -1,3 +1,18 @@ +//! Wrapper around the `fetch` API. +//! +//! # Example +//! +//! ``` +//! # use reqwasm::http::Request; +//! # async fn no_run() { +//! let resp = Request::get("/path") +//! .send() +//! .await +//! .unwrap(); +//! assert_eq!(resp.status(), 200); +//! # } +//! ``` + use crate::{js_to_error, Error}; use serde::de::DeserializeOwned; use std::fmt; @@ -152,27 +167,27 @@ impl Request { } } - /// Creates a new [`GET`][RequestMethod::GET] `Request` with url. + /// Creates a new [`GET`][Method::GET] `Request` with url. pub fn get(url: &str) -> Self { Self::new(url).method(Method::GET) } - /// Creates a new [`POST`][RequestMethod::POST] `Request` with url. + /// Creates a new [`POST`][Method::POST] `Request` with url. pub fn post(url: &str) -> Self { Self::new(url).method(Method::POST) } - /// Creates a new [`PUT`][RequestMethod::PUT] `Request` with url. + /// Creates a new [`PUT`][Method::PUT] `Request` with url. pub fn put(url: &str) -> Self { Self::new(url).method(Method::PUT) } - /// Creates a new [`DELETE`][RequestMethod::DELETE] `Request` with url. + /// Creates a new [`DELETE`][Method::DELETE] `Request` with url. pub fn delete(url: &str) -> Self { Self::new(url).method(Method::DELETE) } - /// Creates a new [`PATCH`][RequestMethod::PATCH] `Request` with url. + /// Creates a new [`PATCH`][Method::PATCH] `Request` with url. pub fn patch(url: &str) -> Self { Self::new(url).method(Method::PATCH) } diff --git a/src/lib.rs b/src/lib.rs index 3f2bac90..3e024089 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,15 +1,7 @@ -//! HTTP requests library for WASM apps. It provides idiomatic Rust bindings for the `web_sys` `fetch` API. +//! HTTP requests library for WASM apps. It provides idiomatic Rust bindings for the `web_sys` +//! `fetch` and `WebSocket` API. //! -//! # Example -//! -//! ```no_run -//! # use reqwasm::Request; -//! let resp = Request::get("/path") -//! .send() -//! .await -//! .unwrap(); -//! assert_eq!(resp.status(), 200); -//! ``` +//! See module level documentation for [`http`] and [`websocket`] to learn more. #![deny( missing_docs, @@ -18,9 +10,7 @@ )] mod error; -mod http; -mod websocket; +pub mod http; +pub mod websocket; pub use error::*; -pub use http::*; -pub use websocket::*; diff --git a/src/websocket.rs b/src/websocket.rs index 49946684..d7441f79 100644 --- a/src/websocket.rs +++ b/src/websocket.rs @@ -1,8 +1,38 @@ +//! Wrapper around `WebSocket` API +//! +//! # Example +//! +//! ```rust +//! # use reqwasm::websocket::*; +//! # use wasm_bindgen_futures::spawn_local; +//! # use futures::{SinkExt, StreamExt}; +//! # macro_rules! console_log { +//! ($($expr:expr),*) => {{}}; +//! } +//! # fn no_run() { +//! let ws = WebSocket::open("wss://echo.websocket.org").unwrap(); +//! +//! let (mut sender, mut receiver) = (ws.sender, ws.receiver); +//! +//! spawn_local(async move { +//! while let Some(m) = receiver.next().await { +//! match m { +//! Ok(Message::Text(m)) => console_log!("message", m), +//! Ok(Message::Bytes(m)) => console_log!("message", format!("{:?}", m)), +//! Err(e) => {} +//! } +//! } +//! }); +//! +//! spawn_local(async move { +//! sender.send(Message::Text("test".to_string())).await.unwrap(); +//! }) +//! # } +//! ``` use crate::{js_to_error, JsError}; use futures::channel::mpsc; use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender}; use futures::StreamExt; -pub use gloo::file::Blob; use wasm_bindgen::prelude::*; use wasm_bindgen::JsCast; use web_sys::{ErrorEvent, MessageEvent}; @@ -18,7 +48,7 @@ pub struct WebSocket { pub sender: UnboundedSender, } -/// Message received from WebSocket. +/// Message sent to and received from WebSocket. #[derive(Debug, PartialEq)] pub enum Message { /// String message diff --git a/tests/http.rs b/tests/http.rs index 0e07b980..0878996c 100644 --- a/tests/http.rs +++ b/tests/http.rs @@ -1,4 +1,4 @@ -use reqwasm::*; +use reqwasm::http::*; use serde::{Deserialize, Serialize}; use wasm_bindgen_test::*; diff --git a/tests/websocket.rs b/tests/websocket.rs index 7ac38a1a..e60cebce 100644 --- a/tests/websocket.rs +++ b/tests/websocket.rs @@ -1,5 +1,5 @@ use futures::{SinkExt, StreamExt}; -use reqwasm::*; +use reqwasm::websocket::*; use wasm_bindgen_test::*; wasm_bindgen_test_configure!(run_in_browser); @@ -8,7 +8,7 @@ const ECHO_SERVER_URL: &str = env!("ECHO_SERVER_URL"); #[wasm_bindgen_test] async fn websocket_works() { - let ws = reqwasm::WebSocket::open(ECHO_SERVER_URL).unwrap(); + let ws = WebSocket::open(ECHO_SERVER_URL).unwrap(); let (mut sender, mut receiver) = (ws.sender, ws.receiver); From 75cab8953d9dda1defb5980c1649a2a0f39d8f4e Mon Sep 17 00:00:00 2001 From: Hamza Date: Mon, 26 Apr 2021 17:52:22 +0500 Subject: [PATCH 13/36] fix mistake in documentation --- Cargo.toml | 2 +- src/websocket.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 5c638954..1c30d592 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "reqwasm" -version = "0.2.0" +version = "0.2.1" authors = ["Hamza "] edition = "2018" license = "MIT OR Apache-2.0" diff --git a/src/websocket.rs b/src/websocket.rs index d7441f79..e09d7491 100644 --- a/src/websocket.rs +++ b/src/websocket.rs @@ -7,8 +7,8 @@ //! # use wasm_bindgen_futures::spawn_local; //! # use futures::{SinkExt, StreamExt}; //! # macro_rules! console_log { -//! ($($expr:expr),*) => {{}}; -//! } +//! # ($($expr:expr),*) => {{}}; +//! # } //! # fn no_run() { //! let ws = WebSocket::open("wss://echo.websocket.org").unwrap(); //! From 7866f5e05d498bcd9c8066ed27f400869d1c4929 Mon Sep 17 00:00:00 2001 From: Muhammad Hamza Date: Wed, 1 Sep 2021 16:22:40 +0500 Subject: [PATCH 14/36] Rewrite WebSockets code (#4) * redo websockets * docs + tests * remove gloo-console * fix CI * Add getters for the underlying WebSocket fields * better API * better API part 2 electric boogaloo --- .idea/reqwasm.iml | 14 ++ Cargo.toml | 4 + src/error.rs | 10 +- src/websocket.rs | 162 --------------------- src/websocket/events.rs | 19 +++ src/websocket/futures.rs | 305 +++++++++++++++++++++++++++++++++++++++ src/websocket/mod.rs | 63 ++++++++ tests/websocket.rs | 26 ---- 8 files changed, 412 insertions(+), 191 deletions(-) delete mode 100644 src/websocket.rs create mode 100644 src/websocket/events.rs create mode 100644 src/websocket/futures.rs create mode 100644 src/websocket/mod.rs delete mode 100644 tests/websocket.rs diff --git a/.idea/reqwasm.iml b/.idea/reqwasm.iml index fe377a5e..b4a205a4 100644 --- a/.idea/reqwasm.iml +++ b/.idea/reqwasm.iml @@ -28,6 +28,20 @@ + + + + + + + + + + + + + + diff --git a/Cargo.toml b/Cargo.toml index 1c30d592..f3680311 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,9 @@ anyhow = "1.0" thiserror = "1.0" futures = "0.3.14" +async-broadcast = "0.3" +pin-project = "1" + [dependencies.web-sys] version = "0.3.4" features = [ @@ -42,6 +45,7 @@ features = [ 'Blob', 'FormData', 'FileReader', + 'CloseEvent', 'WebSocket', 'ErrorEvent', diff --git a/src/error.rs b/src/error.rs index 939e5d55..ddb83da3 100644 --- a/src/error.rs +++ b/src/error.rs @@ -2,7 +2,7 @@ use std::fmt; use thiserror::Error as ThisError; use wasm_bindgen::{JsCast, JsValue}; -#[derive(Debug)] +#[derive(Debug, Clone)] #[doc(hidden)] pub struct JsError { pub name: String, @@ -35,12 +35,16 @@ pub enum Error { } pub(crate) fn js_to_error(js_value: JsValue) -> Error { + Error::JsError(js_to_js_error(js_value)) +} + +pub(crate) fn js_to_js_error(js_value: JsValue) -> JsError { match js_value.dyn_into::() { - Ok(error) => Error::JsError(JsError { + Ok(error) => JsError { name: String::from(error.name()), message: String::from(error.message()), js_to_string: String::from(error.to_string()), - }), + }, Err(_) => unreachable!("JsValue passed is not an Error type -- this is a bug"), } } diff --git a/src/websocket.rs b/src/websocket.rs deleted file mode 100644 index e09d7491..00000000 --- a/src/websocket.rs +++ /dev/null @@ -1,162 +0,0 @@ -//! Wrapper around `WebSocket` API -//! -//! # Example -//! -//! ```rust -//! # use reqwasm::websocket::*; -//! # use wasm_bindgen_futures::spawn_local; -//! # use futures::{SinkExt, StreamExt}; -//! # macro_rules! console_log { -//! # ($($expr:expr),*) => {{}}; -//! # } -//! # fn no_run() { -//! let ws = WebSocket::open("wss://echo.websocket.org").unwrap(); -//! -//! let (mut sender, mut receiver) = (ws.sender, ws.receiver); -//! -//! spawn_local(async move { -//! while let Some(m) = receiver.next().await { -//! match m { -//! Ok(Message::Text(m)) => console_log!("message", m), -//! Ok(Message::Bytes(m)) => console_log!("message", format!("{:?}", m)), -//! Err(e) => {} -//! } -//! } -//! }); -//! -//! spawn_local(async move { -//! sender.send(Message::Text("test".to_string())).await.unwrap(); -//! }) -//! # } -//! ``` -use crate::{js_to_error, JsError}; -use futures::channel::mpsc; -use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender}; -use futures::StreamExt; -use wasm_bindgen::prelude::*; -use wasm_bindgen::JsCast; -use web_sys::{ErrorEvent, MessageEvent}; - -/// Wrapper around browser's WebSocket API. -#[allow(missing_debug_implementations)] -pub struct WebSocket { - /// Raw websocket instance - pub websocket: web_sys::WebSocket, - /// Channel's receiver component used to receive messages from the WebSocket - pub receiver: UnboundedReceiver>, - /// Channel's sender component used to send messages over the WebSocket - pub sender: UnboundedSender, -} - -/// Message sent to and received from WebSocket. -#[derive(Debug, PartialEq)] -pub enum Message { - /// String message - Text(String), - /// ArrayBuffer parsed into bytes - Bytes(Vec), -} - -impl WebSocket { - /// Establish a WebSocket connection. - pub fn open(url: &str) -> Result { - let ws = web_sys::WebSocket::new(url).map_err(js_to_error)?; - - let (internal_sender, receiver) = mpsc::unbounded(); - let (sender, mut internal_receiver) = mpsc::unbounded(); - - let (notify_sender, mut notify_receiver) = mpsc::unbounded(); - - let open_callback: Closure = { - Closure::wrap(Box::new(move || { - notify_sender.unbounded_send(()).unwrap(); - }) as Box) - }; - - ws.set_onopen(Some(open_callback.as_ref().unchecked_ref())); - open_callback.forget(); - - let message_callback: Closure = { - let sender = internal_sender.clone(); - Closure::wrap(Box::new(move |e: MessageEvent| { - sender - .unbounded_send(Ok(parse_message(e))) - .expect("message send") - }) as Box) - }; - - ws.set_onmessage(Some(message_callback.as_ref().unchecked_ref())); - message_callback.forget(); - - let error_callback: Closure = { - let sender = internal_sender.clone(); - Closure::wrap(Box::new(move |e: ErrorEvent| { - sender.unbounded_send(parse_error(e)).expect("message send") - }) as Box) - }; - - ws.set_onerror(Some(error_callback.as_ref().unchecked_ref())); - error_callback.forget(); - - let fut = { - let ws = ws.clone(); - let sender = internal_sender; - async move { - notify_receiver.next().await; - while let Some(message) = internal_receiver.next().await { - let result = match message { - Message::Bytes(bytes) => ws.send_with_u8_array(&bytes), - Message::Text(message) => ws.send_with_str(&message), - }; - - if let Err(e) = result { - match js_to_error(e) { - crate::Error::JsError(error) => sender - .unbounded_send(Err(WebSocketError::JsError(error))) - .unwrap(), - _ => unreachable!(), - } - } - } - } - }; - wasm_bindgen_futures::spawn_local(fut); - - Ok(Self { - websocket: ws, - receiver, - sender, - }) - } -} - -fn parse_error(event: ErrorEvent) -> Result { - Err(WebSocketError::ConnectionError { - message: event.message(), - }) -} - -/// Error from a WebSocket -#[derive(Debug, thiserror::Error)] -pub enum WebSocketError { - /// This is created from [`ErrorEvent`] received from `onerror` listener of the WebSocket. - #[error("{message}")] - ConnectionError { - /// The error message. - message: String, - }, - /// Error from JavaScript - #[error("{0}")] - JsError(JsError), -} - -fn parse_message(event: MessageEvent) -> Message { - if let Ok(array_buffer) = event.data().dyn_into::() { - let array = js_sys::Uint8Array::new(&array_buffer); - Message::Bytes(array.to_vec()) - } else if let Ok(txt) = event.data().dyn_into::() { - Message::Text(String::from(&txt)) - } else { - unreachable!("message event, received Unknown: {:?}", event.data()); - } -} diff --git a/src/websocket/events.rs b/src/websocket/events.rs new file mode 100644 index 00000000..44b333c9 --- /dev/null +++ b/src/websocket/events.rs @@ -0,0 +1,19 @@ +//! WebSocket Events + +/// This is created from [`ErrorEvent`][web_sys::ErrorEvent] received from `onerror` listener of the WebSocket. +#[derive(Clone, Debug)] +pub struct ErrorEvent { + /// The error message. + pub message: String, +} + +/// Data emiited by `onclose` event +#[derive(Clone, Debug)] +pub struct CloseEvent { + /// Close code + pub code: u16, + /// Close reason + pub reason: String, + /// If the websockt was closed cleanly + pub was_clean: bool, +} diff --git a/src/websocket/futures.rs b/src/websocket/futures.rs new file mode 100644 index 00000000..4c7d7b3d --- /dev/null +++ b/src/websocket/futures.rs @@ -0,0 +1,305 @@ +//! The wrapper around `WebSocket` API using the Futures API to be used in async rust +//! +//! # Example +//! +//! ```rust +//! use reqwasm::websocket::{Message, futures::WebSocket}; +//! use wasm_bindgen_futures::spawn_local; +//! use futures::{SinkExt, StreamExt}; +//! +//! # macro_rules! console_log { +//! # ($($expr:expr),*) => {{}}; +//! # } +//! # fn no_run() { +//! let mut ws = WebSocket::open("wss://echo.websocket.org").unwrap(); +//! +//! spawn_local({ +//! let mut ws = ws.clone(); +//! async move { +//! ws.send(Message::Text(String::from("test"))).await.unwrap(); +//! ws.send(Message::Text(String::from("test 2"))).await.unwrap(); +//! } +//! }); +//! +//! spawn_local(async move { +//! while let Some(msg) = ws.next().await { +//! console_log!(format!("1. {:?}", msg)) +//! } +//! console_log!("WebSocket Closed") +//! }) +//! # } +//! ``` +use crate::websocket::{ + events::{CloseEvent, ErrorEvent}, + Message, State, WebSocketError, +}; +use crate::{js_to_js_error, JsError}; +use async_broadcast::Receiver; +use futures::ready; +use futures::{Sink, Stream}; +use pin_project::{pin_project, pinned_drop}; +use std::cell::RefCell; +use std::pin::Pin; +use std::rc::Rc; +use std::task::{Context, Poll, Waker}; +use wasm_bindgen::prelude::*; +use wasm_bindgen::JsCast; +use web_sys::MessageEvent; + +/// Wrapper around browser's WebSocket API. +#[allow(missing_debug_implementations)] +#[pin_project(PinnedDrop)] +#[derive(Clone)] +pub struct WebSocket { + ws: web_sys::WebSocket, + sink_wakers: Rc>>, + #[pin] + message_receiver: Receiver, + #[allow(clippy::type_complexity)] + closures: Rc<( + Closure, + Closure, + Closure, + Closure, + )>, +} + +impl WebSocket { + /// Establish a WebSocket connection. + /// + /// This function may error in the following cases: + /// - The port to which the connection is being attempted is being blocked. + /// - The URL is invalid. + /// + /// The error returned is [`JsError`]. See the + /// [MDN Documentation](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/WebSocket#exceptions_thrown) + /// to learn more. + pub fn open(url: &str) -> Result { + let wakers: Rc>> = Rc::new(RefCell::new(vec![])); + let ws = web_sys::WebSocket::new(url).map_err(js_to_js_error)?; + + let (sender, receiver) = async_broadcast::broadcast(10); + + let open_callback: Closure = { + let wakers = Rc::clone(&wakers); + Closure::wrap(Box::new(move || { + for waker in wakers.borrow_mut().drain(..) { + waker.wake(); + } + }) as Box) + }; + + ws.set_onopen(Some(open_callback.as_ref().unchecked_ref())); + // open_callback.forget(); + // + let message_callback: Closure = { + let sender = sender.clone(); + Closure::wrap(Box::new(move |e: MessageEvent| { + let sender = sender.clone(); + wasm_bindgen_futures::spawn_local(async move { + let _ = sender + .broadcast(StreamMessage::Message(parse_message(e))) + .await; + }) + }) as Box) + }; + + ws.set_onmessage(Some(message_callback.as_ref().unchecked_ref())); + // message_callback.forget(); + + let error_callback: Closure = { + let sender = sender.clone(); + Closure::wrap(Box::new(move |e: web_sys::ErrorEvent| { + let sender = sender.clone(); + wasm_bindgen_futures::spawn_local(async move { + let _ = sender + .broadcast(StreamMessage::ErrorEvent(ErrorEvent { + message: e.message(), + })) + .await; + }) + }) as Box) + }; + + ws.set_onerror(Some(error_callback.as_ref().unchecked_ref())); + // error_callback.forget(); + + let close_callback: Closure = { + Closure::wrap(Box::new(move |e: web_sys::CloseEvent| { + let sender = sender.clone(); + wasm_bindgen_futures::spawn_local(async move { + let close_event = CloseEvent { + code: e.code(), + reason: e.reason(), + was_clean: e.was_clean(), + }; + + let _ = sender + .broadcast(StreamMessage::CloseEvent(close_event)) + .await; + let _ = sender.broadcast(StreamMessage::ConnectionClose).await; + }) + }) as Box) + }; + + ws.set_onerror(Some(close_callback.as_ref().unchecked_ref())); + // close_callback.forget(); + + Ok(Self { + ws, + sink_wakers: wakers, + message_receiver: receiver, + closures: Rc::new(( + open_callback, + message_callback, + error_callback, + close_callback, + )), + }) + } + + /// Closes the websocket. + /// + /// See the [MDN Documentation](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/close#parameters) + /// to learn about parameters passed to this function and when it can return an `Err(_)` + /// + /// **Note**: If *only one* of the instances of websocket is closed, the entire connection closes. + /// This is unlikely to happen in real-world as [`wasm_bindgen_futures::spawn_local`] requires `'static`. + pub fn close(self, code: Option, reason: Option<&str>) -> Result<(), JsError> { + let result = match (code, reason) { + (None, None) => self.ws.close(), + (Some(code), None) => self.ws.close_with_code(code), + (Some(code), Some(reason)) => self.ws.close_with_code_and_reason(code, reason), + // default code is 1005 so we use it, + // see: https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/close#parameters + (None, Some(reason)) => self.ws.close_with_code_and_reason(1005, reason), + }; + result.map_err(js_to_js_error) + } + + /// The current state of the websocket. + pub fn state(&self) -> State { + let ready_state = self.ws.ready_state(); + match ready_state { + 0 => State::Connecting, + 1 => State::Open, + 2 => State::Closing, + 3 => State::Closed, + _ => unreachable!(), + } + } + + /// The extensions in use. + pub fn extensions(&self) -> String { + self.ws.extensions() + } + + /// The sub-protocol in use. + pub fn protocol(&self) -> String { + self.ws.protocol() + } +} + +#[derive(Clone)] +enum StreamMessage { + ErrorEvent(ErrorEvent), + CloseEvent(CloseEvent), + Message(Message), + ConnectionClose, +} + +fn parse_message(event: MessageEvent) -> Message { + if let Ok(array_buffer) = event.data().dyn_into::() { + let array = js_sys::Uint8Array::new(&array_buffer); + Message::Bytes(array.to_vec()) + } else if let Ok(txt) = event.data().dyn_into::() { + Message::Text(String::from(&txt)) + } else { + unreachable!("message event, received Unknown: {:?}", event.data()); + } +} + +impl Sink for WebSocket { + type Error = WebSocketError; + + fn poll_ready(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let ready_state = self.ws.ready_state(); + if ready_state == 0 { + self.sink_wakers.borrow_mut().push(cx.waker().clone()); + Poll::Pending + } else { + Poll::Ready(Ok(())) + } + } + + fn start_send(self: Pin<&mut Self>, item: Message) -> Result<(), Self::Error> { + let result = match item { + Message::Bytes(bytes) => self.ws.send_with_u8_array(&bytes), + Message::Text(message) => self.ws.send_with_str(&message), + }; + match result { + Ok(_) => Ok(()), + Err(e) => Err(WebSocketError::MessageSendError(js_to_js_error(e))), + } + } + + fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } + + fn poll_close(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } +} + +impl Stream for WebSocket { + type Item = Result; + + fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let msg = ready!(self.project().message_receiver.poll_next(cx)); + match msg { + Some(StreamMessage::Message(msg)) => Poll::Ready(Some(Ok(msg))), + Some(StreamMessage::ErrorEvent(err)) => { + Poll::Ready(Some(Err(WebSocketError::ConnectionError(err)))) + } + Some(StreamMessage::CloseEvent(e)) => { + Poll::Ready(Some(Err(WebSocketError::ConnectionClose(e)))) + } + Some(StreamMessage::ConnectionClose) => Poll::Ready(None), + None => Poll::Ready(None), + } + } +} + +#[pinned_drop] +impl PinnedDrop for WebSocket { + fn drop(self: Pin<&mut Self>) { + self.ws.close().unwrap(); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use futures::{SinkExt, StreamExt}; + use wasm_bindgen_test::*; + + wasm_bindgen_test_configure!(run_in_browser); + + const ECHO_SERVER_URL: &str = env!("ECHO_SERVER_URL"); + + #[wasm_bindgen_test] + async fn websocket_works() { + let mut ws = WebSocket::open(ECHO_SERVER_URL).unwrap(); + + ws.send(Message::Text("test".to_string())).await.unwrap(); + + // ignore first message + // the echo-server used sends it's info in the first message + let _ = ws.next().await; + assert_eq!( + ws.next().await.unwrap().unwrap(), + Message::Text("test".to_string()) + ) + } +} diff --git a/src/websocket/mod.rs b/src/websocket/mod.rs new file mode 100644 index 00000000..0242eaa5 --- /dev/null +++ b/src/websocket/mod.rs @@ -0,0 +1,63 @@ +//! Wrapper around `WebSocket` API +//! +//! This API is provided in the following flavors: +//! - [Futures API][futures] + +pub mod events; +pub mod futures; + +use crate::JsError; +use events::{CloseEvent, ErrorEvent}; +use std::fmt; + +/// Message sent to and received from WebSocket. +#[derive(Debug, PartialEq, Clone)] +pub enum Message { + /// String message + Text(String), + /// ArrayBuffer parsed into bytes + Bytes(Vec), +} + +/// The state of the websocket. +/// +/// See [`WebSocket.readyState` on MDN](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/readyState) +/// to learn more. +#[derive(Copy, Clone, Debug)] +pub enum State { + /// The connection has not yet been established. + Connecting, + /// The WebSocket connection is established and communication is possible. + Open, + /// The connection is going through the closing handshake, or the close() method has been + /// invoked. + Closing, + /// The connection has been closed or could not be opened. + Closed, +} + +/// Error returned by WebSocket +#[derive(Debug)] +#[non_exhaustive] +pub enum WebSocketError { + /// The `error` event + ConnectionError(ErrorEvent), + /// The `close` event + ConnectionClose(CloseEvent), + /// Message failed to send. + MessageSendError(JsError), +} + +impl fmt::Display for WebSocketError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + WebSocketError::ConnectionError(e) => write!(f, "{}", e.message), + WebSocketError::ConnectionClose(e) => write!( + f, + "WebSocket Closed: code: {}, reason: {}", + e.code, e.reason + ), + WebSocketError::MessageSendError(e) => write!(f, "{}", e), + } + } +} diff --git a/tests/websocket.rs b/tests/websocket.rs deleted file mode 100644 index e60cebce..00000000 --- a/tests/websocket.rs +++ /dev/null @@ -1,26 +0,0 @@ -use futures::{SinkExt, StreamExt}; -use reqwasm::websocket::*; -use wasm_bindgen_test::*; - -wasm_bindgen_test_configure!(run_in_browser); - -const ECHO_SERVER_URL: &str = env!("ECHO_SERVER_URL"); - -#[wasm_bindgen_test] -async fn websocket_works() { - let ws = WebSocket::open(ECHO_SERVER_URL).unwrap(); - - let (mut sender, mut receiver) = (ws.sender, ws.receiver); - - sender - .send(Message::Text("test".to_string())) - .await - .unwrap(); - - // ignore the first message - let _ = receiver.next().await; - assert_eq!( - receiver.next().await.unwrap().unwrap(), - Message::Text("test".to_string()) - ) -} From fe1036f57829be5e1dcdfbc2f4d2138b4749f601 Mon Sep 17 00:00:00 2001 From: Michael Hueschen Date: Tue, 26 Oct 2021 02:00:29 -0600 Subject: [PATCH 15/36] deserialize Blob to Vec (#9) --- src/websocket/futures.rs | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/src/websocket/futures.rs b/src/websocket/futures.rs index 4c7d7b3d..52cb7b21 100644 --- a/src/websocket/futures.rs +++ b/src/websocket/futures.rs @@ -44,7 +44,8 @@ use std::rc::Rc; use std::task::{Context, Poll, Waker}; use wasm_bindgen::prelude::*; use wasm_bindgen::JsCast; -use web_sys::MessageEvent; +use wasm_bindgen_futures::JsFuture; +use web_sys::{Blob, MessageEvent}; /// Wrapper around browser's WebSocket API. #[allow(missing_debug_implementations)] @@ -97,9 +98,8 @@ impl WebSocket { Closure::wrap(Box::new(move |e: MessageEvent| { let sender = sender.clone(); wasm_bindgen_futures::spawn_local(async move { - let _ = sender - .broadcast(StreamMessage::Message(parse_message(e))) - .await; + let msg = parse_message(e).await; + let _ = sender.broadcast(StreamMessage::Message(msg)).await; }) }) as Box) }; @@ -208,17 +208,31 @@ enum StreamMessage { ConnectionClose, } -fn parse_message(event: MessageEvent) -> Message { +async fn parse_message(event: MessageEvent) -> Message { if let Ok(array_buffer) = event.data().dyn_into::() { let array = js_sys::Uint8Array::new(&array_buffer); Message::Bytes(array.to_vec()) } else if let Ok(txt) = event.data().dyn_into::() { Message::Text(String::from(&txt)) + } else if let Ok(blob) = event.data().dyn_into::() { + let vec = blob_into_bytes(blob).await; + Message::Bytes(vec) } else { unreachable!("message event, received Unknown: {:?}", event.data()); } } +// copied verbatim from https://github.com/rustwasm/wasm-bindgen/issues/2551 +async fn blob_into_bytes(blob: Blob) -> Vec { + let array_buffer_promise: JsFuture = blob.array_buffer().into(); + + let array_buffer: JsValue = array_buffer_promise + .await + .expect("Could not get ArrayBuffer from file"); + + js_sys::Uint8Array::new(&array_buffer).to_vec() +} + impl Sink for WebSocket { type Error = WebSocketError; From c0f2c6d8369d3915beae0e2de2e1d165617cf2b8 Mon Sep 17 00:00:00 2001 From: Muhammad Hamza Date: Tue, 26 Oct 2021 13:30:54 +0500 Subject: [PATCH 16/36] Update to Rust 2021 and use JsError from gloo-utils (#10) * Update to Rust 2021 and use JsError from gloo-utils * use new toolchain --- .github/workflows/ci.yml | 14 ++++++++++++++ Cargo.toml | 4 ++-- src/error.rs | 29 ++++------------------------- src/http.rs | 2 +- src/websocket/futures.rs | 3 ++- src/websocket/mod.rs | 2 +- 6 files changed, 24 insertions(+), 30 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 92a7015d..0984ca87 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,6 +16,13 @@ jobs: steps: - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + toolchain: stable + profile: minimal + components: clippy + target: wasm32-unknown-unknown + - uses: actions/cache@v2 with: path: | @@ -77,6 +84,13 @@ jobs: steps: - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + toolchain: stable + profile: minimal + components: clippy + target: wasm32-unknown-unknown + - name: Install wasm-pack run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh diff --git a/Cargo.toml b/Cargo.toml index f3680311..110d6e74 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ name = "reqwasm" version = "0.2.1" authors = ["Hamza "] -edition = "2018" +edition = "2021" license = "MIT OR Apache-2.0" repository = "https://github.com/hamza1311/reqwasm" description = "HTTP requests library for WASM Apps" @@ -19,9 +19,9 @@ js-sys = "0.3" wasm-bindgen-futures = "0.4" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -anyhow = "1.0" thiserror = "1.0" futures = "0.3.14" +gloo-utils = "0.1.0" async-broadcast = "0.3" pin-project = "1" diff --git a/src/error.rs b/src/error.rs index ddb83da3..c0accb9e 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,20 +1,6 @@ -use std::fmt; +use gloo_utils::errors::JsError; use thiserror::Error as ThisError; -use wasm_bindgen::{JsCast, JsValue}; - -#[derive(Debug, Clone)] -#[doc(hidden)] -pub struct JsError { - pub name: String, - pub message: String, - js_to_string: String, -} - -impl fmt::Display for JsError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.js_to_string) - } -} +use wasm_bindgen::JsValue; /// All the errors returned by this crate. #[derive(Debug, ThisError)] @@ -29,9 +15,6 @@ pub enum Error { #[from] serde_json::Error, ), - /// Unknown error. - #[error("{0}")] - Other(anyhow::Error), } pub(crate) fn js_to_error(js_value: JsValue) -> Error { @@ -39,12 +22,8 @@ pub(crate) fn js_to_error(js_value: JsValue) -> Error { } pub(crate) fn js_to_js_error(js_value: JsValue) -> JsError { - match js_value.dyn_into::() { - Ok(error) => JsError { - name: String::from(error.name()), - message: String::from(error.message()), - js_to_string: String::from(error.to_string()), - }, + match JsError::try_from(js_value) { + Ok(error) => error, Err(_) => unreachable!("JsValue passed is not an Error type -- this is a bug"), } } diff --git a/src/http.rs b/src/http.rs index e504b4fe..a254a984 100644 --- a/src/http.rs +++ b/src/http.rs @@ -163,7 +163,7 @@ impl Request { Ok(response) => Ok(Response { response: response.unchecked_into(), }), - Err(_) => Err(Error::Other(anyhow::anyhow!("can't convert to Response"))), + Err(e) => panic!("fetch returned {:?}, not `Response` - this is a bug", e), } } diff --git a/src/websocket/futures.rs b/src/websocket/futures.rs index 52cb7b21..66cf9d0f 100644 --- a/src/websocket/futures.rs +++ b/src/websocket/futures.rs @@ -29,14 +29,15 @@ //! }) //! # } //! ``` +use crate::js_to_js_error; use crate::websocket::{ events::{CloseEvent, ErrorEvent}, Message, State, WebSocketError, }; -use crate::{js_to_js_error, JsError}; use async_broadcast::Receiver; use futures::ready; use futures::{Sink, Stream}; +use gloo_utils::errors::JsError; use pin_project::{pin_project, pinned_drop}; use std::cell::RefCell; use std::pin::Pin; diff --git a/src/websocket/mod.rs b/src/websocket/mod.rs index 0242eaa5..5e11a3f7 100644 --- a/src/websocket/mod.rs +++ b/src/websocket/mod.rs @@ -6,8 +6,8 @@ pub mod events; pub mod futures; -use crate::JsError; use events::{CloseEvent, ErrorEvent}; +use gloo_utils::errors::JsError; use std::fmt; /// Message sent to and received from WebSocket. From fb356775a579cd2cbad1111bcb77fa68c0b83366 Mon Sep 17 00:00:00 2001 From: Hamza Date: Fri, 29 Oct 2021 15:22:52 +0500 Subject: [PATCH 17/36] Add response.binary method to obtain response as Vec Fixes: https://github.com/hamza1311/reqwasm/issues/7 --- src/http.rs | 17 +++++++++++++++++ tests/http.rs | 19 +++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/src/http.rs b/src/http.rs index a254a984..fa01d896 100644 --- a/src/http.rs +++ b/src/http.rs @@ -14,6 +14,7 @@ //! ``` use crate::{js_to_error, Error}; +use js_sys::{ArrayBuffer, Uint8Array}; use serde::de::DeserializeOwned; use std::fmt; use wasm_bindgen::prelude::*; @@ -272,6 +273,22 @@ impl Response { let string = js_sys::JsString::from(val); Ok(String::from(&string)) } + + /// Gets the binary response + /// + /// This works by obtaining the response as an `ArrayBuffer`, creating a `Uint8Array` from it + /// and then converting it to `Vec` + pub async fn binary(&self) -> Result, Error> { + let promise = self.response.array_buffer().map_err(js_to_error)?; + let array_buffer: ArrayBuffer = JsFuture::from(promise) + .await + .map_err(js_to_error)? + .unchecked_into(); + let typed_buff: Uint8Array = Uint8Array::new(&array_buffer); + let mut body = vec![0; typed_buff.length() as usize]; + typed_buff.copy_to(&mut body); + Ok(body) + } } impl fmt::Debug for Response { diff --git a/tests/http.rs b/tests/http.rs index 0878996c..d650a997 100644 --- a/tests/http.rs +++ b/tests/http.rs @@ -1,5 +1,7 @@ +use js_sys::Uint8Array; use reqwasm::http::*; use serde::{Deserialize, Serialize}; +use wasm_bindgen::JsValue; use wasm_bindgen_test::*; wasm_bindgen_test_configure!(run_in_browser); @@ -82,3 +84,20 @@ async fn post_json() { assert_eq!(resp.status(), 200); assert_eq!(json.json.data, "data"); } + +#[wasm_bindgen_test] +async fn fetch_binary() { + #[derive(Deserialize, Debug)] + struct HttpBin { + data: String, + } + + let resp = Request::post(&format!("{}/post", HTTPBIN_URL)) + .send() + .await + .unwrap(); + let json = resp.binary().await.unwrap(); + assert_eq!(resp.status(), 200); + let json: HttpBin = serde_json::from_slice(&json).unwrap(); + assert_eq!(json.data, ""); // default is empty string +} From 445e9a5bf555a0e37d0ff7fb71e69d34cb8f2493 Mon Sep 17 00:00:00 2001 From: Hamza Date: Fri, 12 Nov 2021 14:06:23 +0500 Subject: [PATCH 18/36] Remove `Clone` impl from WebSocket. When the WebSocket is used with frameworks, passed down as props, it might be `drop`ed automatically, which closes the WebSocket connection. Initially `Clone` was added so sender and receiver can be in different `spawn_local`s but it turns out that `StreamExt::split` solves that problem very well. See #13 for more information about the issue --- src/websocket/futures.rs | 58 +++++++++++++++++++++------------------- 1 file changed, 31 insertions(+), 27 deletions(-) diff --git a/src/websocket/futures.rs b/src/websocket/futures.rs index 66cf9d0f..bf2737c4 100644 --- a/src/websocket/futures.rs +++ b/src/websocket/futures.rs @@ -51,10 +51,9 @@ use web_sys::{Blob, MessageEvent}; /// Wrapper around browser's WebSocket API. #[allow(missing_debug_implementations)] #[pin_project(PinnedDrop)] -#[derive(Clone)] pub struct WebSocket { ws: web_sys::WebSocket, - sink_wakers: Rc>>, + sink_waker: Rc>>, #[pin] message_receiver: Receiver, #[allow(clippy::type_complexity)] @@ -77,23 +76,22 @@ impl WebSocket { /// [MDN Documentation](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/WebSocket#exceptions_thrown) /// to learn more. pub fn open(url: &str) -> Result { - let wakers: Rc>> = Rc::new(RefCell::new(vec![])); + let waker: Rc>> = Rc::new(RefCell::new(None)); let ws = web_sys::WebSocket::new(url).map_err(js_to_js_error)?; let (sender, receiver) = async_broadcast::broadcast(10); let open_callback: Closure = { - let wakers = Rc::clone(&wakers); + let waker = Rc::clone(&waker); Closure::wrap(Box::new(move || { - for waker in wakers.borrow_mut().drain(..) { + if let Some(waker) = waker.borrow_mut().take() { waker.wake(); } }) as Box) }; ws.set_onopen(Some(open_callback.as_ref().unchecked_ref())); - // open_callback.forget(); - // + let message_callback: Closure = { let sender = sender.clone(); Closure::wrap(Box::new(move |e: MessageEvent| { @@ -106,7 +104,6 @@ impl WebSocket { }; ws.set_onmessage(Some(message_callback.as_ref().unchecked_ref())); - // message_callback.forget(); let error_callback: Closure = { let sender = sender.clone(); @@ -123,7 +120,6 @@ impl WebSocket { }; ws.set_onerror(Some(error_callback.as_ref().unchecked_ref())); - // error_callback.forget(); let close_callback: Closure = { Closure::wrap(Box::new(move |e: web_sys::CloseEvent| { @@ -144,11 +140,10 @@ impl WebSocket { }; ws.set_onerror(Some(close_callback.as_ref().unchecked_ref())); - // close_callback.forget(); Ok(Self { ws, - sink_wakers: wakers, + sink_waker: waker, message_receiver: receiver, closures: Rc::new(( open_callback, @@ -163,9 +158,6 @@ impl WebSocket { /// /// See the [MDN Documentation](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/close#parameters) /// to learn about parameters passed to this function and when it can return an `Err(_)` - /// - /// **Note**: If *only one* of the instances of websocket is closed, the entire connection closes. - /// This is unlikely to happen in real-world as [`wasm_bindgen_futures::spawn_local`] requires `'static`. pub fn close(self, code: Option, reason: Option<&str>) -> Result<(), JsError> { let result = match (code, reason) { (None, None) => self.ws.close(), @@ -240,7 +232,7 @@ impl Sink for WebSocket { fn poll_ready(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { let ready_state = self.ws.ready_state(); if ready_state == 0 { - self.sink_wakers.borrow_mut().push(cx.waker().clone()); + *self.sink_waker.borrow_mut() = Some(cx.waker().clone()); Poll::Pending } else { Poll::Ready(Ok(())) @@ -298,23 +290,35 @@ mod tests { use super::*; use futures::{SinkExt, StreamExt}; use wasm_bindgen_test::*; + use wasm_bindgen_futures::spawn_local; wasm_bindgen_test_configure!(run_in_browser); const ECHO_SERVER_URL: &str = env!("ECHO_SERVER_URL"); #[wasm_bindgen_test] - async fn websocket_works() { - let mut ws = WebSocket::open(ECHO_SERVER_URL).unwrap(); - - ws.send(Message::Text("test".to_string())).await.unwrap(); - - // ignore first message - // the echo-server used sends it's info in the first message - let _ = ws.next().await; - assert_eq!( - ws.next().await.unwrap().unwrap(), - Message::Text("test".to_string()) - ) + fn websocket_works() { + let ws = WebSocket::open(ECHO_SERVER_URL).unwrap(); + let (mut sender, mut receiver) = ws.split(); + + spawn_local(async move { + sender.send(Message::Text(String::from("test 1"))).await.unwrap(); + sender.send(Message::Text(String::from("test 2"))).await.unwrap(); + }); + + spawn_local(async move { + // ignore first message + // the echo-server used sends it's info in the first message + // let _ = ws.next().await; + + assert_eq!( + receiver.next().await.unwrap().unwrap(), + Message::Text("test 1".to_string()) + ); + assert_eq!( + receiver.next().await.unwrap().unwrap(), + Message::Text("test 2".to_string()) + ); + }); } } From 7bf2d38fb1ba192bf71879c200a967d7ac04d8ef Mon Sep 17 00:00:00 2001 From: Hamza Date: Fri, 12 Nov 2021 14:13:47 +0500 Subject: [PATCH 19/36] Rustfmt + ignore editor config files --- .gitignore | 1 + .idea/.gitignore | 8 ------- .idea/modules.xml | 8 ------- .idea/reqwasm.iml | 50 ---------------------------------------- .idea/vcs.xml | 6 ----- src/websocket/futures.rs | 12 +++++++--- 6 files changed, 10 insertions(+), 75 deletions(-) delete mode 100644 .idea/.gitignore delete mode 100644 .idea/modules.xml delete mode 100644 .idea/reqwasm.iml delete mode 100644 .idea/vcs.xml diff --git a/.gitignore b/.gitignore index 96ef6c0b..b4710678 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /target Cargo.lock +.idea diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 73f69e09..00000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml -# Editor-based HTTP Client requests -/httpRequests/ diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 3bc95263..00000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/reqwasm.iml b/.idea/reqwasm.iml deleted file mode 100644 index b4a205a4..00000000 --- a/.idea/reqwasm.iml +++ /dev/null @@ -1,50 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 35eb1ddf..00000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/src/websocket/futures.rs b/src/websocket/futures.rs index bf2737c4..174c3ed7 100644 --- a/src/websocket/futures.rs +++ b/src/websocket/futures.rs @@ -289,8 +289,8 @@ impl PinnedDrop for WebSocket { mod tests { use super::*; use futures::{SinkExt, StreamExt}; - use wasm_bindgen_test::*; use wasm_bindgen_futures::spawn_local; + use wasm_bindgen_test::*; wasm_bindgen_test_configure!(run_in_browser); @@ -302,8 +302,14 @@ mod tests { let (mut sender, mut receiver) = ws.split(); spawn_local(async move { - sender.send(Message::Text(String::from("test 1"))).await.unwrap(); - sender.send(Message::Text(String::from("test 2"))).await.unwrap(); + sender + .send(Message::Text(String::from("test 1"))) + .await + .unwrap(); + sender + .send(Message::Text(String::from("test 2"))) + .await + .unwrap(); }); spawn_local(async move { From f6531f57665ccfb954a9d1a69542b4771076cca7 Mon Sep 17 00:00:00 2001 From: Stepan Henek Date: Mon, 15 Nov 2021 03:38:45 +0100 Subject: [PATCH 20/36] Fix onclose handling (#14) --- src/websocket/futures.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/websocket/futures.rs b/src/websocket/futures.rs index 174c3ed7..5b441a51 100644 --- a/src/websocket/futures.rs +++ b/src/websocket/futures.rs @@ -139,7 +139,7 @@ impl WebSocket { }) as Box) }; - ws.set_onerror(Some(close_callback.as_ref().unchecked_ref())); + ws.set_onclose(Some(close_callback.as_ref().unchecked_ref())); Ok(Self { ws, From c5d3e12f14aa5a711394bc015d6c689456fb80aa Mon Sep 17 00:00:00 2001 From: Yusuf Bera Ertan Date: Thu, 18 Nov 2021 20:05:05 +0300 Subject: [PATCH 21/36] feat: feature gate json, websocket and http; enable them by default (#16) * feat: feature gate json support * feat: feature gate weboscket api * ci: check websocket and json features seperately in CI, check no default features * feat: feature gate the http API * refactor: use futures-core and futures-sink instead of depending on whole of futures * ci: test http feature seperately in CI * fix: only compile error conversion funcs if either APIs are enabled * fix: add futures to dev-deps for tests, fix doc test --- .github/workflows/ci.yml | 24 +++++++++++ Cargo.toml | 91 ++++++++++++++++++++++++---------------- src/error.rs | 25 +++++++---- src/http.rs | 6 ++- src/lib.rs | 2 + src/websocket/futures.rs | 18 ++++---- tests/http.rs | 2 - 7 files changed, 111 insertions(+), 57 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0984ca87..e924df79 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -68,6 +68,30 @@ jobs: with: command: clippy args: --target wasm32-unknown-unknown -- -D warnings + + - name: Run clippy (no default features) + uses: actions-rs/cargo@v1 + with: + command: clippy + args: --no-default-features --target wasm32-unknown-unknown -- -D warnings + + - name: Run clippy (with json feature) + uses: actions-rs/cargo@v1 + with: + command: clippy + args: --no-default-features --features json --target wasm32-unknown-unknown -- -D warnings + + - name: Run clippy (with websocket feature) + uses: actions-rs/cargo@v1 + with: + command: clippy + args: --no-default-features --features websocket --target wasm32-unknown-unknown -- -D warnings + + - name: Run clippy (with http feature) + uses: actions-rs/cargo@v1 + with: + command: clippy + args: --no-default-features --features http --target wasm32-unknown-unknown -- -D warnings test: name: Test diff --git a/Cargo.toml b/Cargo.toml index 110d6e74..d10fedf0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,47 +13,66 @@ exclude = [ ".idea", ] +[package.metadata.docs.rs] +all-features = true + +[features] +default = ["json", "websocket", "http"] + +# Enables `.json()` on `Response` +json = ["wasm-bindgen/serde-serialize", "serde", "serde_json"] +# Enables the WebSocket API +websocket = [ + 'web-sys/WebSocket', + 'web-sys/ErrorEvent', + 'web-sys/FileReader', + 'web-sys/MessageEvent', + 'web-sys/ProgressEvent', + 'web-sys/CloseEvent', + 'web-sys/BinaryType', + 'web-sys/Blob', + "async-broadcast", + "pin-project", + "futures-core", + "futures-sink", +] +# Enables the HTTP API +http = [ + 'web-sys/Headers', + 'web-sys/Request', + 'web-sys/RequestInit', + 'web-sys/RequestMode', + 'web-sys/Response', + 'web-sys/Window', + 'web-sys/RequestCache', + 'web-sys/RequestCredentials', + 'web-sys/ObserverCallback', + 'web-sys/RequestRedirect', + 'web-sys/ReferrerPolicy', + 'web-sys/AbortSignal', + 'web-sys/ReadableStream', + 'web-sys/Blob', + 'web-sys/FormData', +] + [dependencies] -wasm-bindgen = { version = "0.2", features = ["serde-serialize"] } +wasm-bindgen = "0.2" +web-sys = "0.3" js-sys = "0.3" +gloo-utils = "0.1.0" + wasm-bindgen-futures = "0.4" -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" +futures-core = { version = "0.3", optional = true } +futures-sink = { version = "0.3", optional = true } + thiserror = "1.0" -futures = "0.3.14" -gloo-utils = "0.1.0" -async-broadcast = "0.3" -pin-project = "1" - -[dependencies.web-sys] -version = "0.3.4" -features = [ - 'Headers', - 'Request', - 'RequestInit', - 'RequestMode', - 'Response', - 'Window', - 'RequestCache', - 'RequestCredentials', - 'ObserverCallback', - 'RequestRedirect', - 'ReferrerPolicy', - 'AbortSignal', - 'ReadableStream', - 'Blob', - 'FormData', - 'FileReader', - 'CloseEvent', - - 'WebSocket', - 'ErrorEvent', - 'FileReader', - 'MessageEvent', - 'ProgressEvent', - 'BinaryType', -] +serde = { version = "1.0", features = ["derive"], optional = true } +serde_json = { version = "1.0", optional = true } + +async-broadcast = { version = "0.3", optional = true } +pin-project = { version = "1.0", optional = true } [dev-dependencies] wasm-bindgen-test = "0.3" +futures = "0.3" \ No newline at end of file diff --git a/src/error.rs b/src/error.rs index c0accb9e..0f25c1b5 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,6 +1,5 @@ use gloo_utils::errors::JsError; use thiserror::Error as ThisError; -use wasm_bindgen::JsValue; /// All the errors returned by this crate. #[derive(Debug, ThisError)] @@ -9,6 +8,7 @@ pub enum Error { #[error("{0}")] JsError(JsError), /// Error returned by `serde` during deserialization. + #[cfg(feature = "json")] #[error("{0}")] SerdeError( #[source] @@ -17,13 +17,22 @@ pub enum Error { ), } -pub(crate) fn js_to_error(js_value: JsValue) -> Error { - Error::JsError(js_to_js_error(js_value)) -} +#[cfg(any(feature = "http", feature = "websocket"))] +pub(crate) use conversion::*; +#[cfg(any(feature = "http", feature = "websocket"))] +mod conversion { + use gloo_utils::errors::JsError; + use wasm_bindgen::JsValue; + + #[cfg(feature = "http")] + pub(crate) fn js_to_error(js_value: JsValue) -> super::Error { + super::Error::JsError(js_to_js_error(js_value)) + } -pub(crate) fn js_to_js_error(js_value: JsValue) -> JsError { - match JsError::try_from(js_value) { - Ok(error) => error, - Err(_) => unreachable!("JsValue passed is not an Error type -- this is a bug"), + pub(crate) fn js_to_js_error(js_value: JsValue) -> JsError { + match JsError::try_from(js_value) { + Ok(error) => error, + Err(_) => unreachable!("JsValue passed is not an Error type -- this is a bug"), + } } } diff --git a/src/http.rs b/src/http.rs index fa01d896..1507c5a2 100644 --- a/src/http.rs +++ b/src/http.rs @@ -15,12 +15,15 @@ use crate::{js_to_error, Error}; use js_sys::{ArrayBuffer, Uint8Array}; -use serde::de::DeserializeOwned; use std::fmt; use wasm_bindgen::prelude::*; use wasm_bindgen::JsCast; use wasm_bindgen_futures::JsFuture; use web_sys::window; + +#[cfg(feature = "json")] +use serde::de::DeserializeOwned; + pub use web_sys::{ AbortSignal, FormData, Headers, ObserverCallback, ReadableStream, ReferrerPolicy, RequestCache, RequestCredentials, RequestMode, RequestRedirect, @@ -259,6 +262,7 @@ impl Response { } /// Gets and parses the json. + #[cfg(feature = "json")] pub async fn json(&self) -> Result { let promise = self.response.json().map_err(js_to_error)?; let json = JsFuture::from(promise).await.map_err(js_to_error)?; diff --git a/src/lib.rs b/src/lib.rs index 3e024089..abd7c606 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,7 +10,9 @@ )] mod error; +#[cfg(feature = "http")] pub mod http; +#[cfg(feature = "websocket")] pub mod websocket; pub use error::*; diff --git a/src/websocket/futures.rs b/src/websocket/futures.rs index 5b441a51..8bdf517d 100644 --- a/src/websocket/futures.rs +++ b/src/websocket/futures.rs @@ -11,18 +11,16 @@ //! # ($($expr:expr),*) => {{}}; //! # } //! # fn no_run() { -//! let mut ws = WebSocket::open("wss://echo.websocket.org").unwrap(); +//! let mut ws = WebSocket::open("wss://echo.websocket.org").unwrap(); +//! let (mut write, mut read) = ws.split(); //! -//! spawn_local({ -//! let mut ws = ws.clone(); -//! async move { -//! ws.send(Message::Text(String::from("test"))).await.unwrap(); -//! ws.send(Message::Text(String::from("test 2"))).await.unwrap(); -//! } +//! spawn_local(async move { +//! write.send(Message::Text(String::from("test"))).await.unwrap(); +//! write.send(Message::Text(String::from("test 2"))).await.unwrap(); //! }); //! //! spawn_local(async move { -//! while let Some(msg) = ws.next().await { +//! while let Some(msg) = read.next().await { //! console_log!(format!("1. {:?}", msg)) //! } //! console_log!("WebSocket Closed") @@ -35,8 +33,8 @@ use crate::websocket::{ Message, State, WebSocketError, }; use async_broadcast::Receiver; -use futures::ready; -use futures::{Sink, Stream}; +use futures_core::{ready, Stream}; +use futures_sink::Sink; use gloo_utils::errors::JsError; use pin_project::{pin_project, pinned_drop}; use std::cell::RefCell; diff --git a/tests/http.rs b/tests/http.rs index d650a997..a2672423 100644 --- a/tests/http.rs +++ b/tests/http.rs @@ -1,7 +1,5 @@ -use js_sys::Uint8Array; use reqwasm::http::*; use serde::{Deserialize, Serialize}; -use wasm_bindgen::JsValue; use wasm_bindgen_test::*; wasm_bindgen_test_configure!(run_in_browser); From 22164e9f10e6da480d285461605b9beed9e17de8 Mon Sep 17 00:00:00 2001 From: Hamza Date: Fri, 19 Nov 2021 09:05:28 +0500 Subject: [PATCH 22/36] 0.3.0 --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d10fedf0..b5a61d00 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "reqwasm" -version = "0.2.1" +version = "0.3.0" authors = ["Hamza "] edition = "2021" license = "MIT OR Apache-2.0" @@ -75,4 +75,4 @@ pin-project = { version = "1.0", optional = true } [dev-dependencies] wasm-bindgen-test = "0.3" -futures = "0.3" \ No newline at end of file +futures = "0.3" From 8d0be9dc4833aa220f48b13eff574c2f71bc8e2f Mon Sep 17 00:00:00 2001 From: Hamza Date: Sun, 21 Nov 2021 20:54:00 +0500 Subject: [PATCH 23/36] Fix outdated/missing docs --- README.md | 21 +++++++++++---------- src/error.rs | 1 + src/http.rs | 2 ++ src/lib.rs | 3 +++ 4 files changed, 17 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index c5a82bc2..b6208956 100644 --- a/README.md +++ b/README.md @@ -21,22 +21,23 @@ assert_eq!(resp.status(), 200); ### WebSocket ```rust -let ws = WebSocket::open("wss://echo.websocket.org").unwrap(); +use reqwasm::websocket::{Message, futures::WebSocket}; +use wasm_bindgen_futures::spawn_local; +use futures::{SinkExt, StreamExt}; -let (mut sender, mut receiver) = (ws.sender, ws.receiver); +let mut ws = WebSocket::open("wss://echo.websocket.org").unwrap(); +let (mut write, mut read) = ws.split(); spawn_local(async move { - while let Some(m) = receiver.next().await { - match m { - Ok(Message::Text(m)) => console_log!("message", m), - Ok(Message::Bytes(m)) => console_log!("message", format!("{:?}", m)), - Err(e) => {} - } - } + write.send(Message::Text(String::from("test"))).await.unwrap(); + write.send(Message::Text(String::from("test 2"))).await.unwrap(); }); spawn_local(async move { - sender.send(Message::Text("test".to_string())).await.unwrap(); + while let Some(msg) = read.next().await { + console_log!(format!("1. {:?}", msg)) + } + console_log!("WebSocket Closed") }) ``` ## Contributions diff --git a/src/error.rs b/src/error.rs index 0f25c1b5..2b88618d 100644 --- a/src/error.rs +++ b/src/error.rs @@ -9,6 +9,7 @@ pub enum Error { JsError(JsError), /// Error returned by `serde` during deserialization. #[cfg(feature = "json")] + #[cfg_attr(docsrs, doc(cfg(feature = "json")))] #[error("{0}")] SerdeError( #[source] diff --git a/src/http.rs b/src/http.rs index 1507c5a2..a32dc5d3 100644 --- a/src/http.rs +++ b/src/http.rs @@ -22,6 +22,7 @@ use wasm_bindgen_futures::JsFuture; use web_sys::window; #[cfg(feature = "json")] +#[cfg_attr(docsrs, doc(cfg(feature = "json")))] use serde::de::DeserializeOwned; pub use web_sys::{ @@ -263,6 +264,7 @@ impl Response { /// Gets and parses the json. #[cfg(feature = "json")] + #[cfg_attr(docsrs, doc(cfg(feature = "json")))] pub async fn json(&self) -> Result { let promise = self.response.json().map_err(js_to_error)?; let json = JsFuture::from(promise).await.map_err(js_to_error)?; diff --git a/src/lib.rs b/src/lib.rs index abd7c606..d3cabced 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,11 +8,14 @@ missing_debug_implementations, missing_copy_implementations )] +#![cfg_attr(docsrs, feature(doc_cfg))] mod error; #[cfg(feature = "http")] +#[cfg_attr(docsrs, doc(cfg(feature = "http")))] pub mod http; #[cfg(feature = "websocket")] +#[cfg_attr(docsrs, doc(cfg(feature = "websocket")))] pub mod websocket; pub use error::*; From 5bf1c880c2bd1393fba5688b1ec908dfdd7acdc1 Mon Sep 17 00:00:00 2001 From: Hamza Date: Sun, 21 Nov 2021 20:55:39 +0500 Subject: [PATCH 24/36] 0.3.1 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index b5a61d00..f457c60b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "reqwasm" -version = "0.3.0" +version = "0.3.1" authors = ["Hamza "] edition = "2021" license = "MIT OR Apache-2.0" From c62e8cb6019b593e58c8a7a9ac661e9fd716fdbe Mon Sep 17 00:00:00 2001 From: Luke Chu <37006668+lukechu10@users.noreply.github.com> Date: Thu, 2 Dec 2021 10:11:34 -0800 Subject: [PATCH 25/36] Change edition from 2021 to 2018 (#18) * Change edition from 2021 to 2018 * Fix missing import due to edition 2021 prelude --- Cargo.toml | 2 +- src/error.rs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index f457c60b..3265b32d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ name = "reqwasm" version = "0.3.1" authors = ["Hamza "] -edition = "2021" +edition = "2018" license = "MIT OR Apache-2.0" repository = "https://github.com/hamza1311/reqwasm" description = "HTTP requests library for WASM Apps" diff --git a/src/error.rs b/src/error.rs index 2b88618d..3c254c21 100644 --- a/src/error.rs +++ b/src/error.rs @@ -23,6 +23,7 @@ pub(crate) use conversion::*; #[cfg(any(feature = "http", feature = "websocket"))] mod conversion { use gloo_utils::errors::JsError; + use std::convert::TryFrom; use wasm_bindgen::JsValue; #[cfg(feature = "http")] From 863668a3724b283b3b47e6308f5d9c748b08c983 Mon Sep 17 00:00:00 2001 From: Muhammad Hamza Date: Thu, 16 Dec 2021 19:13:32 +0500 Subject: [PATCH 26/36] hopefully this will fix the issue (#19) --- src/websocket/futures.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/websocket/futures.rs b/src/websocket/futures.rs index 8bdf517d..bc67e857 100644 --- a/src/websocket/futures.rs +++ b/src/websocket/futures.rs @@ -58,7 +58,7 @@ pub struct WebSocket { closures: Rc<( Closure, Closure, - Closure, + Closure, Closure, )>, } @@ -103,18 +103,18 @@ impl WebSocket { ws.set_onmessage(Some(message_callback.as_ref().unchecked_ref())); - let error_callback: Closure = { + let error_callback: Closure = { let sender = sender.clone(); - Closure::wrap(Box::new(move |e: web_sys::ErrorEvent| { + Closure::wrap(Box::new(move |e: web_sys::Event| { let sender = sender.clone(); wasm_bindgen_futures::spawn_local(async move { let _ = sender .broadcast(StreamMessage::ErrorEvent(ErrorEvent { - message: e.message(), + message: String::from(js_sys::JsString::from(JsValue::from(e))), })) .await; }) - }) as Box) + }) as Box) }; ws.set_onerror(Some(error_callback.as_ref().unchecked_ref())); From 1a64fb97b71420c7cc9e5cada649e5f4cbe3325b Mon Sep 17 00:00:00 2001 From: Hamza Date: Thu, 16 Dec 2021 19:24:44 +0500 Subject: [PATCH 27/36] There's no message --- src/websocket/events.rs | 7 ------- src/websocket/futures.rs | 19 ++++++------------- src/websocket/mod.rs | 6 +++--- 3 files changed, 9 insertions(+), 23 deletions(-) diff --git a/src/websocket/events.rs b/src/websocket/events.rs index 44b333c9..b8df72f1 100644 --- a/src/websocket/events.rs +++ b/src/websocket/events.rs @@ -1,12 +1,5 @@ //! WebSocket Events -/// This is created from [`ErrorEvent`][web_sys::ErrorEvent] received from `onerror` listener of the WebSocket. -#[derive(Clone, Debug)] -pub struct ErrorEvent { - /// The error message. - pub message: String, -} - /// Data emiited by `onclose` event #[derive(Clone, Debug)] pub struct CloseEvent { diff --git a/src/websocket/futures.rs b/src/websocket/futures.rs index bc67e857..fb727b98 100644 --- a/src/websocket/futures.rs +++ b/src/websocket/futures.rs @@ -28,10 +28,7 @@ //! # } //! ``` use crate::js_to_js_error; -use crate::websocket::{ - events::{CloseEvent, ErrorEvent}, - Message, State, WebSocketError, -}; +use crate::websocket::{events::CloseEvent, Message, State, WebSocketError}; use async_broadcast::Receiver; use futures_core::{ready, Stream}; use futures_sink::Sink; @@ -105,14 +102,10 @@ impl WebSocket { let error_callback: Closure = { let sender = sender.clone(); - Closure::wrap(Box::new(move |e: web_sys::Event| { + Closure::wrap(Box::new(move |_e: web_sys::Event| { let sender = sender.clone(); wasm_bindgen_futures::spawn_local(async move { - let _ = sender - .broadcast(StreamMessage::ErrorEvent(ErrorEvent { - message: String::from(js_sys::JsString::from(JsValue::from(e))), - })) - .await; + let _ = sender.broadcast(StreamMessage::ErrorEvent).await; }) }) as Box) }; @@ -193,7 +186,7 @@ impl WebSocket { #[derive(Clone)] enum StreamMessage { - ErrorEvent(ErrorEvent), + ErrorEvent, CloseEvent(CloseEvent), Message(Message), ConnectionClose, @@ -264,8 +257,8 @@ impl Stream for WebSocket { let msg = ready!(self.project().message_receiver.poll_next(cx)); match msg { Some(StreamMessage::Message(msg)) => Poll::Ready(Some(Ok(msg))), - Some(StreamMessage::ErrorEvent(err)) => { - Poll::Ready(Some(Err(WebSocketError::ConnectionError(err)))) + Some(StreamMessage::ErrorEvent) => { + Poll::Ready(Some(Err(WebSocketError::ConnectionError))) } Some(StreamMessage::CloseEvent(e)) => { Poll::Ready(Some(Err(WebSocketError::ConnectionClose(e)))) diff --git a/src/websocket/mod.rs b/src/websocket/mod.rs index 5e11a3f7..41696e1d 100644 --- a/src/websocket/mod.rs +++ b/src/websocket/mod.rs @@ -6,7 +6,7 @@ pub mod events; pub mod futures; -use events::{CloseEvent, ErrorEvent}; +use events::CloseEvent; use gloo_utils::errors::JsError; use std::fmt; @@ -41,7 +41,7 @@ pub enum State { #[non_exhaustive] pub enum WebSocketError { /// The `error` event - ConnectionError(ErrorEvent), + ConnectionError, /// The `close` event ConnectionClose(CloseEvent), /// Message failed to send. @@ -51,7 +51,7 @@ pub enum WebSocketError { impl fmt::Display for WebSocketError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - WebSocketError::ConnectionError(e) => write!(f, "{}", e.message), + WebSocketError::ConnectionError => write!(f, "WebSocket connection failed"), WebSocketError::ConnectionClose(e) => write!( f, "WebSocket Closed: code: {}, reason: {}", From 785b093f013a8aae00dfc906be42d67c3f462bba Mon Sep 17 00:00:00 2001 From: Muhammad Hamza Date: Fri, 24 Dec 2021 16:41:35 +0500 Subject: [PATCH 28/36] Replace `async-broadcast` with `futures-channel::mpsc` (#21) We no longer need a multi-producer-multi-consumer channel. There's only one consumer as of https://github.com/hamza1311/reqwasm/commit/445e9a5bf555a0e37d0ff7fb71e69d34cb8f2493 --- Cargo.toml | 46 ++++++++++++++++++++-------------------- src/websocket/futures.rs | 16 ++++++-------- 2 files changed, 30 insertions(+), 32 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3265b32d..6bbc517a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,28 @@ exclude = [ [package.metadata.docs.rs] all-features = true +[dependencies] +wasm-bindgen = "0.2" +web-sys = "0.3" +js-sys = "0.3" +gloo-utils = "0.1.0" + +wasm-bindgen-futures = "0.4" +futures-core = { version = "0.3", optional = true } +futures-sink = { version = "0.3", optional = true } + +thiserror = "1.0" + +serde = { version = "1.0", features = ["derive"], optional = true } +serde_json = { version = "1.0", optional = true } + +futures-channel = { version = "0.3", optional = true } +pin-project = { version = "1.0", optional = true } + +[dev-dependencies] +wasm-bindgen-test = "0.3" +futures = "0.3" + [features] default = ["json", "websocket", "http"] @@ -31,7 +53,7 @@ websocket = [ 'web-sys/CloseEvent', 'web-sys/BinaryType', 'web-sys/Blob', - "async-broadcast", + "futures-channel", "pin-project", "futures-core", "futures-sink", @@ -54,25 +76,3 @@ http = [ 'web-sys/Blob', 'web-sys/FormData', ] - -[dependencies] -wasm-bindgen = "0.2" -web-sys = "0.3" -js-sys = "0.3" -gloo-utils = "0.1.0" - -wasm-bindgen-futures = "0.4" -futures-core = { version = "0.3", optional = true } -futures-sink = { version = "0.3", optional = true } - -thiserror = "1.0" - -serde = { version = "1.0", features = ["derive"], optional = true } -serde_json = { version = "1.0", optional = true } - -async-broadcast = { version = "0.3", optional = true } -pin-project = { version = "1.0", optional = true } - -[dev-dependencies] -wasm-bindgen-test = "0.3" -futures = "0.3" diff --git a/src/websocket/futures.rs b/src/websocket/futures.rs index fb727b98..51cd18c3 100644 --- a/src/websocket/futures.rs +++ b/src/websocket/futures.rs @@ -29,7 +29,7 @@ //! ``` use crate::js_to_js_error; use crate::websocket::{events::CloseEvent, Message, State, WebSocketError}; -use async_broadcast::Receiver; +use futures_channel::mpsc; use futures_core::{ready, Stream}; use futures_sink::Sink; use gloo_utils::errors::JsError; @@ -50,7 +50,7 @@ pub struct WebSocket { ws: web_sys::WebSocket, sink_waker: Rc>>, #[pin] - message_receiver: Receiver, + message_receiver: mpsc::UnboundedReceiver, #[allow(clippy::type_complexity)] closures: Rc<( Closure, @@ -74,7 +74,7 @@ impl WebSocket { let waker: Rc>> = Rc::new(RefCell::new(None)); let ws = web_sys::WebSocket::new(url).map_err(js_to_js_error)?; - let (sender, receiver) = async_broadcast::broadcast(10); + let (sender, receiver) = mpsc::unbounded(); let open_callback: Closure = { let waker = Rc::clone(&waker); @@ -93,7 +93,7 @@ impl WebSocket { let sender = sender.clone(); wasm_bindgen_futures::spawn_local(async move { let msg = parse_message(e).await; - let _ = sender.broadcast(StreamMessage::Message(msg)).await; + let _ = sender.unbounded_send(StreamMessage::Message(msg)); }) }) as Box) }; @@ -105,7 +105,7 @@ impl WebSocket { Closure::wrap(Box::new(move |_e: web_sys::Event| { let sender = sender.clone(); wasm_bindgen_futures::spawn_local(async move { - let _ = sender.broadcast(StreamMessage::ErrorEvent).await; + let _ = sender.unbounded_send(StreamMessage::ErrorEvent); }) }) as Box) }; @@ -122,10 +122,8 @@ impl WebSocket { was_clean: e.was_clean(), }; - let _ = sender - .broadcast(StreamMessage::CloseEvent(close_event)) - .await; - let _ = sender.broadcast(StreamMessage::ConnectionClose).await; + let _ = sender.unbounded_send(StreamMessage::CloseEvent(close_event)); + let _ = sender.unbounded_send(StreamMessage::ConnectionClose); }) }) as Box) }; From b4f02721c7227ae9ab7406d0e8b43e43448ba45c Mon Sep 17 00:00:00 2001 From: Hamza Date: Fri, 24 Dec 2021 16:42:29 +0500 Subject: [PATCH 29/36] Release 0.4.0 --- Cargo.toml | 2 +- src/websocket/events.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 6bbc517a..76b73ec6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "reqwasm" -version = "0.3.1" +version = "0.4.0" authors = ["Hamza "] edition = "2018" license = "MIT OR Apache-2.0" diff --git a/src/websocket/events.rs b/src/websocket/events.rs index b8df72f1..5261ab76 100644 --- a/src/websocket/events.rs +++ b/src/websocket/events.rs @@ -1,12 +1,12 @@ //! WebSocket Events -/// Data emiited by `onclose` event +/// Data emitted by `onclose` event #[derive(Clone, Debug)] pub struct CloseEvent { /// Close code pub code: u16, /// Close reason pub reason: String, - /// If the websockt was closed cleanly + /// If the websockets was closed cleanly pub was_clean: bool, } From 3100e9129000c6365571b915f2c887cbc4e4079d Mon Sep 17 00:00:00 2001 From: Valentin Date: Wed, 9 Feb 2022 14:40:13 +0100 Subject: [PATCH 30/36] Fix message ordering not being preserved (#29) The websocket specification guarantees that messages are received in the same order they are sent. The implementation in this library can violate this guarantee because messages are parsed in a spawn_local block before being sent over the channel. When multiple messages are received before the next executor tick the scheduling order of the futures is unspecified. We fix this by performing all operations synchronously. The only part where async is needed is the conversion of Blob to ArrayBuffer which we obsolete by setting the websocket to always receive binary data as ArrayBuffer. --- src/websocket/futures.rs | 51 ++++++++++++++-------------------------- 1 file changed, 17 insertions(+), 34 deletions(-) diff --git a/src/websocket/futures.rs b/src/websocket/futures.rs index 51cd18c3..5a9a9085 100644 --- a/src/websocket/futures.rs +++ b/src/websocket/futures.rs @@ -40,8 +40,7 @@ use std::rc::Rc; use std::task::{Context, Poll, Waker}; use wasm_bindgen::prelude::*; use wasm_bindgen::JsCast; -use wasm_bindgen_futures::JsFuture; -use web_sys::{Blob, MessageEvent}; +use web_sys::{BinaryType, MessageEvent}; /// Wrapper around browser's WebSocket API. #[allow(missing_debug_implementations)] @@ -74,6 +73,11 @@ impl WebSocket { let waker: Rc>> = Rc::new(RefCell::new(None)); let ws = web_sys::WebSocket::new(url).map_err(js_to_js_error)?; + // We rely on this because the other type Blob can be converted to Vec only through a + // promise which makes it awkward to use in our event callbacks where we want to guarantee + // the order of the events stays the same. + ws.set_binary_type(BinaryType::Arraybuffer); + let (sender, receiver) = mpsc::unbounded(); let open_callback: Closure = { @@ -91,10 +95,8 @@ impl WebSocket { let sender = sender.clone(); Closure::wrap(Box::new(move |e: MessageEvent| { let sender = sender.clone(); - wasm_bindgen_futures::spawn_local(async move { - let msg = parse_message(e).await; - let _ = sender.unbounded_send(StreamMessage::Message(msg)); - }) + let msg = parse_message(e); + let _ = sender.unbounded_send(StreamMessage::Message(msg)); }) as Box) }; @@ -104,9 +106,7 @@ impl WebSocket { let sender = sender.clone(); Closure::wrap(Box::new(move |_e: web_sys::Event| { let sender = sender.clone(); - wasm_bindgen_futures::spawn_local(async move { - let _ = sender.unbounded_send(StreamMessage::ErrorEvent); - }) + let _ = sender.unbounded_send(StreamMessage::ErrorEvent); }) as Box) }; @@ -115,16 +115,13 @@ impl WebSocket { let close_callback: Closure = { Closure::wrap(Box::new(move |e: web_sys::CloseEvent| { let sender = sender.clone(); - wasm_bindgen_futures::spawn_local(async move { - let close_event = CloseEvent { - code: e.code(), - reason: e.reason(), - was_clean: e.was_clean(), - }; - - let _ = sender.unbounded_send(StreamMessage::CloseEvent(close_event)); - let _ = sender.unbounded_send(StreamMessage::ConnectionClose); - }) + let close_event = CloseEvent { + code: e.code(), + reason: e.reason(), + was_clean: e.was_clean(), + }; + let _ = sender.unbounded_send(StreamMessage::CloseEvent(close_event)); + let _ = sender.unbounded_send(StreamMessage::ConnectionClose); }) as Box) }; @@ -190,31 +187,17 @@ enum StreamMessage { ConnectionClose, } -async fn parse_message(event: MessageEvent) -> Message { +fn parse_message(event: MessageEvent) -> Message { if let Ok(array_buffer) = event.data().dyn_into::() { let array = js_sys::Uint8Array::new(&array_buffer); Message::Bytes(array.to_vec()) } else if let Ok(txt) = event.data().dyn_into::() { Message::Text(String::from(&txt)) - } else if let Ok(blob) = event.data().dyn_into::() { - let vec = blob_into_bytes(blob).await; - Message::Bytes(vec) } else { unreachable!("message event, received Unknown: {:?}", event.data()); } } -// copied verbatim from https://github.com/rustwasm/wasm-bindgen/issues/2551 -async fn blob_into_bytes(blob: Blob) -> Vec { - let array_buffer_promise: JsFuture = blob.array_buffer().into(); - - let array_buffer: JsValue = array_buffer_promise - .await - .expect("Could not get ArrayBuffer from file"); - - js_sys::Uint8Array::new(&array_buffer).to_vec() -} - impl Sink for WebSocket { type Error = WebSocketError; From 5766120f51cfca0f771b7cff88e48dd21d76c868 Mon Sep 17 00:00:00 2001 From: Muhammad Hamza Date: Wed, 9 Feb 2022 18:40:46 +0500 Subject: [PATCH 31/36] 0.4.1 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 76b73ec6..a1a688a5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "reqwasm" -version = "0.4.0" +version = "0.4.1" authors = ["Hamza "] edition = "2018" license = "MIT OR Apache-2.0" From c2c47bb380d3d34d4fdd992eb88d6288e02d7bb9 Mon Sep 17 00:00:00 2001 From: Muhammad Hamza Date: Tue, 15 Feb 2022 18:57:06 +0500 Subject: [PATCH 32/36] move files for gloo merge --- Cargo.toml => crates/net/Cargo.toml | 0 README.md => crates/net/README.md | 0 {src => crates/net/src}/error.rs | 0 {src => crates/net/src}/http.rs | 0 {src => crates/net/src}/lib.rs | 0 {src => crates/net/src}/websocket/events.rs | 0 {src => crates/net/src}/websocket/futures.rs | 0 {src => crates/net/src}/websocket/mod.rs | 0 {tests => crates/net/tests}/http.rs | 0 9 files changed, 0 insertions(+), 0 deletions(-) rename Cargo.toml => crates/net/Cargo.toml (100%) rename README.md => crates/net/README.md (100%) rename {src => crates/net/src}/error.rs (100%) rename {src => crates/net/src}/http.rs (100%) rename {src => crates/net/src}/lib.rs (100%) rename {src => crates/net/src}/websocket/events.rs (100%) rename {src => crates/net/src}/websocket/futures.rs (100%) rename {src => crates/net/src}/websocket/mod.rs (100%) rename {tests => crates/net/tests}/http.rs (100%) diff --git a/Cargo.toml b/crates/net/Cargo.toml similarity index 100% rename from Cargo.toml rename to crates/net/Cargo.toml diff --git a/README.md b/crates/net/README.md similarity index 100% rename from README.md rename to crates/net/README.md diff --git a/src/error.rs b/crates/net/src/error.rs similarity index 100% rename from src/error.rs rename to crates/net/src/error.rs diff --git a/src/http.rs b/crates/net/src/http.rs similarity index 100% rename from src/http.rs rename to crates/net/src/http.rs diff --git a/src/lib.rs b/crates/net/src/lib.rs similarity index 100% rename from src/lib.rs rename to crates/net/src/lib.rs diff --git a/src/websocket/events.rs b/crates/net/src/websocket/events.rs similarity index 100% rename from src/websocket/events.rs rename to crates/net/src/websocket/events.rs diff --git a/src/websocket/futures.rs b/crates/net/src/websocket/futures.rs similarity index 100% rename from src/websocket/futures.rs rename to crates/net/src/websocket/futures.rs diff --git a/src/websocket/mod.rs b/crates/net/src/websocket/mod.rs similarity index 100% rename from src/websocket/mod.rs rename to crates/net/src/websocket/mod.rs diff --git a/tests/http.rs b/crates/net/tests/http.rs similarity index 100% rename from tests/http.rs rename to crates/net/tests/http.rs From 9cc8722d117ac1364fd0163dd3ad52821d350276 Mon Sep 17 00:00:00 2001 From: Muhammad Hamza Date: Tue, 15 Feb 2022 19:00:53 +0500 Subject: [PATCH 33/36] remove licence files --- .gitignore | 3 - LICENSE-APACHE.md | 201 ---------------------------------------------- LICENSE-MIT.md | 21 ----- 3 files changed, 225 deletions(-) delete mode 100644 .gitignore delete mode 100644 LICENSE-APACHE.md delete mode 100644 LICENSE-MIT.md diff --git a/.gitignore b/.gitignore deleted file mode 100644 index b4710678..00000000 --- a/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -/target -Cargo.lock -.idea diff --git a/LICENSE-APACHE.md b/LICENSE-APACHE.md deleted file mode 100644 index c319da33..00000000 --- a/LICENSE-APACHE.md +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - -Copyright [yyyy] [name of copyright owner] - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. diff --git a/LICENSE-MIT.md b/LICENSE-MIT.md deleted file mode 100644 index d8f92278..00000000 --- a/LICENSE-MIT.md +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2021 Muhammad Hamza - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. From f2d38a0995706c398d314a4c297f124d092039cd Mon Sep 17 00:00:00 2001 From: Muhammad Hamza Date: Tue, 15 Feb 2022 19:12:08 +0500 Subject: [PATCH 34/36] gloo-specific patches --- .github/workflows/ci.yml | 145 ----------------------------------- .github/workflows/tests.yml | 53 +++++++++++++ Cargo.toml | 1 + crates/net/Cargo.toml | 11 +-- crates/net/README.md | 26 +++++-- website/src/pages/__index.md | 12 +-- 6 files changed, 84 insertions(+), 164 deletions(-) delete mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index e924df79..00000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,145 +0,0 @@ -name: CI - -on: - push: - branches: [ master ] - pull_request: - branches: [ master ] - -env: - CARGO_TERM_COLOR: always - -jobs: - fmt: - name: Format - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - profile: minimal - components: clippy - target: wasm32-unknown-unknown - - - uses: actions/cache@v2 - with: - path: | - ~/.cargo/registry - ~/.cargo/git - target - key: cargo-${{ runner.os }}-fmt-${{ hashFiles('**/Cargo.toml') }} - restore-keys: | - cargo-${{ runner.os }}-fmt- - cargo-${{ runner.os }}- - - - name: Run cargo fmt - uses: actions-rs/cargo@v1 - with: - command: fmt - args: -- --check - - clippy: - name: Clippy - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - profile: minimal - components: clippy - target: wasm32-unknown-unknown - - - uses: actions/cache@v2 - with: - path: | - ~/.cargo/registry - ~/.cargo/git - target - key: cargo-${{ runner.os }}-clippy-${{ hashFiles('**/Cargo.toml') }} - restore-keys: | - cargo-${{ runner.os }}-clippy- - cargo-${{ runner.os }}- - - - name: Run clippy - uses: actions-rs/cargo@v1 - with: - command: clippy - args: --target wasm32-unknown-unknown -- -D warnings - - - name: Run clippy (no default features) - uses: actions-rs/cargo@v1 - with: - command: clippy - args: --no-default-features --target wasm32-unknown-unknown -- -D warnings - - - name: Run clippy (with json feature) - uses: actions-rs/cargo@v1 - with: - command: clippy - args: --no-default-features --features json --target wasm32-unknown-unknown -- -D warnings - - - name: Run clippy (with websocket feature) - uses: actions-rs/cargo@v1 - with: - command: clippy - args: --no-default-features --features websocket --target wasm32-unknown-unknown -- -D warnings - - - name: Run clippy (with http feature) - uses: actions-rs/cargo@v1 - with: - command: clippy - args: --no-default-features --features http --target wasm32-unknown-unknown -- -D warnings - - test: - name: Test - runs-on: ubuntu-latest - services: - httpbin: - image: kennethreitz/httpbin@sha256:599fe5e5073102dbb0ee3dbb65f049dab44fa9fc251f6835c9990f8fb196a72b - ports: - - 8080:80 - echo_server: - image: jmalloc/echo-server@sha256:c461e7e54d947a8777413aaf9c624b4ad1f1bac5d8272475da859ae82c1abd7d - ports: - - 8081:8080 - - steps: - - uses: actions/checkout@v2 - - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - profile: minimal - components: clippy - target: wasm32-unknown-unknown - - - - name: Install wasm-pack - run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh - - - uses: actions/cache@v2 - with: - path: | - ~/.cargo/registry - ~/.cargo/git - target - key: cargo-${{ runner.os }}-test-${{ hashFiles('**/Cargo.toml') }} - restore-keys: | - cargo-${{ runner.os }}-test- - cargo-${{ runner.os }}- - - - name: Run browser tests - env: - HTTPBIN_URL: "http://localhost:8080" - ECHO_SERVER_URL: "ws://localhost:8081" - run: wasm-pack test --chrome --firefox --headless - - - name: Run browser tests - env: - HTTPBIN_URL: "http://localhost:8080" - ECHO_SERVER_URL: "ws://localhost:8081" - uses: actions-rs/cargo@v1 - with: - command: test diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 038305bd..84a70a14 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -63,6 +63,59 @@ jobs: - name: Run tests run: | for x in $(ls crates); do + # gloo-net is tested separately + if [[ "$example" == "gloo-net" ]]; then + continue + fi wasm-pack test --headless --firefox --chrome crates/$x --all-features wasm-pack test --headless --firefox --chrome crates/$x --no-default-features done + + test-net: + name: Test gloo-net + runs-on: ubuntu-latest + services: + httpbin: + image: kennethreitz/httpbin@sha256:599fe5e5073102dbb0ee3dbb65f049dab44fa9fc251f6835c9990f8fb196a72b + ports: + - 8080:80 + echo_server: + image: jmalloc/echo-server@sha256:c461e7e54d947a8777413aaf9c624b4ad1f1bac5d8272475da859ae82c1abd7d + ports: + - 8081:8080 + + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + toolchain: stable + profile: minimal + components: clippy + target: wasm32-unknown-unknown + + - name: Install wasm-pack + run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh + + - uses: actions/cache@v2 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: cargo-${{ runner.os }}-test-${{ hashFiles('**/Cargo.toml') }} + restore-keys: | + cargo-${{ runner.os }}-test- + cargo-${{ runner.os }}- + - name: Run browser tests + env: + HTTPBIN_URL: "http://localhost:8080" + ECHO_SERVER_URL: "ws://localhost:8081" + run: wasm-pack test --chrome --firefox --headless + + - name: Run browser tests + env: + HTTPBIN_URL: "http://localhost:8080" + ECHO_SERVER_URL: "ws://localhost:8081" + uses: actions-rs/cargo@v1 + with: + command: test diff --git a/Cargo.toml b/Cargo.toml index 8f0f4e8a..9dd3a228 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,4 +41,5 @@ members = [ "crates/utils", "crates/history", "crates/worker", + "crates/net", ] diff --git a/crates/net/Cargo.toml b/crates/net/Cargo.toml index a1a688a5..1486baa8 100644 --- a/crates/net/Cargo.toml +++ b/crates/net/Cargo.toml @@ -1,7 +1,7 @@ [package] -name = "reqwasm" -version = "0.4.1" -authors = ["Hamza "] +name = "gloo-net" +version = "0.1.0" +authors = ["Rust and WebAssembly Working Group", "Muhammad Hamza "] edition = "2018" license = "MIT OR Apache-2.0" repository = "https://github.com/hamza1311/reqwasm" @@ -9,9 +9,6 @@ description = "HTTP requests library for WASM Apps" readme = "README.md" keywords = ["requests", "http", "wasm", "websockets"] categories = ["wasm", "web-programming::http-client", "api-bindings"] -exclude = [ - ".idea", -] [package.metadata.docs.rs] all-features = true @@ -20,7 +17,7 @@ all-features = true wasm-bindgen = "0.2" web-sys = "0.3" js-sys = "0.3" -gloo-utils = "0.1.0" +gloo-utils = { version = "0.1", path = "../utils" } wasm-bindgen-futures = "0.4" futures-core = { version = "0.3", optional = true } diff --git a/crates/net/README.md b/crates/net/README.md index b6208956..f5921590 100644 --- a/crates/net/README.md +++ b/crates/net/README.md @@ -1,8 +1,23 @@ -# Reqwasm +
-[![crates.io](https://img.shields.io/crates/v/reqwasm.svg?style=flat)](https://crates.io/crates/reqwasm) -[![docs.rs](https://img.shields.io/docsrs/reqwasm)](https://docs.rs/reqwasm/) -![licence](https://img.shields.io/crates/l/reqwasm) +

gloo-net

+ +

+ Crates.io version + Download + docs.rs docs +

+ +

+ API Docs + | + Contributing + | + Chat +

+ +Built with 🦀🕸 by The Rust and WebAssembly Working Group +
HTTP requests library for WASM Apps. It provides idiomatic Rust bindings for the `web_sys` `fetch` and `WebSocket` API @@ -40,6 +55,3 @@ spawn_local(async move { console_log!("WebSocket Closed") }) ``` -## Contributions - -Your PRs and Issues are welcome. Note that all the contribution submitted by you, shall be licensed as MIT or APACHE 2.0 at your choice, without any additional terms or conditions. diff --git a/website/src/pages/__index.md b/website/src/pages/__index.md index 82803b72..ac17a3d9 100644 --- a/website/src/pages/__index.md +++ b/website/src/pages/__index.md @@ -9,11 +9,13 @@ as [API documentation](https://docs.rs/gloo/). 2. [`dialog`](https://crates.io/crates/gloo-dialog) 3. [`event`](https://crates.io/crates/gloo-event) 4. [`file`](https://crates.io/crates/gloo-file) -4. [`history`](https://crates.io/crates/gloo-history) -5. [`storage`](https://crates.io/crates/gloo-storage) -6. [`timer`](https://crates.io/crates/gloo-timer) -6. [`utils`](https://crates.io/crates/gloo-utils) -6. [`worker`](https://crates.io/crates/gloo-worker) +5. [`history`](https://crates.io/crates/gloo-history) +6. [`net`](https://crates.io/crates/gloo-net) +7. [`render`](https://crates.io/crates/gloo-render) +8. [`storage`](https://crates.io/crates/gloo-storage) +9. [`timer`](https://crates.io/crates/gloo-timer) +10. [`utils`](https://crates.io/crates/gloo-utils) +11. [`worker`](https://crates.io/crates/gloo-worker) ## Using Gloo From 5fb431eaee2cecc085fedab77b7978fff22c2b42 Mon Sep 17 00:00:00 2001 From: Muhammad Hamza Date: Tue, 15 Feb 2022 19:20:33 +0500 Subject: [PATCH 35/36] fix CI --- .github/workflows/tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 84a70a14..e64f5c04 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -32,7 +32,7 @@ jobs: uses: actions-rs/cargo@v1 with: command: test - args: --all + args: --workspace --exclude gloo-net browser_tests: name: Browser Tests @@ -64,7 +64,7 @@ jobs: run: | for x in $(ls crates); do # gloo-net is tested separately - if [[ "$example" == "gloo-net" ]]; then + if [[ "$x" == "net" ]]; then continue fi wasm-pack test --headless --firefox --chrome crates/$x --all-features From 54a66f27dae4e23070564cbad836152f67377777 Mon Sep 17 00:00:00 2001 From: Muhammad Hamza Date: Wed, 16 Feb 2022 00:54:21 +0500 Subject: [PATCH 36/36] re-export net from gloo --- Cargo.toml | 1 + src/lib.rs | 1 + 2 files changed, 2 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 9dd3a228..9c268ba0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ gloo-console = { version = "0.2.1", path = "crates/console" } gloo-utils = { version = "0.1.1", path = "crates/utils" } gloo-history = { version = "0.1.0", path = "crates/history" } gloo-worker = { version = "0.1.0", path = "crates/worker" } +gloo-net = { path = "crates/net" } [features] default = [] diff --git a/src/lib.rs b/src/lib.rs index 655f5c61..57a8382d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,6 +9,7 @@ pub use gloo_dialogs as dialogs; pub use gloo_events as events; pub use gloo_file as file; pub use gloo_history as history; +pub use gloo_net as net; pub use gloo_render as render; pub use gloo_storage as storage; pub use gloo_timers as timers;